diff --git a/modules/services/improveOSM.js b/modules/services/improveOSM.js index 5623a09db..83572cefc 100644 --- a/modules/services/improveOSM.js +++ b/modules/services/improveOSM.js @@ -329,6 +329,36 @@ export default { }); }, + getComments: function(d, callback) { + // If comments already retrieved no need to do so again + if (d.comments !== undefined) { return callback({}, d); } + + var key = d.error_key; + var qParams = {}; + + if (key === 'ow') { + qParams = d.identifier; + } else if (key === 'mr') { + qParams.tileX = d.identifier.x; + qParams.tileY = d.identifier.y; + } else if (key === 'tr') { + qParams.targetId = d.identifier; + } + + var url = _impOsmUrls[key] + '/retrieveComments?' + utilQsString(qParams); + + var that = this; + d3_json(url, function(err, data) { + // comments are served newest to oldest + var comments = data.comments ? data.comments.reverse() : []; + + that.replaceError(d.update({ + comments: comments + })); + return callback(err, d); + }); + }, + postUpdate: function(d, callback) { if (!services.osm.authenticated()) { // Username required in payload return callback({ message: 'Not Authenticated', status: -3}, d); @@ -360,17 +390,16 @@ export default { payload.targetIds = [ d.identifier ]; } - // Comments don't currently work, if they ever do in future - // it looks as though they require a separate post - // if (d.newComment !== undefined) { - // payload.text = d.newComment; - // } - - if (d.newStatus !== d.status) { + if (d.newStatus !== undefined) { payload.status = d.newStatus; payload.text = 'status changed'; } + // Comment take place of default text + if (d.newComment !== undefined) { + payload.text = d.newComment; + } + _erCache.inflightPost[d.id] = d3_request(url) .header('Content-Type', 'application/json') .post(JSON.stringify(payload), function(err) { @@ -379,12 +408,29 @@ export default { // Unsuccessful response status, keep issue open if (err.status !== 200) { return callback(err, d); } - that.removeError(d); + // Just a comment, update error in cache + if (d.newStatus === undefined) { + var now = new Date(); + var comments = d.comments ? d.comments : []; - // No pretty identifier, so we just use coordinates - if (d.newStatus === 'SOLVED') { - var closedID = d.loc[1].toFixed(5) + '/' + d.loc[0].toFixed(5); - _erCache.closed[key + ':' + closedID] = true; + comments.push({ + username: payload.username, + text: payload.text, + timestamp: now.getTime() / 1000 + }); + + that.replaceError(d.update({ + comments: comments, + newComment: undefined + })); + } else { + that.removeError(d); + + if (d.newStatus === 'SOLVED') { + // No pretty identifier, so we just use coordinates + var closedID = d.loc[1].toFixed(5) + '/' + d.loc[0].toFixed(5); + _erCache.closed[key + ':' + closedID] = true; + } } return callback(err, d); diff --git a/modules/ui/improveOSM_comments.js b/modules/ui/improveOSM_comments.js new file mode 100644 index 000000000..47608e0c5 --- /dev/null +++ b/modules/ui/improveOSM_comments.js @@ -0,0 +1,94 @@ +import { select as d3_select } from 'd3-selection'; + +import { t } from '../util/locale'; +import { svgIcon } from '../svg'; +import { services } from '../services'; +import { utilDetect } from '../util/detect'; + +export function uiImproveOsmComments() { + var _error; + + + function errorComments(selection) { + // make the div immediately so it appears above the buttons + var comments = selection.selectAll('.comments-container') + .data([0]); + + comments = comments.enter() + .append('div') + .attr('class', 'comments-container') + .merge(comments); + + // must retrieve comments from API before they can be displayed + services.improveOSM.getComments(_error, function(err, d) { + if (!d.comments) { return; } // nothing to do here + + var commentEnter = comments.selectAll('.comment') + .data(d.comments) + .enter() + .append('div') + .attr('class', 'comment'); + + commentEnter + .append('div') + .attr('class', 'comment-avatar') + .call(svgIcon('#iD-icon-avatar', 'comment-avatar-icon')); + + var mainEnter = commentEnter + .append('div') + .attr('class', 'comment-main'); + + var metadataEnter = mainEnter + .append('div') + .attr('class', 'comment-metadata'); + + metadataEnter + .append('div') + .attr('class', 'comment-author') + .each(function(d) { + var selection = d3_select(this); + var osm = services.osm; + if (osm && d.username) { + selection = selection + .append('a') + .attr('class', 'comment-author-link') + .attr('href', osm.userURL(d.username)) + .attr('tabindex', -1) + .attr('target', '_blank'); + } + selection + .text(function(d) { return d.username; }); + }); + + metadataEnter + .append('div') + .attr('class', 'comment-date') + .text(function(d) { + return t('note.status.commented', { when: localeDateString(d.timestamp) }); + }); + + mainEnter + .append('div') + .attr('class', 'comment-text') + .append('p') + .text(function(d) { return d.text; }); + }); + } + + function localeDateString(s) { + if (!s) return null; + var detected = utilDetect(); + var options = { day: 'numeric', month: 'short', year: 'numeric' }; + var d = new Date(s * 1000); // timestamp is served in seconds, date takes ms + if (isNaN(d.getTime())) return null; + return d.toLocaleDateString(detected.locale, options); + } + + errorComments.error = function(val) { + if (!arguments.length) return _error; + _error = val; + return errorComments; + }; + + return errorComments; +} \ No newline at end of file diff --git a/modules/ui/improveOSM_editor.js b/modules/ui/improveOSM_editor.js index c1c29c1b5..54699067f 100644 --- a/modules/ui/improveOSM_editor.js +++ b/modules/ui/improveOSM_editor.js @@ -1,4 +1,5 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; import { services } from '../services'; @@ -6,18 +7,20 @@ import { modeBrowse } from '../modes'; import { svgIcon } from '../svg'; import { + uiImproveOsmComments, uiImproveOsmDetails, uiImproveOsmHeader, uiQuickLinks, uiTooltipHtml } from './index'; -import { utilRebind } from '../util'; +import { utilNoAuto, utilRebind } from '../util'; export function uiImproveOsmEditor(context) { var dispatch = d3_dispatch('change'); var errorDetails = uiImproveOsmDetails(context); + var errorComments = uiImproveOsmComments(context); var errorHeader = uiImproveOsmHeader(context); var quickLinks = uiQuickLinks(); @@ -76,6 +79,7 @@ export function uiImproveOsmEditor(context) { .call(errorHeader.error(_error)) .call(quickLinks.choices(choices)) .call(errorDetails.error(_error)) + .call(errorComments.error(_error)) .call(improveOsmSaveSection); } @@ -97,10 +101,45 @@ export function uiImproveOsmEditor(context) { .append('div') .attr('class', 'keepRight-save save-section cf'); + saveSectionEnter + .append('h4') + .attr('class', '.error-save-header') + .text(t('note.newComment')); + + saveSectionEnter + .append('textarea') + .attr('class', 'new-comment-input') + .attr('placeholder', t('QA.keepRight.comment_placeholder')) + .attr('maxlength', 1000) + .property('value', function(d) { return d.newComment; }) + .call(utilNoAuto) + .on('input', changeInput) + .on('blur', changeInput); + // update saveSection = saveSectionEnter .merge(saveSection) .call(errorSaveButtons); + + function changeInput() { + var input = d3_select(this); + var val = input.property('value').trim(); + + if (val === '') { + val = undefined; + } + + // store the unsaved comment with the error itself + _error = _error.update({ newComment: val }); + + var errorService = services.improveOSM; + if (errorService) { + errorService.replaceError(_error); + } + + saveSection + .call(errorSaveButtons); + } } function errorSaveButtons(selection) { @@ -117,11 +156,10 @@ export function uiImproveOsmEditor(context) { .append('div') .attr('class', 'buttons'); - // Comments don't currently work - // buttonEnter - // .append('button') - // .attr('class', 'button comment-button action') - // .text(t('QA.keepRight.save_comment')); + buttonEnter + .append('button') + .attr('class', 'button comment-button action') + .text(t('QA.keepRight.save_comment')); buttonEnter .append('button') @@ -136,20 +174,19 @@ export function uiImproveOsmEditor(context) { buttonSection = buttonSection .merge(buttonEnter); - // Comments don't currently work - // buttonSection.select('.comment-button') - // .attr('disabled', function(d) { - // return d.newComment === undefined ? true : null; - // }) - // .on('click.comment', function(d) { - // this.blur(); // avoid keeping focus on the button - #4641 - // var errorService = services.improveOSM; - // if (errorService) { - // errorService.postUpdate(d, function(err, error) { - // dispatch.call('change', error); - // }); - // } - // }); + buttonSection.select('.comment-button') + .attr('disabled', function(d) { + return d.newComment === undefined ? true : null; + }) + .on('click.comment', function(d) { + this.blur(); // avoid keeping focus on the button - #4641 + var errorService = services.improveOSM; + if (errorService) { + errorService.postUpdate(d, function(err, error) { + dispatch.call('change', error); + }); + } + }); buttonSection.select('.close-button') .text(function(d) { @@ -192,4 +229,4 @@ export function uiImproveOsmEditor(context) { return utilRebind(improveOsmEditor, dispatch, 'on'); -} +} \ No newline at end of file diff --git a/modules/ui/index.js b/modules/ui/index.js index c1361064e..25775c454 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -28,6 +28,7 @@ export { uiFormFields } from './form_fields'; export { uiFullScreen } from './full_screen'; export { uiGeolocate } from './geolocate'; export { uiHelp } from './help'; +export { uiImproveOsmComments } from './improveOSM_comments'; export { uiImproveOsmDetails } from './improveOSM_details'; export { uiImproveOsmEditor } from './improveOSM_editor'; export { uiImproveOsmHeader } from './improveOSM_header'; @@ -72,4 +73,4 @@ export { uiUndoRedo } from './undo_redo'; export { uiVersion } from './version'; export { uiViewOnOSM } from './view_on_osm'; export { uiViewOnKeepRight } from './view_on_keepRight'; -export { uiZoom } from './zoom'; +export { uiZoom } from './zoom'; \ No newline at end of file