From 97ef57ea2703aafca162bbfe78be48a24a34aeb7 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 6 Sep 2018 21:47:48 -0500 Subject: [PATCH] Conflict resolution window --- .../views/conflictResolutionModal.js | 75 +++++++++++++++++++ .../app/services/componentManager.js | 14 +++- .../javascripts/app/services/syncManager.js | 14 +++- app/assets/stylesheets/app/_modals.scss | 27 +++++++ .../conflict-resolution-modal.html.haml | 25 +++++++ 5 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/app/directives/views/conflictResolutionModal.js create mode 100644 app/assets/templates/directives/conflict-resolution-modal.html.haml diff --git a/app/assets/javascripts/app/directives/views/conflictResolutionModal.js b/app/assets/javascripts/app/directives/views/conflictResolutionModal.js new file mode 100644 index 000000000..36adce549 --- /dev/null +++ b/app/assets/javascripts/app/directives/views/conflictResolutionModal.js @@ -0,0 +1,75 @@ +/* + The purpose of the conflict resoltion modal is to present two versions of a conflicted item, + and allow the user to choose which to keep (or to keep both.) +*/ + +class ConflictResolutionModal { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/conflict-resolution-modal.html"; + this.scope = { + item1: "=", + item2: "=", + callback: "=" + }; + } + + link($scope, el, attrs) { + + $scope.dismiss = function() { + el.remove(); + } + } + + controller($scope, modelManager, syncManager) { + 'ngInject'; + + $scope.createContentString = function(item) { + return JSON.stringify( + Object.assign({created_at: item.created_at, updated_at: item.updated_at}, item.content), null, 2 + ) + } + + $scope.contentType = $scope.item1.content_type; + + $scope.item1Content = $scope.createContentString($scope.item1); + $scope.item2Content = $scope.createContentString($scope.item2); + + $scope.keepItem1 = function() { + if(!confirm("Are you sure you want to delete the item on the right?")) { + return; + } + modelManager.setItemToBeDeleted($scope.item2); + syncManager.sync().then(() => { + $scope.applyCallback(); + }) + + $scope.dismiss(); + } + + $scope.keepItem2 = function() { + if(!confirm("Are you sure you want to delete the item on the left?")) { + return; + } + modelManager.setItemToBeDeleted($scope.item1); + syncManager.sync().then(() => { + $scope.applyCallback(); + }) + + $scope.dismiss(); + } + + $scope.keepBoth = function() { + $scope.applyCallback(); + $scope.dismiss(); + } + + $scope.applyCallback = function() { + $scope.callback && $scope.callback(); + } + + } +} + +angular.module('app').directive('conflictResolutionModal', () => new ConflictResolutionModal); diff --git a/app/assets/javascripts/app/services/componentManager.js b/app/assets/javascripts/app/services/componentManager.js index 8741e4a33..734ee1780 100644 --- a/app/assets/javascripts/app/services/componentManager.js +++ b/app/assets/javascripts/app/services/componentManager.js @@ -319,6 +319,7 @@ class ComponentManager { install-local-component toggle-activate-component request-permissions + present-conflict-resolution */ if(message.action === "stream-items") { @@ -340,6 +341,8 @@ class ComponentManager { this.handleRequestPermissionsMessage(component, message); } else if(message.action === "install-local-component") { this.handleInstallLocalComponentMessage(component, message); + } else if(message.action === "present-conflict-resolution") { + this.handlePresentConflictResolutionMessage(component, message); } // Notify observers @@ -352,6 +355,7 @@ class ComponentManager { } } + removePrivatePropertiesFromResponseItems(responseItems, component, options = {}) { if(component) { // System extensions can bypass this step @@ -365,7 +369,6 @@ class ComponentManager { if(options.includeUrls) { privateProperties = privateProperties.concat(["url", "hosted_url", "local_url"])} } for(var responseItem of responseItems) { - // Do not pass in actual items here, otherwise that would be destructive. // Instead, generic JS/JSON objects should be passed. console.assert(typeof responseItem.setDirty !== 'function'); @@ -376,6 +379,15 @@ class ComponentManager { } } + handlePresentConflictResolutionMessage(component, message) { + console.log("handlePresentConflictResolutionMessage", message); + var ids = message.data.item_ids; + var items = this.modelManager.findItems(ids); + this.syncManager.presentConflictResolutionModal(items, () => { + this.replyToMessage(component, message, {}); + }); + } + handleStreamItemsMessage(component, message) { var requiredPermissions = [ { diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index eaac90668..291eaf50c 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -1,8 +1,20 @@ class SyncManager extends SFSyncManager { - constructor(modelManager, storageManager, httpManager, $timeout, $interval) { + constructor(modelManager, storageManager, httpManager, $timeout, $interval, $compile, $rootScope) { super(modelManager, storageManager, httpManager, $timeout, $interval); + this.$rootScope = $rootScope; + this.$compile = $compile; } + + presentConflictResolutionModal(items, callback) { + var scope = this.$rootScope.$new(true); + scope.item1 = items[0]; + scope.item2 = items[1]; + scope.callback = callback; + var el = this.$compile( "" )(scope); + angular.element(document.body).append(el); + } + } angular.module('app').service('syncManager', SyncManager); diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index ffe6415f4..b1909e151 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -23,6 +23,26 @@ } } +#conflict-resolution-modal { + #items { + display: flex; + height: 100%; + } + + .item { + width: 50%; + height: 100%; + flex-grow: 1; + } + + .border { + height: 100%; + background-color: rgba(black, 0.1); + width: 1px; + margin: 0 15px; + } +} + .panel { background-color: white; } @@ -61,6 +81,13 @@ } } + &.large { + > .content { + width: 900px; + height: 600px; + } + } + &.medium { > .content { width: 700px; diff --git a/app/assets/templates/directives/conflict-resolution-modal.html.haml b/app/assets/templates/directives/conflict-resolution-modal.html.haml new file mode 100644 index 000000000..e2cf28b7f --- /dev/null +++ b/app/assets/templates/directives/conflict-resolution-modal.html.haml @@ -0,0 +1,25 @@ +.modal.large#conflict-resolution-modal + .content + .sn-component + .panel + .header + %h1.title Conflicted items — choose which version to keep + .horizontal-group + %a.close-button.info{"ng-click" => "keepItem1()"} Keep left + %a.close-button.info{"ng-click" => "keepItem2()"} Keep right + %a.close-button.info{"ng-click" => "keepBoth()"} Keep both + %a.close-button.info{"ng-click" => "dismiss(); $event.stopPropagation()"} Close + .content.selectable + .panel-section + %h3 + %strong Content type: + {{contentType}} + %p You may wish to look at the "created_at" and "updated_at" fields of the items to gain better context in deciding which to keep. + #items + .panel.static#item1.item.border-color + %p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{item1Content}} + + .border + + .panel.static#item2.item + %p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{item2Content}}