Commit a90c491f authored by Maksym Tishchenko's avatar Maksym Tishchenko
Browse files

Accession actions update

parent 85fdadc9
......@@ -589,7 +589,9 @@
"notBeforeDate": "Not Before Date",
"notBeforeDateCode": "Not Before Date Format",
"startedDateCode": "Start Date Format",
"completedDateCode": "Completed Date Format"
"completedDateCode": "Completed Date Format",
"method": "Method",
"cooperator": "Cooperator"
},
"AccessionPedigree": {
"releasedDate": "Released Date",
......
......@@ -32,7 +32,9 @@ class AccessionAction {
public static CodeGroup = {
actionNameCode: 'ACCESSION_ACTION',
notBeforeDateCode: 'DATE_FORMAT'
notBeforeDateCode: 'DATE_FORMAT',
startedDateCode: 'DATE_FORMAT',
completedDateCode: 'DATE_FORMAT',
}
// Order: nulls first, then newest
......
import Cooperator from '@gringlobal-ce/client/model/gringlobal/Cooperator';
import Method from '@gringlobal-ce/client/model/gringlobal/Method';
/**
* AccessionActionRequest
*
* GRIN-Global CE API
*/
class AccessionActionRequest {
public id: number[];
public actionNameCode: string;
public note: string;
public cooperator: Cooperator;
public webVisible: string;
public method: Method;
}
export default AccessionActionRequest;
......@@ -576,7 +576,9 @@
"notBeforeDate": "Not Before Date",
"notBeforeDateCode": "Not Before Date Format",
"startedDateCode": "Start Date Format",
"completedDateCode": "Completed Date Format"
"completedDateCode": "Completed Date Format",
"method": "Method",
"cooperator": "Cooperator"
},
"AccessionPedigree": {
"releasedDate": "Released Date",
......
......@@ -15,6 +15,7 @@ import AccessionPedigree from '@gringlobal-ce/client/model/gringlobal/AccessionP
import AccessionQuarantine from '@gringlobal-ce/client/model/gringlobal/AccessionQuarantine';
import Citation from '@gringlobal-ce/client/model/gringlobal/Citation';
import AccessionInvGroup from '@gringlobal-ce/client/model/gringlobal/AccessionInvGroup';
import AccessionActionRequest from '@gringlobal-ce/client/model/gringlobal/AccessionActionRequest';
const URL_GET_DETAILS = UrlTemplate.parse('/api/v1/a/details/{id}');
const URL_ACCESSION_AUDIT_LOGS = UrlTemplate.parse('/api/v1/a/auditlog/{id}');
......@@ -23,6 +24,11 @@ const URL_REOPEN_ACTIONS = '/api/v1/a/action/reopen';
const URL_LIST_ACTIONS = '/api/v1/a/action/list';
const URL_ACCESSION_OVERVIEW = UrlTemplate.parse('/api/v1/a/overview/{groupBy}');
const URL_START_ACTIONS = '/api/v1/a/action/start';
const URL_UPDATE_ACTION = '/api/v1/a/action';
const URL_CREATE_ACTION = '/api/v1/a/action';
const URL_SCHEDULE_ACTIONS = '/api/v1/a/action/schedule';
const URL_GET_ACTION = UrlTemplate.parse(`/api/v1/a/action/{id}`);
const URL_REMOVE_ACTION = UrlTemplate.parse(`/api/v1/a/action/{id}`);
const URL_UPDATE = '/api/v1/a';
const URL_CREATE = '/api/v1/a';
const URL_LIST = '/api/v1/a/list';
......@@ -231,6 +237,109 @@ class AccessionService {
}).then(({ data }) => data as AccessionAction);
}
/**
* updateAction at /api/v1/a/action
*
* @param data Request body
* @param xhrConfig additional xhr config
*/
public updateAction = (data: AccessionAction, xhrConfig?: AxiosRequestConfig): Promise<AccessionAction> => {
const apiUrl = URL_UPDATE_ACTION;
// console.log(`Fetching from ${apiUrl}`);
const content = { data };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'PUT',
...content,
}).then(({ data }) => data as AccessionAction);
}
/**
* createAction at /api/v1/a/action
*
* @param data Request body
* @param xhrConfig additional xhr config
*/
public createAction = (data: AccessionAction, xhrConfig?: AxiosRequestConfig): Promise<AccessionAction> => {
const apiUrl = URL_CREATE_ACTION;
// console.log(`Fetching from ${apiUrl}`);
const content = { data };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as AccessionAction);
}
/**
* scheduleActions at /api/v1/a/action/schedule
*
* @param data Request body
* @param xhrConfig additional xhr config
*/
public scheduleActions = (data: AccessionActionRequest, xhrConfig?: AxiosRequestConfig): Promise<AccessionAction[]> => {
const apiUrl = URL_SCHEDULE_ACTIONS;
// console.log(`Fetching from ${apiUrl}`);
const content = { data };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as AccessionAction[]);
}
/**
* getAction at /api/v1/a/action/{id}
*
* @param id undefined
* @param xhrConfig additional xhr config
*/
public getAction = (id: number, xhrConfig?: AxiosRequestConfig): Promise<AccessionAction> => {
const apiUrl = URL_GET_ACTION.expand({ id });
// 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 AccessionAction);
}
/**
* removeAction at /api/v1/a/action/{id}
*
* @param id undefined
* @param xhrConfig additional xhr config
*/
public removeAction = (id: number, xhrConfig?: AxiosRequestConfig): Promise<AccessionAction> => {
const apiUrl = URL_REMOVE_ACTION.expand({ id });
// console.log(`Fetching from ${apiUrl}`);
const content = { /* No content in request body */ };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'DELETE',
...content,
}).then(({ data }) => data as AccessionAction);
}
/**
* update at /api/v1/a
*
......@@ -298,7 +407,7 @@ class AccessionService {
}).then(({ data }) => data as AccessionFilteredPage);
}
/**
* listMcpd at /api/v1/a/mcpd
*
......
......@@ -186,7 +186,8 @@
"browse": {
"title": "One accession",
"title_plural": "{{count,number}} accessions",
"loading": "Loading accession data"
"loading": "Loading accession data",
"scheduleAccessionAction": "Schedule accession action"
},
"public": {
"p": {
......
......@@ -8,7 +8,8 @@
"browse": {
"title": "One accession",
"title_plural": "{{count,number}} accessions",
"loading": "Loading accession data"
"loading": "Loading accession data",
"scheduleAccessionAction": "Schedule accession action"
},
"public": {
"p": {
......
......@@ -9,7 +9,7 @@ import { AccessionService, UISecurity as P } from '@gringlobal-ce/client/service
import { listAccessionsAction, loadMoreAccessionsAction, receiveAccessionSuccessAction } from 'accession/action/public';
import navigateTo from '@gringlobal-ce/client/action/navigation';
// Model
import { Accession, AccessionFilter, AccessionFilteredPage, Site } from '@gringlobal-ce/client/model/gringlobal';
import { Accession, AccessionAction, AccessionFilter, AccessionFilteredPage, Site } from '@gringlobal-ce/client/model/gringlobal';
import { SortDirection } from '@gringlobal-ce/client/model/page';
// Ui
import ContentHeader from '@gringlobal-ce/client/ui/common/heading/ContentHeader';
......@@ -26,6 +26,9 @@ import PrettyDate from '@gringlobal-ce/client/ui/common/time/PrettyDate';
import FiltersButton from 'accession/ui/c/FiltersButton';
import TaxonomySpecies from '@gringlobal-ce/client/src/model/gringlobal/TaxonomySpecies';
import AccessionForm from 'accession/ui/c/AccessionForm';
import ScheduleOutlinedIcon from '@material-ui/icons/ScheduleOutlined';
import FABMenu from '@gringlobal-ce/client/ui/common/button/FABMenu';
import AccessionActionDialog from 'accession/ui/c/AccessionActionDialog';
interface IBrowsePageProps extends React.ClassAttributes<any>, WithTranslation, WithBrowsePageBase {
onSortChange: (sortBy: string, dir: SortDirection) => void;
......@@ -81,7 +84,9 @@ const AccessionTableConfig = new TableConfiguration(AccessionTableDefaultConfig)
class BrowsePage extends React.Component<IBrowsePageProps> {
public state = {
accessionDialogIsOpen: false,
accessionActionDialogIsOpen: false,
error: null,
selected: [],
};
protected static needs = [
......@@ -116,11 +121,50 @@ class BrowsePage extends React.Component<IBrowsePageProps> {
return this.setState({ error: null });
}
private handleScheduleAction = (actionData: AccessionAction) => {
const { selected }: { selected: Accession[] } = this.state;
this.setState({ error: null });
const { id: ignoreId, isWebVisible: webVisible, ...usableActionData } = actionData;
AccessionService.scheduleActions({
id: selected?.map((a) => a.id),
webVisible,
...usableActionData,
}).then(() => {
this.closeAccessionActionDialog();
this.setState({ selected: [] });
}).catch((e) => {
this.setState({ error: e.data && e.data.error || e.toString() });
});
}
private onRowsToggled = (selectedRows: number[]) => {
const { data } = this.props;
const selected = selectedRows.map((rowIndex) => data?.content[rowIndex])
this.setState({ selected });
}
private openAccessionActionDialog = () => {
this.setState({ accessionActionDialogIsOpen: true });
};
private closeAccessionActionDialog = () => {
this.setState({ accessionActionDialogIsOpen: false });
};
public render() {
const { data, t, onSortChange, applyFilter, loadMore } = this.props;
const { accessionDialogIsOpen, error } = this.state;
const { accessionDialogIsOpen, accessionActionDialogIsOpen, selected, error } = this.state;
const columns = AccessionTableConfig.getColumns(data && data.content ? data.content[0] : null);
const actions = [
{
title: 'accession.browse.scheduleAccessionAction',
action: this.openAccessionActionDialog,
icon: <ScheduleOutlinedIcon/>,
},
]
return (
<>
<PageTitle title={ data ? t('accession.browse.title', { count: data?.totalElements }) : t('accession.browse.loading') }/>
......@@ -140,11 +184,14 @@ class BrowsePage extends React.Component<IBrowsePageProps> {
tableConfig={ AccessionTableConfig }
total={ data?.content && data.totalElements }
loadMore={ loadMore }
onRowsToggled={ this.onRowsToggled }
sort={ data?.sort }
onSortChange={ onSortChange }
/>
<P.HasAccess action={ P.PassportData } permission={ P.create }>
<AddNewButton action={ this.openAccessionDialog }/>
{ selected.length === 0 &&
<AddNewButton action={ this.openAccessionDialog }/>
}
<AccessionForm
onSubmit={ this.handleSubmit }
isOpen={ accessionDialogIsOpen }
......@@ -157,6 +204,22 @@ class BrowsePage extends React.Component<IBrowsePageProps> {
sectioned
/>
</P.HasAccess>
{ selected.length > 0 &&
<FABMenu
title="common:label.openActionList"
actions={ actions }
/>
}
<AccessionActionDialog
isOpen={ accessionActionDialogIsOpen }
onClose={ this.closeAccessionActionDialog }
onSubmit={ this.handleScheduleAction }
error={ error }
resetError={ this.resetError }
accessions={ selected }
tableConfig={ AccessionTableConfig }
initialValues={ { isWebVisible: 'N' } }
/>
</>
);
}
......
......@@ -88,6 +88,12 @@ import { InventoryGroupTableDefaultConfig } from 'inventorygroup/ui/GroupBrowseP
import { CooperatorOwnedTableConfiguration as TableConfiguration } from '@gringlobal-ce/client/ui/common/table/TableConfiguration';
import AccessionSectionTable from './c/AccessionSectionTable';
import { ExtraRenderers } from 'common/ExtraRenderers';
import AddNewButton from '@gringlobal-ce/client/ui/common/button/AddNewButton';
import FABMenu from '@gringlobal-ce/client/ui/common/button/FABMenu';
import AccessionActionDialog from 'accession/ui/c/AccessionActionDialog';
import { AccessionTableDefaultConfig } from 'accession/ui/AccessionBrowsePage';
import CancelOutlinedIcon from '@material-ui/icons/CancelOutlined';
import EditIcon from '@material-ui/icons/Edit';
interface IDetailsPageProps extends React.ClassAttributes<any>, WithTranslation, IWithTabs {
id: string;
......@@ -122,6 +128,11 @@ interface IDetailsPageState {
auditLogs: Page<AuditLog>;
error: string;
accessionDialogIsOpen: boolean;
actionDialogIsOpen: boolean;
actions: AccessionAction[];
selected: number[];
selectedAction: AccessionAction;
createNew: boolean;
}
enum AccessionDetailsTabs {
......@@ -134,6 +145,7 @@ enum AccessionDetailsTabs {
}
const InventoryGroupTableConfig = new TableConfiguration(InventoryGroupTableDefaultConfig);
const AccessionTableConfig = new TableConfiguration(AccessionTableDefaultConfig);
class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPageState> {
......@@ -148,6 +160,11 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
auditLogs: null,
error: null,
accessionDialogIsOpen: false,
actionDialogIsOpen: false,
actions: [],
selected: [],
selectedAction: null,
createNew: true,
};
public constructor(props) {
......@@ -162,19 +179,28 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
}
}
public componentDidUpdate(prevProps: IDetailsPageProps) {
public componentDidUpdate(prevProps: IDetailsPageProps, prevState) {
const { accessionCall, id, getAccessionAction, getAccessionInventories, currentTab } = this.props;
const { actions, selected } = this.state;
if (!accessionCall || !accessionCall.loading && (!accessionCall.error && id && +id !== accessionCall.data.id)) {
getAccessionAction(id);
getAccessionInventories(id);
}
if (prevState.actions.length === 0 && actions.length === 0 && accessionCall?.data?.actions.length > 0) {
this.setState({ actions: accessionCall.data.actions })
}
if (currentTab === AccessionDetailsTabs.AUDITLOGS && currentTab !== prevProps.currentTab) {
if (!this.state.auditLogs) {
AccessionService.accessionAuditLogs(+this.props.id)
.then((auditLogs) => this.setState({ auditLogs }));
}
}
if (prevState.selected.length !== 1 && selected.length === 1) {
this.getSelectedAction(selected[0]);
}
}
private handleUploading = (files: File[]) => {
......@@ -220,6 +246,82 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
return el;
}
private handleCreateAction = (actionData: AccessionAction) => {
const { accessionCall } = this.props;
const { actions } = this.state;
const { data: accession } = accessionCall
this.setState({ error: null });
actionData.accession = { id: accession.id } as Accession;
AccessionService.createAction(actionData).then((action) => {
const updatedActions = [action, ...actions];
this.closeActionDialog();
this.setState({ selected: [], actions: updatedActions });
}).catch((e) => {
this.setState({ error: e.data && e.data.error || e.toString() });
});
}
private handleRemoveAction = () => {
const { t, showSnackbar } = this.props;
const { selected, actions } = this.state;
confirm(t('common:label.deleteListConfirm', { what: t('accession.public.p.details.actions'), count: selected.length }), {
confirmLabel: t('common:label.yes'),
abortLabel: t('common:label.no'),
}).then(async () => {
for (let i = 0; i < selected.length; i++) {
await AccessionService.removeAction(selected[i]);
}
const updatedActions = actions.filter((action) => !selected.includes(action.id))
console.log("updatedActions", updatedActions);
this.setState({ selected: [], actions: updatedActions });
console.log("actions", this.state.actions);
}).catch((e) => {
showSnackbar(e.data && e.data.error || e.toString());
});
}
private handleUpdateAction = (actionData: AccessionAction) => {
const { accessionCall } = this.props;
const { actions } = this.state;
const { data: accession } = accessionCall
this.setState({ error: null });
actionData.accession = { id: accession.id } as Accession;
AccessionService.updateAction(actionData).then((response) => {
const updatedActionIndex = accession.actions.findIndex((action) => +action.id === +response.id)
const updatedActions = actions;
updatedActions[updatedActionIndex] = response;
this.closeActionDialog();
this.setState({ selected: [], actions: updatedActions });
}).catch((e) => {
this.setState({ error: e.data && e.data.error || e.toString() });
});
}
private getSelectedAction = (id) => {
AccessionService.getAction(id).then((response) => {
this.setState({ selectedAction: response });
}).catch((e) => {
this.props.showSnackbar(e.data && e.data.error || e.toString());
});
}
private openActionDialog = (createNew = true) => {
this.setState({ actionDialogIsOpen: true, createNew });
};
private closeActionDialog = () => {
this.setState({ actionDialogIsOpen: false, createNew: true });
};
private actionsSelected = (selectedRows: number[]) => {
const { actions } = this.state;
const selectedIds = selectedRows.map((rowIndex) => actions[rowIndex].id)
this.setState({ selected: selectedIds });
}
private handleInvNameSubmit = (name: AccessionInvName, edit: boolean) => {
const { receiveAccessionInvNameSuccessAction, inventories, t } = this.props;
let accessionInvNamePromise: Promise<AccessionInvName>;
......@@ -394,7 +496,7 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
removeCitationAction, removeAccessionQuarantineAction,
removeAccessionAttachmentAction, removeAccessionAttachmentsAction
} = this.props;
const { auditLogs, error, accessionDialogIsOpen } = this.state;
const { auditLogs, error, accessionDialogIsOpen, actionDialogIsOpen, actions, selectedAction, selected, createNew } = this.state;
// const columns = InventoryTableConfig.getColumns(inventories && inventories.data && inventories.data.content ? inventories.data.content[0] : null);
if (!accessionCall) {
return null;
......@@ -404,6 +506,20 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
const systemInventory = inventories?.data?.content?.find((inv) => inv.formTypeCode === '**');
const groupsColumns = InventoryGroupTableConfig.getColumns(accession && accession.groups && accession.groups.length ? accession.groups[0] : null );
const fabActions = [
{
title: 'common:action.remove',
action: this.handleRemoveAction,
icon: <CancelOutlinedIcon/>,
},
{
title: 'common:action.edit',
action: () => this.openActionDialog(false),
icon: <EditIcon/>,
disabled: selected.length !== 1
},
]
return (
<>
<PageTitle title={ accession ? accession.accessionNumber : t('accession.public.p.details.title') }/>
......@@ -777,8 +893,32 @@ class AccessionDetailsPage extends React.Component<IDetailsPageProps, IDetailsPa
<TabPanel value={ currentTab } index={ AccessionDetailsTabs.ACTIONS }>
<BasicAccessionActionsTable
actions={ accession.actions && accession.actions.sort(AccessionAction.completedDateSort) }
onRowsToggled={ this.actionsSelected }
actions={ actions.sort(AccessionAction.completedDateSort) }
/>
{ selected.length === 0 &&
<AddNewButton action={ this.openActionDialog }/>
}
<AccessionActionDialog
isOpen={ actionDialogIsOpen }
onClose={ this.closeActionDialog }
onSubmit={ ! createNew && selectedAction ? this.handleUpdateAction : this.handleCreateAction }
initialValues={ ! createNew ? selectedAction : { isWebVisible: 'N' } }
error={ error }
resetError={ this.resetError }
title={ ! createNew && selectedAction
? <CodeValueDisplay codeGroup={ AccessionAction.CodeGroup.actionNameCode } value={ selectedAction.actionNameCode } />
: t('common:action.add', { what: t('client:model.name.AccessionAction') })
}
size="md"
tableConfig={ AccessionTableConfig }
/>
{ selected.length > 0 &&
<FABMenu
title="common:label.openActionList"
actions={ fabActions }
/>
}
</TabPanel>
<TabPanel value={ currentTab } index={ AccessionDetailsTabs.AUDITLOGS }>
......
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { compose } from 'redux';
// model
import { Accession } from '@gringlobal-ce/client/model/gringlobal';
import Dialog from '@material-ui/core/Dialog';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import { WithStyles, withStyles } from '@material-ui/core/styles';
import { Button, DialogActions } from '@material-ui/core';
import Table from '@gringlobal-ce/client/ui/common/table/Table';
import AccessionActionForm from 'accession/ui/c/AccessionActionForm';
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';
import { CooperatorOwnedTableConfiguration } from '@gringlobal-ce/client/ui/common/table/TableConfiguration';
const styles = (theme) => ({
dialogContent: {
height: '100%',
},
form: {
height: '50%',
width: '100%',
},
dialogActions: {