Commit 68faaa8d authored by Viacheslav Pavlov's avatar Viacheslav Pavlov Committed by Matija Obreza

Dashboard: Subsets

- All stepper and stepper page logic moved to abstract super-class
- Moved all related to dashboard files to ui/dashboard, changed publish API use, improved UI, new usage of Subset.AccessionId

Replaced multiple accession get requests by one request with filter

resolved merge conflicts
parent 70723d8d
......@@ -6,7 +6,7 @@ import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import Accession from 'model/Accession';
import AccessionFilter from 'model/AccessionFilter';
import {list as listAccessions, getByUuid, getByDoi, listOverview as listAccessionOverview} from 'actions/genesys/accessionService';
import {list as listAccessions, getByUuid, getByDoi, listOverview as listAccessionOverview, toUUID} from 'actions/genesys/accessionService';
import {RECEIVE_ACCESSIONS, RECEIVE_ACCESSION, RECEIVE_ACCESSION_OVERVIEW, APPEND_ACCESSIONS} from 'constants/accessions';
const receiveAccessions = (paged: FilteredPage<Accession>, error = null) => ({
......@@ -29,7 +29,7 @@ const receiveAccession = (accession: Accession, error = null) => ({
payload: { accession, error },
});
export { listAccessions as listAccessionsPromise };
export { listAccessions as listAccessionsPromise, toUUID as toUUIDPromise };
export const updateRoute = (paged: FilteredPage<Accession>, path: string = '/a') => (dispatch) => {
const qs = {
......
......@@ -5,6 +5,7 @@ import AccessionService from 'service/genesys/AccessionService';
import Accession from 'model/Accession';
import AccessionFilter from 'model/AccessionFilter';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import {AccessionIdentifier} from 'model/dataset.model';
......@@ -28,6 +29,11 @@ export const getByUuid = (UUID: string) => (dispatch, getState): Promise<Accessi
};
export const toUUID = (identifiers: AccessionIdentifier[]) => (dispatch, getState): Promise<Map<string, AccessionIdentifier>> => {
const authorization = getState().login.access_token;
return AccessionService.toUUID(authorization, identifiers);
};
/**
* Action list
*
......
import SubsetService from 'service/genesys/SubsetService';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import Subset from 'model/Subset';
import SubsetFilter from 'model/SubsetFilter';
import {PublishState} from 'model/common.model';
/**
......@@ -85,3 +84,14 @@ export const remove = (UUID: string, version: number) => (dispatch, getState): P
const authorization = getState().login.access_token;
return SubsetService.remove(authorization, UUID, version);
};
/**
* Action to update PublishState
*
* @param subset subset to update
* @param newState new PublishState
*/
export const updatePublishState = (subset: Subset, newState: PublishState) => (dispatch, getState): Promise<Subset> => {
const authorization = getState().login.access_token;
return SubsetService.updatePublishState(authorization, subset, newState);
};
/**
* This is a top-level group for actions in /subsets/* routes.
*/
import navigateTo from 'actions/navigation';
import {list as listSubsets, get, update, addAccessions, remove, create, updatePublishState} from 'actions/genesys/subsetService';
import {toUUIDPromise} from 'actions/accessions';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import Subset from 'model/Subset';
import SubsetFilter from 'model/SubsetFilter';
import {AccessionIdentifier} from 'model/dataset.model';
import {PublishState} from 'model/common.model';
import { list as listSubsets, get } from 'actions/genesys/subsetService';
import {RECEIVE_SUBSETS, RECEIVE_SUBSET, APPEND_SUBSETS} from 'constants/subsets';
import steps from 'ui/pages/dashboard/subsets/subset-stepper/steps';
import {RECEIVE_SUBSETS, RECEIVE_SUBSET, APPEND_SUBSETS, REMOVE_SUBSET} from 'constants/subsets';
const receiveSubsets = (paged: FilteredPage<Subset>, error = null) => ({
type: RECEIVE_SUBSETS,
......@@ -26,6 +32,13 @@ const receiveSubset = (subset: Subset, error = null) => ({
export { listSubsets as listSubsetsPromise };
const removeSubset = (subset) => (dispatch) => {
dispatch({
type: REMOVE_SUBSET,
payload: { subset },
});
};
export const updateRoute = (paged: FilteredPage<Subset>) => (dispatch) => {
const qs = {
s: paged.sort[0].property === Subset.DEFAULT_SORT.property ? undefined : paged.sort[0].property,
......@@ -62,6 +75,16 @@ export const loadSubsetsPage = (page: IPageRequest) => (dispatch, getState) => {
});
};
export const listSubsetDashboardPage = (page: IPageRequest) => (dispatch, getState) => {
return dispatch(listSubsets('', page))
.then((paged) => {
dispatch(receiveSubsets(paged));
}).catch((error) => {
console.log(`API error`, error);
dispatch(receiveSubsets(null, error.response));
});
};
export const loadSubset = (uuid: string) => (dispatch) => {
return dispatch(get(uuid))
.then((subset) => {
......@@ -71,3 +94,69 @@ export const loadSubset = (uuid: string) => (dispatch) => {
dispatch(receiveSubset(null, error.response));
});
};
export const createSubset = () => (dispatch) => {
dispatch(receiveSubset(new Subset()));
};
export const saveSubset = (subset: Subset) => (dispatch) => {
if (! subset.uuid) {
subset.publisher = subset.wiewsCode;
return dispatch(create(subset))
.then((subset) => {
dispatch(receiveSubset(subset));
dispatch(gotoNextStep(subset));
});
}
return dispatch(update(subset))
.then((subset) => dispatch(receiveSubset(subset)));
};
export const updateSubsetAccessionIdentifiers = (subset: Subset, accessionIdentifiers: AccessionIdentifier[]) => (dispatch) => {
dispatch(toUUIDPromise(accessionIdentifiers))
.then((accessionIdentifiers) => {
dispatch(addAccessions(subset.uuid, subset.version, Object.keys(accessionIdentifiers)))
.then((subset) => {
dispatch(receiveSubset(subset));
});
});
};
export const publishSubset = (subset: Subset) => (dispatch) => {
return dispatch(updatePublishState(subset, PublishState.REVIEWING))
.then((subset) => {
dispatch(receiveSubset(subset));
});
};
export const unpublishSubset = (subset: Subset) => (dispatch) => {
return dispatch(updatePublishState(subset, PublishState.DRAFT))
.then((subset) => {
dispatch(receiveSubset(subset));
});
};
export const approveSubset = (subset: Subset) => (dispatch) => {
return dispatch(updatePublishState(subset, PublishState.PUBLISHED))
.then((subset) => {
dispatch(receiveSubset(subset));
});
};
export const deleteSubset = (subset: Subset) => (dispatch) => {
return dispatch(remove(subset.uuid, subset.version))
.then((subset) => {
dispatch(removeSubset(subset));
});
};
const gotoNextStep = (subset: Subset) => {
return (dispatch, getState) => {
const link = window.location.pathname.split('/').pop();
const stepId = steps.find((e) => e.link.endsWith(link)).id;
const path = steps.find((e) => e.id === (stepId + 1)).link;
dispatch(navigateTo(`/subsets/${subset.uuid}/${path}`));
};
};
export const remoteSubmit = (values: Subset, dispatch) => {
dispatch(saveSubset(values));
};
export const RECEIVE_SUBSETS = 'subsets/RECEIVE_SUBSETS';
export const APPEND_SUBSETS = 'subsets/APPEND_SUBSETS';
export const RECEIVE_SUBSET = 'subsets/RECEIVE_SUBSET';
export const REMOVE_SUBSET = 'subsets/REMOVE_SUBSET';
export const SUBSET_FILTERFORM = 'Form/Subset/SUBSET_FILTERFORM';
export const SUBSET_FORM = 'Form/Subset/SUBSET_FORM';
......@@ -2,9 +2,11 @@
/*
* Defined in OpenAPI as '#/definitions/Subset'
*/
import {PublishState} from 'model/common.model';
class Subset {
public accessionCount: number;
public accessionIds: any[];
public accessionIds: string[];
public active: boolean;
public createdBy: number;
public createdDate: Date;
......@@ -13,7 +15,7 @@ class Subset {
public id: number;
public lastModifiedBy: number;
public lastModifiedDate: Date;
public published: boolean;
public state: PublishState;
public publisher: string;
public rights: string;
public title: string;
......@@ -33,4 +35,5 @@ class Subset {
};
}
export default Subset;
......@@ -100,3 +100,9 @@ export abstract class UuidModel extends AuditedVersionedModel {
super(obj);
}
}
export enum PublishState {
DRAFT = 'DRAFT',
PUBLISHED = 'PUBLISHED',
REVIEWING = 'REVIEWING',
}
export class License {
public code: string;
public title: string;
public url: string;
constructor(code: string, title: string, url: string) {
this.code = code;
this.title = title;
this.url = url;
}
}
export const AVAILABLE_LICENSES = [
new License('CC0', 'Public Domain Dedication', 'https://creativecommons.org/publicdomain/zero/1.0/'),
new License('CC BY 4.0', 'Attribution 4.0 International', 'https://creativecommons.org/licenses/by/4.0/'),
new License('CC BY-SA 4.0', 'Attribution-ShareAlike 4.0 International', 'https://creativecommons.org/licenses/by-sa/4.0/'),
new License('CC BY-NC 4.0', 'Attribution-NonCommercial 4.0 International', 'https://creativecommons.org/licenses/by-nc/4.0/'),
new License('CC BY-NC-ND 4.0', 'Attribution-NonCommercial-NoDerivatives 4.0 International', 'https://creativecommons.org/licenses/by-nc-nd/4.0/'),
];
import update from 'immutability-helper';
import { IReducerAction } from 'model/common.model';
import {RECEIVE_SUBSETS, RECEIVE_SUBSET, APPEND_SUBSETS} from 'constants/subsets';
import { RECEIVE_SUBSETS, RECEIVE_SUBSET, REMOVE_SUBSET, APPEND_SUBSETS } from 'constants/subsets';
import FilteredPage from 'model/FilteredPage';
import Subset from 'model/Subset';
import * as _ from 'lodash';
const INITIAL_STATE: {
subset: Subset;
subsetError: any;
......@@ -69,6 +71,17 @@ function subsets(state = INITIAL_STATE, action: IReducerAction) {
pagedError: {$set: error},
});
}
case REMOVE_SUBSET: {
const removeIndex = state.paged ? _.findIndex(state.paged.content, (item) => item.uuid === action.payload.subset.uuid) : -1;
return removeIndex === -1 ? state
: update(state, {
paged: {
content: {$splice: [[removeIndex, 1]]},
numberOfElements: {$set: state.paged.numberOfElements - 1},
totalElements: {$set: state.paged.totalElements - 1},
},
});
}
default:
return state;
......
......@@ -7,9 +7,11 @@ import { API_ROOT } from 'constants/apiURLS';
import Accession from 'model/Accession';
import AccessionFilter from 'model/AccessionFilter';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import {AccessionIdentifier} from 'model/dataset.model';
const URL_GET_BY_DOI = `${API_ROOT}/api/v1/acn/{doi}`; // UrlTemplate doesn't like the / in DOI
const URL_GET_BY_UUID = UrlTemplate.parse(`${API_ROOT}/api/v1/acn/{UUID}`);
const URL_TO_UUID = `${API_ROOT}/api/v1/acn/toUUID`;
const URL_LIST = `${API_ROOT}/api/v1/acn/list`;
const URL_LIST_OVERVIEW = `${API_ROOT}/api/v1/acn/overview`;
......@@ -56,6 +58,17 @@ class AccessionService {
}).then(({ data }) => data as Accession);
}
public static toUUID(authToken: string, identifiers: AccessionIdentifier[]) {
const apiUrl = URL_TO_UUID;
const content = { data: identifiers };
return authenticatedRequest(authToken, {
url: apiUrl,
method: 'POST',
...content,
}).then(({ data }) => data as Map<string, AccessionIdentifier>);
}
/**
* list at /api/v1/acn/list
*
......
......@@ -7,12 +7,16 @@ import { API_ROOT } from 'constants/apiURLS';
import FilteredPage, { IPageRequest } from 'model/FilteredPage';
import Subset from 'model/Subset';
import SubsetFilter from 'model/SubsetFilter';
import {PublishState} from 'model/common.model';
const URL_ADD_ACCESSIONS = UrlTemplate.parse(`${API_ROOT}/api/v1/subset/add-accessions/{UUID},{version}`);
const URL_CREATE = `${API_ROOT}/api/v1/subset/create`;
const URL_LIST = `${API_ROOT}/api/v1/subset/list`;
const URL_REMOVE_ACCESSIONS = UrlTemplate.parse(`${API_ROOT}/api/v1/subset/remove-accessions/{UUID},{version}`);
const URL_UPDATE = `${API_ROOT}/api/v1/subset/update`;
const URL_REJECT = `${API_ROOT}/api/v1/subset/reject`;
const URL_FOR_REVIEW = `${API_ROOT}/api/v1/subset/for-review`;
const URL_APPROVE = `${API_ROOT}/api/v1/subset/approve`;
const URL_GET = UrlTemplate.parse(`${API_ROOT}/api/v1/subset/{UUID}`);
const URL_DELETE = UrlTemplate.parse(`${API_ROOT}/api/v1/subset/{UUID},{version}`);
......@@ -172,6 +176,30 @@ class SubsetService {
}
public static updatePublishState(token: string, subset: Subset, newState: PublishState): Promise<Subset> {
let url: string;
switch (newState) {
case PublishState.PUBLISHED: {
url = URL_APPROVE;
break;
}
case PublishState.REVIEWING: {
url = URL_FOR_REVIEW;
break;
}
default: {
url = URL_REJECT;
break;
}
}
return authenticatedRequest(token, {
url: `${url}?uuid=${subset.uuid}&version=${subset.version}`,
method: 'POST',
}).then(({ data }) => data as Subset);
}
}
export default SubsetService;
import * as React from 'react';
import Grid from '@material-ui/core/Grid';
import StepNavigation from 'ui/common/stepper/StepNavigation';
import ProgressMenu from 'ui/common/stepper/progress-menu';
import Loading from 'ui/common/Loading';
function NavigationWrapper({children, location, stillLoading, steps, disabled, disabledNext, gotoStep, onDelete, onPublish}) {
return (
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 9 } xl={ 10 } className="back-gray p-20">
<Grid container spacing={ 0 } className="back-white">
<StepNavigation disabled={ disabled } disabledNext={ disabledNext } onGotoStep={ gotoStep } onDelete={ onDelete } steps={ steps } location={ location } showStepName bottomDivider onPublish={ onPublish } />
<Grid item xs={ 12 }>
{ stillLoading ? <Loading /> :
<div>
{ children }
</div>
}
</Grid>
<StepNavigation disabled={ disabled } disabledNext={ disabledNext } onGotoStep={ gotoStep } onDelete={ onDelete } steps={ steps } location={ location } topDivider onPublish={ onPublish } />
</Grid>
</Grid>
<Grid item xs={ 12 } md={ 3 } xl={ 2 }>
<ProgressMenu disabled={ disabled } onGotoStep={ gotoStep } steps={ steps } location={ location } />
</Grid>
</Grid>
);
}
export default NavigationWrapper;
......@@ -4,16 +4,21 @@ import Grid from '@material-ui/core/Grid';
import Divider from '@material-ui/core/Divider';
import Button from '@material-ui/core/Button';
import {withStyles} from '@material-ui/core/styles';
import {PublishState} from 'model/common.model';
import Authorize from 'ui/common/authorized/Authorize';
interface IStepNavigationProps extends React.ClassAttributes<any> {
classes: any;
steps: any;
itemState: PublishState;
uuid: string;
location: any;
disabled: boolean;
disabledNext: boolean;
onPublish: () => void;
onApprove: () => void;
onUnpublish: () => void;
onGotoStep: (i: number) => () => void;
onDelete: () => void;
showStepName: boolean;
......@@ -33,6 +38,11 @@ const styles = (theme) => ({
float: 'right' as 'right',
},
},
actionsArea: {
'& > *': {
marginLeft:'8px',
},
},
/* tslint:enable */
flexGrow: {
flex: '1 1 auto',
......@@ -72,9 +82,46 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
return steps.find((e) => e.link.endsWith(path)).id;
}
protected getItemActionButtons = () => {
const {itemState, disabled, onApprove, onPublish, onUnpublish, classes} = this.props;
switch (itemState) {
case PublishState.PUBLISHED: {
return (
<Button disabled={ disabled } variant="raised" onClick={ onUnpublish } className={ classes.btnBlue }>
UNPUBLISH
</Button>
);
}
case PublishState.REVIEWING: {
return (
<div className={ classes.actionsArea }>
<Button disabled={ disabled } variant="raised" onClick={ onUnpublish } className={ classes.btnBlue }>
REJECT
</Button>
<Authorize role="ROLE_ADMINISTRATOR">
<Button disabled={ disabled } variant="raised" onClick={ onApprove } className={ classes.btnBlue }>
APPROVE AND PUBLISH
</Button>
</Authorize>
</div>
);
}
case PublishState.DRAFT: {
return (
<Button disabled={ disabled } variant="raised" onClick={ onPublish } className={ classes.btnBlue }>
SEND TO REVIEW
</Button>
);
}
}
}
public render() {
const {classes, disabled, disabledNext, steps, showStepName, topDivider, bottomDivider, onGotoStep, onDelete, onPublish} = this.props;
const {classes, disabled, disabledNext, steps, showStepName, topDivider, bottomDivider, onGotoStep, onDelete} = this.props;
const actionButtons = this.getItemActionButtons();
return (
<Grid container spacing={ 0 } className={ classes.root }>
......@@ -93,21 +140,17 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
)
}
{ this.state.id > 1 && (
<Button onClick={ onGotoStep(this.state.id - 1) } className={ classes.btnReturn }>
<Button onClick={ () => onGotoStep(this.state.id - 1) } className={ classes.btnReturn }>
Return
</Button>
)
}
{ this.state.id !== steps.length && (
<Button disabled={ disabledNext } variant="raised" onClick={ onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
<Button disabled={ disabledNext } variant="raised" onClick={ () => onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
NEXT STEP
</Button>
) }
{ this.state.id === steps.length && (
<Button disabled={ disabled } variant="raised" onClick={ onPublish } className={ classes.btnBlue }>
ACCEPT AND PUBLISH
</Button>
) }
{ this.state.id === steps.length && actionButtons }
</Grid>
{ bottomDivider && <Divider/> }
</Grid>
......
import * as React from 'react';
import {PublishState} from 'model/common.model';
import Grid from '@material-ui/core/Grid';
import Loading from 'ui/common/Loading';
import PageLayout from 'ui/layout/PageLayout';
import StepNavigation from './StepNavigation';
import ProgressMenu from './progress-menu';
import TopSection from './TopSection';
import BottomSection from './BottomSection';
interface IStepperTemplateProps extends React.ClassAttributes<any> {
item: any;
disabled: any;
onGotoStep: any;
onDelete: any;
onPublish: any;
onUnpublish: any;
onApprove: any;
steps: any;
stillLoading: any;
pageTitle: any;
path: string;
location: any;
}
class StepperTemplate<T> extends React.Component<T & IStepperTemplateProps> {
protected setDisabled = (disabled: boolean) => {
this.setState({disabledNext: disabled});
}
private getStepNavigation = () => {
const {disabled, onDelete, onPublish, onUnpublish, onApprove, steps, item, location} = this.props;
return (
<StepNavigation
disabled={ disabled }
disabledNext={ this.state.disabledNext }
onGotoStep={ this.gotoStep }
onDelete={ onDelete }
steps={ steps }
location={ location }
showStepName
bottomDivider
onPublish={ onPublish }
onUnpublish={ onUnpublish }
onApprove={ onApprove }
itemState={ item && item.state || PublishState.DRAFT }
/>
);
}
protected renderContent = () => (<h3>Not implemented in parent</h3>);
protected gotoStep = (id) => (this.props.onGotoStep(id));
public state = {
disabledNext: false,
};
public render() {
const {disabled, steps, stillLoading, pageTitle, path, location} = this.props;
const child = this.renderContent();
const stepNavigation = this.getStepNavigation();
return (
<PageLayout sidebar={ <ProgressMenu disabled={ disabled } onGotoStep={ this.gotoStep } steps={ steps } location={ location } /> }>
<Grid container spacing={ 0 }>
<TopSection pageTitle={ pageTitle } backTarget={ `/dashboard/${path}` }/>
<Grid container spacing={ 0 }>
<Grid item xs={ 12 } md={ 12 } xl={ 12 } className="back-gray p-20">
<Grid container spacing={ 0 } className="back-white">
{ stepNavigation }
<Grid item xs={ 12 }>
{ stillLoading ? <Loading/> : { ...child } }
</Grid>
{ stepNavigation }