From 12a9bb4dbf63131d5bd4273215e9e344015a4fbb Mon Sep 17 00:00:00 2001 From: Sarah Berenji Date: Thu, 23 Nov 2017 11:30:39 +0100 Subject: [PATCH] Adding UI for publication workflow (the user part) --- webui/src/components/editfiles.jsx | 62 ++++---- webui/src/components/editrecord.jsx | 222 ++++++++++++++++++++++------ webui/src/components/selectbig.jsx | 6 +- webui/src/components/user.jsx | 4 + 4 files changed, 218 insertions(+), 76 deletions(-) diff --git a/webui/src/components/editfiles.jsx b/webui/src/components/editfiles.jsx index 1cc4f33ad9..3d0a6691c6 100644 --- a/webui/src/components/editfiles.jsx +++ b/webui/src/components/editfiles.jsx @@ -266,6 +266,10 @@ export const EditFiles = React.createClass({ } }, + componentWillReceiveProps(props) { + return props.readOnly !== this.props.readOnly; + }, + handleAdd: function(fs, location) { const files = this.state.files; for (let i = 0; i < fs.length; ++i) { @@ -378,39 +382,43 @@ export const EditFiles = React.createClass({ this.updateNext(); const b2dropZone = this.props.setModal(false)} onFiles={fs => this.handleAdd(fs, 'b2drop')} />; + // FIXME: the following doesn't work, need to fix it and change the cursor to disabled icon + const disabledCursor = this.props.readOnly ? " , cursor: 'not-allowed'" : ""; return (
-
-

- Add files -

-
- this.handleAdd(fs, 'local')}/> -
-
- -
- - { !this.state.files.length ? false : -
- { this.renderUploadQueue() } -
- } -
- - { !this.props.files.length ? false : +
-
-

Uploaded files

+

+ Add files +

+
+ this.handleAdd(fs, 'local')}/>
-
- { this.renderRecordFiles() } +
+
+ + { !this.state.files.length ? false : +
+ { this.renderUploadQueue() } +
+ }
- } + + { !this.props.files.length ? false : +
+
+

Uploaded files

+
+
+ { this.renderRecordFiles() } +
+
+ } +
); }, diff --git a/webui/src/components/editrecord.jsx b/webui/src/components/editrecord.jsx index 1c12cb6487..89b5202f95 100644 --- a/webui/src/components/editrecord.jsx +++ b/webui/src/components/editrecord.jsx @@ -99,6 +99,9 @@ const EditRecord = React.createClass({ errors: {}, dirty: false, waitingForServer: false, + readOnly: false, + checkboxValue: null, + revoking:false, }; }, @@ -122,7 +125,8 @@ const EditRecord = React.createClass({ this.setState({modal})} /> + setModal={modal => this.setState({modal})} + readOnly={this.state.readOnly} /> ); }, @@ -268,7 +272,7 @@ const EditRecord = React.createClass({ }; return ( + defaultValue={initial} onChange={onChange} disabled={this.state.readOnly} /> ); }, @@ -293,12 +297,12 @@ const EditRecord = React.createClass({ const languages = serverCache.getLanguages(); field = (languages instanceof Error) ? : this.setValue(schema, path, x)} value={this.getValue(path)} />; + onSelect={x=>this.setValue(schema, path, x)} value={this.getValue(path)} readOnly={this.state.readOnly} />; } else if (path.length === 2 && path[0] === 'disciplines') { const disciplines = serverCache.getDisciplines(); field = (disciplines instanceof Error) ? : this.setValue(schema, path, x)} value={this.getValue(path)} />; + onSelect={x=>this.setValue(schema, path, x)} value={this.getValue(path)} readOnly={this.state.readOnly} />; } else if (schema.get('type') === 'array') { const itemSchema = schema.get('items'); const raw_values = this.getValue(path); @@ -465,6 +469,7 @@ const EditRecord = React.createClass({ } record = addEmptyMetadataBlocks(record, props.blockSchemas) || record; this.setState({record}); + this.setState({checkboxValue:record.get('publication_state')}); } else if (this.state.record && props.blockSchemas) { const record = addEmptyMetadataBlocks(this.state.record, props.blockSchemas); if (record) { @@ -472,6 +477,13 @@ const EditRecord = React.createClass({ } } + // Record is submitted for review: the publication_state is 'submitted' and readonly should be True + if(this.props.community && this.state.record){ + if(this.props.community.getIn(["publication_workflow"]) == 'review_and_publish' && this.state.record.get('publication_state')=='submitted' && !this.state.revoking){ + this.setState({readOnly: true}); + } + } + function addEmptyMetadataBlocks(record, blockSchemas) { if (!blockSchemas || !blockSchemas.length) { return false; @@ -550,6 +562,26 @@ const EditRecord = React.createClass({ return errors; }, + componentDidUpdate(prevProps, prevState) { + const original = this.props.record.get('metadata').toJS(); + let updated = this.state.record.toJS(); + // checkbox is enabled and record is going to be "submitted" for review by community admin + if (prevState.record.getIn(['publication_state']) !== this.state.record.getIn(['publication_state']) && !this.state.revoking){ + const patch = compare(original, updated); + if (!patch || !patch.length) { + this.setState({dirty:false}); + return; + } + this.applyPatch(patch, 'submitted'); + } + // Revoking the submitted record for review. Going back to the draft mode. + if(prevState.revoking !== this.state.revoking && this.state.revoking){ + updated['publication_state'] = 'draft'; + const patch = compare(original, updated); + this.applyPatch(patch, 'edit'); + } + }, + updateRecord(event) { event.preventDefault(); const errors = this.findValidationErrors(); @@ -557,25 +589,84 @@ const EditRecord = React.createClass({ this.setState({errors}); return; } - const original = this.props.record.get('metadata').toJS(); - const updated = this.state.record.toJS(); - const patch = compare(original, updated); - if (!patch || !patch.length) { - this.setState({dirty:false}); - return; - } - const afterPatch = (record) => { - if (this.props.isDraft && !this.isForPublication()) { - this.props.refreshCache(); - // TODO(edima): when a draft is publised, clean the state of - // records in versioned chain, to trigger a refetch of - // versioning data - this.setState({dirty:false, waitingForServer: false}); - notifications.clearAll(); - } else { - browser.gotoRecord(record.id); + + if(this.state.checkboxValue == 'draft'){ + // Save draft + const original = this.props.record.get('metadata').toJS(); + const updated = this.state.record.toJS(); + const patch = compare(original, updated); + if (!patch || !patch.length) { + this.setState({dirty:false}); + return; + } + this.applyPatch(patch, 'save_draft'); + } else if(this.state.checkboxValue == 'published' && this.state.record.get('publication_state') == 'published'){ + // Editing metadata of a published record (workflow = direct_publish) + const original = this.props.record.get('metadata').toJS(); + const updated = this.state.record.toJS(); + const patch = compare(original, updated); + if (!patch || !patch.length) { + this.setState({dirty:false}); + return; } + this.applyPatch(patch, 'edit_metadata'); + } else { + // (review workflow) submit a record for review or (direct publish workflow) Publishing a record + const record = this.state.record.set('publication_state', this.state.checkboxValue); + this.setState({record}); } + }, + + applyPatch(patch, caseValue) { + let afterPatch; + switch (caseValue) { + case 'submitted': + afterPatch = (record) => { + // Sending a record for review + if (this.props.community.getIn(["publication_workflow"]) == 'review_and_publish'){ + this.setState({dirty:false, waitingForServer: false, readOnly: true}); + notifications.warning(`This record is submitted and waiting for review by your community administrator`); + browser.gotoEditRecord(record.id); + } else { + // direct publish workflow + browser.gotoRecord(record.id); + } + } + break; + + case 'edit': + afterPatch = (record) => { + this.setState({waitingForServer: false, revoking: false}); + notifications.clearAll(); + browser.gotoEditRecord(record.id); + } + break; + + case 'save_draft': + afterPatch = (record) => { + // if (this.props.isDraft && !this.isForPublication()) { + this.props.refreshCache(); + // TODO(edima): when a draft is publised, clean the state of + // records in versioned chain, to trigger a refetch of + // versioning data + this.setState({dirty: false, waitingForServer: false, checkboxValue: 'draft'}); + notifications.clearAll(); + // } + } + break; + + case 'edit_metadata': + afterPatch = (record) => { + // TODO(edima): when a draft is publised, clean the state of + // records in versioned chain, to trigger a refetch of + // versioning data + this.setState({dirty:false, waitingForServer: false}); + notifications.clearAll(); + browser.gotoRecord(record.id); + } + break; + } + const onError = (xhr) => { this.setState({waitingForServer: false}); onAjaxError(xhr); @@ -587,19 +678,24 @@ const EditRecord = React.createClass({ } catch (_) { } } - this.setState({waitingForServer: true}); this.props.patchFn(patch, afterPatch, onError); }, + editSubmittedRecord(event){ + event.preventDefault(); + const record = this.state.record.set('publication_state', "draft"); + this.setState({record}); + this.setState({dirty:false, checkboxValue:'draft', revoking:true, readOnly:false}); + }, + isForPublication() { - return this.state.record.get('publication_state') == 'submitted'; + return this.state.checkboxValue == 'submitted'; }, setPublishedState(e) { const state = e.target.checked ? 'submitted' : 'draft'; - const record = this.state.record.set('publication_state', state); - this.setState({record}); + this.setState({checkboxValue:state}); }, renderUpdateRecordForm() { @@ -616,23 +712,44 @@ const EditRecord = React.createClass({ }, renderSubmitDraftForm() { - const klass = this.state.waitingForServer ? 'disabled' : - this.isForPublication() ? 'btn-primary btn-danger' : - this.state.dirty ? 'btn-primary' : 'disabled'; - const text = this.state.waitingForServer ? "Updating record, please wait..." : - this.isForPublication() ? 'Save and Publish' : - this.state.dirty ? 'Save Draft' : 'The draft is up to date'; - return ( -
- -

When the draft is published it will be assigned a PID, making it publicly citable. - But a published record's files can no longer be modified by its owner.

- -
- ); + if(this.props.community){ + if(this.props.community.getIn(["publication_workflow"]) == 'review_and_publish'){ + const klass = this.state.waitingForServer ? 'disabled' : + this.isForPublication() ? 'btn-primary btn-danger' : + this.state.dirty ? 'btn-primary' : 'disabled'; + const text = this.state.waitingForServer ? "Updating record, please wait..." : + this.isForPublication() ? 'Save and submit for review' : + this.state.dirty ? 'Save Draft' : 'The draft is up to date'; + return ( +
+ + +
+ ); + } else { + const klass = this.state.waitingForServer ? 'disabled' : + this.isForPublication() ? 'btn-primary btn-danger' : + this.state.dirty ? 'btn-primary' : 'disabled'; + const text = this.state.waitingForServer ? "Updating record, please wait..." : + this.isForPublication() ? 'Save and Publish' : + this.state.dirty ? 'Save Draft' : 'The draft is up to date'; + return ( +
+ +

When the draft is published it will be assigned a PID, making it publicly citable. + But a published record's files can no longer be modified by its owner.

+ +
+ ); + } + + } }, render() { @@ -666,6 +783,7 @@ const EditRecord = React.createClass({ { this.props.isDraft ? this.renderFileBlock() : false }
+
{ this.renderFieldBlock(null, rootSchema) } @@ -673,14 +791,24 @@ const EditRecord = React.createClass({ blockSchemas.map(([id, blockSchema]) => this.renderFieldBlock(id, (blockSchema||Map()).get('json_schema'))) }
+
-
- {pairs(this.state.errors).map( ([id, msg]) => -
{msg}
) } - { this.props.isDraft ? this.renderSubmitDraftForm() : this.renderUpdateRecordForm() } -
+ {this.state.record.get('publication_state') == 'submitted' && this.state.readOnly ? +
+
+

Note that by editing the record, it will be revoked and won't being reviewd by your community admin anymore. You will need to submit it again.

+ +
+
+ : +
+ {pairs(this.state.errors).map( ([id, msg]) => +
{msg}
) } + { this.props.isDraft ? this.renderSubmitDraftForm() : this.renderUpdateRecordForm() } +
+ }
); diff --git a/webui/src/components/selectbig.jsx b/webui/src/components/selectbig.jsx index 36b8bf1f99..2ec0eab7e7 100644 --- a/webui/src/components/selectbig.jsx +++ b/webui/src/components/selectbig.jsx @@ -17,7 +17,8 @@ export const SelectBig = React.createClass({ const len = x => (x && x.length !== undefined) ? x.length : 0; // fast check, not exact, but should work for our use case return nextProps.value !== this.props.value - || len(nextProps.data) !== len(this.props.data); + || len(nextProps.data) !== len(this.props.data) + || nextProps.readOnly !== this.props.readOnly; }, getInitialState: function(){ @@ -73,7 +74,8 @@ export const SelectBig = React.createClass({ filter={this.filter} caseSensitive={false} minLength={2} - busy={busy} /> + busy={busy} + disabled={this.props.readOnly} /> ); } }); diff --git a/webui/src/components/user.jsx b/webui/src/components/user.jsx index 5718cd454c..8c331db9a8 100644 --- a/webui/src/components/user.jsx +++ b/webui/src/components/user.jsx @@ -133,6 +133,10 @@ export const UserProfile = React.createClass({

Roles

{roles && roles.count() && typeof(communitiesList) !== "undefined" ? this.listRoles(roles, communitiesList) :

You have no assigned roles

} +
+

Submitted Records for Review

+

List of submitted records waiting for review by your community administrator

+

Own records