Merge pull request #237 from standardnotes/conflict-wind

Conflict resolution window
This commit is contained in:
Mo Bitar
2018-09-07 11:15:25 -05:00
committed by GitHub
8 changed files with 2141 additions and 1444 deletions

View File

@@ -0,0 +1,79 @@
/*
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, archiveManager) {
'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.export = function() {
archiveManager.downloadBackupOfItems([$scope.item1, $scope.item2], true);
}
$scope.applyCallback = function() {
$scope.callback && $scope.callback();
}
}
}
angular.module('app').directive('conflictResolutionModal', () => new ConflictResolutionModal);

View File

@@ -11,6 +11,10 @@ class ArchiveManager {
*/
async downloadBackup(encrypted) {
return this.downloadBackupOfItems(this.modelManager.allItems, encrypted);
}
async downloadBackupOfItems(items, encrypted) {
// download in Standard File format
var keys, authParams;
if(encrypted) {
@@ -22,7 +26,7 @@ class ArchiveManager {
authParams = await this.authManager.getAuthParams();
}
}
this.__itemsData(keys, authParams).then((data) => {
this.__itemsData(items, keys, authParams).then((data) => {
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
// download as zipped plain text files
@@ -37,8 +41,8 @@ class ArchiveManager {
Private
*/
async __itemsData(keys, authParams) {
let data = await this.modelManager.getAllItemsJSONData(keys, authParams);
async __itemsData(items, keys, authParams) {
let data = await this.modelManager.getJSONDataForItems(items, keys, authParams);
let blobData = new Blob([data], {type: 'text/json'});
return blobData;
}

View File

@@ -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,14 @@ class ComponentManager {
}
}
handlePresentConflictResolutionMessage(component, 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 = [
{

View File

@@ -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( "<conflict-resolution-modal item1='item1' item2='item2' callback='callback' class='modal'></conflict-resolution-modal>" )(scope);
angular.element(document.body).append(el);
}
}
angular.module('app').service('syncManager', SyncManager);

View File

@@ -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;

View File

@@ -0,0 +1,26 @@
.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" => "export()"} Export
%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}}

3414
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@
"karma-jasmine": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.2",
"sn-stylekit": "1.0.15",
"standard-file-js": "0.3.14",
"standard-file-js": "0.3.15",
"sn-models": "0.1.1",
"connect": "^3.6.6",
"mocha": "^5.2.0",