Commit 7a460efa authored by Matija Obreza's avatar Matija Obreza
Browse files

Merge branch '309-dataset-autosave-on-blur' into 'master'

Resolve "Dataset autosave on blur"

Closes #309

See merge request !247
parents 8d903765 ac1dfc0f
......@@ -15,6 +15,9 @@ import {log} from 'utilities/debug';
import { CREATE_DATASET, RECEIVE_DATASET, RECEIVE_DATASET_PAGE, ADD_CREATOR_TO_DATASET, REMOVE_CREATOR_FROM_DATASET, UPDATE_DATASET_CREATOR, ADD_LOCATION, RECEIVE_LOCATION, REMOVE_LOCATION } from 'constants/datasets';
import {addFilterCode} from 'actions/filterCode';
import {cleanFilters} from 'utilities';
import steps from 'ui/pages/dataset/dataset-stepper/steps';
import { navigateTo } from 'actions/navigation';
import * as _ from 'lodash';
const receiveDataset = (dataset: Dataset) => ({
type: RECEIVE_DATASET, payload: dataset,
......@@ -118,6 +121,12 @@ const createDataset = () => (dispatch) => {
const saveDataset = (dataset: Dataset) => (dispatch, getState) => {
const needToRedirect: boolean = !(dataset.version && dataset.uuid);
if (_.isEqual({...getState().datasets.currentDataset}, {...dataset})) {
return;
}
// remove normalized data here
const data = new Dataset({
...dataset,
......@@ -132,11 +141,23 @@ const saveDataset = (dataset: Dataset) => (dispatch, getState) => {
return DatasetService.saveDataset(token, data)
.then((saved) => {
dispatch(receiveDataset(saved));
if (needToRedirect) {
dispatch(gotoNextStep(saved));
}
}).catch((error) => {
log('Save error', error);
});
};
function gotoNextStep(dataset: Dataset) {
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(`/datasets/${dataset.uuid}/${path}`));
};
}
function publishDataset(dataset: Dataset, published: boolean = true) {
return (dispatch, getState) => {
const token = getState().login.access_token;
......
......@@ -12,23 +12,20 @@ interface ICropSelectorProps extends React.ClassAttributes<any> {
t: any;
crops: Crop[];
fields: any;
onSave: () => void;
label: string;
}
const handleChange = (fields, onSave, code) => () => {
const index = fields.getAll().indexOf(code);
const handleChange = (fields, code) => () => {
const index = fields.getAll() ? fields.getAll().indexOf(code) : -1;
if (index === -1) {
fields.push(code);
} else {
fields.remove(index);
}
onSave();
};
const CropSelector = ({t, fields, crops, onSave, label}: ICropSelectorProps) => crops && (
const CropSelector = ({t, fields, crops, label}: ICropSelectorProps) => crops && (
<FormControl fullWidth>
<FormLabel>{ label }</FormLabel>
<FormHelperText>{ t('m.crop.helper') }</FormHelperText>
......@@ -40,7 +37,7 @@ const CropSelector = ({t, fields, crops, onSave, label}: ICropSelectorProps) =>
control={
<Checkbox
checked={ fields.getAll() && fields.getAll().indexOf(crop.code) !== -1 }
onChange={ handleChange(fields, onSave, crop.code) }
onChange={ handleChange(fields, crop.code) }
value={ crop.code }
/>
}
......
......@@ -12,6 +12,7 @@ interface IStepNavigationProps extends React.ClassAttributes<any> {
uuid: string;
location: any;
disabled: boolean;
disabledNext: boolean;
onPublish: () => void;
onGotoStep: (i: number) => () => void;
onDelete: () => void;
......@@ -73,7 +74,7 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
public render() {
const {classes, disabled, steps, showStepName, topDivider, bottomDivider, onGotoStep, onDelete, onPublish} = this.props;
const {classes, disabled, disabledNext, steps, showStepName, topDivider, bottomDivider, onGotoStep, onDelete, onPublish} = this.props;
return (
<Grid container spacing={ 0 } className={ classes.root }>
......@@ -98,7 +99,7 @@ class StepNavigation extends React.Component<IStepNavigationProps, any> {
)
}
{ this.state.id !== steps.length && (
<Button disabled={ disabled } raised onClick={ onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
<Button disabled={ disabledNext } raised onClick={ onGotoStep(this.state.id + 1) } className={ classes.btnBlue }>
NEXT STEP
</Button>
) }
......
......@@ -9,7 +9,7 @@ interface IProgressMenuProps extends React.ClassAttributes<any> {
classes: any;
location: any;
steps: any;
onGotoStep: (i: number) => () => void;
onGotoStep: (i: number) => void;
}
const styleSheet = (theme) => ({
......
import * as React from 'react';
import Grid from 'material-ui/Grid';
import StepNavigation from 'ui/common/stepper/StepNavigation';
import ProgressMenu from 'ui/common/stepper/progress-menu';
import Loading from 'ui/common/Loading';
function DatasetNavigator({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 DatasetNavigator;
......@@ -6,61 +6,14 @@ import Grid from 'material-ui/Grid';
import { log } from 'utilities/debug';
import confirm from 'utilities/confirmAlert';
import ProgressMenu from 'ui/common/stepper/progress-menu';
import TopSection from 'ui/common/stepper/TopSection';
import BottomSection from 'ui/common/stepper/BottomSection';
import StepNavigation from 'ui/common/stepper/StepNavigation';
import { navigateTo } from 'actions/navigation';
import { setPageTitle } from 'actions/pageTitle';
import { loadDataset, publishDataset, deleteDataset } from 'actions/dataset';
import { Dataset } from 'model/dataset.model';
import Loading from 'ui/common/Loading';
import renderRoutes from 'ui/renderRoutes';
const steps = [
{
id: 1,
name: 'Basic information',
link: 'edit',
active: false,
},
{
id: 2,
name: 'Dataset attachments',
link: 'edit/files',
active: false,
},
{
id: 3,
name: 'Dataset creators',
link: 'edit/dataset-creator',
active: false,
},
{
id: 4,
name: 'Timing and location',
link: 'edit/timing-and-location',
active: false,
},
{
id: 5,
name: 'List of accessions',
link: 'edit/list-of-accessions',
active: false,
},
{
id: 6,
name: 'Traits observed',
link: 'edit/traits-observed',
active: false,
},
{
id: 7,
name: 'Review and publish',
link: 'edit/review-and-publish',
active: false,
},
];
import steps from './steps';
interface IDatasetProps extends React.ClassAttributes<any> {
classes: any;
......@@ -95,18 +48,16 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
const {uuid, dataset, loadDataset} = this.props;
if (uuid && (!dataset || dataset.uuid !== uuid)) {
loadDataset(uuid);
}
loadDataset(uuid);
}
}
protected gotoStep = (id) => () => {
protected gotoStep = (id) => {
const {dataset, uuid, navigateTo} = this.props;
const path = steps.find((e) => e.id === id).link;
log('Go to step', path);
if (uuid || (dataset && dataset.uuid)) {
navigateTo(`/datasets/${dataset ? dataset.uuid : uuid}/${path}`);
steps.forEach((i) => i.active = false);
steps[id - 1].active = true;
} else {
// no navigation!
}
......@@ -133,7 +84,7 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
}
public render() {
const {uuid, location, pageTitle, route, match} = this.props;
const {uuid, pageTitle, route, match} = this.props;
let {dataset} = this.props;
const stillLoading: boolean = (! dataset || (uuid && (dataset.uuid !== uuid)));
......@@ -150,22 +101,9 @@ class DatasetStepper extends React.Component<IDatasetProps, any> {
return (
<Grid container spacing={ 0 }>
<TopSection pageTitle={ pageTitle } subTitle="Publish your datasets" />
<Grid item xs={ 12 } md={ 9 } xl={ 10 } className="back-gray p-20">
<Grid container spacing={ 0 } className="back-white">
<StepNavigation disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } onDelete={ this.onDelete } steps={ steps } location={ location } showStepName bottomDivider onPublish={ this.onPublish } />
<Grid item xs={ 12 }>
{ stillLoading ? <Loading /> :
<div>
{ renderRoutes(route.routes, match.path) }
</div>
}
</Grid>
<StepNavigation disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } onDelete={ this.onDelete } steps={ steps } location={ location } topDivider onPublish={ this.onPublish } />
</Grid>
</Grid>
<Grid item xs={ 12 } md={ 3 } xl={ 2 }>
<ProgressMenu disabled={ !(dataset && dataset.uuid) } onGotoStep={ this.gotoStep } steps={ steps } location={ location } />
</Grid>
{
renderRoutes(route.routes, match.path, { stillLoading, onGotoStep: this.gotoStep, onDelete: this.onDelete, onPublish: this.onPublish })
}
<BottomSection/>
</Grid>
);
......
const steps = [
{
id: 1,
name: 'Basic information',
link: 'edit',
},
{
id: 2,
name: 'Dataset attachments',
link: 'edit/files',
},
{
id: 3,
name: 'Dataset creators',
link: 'edit/dataset-creator',
},
{
id: 4,
name: 'Timing and location',
link: 'edit/timing-and-location',
},
{
id: 5,
name: 'List of accessions',
link: 'edit/list-of-accessions',
},
{
id: 6,
name: 'Traits observed',
link: 'edit/traits-observed',
},
{
id: 7,
name: 'Review and publish',
link: 'edit/review-and-publish',
},
];
export default steps;
......@@ -113,7 +113,7 @@ class ListOfAccession extends React.Component<IListOfAccession, any> {
</div>
<h3>Accession list: { accessionIdentifiers.length } rows</h3>
<h3>Accession list: { accessionIdentifiers ? accessionIdentifiers.length : 0 } rows</h3>
<AccessionIdentifiersTable accessionIdentifiers={ accessionIdentifiers } />
</div>
);
......
......@@ -5,10 +5,17 @@ import {connect} from 'react-redux';
import {Dataset, AccessionIdentifier} from 'model/dataset.model';
import {updateDatasetAccessionIdentifiers} from 'actions/dataset';
import ListOfAccesion from './ListOfAccesion';
import steps from '../../steps';
import DatasetNavigator from '../../DatasetNavigator';
interface IAccessionsListStep extends React.ClassAttributes<any> {
dataset: Dataset;
updateDatasetAccessionIdentifiers: (dataset: Dataset, accessionIdentifiers: AccessionIdentifier[]) => Promise<Dataset>;
stillLoading: boolean;
onDelete: () => void;
onPublish: () => void;
onGotoStep: (id: number) => () => void;
location: any;
}
class AccessionsListStep extends React.Component<IAccessionsListStep, any> {
......@@ -18,21 +25,37 @@ class AccessionsListStep extends React.Component<IAccessionsListStep, any> {
}
protected updateAccessionIdentifiers = (accessionIdentifiers: AccessionIdentifier[]) => {
const {dataset, updateDatasetAccessionIdentifiers} = this.props;
updateDatasetAccessionIdentifiers(dataset, accessionIdentifiers);
const {dataset, updateDatasetAccessionIdentifiers} = this.props;
updateDatasetAccessionIdentifiers(dataset, accessionIdentifiers);
}
protected gotoStep = (id) => () => {
this.props.onGotoStep(id);
}
public render() {
const {dataset} = this.props;
const {dataset, stillLoading, onDelete, onPublish} = this.props;
return (
<ListOfAccesion accessionIdentifiers={ dataset.accessionIdentifiers } onAccessionsUpdated={ this.updateAccessionIdentifiers }/>
<DatasetNavigator location={ location } stillLoading={ stillLoading } disabledNext={ false }
disabled={ false } steps={ steps }
gotoStep={ this.gotoStep } onDelete={ onDelete } onPublish={ onPublish }>
<ListOfAccesion
accessionIdentifiers={ dataset.accessionIdentifiers }
onAccessionsUpdated={ this.updateAccessionIdentifiers }
/>
</DatasetNavigator>
);
}
}
const mapStateToProps = (state, ownProps) => ({
dataset: state.datasets.currentDataset,
stillLoading: ownProps.stillLoading,
location: ownProps.location,
onDelete: ownProps.onDelete,
onPublish: ownProps.onPublish,
onGotoStep: ownProps.onGotoStep,
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
......
......@@ -14,6 +14,7 @@ import CropSelector from 'ui/catalog/crop/CropSelector';
import Validators from 'utilities/Validators';
import BasicInfoRadioGroup from './BasicInfoRadioGroup';
import remoteSubmit from './RemoteSubmit';
interface ILoginContainerProps extends React.ClassAttributes<any> {
handleSubmit: () => void;
......@@ -25,16 +26,12 @@ interface ILoginContainerProps extends React.ClassAttributes<any> {
class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
protected save = () => {
setTimeout(this.props.handleSubmit);
}
public render() {
const {partners, initialValues} = this.props;
const {partners, initialValues, handleSubmit} = this.props;
return (
<form className="p-20 m-20 even-row">
<form onSubmit={ handleSubmit } className="p-20 m-20 even-row">
<Field required
name="owner"
component={ SelectPartner }
......@@ -42,7 +39,6 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
placeholder="Partner"
partners={ partners }
editable={ ! (initialValues.uuid && initialValues.version) }
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field required
......@@ -51,7 +47,6 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
component={ MarkdownField }
label="Title of the dataset"
placeholder="Title"
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field required
......@@ -59,7 +54,6 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
component={ TextField }
label="Dataset version"
placeholder="2018.1"
onBlur={ this.save }
validate={ [ Validators.required ] }
/>
<Field
......@@ -67,25 +61,21 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
component={ MarkdownField }
label="Dataset description"
placeholder="An abstract, short or long description of the resource. Descriptive details improves discoverability of the resource."
onBlur={ this.save }
/>
<Field
name="created"
component={ TextField }
label="Date of creation of the document"
onBlur={ this.save }
/>
<Field
name="rights"
component={ BasicInfoRadioGroup }
onBlur={ this.save }
/>
<Field
name="language"
component={ MaterialAutosuggest }
label="Language"
placeholder="Select language*"
onBlur={ this.save }
suggestions={ languages }
suggestionLabel="label"
/>
......@@ -93,13 +83,11 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
name="source"
component={ TextField }
label="Source"
onBlur={ this.save }
/>
<FieldArray
name="crops"
component={ CropSelector }
label="Crops"
onSave={ this.save }
/>
</form>
);
......@@ -108,5 +96,6 @@ class BasicInfoStep extends React.Component<ILoginContainerProps, any> {
export default reduxForm({
form: DATASET_BASIC_INFO_FORM,
onSubmit: remoteSubmit,
enableReinitialize: true,
})((BasicInfoStep));
import {Dataset} from 'model/dataset.model';
import { saveDataset } from 'actions/dataset';
export default function remoteSubmit(values: Dataset, dispatch) {
dispatch(saveDataset(values));
}
import * as React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import * as _ from 'lodash';
import {log} from 'utilities/debug';
import BasicInfoForm from './BasicInfoForm';
import {Dataset} from 'model/dataset.model';
import {saveDataset} from 'actions/dataset';
import {loadMyPartners} from 'actions/partner';
import {Partner} from 'model/partner.model';
import {Crop} from 'model/crop.model';
import {submit, isInvalid} from 'redux-form';
import {DATASET_BASIC_INFO_FORM} from 'constants/datasets';
import DatasetNavigator from '../../DatasetNavigator';
import steps from '../../steps';
interface IDatasetProps extends React.ClassAttributes<any> {
uuid: string;
......@@ -19,6 +21,14 @@ interface IDatasetProps extends React.ClassAttributes<any> {
loadMyPartners: any;
myPartners: Partner[];
crops: Crop[];
submit: any;
isInvalidForm: boolean;
stillLoading: boolean;
onDelete: () => void;
onPublish: () => void;
onGotoStep: (id: number) => () => void;
location: any;
}
class BasicInfoStep extends React.Component<IDatasetProps, any> {
......@@ -33,27 +43,25 @@ class BasicInfoStep extends React.Component<IDatasetProps, any> {
}
}
protected handleSubmit = (values: Dataset) => {
const {dataset, saveDataset} = this.props;
log('Saving', values);
if (!_.isEqual({...dataset}, {...values})) {
saveDataset(values);
}
protected gotoStep = (id) => () => {
const { submit, onGotoStep } = this.props;
log('Saving form');
submit(DATASET_BASIC_INFO_FORM);
setTimeout(() => onGotoStep(id));
}
public render() {
const {myPartners, dataset} = this.props;
if (! dataset) {
return null;
}
const {myPartners, dataset, location, stillLoading, onDelete, onPublish, isInvalidForm} = this.props;
return (
<BasicInfoForm
onSubmit={ this.handleSubmit }
initialValues={ dataset }
partners={ myPartners }
/>
<DatasetNavigator location={ location } stillLoading={ stillLoading } disabledNext={ isInvalidForm }
disabled={ !(dataset && dataset.uuid) } steps={ steps }
gotoStep={ this.gotoStep } onDelete={ onDelete } onPublish={ onPublish }>
<BasicInfoForm
initialValues={ dataset }
partners={ myPartners }
/>
</DatasetNavigator>
);
}
}
......@@ -61,11 +69,17 @@ class BasicInfoStep extends React.Component<IDatasetProps, any> {
const mapStateToProps = (state, ownProps) => ({
myPartners: state.partner.myPartners,
dataset: state.datasets.currentDataset,
stillLoading: ownProps.stillLoading,
location: ownProps.location,
onDelete: ownProps.onDelete,
onPublish: ownProps.onPublish,
onGotoStep: ownProps.onGotoStep,
isInvalidForm: isInvalid(DATASET_BASIC_INFO_FORM)(state),
});
const mapDispatchToProps = (dispatch) => bindActionCreators({
saveDataset,
loadMyPartners,
submit,