From fb0d17e7135407472d87c60ce8235971c219ec8e Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 11 Jan 2015 23:13:31 -0500 Subject: [PATCH] WIP: Add choices ui for resolving conflicts --- data/core.yaml | 15 ++++--- dist/locales/en.json | 16 +++++--- js/id/modes/save.js | 94 +++++++++++++++++++++++++++++++++----------- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 81d8fe0ab..181bccdae 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -312,18 +312,25 @@ en: title: Save help: "Save changes to OpenStreetMap, making them visible to other users." no_changes: No changes to save. - errors: Errors occurred while trying to save + error: Errors occurred while trying to save status_code: "Server returned status code {code}" status_gone: '{type} "{id}" {name} has already been deleted.' unknown_error_details: "Please ensure you are connected to the internet." uploading: Uploading changes to OpenStreetMap. unsaved_changes: You have unsaved changes - conflicts: + conflict: header: Conflicting edits detected + message: 'Conflicting edits were made to {type} "{id}" {name}' keep_local: Keep Mine keep_remote: Keep Theirs restore: Restore - leave_deleted: Leave Deleted + delete: Leave Deleted + annotation: + safe: Merged remote changes from server. + keep_local: 'Kept local version of "{id}".' + keep_remote: 'Kept remote version of "{id}".' + restore: 'Restored local version of "{id}".' + delete: 'Deleted local version of "{id}".' try_again: Try Again download_changes: Download Changes help: | @@ -331,9 +338,7 @@ en: You can click on each item below for more details about the conflict, and choose whether to keep your changes or the other user's changes. Or, you can download your changes to a file. merge_remote_changes: - annotation: Merged remote changes from server. conflict: - general: 'Conflicting edits were made to {type} "{id}" {name}' location: Location was changed both locally and remotely. nodelist: Nodes were changed both locally and remotely. memberlist: Relation members were changed both locally and remotely. diff --git a/dist/locales/en.json b/dist/locales/en.json index 15fec00ad..350f8e016 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -386,27 +386,33 @@ "title": "Save", "help": "Save changes to OpenStreetMap, making them visible to other users.", "no_changes": "No changes to save.", - "errors": "Errors occurred while trying to save", + "error": "Errors occurred while trying to save", "status_code": "Server returned status code {code}", "status_gone": "{type} \"{id}\" {name} has already been deleted.", "unknown_error_details": "Please ensure you are connected to the internet.", "uploading": "Uploading changes to OpenStreetMap.", "unsaved_changes": "You have unsaved changes", - "conflicts": { + "conflict": { "header": "Conflicting edits detected", + "message": "Conflicting edits were made to {type} \"{id}\" {name}", "keep_local": "Keep Mine", "keep_remote": "Keep Theirs", "restore": "Restore", - "leave_deleted": "Leave Deleted", + "delete": "Leave Deleted", + "annotation": { + "safe": "Merged remote changes from server.", + "keep_local": "Kept local version of \"{id}\".", + "keep_remote": "Kept remote version of \"{id}\".", + "restore": "Restored local version of \"{id}\".", + "delete": "Deleted local version of \"{id}\"." + }, "try_again": "Try Again", "download_changes": "Download Changes", "help": "It looks like another OpenStreetMap user has changed some of the same map features that you changed.\nYou can click on each item below for more details about the conflict, and choose whether to keep\nyour changes or the other user's changes. Or, you can download your changes to a file.\n" } }, "merge_remote_changes": { - "annotation": "Merged remote changes from server.", "conflict": { - "general": "Conflicting edits were made to {type} \"{id}\" {name}", "location": "Location was changed both locally and remotely.", "nodelist": "Nodes were changed both locally and remotely.", "memberlist": "Relation members were changed both locally and remotely.", diff --git a/js/id/modes/save.js b/js/id/modes/save.js index 9d8bab811..67d93d65c 100644 --- a/js/id/modes/save.js +++ b/js/id/modes/save.js @@ -3,6 +3,13 @@ iD.modes.Save = function(context) { .on('cancel', cancel) .on('save', save); + function choice(text, actions) { + return { + text: text, + action: function() { context.perform.apply(this, actions); } + }; + } + function cancel() { context.enter(iD.modes.Browse(context)); } @@ -14,7 +21,8 @@ iD.modes.Save = function(context) { toCheck = _.pluck(history.changes().modified, 'id'), didMerge = false, conflicts = [], - errors = []; + errors = [], + confirm; context.container() .call(loading); @@ -39,14 +47,20 @@ iD.modes.Save = function(context) { if (err.status === 410) { // Status: Gone (contains no responseText) conflicts.push({ id: id, - msg: t('save.status_gone', {id: id, type: type, name: name}), - details: [ t('save.status_code', {code: err.status}) ] + msg: t('save.status_gone', { id: id, type: type, name: name }), + details: [ t('save.status_code', { code: err.status }) ], + choices: [ + choice(t('save.conflict.restore'), + [ iD.actions.Noop() /*FIXME*/, t('save.conflict.annotation.restore', {id: id}) ]), + choice(t('save.conflict.delete'), + [ iD.actions.DeleteMultiple([id]), t('save.conflict.annotation.delete', {id: id}) ]) + ] }); } else { errors.push({ id: id, msg: err.responseText, - details: [ t('save.status_code', {code: err.status}) ] + details: [ t('save.status_code', { code: err.status }) ] }); } @@ -55,16 +69,27 @@ iD.modes.Save = function(context) { var remote = altGraph.entity(id); if (local.version !== remote.version) { - var action = iD.actions.MergeRemoteChanges(id, graph, altGraph), - diff = history.perform(action); + var merge = iD.actions.MergeRemoteChanges, + safe = merge(id, graph, altGraph), + diff = context.perform(safe), + details = safe.conflicts(); if (diff.length()) { didMerge = true; } else { + var forceLocal = merge(id, graph, altGraph).withOption('force_local'), + forceRemote = merge(id, graph, altGraph).withOption('force_remote'); + conflicts.push({ id: id, - msg: t('merge_remote_changes.conflict.general', {id: id, type: type, name: name}), - details: action.conflicts() + msg: t('save.conflict.message', { id: id, type: type, name: name }), + details: details, + choices: [ + choice(t('save.conflict.keep_local'), + [ forceLocal, t('save.conflict.annotation.keep_local', {id: id}) ]), + choice(t('save.conflict.keep_remote'), + [ forceRemote, t('save.conflict.annotation.keep_remote', {id: id}) ]) + ] }); } } @@ -78,7 +103,7 @@ iD.modes.Save = function(context) { function finalize() { if (didMerge) { // set undo checkpoint.. - history.perform([iD.actions.Noop, t('merge_remote_changes.annotation')]); + context.perform(iD.actions.Noop(), t('save.conflict.annotation.safe')); } if (conflicts.length) { @@ -107,20 +132,19 @@ iD.modes.Save = function(context) { } function showConflicts() { - var confirm = iD.ui.confirm(context.container()); - + confirm = iD.ui.confirm(context.container()); loading.close(); confirm .select('.modal-section.header') .append('h3') - .text(t('save.conflicts.header')); + .text(t('save.conflict.header')); confirm .select('.modal-section.message-text') .append('div') .attr('class', 'conflicts-help') - .text(t('save.conflicts.help')); + .text(t('save.conflict.help')); addItems(confirm, conflicts); @@ -135,7 +159,7 @@ iD.modes.Save = function(context) { confirm.remove(); save(e); }) - .text(t('save.conflicts.try_again')); + .text(t('save.conflict.try_again')); buttons .append('button') @@ -152,22 +176,22 @@ iD.modes.Save = function(context) { var diff = iD.actions.DiscardTags(history.difference()), changes = history.changes(diff), data = JXON.stringify(context.connection().osmChangeJXON('CHANGEME', changes)), - win = window.open("data:text/xml," + encodeURIComponent(data), "_blank"); + win = window.open('data:text/xml,' + encodeURIComponent(data), '_blank'); win.focus(); confirm.remove(); }) - .text(t('save.conflicts.download_changes')); + .text(t('save.conflict.download_changes')); } function showErrors() { - var confirm = iD.ui.confirm(context.container()); + confirm = iD.ui.confirm(context.container()); loading.close(); confirm .select('.modal-section.header') .append('h3') - .text(t('save.errors')); + .text(t('save.error')); addItems(confirm, errors); confirm.okButton(); @@ -193,25 +217,47 @@ iD.modes.Save = function(context) { .text(function(d) { return d.msg || t('save.unknown_error_details'); }) .on('click', function() { var error = d3.select(this), - details = d3.select(this.nextElementSibling), + detail = d3.select(this.nextElementSibling), exp = error.classed('expanded'); - details.style('display', exp ? 'none' : 'block'); + detail.style('display', exp ? 'none' : 'block'); error.classed('expanded', !exp); d3.event.preventDefault(); }); - enter + var details = enter + .append('div') + .attr('class', 'error-detail-container') + .style('display', 'none'); + + details .append('ul') .attr('class', 'error-detail-list') - .style('display', 'none') .selectAll('li') - .data(function(d) { return d.details; }) + .data(function(d) { return d.details || []; }) .enter() .append('li') .attr('class', 'error-detail-item') - .text(function(d) { return d;}); + .text(function(d) { return d; }); + + details + .append('div') + .attr('class', 'error-choices') + .selectAll('a') + .data(function(d) { return d.choices || []; }) + .enter() + .append('a') + .attr('class', 'error-choice') + .text(function(d) { return d.text; }) + .on('click', function(d) { + d.action(); + d3.event.preventDefault(); + d3.select(this.parentElement.parentElement.parentElement) + .transition() + .style('opacity', 0) + .remove(); + }); items.exit() .remove();