Commit 7d069c61 authored by Maksym Tishchenko's avatar Maksym Tishchenko
Browse files

Merge branch '393-assign-doi-to-accessions' into 'main'

Resolve "Assign DOI to accessions"

Closes #393

See merge request grin-global/grin-global-ui!397
parents 85127cd7 6b12fc46
......@@ -113,6 +113,30 @@ tag edge gg-ce-web image:
- ${DOCKER_CMD} push ${REGISTRY_IMAGE}:edge
- ${DOCKER_CMD} logout $CI_REGISTRY
deploy branch to staging.ggce.genesys-pgr.org:
stage: deploy
<<: *docker_setup
variables:
GIT_STRATEGY: none
dependencies:
- dockerize gg-ce-web
rules:
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
when: manual
allow_failure: true
script:
# Address the swarm
- export DOCKER_HOST=swarm.genesys-pgr.org
# Actions
- export REGISTRY_IMAGE="${CI_REGISTRY_IMAGE}/${UI_EXPRESS_IMAGE_NAME}"
- echo "Deploying ${REGISTRY_IMAGE}:${IMAGE_TAG} to https://staging.ggce.genesys-pgr.org"
- ${DOCKER_CMD} service update --image ${REGISTRY_IMAGE}:${IMAGE_TAG} ggce-staging_web
environment:
name: GG-CE Staging
url: https://staging.ggce.genesys-pgr.org
deploy to staging.ggce.genesys-pgr.org:
stage: deploy
<<: *docker_setup
......
......@@ -71,6 +71,10 @@ const URL_CREATE_CITATION = '/api/v1/a/citation';
const URL_GET_CITATION = UrlTemplate.parse('/api/v1/a/citation/{id}');
const URL_REMOVE_CITATION = UrlTemplate.parse('/api/v1/a/citation/{id}');
const URL_DOWNLOAD_MCPD = '/api/v1/a/download-mcpd';
const URL_ASSIGN_DOI = '/api/v1/a/assign-doi';
const URL_ASSIGN_DOI1 = UrlTemplate.parse('/api/v1/a/{id}/assign-doi');
/**
* Accession service
*
......@@ -1178,6 +1182,55 @@ class AccessionService {
form.appendChild(input);
return form;
}
/**
* Update one Accession on GLIS DOI Registration Service at /api/v1/a/{id}/assign-doi
*
* @param accessionIds List of Accession IDs
* @param xhrConfig additional xhr config
*/
public updateGlisDoiForAccession = (accessionId: number, xhrConfig?: AxiosRequestConfig): Promise<DoiUpdate> => {
const apiUrl = URL_ASSIGN_DOI1.expand({ id: accessionId });
// console.log(`Fetching from ${apiUrl}`);
const content = { };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as DoiUpdate);
}
/**
* Update GLIS DOI Registration for multiple Accessions at /api/v1/a/assign-doi
*
* @param accessionIds List of Accession IDs
* @param xhrConfig additional xhr config
*/
public assignDoiToAccessions = (accessionIds: number[], xhrConfig?: AxiosRequestConfig): Promise<DoiUpdate[]> => {
const apiUrl = URL_ASSIGN_DOI;
// console.log(`Fetching from ${apiUrl}`);
const content = { data: accessionIds };
return this._axios.request({
...xhrConfig,
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as DoiUpdate[]);
}
}
export type DoiUpdate = {
accessionId: number;
accessionNumber: string;
doi: string;
error: string;
}
export class AcquisitionData {
......
type MultiOpError = { index: number, message: string, localizedMessage: string };
class MultiOp<T> {
success: T[];
errors: MultiOpError[];
}
export { MultiOp as default, MultiOpError };
......@@ -254,6 +254,7 @@ export interface ITableProps<T> {
onSortChange?: (sortBy: string, dir: SortDirection | null) => void;
tableConfig?: TableConfiguration<T>;
actions?: ACTIONS;
loading?: boolean
}
// CSS Utility
......@@ -413,9 +414,11 @@ class Table<T> extends React.Component<ITableProps<T> & WithTranslation & WithSt
}
public componentDidUpdate(prevProps: Readonly<ITableProps<T>>) {
const { data, total } = this.props;
if (prevProps.data && data && prevProps.data.length > data.length || prevProps.total > total) {
const { data, total, loading } = this.props;
if (prevProps.data && data && (prevProps.data.length > data.length || prevProps.total > total
|| (!isDeepEqual(prevProps.data, data) || prevProps.loading && !loading) && data.length === prevProps.data.length && total === prevProps.total)) {
this.setState({ selectedRows: [] });
this.props.onRowsToggled?.([])
}
return null;
}
......@@ -703,6 +706,7 @@ class Table<T> extends React.Component<ITableProps<T> & WithTranslation & WithSt
const { columnkey: columnKey, sortvalue: sortValue = null } = (e.currentTarget as HTMLElement).dataset;
const sortProperty = this.props.columnSettings[columnKey] && this.props.columnSettings[columnKey].sort || columnKey;
this.props.onSortChange(sortProperty, SortDirection[sortValue]);
this.props.onRowsToggled?.([])
};
private onCheckRow = (selected) => {
......
......@@ -248,7 +248,12 @@
"ipr": "Intellectual Property Rights/Restrictions",
"names": "Accession names",
"delete": "Do you really want to delete {{name}} accession?",
"deleteRecords": "Do you really want to delete {{count,number}} {{what, lowercase}}?"
"deleteRecords": "Do you really want to delete {{count,number}} {{what, lowercase}}?",
"submitToGLIS": "Submit to GLIS",
"getDOISuccess": "GLIS DOI Registration Service updated without any errors",
"getDOIError": "GLIS DOI Registration Service returned error",
"getDOIError_plural": "GLIS DOI Registration Service returned errors",
"failedAccessions": "Failed accessions"
},
"mcpd": {
"webVisible": "Please apply the filter for 'Web Visible' accessions.",
......@@ -349,6 +354,11 @@
"AccessionInventorySelector": {
"selectAccessions": "Select from one accession",
"selectAccessions_plural": "Select from {{count}} accessions"
},
"DOIResponseDialog": {
"title": "Response from GLIS DOI Registration Service",
"successAccessions": "{{count}} accessions were successfully uploaded to GLIS DOI Registration Service",
"failedAccessions": "GLIS reported errors with {{count}} accessions"
}
}
}
......
......@@ -46,7 +46,12 @@
"ipr": "Intellectual Property Rights/Restrictions",
"names": "Accession names",
"delete": "Do you really want to delete {{name}} accession?",
"deleteRecords": "Do you really want to delete {{count,number}} {{what, lowercase}}?"
"deleteRecords": "Do you really want to delete {{count,number}} {{what, lowercase}}?",
"submitToGLIS": "Submit to GLIS",
"getDOISuccess": "GLIS DOI Registration Service updated without any errors",
"getDOIError": "GLIS DOI Registration Service returned error",
"getDOIError_plural": "GLIS DOI Registration Service returned errors",
"failedAccessions": "Failed accessions"
},
"mcpd": {
"webVisible": "Please apply the filter for 'Web Visible' accessions.",
......@@ -147,6 +152,11 @@
"AccessionInventorySelector": {
"selectAccessions": "Select from one accession",
"selectAccessions_plural": "Select from {{count}} accessions"
},
"DOIResponseDialog": {
"title": "Response from GLIS DOI Registration Service",
"successAccessions": "{{count}} accessions were successfully uploaded to GLIS DOI Registration Service",
"failedAccessions": "GLIS reported errors with {{count}} accessions"
}
}
}
......
......@@ -12,9 +12,11 @@ import navigateTo from '@gringlobal-ce/client/action/navigation';
import { Accession, AccessionAction, Site } from '@gringlobal-ce/client/model/gringlobal';
// Ui
import ContentHeader from '@gringlobal-ce/client/ui/common/heading/ContentHeader';
import Table, { TextAlign, Renderers } from '@gringlobal-ce/client/ui/common/table/Table';
import Table, { Renderers, TextAlign } from '@gringlobal-ce/client/ui/common/table/Table';
import AddNewButton from '@gringlobal-ce/client/ui/common/button/AddNewButton';
import { CooperatorOwnedTableConfiguration as TableConfiguration } from '@gringlobal-ce/client/ui/common/table/TableConfiguration';
import {
CooperatorOwnedTableConfiguration as TableConfiguration
} from '@gringlobal-ce/client/ui/common/table/TableConfiguration';
import { PrintSpecies } from 'common/Taxonomy';
import Filters from 'accession/ui/c/Filters';
import withBrowsePageBase, { WithBrowsePage } from 'ui/common/withBrowsePageBase';
......@@ -28,6 +30,10 @@ 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';
import AdjustIcon from "@material-ui/icons/Adjust";
import { showSnackbar } from "@gringlobal-ce/client/action/snackbar";
import DOIErrorsDialog from "accession/ui/c/DOIErrorsDialog";
import { DoiUpdate } from "@gringlobal-ce/client/service/AccessionService";
import { Page } from "@gringlobal-ce/client/model/page";
export const AccessionTableDefaultConfig = {
......@@ -87,8 +93,17 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
public state = {
accessionDialogIsOpen: false,
accessionActionDialogIsOpen: false,
doiResponseDialogIsOpen: false,
doiResponse: null,
error: null,
selected: [],
} as {
accessionDialogIsOpen: boolean;
accessionActionDialogIsOpen: boolean;
doiResponseDialogIsOpen: boolean;
doiResponse: DoiUpdate[]
selected: Accession[];
error: unknown;
};
protected static needs = [
......@@ -140,6 +155,26 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
});
}
private getDOIForSelected = () => {
const { showSnackbar, receiveAccessionSuccessAction, data, listAction, t } = this.props;
const { selected } = this.state;
AccessionService.assignDoiToAccessions(selected.map(a => a.id)).then((res) => {
if (res.length > 0 && res.findIndex((doiRes) => !!doiRes.error) !== -1) {
this.setState({ doiResponse: res })
this.openDOIResponseDialog()
return
}
showSnackbar(t('accession.public.p.details.getDOISuccess'))
receiveAccessionSuccessAction(null as Accession)
listAction(data.filter, { ...Page.nextPage(data), page: 0 })
if (data.number > 0) {
this.setState({ selected: [] })
}
}).catch((e) => {
showSnackbar(e.data && e.data.error || e.toString())
});
}
private onRowsToggled = (selectedRows: number[]) => {
const { data } = this.props;
const selected = selectedRows.map((rowIndex) => data?.content[rowIndex])
......@@ -154,6 +189,14 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
this.setState({ accessionActionDialogIsOpen: false });
};
private openDOIResponseDialog = () => {
this.setState({ doiResponseDialogIsOpen: true, });
};
private closeDOIResponseDialog = () => {
this.setState({ doiResponseDialogIsOpen: false });
};
private onRefresh = () => {
const { data, listAction } = this.props;
listAction(data.filter, { ...Page.nextPage(data), page: 0 })
......@@ -163,8 +206,8 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
}
public render() {
const { data, t, onSortChange, applyFilter, loadMore } = this.props;
const { accessionDialogIsOpen, accessionActionDialogIsOpen, selected, error } = this.state;
const { data, loading, t, onSortChange, applyFilter, loadMore } = this.props;
const { accessionDialogIsOpen, accessionActionDialogIsOpen, selected, doiResponse, doiResponseDialogIsOpen, error } = this.state;
const columns = AccessionTableConfig.getColumns(data && data.content ? data.content[0] : null);
const actions = [
......@@ -173,6 +216,11 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
action: this.openAccessionActionDialog,
icon: <ScheduleOutlinedIcon/>,
},
{
title: 'accession.public.p.details.submitToGLIS',
action: this.getDOIForSelected,
icon: <AdjustIcon/>,
}
]
return (
......@@ -197,6 +245,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
onRowsToggled={ this.onRowsToggled }
sort={ data?.sort }
onSortChange={ onSortChange }
loading={ loading }
/>
<P.HasAccess action={ P.PassportData } permission={ P.create }>
{ selected.length === 0 &&
......@@ -229,6 +278,11 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
accessions={ selected }
tableConfig={ AccessionTableConfig }
/>
<DOIErrorsDialog
isOpen={ doiResponseDialogIsOpen }
onClose={ this.closeDOIResponseDialog }
response={ doiResponse }
/>
</>
);
}
......@@ -244,6 +298,7 @@ const mapDispatch = {
listAction: listAccessionsAction,
loadMoreData: loadMoreAccessionsAction,
receiveAccessionSuccessAction,
showSnackbar,
navigateTo,
}
......
......@@ -608,6 +608,23 @@ class AccessionDetailsPage extends React.Component<PropsFromRedux & WithBrowsePa
this.setState({ descObservationDialogIsOpen: false, createNew: true });
};
private getDOI = () => {
const { showSnackbar, getAccessionAction, getAccessionInventories, id, accessionCall: { data: accession } } = this.props;
if (! accession) {
return;
}
AccessionService.updateGlisDoiForAccession(accession.id).then((res) => {
if (res.error) {
showSnackbar(res.error)
return
}
getAccessionAction(id);
getAccessionInventories(id);
}).catch((e) => {
showSnackbar(e.data && e.data.error || e.toString())
});
}
private refreshData = () => {
const { getAccessionAction, id, getAccessionInventories } = this.props;
......@@ -702,6 +719,9 @@ class AccessionDetailsPage extends React.Component<PropsFromRedux & WithBrowsePa
<P.HasAccess action={ P.PassportData } permission={ P.delete } siteId={ accession.site.id }>
<Button onClick={ this.handleRemove } variant="text">{ t('common:action.delete') }</Button>
</P.HasAccess>
<Button variant="outlined" color="secondary" onClick={ this.getDOI }>
{ t('accession.public.p.details.submitToGLIS') }
</Button>
<Button variant="outlined" color="secondary" onClick={ () => this.props.showDialog(AccessionDetailsPage.PRINT_DIALOG_KEY) }>
{ t('common:action.generatePdf') }
</Button>
......
......@@ -145,7 +145,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
}
public render() {
const { t, onSortChange, applyFilter, loadMore, data } = this.props;
const { t, onSortChange, loading, applyFilter, loadMore, data } = this.props;
const { selected, descriptorDialogIsOpen, createNew, error } = this.state;
const columns = SourceDescObservationTableConfig.getColumns(data && data.content ? data.content[0] : null);
......@@ -187,6 +187,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
onRowsToggled={ this.rowsToggled }
sort={ data && data.sort }
onSortChange={ onSortChange }
loading={ loading }
/>
{ selected.length > 0 &&
<FABMenu
......
import * as React from 'react';
import { WithTranslation, withTranslation } from 'react-i18next';
import { compose } from 'redux';
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 { DoiUpdate } from "@gringlobal-ce/client/service/AccessionService";
const styles = (theme) => ({
sectionLabel: {
fontSize: "1.5rem"
}
});
interface IDOIResponseDialogProps extends React.ClassAttributes<any>, WithTranslation, WithStyles {
isOpen: boolean;
onClose: () => void;
response: DoiUpdate[];
}
class DOIResponseDialog extends React.Component <IDOIResponseDialogProps, any> {
public componentWillUnmount() {
this.props.onClose();
}
public render() {
const { isOpen, t, classes, onClose, response } = this.props;
const success = []
const failed = response?.filter((doiRes) => {
if (doiRes.error) {
return true
}
success.push(doiRes)
return false
})
return (
<div>
{ isOpen &&
<Dialog
open={ isOpen }
onClose={ onClose }
fullWidth
maxWidth="md"
disableEnforceFocus
>
<DialogTitle>{ t('accession.public.c.DOIResponseDialog.title', { count: failed.length }) }</DialogTitle>
<DialogContent>
<h2 className={ `mb-20 ${classes.sectionLabel}` }>{ t('accession.public.c.DOIResponseDialog.successAccessions', { count: success.length }) }</h2>
<h2 className={ classes.sectionLabel }>{ t('accession.public.c.DOIResponseDialog.failedAccessions', { count: failed.length }) }</h2>
{ failed.map(({ error }) => (
<div className="mb-10" key={ `doi-error-${error}` } style={ { color: 'red' } }>
{ error }
</div>
))}
</DialogContent>
<DialogActions>
<Button type="button" onClick={ onClose }>{ t('common:action.close') }</Button>
</DialogActions>
</Dialog>
}
</div>
);
}
}
export default compose(
withStyles(styles),
withTranslation(),
)(DOIResponseDialog);
......@@ -117,7 +117,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
}
public render() {
const { t, data, loadMore } = this.props;
const { t, data, loading, loadMore } = this.props;
const { appSettingDialogIsOpen, error, selected, createNew } = this.state;
const actions = [
......@@ -147,6 +147,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
tableConfig={ AppSettingsTableConfig }
total={ data?.totalElements }
loadMore={ loadMore }
loading={ loading }
/>
{ selected.length === 0 &&
<AddNewButton action={ this.openAppSettingDialog }/>
......
......@@ -84,7 +84,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
}
public render() {
const { data, t, onSortChange, showDialog, loadMore } = this.props;
const { data, loading, t, onSortChange, showDialog, loadMore } = this.props;
const { cropDialogIsOpen, selected } = this.state;
const columns = CropTableConfig.getColumns(data && data.content ? data.content[0] : null);
const actions = [
......@@ -110,6 +110,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
sort={ data && data.sort }
onSortChange={ onSortChange }
onRowsToggled={ this.onRowsToggled }
loading={ loading }
/>
{ selected.length > 0 &&
<FABMenu
......
......@@ -113,7 +113,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
}
public render() {
const { t, onSortChange, applyFilter, loadMore, data } = this.props;
const { t, loading, onSortChange, applyFilter, loadMore, data } = this.props;
const { selected, editGeoDialogIsOpen, createNew } = this.state;
const columns = GeographyTableConfig.getColumns(data && data.content ? data.content[0] : null);
......@@ -155,6 +155,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
onRowsToggled={ this.rowsToggled }
sort={ data && data.sort }
onSortChange={ onSortChange }
loading={ loading }
/>
{ selected.length > 0 &&
<FABMenu
......
......@@ -197,7 +197,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
}
public render() {
const { data, t, onSortChange, showDialog, applyFilter, loadMore } = this.props;
const { data, loading, t, onSortChange, showDialog, applyFilter, loadMore } = this.props;
const { inventoryDialogIsOpen, inventoryActionDialogIsOpen, error, selected } = this.state;
const columns = InventoryTableConfig.getColumns(data && data.content ? data.content[0] : null);
......@@ -236,6 +236,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
onRowsToggled={ this.onRowsToggled }
sort={ data && data.sort }
onSortChange={ onSortChange }
loading={ loading }
/>
<P.HasAccess action={ P.InventoryData } permission={ P.create }>
{ selected.length === 0 &&
......
......@@ -109,7 +109,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
}
public render() {
const { data, t, onSortChange, loadMore } = this.props;
const { data, loading, t, onSortChange, loadMore } = this.props;
const { inventoryPolicyDialogIsOpen } = this.state;
const columns = InventoryPolicyTableConfig.getColumns(data && data.content ? data.content[0] : null);
......@@ -135,6 +135,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
onRowsToggled={ this.rowsToggled }
sort={ data && data.sort }
onSortChange={ onSortChange }
loading={ loading }
/>
<AddNewButton
title="InventoryPolicy"
......
......@@ -111,7 +111,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
}
public render() {
const { t, onSortChange, applyFilter, loadMore, data } = this.props;
const { t, loading, onSortChange, applyFilter, loadMore, data } = this.props;
const { selected, methodDialogIsOpen, selectedMethodId, createNew } = this.state;
const columns = MethodTableConfig.getColumns(data && data.content ? data.content[0] : null);
const selectedMethod = selectedMethodId && data.content.find((method) => method.id === selectedMethodId)
......@@ -155,6 +155,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithTranslation & With
sort={ data && data.sort }
onSortChange={ onSortChange }
actions={{ selectMethod: this.selectMethod }}
loading={ loading }
/>
{ selected.length > 0 &&
<FABMenu
......
......@@ -89,7 +89,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
}
public render() {
const { data, t } = this.props;
const { data, loading, t } = this.props;
const columns = SecuredActionTableConfig.getColumns(data?.[0] ?? null);
const { dialogIsOpen, selected } = this.state;
......@@ -113,6 +113,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
tableConfig={ SecuredActionTableConfig }
total={ data?.length }
onRowsToggled={ this.rowsToggled }
loading={ loading }
/>
{ selected.length === 0 &&
<AddNewButton
......
......@@ -108,7 +108,7 @@ class BrowsePage extends React.Component<PropsFromRedux & WithBrowsePage & WithT
}
public render() {
const { data, t, onSortChange, loadMore, applyFilter } = this.props;