Merge pull request #218 from standardnotes/flexible-content
Flexible content and session history
This commit is contained in:
@@ -70,9 +70,10 @@ module.exports = function(grunt) {
|
||||
},
|
||||
app: {
|
||||
src: [
|
||||
'node_modules/sn-models/dist/sn-models.js',
|
||||
'app/assets/javascripts/app/*.js',
|
||||
'app/assets/javascripts/app/controllers/**/*.js',
|
||||
'app/assets/javascripts/app/models/**/*.js',
|
||||
'app/assets/javascripts/app/controllers/**/*.js',
|
||||
'app/assets/javascripts/app/services/**/*.js',
|
||||
'app/assets/javascripts/app/filters/**/*.js',
|
||||
'app/assets/javascripts/app/directives/**/*.js',
|
||||
@@ -161,9 +162,9 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-babel');
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
|
||||
// grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat:app',
|
||||
// 'concat:lib', 'concat:dist', 'concat:css', 'babel', 'browserify', 'uglify']);
|
||||
|
||||
grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat:app', 'babel', 'browserify',
|
||||
'concat:lib', 'concat:dist', 'ngAnnotate', 'concat:css', 'uglify']);
|
||||
|
||||
grunt.registerTask('vendor', ['concat:app', 'babel', 'browserify',
|
||||
'concat:lib', 'concat:dist', 'ngAnnotate', 'concat:css', 'uglify']);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ angular.module('app')
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
save: "&",
|
||||
remove: "&",
|
||||
note: "=",
|
||||
updateTags: "&"
|
||||
@@ -23,8 +22,8 @@ angular.module('app')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager) {
|
||||
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) {
|
||||
|
||||
this.spellcheck = true;
|
||||
this.componentManager = componentManager;
|
||||
|
||||
@@ -36,10 +35,6 @@ angular.module('app')
|
||||
this.syncTakingTooLong = false;
|
||||
}.bind(this));
|
||||
|
||||
$rootScope.$on("tag-changed", function(){
|
||||
this.loadTagsString();
|
||||
}.bind(this));
|
||||
|
||||
// Right now this only handles offline saving status changes.
|
||||
this.syncStatusObserver = syncManager.registerSyncStatusObserver((status) => {
|
||||
if(status.localError) {
|
||||
@@ -53,7 +48,7 @@ angular.module('app')
|
||||
}
|
||||
})
|
||||
|
||||
modelManager.addItemSyncObserver("component-manager", "Note", (allItems, validItems, deletedItems, source) => {
|
||||
modelManager.addItemSyncObserver("editor-note-observer", "Note", (allItems, validItems, deletedItems, source) => {
|
||||
if(!this.note) { return; }
|
||||
|
||||
// Before checking if isMappingSourceRetrieved, we check if this item was deleted via a local source,
|
||||
@@ -64,7 +59,7 @@ angular.module('app')
|
||||
return;
|
||||
}
|
||||
|
||||
if(!ModelManager.isMappingSourceRetrieved(source)) {
|
||||
if(!SFModelManager.isMappingSourceRetrieved(source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,6 +75,37 @@ angular.module('app')
|
||||
this.loadTagsString();
|
||||
});
|
||||
|
||||
modelManager.addItemSyncObserver("editor-tag-observer", "Tag", (allItems, validItems, deletedItems, source) => {
|
||||
if(!this.note) { return; }
|
||||
|
||||
for(var tag of allItems) {
|
||||
// If a tag is deleted then we'll have lost references to notes. Reload anyway.
|
||||
if(this.note.savedTagsString == null || tag.deleted || tag.hasRelationshipWithItem(this.note)) {
|
||||
this.loadTagsString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Observe editor changes to see if the current note should update its editor
|
||||
|
||||
modelManager.addItemSyncObserver("editor-component-observer", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
if(!this.note) { return; }
|
||||
|
||||
var editors = allItems.filter(function(item) {
|
||||
return item.isEditor();
|
||||
});
|
||||
|
||||
// If no editors have changed
|
||||
if(editors.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look through editors again and find the most proper one
|
||||
var editor = this.editorForNote(this.note);
|
||||
this.selectedEditor = editor;
|
||||
});
|
||||
|
||||
this.noteDidChange = function(note, oldNote) {
|
||||
this.setNote(note, oldNote);
|
||||
this.reloadComponentContext();
|
||||
@@ -88,6 +114,7 @@ angular.module('app')
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.showExtensions = false;
|
||||
this.showMenu = false;
|
||||
this.noteStatus = null;
|
||||
this.loadTagsString();
|
||||
|
||||
let onReady = () => {
|
||||
@@ -122,32 +149,13 @@ angular.module('app')
|
||||
|
||||
if(oldNote && oldNote != note) {
|
||||
if(oldNote.hasChanges) {
|
||||
this.save()(oldNote, null);
|
||||
this.saveNote(oldNote);
|
||||
} else if(oldNote.dummy) {
|
||||
this.remove()(oldNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Observe editor changes to see if the current note should update its editor
|
||||
|
||||
modelManager.addItemSyncObserver("component-manager", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
if(!this.note) { return; }
|
||||
|
||||
var editors = allItems.filter(function(item) {
|
||||
return item.isEditor();
|
||||
});
|
||||
|
||||
// If no editors have changed
|
||||
if(editors.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Look through editors again and find the most proper one
|
||||
var editor = this.editorForNote(this.note);
|
||||
this.selectedEditor = editor;
|
||||
});
|
||||
|
||||
this.editorForNote = function(note) {
|
||||
let editors = componentManager.componentsForArea("editor-editor");
|
||||
for(var editor of editors) {
|
||||
@@ -207,7 +215,7 @@ angular.module('app')
|
||||
}
|
||||
|
||||
// Lots of dirtying can happen above, so we'll sync
|
||||
syncManager.sync("editorMenuOnSelect");
|
||||
syncManager.sync();
|
||||
}.bind(this)
|
||||
|
||||
this.hasAvailableExtensions = function() {
|
||||
@@ -235,10 +243,9 @@ angular.module('app')
|
||||
|
||||
var statusTimeout;
|
||||
|
||||
this.saveNote = function($event) {
|
||||
this.save = function(dontUpdateClientModified) {
|
||||
var note = this.note;
|
||||
note.dummy = false;
|
||||
|
||||
// Make sure the note exists. A safety measure, as toggling between tags triggers deletes for dummy notes.
|
||||
// Race conditions have been fixed, but we'll keep this here just in case.
|
||||
if(!modelManager.findItem(note.uuid)) {
|
||||
@@ -246,29 +253,51 @@ angular.module('app')
|
||||
return;
|
||||
}
|
||||
|
||||
this.save()(note, function(success){
|
||||
this.saveNote(note, (success) => {
|
||||
if(success) {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
statusTimeout = $timeout(() => {
|
||||
this.showAllChangesSavedStatus();
|
||||
}.bind(this), 200)
|
||||
}, 200)
|
||||
} else {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
statusTimeout = $timeout(() => {
|
||||
this.showErrorStatus();
|
||||
}.bind(this), 200)
|
||||
}, 200)
|
||||
}
|
||||
}.bind(this));
|
||||
}, dontUpdateClientModified);
|
||||
}
|
||||
|
||||
this.saveNote = function(note, callback, dontUpdateClientModified) {
|
||||
// We don't want to update the client modified date if toggling lock for note.
|
||||
note.setDirty(true, dontUpdateClientModified);
|
||||
|
||||
syncManager.sync().then((response) => {
|
||||
if(response && response.error) {
|
||||
if(!this.didShowErrorAlert) {
|
||||
this.didShowErrorAlert = true;
|
||||
alert("There was an error saving your note. Please try again.");
|
||||
}
|
||||
$timeout(() => {
|
||||
callback && callback(false);
|
||||
})
|
||||
} else {
|
||||
note.hasChanges = false;
|
||||
$timeout(() => {
|
||||
callback && callback(true);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.saveTitle = function($event) {
|
||||
$event.target.blur();
|
||||
this.saveNote($event);
|
||||
this.save($event);
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
var saveTimeout;
|
||||
this.changesMade = function(bypassDebouncer = false) {
|
||||
this.changesMade = function({bypassDebouncer, dontUpdateClientModified} = {}) {
|
||||
this.note.dummy = false;
|
||||
|
||||
/* In the case of keystrokes, saving should go through a debouncer to avoid frequent calls.
|
||||
@@ -283,10 +312,10 @@ angular.module('app')
|
||||
|
||||
if(saveTimeout) $timeout.cancel(saveTimeout);
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
saveTimeout = $timeout(function(){
|
||||
saveTimeout = $timeout(() => {
|
||||
this.showSavingStatus();
|
||||
this.saveNote();
|
||||
}.bind(this), delay)
|
||||
this.save(dontUpdateClientModified);
|
||||
}, delay)
|
||||
}
|
||||
|
||||
this.showSavingStatus = function() {
|
||||
@@ -317,7 +346,8 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.contentChanged = function() {
|
||||
this.changesMade();
|
||||
// content changes should bypass manual debouncer as we use the built in ng-model-options debouncer
|
||||
this.changesMade({bypassDebouncer: true});
|
||||
}
|
||||
|
||||
this.nameChanged = function() {
|
||||
@@ -358,12 +388,13 @@ angular.module('app')
|
||||
|
||||
this.toggleLockNote = function() {
|
||||
this.note.setAppDataItem("locked", !this.note.locked);
|
||||
this.changesMade();
|
||||
var dontUpdateClientModified = true;
|
||||
this.changesMade({dontUpdateClientModified});
|
||||
}
|
||||
|
||||
this.toggleArchiveNote = function() {
|
||||
this.note.setAppDataItem("archived", !this.note.archived);
|
||||
this.changesMade(true);
|
||||
this.changesMade({bypassDebouncer: true});
|
||||
$rootScope.$broadcast("noteArchived");
|
||||
}
|
||||
|
||||
@@ -518,7 +549,7 @@ angular.module('app')
|
||||
// We also check if the selectedEditor is active. If it's inactive, we want to treat it as an external reference wishing to deactivate this editor (i.e componentView)
|
||||
if(this.selectedEditor && this.selectedEditor == component && component.active == false) {
|
||||
this.selectedEditor = null;
|
||||
}
|
||||
}
|
||||
else if(this.selectedEditor) {
|
||||
if(this.selectedEditor.active) {
|
||||
if(component.isExplicitlyEnabledForItem(this.note)) {
|
||||
@@ -568,7 +599,7 @@ angular.module('app')
|
||||
|
||||
// Currently extensions are not notified of association until a full server sync completes.
|
||||
// We need a better system for this, but for now, we'll manually notify observers
|
||||
modelManager.notifySyncObserversOfModels([this.note], ModelManager.MappingSourceLocalSaved);
|
||||
modelManager.notifySyncObserversOfModels([tag], SFModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,7 +609,7 @@ angular.module('app')
|
||||
|
||||
// Currently extensions are not notified of association until a full server sync completes.
|
||||
// We need a better system for this, but for now, we'll manually notify observers
|
||||
modelManager.notifySyncObserversOfModels([this.note], ModelManager.MappingSourceLocalSaved);
|
||||
modelManager.notifySyncObserversOfModels([tag], SFModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
else if(action === "save-items" || action === "save-success" || action == "save-error") {
|
||||
|
||||
@@ -10,7 +10,7 @@ angular.module('app')
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$on("sync:updated_token", function(){
|
||||
scope.$on("sync:completed", function(){
|
||||
ctrl.syncUpdated();
|
||||
ctrl.findErrors();
|
||||
ctrl.updateOfflineStatus();
|
||||
@@ -25,7 +25,10 @@ angular.module('app')
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
|
||||
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) {
|
||||
|
||||
this.securityUpdateAvailable = authManager.checkForSecurityUpdate();
|
||||
authManager.checkForSecurityUpdate().then((available) => {
|
||||
this.securityUpdateAvailable = available;
|
||||
})
|
||||
|
||||
$rootScope.$on("security-update-status-changed", () => {
|
||||
this.securityUpdateAvailable = authManager.securityUpdateAvailable;
|
||||
})
|
||||
@@ -94,16 +97,16 @@ angular.module('app')
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync((response) => {
|
||||
syncManager.sync({force: true}).then((response) => {
|
||||
$timeout(function(){
|
||||
this.isRefreshing = false;
|
||||
}.bind(this), 200)
|
||||
if(response && response.error) {
|
||||
alert("There was an error syncing. Please try again. If all else fails, log out and log back in.");
|
||||
alert("There was an error syncing. Please try again. If all else fails, try signing out and signing back in.");
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
}, {force: true}, "refreshData");
|
||||
});
|
||||
}
|
||||
|
||||
this.syncUpdated = function() {
|
||||
@@ -133,8 +136,7 @@ angular.module('app')
|
||||
this.rooms = [];
|
||||
|
||||
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"});
|
||||
this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted});
|
||||
this.rooms = modelManager.components.filter((candidate) => {return candidate.area == "rooms" && !candidate.deleted});
|
||||
});
|
||||
|
||||
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ angular.module('app')
|
||||
|
||||
/* Used to avoid circular dependencies where syncManager cannot be imported but rootScope can */
|
||||
$rootScope.sync = function(source) {
|
||||
syncManager.sync("$rootScope.sync - " + source);
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
$rootScope.lockApplication = function() {
|
||||
@@ -66,28 +66,56 @@ angular.module('app')
|
||||
dbManager.openDatabase(null, function() {
|
||||
// new database, delete syncToken so that items can be refetched entirely from server
|
||||
syncManager.clearSyncToken();
|
||||
syncManager.sync("openDatabase");
|
||||
syncManager.sync();
|
||||
})
|
||||
}
|
||||
|
||||
function initiateSync() {
|
||||
authManager.loadInitialData();
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.allTag.didLoad = true;
|
||||
$scope.$apply();
|
||||
|
||||
$rootScope.$broadcast("initial-data-loaded");
|
||||
syncManager.setKeyRequestHandler(async () => {
|
||||
let offline = authManager.offline();
|
||||
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
|
||||
let keys = offline ? passcodeManager.keys() : await authManager.keys();
|
||||
return {
|
||||
keys: keys,
|
||||
offline: offline,
|
||||
auth_params: auth_params
|
||||
}
|
||||
});
|
||||
|
||||
syncManager.sync("initiateSync");
|
||||
syncManager.addEventHandler((syncEvent, data) => {
|
||||
$rootScope.$broadcast(syncEvent, data || {});
|
||||
|
||||
if(syncEvent == "sync-session-invalid") {
|
||||
alert("Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.");
|
||||
}
|
||||
});
|
||||
|
||||
syncManager.loadLocalItems().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.allTag.didLoad = true;
|
||||
$rootScope.$broadcast("initial-data-loaded");
|
||||
})
|
||||
|
||||
syncManager.sync();
|
||||
// refresh every 30s
|
||||
setInterval(function () {
|
||||
syncManager.sync("timer");
|
||||
syncManager.sync();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
authManager.addEventHandler((event) => {
|
||||
if(event == SFAuthManager.DidSignOutEvent) {
|
||||
modelManager.handleSignout();
|
||||
syncManager.handleSignout();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function loadAllTag() {
|
||||
var allTag = new Tag({all: true, title: "All"});
|
||||
var allTag = new SNTag({content: {title: "All"}});
|
||||
allTag.all = true;
|
||||
allTag.needsLoad = true;
|
||||
$scope.allTag = allTag;
|
||||
$scope.tags = modelManager.tags;
|
||||
@@ -95,9 +123,13 @@ angular.module('app')
|
||||
}
|
||||
|
||||
function loadArchivedTag() {
|
||||
var archiveTag = new Tag({archiveTag: true, title: "Archived"});
|
||||
var archiveTag = new SNSmartTag({content: {title: "Archived", predicate: ["archived", "=", true]}});
|
||||
Object.defineProperty(archiveTag, "notes", {
|
||||
get: () => {
|
||||
return modelManager.notesMatchingPredicate(archiveTag.content.predicate);
|
||||
}
|
||||
});
|
||||
$scope.archiveTag = archiveTag;
|
||||
$scope.archiveTag.notes = modelManager.notes;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -114,7 +146,6 @@ angular.module('app')
|
||||
}
|
||||
|
||||
for(var tagToRemove of toRemove) {
|
||||
note.removeItemAsRelationship(tagToRemove);
|
||||
tagToRemove.removeItemAsRelationship(note);
|
||||
tagToRemove.setDirty(true);
|
||||
}
|
||||
@@ -128,25 +159,27 @@ angular.module('app')
|
||||
}
|
||||
|
||||
for(var tag of tags) {
|
||||
modelManager.createRelationshipBetweenItems(note, tag);
|
||||
tag.addItemAsRelationship(note);
|
||||
tag.setDirty(true);
|
||||
}
|
||||
|
||||
note.setDirty(true);
|
||||
syncManager.sync("updateTagsForNote");
|
||||
syncManager.sync();
|
||||
}
|
||||
|
||||
/*
|
||||
Tags Ctrl Callbacks
|
||||
*/
|
||||
|
||||
|
||||
$scope.tagsWillMakeSelection = function(tag) {
|
||||
|
||||
}
|
||||
|
||||
$scope.tagsSelectionMade = function(tag) {
|
||||
// If a tag is selected twice, then the needed dummy note is removed.
|
||||
// So we perform this check.
|
||||
if($scope.selectedTag && tag && $scope.selectedTag.uuid == tag.uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($scope.selectedNote && $scope.selectedNote.dummy) {
|
||||
modelManager.removeItemLocally($scope.selectedNote);
|
||||
$scope.selectedNote = null;
|
||||
}
|
||||
|
||||
$scope.selectedTag = tag;
|
||||
@@ -162,8 +195,7 @@ angular.module('app')
|
||||
return;
|
||||
}
|
||||
tag.setDirty(true);
|
||||
syncManager.sync(callback, null, "tagsSave");
|
||||
$rootScope.$broadcast("tag-changed");
|
||||
syncManager.sync().then(callback);
|
||||
modelManager.resortTag(tag);
|
||||
}
|
||||
|
||||
@@ -174,11 +206,10 @@ angular.module('app')
|
||||
$scope.removeTag = function(tag) {
|
||||
if(confirm("Are you sure you want to delete this tag? Note: deleting a tag will not delete its notes.")) {
|
||||
modelManager.setItemToBeDeleted(tag);
|
||||
// if no more notes, delete tag
|
||||
syncManager.sync(function(){
|
||||
syncManager.sync().then(() => {
|
||||
// force scope tags to update on sub directives
|
||||
$scope.safeApply();
|
||||
}, null, "removeTag");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,8 +220,9 @@ angular.module('app')
|
||||
$scope.notesAddNew = function(note) {
|
||||
modelManager.addItem(note);
|
||||
|
||||
if(!$scope.selectedTag.all && !$scope.selectedTag.archiveTag) {
|
||||
modelManager.createRelationshipBetweenItems($scope.selectedTag, note);
|
||||
if(!$scope.selectedTag.all && !$scope.selectedTag.isSmartTag()) {
|
||||
$scope.selectedTag.addItemAsRelationship(note);
|
||||
$scope.selectedTag.setDirty(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,27 +230,6 @@ angular.module('app')
|
||||
Shared Callbacks
|
||||
*/
|
||||
|
||||
$scope.saveNote = function(note, callback) {
|
||||
note.setDirty(true);
|
||||
|
||||
syncManager.sync(function(response){
|
||||
if(response && response.error) {
|
||||
if(!$scope.didShowErrorAlert) {
|
||||
$scope.didShowErrorAlert = true;
|
||||
alert("There was an error saving your note. Please try again.");
|
||||
}
|
||||
if(callback) {
|
||||
callback(false);
|
||||
}
|
||||
} else {
|
||||
note.hasChanges = false;
|
||||
if(callback) {
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
}, null, "saveNote")
|
||||
}
|
||||
|
||||
$scope.safeApply = function(fn) {
|
||||
var phase = this.$root.$$phase;
|
||||
if(phase == '$apply' || phase == '$digest')
|
||||
@@ -234,7 +245,6 @@ angular.module('app')
|
||||
}
|
||||
|
||||
$scope.deleteNote = function(note) {
|
||||
|
||||
modelManager.setItemToBeDeleted(note);
|
||||
|
||||
if(note == $scope.selectedNote) {
|
||||
@@ -247,7 +257,7 @@ angular.module('app')
|
||||
return;
|
||||
}
|
||||
|
||||
syncManager.sync(function(){
|
||||
syncManager.sync().then(() => {
|
||||
if(authManager.offline()) {
|
||||
// when deleting items while ofline, we need to explictly tell angular to refresh UI
|
||||
setTimeout(function () {
|
||||
@@ -255,9 +265,11 @@ angular.module('app')
|
||||
$scope.safeApply();
|
||||
}, 50);
|
||||
} else {
|
||||
$rootScope.notifyDelete();
|
||||
$timeout(() => {
|
||||
$rootScope.notifyDelete();
|
||||
});
|
||||
}
|
||||
}, null, "deleteNote");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -268,25 +280,24 @@ angular.module('app')
|
||||
return $location.search()[key];
|
||||
}
|
||||
|
||||
function autoSignInFromParams() {
|
||||
async function autoSignInFromParams() {
|
||||
var server = urlParam("server");
|
||||
var email = urlParam("email");
|
||||
var pw = urlParam("pw");
|
||||
|
||||
if(!authManager.offline()) {
|
||||
// check if current account
|
||||
if(syncManager.serverURL === server && authManager.user.email === email) {
|
||||
if(await syncManager.getServerURL() === server && authManager.user.email === email) {
|
||||
// already signed in, return
|
||||
return;
|
||||
} else {
|
||||
// sign out
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
authManager.signout(true).then(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
});
|
||||
}
|
||||
} else {
|
||||
authManager.login(server, email, pw, false, false, {}, function(response){
|
||||
authManager.login(server, email, pw, false, false, {}).then((response) => {
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@ class LockScreen {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, passcodeManager, authManager, syncManager) {
|
||||
controller($scope, passcodeManager, authManager, syncManager, storageManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.submitPasscodeForm = function() {
|
||||
if(!$scope.formData.passcode || $scope.formData.passcode.length == 0) {
|
||||
return;
|
||||
}
|
||||
passcodeManager.unlock($scope.formData.passcode, (success) => {
|
||||
if(!success) {
|
||||
alert("Invalid passcode. Please try again.");
|
||||
@@ -33,8 +36,7 @@ class LockScreen {
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
authManager.signout(true).then(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ angular.module('app')
|
||||
bindToController: true,
|
||||
|
||||
link:function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch('ctrl.tag', function(tag, oldTag){
|
||||
scope.$watch('ctrl.tag', (tag, oldTag) => {
|
||||
if(tag) {
|
||||
if(tag.needsLoad) {
|
||||
scope.$watch('ctrl.tag.didLoad', function(didLoad){
|
||||
@@ -133,12 +133,14 @@ angular.module('app')
|
||||
base += " Title";
|
||||
}
|
||||
|
||||
if(this.showArchived && (!this.tag || !this.tag.archiveTag)) {
|
||||
base += " | + Archived"
|
||||
}
|
||||
|
||||
if(this.hidePinned) {
|
||||
base += " | – Pinned"
|
||||
if(!this.tag || !this.tag.isSmartTag()) {
|
||||
// These rules don't apply for smart tags
|
||||
if(this.showArchived) {
|
||||
base += " | + Archived"
|
||||
}
|
||||
if(this.hidePinned) {
|
||||
base += " | – Pinned"
|
||||
}
|
||||
}
|
||||
|
||||
return base;
|
||||
@@ -172,11 +174,11 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.setNotes = function(notes) {
|
||||
notes.forEach(function(note){
|
||||
notes.forEach((note) => {
|
||||
note.visible = true;
|
||||
})
|
||||
|
||||
var createNew = notes.length == 0;
|
||||
var createNew = this.visibleNotes().length == 0;
|
||||
this.selectFirstNote(createNew);
|
||||
}
|
||||
|
||||
@@ -201,6 +203,7 @@ angular.module('app')
|
||||
this.createNewNote();
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectedNote = note;
|
||||
note.conflict_of = null; // clear conflict
|
||||
this.selectionMade()(note);
|
||||
@@ -216,9 +219,12 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.createNewNote = function() {
|
||||
var title = "New Note" + (this.tag.notes ? (" " + (this.tag.notes.length + 1)) : "");
|
||||
this.newNote = modelManager.createItem({content_type: "Note", dummy: true, text: ""});
|
||||
this.newNote.title = title;
|
||||
// The "Note X" counter is based off this.tag.notes.length, but sometimes, what you see in the list is only a subset.
|
||||
// We can use this.visibleNotes().length, but that only accounts for non-paginated results, so first 15 or so.
|
||||
var title = "Note" + (this.tag.notes ? (" " + (this.tag.notes.length + 1)) : "");
|
||||
let newNote = modelManager.createItem({content_type: "Note", content: {text: "", title: title}});
|
||||
newNote.dummy = true;
|
||||
this.newNote = newNote;
|
||||
this.selectNote(this.newNote);
|
||||
this.addNew()(this.newNote);
|
||||
}
|
||||
@@ -226,7 +232,16 @@ angular.module('app')
|
||||
this.noteFilter = {text : ''};
|
||||
|
||||
this.filterNotes = function(note) {
|
||||
if((note.archived && !this.showArchived && !this.tag.archiveTag) || (note.pinned && this.hidePinned)) {
|
||||
var canShowArchived = false, canShowPinned = true;
|
||||
var isSmartTag = this.tag.isSmartTag();
|
||||
if(isSmartTag) {
|
||||
canShowArchived = this.tag.isReferencingArchivedNotes();
|
||||
} else {
|
||||
canShowArchived = this.showArchived;
|
||||
canShowPinned = !this.hidePinned;
|
||||
}
|
||||
|
||||
if((note.archived && !canShowArchived) || (note.pinned && !canShowPinned)) {
|
||||
note.visible = false;
|
||||
return note.visible;
|
||||
}
|
||||
@@ -241,10 +256,6 @@ angular.module('app')
|
||||
note.visible = matchesTitle || matchesBody;
|
||||
}
|
||||
|
||||
if(this.tag.archiveTag) {
|
||||
note.visible = note.visible && note.archived;
|
||||
}
|
||||
|
||||
return note.visible;
|
||||
}.bind(this)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ angular.module('app')
|
||||
scope: {
|
||||
addNew: "&",
|
||||
selectionMade: "&",
|
||||
willSelect: "&",
|
||||
save: "&",
|
||||
tags: "=",
|
||||
allTag: "=",
|
||||
@@ -66,13 +65,21 @@ angular.module('app')
|
||||
return null;
|
||||
}.bind(this), actionHandler: function(component, action, data){
|
||||
if(action === "select-item") {
|
||||
var tag = modelManager.findItem(data.item.uuid);
|
||||
if(tag) {
|
||||
if(data.item.content_type == "Tag") {
|
||||
var tag = modelManager.findItem(data.item.uuid);
|
||||
if(tag) {
|
||||
this.selectTag(tag);
|
||||
}
|
||||
} else if(data.item.content_type == "SN|SmartTag") {
|
||||
var tag = new SNSmartTag(data.item);
|
||||
Object.defineProperty(tag, "notes", {
|
||||
get: () => {
|
||||
return modelManager.notesMatchingPredicate(tag.content.predicate);
|
||||
}
|
||||
});
|
||||
this.selectTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
else if(action === "clear-selection") {
|
||||
} else if(action === "clear-selection") {
|
||||
this.selectTag(this.allTag);
|
||||
}
|
||||
}.bind(this)});
|
||||
@@ -93,7 +100,6 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.selectTag = function(tag) {
|
||||
this.willSelect()(tag);
|
||||
this.selectedTag = tag;
|
||||
tag.conflict_of = null; // clear conflict
|
||||
this.selectionMade()(tag);
|
||||
@@ -152,10 +158,11 @@ angular.module('app')
|
||||
|
||||
this.selectedDeleteTag = function(tag) {
|
||||
this.removeTag()(tag);
|
||||
this.selectTag(this.allTag);
|
||||
}
|
||||
|
||||
this.noteCount = function(tag) {
|
||||
var validNotes = Note.filterDummyNotes(tag.notes).filter(function(note){
|
||||
var validNotes = SNNote.filterDummyNotes(tag.notes).filter(function(note){
|
||||
return !note.archived;
|
||||
});
|
||||
return validNotes.length;
|
||||
|
||||
@@ -9,14 +9,23 @@ class AccountMenu {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager,
|
||||
$timeout, storageManager, $compile, archiveManager) {
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager,
|
||||
$timeout, $compile, archiveManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.formData = {mergeLocal: true, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
$scope.securityUpdateAvailable = authManager.checkForSecurityUpdate();
|
||||
|
||||
syncManager.getServerURL().then((url) => {
|
||||
$timeout(() => {
|
||||
$scope.server = url;
|
||||
$scope.formData.url = url;
|
||||
})
|
||||
})
|
||||
|
||||
authManager.checkForSecurityUpdate().then((available) => {
|
||||
$scope.securityUpdateAvailable = available;
|
||||
})
|
||||
|
||||
$scope.close = function() {
|
||||
$timeout(() => {
|
||||
@@ -59,40 +68,35 @@ class AccountMenu {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password,
|
||||
$scope.formData.ephemeral, $scope.formData.strictSignin, extraParams,
|
||||
(response) => {
|
||||
if(!response || response.error) {
|
||||
$scope.formData.ephemeral, $scope.formData.strictSignin, extraParams).then((response) => {
|
||||
$timeout(() => {
|
||||
if(!response || response.error) {
|
||||
|
||||
syncManager.unlockSyncing();
|
||||
syncManager.unlockSyncing();
|
||||
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
|
||||
// MFA Error
|
||||
if(error.tag == "mfa-required" || error.tag == "mfa-invalid") {
|
||||
$timeout(() => {
|
||||
// MFA Error
|
||||
if(error.tag == "mfa-required" || error.tag == "mfa-invalid") {
|
||||
$scope.formData.showLogin = false;
|
||||
$scope.formData.mfa = error;
|
||||
})
|
||||
}
|
||||
|
||||
// General Error
|
||||
else {
|
||||
$timeout(() => {
|
||||
}
|
||||
// General Error
|
||||
else {
|
||||
$scope.formData.showLogin = true;
|
||||
$scope.formData.mfa = null;
|
||||
if(error.message) { alert(error.message); }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success
|
||||
else {
|
||||
$scope.onAuthSuccess(() => {
|
||||
syncManager.unlockSyncing();
|
||||
syncManager.sync("onLogin");
|
||||
});
|
||||
}
|
||||
// Success
|
||||
else {
|
||||
$scope.onAuthSuccess(() => {
|
||||
syncManager.unlockSyncing();
|
||||
syncManager.sync();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -108,16 +112,18 @@ class AccountMenu {
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral ,function(response){
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
$scope.onAuthSuccess(() => {
|
||||
syncManager.sync("onRegister");
|
||||
});
|
||||
}
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral).then((response) => {
|
||||
$timeout(() => {
|
||||
if(!response || response.error) {
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
$scope.onAuthSuccess(() => {
|
||||
syncManager.sync();
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -145,8 +151,8 @@ class AccountMenu {
|
||||
$scope.clearDatabaseAndRewriteAllItems(true, block);
|
||||
}
|
||||
else {
|
||||
modelManager.resetLocalMemory();
|
||||
storageManager.clearAllModels(function(){
|
||||
modelManager.removeAllItemsFromMemory();
|
||||
storageManager.clearAllModels().then(() => {
|
||||
block();
|
||||
})
|
||||
}
|
||||
@@ -163,10 +169,10 @@ class AccountMenu {
|
||||
// clearAllModels will remove data from backing store, but not from working memory
|
||||
// See: https://github.com/standardnotes/desktop/issues/131
|
||||
$scope.clearDatabaseAndRewriteAllItems = function(alternateUuids, callback) {
|
||||
storageManager.clearAllModels(() => {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(function(){
|
||||
storageManager.clearAllModels().then(() => {
|
||||
syncManager.markAllItemsDirtyAndSaveOffline(alternateUuids).then(() => {
|
||||
callback && callback();
|
||||
}, alternateUuids)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,8 +181,7 @@ class AccountMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.signOut();
|
||||
syncManager.destroyLocalData(function(){
|
||||
authManager.signout(true).then(() => {
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
@@ -201,7 +206,8 @@ class AccountMenu {
|
||||
|
||||
// Update UI before showing alert
|
||||
setTimeout(function () {
|
||||
if(!response) {
|
||||
// Response can be null if syncing offline
|
||||
if(response && response.error) {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
} else {
|
||||
if(errorCount > 0) {
|
||||
@@ -243,24 +249,19 @@ class AccountMenu {
|
||||
}
|
||||
|
||||
$scope.importJSONData = function(data, password, callback) {
|
||||
var onDataReady = function(errorCount) {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport);
|
||||
items.forEach(function(item){
|
||||
item.setDirty(true, true);
|
||||
item.deleted = false;
|
||||
item.markAllReferencesDirty(true);
|
||||
|
||||
var onDataReady = (errorCount) => {
|
||||
var items = modelManager.importItems(data.items);
|
||||
for(var item of items) {
|
||||
// We don't want to activate any components during import process in case of exceptions
|
||||
// breaking up the import proccess
|
||||
if(item.content_type == "SN|Component") {
|
||||
item.active = false;
|
||||
}
|
||||
})
|
||||
if(item.content_type == "SN|Component") { item.active = false; }
|
||||
}
|
||||
|
||||
syncManager.sync((response) => {
|
||||
syncManager.sync({additionalFields: ["created_at", "updated_at"]}).then((response) => {
|
||||
// Response can be null if syncing offline
|
||||
callback(response, errorCount);
|
||||
}, {additionalFields: ["created_at", "updated_at"]}, "importJSONData");
|
||||
}.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
if(data.auth_params) {
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, data.auth_params).then((keys) => {
|
||||
|
||||
@@ -11,8 +11,6 @@ class ActionsMenu {
|
||||
controller($scope, modelManager, actionsManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.renderData = {};
|
||||
|
||||
$scope.extensions = actionsManager.extensions.sort((a, b) => {return a.name.toLowerCase() > b.name.toLowerCase()});
|
||||
|
||||
for(let ext of $scope.extensions) {
|
||||
@@ -51,11 +49,7 @@ class ActionsMenu {
|
||||
switch (action.verb) {
|
||||
case "render": {
|
||||
var item = response.item;
|
||||
if(item.content_type == "Note") {
|
||||
$scope.renderData.title = item.title;
|
||||
$scope.renderData.text = item.text;
|
||||
$scope.renderData.showRenderModal = true;
|
||||
}
|
||||
actionsManager.presentRevisionPreviewModal(item.uuid, item.content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,8 +71,6 @@ class ActionsMenu {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,19 +18,30 @@ class ComponentView {
|
||||
$scope.el = el;
|
||||
|
||||
$scope.identifier = "component-view-" + Math.random();
|
||||
$scope.componentValid = true;
|
||||
|
||||
// console.log("Registering handler", $scope.identifier, $scope.component.name);
|
||||
|
||||
this.componentManager.registerHandler({identifier: $scope.identifier, areas: [$scope.component.area], activationHandler: (component) => {
|
||||
// activationHandlers may be called multiple times, design below to be idempotent
|
||||
if(component.active) {
|
||||
this.timeout(() => {
|
||||
var iframe = this.componentManager.iframeForComponent(component);
|
||||
if(iframe) {
|
||||
iframe.onload = function() {
|
||||
this.componentManager.registerComponentWindow(component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}
|
||||
});
|
||||
$scope.loading = true;
|
||||
let iframe = this.componentManager.iframeForComponent(component);
|
||||
if(iframe) {
|
||||
// begin loading error handler. If onload isn't called in x seconds, display an error
|
||||
if($scope.loadTimeout) { this.timeout.cancel($scope.loadTimeout);}
|
||||
$scope.loadTimeout = this.timeout(() => {
|
||||
if($scope.loading) {
|
||||
$scope.issueLoading = true;
|
||||
}
|
||||
}, 3500)
|
||||
iframe.onload = function(event) {
|
||||
this.timeout.cancel($scope.loadTimeout);
|
||||
$scope.loading = false;
|
||||
$scope.issueLoading = false;
|
||||
this.componentManager.registerComponentWindow(component, iframe.contentWindow);
|
||||
}.bind(this);
|
||||
}
|
||||
}
|
||||
},
|
||||
actionHandler: (component, action, data) => {
|
||||
@@ -97,9 +108,9 @@ class ComponentView {
|
||||
offlineRestricted = component.offlineOnly && !isDesktopApplication();
|
||||
|
||||
urlError =
|
||||
(!isDesktopApplication() && (!component.url && !component.hosted_url))
|
||||
(!isDesktopApplication() && (!component.hasValidHostedUrl()))
|
||||
||
|
||||
(isDesktopApplication() && (!component.local_url && !component.url && !component.hosted_url))
|
||||
(isDesktopApplication() && (!component.local_url && !component.hasValidHostedUrl()))
|
||||
|
||||
expired = component.valid_until && component.valid_until <= new Date();
|
||||
|
||||
@@ -112,7 +123,8 @@ class ComponentView {
|
||||
|
||||
if($scope.componentValid !== previouslyValid) {
|
||||
if($scope.componentValid) {
|
||||
componentManager.activateComponent(component, true);
|
||||
// We want to reload here, rather than `activateComponent`, because the component will already have attempted to been activated.
|
||||
componentManager.reloadComponent(component, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +141,7 @@ class ComponentView {
|
||||
|
||||
$scope.getUrl = function() {
|
||||
var url = componentManager.urlForComponent($scope.component);
|
||||
$scope.component.runningLocally = (url !== $scope.component.url) && url !== ($scope.component.hosted_url);
|
||||
$scope.component.runningLocally = (url == $scope.component.local_url);
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ class EditorMenu {
|
||||
|
||||
component.setAppDataItem("defaultEditor", true);
|
||||
component.setDirty(true);
|
||||
syncManager.sync("makeEditorDefault");
|
||||
syncManager.sync();
|
||||
|
||||
$scope.defaultEditor = component;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class EditorMenu {
|
||||
$scope.removeEditorDefault = function(component) {
|
||||
component.setAppDataItem("defaultEditor", false);
|
||||
component.setDirty(true);
|
||||
syncManager.sync("removeEditorDefault");
|
||||
syncManager.sync();
|
||||
|
||||
$scope.defaultEditor = null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class MenuRow {
|
||||
this.scope = {
|
||||
circle: "=",
|
||||
label: "=",
|
||||
subtite: "=",
|
||||
subtitle: "=",
|
||||
hasButton: "=",
|
||||
buttonText: "=",
|
||||
buttonClass: "=",
|
||||
|
||||
@@ -147,7 +147,7 @@ class PasswordWizard {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.validateCurrentPassword = function(callback) {
|
||||
$scope.validateCurrentPassword = async function(callback) {
|
||||
let currentPassword = $scope.formData.currentPassword;
|
||||
let newPass = $scope.securityUpdate ? currentPassword : $scope.formData.newPassword;
|
||||
|
||||
@@ -173,10 +173,10 @@ class PasswordWizard {
|
||||
}
|
||||
|
||||
// Ensure value for current password matches what's saved
|
||||
let authParams = authManager.getAuthParams();
|
||||
let authParams = await authManager.getAuthParams();
|
||||
let password = $scope.formData.currentPassword;
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||
let success = keys.mk === authManager.keys().mk;
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then(async (keys) => {
|
||||
let success = keys.mk === (await authManager.keys()).mk;
|
||||
if(success) {
|
||||
this.currentServerPw = keys.pw;
|
||||
} else {
|
||||
@@ -188,8 +188,8 @@ class PasswordWizard {
|
||||
|
||||
$scope.resyncData = function(callback) {
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync((response) => {
|
||||
if(response.error) {
|
||||
syncManager.sync().then((response) => {
|
||||
if(!response || response.error) {
|
||||
alert(FailedSyncMessage)
|
||||
$timeout(() => callback(false));
|
||||
} else {
|
||||
@@ -198,27 +198,25 @@ class PasswordWizard {
|
||||
});
|
||||
}
|
||||
|
||||
$scope.processPasswordChange = function(callback) {
|
||||
$scope.processPasswordChange = async function(callback) {
|
||||
let newUserPassword = $scope.securityUpdate ? $scope.formData.currentPassword : $scope.formData.newPassword;
|
||||
|
||||
let currentServerPw = this.currentServerPw;
|
||||
|
||||
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(authManager.user.email, newUserPassword).then((results) => {
|
||||
let newKeys = results.keys;
|
||||
let newAuthParams = results.authParams;
|
||||
let results = await SFJS.crypto.generateInitialKeysAndAuthParamsForUser(authManager.user.email, newUserPassword);
|
||||
let newKeys = results.keys;
|
||||
let newAuthParams = results.authParams;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync((response) => {
|
||||
authManager.changePassword(currentServerPw, newKeys, newAuthParams, (response) => {
|
||||
if(response.error) {
|
||||
alert(response.error.message ? response.error.message : "There was an error changing your password. Please try again.");
|
||||
$timeout(() => callback(false));
|
||||
} else {
|
||||
$timeout(() => callback(true));
|
||||
}
|
||||
})
|
||||
}, null, "submitPasswordChange")
|
||||
});
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
let syncResponse = await syncManager.sync();
|
||||
authManager.changePassword(await syncManager.getServerURL(), authManager.user.email, currentServerPw, newKeys, newAuthParams).then((response) => {
|
||||
if(response.error) {
|
||||
alert(response.error.message ? response.error.message : "There was an error changing your password. Please try again.");
|
||||
$timeout(() => callback(false));
|
||||
} else {
|
||||
$timeout(() => callback(true));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
class RevisionPreviewModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/revision-preview-modal.html";
|
||||
this.scope = {
|
||||
uuid: "=",
|
||||
content: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
|
||||
$scope.dismiss = function() {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
controller($scope, modelManager, syncManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.restore = function(asCopy) {
|
||||
if(!asCopy && !confirm("Are you sure you want to replace the current note's contents with what you see in this preview?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item;
|
||||
if(asCopy) {
|
||||
var contentCopy = Object.assign({}, $scope.content);
|
||||
if(contentCopy.title) { contentCopy.title += " (copy)"; }
|
||||
item = modelManager.createItem({content_type: "Note", content: contentCopy});
|
||||
modelManager.addItem(item);
|
||||
} else {
|
||||
var uuid = $scope.uuid;
|
||||
item = modelManager.findItem(uuid);
|
||||
item.content = Object.assign({}, $scope.content);
|
||||
modelManager.mapResponseItemsToLocalModels([item], SFModelManager.MappingSourceRemoteActionRetrieved);
|
||||
}
|
||||
|
||||
item.setDirty(true);
|
||||
syncManager.sync();
|
||||
|
||||
$scope.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').directive('revisionPreviewModal', () => new RevisionPreviewModal);
|
||||
@@ -0,0 +1,81 @@
|
||||
class SessionHistoryMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/session-history-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, sessionHistory, actionsManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
||||
$scope.autoOptimize = sessionHistory.autoOptimize;
|
||||
|
||||
$scope.reloadHistory = function() {
|
||||
$scope.history = sessionHistory.historyForItem($scope.item);
|
||||
}
|
||||
$scope.reloadHistory();
|
||||
|
||||
$scope.openRevision = function(revision) {
|
||||
actionsManager.presentRevisionPreviewModal(revision.item.uuid, revision.item.content);
|
||||
}
|
||||
|
||||
$scope.classForRevision = function(revision) {
|
||||
var vector = revision.operationVector();
|
||||
if(vector == 0) {
|
||||
return "default";
|
||||
} else if(vector == 1) {
|
||||
return "success";
|
||||
} else if(vector == -1) {
|
||||
return "danger";
|
||||
}
|
||||
}
|
||||
|
||||
$scope.clearItemHistory = function() {
|
||||
if(!confirm("Are you sure you want to delete the local session history for this note?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionHistory.clearItemHistory($scope.item).then(() => {
|
||||
$timeout(() => {
|
||||
$scope.reloadHistory();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.clearAllHistory = function() {
|
||||
if(!confirm("Are you sure you want to delete the local session history for all notes?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionHistory.clearAllHistory().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.reloadHistory();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggleDiskSaving = function() {
|
||||
sessionHistory.toggleDiskSaving().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.diskEnabled = sessionHistory.diskEnabled;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
$scope.toggleAutoOptimize = function() {
|
||||
sessionHistory.toggleAutoOptimize().then(() => {
|
||||
$timeout(() => {
|
||||
$scope.autoOptimize = sessionHistory.autoOptimize;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('sessionHistoryMenu', () => new SessionHistoryMenu);
|
||||
@@ -1,3 +1,6 @@
|
||||
// reuse
|
||||
var locale, formatter;
|
||||
|
||||
angular.module('app')
|
||||
.filter('appDate', function ($filter) {
|
||||
return function (input) {
|
||||
@@ -6,6 +9,20 @@ angular.module('app')
|
||||
})
|
||||
.filter('appDateTime', function ($filter) {
|
||||
return function (input) {
|
||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||
if (!formatter) {
|
||||
locale = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;
|
||||
formatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
return formatter.format(input);
|
||||
} else {
|
||||
return input ? $filter('date')(new Date(input), 'MM/dd/yyyy h:mm a') : '';
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,8 +35,9 @@ angular.module('app')
|
||||
}
|
||||
|
||||
items = items || [];
|
||||
return items.sort(function(a, b){
|
||||
var result = items.sort(function(a, b){
|
||||
return sortValueFn(a, b);
|
||||
})
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,317 +0,0 @@
|
||||
let AppDomain = "org.standardnotes.sn";
|
||||
var dateFormatter;
|
||||
|
||||
class Item {
|
||||
|
||||
constructor(json_obj = {}) {
|
||||
this.appData = {};
|
||||
this.updateFromJSON(json_obj);
|
||||
this.observers = [];
|
||||
|
||||
if(!this.uuid) {
|
||||
this.uuid = SFJS.crypto.generateUUIDSync();
|
||||
}
|
||||
}
|
||||
|
||||
static sortItemsByDate(items) {
|
||||
items.sort(function(a,b){
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
});
|
||||
}
|
||||
|
||||
get contentObject() {
|
||||
if(!this.content) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if(this.content !== null && typeof this.content === 'object') {
|
||||
// this is the case when mapping localStorage content, in which case the content is already parsed
|
||||
return this.content;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(this.content);
|
||||
} catch (e) {
|
||||
console.log("Error parsing json", e, this);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJSON(json) {
|
||||
_.merge(this, json);
|
||||
|
||||
if(this.created_at) {
|
||||
this.created_at = new Date(this.created_at);
|
||||
this.updated_at = new Date(this.updated_at);
|
||||
} else {
|
||||
this.created_at = new Date();
|
||||
this.updated_at = new Date();
|
||||
}
|
||||
|
||||
// Allows the getter to be re-invoked
|
||||
this._client_updated_at = null;
|
||||
|
||||
if(json.content) {
|
||||
this.mapContentToLocalProperties(this.contentObject);
|
||||
} else if(json.deleted == true) {
|
||||
this.handleDeletedContent();
|
||||
}
|
||||
}
|
||||
|
||||
refreshContentObject() {
|
||||
// Before an item can be duplicated or cloned, we must update this.content (if it is an object) with the object's
|
||||
// current physical properties, because updateFromJSON, which is what all new items must go through,
|
||||
// will call this.mapContentToLocalProperties(this.contentObject), which may have stale values if not explicitly updated.
|
||||
|
||||
this.content = this.structureParams();
|
||||
}
|
||||
|
||||
/* Allows the item to handle the case where the item is deleted and the content is null */
|
||||
handleDeletedContent() {
|
||||
// Subclasses can override
|
||||
}
|
||||
|
||||
setDirty(dirty, dontUpdateClientDate) {
|
||||
this.dirty = dirty;
|
||||
|
||||
// Allows the syncManager to check if an item has been marked dirty after a sync has been started
|
||||
// This prevents it from clearing it as a dirty item after sync completion, if someone else has marked it dirty
|
||||
// again after an ongoing sync.
|
||||
if(!this.dirtyCount) { this.dirtyCount = 0; }
|
||||
if(dirty) {
|
||||
this.dirtyCount++;
|
||||
} else {
|
||||
this.dirtyCount = 0;
|
||||
}
|
||||
|
||||
if(dirty && !dontUpdateClientDate) {
|
||||
// Set the client modified date to now if marking the item as dirty
|
||||
this.client_updated_at = new Date();
|
||||
} else if(!this.hasRawClientUpdatedAtValue()) {
|
||||
// copy updated_at
|
||||
this.client_updated_at = new Date(this.updated_at);
|
||||
}
|
||||
|
||||
if(dirty) {
|
||||
this.notifyObserversOfChange();
|
||||
}
|
||||
}
|
||||
|
||||
markAllReferencesDirty(dontUpdateClientDate) {
|
||||
this.allReferencedObjects().forEach(function(reference){
|
||||
reference.setDirty(true, dontUpdateClientDate);
|
||||
})
|
||||
}
|
||||
|
||||
addObserver(observer, callback) {
|
||||
if(!_.find(this.observers, observer)) {
|
||||
this.observers.push({observer: observer, callback: callback});
|
||||
}
|
||||
}
|
||||
|
||||
removeObserver(observer) {
|
||||
_.remove(this.observers, {observer: observer})
|
||||
}
|
||||
|
||||
notifyObserversOfChange() {
|
||||
for(var observer of this.observers) {
|
||||
observer.callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObj) {
|
||||
if(contentObj.appData) {
|
||||
this.appData = contentObj.appData;
|
||||
}
|
||||
if(!this.appData) { this.appData = {}; }
|
||||
}
|
||||
|
||||
createContentJSONFromProperties() {
|
||||
return this.structureParams();
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
// must override
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
return {
|
||||
references: this.referenceParams(),
|
||||
appData: this.appData
|
||||
}
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
// must override
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
// must override
|
||||
}
|
||||
|
||||
isBeingRemovedLocally() {
|
||||
|
||||
}
|
||||
|
||||
removeAndDirtyAllRelationships() {
|
||||
// must override
|
||||
this.setDirty(true);
|
||||
}
|
||||
|
||||
removeReferencesNotPresentIn(references) {
|
||||
|
||||
}
|
||||
|
||||
mergeMetadataFromItem(item) {
|
||||
_.merge(this, _.omit(item, ["content"]));
|
||||
}
|
||||
|
||||
informReferencesOfUUIDChange(oldUUID, newUUID) {
|
||||
// optional override
|
||||
}
|
||||
|
||||
potentialItemOfInterestHasChangedItsUUID(newItem, oldUUID, newUUID) {
|
||||
// optional override
|
||||
}
|
||||
|
||||
allReferencedObjects() {
|
||||
// must override
|
||||
return [];
|
||||
}
|
||||
|
||||
doNotEncrypt() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
App Data
|
||||
*/
|
||||
|
||||
setDomainDataItem(key, value, domain) {
|
||||
var data = this.appData[domain];
|
||||
if(!data) {
|
||||
data = {}
|
||||
}
|
||||
data[key] = value;
|
||||
this.appData[domain] = data;
|
||||
}
|
||||
|
||||
getDomainDataItem(key, domain) {
|
||||
var data = this.appData[domain];
|
||||
if(data) {
|
||||
return data[key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setAppDataItem(key, value) {
|
||||
this.setDomainDataItem(key, value, AppDomain);
|
||||
}
|
||||
|
||||
getAppDataItem(key) {
|
||||
return this.getDomainDataItem(key, AppDomain);
|
||||
}
|
||||
|
||||
get pinned() {
|
||||
return this.getAppDataItem("pinned");
|
||||
}
|
||||
|
||||
get archived() {
|
||||
return this.getAppDataItem("archived");
|
||||
}
|
||||
|
||||
get locked() {
|
||||
return this.getAppDataItem("locked");
|
||||
}
|
||||
|
||||
hasRawClientUpdatedAtValue() {
|
||||
return this.getAppDataItem("client_updated_at") != null;
|
||||
}
|
||||
|
||||
get client_updated_at() {
|
||||
if(!this._client_updated_at) {
|
||||
var saved = this.getAppDataItem("client_updated_at");
|
||||
if(saved) {
|
||||
this._client_updated_at = new Date(saved);
|
||||
} else {
|
||||
this._client_updated_at = new Date(this.updated_at);
|
||||
}
|
||||
}
|
||||
return this._client_updated_at;
|
||||
}
|
||||
|
||||
set client_updated_at(date) {
|
||||
this._client_updated_at = date;
|
||||
|
||||
this.setAppDataItem("client_updated_at", date);
|
||||
}
|
||||
|
||||
/*
|
||||
During sync conflicts, when determing whether to create a duplicate for an item, we can omit keys that have no
|
||||
meaningful weight and can be ignored. For example, if one component has active = true and another component has active = false,
|
||||
it would be silly to duplicate them, so instead we ignore this.
|
||||
*/
|
||||
keysToIgnoreWhenCheckingContentEquality() {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Same as above, but keys inside appData[AppDomain]
|
||||
appDataKeysToIgnoreWhenCheckingContentEquality() {
|
||||
return ["client_updated_at"];
|
||||
}
|
||||
|
||||
isItemContentEqualWith(otherItem) {
|
||||
let omit = (obj, keys) => {
|
||||
for(var key of keys) {
|
||||
delete obj[key];
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
var left = this.structureParams();
|
||||
left.appData[AppDomain] = omit(left.appData[AppDomain], this.appDataKeysToIgnoreWhenCheckingContentEquality());
|
||||
left = omit(left, this.keysToIgnoreWhenCheckingContentEquality());
|
||||
|
||||
var right = otherItem.structureParams();
|
||||
right.appData[AppDomain] = omit(right.appData[AppDomain], otherItem.appDataKeysToIgnoreWhenCheckingContentEquality());
|
||||
right = omit(right, otherItem.keysToIgnoreWhenCheckingContentEquality());
|
||||
|
||||
return JSON.stringify(left) === JSON.stringify(right)
|
||||
}
|
||||
|
||||
/*
|
||||
Dates
|
||||
*/
|
||||
|
||||
createdAtString() {
|
||||
return this.dateToLocalizedString(this.created_at);
|
||||
}
|
||||
|
||||
updatedAtString() {
|
||||
return this.dateToLocalizedString(this.client_updated_at);
|
||||
}
|
||||
|
||||
dateToLocalizedString(date) {
|
||||
if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
|
||||
if (!dateFormatter) {
|
||||
var locale = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;
|
||||
dateFormatter = new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
weekday: 'long',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
return dateFormatter.format(date);
|
||||
} else {
|
||||
// IE < 11, Safari <= 9.0.
|
||||
// In English, this generates the string most similar to
|
||||
// the toLocaleDateString() result above.
|
||||
return date.toDateString() + ' ' + date.toLocaleTimeString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
class Mfa extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.serverContent = content;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
return _.merge(this.serverContent, super.structureParams());
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SF|MFA";
|
||||
}
|
||||
|
||||
doNotEncrypt() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
class ServerExtension extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.url = content.url;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
// There was a bug with the way Base64 content was parsed in previous releases related to this item.
|
||||
// The bug would not parse the JSON behind the base64 string and thus saved data in an invalid format.
|
||||
// This is the line: https://github.com/standardnotes/web/commit/1ad0bf73d8e995b7588854f1b1e4e4a02303a42f#diff-15753bac364782a3a5876032bcdbf99aR76
|
||||
// We'll remedy this for affected users by trying to parse the content string
|
||||
if(typeof this.content !== 'object') {
|
||||
try {
|
||||
this.content = JSON.parse(this.content);
|
||||
} catch (e) {}
|
||||
}
|
||||
var params = this.content || {};
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SF|Extension";
|
||||
}
|
||||
|
||||
doNotEncrypt() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
class Component extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
// If making a copy of an existing component (usually during sign in if you have a component active in the session),
|
||||
// which may have window set, you may get a cross-origin exception since you'll be trying to copy the window. So we clear it here.
|
||||
json_obj.window = null;
|
||||
|
||||
super(json_obj);
|
||||
|
||||
if(!this.componentData) {
|
||||
this.componentData = {};
|
||||
}
|
||||
|
||||
if(!this.disassociatedItemIds) {
|
||||
this.disassociatedItemIds = [];
|
||||
}
|
||||
|
||||
if(!this.associatedItemIds) {
|
||||
this.associatedItemIds = [];
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
/* Legacy */
|
||||
this.url = content.url || content.hosted_url;
|
||||
/* New */
|
||||
this.local_url = content.local_url;
|
||||
this.hosted_url = content.hosted_url || content.url;
|
||||
this.offlineOnly = content.offlineOnly;
|
||||
|
||||
if(content.valid_until) {
|
||||
this.valid_until = new Date(content.valid_until);
|
||||
}
|
||||
|
||||
this.name = content.name;
|
||||
this.autoupdateDisabled = content.autoupdateDisabled;
|
||||
|
||||
this.package_info = content.package_info;
|
||||
|
||||
// the location in the view this component is located in. Valid values are currently tags-list, note-tags, and editor-stack`
|
||||
this.area = content.area;
|
||||
|
||||
this.permissions = content.permissions;
|
||||
if(!this.permissions) {
|
||||
this.permissions = [];
|
||||
}
|
||||
|
||||
this.active = content.active;
|
||||
|
||||
// custom data that a component can store in itself
|
||||
this.componentData = content.componentData || {};
|
||||
|
||||
// items that have requested a component to be disabled in its context
|
||||
this.disassociatedItemIds = content.disassociatedItemIds || [];
|
||||
|
||||
// items that have requested a component to be enabled in its context
|
||||
this.associatedItemIds = content.associatedItemIds || [];
|
||||
}
|
||||
|
||||
handleDeletedContent() {
|
||||
super.handleDeletedContent();
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
url: this.url,
|
||||
hosted_url: this.hosted_url,
|
||||
local_url: this.local_url,
|
||||
valid_until: this.valid_until,
|
||||
offlineOnly: this.offlineOnly,
|
||||
name: this.name,
|
||||
area: this.area,
|
||||
package_info: this.package_info,
|
||||
permissions: this.permissions,
|
||||
active: this.active,
|
||||
autoupdateDisabled: this.autoupdateDisabled,
|
||||
componentData: this.componentData,
|
||||
disassociatedItemIds: this.disassociatedItemIds,
|
||||
associatedItemIds: this.associatedItemIds,
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Component";
|
||||
}
|
||||
|
||||
isEditor() {
|
||||
return this.area == "editor-editor";
|
||||
}
|
||||
|
||||
isTheme() {
|
||||
return this.content_type == "SN|Theme" || this.area == "themes";
|
||||
}
|
||||
|
||||
isDefaultEditor() {
|
||||
return this.getAppDataItem("defaultEditor") == true;
|
||||
}
|
||||
|
||||
setLastSize(size) {
|
||||
this.setAppDataItem("lastSize", size);
|
||||
}
|
||||
|
||||
getLastSize() {
|
||||
return this.getAppDataItem("lastSize");
|
||||
}
|
||||
|
||||
keysToIgnoreWhenCheckingContentEquality() {
|
||||
return ["active"].concat(super.keysToIgnoreWhenCheckingContentEquality());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
An associative component depends on being explicitly activated for a given item, compared to a dissaciative component,
|
||||
which is enabled by default in areas unrelated to a certain item.
|
||||
*/
|
||||
static associativeAreas() {
|
||||
return ["editor-editor"];
|
||||
}
|
||||
|
||||
isAssociative() {
|
||||
return Component.associativeAreas().includes(this.area);
|
||||
}
|
||||
|
||||
associateWithItem(item) {
|
||||
this.associatedItemIds.push(item.uuid);
|
||||
}
|
||||
|
||||
isExplicitlyEnabledForItem(item) {
|
||||
return this.associatedItemIds.indexOf(item.uuid) !== -1;
|
||||
}
|
||||
|
||||
isExplicitlyDisabledForItem(item) {
|
||||
return this.disassociatedItemIds.indexOf(item.uuid) !== -1;
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
class Editor extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
if(!this.notes) {
|
||||
this.notes = [];
|
||||
}
|
||||
if(!this.data) {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.url = content.url;
|
||||
this.name = content.name;
|
||||
this.data = content.data || {};
|
||||
this.default = content.default;
|
||||
this.systemEditor = content.systemEditor;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
url: this.url,
|
||||
name: this.name,
|
||||
data: this.data,
|
||||
default: this.default,
|
||||
systemEditor: this.systemEditor
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
var references = _.map(this.notes, function(note){
|
||||
return {uuid: note.uuid, content_type: note.content_type};
|
||||
})
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
if(!_.find(this.notes, item)) {
|
||||
this.notes.push(item);
|
||||
}
|
||||
}
|
||||
super.addItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
_.pull(this.notes, item);
|
||||
}
|
||||
super.removeItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeAndDirtyAllRelationships() {
|
||||
super.removeAndDirtyAllRelationships();
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
removeReferencesNotPresentIn(references) {
|
||||
super.removeReferencesNotPresentIn(references);
|
||||
|
||||
var uuids = references.map(function(ref){return ref.uuid});
|
||||
this.notes.forEach(function(note){
|
||||
if(!uuids.includes(note.uuid)) {
|
||||
_.pull(this.notes, note);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
potentialItemOfInterestHasChangedItsUUID(newItem, oldUUID, newUUID) {
|
||||
if(newItem.content_type === "Note" && _.find(this.notes, {uuid: oldUUID})) {
|
||||
_.pull(this.notes, {uuid: oldUUID});
|
||||
this.notes.push(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
allReferencedObjects() {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Editor";
|
||||
}
|
||||
|
||||
setData(key, value) {
|
||||
var dataHasChanged = JSON.stringify(this.data[key]) !== JSON.stringify(value);
|
||||
if(dataHasChanged) {
|
||||
this.data[key] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dataForKey(key) {
|
||||
return this.data[key] || {};
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
class Action {
|
||||
constructor(json) {
|
||||
_.merge(this, json);
|
||||
this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory
|
||||
this.error = false;
|
||||
if(this.lastExecuted) {
|
||||
// is string
|
||||
this.lastExecuted = new Date(this.lastExecuted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Extension extends Component {
|
||||
constructor(json) {
|
||||
super(json);
|
||||
|
||||
if(json.actions) {
|
||||
this.actions = json.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
|
||||
if(!this.actions) {
|
||||
this.actions = [];
|
||||
}
|
||||
}
|
||||
|
||||
actionsWithContextForItem(item) {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == item.content_type || action.context == "Item";
|
||||
})
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.description = content.description;
|
||||
|
||||
this.supported_types = content.supported_types;
|
||||
if(content.actions) {
|
||||
this.actions = content.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Extension";
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
description: this.description,
|
||||
actions: this.actions.map((a) => {return _.omit(a, ["subrows", "subactions"])}),
|
||||
supported_types: this.supported_types
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
class Note extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
|
||||
if(!this.text) {
|
||||
// Some external editors can't handle a null value for text.
|
||||
// Notes created on mobile with no text have a null value for it,
|
||||
// so we'll just set a default here.
|
||||
this.text = "";
|
||||
}
|
||||
|
||||
if(!this.tags) {
|
||||
this.tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.title = content.title;
|
||||
this.text = content.text;
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
var references = _.map(this.tags, function(tag){
|
||||
return {uuid: tag.uuid, content_type: tag.content_type};
|
||||
})
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
title: this.title,
|
||||
text: this.text
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
this.savedTagsString = null;
|
||||
|
||||
if(item.content_type == "Tag") {
|
||||
if(!_.find(this.tags, item)) {
|
||||
this.tags.push(item);
|
||||
}
|
||||
}
|
||||
super.addItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
this.savedTagsString = null;
|
||||
|
||||
if(item.content_type == "Tag") {
|
||||
_.pull(this.tags, item);
|
||||
}
|
||||
super.removeItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeAndDirtyAllRelationships() {
|
||||
this.savedTagsString = null;
|
||||
|
||||
this.tags.forEach(function(tag){
|
||||
_.pull(tag.notes, this);
|
||||
tag.setDirty(true);
|
||||
}.bind(this))
|
||||
this.tags = [];
|
||||
}
|
||||
|
||||
removeReferencesNotPresentIn(references) {
|
||||
this.savedTagsString = null;
|
||||
|
||||
super.removeReferencesNotPresentIn(references);
|
||||
|
||||
var uuids = references.map(function(ref){return ref.uuid});
|
||||
this.tags.slice().forEach(function(tag){
|
||||
if(!uuids.includes(tag.uuid)) {
|
||||
_.pull(tag.notes, this);
|
||||
_.pull(this.tags, tag);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
isBeingRemovedLocally() {
|
||||
this.tags.forEach(function(tag){
|
||||
_.pull(tag.notes, this);
|
||||
}.bind(this))
|
||||
super.isBeingRemovedLocally();
|
||||
}
|
||||
|
||||
static filterDummyNotes(notes) {
|
||||
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
|
||||
return filtered;
|
||||
}
|
||||
|
||||
informReferencesOfUUIDChange(oldUUID, newUUID) {
|
||||
for(var tag of this.tags) {
|
||||
_.pull(tag.notes, {uuid: oldUUID});
|
||||
tag.notes.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
allReferencedObjects() {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
safeText() {
|
||||
return this.text || "";
|
||||
}
|
||||
|
||||
safeTitle() {
|
||||
return this.title || "";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Note";
|
||||
}
|
||||
|
||||
tagsString() {
|
||||
this.savedTagsString = Tag.arrayToDisplayString(this.tags);
|
||||
return this.savedTagsString;
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
class Tag extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
|
||||
if(!this.notes) {
|
||||
this.notes = [];
|
||||
}
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.title = content.title;
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
var references = _.map(this.notes, function(note){
|
||||
return {uuid: note.uuid, content_type: note.content_type};
|
||||
})
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
title: this.title
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
if(!_.find(this.notes, item)) {
|
||||
this.notes.unshift(item);
|
||||
}
|
||||
}
|
||||
super.addItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
if(item.content_type == "Note") {
|
||||
_.pull(this.notes, item);
|
||||
}
|
||||
super.removeItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeAndDirtyAllRelationships() {
|
||||
this.notes.forEach(function(note){
|
||||
_.pull(note.tags, this);
|
||||
note.setDirty(true);
|
||||
}.bind(this))
|
||||
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
removeReferencesNotPresentIn(references) {
|
||||
var uuids = references.map(function(ref){return ref.uuid});
|
||||
this.notes.slice().forEach(function(note){
|
||||
if(!uuids.includes(note.uuid)) {
|
||||
_.pull(note.tags, this);
|
||||
_.pull(this.notes, note);
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
isBeingRemovedLocally() {
|
||||
this.notes.forEach(function(note){
|
||||
_.pull(note.tags, this);
|
||||
}.bind(this))
|
||||
super.isBeingRemovedLocally();
|
||||
}
|
||||
|
||||
informReferencesOfUUIDChange(oldUUID, newUUID) {
|
||||
for(var note of this.notes) {
|
||||
_.pull(note.tags, {uuid: oldUUID});
|
||||
note.tags.push(this);
|
||||
}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Tag";
|
||||
}
|
||||
|
||||
allReferencedObjects() {
|
||||
return this.notes;
|
||||
}
|
||||
|
||||
static arrayToDisplayString(tags) {
|
||||
return tags.sort((a, b) => {return a.title > b.title}).map(function(tag, i){
|
||||
return "#" + tag.title;
|
||||
}).join(" ");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
class Theme extends Component {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
|
||||
this.area = "themes";
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|Theme";
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
class EncryptedStorage extends Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
super(json_obj);
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(content) {
|
||||
super.mapContentToLocalProperties(content)
|
||||
this.storage = content.storage;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
storage: this.storage,
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "SN|EncryptedStorage";
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
class ItemParams {
|
||||
|
||||
constructor(item, keys, version) {
|
||||
this.item = item;
|
||||
this.keys = keys;
|
||||
this.version = version || SFJS.version();
|
||||
}
|
||||
|
||||
async paramsForExportFile(includeDeleted) {
|
||||
this.additionalFields = ["updated_at"];
|
||||
this.forExportFile = true;
|
||||
if(includeDeleted) {
|
||||
return this.__params();
|
||||
} else {
|
||||
var result = await this.__params();
|
||||
return _.omit(result, ["deleted"]);
|
||||
}
|
||||
}
|
||||
|
||||
async paramsForExtension() {
|
||||
return this.paramsForExportFile();
|
||||
}
|
||||
|
||||
async paramsForLocalStorage() {
|
||||
this.additionalFields = ["updated_at", "dirty", "errorDecrypting"];
|
||||
this.forExportFile = true;
|
||||
return this.__params();
|
||||
}
|
||||
|
||||
async paramsForSync() {
|
||||
return this.__params();
|
||||
}
|
||||
|
||||
async __params() {
|
||||
|
||||
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
||||
|
||||
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
|
||||
if(!this.item.errorDecrypting) {
|
||||
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params.
|
||||
var doNotEncrypt = this.item.doNotEncrypt() && !this.forExportFile;
|
||||
if(this.keys && !doNotEncrypt) {
|
||||
var encryptedParams = await SFJS.itemTransformer.encryptItem(this.item, this.keys, this.version);
|
||||
_.merge(params, encryptedParams);
|
||||
|
||||
if(this.version !== "001") {
|
||||
params.auth_hash = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + await SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
|
||||
if(!this.forExportFile) {
|
||||
params.enc_item_key = null;
|
||||
params.auth_hash = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Error decrypting, keep "content" and related fields as is (and do not try to encrypt, otherwise that would be undefined behavior)
|
||||
params.content = this.item.content;
|
||||
params.enc_item_key = this.item.enc_item_key;
|
||||
params.auth_hash = this.item.auth_hash;
|
||||
}
|
||||
|
||||
if(this.additionalFields) {
|
||||
_.merge(params, _.pick(this.item, this.additionalFields));
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
app/assets/javascripts/app/models/noteHistoryEntry.js
Normal file
18
app/assets/javascripts/app/models/noteHistoryEntry.js
Normal file
@@ -0,0 +1,18 @@
|
||||
class NoteHistoryEntry extends SFItemHistoryEntry {
|
||||
|
||||
previewTitle() {
|
||||
return this.item.updated_at.toLocaleString();
|
||||
}
|
||||
|
||||
previewSubTitle() {
|
||||
if(!this.hasPreviousEntry) {
|
||||
return `${this.textCharDiffLength} characters loaded`
|
||||
} else if(this.textCharDiffLength < 0) {
|
||||
return `${this.textCharDiffLength * -1} characters removed`
|
||||
} else if(this.textCharDiffLength > 0) {
|
||||
return `${this.textCharDiffLength} characters added`
|
||||
} else {
|
||||
return "Title changed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class ActionsManager {
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
return this.modelManager.validItemsForContentType("Extension");
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
@@ -74,11 +74,11 @@ class ActionsManager {
|
||||
|
||||
if(!item.errorDecrypting) {
|
||||
if(merge) {
|
||||
var items = this.modelManager.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
var items = this.modelManager.mapResponseItemsToLocalModels([item], SFModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var mappedItem of items) {
|
||||
mappedItem.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
this.syncManager.sync();
|
||||
customCallback({item: item});
|
||||
} else {
|
||||
item = this.modelManager.createItem(item, true /* Dont notify observers */);
|
||||
@@ -122,9 +122,9 @@ class ActionsManager {
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, this.authManager.keys(), true);
|
||||
handleResponseDecryption(response, await this.authManager.keys(), true);
|
||||
}, (response) => {
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
@@ -134,9 +134,9 @@ class ActionsManager {
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||
this.httpManager.getAbsolute(action.url, {}, async (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, this.authManager.keys(), false);
|
||||
handleResponseDecryption(response, await this.authManager.keys(), false);
|
||||
}, (response) => {
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
@@ -175,11 +175,11 @@ class ActionsManager {
|
||||
}
|
||||
|
||||
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
var keys = this.authManager.keys();
|
||||
var keys = await this.authManager.keys();
|
||||
if(decrypted) {
|
||||
keys = null;
|
||||
}
|
||||
var itemParams = new ItemParams(item, keys, this.authManager.protocolVersion());
|
||||
var itemParams = new SFItemParams(item, keys, await this.authManager.getAuthParams());
|
||||
return itemParams.paramsForExtension();
|
||||
}
|
||||
|
||||
@@ -198,8 +198,15 @@ class ActionsManager {
|
||||
})
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
presentRevisionPreviewModal(uuid, content) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.uuid = uuid;
|
||||
scope.content = content;
|
||||
var el = this.$compile( "<revision-preview-modal uuid='uuid' content='content' class='modal'></revision-preview-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
|
||||
@@ -10,21 +10,19 @@ class ArchiveManager {
|
||||
Public
|
||||
*/
|
||||
|
||||
downloadBackup(encrypted) {
|
||||
async downloadBackup(encrypted) {
|
||||
// download in Standard File format
|
||||
var keys, authParams, protocolVersion;
|
||||
var keys, authParams;
|
||||
if(encrypted) {
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
authParams = this.passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = this.authManager.keys();
|
||||
authParams = this.authManager.getAuthParams();
|
||||
protocolVersion = this.authManager.protocolVersion();
|
||||
keys = await this.authManager.keys();
|
||||
authParams = await this.authManager.getAuthParams();
|
||||
}
|
||||
}
|
||||
this.__itemsData(keys, authParams, protocolVersion).then((data) => {
|
||||
this.__itemsData(keys, authParams).then((data) => {
|
||||
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
@@ -39,8 +37,8 @@ class ArchiveManager {
|
||||
Private
|
||||
*/
|
||||
|
||||
async __itemsData(keys, authParams, protocolVersion) {
|
||||
let data = await this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||
async __itemsData(keys, authParams) {
|
||||
let data = await this.modelManager.getAllItemsJSONData(keys, authParams);
|
||||
let blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
@@ -1,332 +1,184 @@
|
||||
angular.module('app')
|
||||
.provider('authManager', function () {
|
||||
class AuthManager extends SFAuthManager {
|
||||
|
||||
function domainName() {
|
||||
var domain_comps = location.hostname.split(".");
|
||||
var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1];
|
||||
return domain;
|
||||
constructor(modelManager, singletonManager, storageManager, dbManager, httpManager, $rootScope, $timeout, $compile) {
|
||||
super(storageManager, httpManager, $timeout);
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
this.modelManager = modelManager;
|
||||
this.singletonManager = singletonManager;
|
||||
this.storageManager = storageManager;
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
loadInitialData() {
|
||||
var userData = this.storageManager.getItemSync("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = this.storageManager.getItemSync("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
}
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile);
|
||||
this.configureUserPrefs();
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
|
||||
offline() {
|
||||
return !this.user;
|
||||
}
|
||||
|
||||
isEphemeralSession() {
|
||||
if(this.ephemeral == null || this.ephemeral == undefined) {
|
||||
this.ephemeral = JSON.parse(this.storageManager.getItemSync("ephemeral", StorageManager.Fixed));
|
||||
}
|
||||
return this.ephemeral;
|
||||
}
|
||||
|
||||
setEphemeral(ephemeral) {
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
this.storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
this.storageManager.setItemsMode(StorageManager.Ephemeral);
|
||||
} else {
|
||||
this.storageManager.setModelStorageMode(StorageManager.Fixed);
|
||||
this.storageManager.setItemsMode(this.storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
|
||||
this.storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
|
||||
async protocolVersion() {
|
||||
var authParams = await this.getAuthParams();
|
||||
if(authParams && authParams.version) {
|
||||
return authParams.version;
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile) {
|
||||
var keys = await this.keys();
|
||||
if(keys && keys.ak) {
|
||||
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
|
||||
return "002";
|
||||
} else {
|
||||
return "001";
|
||||
}
|
||||
}
|
||||
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
if(userData) {
|
||||
this.user = JSON.parse(userData);
|
||||
} else {
|
||||
// legacy, check for uuid
|
||||
var idData = storageManager.getItem("uuid");
|
||||
if(idData) {
|
||||
this.user = {uuid: idData};
|
||||
}
|
||||
}
|
||||
async getAuthParamsForEmail(url, email, extraParams) {
|
||||
return super.getAuthParamsForEmail(url, email, extraParams);
|
||||
}
|
||||
|
||||
async login(url, email, password, ephemeral, strictSignin, extraParams) {
|
||||
return super.login(url, email, password, strictSignin, extraParams).then((response) => {
|
||||
if(!response.error) {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
|
||||
this.offline = function() {
|
||||
return !this.user;
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async register(url, email, password, ephemeral) {
|
||||
return super.register(url, email, password).then((response) => {
|
||||
if(!response.error) {
|
||||
this.setEphemeral(ephemeral);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
this.isEphemeralSession = function() {
|
||||
if(this.ephemeral == null || this.ephemeral == undefined) {
|
||||
this.ephemeral = JSON.parse(storageManager.getItem("ephemeral", StorageManager.Fixed));
|
||||
}
|
||||
return this.ephemeral;
|
||||
}
|
||||
|
||||
this.setEphemeral = function(ephemeral) {
|
||||
this.ephemeral = ephemeral;
|
||||
if(ephemeral) {
|
||||
storageManager.setModelStorageMode(StorageManager.Ephemeral);
|
||||
storageManager.setItemsMode(StorageManager.Ephemeral);
|
||||
} else {
|
||||
storageManager.setModelStorageMode(StorageManager.Fixed);
|
||||
storageManager.setItemsMode(storageManager.hasPasscode() ? StorageManager.FixedEncrypted : StorageManager.Fixed);
|
||||
storageManager.setItem("ephemeral", JSON.stringify(false), StorageManager.Fixed);
|
||||
}
|
||||
}
|
||||
|
||||
this.getAuthParams = function() {
|
||||
if(!this._authParams) {
|
||||
this._authParams = JSON.parse(storageManager.getItem("auth_params"));
|
||||
}
|
||||
return this._authParams;
|
||||
}
|
||||
|
||||
this.keys = function() {
|
||||
if(!this._keys) {
|
||||
var mk = storageManager.getItem("mk");
|
||||
if(!mk) {
|
||||
return null;
|
||||
}
|
||||
this._keys = {mk: mk, ak: storageManager.getItem("ak")};
|
||||
}
|
||||
return this._keys;
|
||||
}
|
||||
|
||||
this.protocolVersion = function() {
|
||||
var authParams = this.getAuthParams();
|
||||
if(authParams && authParams.version) {
|
||||
return authParams.version;
|
||||
}
|
||||
|
||||
var keys = this.keys();
|
||||
if(keys && keys.ak) {
|
||||
// If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams.
|
||||
return "002";
|
||||
} else {
|
||||
return "001";
|
||||
}
|
||||
}
|
||||
|
||||
this.isProtocolVersionSupported = function(version) {
|
||||
return SFJS.supportedVersions().includes(version);
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
||||
var requestUrl = url + "/auth/params";
|
||||
httpManager.getAbsolute(requestUrl, _.merge({email: email}, extraParams), function(response){
|
||||
callback(response);
|
||||
}, function(response){
|
||||
console.error("Error getting auth params", response);
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "A server error occurred while trying to sign in. Please try again."}};
|
||||
}
|
||||
callback(response);
|
||||
})
|
||||
}
|
||||
|
||||
this.login = function(url, email, password, ephemeral, strictSignin, extraParams, callback) {
|
||||
this.getAuthParamsForEmail(url, email, extraParams, (authParams) => {
|
||||
|
||||
// SF3 requires a unique identifier in the auth params
|
||||
authParams.identifier = email;
|
||||
|
||||
if(authParams.error) {
|
||||
callback(authParams);
|
||||
return;
|
||||
}
|
||||
|
||||
if(!authParams || !authParams.pw_cost) {
|
||||
callback({error : {message: "Invalid email or password."}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.isProtocolVersionSupported(authParams.version)) {
|
||||
var message;
|
||||
if(SFJS.isVersionNewerThanLibraryVersion(authParams.version)) {
|
||||
// The user has a new account type, but is signing in to an older client.
|
||||
message = "This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.";
|
||||
} else {
|
||||
// The user has a very old account type, which is no longer supported by this client
|
||||
message = "The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security for more information.";
|
||||
}
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(SFJS.isProtocolVersionOutdated(authParams.version)) {
|
||||
let message = `The encryption version for your account, ${authParams.version}, is outdated and requires upgrade. You may proceed with login, but are advised to follow prompts for Security Updates once inside. Please visit standardnotes.org/help/security for more information.\n\nClick 'OK' to proceed with login.`
|
||||
if(!confirm(message)) {
|
||||
callback({error: {}});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!SFJS.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
"If we attempted to generate your login keys here, it would take hours. " +
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
var minimum = SFJS.costMinimumForVersion(authParams.version);
|
||||
if(authParams.pw_cost < minimum) {
|
||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/security for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(strictSignin) {
|
||||
// Refuse sign in if authParams.version is anything but the latest version
|
||||
var latestVersion = SFJS.version();
|
||||
if(authParams.version !== latestVersion) {
|
||||
let message = `Strict sign in refused server sign in parameters. The latest security version is ${latestVersion}, but your account is reported to have version ${authParams.version}. If you'd like to proceed with sign in anyway, please disable strict sign in and try again.`;
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
this.checkForSecurityUpdate();
|
||||
$timeout(() => callback(response));
|
||||
}, (response) => {
|
||||
console.error("Error logging in", response);
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "A server error occurred while trying to sign in. Please try again."}};
|
||||
}
|
||||
$timeout(() => callback(response));
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||
try {
|
||||
if(url) {
|
||||
storageManager.setItem("server", url);
|
||||
}
|
||||
|
||||
this.user = response.user;
|
||||
storageManager.setItem("user", JSON.stringify(response.user));
|
||||
|
||||
this._authParams = authParams;
|
||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||
|
||||
storageManager.setItem("jwt", response.token);
|
||||
this.saveKeys(keys);
|
||||
} catch(e) {
|
||||
dbManager.displayOfflineAlert();
|
||||
}
|
||||
}
|
||||
|
||||
this.saveKeys = function(keys) {
|
||||
this._keys = keys;
|
||||
// pw doesn't need to be saved.
|
||||
// storageManager.setItem("pw", keys.pw);
|
||||
storageManager.setItem("mk", keys.mk);
|
||||
storageManager.setItem("ak", keys.ak);
|
||||
}
|
||||
|
||||
this.register = function(url, email, password, ephemeral, callback) {
|
||||
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(email, password).then((results) => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
|
||||
var requestUrl = url + "/auth";
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
callback(response);
|
||||
}, (response) => {
|
||||
console.error("Registration error", response);
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "A server error occurred while trying to register. Please try again."}};
|
||||
}
|
||||
callback(response);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this.changePassword = function(current_server_pw, newKeys, newAuthParams, callback) {
|
||||
let email = this.user.email;
|
||||
let newServerPw = newKeys.pw;
|
||||
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: newServerPw, current_password: current_server_pw}, newAuthParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.handleAuthResponse(response, email, null, newAuthParams, newKeys);
|
||||
callback(response);
|
||||
|
||||
// Allows security update status to be changed if neccessary
|
||||
this.checkForSecurityUpdate();
|
||||
}, (response) => {
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}}
|
||||
}
|
||||
callback(response);
|
||||
})
|
||||
}
|
||||
|
||||
this.checkForSecurityUpdate = function() {
|
||||
if(this.offline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let latest = SFJS.version();
|
||||
let updateAvailable = this.protocolVersion() !== latest;
|
||||
if(updateAvailable !== this.securityUpdateAvailable) {
|
||||
this.securityUpdateAvailable = updateAvailable;
|
||||
$rootScope.$broadcast("security-update-status-changed");
|
||||
}
|
||||
|
||||
return this.securityUpdateAvailable;
|
||||
}
|
||||
|
||||
this.presentPasswordWizard = function(type) {
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.type = type;
|
||||
var el = $compile( "<password-wizard type='type'></password-wizard>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
this.staticifyObject = function(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
this.signOut = function() {
|
||||
this._keys = null;
|
||||
this.user = null;
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
|
||||
/* User Preferences */
|
||||
|
||||
let prefsContentType = "SN|UserPreferences";
|
||||
|
||||
singletonManager.registerSingleton({content_type: prefsContentType}, (resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
this.userPreferencesDidChange();
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
var prefs = new Item({content_type: prefsContentType});
|
||||
modelManager.addItem(prefs);
|
||||
prefs.setDirty(true);
|
||||
$rootScope.sync("authManager singletonCreate");
|
||||
valueCallback(prefs);
|
||||
});
|
||||
|
||||
this.userPreferencesDidChange = function() {
|
||||
$rootScope.$broadcast("user-preferences-changed");
|
||||
}
|
||||
|
||||
this.syncUserPreferences = function() {
|
||||
if(this.userPreferences) {
|
||||
this.userPreferences.setDirty(true);
|
||||
$rootScope.sync("syncUserPreferences");
|
||||
}
|
||||
}
|
||||
|
||||
this.getUserPrefValue = function(key, defaultValue) {
|
||||
if(!this.userPreferences) { return defaultValue; }
|
||||
var value = this.userPreferences.getAppDataItem(key);
|
||||
return (value !== undefined && value != null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
this.setUserPrefValue = function(key, value, sync) {
|
||||
if(!this.userPreferences) { console.log("Prefs are null, not setting value", key); return; }
|
||||
this.userPreferences.setAppDataItem(key, value);
|
||||
if(sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
async changePassword(url, email, current_server_pw, newKeys, newAuthParams) {
|
||||
return super.changePassword(url, email, current_server_pw, newKeys, newAuthParams).then((response) => {
|
||||
if(!response.error) {
|
||||
this.checkForSecurityUpdate();
|
||||
}
|
||||
return response;
|
||||
})
|
||||
}
|
||||
|
||||
async handleAuthResponse(response, email, url, authParams, keys) {
|
||||
try {
|
||||
await super.handleAuthResponse(response, email, url, authParams, keys);
|
||||
this.user = response.user;
|
||||
this.storageManager.setItem("user", JSON.stringify(response.user));
|
||||
} catch (e) {
|
||||
this.dbManager.displayOfflineAlert();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async checkForSecurityUpdate() {
|
||||
if(this.offline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let latest = SFJS.version();
|
||||
let updateAvailable = await this.protocolVersion() !== latest;
|
||||
if(updateAvailable !== this.securityUpdateAvailable) {
|
||||
this.securityUpdateAvailable = updateAvailable;
|
||||
this.$rootScope.$broadcast("security-update-status-changed");
|
||||
}
|
||||
|
||||
return this.securityUpdateAvailable;
|
||||
}
|
||||
|
||||
presentPasswordWizard(type) {
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.type = type;
|
||||
var el = this.$compile( "<password-wizard type='type'></password-wizard>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
signOut() {
|
||||
super.signout();
|
||||
this.user = null;
|
||||
this._authParams = null;
|
||||
}
|
||||
|
||||
|
||||
/* User Preferences */
|
||||
|
||||
configureUserPrefs() {
|
||||
let prefsContentType = "SN|UserPreferences";
|
||||
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
|
||||
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
|
||||
this.userPreferences = resolvedSingleton;
|
||||
this.userPreferencesDidChange();
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
var prefs = new SFItem({content_type: prefsContentType});
|
||||
this.modelManager.addItem(prefs);
|
||||
prefs.setDirty(true);
|
||||
this.$rootScope.sync();
|
||||
valueCallback(prefs);
|
||||
});
|
||||
}
|
||||
|
||||
userPreferencesDidChange() {
|
||||
this.$rootScope.$broadcast("user-preferences-changed");
|
||||
}
|
||||
|
||||
syncUserPreferences() {
|
||||
if(this.userPreferences) {
|
||||
this.userPreferences.setDirty(true);
|
||||
this.$rootScope.sync();
|
||||
}
|
||||
}
|
||||
|
||||
getUserPrefValue(key, defaultValue) {
|
||||
if(!this.userPreferences) { return defaultValue; }
|
||||
var value = this.userPreferences.getAppDataItem(key);
|
||||
return (value !== undefined && value != null) ? value : defaultValue;
|
||||
}
|
||||
|
||||
setUserPrefValue(key, value, sync) {
|
||||
if(!this.userPreferences) { console.log("Prefs are null, not setting value", key); return; }
|
||||
this.userPreferences.setAppDataItem(key, value);
|
||||
if(sync) {
|
||||
this.syncUserPreferences();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').service('authManager', AuthManager);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* This domain will be used to save context item client data */
|
||||
let ClientDataDomain = "org.standardnotes.sn.components";
|
||||
|
||||
class ComponentManager {
|
||||
|
||||
constructor($rootScope, modelManager, syncManager, desktopManager, nativeExtManager, $timeout, $compile) {
|
||||
/* This domain will be used to save context item client data */
|
||||
ComponentManager.ClientDataDomain = "org.standardnotes.sn.components";
|
||||
|
||||
this.$compile = $compile;
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
@@ -57,13 +58,13 @@ class ComponentManager {
|
||||
|
||||
/* If the source of these new or updated items is from a Component itself saving items, we don't need to notify
|
||||
components again of the same item. Regarding notifying other components than the issuing component, other mapping sources
|
||||
will take care of that, like ModelManager.MappingSourceRemoteSaved
|
||||
will take care of that, like SFModelManager.MappingSourceRemoteSaved
|
||||
|
||||
Update: We will now check sourceKey to determine whether the incoming change should be sent to
|
||||
a component. If sourceKey == component.uuid, it will be skipped. This way, if one component triggers a change,
|
||||
it's sent to other components.
|
||||
*/
|
||||
// if(source == ModelManager.MappingSourceComponentRetrieved) {
|
||||
// if(source == SFModelManager.MappingSourceComponentRetrieved) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
@@ -74,7 +75,7 @@ class ComponentManager {
|
||||
/* We only want to sync if the item source is Retrieved, not MappingSourceRemoteSaved to avoid
|
||||
recursion caused by the component being modified and saved after it is updated.
|
||||
*/
|
||||
if(syncedComponents.length > 0 && source != ModelManager.MappingSourceRemoteSaved) {
|
||||
if(syncedComponents.length > 0 && source != SFModelManager.MappingSourceRemoteSaved) {
|
||||
// Ensure any component in our data is installed by the system
|
||||
this.desktopManager.syncComponentsInstallation(syncedComponents);
|
||||
}
|
||||
@@ -157,14 +158,17 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
|
||||
getActiveTheme() {
|
||||
return this.componentsForArea("themes").find((theme) => {return theme.active});
|
||||
getActiveThemes() {
|
||||
return this.componentsForArea("themes").filter((theme) => {return theme.active});
|
||||
}
|
||||
|
||||
postActiveThemeToComponent(component) {
|
||||
var activeTheme = this.getActiveTheme();
|
||||
var themes = this.getActiveThemes();
|
||||
var urls = themes.map((theme) => {
|
||||
return this.urlForComponent(theme);
|
||||
})
|
||||
var data = {
|
||||
themes: [activeTheme ? this.urlForComponent(activeTheme) : null]
|
||||
themes: urls
|
||||
}
|
||||
|
||||
this.sendMessageToComponent(component, {action: "themes", data: data})
|
||||
@@ -193,17 +197,16 @@ class ComponentManager {
|
||||
jsonForItem(item, component, source) {
|
||||
var params = {uuid: item.uuid, content_type: item.content_type, created_at: item.created_at, updated_at: item.updated_at, deleted: item.deleted};
|
||||
params.content = item.createContentJSONFromProperties();
|
||||
/* Legacy is using component.url key, so if it's present, use it, otherwise use uuid */
|
||||
params.clientData = item.getDomainDataItem(component.url || component.uuid, ClientDataDomain) || {};
|
||||
params.clientData = item.getDomainDataItem(component.getClientDataKey(), ComponentManager.ClientDataDomain) || {};
|
||||
|
||||
/* This means the this function is being triggered through a remote Saving response, which should not update
|
||||
actual local content values. The reason is, Save responses may be delayed, and a user may have changed some values
|
||||
in between the Save was initiated, and the time it completes. So we only want to update actual content values (and not just metadata)
|
||||
when its another source, like ModelManager.MappingSourceRemoteRetrieved.
|
||||
when its another source, like SFModelManager.MappingSourceRemoteRetrieved.
|
||||
|
||||
3/7/18: Add MappingSourceLocalSaved as well to handle fully offline saving. github.com/standardnotes/forum/issues/169
|
||||
*/
|
||||
if(source && (source == ModelManager.MappingSourceRemoteSaved || source == ModelManager.MappingSourceLocalSaved)) {
|
||||
if(source && (source == SFModelManager.MappingSourceRemoteSaved || source == SFModelManager.MappingSourceLocalSaved)) {
|
||||
params.isMetadataUpdate = true;
|
||||
}
|
||||
this.removePrivatePropertiesFromResponseItems([params], component);
|
||||
@@ -277,13 +280,13 @@ class ComponentManager {
|
||||
if(component.offlineOnly || (isDesktopApplication() && component.local_url)) {
|
||||
return component.local_url && component.local_url.replace("sn://", offlinePrefix + this.desktopManager.getApplicationDataPath() + "/");
|
||||
} else {
|
||||
return component.hosted_url || component.url;
|
||||
return component.hosted_url || component.legacy_url;
|
||||
}
|
||||
}
|
||||
|
||||
componentForUrl(url) {
|
||||
return this.components.filter(function(component){
|
||||
return component.url === url || component.hosted_url === url;
|
||||
return component.hosted_url === url || component.legacy_url === url;
|
||||
})[0];
|
||||
}
|
||||
|
||||
@@ -476,24 +479,29 @@ class ComponentManager {
|
||||
We map the items here because modelManager is what updates the UI. If you were to instead get the items directly,
|
||||
this would update them server side via sync, but would never make its way back to the UI.
|
||||
*/
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, ModelManager.MappingSourceComponentRetrieved, component.uuid);
|
||||
var localItems = this.modelManager.mapResponseItemsToLocalModels(responseItems, SFModelManager.MappingSourceComponentRetrieved, component.uuid);
|
||||
|
||||
for(var item of localItems) {
|
||||
var responseItem = _.find(responseItems, {uuid: item.uuid});
|
||||
for(var responseItem of responseItems) {
|
||||
var item = _.find(localItems, {uuid: responseItem.uuid});
|
||||
if(!item) {
|
||||
// An item this extension is trying to save was possibly removed locally, notify user
|
||||
alert(`The extension ${component.name} is trying to save an item with type ${responseItem.content_type}, but that item does not exist. Please restart this extension and try again.`);
|
||||
continue;
|
||||
}
|
||||
_.merge(item.content, responseItem.content);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
item.setDomainDataItem(component.getClientDataKey(), responseItem.clientData, ComponentManager.ClientDataDomain);
|
||||
}
|
||||
item.setDirty(true);
|
||||
}
|
||||
|
||||
this.syncManager.sync((response) => {
|
||||
this.syncManager.sync().then((response) => {
|
||||
// Allow handlers to be notified when a save begins and ends, to update the UI
|
||||
var saveMessage = Object.assign({}, message);
|
||||
saveMessage.action = response && response.error ? "save-error" : "save-success";
|
||||
this.replyToMessage(component, message, {error: response.error})
|
||||
this.replyToMessage(component, message, {error: response && response.error})
|
||||
this.handleMessage(component, saveMessage);
|
||||
}, null, "handleSaveItemsMessage");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -513,7 +521,7 @@ class ComponentManager {
|
||||
for(let responseItem of responseItems) {
|
||||
var item = this.modelManager.createItem(responseItem);
|
||||
if(responseItem.clientData) {
|
||||
item.setDomainDataItem(component.url || component.uuid, responseItem.clientData, ClientDataDomain);
|
||||
item.setDomainDataItem(component.getClientDataKey(), responseItem.clientData, ComponentManager.ClientDataDomain);
|
||||
}
|
||||
this.modelManager.addItem(item);
|
||||
this.modelManager.resolveReferencesForItem(item, true);
|
||||
@@ -521,7 +529,7 @@ class ComponentManager {
|
||||
processedItems.push(item);
|
||||
}
|
||||
|
||||
this.syncManager.sync("handleCreateItemMessage");
|
||||
this.syncManager.sync();
|
||||
|
||||
// "create-item" or "create-items" are possible messages handled here
|
||||
let reply =
|
||||
@@ -554,9 +562,12 @@ class ComponentManager {
|
||||
this.deactivateComponent(model, true);
|
||||
}
|
||||
this.modelManager.setItemToBeDeleted(model);
|
||||
// Currently extensions are not notified of association until a full server sync completes.
|
||||
// We manually notify observers.
|
||||
this.modelManager.notifySyncObserversOfModels([model], SFModelManager.MappingSourceRemoteSaved);
|
||||
}
|
||||
|
||||
this.syncManager.sync("handleDeleteItemsMessage");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -572,7 +583,7 @@ class ComponentManager {
|
||||
this.runWithPermissions(component, [], () => {
|
||||
component.componentData = message.data.componentData;
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("handleSetComponentDataMessage");
|
||||
this.syncManager.sync();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -583,11 +594,13 @@ class ComponentManager {
|
||||
if(targetComponent.active) {
|
||||
this.deactivateComponent(targetComponent);
|
||||
} else {
|
||||
if(targetComponent.content_type == "SN|Theme") {
|
||||
// Deactive currently active theme
|
||||
var activeTheme = this.getActiveTheme();
|
||||
if(activeTheme) {
|
||||
this.deactivateComponent(activeTheme);
|
||||
if(targetComponent.content_type == "SN|Theme" && !targetComponent.isLayerable()) {
|
||||
// Deactive currently active theme if new theme is not layerable
|
||||
var activeThemes = this.getActiveThemes();
|
||||
for(var theme of activeThemes) {
|
||||
if(theme && !theme.isLayerable()) {
|
||||
this.deactivateComponent(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.activateComponent(targetComponent);
|
||||
@@ -665,7 +678,7 @@ class ComponentManager {
|
||||
}
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("promptForPermissions");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
||||
@@ -776,14 +789,14 @@ class ComponentManager {
|
||||
// We want to run the handler in a $timeout so the UI updates, but we also don't want it to run asyncronously
|
||||
// so that the steps below this one are run before the handler. So we run in a waitTimeout.
|
||||
await this.waitTimeout(() => {
|
||||
handler.activationHandler(component);
|
||||
handler.activationHandler && handler.activationHandler(component);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("activateComponent");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
if(!this.activeComponents.includes(component)) {
|
||||
@@ -803,14 +816,14 @@ class ComponentManager {
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
await this.waitTimeout(() => {
|
||||
handler.activationHandler(component);
|
||||
handler.activationHandler && handler.activationHandler(component);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(didChange && !dontSync) {
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("deactivateComponent");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
_.pull(this.activeComponents, component);
|
||||
@@ -837,7 +850,7 @@ class ComponentManager {
|
||||
for(let handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
await this.waitTimeout(() => {
|
||||
handler.activationHandler(component);
|
||||
handler.activationHandler && handler.activationHandler(component);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -863,7 +876,7 @@ class ComponentManager {
|
||||
for(var handler of this.handlers) {
|
||||
if(handler.areas.includes(component.area) || handler.areas.includes("*")) {
|
||||
await this.waitTimeout(() => {
|
||||
handler.activationHandler(component);
|
||||
handler.activationHandler && handler.activationHandler(component);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -880,7 +893,7 @@ class ComponentManager {
|
||||
|
||||
deleteComponent(component) {
|
||||
this.modelManager.setItemToBeDeleted(component);
|
||||
this.syncManager.sync("deleteComponent");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
|
||||
isComponentActive(component) {
|
||||
@@ -908,7 +921,9 @@ class ComponentManager {
|
||||
var setSize = function(element, size) {
|
||||
var widthString = typeof size.width === 'string' ? size.width : `${data.width}px`;
|
||||
var heightString = typeof size.height === 'string' ? size.height : `${data.height}px`;
|
||||
element.setAttribute("style", `width:${widthString}; height:${heightString};`);
|
||||
if(element) {
|
||||
element.setAttribute("style", `width:${widthString}; height:${heightString};`);
|
||||
}
|
||||
}
|
||||
|
||||
if(component.area == "rooms" || component.area == "modal") {
|
||||
@@ -919,6 +934,9 @@ class ComponentManager {
|
||||
}
|
||||
} else {
|
||||
var iframe = this.iframeForComponent(component);
|
||||
if(!iframe) {
|
||||
return;
|
||||
}
|
||||
var width = data.width;
|
||||
var height = data.height;
|
||||
iframe.width = width;
|
||||
|
||||
@@ -47,6 +47,10 @@ class DBManager {
|
||||
}
|
||||
};
|
||||
|
||||
request.onblocked = (event) => {
|
||||
console.error("Request blocked error:", event.target.errorCode);
|
||||
}
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
var db = event.target.result;
|
||||
|
||||
@@ -106,7 +110,11 @@ class DBManager {
|
||||
};
|
||||
|
||||
transaction.onerror = function(event) {
|
||||
console.log("Transaction error:", event.target.errorCode);
|
||||
console.error("Transaction error:", event.target.errorCode);
|
||||
};
|
||||
|
||||
transaction.onblocked = function(event) {
|
||||
console.error("Transaction blocked error:", event.target.errorCode);
|
||||
};
|
||||
|
||||
transaction.onabort = function(event) {
|
||||
@@ -127,12 +135,14 @@ class DBManager {
|
||||
function putNext() {
|
||||
if (i < items.length) {
|
||||
var item = items[i];
|
||||
itemObjectStore.put(item).onsuccess = putNext;
|
||||
var request = itemObjectStore.put(item);
|
||||
request.onerror = (event) => {
|
||||
console.error("DB put error:", event.target.error);
|
||||
}
|
||||
request.onsuccess = putNext;
|
||||
++i;
|
||||
} else {
|
||||
if(onsuccess){
|
||||
onsuccess();
|
||||
}
|
||||
onsuccess && onsuccess();
|
||||
}
|
||||
}
|
||||
}, null)
|
||||
|
||||
@@ -37,7 +37,7 @@ class DesktopManager {
|
||||
Keys are not passed into ItemParams, so the result is not encrypted
|
||||
*/
|
||||
async convertComponentForTransmission(component) {
|
||||
return new ItemParams(component).paramsForExportFile(true);
|
||||
return new SFItemParams(component).paramsForExportFile(true);
|
||||
}
|
||||
|
||||
// All `components` should be installed
|
||||
@@ -96,11 +96,11 @@ class DesktopManager {
|
||||
for(var key of permissableKeys) {
|
||||
component[key] = componentData.content[key];
|
||||
}
|
||||
this.modelManager.notifySyncObserversOfModels([component], ModelManager.MappingSourceDesktopInstalled);
|
||||
this.modelManager.notifySyncObserversOfModels([component], SFModelManager.MappingSourceDesktopInstalled);
|
||||
component.setAppDataItem("installError", null);
|
||||
}
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("onComponentInstallationComplete");
|
||||
this.syncManager.sync();
|
||||
|
||||
this.timeout(() => {
|
||||
for(var observer of this.updateObservers) {
|
||||
@@ -129,22 +129,19 @@ class DesktopManager {
|
||||
}
|
||||
}
|
||||
|
||||
desktop_requestBackupFile(callback) {
|
||||
var keys, authParams, protocolVersion;
|
||||
async desktop_requestBackupFile(callback) {
|
||||
var keys, authParams;
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
authParams = this.passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = this.authManager.keys();
|
||||
authParams = this.authManager.getAuthParams();
|
||||
protocolVersion = this.authManager.protocolVersion();
|
||||
keys = await this.authManager.keys();
|
||||
authParams = await this.authManager.getAuthParams();
|
||||
}
|
||||
|
||||
this.modelManager.getAllItemsJSONData(
|
||||
keys,
|
||||
authParams,
|
||||
protocolVersion,
|
||||
true /* return null on empty */
|
||||
).then((data) => {
|
||||
callback(data);
|
||||
|
||||
@@ -1,80 +1,13 @@
|
||||
class HttpManager {
|
||||
class HttpManager extends SFHttpManager {
|
||||
|
||||
constructor($timeout, storageManager) {
|
||||
// calling callbacks in a $timeout allows angular UI to update
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
constructor(storageManager, $timeout) {
|
||||
// calling callbacks in a $timeout allows UI to update
|
||||
super($timeout);
|
||||
|
||||
this.setJWTRequestHandler(async () => {
|
||||
return storageManager.getItem("jwt");;
|
||||
})
|
||||
}
|
||||
|
||||
setAuthHeadersForRequest(request) {
|
||||
var token = this.storageManager.getItem("jwt");
|
||||
if(token) {
|
||||
request.setRequestHeader('Authorization', 'Bearer ' + token);
|
||||
}
|
||||
}
|
||||
|
||||
postAbsolute(url, params, onsuccess, onerror) {
|
||||
this.httpRequest("post", url, params, onsuccess, onerror);
|
||||
}
|
||||
|
||||
patchAbsolute(url, params, onsuccess, onerror) {
|
||||
this.httpRequest("patch", url, params, onsuccess, onerror);
|
||||
}
|
||||
|
||||
getAbsolute(url, params, onsuccess, onerror) {
|
||||
this.httpRequest("get", url, params, onsuccess, onerror);
|
||||
}
|
||||
|
||||
httpRequest(verb, url, params, onsuccess, onerror) {
|
||||
|
||||
var xmlhttp = new XMLHttpRequest();
|
||||
|
||||
xmlhttp.onreadystatechange = function() {
|
||||
if (xmlhttp.readyState == 4) {
|
||||
var response = xmlhttp.responseText;
|
||||
if(response) {
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
if(xmlhttp.status >= 200 && xmlhttp.status <= 299){
|
||||
this.$timeout(function(){
|
||||
onsuccess(response);
|
||||
})
|
||||
} else {
|
||||
console.error("Request error:", response);
|
||||
this.$timeout(function(){
|
||||
onerror(response, xmlhttp.status)
|
||||
})
|
||||
}
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
if(verb == "get" && Object.keys(params).length > 0) {
|
||||
url = url + this.formatParams(params);
|
||||
}
|
||||
|
||||
xmlhttp.open(verb, url, true);
|
||||
this.setAuthHeadersForRequest(xmlhttp);
|
||||
xmlhttp.setRequestHeader('Content-type', 'application/json');
|
||||
|
||||
if(verb == "post" || verb == "patch") {
|
||||
xmlhttp.send(JSON.stringify(params));
|
||||
} else {
|
||||
xmlhttp.send();
|
||||
}
|
||||
}
|
||||
|
||||
formatParams(params) {
|
||||
return "?" + Object
|
||||
.keys(params)
|
||||
.map(function(key){
|
||||
return key+"="+encodeURIComponent(params[key])
|
||||
})
|
||||
.join("&")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('httpManager', HttpManager);
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
class MigrationManager {
|
||||
class MigrationManager extends SFMigrationManager {
|
||||
|
||||
constructor($rootScope, modelManager, syncManager, componentManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.syncManager = syncManager;
|
||||
constructor($rootScope, modelManager, syncManager, componentManager, storageManager) {
|
||||
super(modelManager, syncManager, storageManager);
|
||||
this.componentManager = componentManager;
|
||||
}
|
||||
|
||||
this.migrators = [];
|
||||
|
||||
this.addEditorToComponentMigrator();
|
||||
|
||||
this.modelManager.addItemSyncObserver("migration-manager", "*", (allItems, validItems, deletedItems) => {
|
||||
for(var migrator of this.migrators) {
|
||||
var items = allItems.filter((item) => {return item.content_type == migrator.content_type});
|
||||
if(items.length > 0) {
|
||||
migrator.handler(items);
|
||||
}
|
||||
}
|
||||
});
|
||||
registeredMigrations() {
|
||||
return [
|
||||
this.editorToComponentMigration(),
|
||||
this.componentUrlToHostedUrl()
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -25,10 +17,10 @@ class MigrationManager {
|
||||
convert to using the new component API.
|
||||
*/
|
||||
|
||||
addEditorToComponentMigrator() {
|
||||
this.migrators.push({
|
||||
editorToComponentMigration() {
|
||||
return {
|
||||
name: "editor-to-component",
|
||||
content_type: "SN|Editor",
|
||||
|
||||
handler: (editors) => {
|
||||
// Convert editors to components
|
||||
for(var editor of editors) {
|
||||
@@ -36,9 +28,11 @@ class MigrationManager {
|
||||
if(editor.url && !this.componentManager.componentForUrl(editor.url)) {
|
||||
var component = this.modelManager.createItem({
|
||||
content_type: "SN|Component",
|
||||
url: editor.url,
|
||||
name: editor.name,
|
||||
area: "editor-editor"
|
||||
content: {
|
||||
url: editor.url,
|
||||
name: editor.name,
|
||||
area: "editor-editor"
|
||||
}
|
||||
})
|
||||
component.setAppDataItem("data", editor.data);
|
||||
component.setDirty(true);
|
||||
@@ -50,12 +44,38 @@ class MigrationManager {
|
||||
this.modelManager.setItemToBeDeleted(editor);
|
||||
}
|
||||
|
||||
this.syncManager.sync("addEditorToComponentMigrator");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Migrate component.url fields to component.hosted_url. This involves rewriting any note data that relied on the
|
||||
component.url value to store clientData, such as the CodeEditor, which stores the programming language for the note
|
||||
in the note's clientData[component.url]. We want to rewrite any matching items to transfer that clientData into
|
||||
clientData[component.uuid].
|
||||
Created: July 6, 2018
|
||||
*/
|
||||
componentUrlToHostedUrl() {
|
||||
return {
|
||||
name: "component-url-to-hosted-url",
|
||||
content_type: "SN|Component",
|
||||
handler: (components) => {
|
||||
var notes = this.modelManager.validItemsForContentType("Note");
|
||||
for(var note of notes) {
|
||||
for(var component of components) {
|
||||
var clientData = note.getDomainDataItem(component.hosted_url, ComponentManager.ClientDataDomain);
|
||||
if(clientData) {
|
||||
note.setDomainDataItem(component.uuid, clientData, ComponentManager.ClientDataDomain);
|
||||
note.setDomainDataItem(component.hosted_url, null, ComponentManager.ClientDataDomain);
|
||||
note.setDirty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').service('migrationManager', MigrationManager);
|
||||
|
||||
@@ -1,302 +1,57 @@
|
||||
class ModelManager {
|
||||
SFModelManager.ContentTypeClassMapping = {
|
||||
"Note" : SNNote,
|
||||
"Tag" : SNTag,
|
||||
"SN|SmartTag" : SNSmartTag,
|
||||
"Extension" : SNExtension,
|
||||
"SN|Editor" : SNEditor,
|
||||
"SN|Theme" : SNTheme,
|
||||
"SN|Component" : SNComponent,
|
||||
"SF|Extension" : SNServerExtension,
|
||||
"SF|MFA" : SNMfa
|
||||
};
|
||||
|
||||
constructor(storageManager) {
|
||||
ModelManager.MappingSourceRemoteRetrieved = "MappingSourceRemoteRetrieved";
|
||||
ModelManager.MappingSourceRemoteSaved = "MappingSourceRemoteSaved";
|
||||
ModelManager.MappingSourceLocalSaved = "MappingSourceLocalSaved";
|
||||
ModelManager.MappingSourceLocalRetrieved = "MappingSourceLocalRetrieved";
|
||||
ModelManager.MappingSourceComponentRetrieved = "MappingSourceComponentRetrieved";
|
||||
ModelManager.MappingSourceDesktopInstalled = "MappingSourceDesktopInstalled"; // When a component is installed by the desktop and some of its values change
|
||||
ModelManager.MappingSourceRemoteActionRetrieved = "MappingSourceRemoteActionRetrieved"; /* aciton-based Extensions like note history */
|
||||
ModelManager.MappingSourceFileImport = "MappingSourceFileImport";
|
||||
SFItem.AppDomain = "org.standardnotes.sn";
|
||||
|
||||
ModelManager.isMappingSourceRetrieved = (source) => {
|
||||
return [
|
||||
ModelManager.MappingSourceRemoteRetrieved,
|
||||
ModelManager.MappingSourceComponentRetrieved,
|
||||
ModelManager.MappingSourceRemoteActionRetrieved
|
||||
].includes(source);
|
||||
}
|
||||
class ModelManager extends SFModelManager {
|
||||
|
||||
this.storageManager = storageManager;
|
||||
constructor(storageManager, $timeout) {
|
||||
super($timeout);
|
||||
this.notes = [];
|
||||
this.tags = [];
|
||||
this.itemSyncObservers = [];
|
||||
this.itemChangeObservers = [];
|
||||
this.itemsPendingRemoval = [];
|
||||
this.items = [];
|
||||
this._extensions = [];
|
||||
this.acceptableContentTypes = [
|
||||
"Note", "Tag", "Extension", "SN|Editor", "SN|Theme",
|
||||
"SN|Component", "SF|Extension", "SN|UserPreferences", "SF|MFA"
|
||||
];
|
||||
this.components = [];
|
||||
|
||||
this.storageManager = storageManager;
|
||||
}
|
||||
|
||||
resetLocalMemory() {
|
||||
handleSignout() {
|
||||
super.handleSignout();
|
||||
this.notes.length = 0;
|
||||
this.tags.length = 0;
|
||||
this.items.length = 0;
|
||||
this._extensions.length = 0;
|
||||
this.components.length = 0;
|
||||
}
|
||||
|
||||
get allItems() {
|
||||
return this.items.filter(function(item){
|
||||
return !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this._extensions.filter(function(ext){
|
||||
return !ext.deleted;
|
||||
})
|
||||
}
|
||||
|
||||
alternateUUIDForItem(item, callback, removeOriginal) {
|
||||
// We need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't modify uuid's in our indexeddb setup)
|
||||
|
||||
// Collapse in memory properties to item's content object, as the new item will be created based on the content object, and not the physical properties. (like note.text or note.title)
|
||||
item.refreshContentObject();
|
||||
|
||||
var newItem = this.createItem(item);
|
||||
|
||||
newItem.uuid = SFJS.crypto.generateUUIDSync();
|
||||
|
||||
// Update uuids of relationships
|
||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||
|
||||
this.informModelsOfUUIDChangeForItem(newItem, item.uuid, newItem.uuid);
|
||||
|
||||
|
||||
console.log(item.uuid, "-->", newItem.uuid);
|
||||
|
||||
var block = () => {
|
||||
this.addItem(newItem);
|
||||
newItem.setDirty(true);
|
||||
newItem.markAllReferencesDirty();
|
||||
callback();
|
||||
}
|
||||
|
||||
if(removeOriginal) {
|
||||
// Set to deleted, then run through mapping function so that observers can be notified
|
||||
removeAllItemsFromMemory() {
|
||||
for(var item of this.items) {
|
||||
item.deleted = true;
|
||||
this.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
block();
|
||||
} else {
|
||||
block();
|
||||
}
|
||||
}
|
||||
|
||||
informModelsOfUUIDChangeForItem(newItem, oldUUID, newUUID) {
|
||||
// some models that only have one-way relationships might be interested to hear that an item has changed its uuid
|
||||
// for example, editors have a one way relationship with notes. When a note changes its UUID, it has no way to inform the editor
|
||||
// to update its relationships
|
||||
|
||||
for(var model of this.items) {
|
||||
model.potentialItemOfInterestHasChangedItsUUID(newItem, oldUUID, newUUID);
|
||||
}
|
||||
}
|
||||
|
||||
allItemsMatchingTypes(contentTypes) {
|
||||
return this.allItems.filter(function(item){
|
||||
return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
validItemsForContentType(contentType) {
|
||||
return this.allItems.filter((item) => {
|
||||
return item.content_type == contentType && !item.errorDecrypting;
|
||||
});
|
||||
}
|
||||
|
||||
findItem(itemId) {
|
||||
return _.find(this.items, {uuid: itemId});
|
||||
this.notifySyncObserversOfModels(this.items);
|
||||
this.handleSignout();
|
||||
}
|
||||
|
||||
findOrCreateTagByTitle(title) {
|
||||
var tag = _.find(this.tags, {title: title})
|
||||
if(!tag) {
|
||||
tag = this.createItem({content_type: "Tag", title: title});
|
||||
tag = this.createItem({content_type: "Tag", content: {title: title}});
|
||||
tag.setDirty(true);
|
||||
this.addItem(tag);
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
didSyncModelsOffline(items) {
|
||||
this.notifySyncObserversOfModels(items, ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModels(items, source, sourceKey) {
|
||||
return this.mapResponseItemsToLocalModelsOmittingFields(items, null, source, sourceKey);
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModelsOmittingFields(items, omitFields, source, sourceKey) {
|
||||
var models = [], processedObjects = [], modelsToNotifyObserversOf = [];
|
||||
|
||||
// first loop should add and process items
|
||||
for (var json_obj of items) {
|
||||
if((!json_obj.content_type || !json_obj.content) && !json_obj.deleted && !json_obj.errorDecrypting) {
|
||||
// An item that is not deleted should never have empty content
|
||||
console.error("Server response item is corrupt:", json_obj);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lodash's _.omit, which was previously used, seems to cause unexpected behavior
|
||||
// when json_obj is an ES6 item class. So we instead manually omit each key.
|
||||
if(Array.isArray(omitFields)) {
|
||||
for(var key of omitFields) {
|
||||
delete json_obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
var item = this.findItem(json_obj.uuid);
|
||||
|
||||
if(item) {
|
||||
item.updateFromJSON(json_obj);
|
||||
// If an item goes through mapping, it can no longer be a dummy.
|
||||
item.dummy = false;
|
||||
}
|
||||
|
||||
if(this.itemsPendingRemoval.includes(json_obj.uuid)) {
|
||||
_.pull(this.itemsPendingRemoval, json_obj.uuid);
|
||||
continue;
|
||||
}
|
||||
|
||||
let contentType = json_obj["content_type"] || (item && item.content_type);
|
||||
var unknownContentType = !_.includes(this.acceptableContentTypes, contentType);
|
||||
var isDirtyItemPendingDelete = false;
|
||||
if(json_obj.deleted == true || unknownContentType) {
|
||||
if(json_obj.deleted && json_obj.dirty) {
|
||||
// Item was marked as deleted but not yet synced
|
||||
// We need to create this item as usual, but just not add it to individual arrays
|
||||
// i.e add to this.items but not this.notes (so that it can be retrieved with getDirtyItems)
|
||||
isDirtyItemPendingDelete = true;
|
||||
} else {
|
||||
if(item && !unknownContentType) {
|
||||
modelsToNotifyObserversOf.push(item);
|
||||
this.removeItemLocally(item);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if(!item) {
|
||||
item = this.createItem(json_obj, true);
|
||||
}
|
||||
|
||||
this.addItem(item, isDirtyItemPendingDelete);
|
||||
|
||||
// Observers do not need to handle items that errored while decrypting.
|
||||
if(!item.errorDecrypting) {
|
||||
modelsToNotifyObserversOf.push(item);
|
||||
}
|
||||
|
||||
models.push(item);
|
||||
processedObjects.push(json_obj);
|
||||
}
|
||||
|
||||
// second loop should process references
|
||||
for (var index in processedObjects) {
|
||||
var json_obj = processedObjects[index];
|
||||
if(json_obj.content) {
|
||||
this.resolveReferencesForItem(models[index]);
|
||||
}
|
||||
}
|
||||
|
||||
this.notifySyncObserversOfModels(modelsToNotifyObserversOf, source, sourceKey);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/* Note that this function is public, and can also be called manually (desktopManager uses it) */
|
||||
notifySyncObserversOfModels(models, source, sourceKey) {
|
||||
for(var observer of this.itemSyncObservers) {
|
||||
var allRelevantItems = observer.type == "*" ? models : models.filter(function(item){return item.content_type == observer.type});
|
||||
var validItems = [], deletedItems = [];
|
||||
for(var item of allRelevantItems) {
|
||||
if(item.deleted) {
|
||||
deletedItems.push(item);
|
||||
} else {
|
||||
validItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
if(allRelevantItems.length > 0) {
|
||||
observer.callback(allRelevantItems, validItems, deletedItems, source, sourceKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemChangeObserversOfModels(models) {
|
||||
for(var observer of this.itemChangeObservers) {
|
||||
var relevantItems = models.filter(function(item){
|
||||
return _.includes(observer.content_types, item.content_type) || _.includes(observer.content_types, "*");
|
||||
});
|
||||
|
||||
if(relevantItems.length > 0) {
|
||||
observer.callback(relevantItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createItem(json_obj, dontNotifyObservers) {
|
||||
var item;
|
||||
if(json_obj.content_type == "Note") {
|
||||
item = new Note(json_obj);
|
||||
} else if(json_obj.content_type == "Tag") {
|
||||
item = new Tag(json_obj);
|
||||
} else if(json_obj.content_type == "Extension") {
|
||||
item = new Extension(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Editor") {
|
||||
item = new Editor(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Theme") {
|
||||
item = new Theme(json_obj);
|
||||
} else if(json_obj.content_type == "SN|Component") {
|
||||
item = new Component(json_obj);
|
||||
} else if(json_obj.content_type == "SF|Extension") {
|
||||
item = new ServerExtension(json_obj);
|
||||
} else if(json_obj.content_type == "SF|MFA") {
|
||||
item = new Mfa(json_obj);
|
||||
}
|
||||
|
||||
else {
|
||||
item = new Item(json_obj);
|
||||
}
|
||||
|
||||
// Some observers would be interested to know when an an item is locally created
|
||||
// If we don't send this out, these observers would have to wait until MappingSourceRemoteSaved
|
||||
// to hear about it, but sometimes, RemoveSaved is explicitly ignored by the observer to avoid
|
||||
// recursive callbacks. See componentManager's syncObserver callback.
|
||||
// dontNotifyObservers is currently only set true by modelManagers mapResponseItemsToLocalModels
|
||||
if(!dontNotifyObservers) {
|
||||
this.notifySyncObserversOfModels([item], ModelManager.MappingSourceLocalSaved);
|
||||
}
|
||||
|
||||
item.addObserver(this, function(changedItem){
|
||||
this.notifyItemChangeObserversOfModels([changedItem]);
|
||||
}.bind(this));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/*
|
||||
Be sure itemResponse is a generic Javascript object, and not an Item.
|
||||
An Item needs to collapse its properties into its content object before it can be duplicated.
|
||||
Note: the reason we need this function is specificallty for the call to resolveReferencesForItem.
|
||||
This method creates but does not add the item to the global inventory. It's used by syncManager
|
||||
to check if this prospective duplicate item is identical to another item, including the references.
|
||||
*/
|
||||
createDuplicateItem(itemResponse) {
|
||||
var dup = this.createItem(itemResponse, true);
|
||||
this.resolveReferencesForItem(dup);
|
||||
return dup;
|
||||
}
|
||||
|
||||
addItem(item, globalOnly = false) {
|
||||
this.addItems([item], globalOnly);
|
||||
}
|
||||
|
||||
addItems(items, globalOnly = false) {
|
||||
items.forEach(function(item){
|
||||
super.addItems(items, globalOnly);
|
||||
|
||||
items.forEach((item) => {
|
||||
// In some cases, you just want to add the item to this.items, and not to the individual arrays
|
||||
// This applies when you want to keep an item syncable, but not display it via the individual arrays
|
||||
if(!globalOnly) {
|
||||
@@ -311,17 +66,13 @@ class ModelManager {
|
||||
if(!_.find(this.notes, {uuid: item.uuid})) {
|
||||
this.notes.unshift(item);
|
||||
}
|
||||
} else if(item.content_type == "Extension") {
|
||||
if(!_.find(this._extensions, {uuid: item.uuid})) {
|
||||
this._extensions.unshift(item);
|
||||
} else if(item.content_type == "SN|Component") {
|
||||
if(!_.find(this.components, {uuid: item.uuid})) {
|
||||
this.components.unshift(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!_.find(this.items, {uuid: item.uuid})) {
|
||||
this.items.push(item);
|
||||
}
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
resortTag(tag) {
|
||||
@@ -332,158 +83,37 @@ class ModelManager {
|
||||
}), 0, tag);
|
||||
}
|
||||
|
||||
resolveReferencesForItem(item, markReferencesDirty = false) {
|
||||
|
||||
var contentObject = item.contentObject;
|
||||
|
||||
// If another client removes an item's references, this client won't pick up the removal unless
|
||||
// we remove everything not present in the current list of references
|
||||
item.removeReferencesNotPresentIn(contentObject.references || []);
|
||||
|
||||
if(!contentObject.references) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var reference of contentObject.references) {
|
||||
var referencedItem = this.findItem(reference.uuid);
|
||||
if(referencedItem) {
|
||||
item.addItemAsRelationship(referencedItem);
|
||||
referencedItem.addItemAsRelationship(item);
|
||||
|
||||
if(markReferencesDirty) {
|
||||
referencedItem.setDirty(true);
|
||||
}
|
||||
} else {
|
||||
// console.log("Unable to find reference:", reference.uuid, "for item:", item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addItemSyncObserver(id, type, callback) {
|
||||
this.itemSyncObservers.push({id: id, type: type, callback: callback});
|
||||
}
|
||||
|
||||
removeItemSyncObserver(id) {
|
||||
_.remove(this.itemSyncObservers, _.find(this.itemSyncObservers, {id: id}));
|
||||
}
|
||||
|
||||
addItemChangeObserver(id, content_types, callback) {
|
||||
this.itemChangeObservers.push({id: id, content_types: content_types, callback: callback});
|
||||
}
|
||||
|
||||
removeItemChangeObserver(id) {
|
||||
_.remove(this.itemChangeObservers, _.find(this.itemChangeObservers, {id: id}));
|
||||
}
|
||||
|
||||
get filteredNotes() {
|
||||
return Note.filterDummyNotes(this.notes);
|
||||
}
|
||||
|
||||
getDirtyItems() {
|
||||
return this.items.filter((item) => {
|
||||
// An item that has an error decrypting can be synced only if it is being deleted.
|
||||
// Otherwise, we don't want to send corrupt content up to the server.
|
||||
return item.dirty == true && !item.dummy && (!item.errorDecrypting || item.deleted);
|
||||
})
|
||||
}
|
||||
|
||||
clearDirtyItems(items) {
|
||||
for(var item of items) {
|
||||
item.setDirty(false);
|
||||
}
|
||||
}
|
||||
|
||||
clearAllDirtyItems() {
|
||||
this.clearDirtyItems(this.getDirtyItems());
|
||||
}
|
||||
|
||||
setItemToBeDeleted(item) {
|
||||
item.deleted = true;
|
||||
if(!item.dummy) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
super.setItemToBeDeleted(item);
|
||||
|
||||
// remove from relevant array, but don't remove from all items.
|
||||
// This way, it's removed from the display, but still synced via get dirty items
|
||||
if(item.content_type == "Tag") {
|
||||
_.pull(this.tags, item);
|
||||
} else if(item.content_type == "Note") {
|
||||
_.pull(this.notes, item);
|
||||
} else if(item.content_type == "Extension") {
|
||||
_.pull(this._extensions, item);
|
||||
}
|
||||
|
||||
item.removeAndDirtyAllRelationships();
|
||||
}
|
||||
|
||||
/* Used when changing encryption key */
|
||||
setAllItemsDirty(dontUpdateClientDates = true) {
|
||||
var relevantItems = this.allItems.filter(function(item){
|
||||
return _.includes(this.acceptableContentTypes, item.content_type);
|
||||
}.bind(this));
|
||||
|
||||
for(var item of relevantItems) {
|
||||
item.setDirty(true, dontUpdateClientDates);
|
||||
}
|
||||
this.removeItemFromRespectiveArray(item);
|
||||
}
|
||||
|
||||
removeItemLocally(item, callback) {
|
||||
_.pull(this.items, item);
|
||||
super.removeItemLocally(item, callback);
|
||||
|
||||
item.isBeingRemovedLocally();
|
||||
this.removeItemFromRespectiveArray(item);
|
||||
|
||||
this.itemsPendingRemoval.push(item.uuid);
|
||||
this.storageManager.deleteModel(item).then(callback);
|
||||
}
|
||||
|
||||
removeItemFromRespectiveArray(item) {
|
||||
if(item.content_type == "Tag") {
|
||||
_.pull(this.tags, item);
|
||||
_.remove(this.tags, {uuid: item.uuid});
|
||||
} else if(item.content_type == "Note") {
|
||||
_.pull(this.notes, item);
|
||||
} else if(item.content_type == "Extension") {
|
||||
_.pull(this._extensions, item);
|
||||
_.remove(this.notes, {uuid: item.uuid});
|
||||
} else if(item.content_type == "SN|Component") {
|
||||
_.remove(this.components, {uuid: item.uuid});
|
||||
}
|
||||
|
||||
this.storageManager.deleteModel(item, callback);
|
||||
}
|
||||
|
||||
/*
|
||||
Relationships
|
||||
*/
|
||||
|
||||
createRelationshipBetweenItems(itemOne, itemTwo) {
|
||||
itemOne.addItemAsRelationship(itemTwo);
|
||||
itemTwo.addItemAsRelationship(itemOne);
|
||||
|
||||
itemOne.setDirty(true);
|
||||
itemTwo.setDirty(true);
|
||||
notesMatchingPredicate(predicate) {
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "Note");
|
||||
return this.itemsMatchingPredicates([contentTypePredicate, predicate]);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Archives
|
||||
*/
|
||||
|
||||
async getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
||||
return Promise.all(this.allItems.map((item) => {
|
||||
var itemParams = new ItemParams(item, keys, protocolVersion);
|
||||
return itemParams.paramsForExportFile();
|
||||
})).then((items) => {
|
||||
if(returnNullIfEmpty && items.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = {items: items}
|
||||
|
||||
if(keys) {
|
||||
// auth params are only needed when encrypted with a standard file key
|
||||
data["auth_params"] = authParams;
|
||||
}
|
||||
|
||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Misc
|
||||
*/
|
||||
@@ -492,6 +122,7 @@ class ModelManager {
|
||||
return {
|
||||
"Note" : "note",
|
||||
"Tag" : "tag",
|
||||
"SN|SmartTag": "smart tag",
|
||||
"Extension" : "action-based extension",
|
||||
"SN|Component" : "component",
|
||||
"SN|Editor" : "editor",
|
||||
|
||||
@@ -21,7 +21,10 @@ class NativeExtManager {
|
||||
|
||||
resolveExtensionsManager() {
|
||||
|
||||
this.singletonManager.registerSingleton({content_type: "SN|Component", package_info: {identifier: this.extensionsManagerIdentifier}}, (resolvedSingleton) => {
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.extensionsManagerIdentifier);
|
||||
|
||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||
// Resolved Singleton
|
||||
this.systemExtensions.push(resolvedSingleton.uuid);
|
||||
|
||||
@@ -40,7 +43,7 @@ class NativeExtManager {
|
||||
|
||||
if(needsSync) {
|
||||
resolvedSingleton.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
@@ -81,7 +84,7 @@ class NativeExtManager {
|
||||
this.modelManager.addItem(component);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager createNew");
|
||||
this.syncManager.sync();
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
@@ -91,7 +94,10 @@ class NativeExtManager {
|
||||
|
||||
resolveBatchManager() {
|
||||
|
||||
this.singletonManager.registerSingleton({content_type: "SN|Component", package_info: {identifier: this.batchManagerIdentifier}}, (resolvedSingleton) => {
|
||||
let contentTypePredicate = new SFPredicate("content_type", "=", "SN|Component");
|
||||
let packagePredicate = new SFPredicate("package_info.identifier", "=", this.batchManagerIdentifier);
|
||||
|
||||
this.singletonManager.registerSingleton([contentTypePredicate, packagePredicate], (resolvedSingleton) => {
|
||||
// Resolved Singleton
|
||||
this.systemExtensions.push(resolvedSingleton.uuid);
|
||||
|
||||
@@ -110,7 +116,7 @@ class NativeExtManager {
|
||||
|
||||
if(needsSync) {
|
||||
resolvedSingleton.setDirty(true);
|
||||
this.syncManager.sync("resolveExtensionsManager");
|
||||
this.syncManager.sync();
|
||||
}
|
||||
}, (valueCallback) => {
|
||||
// Safe to create. Create and return object.
|
||||
@@ -151,7 +157,7 @@ class NativeExtManager {
|
||||
this.modelManager.addItem(component);
|
||||
|
||||
component.setDirty(true);
|
||||
this.syncManager.sync("resolveBatchManager createNew");
|
||||
this.syncManager.sync();
|
||||
|
||||
this.systemExtensions.push(component.uuid);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ angular.module('app')
|
||||
|
||||
function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) {
|
||||
|
||||
this._hasPasscode = storageManager.getItem("offlineParams", StorageManager.Fixed) != null;
|
||||
this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null;
|
||||
this._locked = this._hasPasscode;
|
||||
|
||||
this.isLocked = function() {
|
||||
@@ -23,11 +23,7 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.passcodeAuthParams = function() {
|
||||
return JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
||||
}
|
||||
|
||||
this.protocolVersion = function() {
|
||||
return this._authParams && this._authParams.version;
|
||||
return JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed));
|
||||
}
|
||||
|
||||
this.unlock = function(passcode, callback) {
|
||||
|
||||
25
app/assets/javascripts/app/services/sessionHistory.js
Normal file
25
app/assets/javascripts/app/services/sessionHistory.js
Normal file
@@ -0,0 +1,25 @@
|
||||
class SessionHistory extends SFSessionHistoryManager {
|
||||
|
||||
constructor(modelManager, storageManager, authManager, passcodeManager, $timeout) {
|
||||
|
||||
SFItemHistory.HistoryEntryClassMapping = {
|
||||
"Note" : NoteHistoryEntry
|
||||
}
|
||||
|
||||
var keyRequestHandler = async () => {
|
||||
let offline = authManager.offline();
|
||||
let auth_params = offline ? passcodeManager.passcodeAuthParams() : await authManager.getAuthParams();
|
||||
let keys = offline ? passcodeManager.keys() : await authManager.keys();
|
||||
return {
|
||||
keys: keys,
|
||||
offline: offline,
|
||||
auth_params: auth_params
|
||||
}
|
||||
}
|
||||
|
||||
var contentTypes = ["Note"];
|
||||
super(modelManager, storageManager, keyRequestHandler, contentTypes, $timeout);
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').service('sessionHistory', SessionHistory);
|
||||
@@ -40,14 +40,14 @@ class SingletonManager {
|
||||
})
|
||||
}
|
||||
|
||||
registerSingleton(predicate, resolveCallback, createBlock) {
|
||||
registerSingleton(predicates, resolveCallback, createBlock) {
|
||||
/*
|
||||
predicate: a key/value pair that specifies properties that should match in order for an item to be considered a predicate
|
||||
resolveCallback: called when one or more items are deleted and a new item becomes the reigning singleton
|
||||
createBlock: called when a sync is complete and no items are found. The createBlock should create the item and return it.
|
||||
*/
|
||||
this.singletonHandlers.push({
|
||||
predicate: predicate,
|
||||
predicates: predicates,
|
||||
resolutionCallback: resolveCallback,
|
||||
createBlock: createBlock
|
||||
});
|
||||
@@ -58,20 +58,20 @@ class SingletonManager {
|
||||
savedItems = savedItems || [];
|
||||
|
||||
for(let singletonHandler of this.singletonHandlers) {
|
||||
var predicate = singletonHandler.predicate;
|
||||
let retrievedSingletonItems = this.filterItemsWithPredicate(retrievedItems, predicate);
|
||||
var predicates = singletonHandler.predicates;
|
||||
let retrievedSingletonItems = this.modelManager.filterItemsWithPredicates(retrievedItems, predicates);
|
||||
|
||||
// We only want to consider saved items count to see if it's more than 0, and do nothing else with it.
|
||||
// This way we know there was some action and things need to be resolved. The saved items will come up
|
||||
// in filterItemsWithPredicate(this.modelManager.allItems) and be deleted anyway
|
||||
let savedSingletonItemsCount = this.filterItemsWithPredicate(savedItems, predicate).length;
|
||||
let savedSingletonItemsCount = this.modelManager.filterItemsWithPredicates(savedItems, predicates).length;
|
||||
|
||||
if(retrievedSingletonItems.length > 0 || savedSingletonItemsCount > 0) {
|
||||
/*
|
||||
Check local inventory and make sure only 1 similar item exists. If more than 1, delete newest
|
||||
Note that this local inventory will also contain whatever is in retrievedItems.
|
||||
*/
|
||||
var allExtantItemsMatchingPredicate = this.filterItemsWithPredicate(this.modelManager.allItems, predicate);
|
||||
var allExtantItemsMatchingPredicate = this.modelManager.itemsMatchingPredicates(predicates);
|
||||
|
||||
/*
|
||||
Delete all but the earliest created
|
||||
@@ -92,7 +92,7 @@ class SingletonManager {
|
||||
this.modelManager.setItemToBeDeleted(d);
|
||||
}
|
||||
|
||||
this.$rootScope.sync("resolveSingletons");
|
||||
this.$rootScope.sync();
|
||||
|
||||
// Send remaining item to callback
|
||||
singletonHandler.singleton = winningItem;
|
||||
@@ -122,35 +122,6 @@ class SingletonManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterItemsWithPredicate(items, predicate) {
|
||||
return items.filter((candidate) => {
|
||||
return this.itemSatisfiesPredicate(candidate, predicate);
|
||||
})
|
||||
}
|
||||
|
||||
itemSatisfiesPredicate(candidate, predicate) {
|
||||
for(var key in predicate) {
|
||||
var predicateValue = predicate[key];
|
||||
var candidateValue = candidate[key];
|
||||
if(typeof predicateValue == 'object') {
|
||||
// Check nested properties
|
||||
if(!candidateValue) {
|
||||
// predicateValue is 'object' but candidateValue is null
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!this.itemSatisfiesPredicate(candidateValue, predicateValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if(candidateValue != predicateValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('singletonManager', SingletonManager);
|
||||
|
||||
@@ -7,6 +7,10 @@ class MemoryStorage {
|
||||
return this.memory[key] || null;
|
||||
}
|
||||
|
||||
getItemSync(key) {
|
||||
return this.getItem(key);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.memory).length;
|
||||
}
|
||||
@@ -32,9 +36,10 @@ class MemoryStorage {
|
||||
}
|
||||
}
|
||||
|
||||
class StorageManager {
|
||||
class StorageManager extends SFStorageManager {
|
||||
|
||||
constructor(dbManager) {
|
||||
super();
|
||||
this.dbManager = dbManager;
|
||||
}
|
||||
|
||||
@@ -100,7 +105,7 @@ class StorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
setItem(key, value, vaultKey) {
|
||||
async setItem(key, value, vaultKey) {
|
||||
var storage = this.getVault(vaultKey);
|
||||
storage.setItem(key, value);
|
||||
|
||||
@@ -109,25 +114,21 @@ class StorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
getItem(key, vault) {
|
||||
async getItem(key, vault) {
|
||||
return this.getItemSync(key, vault);
|
||||
}
|
||||
|
||||
getItemSync(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
return storage.getItem(key);
|
||||
}
|
||||
|
||||
setBooleanValue(key, value, vault) {
|
||||
this.setItem(key, JSON.stringify(value), vault);
|
||||
}
|
||||
|
||||
getBooleanValue(key, vault) {
|
||||
return JSON.parse(this.getItem(key, vault));
|
||||
}
|
||||
|
||||
removeItem(key, vault) {
|
||||
async removeItem(key, vault) {
|
||||
var storage = this.getVault(vault);
|
||||
storage.removeItem(key);
|
||||
return storage.removeItem(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
async clear() {
|
||||
this.memoryStorage.clear();
|
||||
localStorage.clear();
|
||||
}
|
||||
@@ -148,29 +149,29 @@ class StorageManager {
|
||||
}
|
||||
|
||||
writeEncryptedStorageToDisk() {
|
||||
var encryptedStorage = new EncryptedStorage();
|
||||
var encryptedStorage = new SNEncryptedStorage();
|
||||
// Copy over totality of current storage
|
||||
encryptedStorage.storage = this.storageAsHash();
|
||||
encryptedStorage.content.storage = this.storageAsHash();
|
||||
|
||||
// Save new encrypted storage in Fixed storage
|
||||
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams.version);
|
||||
var params = new SFItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams);
|
||||
params.paramsForSync().then((syncParams) => {
|
||||
this.setItem("encryptedStorage", JSON.stringify(syncParams), StorageManager.Fixed);
|
||||
})
|
||||
}
|
||||
|
||||
async decryptStorage() {
|
||||
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
||||
var stored = JSON.parse(this.getItemSync("encryptedStorage", StorageManager.Fixed));
|
||||
await SFJS.itemTransformer.decryptItem(stored, this.encryptedStorageKeys);
|
||||
var encryptedStorage = new EncryptedStorage(stored);
|
||||
var encryptedStorage = new SNEncryptedStorage(stored);
|
||||
|
||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||
for(var key of Object.keys(encryptedStorage.content.storage)) {
|
||||
this.setItem(key, encryptedStorage.storage[key]);
|
||||
}
|
||||
}
|
||||
|
||||
hasPasscode() {
|
||||
return this.getItem("encryptedStorage", StorageManager.Fixed) !== null;
|
||||
return this.getItemSync("encryptedStorage", StorageManager.Fixed) !== null;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,36 +197,44 @@ class StorageManager {
|
||||
this.modelStorageMode = mode;
|
||||
}
|
||||
|
||||
getAllModels(callback) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.getAllModels(callback);
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
async getAllModels() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.getAllModels(resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
saveModel(item) {
|
||||
this.saveModels([item]);
|
||||
async saveModel(item) {
|
||||
return this.saveModels([item]);
|
||||
}
|
||||
|
||||
saveModels(items, onsuccess, onerror) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.saveModels(items, onsuccess, onerror);
|
||||
} else {
|
||||
onsuccess && onsuccess();
|
||||
}
|
||||
async saveModels(items, onsuccess, onerror) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.saveModels(items, resolve, reject);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteModel(item, callback) {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.deleteModel(item, callback);
|
||||
} else {
|
||||
callback && callback();
|
||||
}
|
||||
async deleteModel(item) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if(this.modelStorageMode == StorageManager.Fixed) {
|
||||
this.dbManager.deleteModel(item, resolve);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
clearAllModels(callback) {
|
||||
this.dbManager.clearAllModels(callback);
|
||||
async clearAllModels() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dbManager.clearAllModels(resolve);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,583 +1,7 @@
|
||||
class SyncManager {
|
||||
class SyncManager extends SFSyncManager {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, dbManager, httpManager, $interval, $timeout, storageManager, passcodeManager) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.dbManager = dbManager;
|
||||
this.$interval = $interval;
|
||||
this.$timeout = $timeout;
|
||||
this.storageManager = storageManager;
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.syncStatus = {};
|
||||
this.syncStatusObservers = [];
|
||||
}
|
||||
|
||||
get serverURL() {
|
||||
return this.storageManager.getItem("server") || window._default_sf_server;
|
||||
}
|
||||
|
||||
get masterKey() {
|
||||
return this.storageManager.getItem("mk");
|
||||
}
|
||||
|
||||
registerSyncStatusObserver(callback) {
|
||||
var observer = {key: new Date(), callback: callback};
|
||||
this.syncStatusObservers.push(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
removeSyncStatusObserver(observer) {
|
||||
_.pull(this.syncStatusObservers, observer);
|
||||
}
|
||||
|
||||
syncStatusDidChange() {
|
||||
this.syncStatusObservers.forEach((observer) => {
|
||||
observer.callback(this.syncStatus);
|
||||
})
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
if(items.length == 0) {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var version = this.authManager.offline() ? this.passcodeManager.protocolVersion() : this.authManager.protocolVersion();
|
||||
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
||||
|
||||
Promise.all(items.map(async (item) => {
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams = await itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
}
|
||||
return itemParams;
|
||||
})).then((params) => {
|
||||
this.storageManager.saveModels(params, () => {
|
||||
// on success
|
||||
if(this.syncStatus.localError) {
|
||||
this.syncStatus.localError = null;
|
||||
this.syncStatusDidChange();
|
||||
}
|
||||
callback && callback();
|
||||
}, (error) => {
|
||||
// on error
|
||||
this.syncStatus.localError = error;
|
||||
this.syncStatusDidChange();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
async loadLocalItems(callback) {
|
||||
this.storageManager.getAllModels((items) => {
|
||||
// break it up into chunks to make interface more responsive for large item counts
|
||||
let total = items.length;
|
||||
let iteration = 50;
|
||||
var current = 0;
|
||||
var processed = [];
|
||||
|
||||
var completion = () => {
|
||||
Item.sortItemsByDate(processed);
|
||||
callback(processed);
|
||||
}
|
||||
|
||||
var decryptNext = async () => {
|
||||
var subitems = items.slice(current, current + iteration);
|
||||
var processedSubitems = await this.handleItemsResponse(subitems, null, ModelManager.MappingSourceLocalRetrieved);
|
||||
processed.push(processedSubitems);
|
||||
|
||||
current += subitems.length;
|
||||
|
||||
if(current < total) {
|
||||
this.$timeout(() => { decryptNext(); });
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
|
||||
decryptNext();
|
||||
})
|
||||
}
|
||||
|
||||
syncOffline(items, callback) {
|
||||
// Update all items updated_at to now
|
||||
for(var item of items) {
|
||||
item.updated_at = new Date();
|
||||
}
|
||||
this.writeItemsToLocalStorage(items, true, (responseItems) => {
|
||||
// delete anything needing to be deleted
|
||||
for(var item of items) {
|
||||
if(item.deleted) {
|
||||
this.modelManager.removeItemLocally(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.$rootScope.$broadcast("sync:completed", {});
|
||||
|
||||
// Required in order for modelManager to notify sync observers
|
||||
this.modelManager.didSyncModelsOffline(items);
|
||||
|
||||
if(callback) {
|
||||
callback({success: true});
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
In the case of signing in and merging local data, we alternative UUIDs
|
||||
to avoid overwriting data a user may retrieve that has the same UUID.
|
||||
Alternating here forces us to to create duplicates of the items instead.
|
||||
*/
|
||||
markAllItemsDirtyAndSaveOffline(callback, alternateUUIDs) {
|
||||
|
||||
// use a copy, as alternating uuid will affect array
|
||||
var originalItems = this.modelManager.allItems.filter((item) => {return !item.errorDecrypting}).slice();
|
||||
|
||||
var block = () => {
|
||||
var allItems = this.modelManager.allItems;
|
||||
for(var item of allItems) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
this.writeItemsToLocalStorage(allItems, false, callback);
|
||||
}
|
||||
|
||||
if(alternateUUIDs) {
|
||||
var index = 0;
|
||||
|
||||
let alternateNextItem = () => {
|
||||
if(index >= originalItems.length) {
|
||||
// We don't use originalItems as alternating UUID will have deleted them.
|
||||
block();
|
||||
return;
|
||||
}
|
||||
|
||||
var item = originalItems[index];
|
||||
index++;
|
||||
|
||||
// alternateUUIDForItem last param is a boolean that controls whether the original item
|
||||
// should be removed locally after new item is created. We set this to true, since during sign in,
|
||||
// all item ids are alternated, and we only want one final copy of the entire data set.
|
||||
// Passing false can be desired sometimes, when for example the app has signed out the user,
|
||||
// but for some reason retained their data (This happens in Firefox when using private mode).
|
||||
// In this case, we should pass false so that both copies are kept. However, it's difficult to
|
||||
// detect when the app has entered this state. We will just use true to remove original items for now.
|
||||
this.modelManager.alternateUUIDForItem(item, alternateNextItem, true);
|
||||
}
|
||||
|
||||
alternateNextItem();
|
||||
} else {
|
||||
block();
|
||||
}
|
||||
}
|
||||
|
||||
get syncURL() {
|
||||
return this.serverURL + "/items/sync";
|
||||
}
|
||||
|
||||
set syncToken(token) {
|
||||
this._syncToken = token;
|
||||
this.storageManager.setItem("syncToken", token);
|
||||
}
|
||||
|
||||
get syncToken() {
|
||||
if(!this._syncToken) {
|
||||
this._syncToken = this.storageManager.getItem("syncToken");
|
||||
}
|
||||
return this._syncToken;
|
||||
}
|
||||
|
||||
set cursorToken(token) {
|
||||
this._cursorToken = token;
|
||||
if(token) {
|
||||
this.storageManager.setItem("cursorToken", token);
|
||||
} else {
|
||||
this.storageManager.removeItem("cursorToken");
|
||||
}
|
||||
}
|
||||
|
||||
get cursorToken() {
|
||||
if(!this._cursorToken) {
|
||||
this._cursorToken = this.storageManager.getItem("cursorToken");
|
||||
}
|
||||
return this._cursorToken;
|
||||
}
|
||||
|
||||
get queuedCallbacks() {
|
||||
if(!this._queuedCallbacks) {
|
||||
this._queuedCallbacks = [];
|
||||
}
|
||||
return this._queuedCallbacks;
|
||||
}
|
||||
|
||||
clearQueuedCallbacks() {
|
||||
this._queuedCallbacks = [];
|
||||
}
|
||||
|
||||
callQueuedCallbacksAndCurrent(currentCallback, response) {
|
||||
var allCallbacks = this.queuedCallbacks;
|
||||
if(currentCallback) {
|
||||
allCallbacks.push(currentCallback);
|
||||
}
|
||||
if(allCallbacks.length) {
|
||||
for(var eachCallback of allCallbacks) {
|
||||
eachCallback(response);
|
||||
}
|
||||
this.clearQueuedCallbacks();
|
||||
}
|
||||
}
|
||||
|
||||
beginCheckingIfSyncIsTakingTooLong() {
|
||||
this.syncStatus.checker = this.$interval(function(){
|
||||
// check to see if the ongoing sync is taking too long, alert the user
|
||||
var secondsPassed = (new Date() - this.syncStatus.syncStart) / 1000;
|
||||
var warningThreshold = 5.0; // seconds
|
||||
if(secondsPassed > warningThreshold) {
|
||||
this.$rootScope.$broadcast("sync:taking-too-long");
|
||||
this.stopCheckingIfSyncIsTakingTooLong();
|
||||
}
|
||||
}.bind(this), 500)
|
||||
}
|
||||
|
||||
stopCheckingIfSyncIsTakingTooLong() {
|
||||
this.$interval.cancel(this.syncStatus.checker);
|
||||
}
|
||||
|
||||
lockSyncing() {
|
||||
this.syncLocked = true;
|
||||
}
|
||||
|
||||
unlockSyncing() {
|
||||
this.syncLocked = false;
|
||||
}
|
||||
|
||||
async sync(callback, options = {}, source) {
|
||||
|
||||
if(this.syncLocked) {
|
||||
console.log("Sync Locked, Returning;");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!options) options = {};
|
||||
|
||||
if(typeof callback == 'string') {
|
||||
// is source string, used to avoid filling parameters on call
|
||||
source = callback;
|
||||
callback = null;
|
||||
}
|
||||
|
||||
// console.log("Syncing from", source);
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
// When a user hits the physical refresh button, we want to force refresh, in case
|
||||
// the sync engine is stuck in some inProgress loop.
|
||||
if(this.syncStatus.syncOpInProgress && !options.force) {
|
||||
this.repeatOnCompletion = true;
|
||||
if(callback) {
|
||||
this.queuedCallbacks.push(callback);
|
||||
}
|
||||
|
||||
// write to local storage nonetheless, since some users may see several second delay in server response.
|
||||
// if they close the browser before the ongoing sync request completes, local changes will be lost if we dont save here
|
||||
this.writeItemsToLocalStorage(allDirtyItems, false, null);
|
||||
|
||||
console.log("Sync op in progress; returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||
if(this.authManager.offline()) {
|
||||
this.syncOffline(allDirtyItems, callback);
|
||||
this.modelManager.clearDirtyItems(allDirtyItems);
|
||||
return;
|
||||
}
|
||||
|
||||
var isContinuationSync = this.syncStatus.needsMoreSync;
|
||||
|
||||
this.syncStatus.syncOpInProgress = true;
|
||||
this.syncStatus.syncStart = new Date();
|
||||
this.beginCheckingIfSyncIsTakingTooLong();
|
||||
|
||||
let submitLimit = 100;
|
||||
var subItems = allDirtyItems.slice(0, submitLimit);
|
||||
if(subItems.length < allDirtyItems.length) {
|
||||
// more items left to be synced, repeat
|
||||
this.syncStatus.needsMoreSync = true;
|
||||
} else {
|
||||
this.syncStatus.needsMoreSync = false;
|
||||
}
|
||||
|
||||
if(!isContinuationSync) {
|
||||
this.syncStatus.total = allDirtyItems.length;
|
||||
this.syncStatus.current = 0;
|
||||
}
|
||||
|
||||
// If items are marked as dirty during a long running sync request, total isn't updated
|
||||
// This happens mostly in the case of large imports and sync conflicts where duplicated items are created
|
||||
if(this.syncStatus.current > this.syncStatus.total) {
|
||||
this.syncStatus.total = this.syncStatus.current;
|
||||
}
|
||||
|
||||
// when doing a sync request that returns items greater than the limit, and thus subsequent syncs are required,
|
||||
// we want to keep track of all retreived items, then save to local storage only once all items have been retrieved,
|
||||
// so that relationships remain intact
|
||||
if(!this.allRetreivedItems) {
|
||||
this.allRetreivedItems = [];
|
||||
}
|
||||
|
||||
// We also want to do this for savedItems
|
||||
if(!this.allSavedItems) {
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
|
||||
var version = this.authManager.protocolVersion();
|
||||
var keys = this.authManager.keys();
|
||||
|
||||
var params = {};
|
||||
params.limit = 150;
|
||||
|
||||
await Promise.all(subItems.map((item) => {
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
})).then((itemsParams) => {
|
||||
params.items = itemsParams;
|
||||
})
|
||||
|
||||
for(var item of subItems) {
|
||||
// Reset dirty counter to 0, since we're about to sync it.
|
||||
// This means anyone marking the item as dirty after this will cause it so sync again and not be cleared on sync completion.
|
||||
item.dirtyCount = 0;
|
||||
}
|
||||
|
||||
params.sync_token = this.syncToken;
|
||||
params.cursor_token = this.cursorToken;
|
||||
|
||||
var onSyncCompletion = function(response) {
|
||||
this.stopCheckingIfSyncIsTakingTooLong();
|
||||
}.bind(this);
|
||||
|
||||
var onSyncSuccess = async function(response) {
|
||||
// Check to make sure any subItem hasn't been marked as dirty again while a sync was ongoing
|
||||
var itemsToClearAsDirty = [];
|
||||
for(var item of subItems) {
|
||||
if(item.dirtyCount == 0) {
|
||||
// Safe to clear as dirty
|
||||
itemsToClearAsDirty.push(item);
|
||||
}
|
||||
}
|
||||
this.modelManager.clearDirtyItems(itemsToClearAsDirty);
|
||||
this.syncStatus.error = null;
|
||||
|
||||
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||
|
||||
// Filter retrieved_items to remove any items that may be in saved_items for this complete sync operation
|
||||
// When signing in, and a user requires many round trips to complete entire retrieval of data, an item may be saved
|
||||
// on the first trip, then on subsequent trips using cursor_token, this same item may be returned, since it's date is
|
||||
// greater than cursor_token. We keep track of all saved items in whole sync operation with this.allSavedItems
|
||||
// We need this because singletonManager looks at retrievedItems as higher precendence than savedItems, but if it comes in both
|
||||
// then that's problematic.
|
||||
let allSavedUUIDs = this.allSavedItems.map((item) => {return item.uuid});
|
||||
response.retrieved_items = response.retrieved_items.filter((candidate) => {return !allSavedUUIDs.includes(candidate.uuid)});
|
||||
|
||||
// Map retrieved items to local data
|
||||
// Note that deleted items will not be returned
|
||||
var retrieved = await this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||
|
||||
// Append items to master list of retrieved items for this ongoing sync operation
|
||||
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
|
||||
|
||||
// Merge only metadata for saved items
|
||||
// we write saved items to disk now because it clears their dirty status then saves
|
||||
// if we saved items before completion, we had have to save them as dirty and save them again on success as clean
|
||||
var omitFields = ["content", "auth_hash"];
|
||||
|
||||
// Map saved items to local data
|
||||
var saved = await this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||
|
||||
// Append items to master list of saved items for this ongoing sync operation
|
||||
this.allSavedItems = this.allSavedItems.concat(saved);
|
||||
|
||||
// Create copies of items or alternate their uuids if neccessary
|
||||
var unsaved = response.unsaved;
|
||||
this.handleUnsavedItemsResponse(unsaved)
|
||||
|
||||
this.writeItemsToLocalStorage(saved, false, null);
|
||||
|
||||
this.syncStatus.syncOpInProgress = false;
|
||||
this.syncStatus.current += subItems.length;
|
||||
|
||||
// set the sync token at the end, so that if any errors happen above, you can resync
|
||||
this.syncToken = response.sync_token;
|
||||
this.cursorToken = response.cursor_token;
|
||||
|
||||
onSyncCompletion(response);
|
||||
|
||||
if(this.cursorToken || this.syncStatus.needsMoreSync) {
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options, "onSyncSuccess cursorToken || needsMoreSync");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else if(this.repeatOnCompletion) {
|
||||
this.repeatOnCompletion = false;
|
||||
setTimeout(function () {
|
||||
this.sync(callback, options, "onSyncSuccess repeatOnCompletion");
|
||||
}.bind(this), 10); // wait 10ms to allow UI to update
|
||||
} else {
|
||||
this.writeItemsToLocalStorage(this.allRetreivedItems, false, null);
|
||||
|
||||
// The number of changed items that constitute a major change
|
||||
// This is used by the desktop app to create backups
|
||||
let majorDataChangeThreshold = 10;
|
||||
if(
|
||||
this.allRetreivedItems.length >= majorDataChangeThreshold ||
|
||||
saved.length >= majorDataChangeThreshold ||
|
||||
unsaved.length >= majorDataChangeThreshold
|
||||
) {
|
||||
this.$rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
|
||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems, savedItems: this.allSavedItems});
|
||||
|
||||
this.allRetreivedItems = [];
|
||||
this.allSavedItems = [];
|
||||
}
|
||||
}.bind(this);
|
||||
|
||||
try {
|
||||
this.httpManager.postAbsolute(this.syncURL, params, function(response){
|
||||
|
||||
try {
|
||||
onSyncSuccess(response);
|
||||
} catch(e) {
|
||||
console.log("Caught sync success exception:", e);
|
||||
}
|
||||
|
||||
}.bind(this), function(response, statusCode){
|
||||
if(statusCode == 401) {
|
||||
alert("Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session.");
|
||||
}
|
||||
console.log("Sync error: ", response);
|
||||
var error = response ? response.error : {message: "Could not connect to server."};
|
||||
|
||||
this.syncStatus.syncOpInProgress = false;
|
||||
this.syncStatus.error = error;
|
||||
this.writeItemsToLocalStorage(allDirtyItems, false, null);
|
||||
|
||||
onSyncCompletion(response);
|
||||
|
||||
this.$rootScope.$broadcast("sync:error", error);
|
||||
|
||||
this.callQueuedCallbacksAndCurrent(callback, {error: "Sync error"});
|
||||
}.bind(this));
|
||||
}
|
||||
catch(e) {
|
||||
console.log("Sync exception caught:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async handleItemsResponse(responseItems, omitFields, source) {
|
||||
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
||||
await SFJS.itemTransformer.decryptMultipleItems(responseItems, keys);
|
||||
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
|
||||
|
||||
// During the decryption process, items may be marked as "errorDecrypting". If so, we want to be sure
|
||||
// to persist this new state by writing these items back to local storage. When an item's "errorDecrypting"
|
||||
// flag is changed, its "errorDecryptingValueChanged" flag will be set, so we can find these items by filtering (then unsetting) below:
|
||||
var itemsWithErrorStatusChange = items.filter((item) => {
|
||||
var valueChanged = item.errorDecryptingValueChanged;
|
||||
// unset after consuming value
|
||||
item.errorDecryptingValueChanged = false;
|
||||
return valueChanged;
|
||||
});
|
||||
if(itemsWithErrorStatusChange.length > 0) {
|
||||
this.writeItemsToLocalStorage(itemsWithErrorStatusChange, false, null);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
refreshErroredItems() {
|
||||
var erroredItems = this.modelManager.allItems.filter((item) => {return item.errorDecrypting == true});
|
||||
if(erroredItems.length > 0) {
|
||||
this.handleItemsResponse(erroredItems, null, ModelManager.MappingSourceLocalRetrieved);
|
||||
}
|
||||
}
|
||||
|
||||
async handleUnsavedItemsResponse(unsaved) {
|
||||
if(unsaved.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Handle unsaved", unsaved);
|
||||
|
||||
var i = 0;
|
||||
var handleNext = async () => {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
return;
|
||||
}
|
||||
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
await SFJS.itemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
|
||||
if(!item) {
|
||||
// Could be deleted
|
||||
return;
|
||||
}
|
||||
|
||||
var error = mapping.error;
|
||||
|
||||
if(error.tag === "uuid_conflict") {
|
||||
// UUID conflicts can occur if a user attempts to
|
||||
// import an old data archive with uuids from the old account into a new account
|
||||
this.modelManager.alternateUUIDForItem(item, () => {
|
||||
i++;
|
||||
handleNext();
|
||||
}, true);
|
||||
}
|
||||
|
||||
else if(error.tag === "sync_conflict") {
|
||||
// Create a new item with the same contents of this item if the contents differ
|
||||
|
||||
// We want a new uuid for the new item. Note that this won't neccessarily adjust references.
|
||||
itemResponse.uuid = null;
|
||||
|
||||
var dup = this.modelManager.createDuplicateItem(itemResponse);
|
||||
if(!itemResponse.deleted && !item.isItemContentEqualWith(dup)) {
|
||||
this.modelManager.addItem(dup);
|
||||
dup.conflict_of = item.uuid;
|
||||
dup.setDirty(true);
|
||||
}
|
||||
|
||||
i++;
|
||||
handleNext();
|
||||
}
|
||||
}
|
||||
|
||||
handleNext();
|
||||
}
|
||||
|
||||
clearSyncToken() {
|
||||
this.storageManager.removeItem("syncToken");
|
||||
}
|
||||
|
||||
destroyLocalData(callback) {
|
||||
this.storageManager.clear();
|
||||
this.storageManager.clearAllModels(function(){
|
||||
if(callback) {
|
||||
this.$timeout(function(){
|
||||
callback();
|
||||
})
|
||||
}
|
||||
}.bind(this));
|
||||
constructor(modelManager, storageManager, httpManager, $timeout, $interval) {
|
||||
super(modelManager, storageManager, httpManager, $timeout, $interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ $heading-height: 75px;
|
||||
text-align: right;
|
||||
color: rgba(black, 0.23);
|
||||
|
||||
.error {
|
||||
&.error, .error {
|
||||
color: #f6a200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#item-preview-modal {
|
||||
> .content {
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: white;
|
||||
}
|
||||
@@ -99,6 +106,7 @@
|
||||
.component-view {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// not sure why we need this. Removed because it creates unncessary scroll bars. Tested on folders extension, creates horizontal scrollbar at bottom on windows
|
||||
// overflow: auto;
|
||||
|
||||
@@ -87,10 +87,13 @@
|
||||
height: inherit;
|
||||
|
||||
// Autohide scrollbar on Windows.
|
||||
// Unfortunately must affect every platform since no way to hide just for Windows.
|
||||
overflow-y: hidden;
|
||||
&:hover {
|
||||
overflow-y: scroll;
|
||||
@at-root {
|
||||
.windows-web &, .windows-desktop & {
|
||||
overflow-y: hidden;
|
||||
&:hover {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
}
|
||||
|
||||
|
||||
.app-bar {
|
||||
&.no-top-edge {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,3 +53,9 @@
|
||||
color: $blue-color;
|
||||
}
|
||||
}
|
||||
|
||||
#session-history-menu {
|
||||
.menu-panel .row .sublabel.opaque {
|
||||
opacity: 1.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +159,8 @@
|
||||
%button.button.info{"type" => "submit"}
|
||||
.label Decrypt & Import
|
||||
%p
|
||||
Importing from backup will overwrite existing notes with matching note from backup. Existing notes not found in the backup will remain as-is and won't be overwritten.
|
||||
%p If you'd like to import only a selection of notes instead of the whole file, please use the Batch Manager extension instead.
|
||||
Importing from backup will not overwrite existing data, but instead create a duplicate of any differing data.
|
||||
%p If you'd like to import only a selection of items instead of the whole file, please use the Batch Manager extension.
|
||||
.panel-row
|
||||
.spinner.small.info{"ng-if" => "importData.loading"}
|
||||
.footer
|
||||
|
||||
@@ -19,14 +19,4 @@
|
||||
%strong {{action.access_type}}
|
||||
access to this note.
|
||||
|
||||
|
||||
.modal.medium-text.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"}
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
.header
|
||||
%h1.title Preview
|
||||
%a.close-button.info{"ng-click" => "renderData.showRenderModal = false; $event.stopPropagation();"} Close
|
||||
.content.selectable
|
||||
%h2 {{renderData.title}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{renderData.text}}
|
||||
%menu-row{"ng-if" => "extension.actionsWithContextForItem(item).length == 0", "label" => "'No Actions Available'", "faded" => "true"}
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
.sn-component{"ng-if" => "issueLoading"}
|
||||
.app-bar.no-edges.no-top-edge
|
||||
.left
|
||||
.item
|
||||
.label.warning There was an issue loading {{component.name}}.
|
||||
.right
|
||||
.item{"ng-click" => "reloadComponent()"}
|
||||
.label Reload
|
||||
|
||||
.sn-component{"ng-if" => "error == 'expired'"}
|
||||
.panel.static
|
||||
.content
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
.modal.medium#item-preview-modal
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
.header
|
||||
%h1.title Preview
|
||||
.horizontal-group
|
||||
%a.close-button.info{"ng-click" => "restore(false)"} Restore
|
||||
%a.close-button.info{"ng-click" => "restore(true)"} Restore as copy
|
||||
%a.close-button.info{"ng-click" => "dismiss(); $event.stopPropagation()"} Close
|
||||
.content.selectable
|
||||
%h2 {{content.title}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{content.text}}
|
||||
@@ -0,0 +1,23 @@
|
||||
.sn-component#session-history-menu
|
||||
.menu-panel.dropdown-menu
|
||||
.header
|
||||
.column
|
||||
%h4.title {{history.entries.length || 'No'}} revisions
|
||||
%h4{"ng-click" => "showOptions = !showOptions; $event.stopPropagation();"}
|
||||
%a Options
|
||||
|
||||
%div{"ng-if" => "showOptions"}
|
||||
%menu-row{"label" => "'Clear note local history'", "ng-click" => "clearItemHistory(); $event.stopPropagation();"}
|
||||
%menu-row{"label" => "'Clear all local history'", "ng-click" => "clearAllHistory(); $event.stopPropagation();"}
|
||||
%menu-row{"label" => "(autoOptimize ? 'Disable' : 'Enable') + ' auto cleanup'", "ng-click" => "toggleAutoOptimize(); $event.stopPropagation();"}
|
||||
.sublabel
|
||||
Automatically cleans up small revisions to conserve space.
|
||||
%menu-row{"label" => "(diskEnabled ? 'Disable' : 'Enable') + ' saving history to disk'", "ng-click" => "toggleDiskSaving(); $event.stopPropagation();"}
|
||||
.sublabel
|
||||
Saving to disk may increase app loading time and memory footprint.
|
||||
|
||||
%menu-row{"ng-repeat" => "revision in history.entries",
|
||||
"ng-click" => "openRevision(revision); $event.stopPropagation();",
|
||||
"label" => "revision.previewTitle()"}
|
||||
.sublabel.opaque{"ng-class" => "classForRevision(revision)"}
|
||||
{{revision.previewSubTitle()}}
|
||||
@@ -50,6 +50,10 @@
|
||||
.label Actions
|
||||
%actions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||
|
||||
.item{"ng-click" => "ctrl.showSessionHistory = !ctrl.showSessionHistory; ctrl.showMenu = false; ctrl.showEditorMenu = false;", "click-outside" => "ctrl.showSessionHistory = false;", "is-open" => "ctrl.showSessionHistory"}
|
||||
.label Session History
|
||||
%session-history-menu{"ng-if" => "ctrl.showSessionHistory", "item" => "ctrl.note"}
|
||||
|
||||
.editor-content#editor-content{"ng-if" => "ctrl.noteReady && !ctrl.note.errorDecrypting"}
|
||||
|
||||
%panel-resizer.left{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "property" => "'left'", "hoverable" => "true"}
|
||||
@@ -58,7 +62,7 @@
|
||||
|
||||
%textarea.editable#note-text-editor{"ng-if" => "!ctrl.selectedEditor", "ng-model" => "ctrl.note.text", "ng-readonly" => "ctrl.note.locked",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-trim" => "false", "ng-click" => "ctrl.clickedTextArea()",
|
||||
"ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}"}
|
||||
"ng-focus" => "ctrl.onContentFocus()", "dir" => "auto", "ng-attr-spellcheck" => "{{ctrl.spellcheck}}", "ng-model-options"=>"{ debounce: 200 }"}
|
||||
{{ctrl.onSystemEditorLoad()}}
|
||||
|
||||
%panel-resizer{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "hoverable" => "true", "property" => "'right'"}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
.main-ui-view{"ng-class" => "platform"}
|
||||
%lock-screen{"ng-if" => "needsUnlock", "on-success" => "onSuccessfulUnlock"}
|
||||
.app#app{"ng-if" => "!needsUnlock"}
|
||||
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "will-select" => "tagsWillMakeSelection", "selection-made" => "tagsSelectionMade",
|
||||
%tags-section{"save" => "tagsSave", "add-new" => "tagsAddNew", "selection-made" => "tagsSelectionMade",
|
||||
"all-tag" => "allTag", "archive-tag" => "archiveTag", "tags" => "tags", "remove-tag" => "removeTag"}
|
||||
|
||||
%notes-section{"add-new" => "notesAddNew", "selection-made" => "notesSelectionMade", "tag" => "selectedTag"}
|
||||
|
||||
%editor-section{"note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"}
|
||||
%editor-section{"note" => "selectedNote", "remove" => "deleteNote", "update-tags" => "updateTagsForNote"}
|
||||
|
||||
%footer{"ng-if" => "!needsUnlock"}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
%menu-row{"label" => "'Date Modified'", "circle" => "ctrl.sortBy == 'client_updated_at' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByUpdated()", "desc" => "'Sort notes with the most recently updated first'"}
|
||||
%menu-row{"label" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByTitle()", "desc" => "'Sort notes alphabetically by their title'"}
|
||||
|
||||
.section{"ng-if" => "!ctrl.tag.archiveTag"}
|
||||
.section{"ng-if" => "!ctrl.tag.isSmartTag()"}
|
||||
.header
|
||||
%h4.title Display
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
%i.icon.ion-bookmark
|
||||
%strong.medium-text Pinned
|
||||
|
||||
.archived.tinted{"ng-if" => "note.archived && !ctrl.tag.archiveTag", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
|
||||
.archived.tinted{"ng-if" => "note.archived && !ctrl.tag.isSmartTag()", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
|
||||
%i.icon.ion-ios-box
|
||||
%strong.medium-text Archived
|
||||
|
||||
|
||||
6609
package-lock.json
generated
6609
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,12 @@
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"sn-stylekit": "1.0.15",
|
||||
"standard-file-js": "0.3.1"
|
||||
"standard-file-js": "0.3.4",
|
||||
"sn-models": "0.1.1",
|
||||
"connect": "^3.6.6",
|
||||
"mocha": "^5.2.0",
|
||||
"serve-static": "^1.13.2",
|
||||
"chai": "^4.1.2"
|
||||
},
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
|
||||
Submodule public/extensions/extensions-manager updated: 8784a82e18...68b78744cb
@@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ApikeyControllerTest < ActionController::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class NamesControllerTest < ActionController::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ProtoControllerTest < ActionController::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
0
test/fixtures/.keep
vendored
0
test/fixtures/.keep
vendored
7
test/fixtures/api_keys.yml
vendored
7
test/fixtures/api_keys.yml
vendored
@@ -1,7 +0,0 @@
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
access_token: MyString
|
||||
|
||||
two:
|
||||
access_token: MyString
|
||||
7
test/fixtures/names.yml
vendored
7
test/fixtures/names.yml
vendored
@@ -1,7 +0,0 @@
|
||||
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
one:
|
||||
text: MyString
|
||||
|
||||
two:
|
||||
text: MyString
|
||||
@@ -6,11 +6,6 @@ describe("date filter", function() {
|
||||
$filter = _$filter_;
|
||||
}));
|
||||
|
||||
it('returns a defined time', function() {
|
||||
var date = $filter('appDate');
|
||||
expect(date(Date())).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns time', function() {
|
||||
var dateTime = $filter('appDateTime');
|
||||
expect(dateTime(Date())).toBeDefined();
|
||||
|
||||
87
test/mocha/lib/factory.js
Normal file
87
test/mocha/lib/factory.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import '../../../vendor/assets/javascripts/compiled.js';
|
||||
import '../../../node_modules/chai/chai.js';
|
||||
import '../vendor/chai-as-promised-built.js';
|
||||
import '../../../vendor/assets/javascripts/lodash/lodash.custom.js';
|
||||
|
||||
import LocalStorageManager from './localStorageManager.js';
|
||||
const sf_default = new StandardFile();
|
||||
SFItem.AppDomain = "org.standardnotes.sn";
|
||||
|
||||
var _globalStorageManager = null;
|
||||
var _globalHttpManager = null;
|
||||
var _globalAuthManager = null;
|
||||
var _globalModelManager = null;
|
||||
var _globalStandardFile = null;
|
||||
|
||||
export default class Factory {
|
||||
|
||||
static initialize() {
|
||||
this.globalStorageManager();
|
||||
this.globalHttpManager();
|
||||
this.globalAuthManager();
|
||||
this.globalModelManager();
|
||||
}
|
||||
|
||||
static globalStorageManager() {
|
||||
if(_globalStorageManager == null) { _globalStorageManager = new LocalStorageManager(); }
|
||||
return _globalStorageManager;
|
||||
}
|
||||
|
||||
static globalHttpManager() {
|
||||
if(_globalHttpManager == null) {
|
||||
_globalHttpManager = new SFHttpManager();
|
||||
_globalHttpManager.setJWTRequestHandler(async () => {
|
||||
return this.globalStorageManager().getItem("jwt");;
|
||||
})
|
||||
}
|
||||
return _globalHttpManager;
|
||||
}
|
||||
|
||||
static globalAuthManager() {
|
||||
if(_globalAuthManager == null) { _globalAuthManager = new SFAuthManager(_globalStorageManager, _globalHttpManager); }
|
||||
return _globalAuthManager;
|
||||
}
|
||||
|
||||
static globalModelManager() {
|
||||
if(_globalModelManager == null) { _globalModelManager = new SFModelManager(); }
|
||||
return _globalModelManager;
|
||||
}
|
||||
|
||||
static globalStandardFile() {
|
||||
if(_globalStandardFile == null) { _globalStandardFile = new StandardFile(); }
|
||||
return _globalStandardFile;
|
||||
}
|
||||
|
||||
static createModelManager() {
|
||||
return new SFModelManager();
|
||||
}
|
||||
|
||||
static createItemParams() {
|
||||
var params = {
|
||||
uuid: SFJS.crypto.generateUUIDSync(),
|
||||
content_type: "Note",
|
||||
content: {
|
||||
title: "hello",
|
||||
text: "world"
|
||||
}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
|
||||
static createItem() {
|
||||
return new SFItem(this.createItemParams());
|
||||
}
|
||||
|
||||
static serverURL() {
|
||||
return "http://localhost:3000";
|
||||
}
|
||||
|
||||
static async newRegisteredUser(email, password) {
|
||||
let url = this.serverURL();
|
||||
if(!email) email = sf_default.crypto.generateUUIDSync();
|
||||
if(!password) password = sf_default.crypto.generateUUIDSync();
|
||||
return this.globalAuthManager().register(url, email, password, false);
|
||||
}
|
||||
}
|
||||
|
||||
Factory.initialize();
|
||||
69
test/mocha/lib/localStorageManager.js
Normal file
69
test/mocha/lib/localStorageManager.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// A test StorageManager class using LocalStorage
|
||||
|
||||
export default class LocalStorageManager extends SFStorageManager {
|
||||
|
||||
/* Simple Key/Value Storage */
|
||||
|
||||
async setItem(key, value, vaultKey) {
|
||||
localStorage.setItem(key, value);
|
||||
}
|
||||
|
||||
async getItem(key, vault) {
|
||||
return localStorage.getItem(key)
|
||||
}
|
||||
|
||||
async removeItem(key, vault) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
// clear only simple key/values
|
||||
localStorage.clear();
|
||||
}
|
||||
|
||||
/*
|
||||
Model Storage
|
||||
*/
|
||||
|
||||
async getAllModels() {
|
||||
var models = [];
|
||||
for(var key in localStorage) {
|
||||
if(key.startsWith("item-")) {
|
||||
models.push(JSON.parse(localStorage[key]))
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
|
||||
async saveModel(item) {
|
||||
return this.saveModels([item]);
|
||||
}
|
||||
|
||||
async saveModels(items) {
|
||||
return Promise.all(items.map((item) => {
|
||||
return this.setItem(`item-${item.uuid}`, JSON.stringify(item));
|
||||
}))
|
||||
}
|
||||
|
||||
async deleteModel(item,) {
|
||||
return this.removeItem(`item-${item.uuid}`);
|
||||
}
|
||||
|
||||
async clearAllModels() {
|
||||
// clear only models
|
||||
for(var key in localStorage) {
|
||||
if(key.startsWith("item-")) {
|
||||
this.removeItem(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
clearAllData() {
|
||||
return Promise.all([
|
||||
this.clear(),
|
||||
this.clearAllModels()
|
||||
])
|
||||
}
|
||||
}
|
||||
548
test/mocha/models.test.js
Normal file
548
test/mocha/models.test.js
Normal file
@@ -0,0 +1,548 @@
|
||||
import '../../vendor/assets/javascripts/compiled.js';
|
||||
import '../../node_modules/chai/chai.js';
|
||||
import './vendor/chai-as-promised-built.js';
|
||||
import '../../vendor/assets/javascripts/lodash/lodash.custom.js';
|
||||
import Factory from './lib/factory.js';
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
var expect = chai.expect;
|
||||
|
||||
const getNoteParams = () => {
|
||||
var params = {
|
||||
uuid: SFJS.crypto.generateUUIDSync(),
|
||||
content_type: "Note",
|
||||
content: {
|
||||
title: "hello",
|
||||
text: "world"
|
||||
}
|
||||
};
|
||||
return params;
|
||||
}
|
||||
|
||||
const createRelatedNoteTagPair = () => {
|
||||
let noteParams = getNoteParams();
|
||||
let tagParams = {
|
||||
uuid: SFJS.crypto.generateUUIDSync(),
|
||||
content_type: "Tag",
|
||||
content: {
|
||||
title: "thoughts",
|
||||
}
|
||||
};
|
||||
tagParams.content.references = [
|
||||
{
|
||||
uuid: noteParams.uuid,
|
||||
content_type: noteParams.content_type
|
||||
}
|
||||
]
|
||||
|
||||
noteParams.content.references = []
|
||||
|
||||
return [noteParams, tagParams];
|
||||
}
|
||||
describe("notes and tags", () => {
|
||||
|
||||
it('uses proper class for note', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
let noteParams = getNoteParams();
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
expect(note).to.be.an.instanceOf(SNNote);
|
||||
});
|
||||
|
||||
it('properly handles legacy relationships', () => {
|
||||
// legacy relationships are when a note has a reference to a tag
|
||||
let modelManager = Factory.createModelManager();
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
tagParams.content.references = null;
|
||||
noteParams.content.references = [
|
||||
{
|
||||
uuid: tagParams.uuid,
|
||||
content_type: tagParams.content_type
|
||||
}
|
||||
];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.tags.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
})
|
||||
|
||||
it('creates two-way relationship between note and tag', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
expect(noteParams.content.references.length).to.equal(0);
|
||||
expect(tagParams.content.references.length).to.equal(1);
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
// expect to be false
|
||||
expect(note.dirty).to.not.be.ok;
|
||||
expect(tag.dirty).to.not.be.ok;
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
|
||||
expect(note.hasRelationshipWithItem(tag)).to.equal(false);
|
||||
expect(tag.hasRelationshipWithItem(note)).to.equal(true);
|
||||
|
||||
expect(note.tags.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
|
||||
modelManager.setItemToBeDeleted(note);
|
||||
expect(note.tags.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
|
||||
// expect to be true
|
||||
expect(note.dirty).to.be.ok;
|
||||
expect(tag.dirty).to.be.ok;
|
||||
});
|
||||
|
||||
it('handles remote deletion of relationship', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
|
||||
tagParams.content.references = [];
|
||||
modelManager.mapResponseItemsToLocalModels([tagParams]);
|
||||
|
||||
expect(tag.content.references.length).to.equal(0);
|
||||
expect(note.tags.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
|
||||
// expect to be false
|
||||
expect(note.dirty).to.not.be.ok;
|
||||
expect(tag.dirty).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('resets cached note tags string when tag is deleted from remote source', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.tagsString().length).to.not.equal(0);
|
||||
|
||||
tagParams.deleted = true;
|
||||
modelManager.mapResponseItemsToLocalModels([tagParams]);
|
||||
|
||||
// should be null
|
||||
expect(note.savedTagsString).to.not.be.ok;
|
||||
|
||||
expect(note.tags.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets cached note tags string when tag reference is removed from remote source', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.tagsString().length).to.not.equal(0);
|
||||
|
||||
tagParams.content.references = [];
|
||||
modelManager.mapResponseItemsToLocalModels([tagParams]);
|
||||
|
||||
// should be null
|
||||
expect(note.savedTagsString).to.not.be.ok;
|
||||
|
||||
expect(note.tags.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets cached note tags string when tag is renamed', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.tagsString()).to.equal(`#${tagParams.content.title}`);
|
||||
|
||||
var title = Math.random();
|
||||
|
||||
// Saving involves modifying local state first, then syncing with omitting content.
|
||||
tag.title = title;
|
||||
tagParams.content.title = title;
|
||||
// simulate a save, which omits `content`
|
||||
modelManager.mapResponseItemsToLocalModelsOmittingFields([tagParams], ['content']);
|
||||
|
||||
// should be null
|
||||
expect(note.savedTagsString).to.not.be.ok;
|
||||
expect(note.tagsString()).to.equal(`#${title}`);
|
||||
});
|
||||
|
||||
it('handles removing relationship between note and tag', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
|
||||
tag.removeItemAsRelationship(note);
|
||||
modelManager.mapResponseItemsToLocalModels([tag]);
|
||||
|
||||
expect(note.tags.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('properly handles tag duplication', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
// Usually content_type will be provided by a server response
|
||||
var duplicateParams = _.merge({content_type: "Tag"}, tag);
|
||||
duplicateParams.uuid = null;
|
||||
|
||||
expect(duplicateParams.content_type).to.equal("Tag");
|
||||
var duplicateTag = modelManager.createDuplicateItem(duplicateParams);
|
||||
modelManager.addDuplicatedItem(duplicateTag, tag);
|
||||
|
||||
expect(tag.uuid).to.not.equal(duplicateTag.uuid);
|
||||
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
|
||||
expect(duplicateTag.content.references.length).to.equal(1);
|
||||
expect(duplicateTag.notes.length).to.equal(1);
|
||||
|
||||
expect(note.tags.length).to.equal(2);
|
||||
|
||||
var noteTag1 = note.tags[0];
|
||||
var noteTag2 = note.tags[1];
|
||||
expect(noteTag1.uuid).to.not.equal(noteTag2.uuid);
|
||||
|
||||
// expect to be false
|
||||
expect(note.dirty).to.not.be.ok;
|
||||
expect(tag.dirty).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('duplicating a note should maintain its tag references', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
// Usually content_type will be provided by a server response
|
||||
var duplicateParams = _.merge({content_type: "Note"}, note);
|
||||
duplicateParams.uuid = null;
|
||||
|
||||
var duplicateNote = modelManager.createDuplicateItem(duplicateParams);
|
||||
modelManager.addDuplicatedItem(duplicateNote, note);
|
||||
|
||||
expect(note.uuid).to.not.equal(duplicateNote.uuid);
|
||||
|
||||
expect(duplicateNote.tags.length).to.equal(note.tags.length);
|
||||
});
|
||||
|
||||
it('deleting a note should update tag references', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(note.tags.length).to.equal(1);
|
||||
|
||||
modelManager.setItemToBeDeleted(tag);
|
||||
modelManager.mapResponseItemsToLocalModels([tag]);
|
||||
expect(tag.content.references.length).to.equal(0);
|
||||
expect(tag.notes.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('importing existing data should keep relationships valid', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(note.tags.length).to.equal(1);
|
||||
|
||||
modelManager.importItems([noteParams, tagParams]);
|
||||
|
||||
expect(modelManager.allItems.length).to.equal(2);
|
||||
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(note.referencingObjects.length).to.equal(1);
|
||||
expect(note.tags.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('importing data with differing content should create duplicates', () => {
|
||||
let modelManager = Factory.createModelManager();
|
||||
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
noteParams.content.title = Math.random();
|
||||
tagParams.content.title = Math.random();
|
||||
modelManager.importItems([noteParams, tagParams]);
|
||||
|
||||
expect(modelManager.allItems.length).to.equal(4);
|
||||
|
||||
var newNote = modelManager.allItemsMatchingTypes(["Note"])[1];
|
||||
var newTag = modelManager.allItemsMatchingTypes(["Tag"])[1];
|
||||
|
||||
expect(newNote.uuid).to.not.equal(note.uuid);
|
||||
expect(newTag.uuid).to.not.equal(tag.uuid);
|
||||
|
||||
expect(tag.content.references.length).to.equal(2);
|
||||
expect(tag.notes.length).to.equal(2);
|
||||
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
expect(note.referencingObjects.length).to.equal(2);
|
||||
expect(note.tags.length).to.equal(2);
|
||||
|
||||
expect(newTag.content.references.length).to.equal(1);
|
||||
expect(newTag.notes.length).to.equal(1);
|
||||
|
||||
expect(newNote.content.references.length).to.equal(0);
|
||||
expect(newNote.referencingObjects.length).to.equal(1);
|
||||
expect(newNote.tags.length).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncing", () => {
|
||||
var totalItemCount = 0;
|
||||
|
||||
beforeEach((done) => {
|
||||
var email = Factory.globalStandardFile().crypto.generateUUIDSync();
|
||||
var password = Factory.globalStandardFile().crypto.generateUUIDSync();
|
||||
Factory.globalStorageManager().clearAllData().then(() => {
|
||||
Factory.newRegisteredUser(email, password).then((user) => {
|
||||
done();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
let modelManager = Factory.createModelManager();
|
||||
let authManager = Factory.globalAuthManager();
|
||||
let syncManager = new SFSyncManager(modelManager, Factory.globalStorageManager(), Factory.globalHttpManager());
|
||||
|
||||
syncManager.setKeyRequestHandler(async () => {
|
||||
return {
|
||||
keys: await authManager.keys(),
|
||||
auth_params: await authManager.getAuthParams(),
|
||||
offline: false
|
||||
};
|
||||
})
|
||||
|
||||
const wait = (secs) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(function () {
|
||||
resolve();
|
||||
}, secs * 1000);
|
||||
})
|
||||
}
|
||||
|
||||
it('syncing a note many times does not cause duplication', async () => {
|
||||
modelManager.handleSignout();
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
for(var i = 0; i < 9; i++) {
|
||||
note.setDirty(true);
|
||||
tag.setDirty(true);
|
||||
await syncManager.sync();
|
||||
syncManager.clearSyncToken();
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(note.tags.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
expect(modelManager.allItems.length).to.equal(2);
|
||||
console.log("Waiting 1.1s...");
|
||||
await wait(1.1);
|
||||
|
||||
}
|
||||
}).timeout(20000);
|
||||
|
||||
it("handles signing in and merging data", async () => {
|
||||
|
||||
let syncManager = new SFSyncManager(modelManager, Factory.globalStorageManager(), Factory.globalHttpManager());
|
||||
|
||||
// be offline
|
||||
syncManager.setKeyRequestHandler(async () => {
|
||||
return {
|
||||
offline: true
|
||||
};
|
||||
})
|
||||
|
||||
modelManager.handleSignout();
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let originalNote = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let originalTag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
originalNote.setDirty(true);
|
||||
originalTag.setDirty(true);
|
||||
|
||||
await syncManager.sync();
|
||||
|
||||
expect(originalTag.content.references.length).to.equal(1);
|
||||
expect(originalTag.notes.length).to.equal(1);
|
||||
expect(originalNote.tags.length).to.equal(1);
|
||||
|
||||
// go online
|
||||
syncManager.setKeyRequestHandler(async () => {
|
||||
return {
|
||||
keys: await authManager.keys(),
|
||||
auth_params: await authManager.getAuthParams(),
|
||||
offline: false
|
||||
};
|
||||
})
|
||||
|
||||
// when signing in, all local items are cleared from storage (but kept in memory; to clear desktop logs),
|
||||
// then resaved with alternated uuids.
|
||||
await Factory.globalStorageManager().clearAllModels();
|
||||
return expect(syncManager.markAllItemsDirtyAndSaveOffline(true)).to.be.fulfilled.then(() => {
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
expect(modelManager.allItems.length).to.equal(2);
|
||||
|
||||
expect(note.uuid).to.not.equal(originalNote.uuid);
|
||||
expect(tag.uuid).to.not.equal(originalTag.uuid);
|
||||
|
||||
expect(tag.content.references.length).to.equal(1);
|
||||
expect(note.content.references.length).to.equal(0);
|
||||
|
||||
expect(note.referencingObjects.length).to.equal(1);
|
||||
expect(tag.notes.length).to.equal(1);
|
||||
expect(note.tags.length).to.equal(1);
|
||||
});
|
||||
})
|
||||
|
||||
it('duplicating a tag should maintian its relationships', async () => {
|
||||
modelManager.handleSignout();
|
||||
let pair = createRelatedNoteTagPair();
|
||||
let noteParams = pair[0];
|
||||
let tagParams = pair[1];
|
||||
|
||||
modelManager.mapResponseItemsToLocalModels([noteParams, tagParams]);
|
||||
let note = modelManager.allItemsMatchingTypes(["Note"])[0];
|
||||
let tag = modelManager.allItemsMatchingTypes(["Tag"])[0];
|
||||
|
||||
note.setDirty(true);
|
||||
tag.setDirty(true);
|
||||
|
||||
await syncManager.sync();
|
||||
await syncManager.clearSyncToken();
|
||||
|
||||
expect(modelManager.allItems.length).to.equal(2);
|
||||
|
||||
tag.title = `${Math.random()}`
|
||||
tag.setDirty(true);
|
||||
|
||||
expect(note.referencingObjects.length).to.equal(1);
|
||||
|
||||
// wait about 1s, which is the value the dev server will ignore conflicting changes
|
||||
return expect(new Promise((resolve, reject) => {
|
||||
setTimeout(function () {
|
||||
resolve();
|
||||
}, 1100);
|
||||
})).to.be.fulfilled.then(async () => {
|
||||
return expect(syncManager.sync()).to.be.fulfilled.then(async (response) => {
|
||||
// tag should now be conflicted and a copy created
|
||||
let models = modelManager.allItems;
|
||||
expect(modelManager.allItems.length).to.equal(3);
|
||||
var tags = modelManager.allItemsMatchingTypes(["Tag"]);
|
||||
var tag1 = tags[0];
|
||||
var tag2 = tags[1];
|
||||
|
||||
expect(tag1.uuid).to.not.equal(tag2.uuid);
|
||||
|
||||
expect(tag1.uuid).to.equal(tag.uuid);
|
||||
expect(tag2.conflict_of).to.equal(tag1.uuid);
|
||||
expect(tag1.notes.length).to.equal(tag2.notes.length);
|
||||
expect(tag1.referencingObjects.length).to.equal(0);
|
||||
expect(tag2.referencingObjects.length).to.equal(0);
|
||||
|
||||
// Two tags now link to this note
|
||||
expect(note.referencingObjects.length).to.equal(2);
|
||||
expect(note.referencingObjects[0]).to.not.equal(note.referencingObjects[1]);
|
||||
})
|
||||
})
|
||||
}).timeout(10000);
|
||||
})
|
||||
23
test/mocha/test.html
Normal file
23
test/mocha/test.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mocha Tests</title>
|
||||
<link href="https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css" rel="stylesheet" />
|
||||
<style>
|
||||
|
||||
body {
|
||||
/* background-color: rgb(195, 195, 195); */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mocha"></div>
|
||||
<script src="../../node_modules/mocha/mocha.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script type="module" src="models.test.js"></script>
|
||||
<script type="module">
|
||||
mocha.checkLeaks();
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
539
test/mocha/vendor/chai-as-promised-built.js
vendored
Normal file
539
test/mocha/vendor/chai-as-promised-built.js
vendored
Normal file
@@ -0,0 +1,539 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.chaiAsPromised = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
/* eslint-disable no-invalid-this */
|
||||
let checkError = require("check-error");
|
||||
|
||||
module.exports = (chai, utils) => {
|
||||
const Assertion = chai.Assertion;
|
||||
const assert = chai.assert;
|
||||
const proxify = utils.proxify;
|
||||
|
||||
// If we are using a version of Chai that has checkError on it,
|
||||
// we want to use that version to be consistent. Otherwise, we use
|
||||
// what was passed to the factory.
|
||||
if (utils.checkError) {
|
||||
checkError = utils.checkError;
|
||||
}
|
||||
|
||||
function isLegacyJQueryPromise(thenable) {
|
||||
// jQuery promises are Promises/A+-compatible since 3.0.0. jQuery 3.0.0 is also the first version
|
||||
// to define the catch method.
|
||||
return typeof thenable.catch !== "function" &&
|
||||
typeof thenable.always === "function" &&
|
||||
typeof thenable.done === "function" &&
|
||||
typeof thenable.fail === "function" &&
|
||||
typeof thenable.pipe === "function" &&
|
||||
typeof thenable.progress === "function" &&
|
||||
typeof thenable.state === "function";
|
||||
}
|
||||
|
||||
function assertIsAboutPromise(assertion) {
|
||||
if (typeof assertion._obj.then !== "function") {
|
||||
throw new TypeError(utils.inspect(assertion._obj) + " is not a thenable.");
|
||||
}
|
||||
if (isLegacyJQueryPromise(assertion._obj)) {
|
||||
throw new TypeError("Chai as Promised is incompatible with thenables of jQuery<3.0.0, sorry! Please " +
|
||||
"upgrade jQuery or use another Promises/A+ compatible library (see " +
|
||||
"http://promisesaplus.com/).");
|
||||
}
|
||||
}
|
||||
|
||||
function proxifyIfSupported(assertion) {
|
||||
return proxify === undefined ? assertion : proxify(assertion);
|
||||
}
|
||||
|
||||
function method(name, asserter) {
|
||||
utils.addMethod(Assertion.prototype, name, function () {
|
||||
assertIsAboutPromise(this);
|
||||
return asserter.apply(this, arguments);
|
||||
});
|
||||
}
|
||||
|
||||
function property(name, asserter) {
|
||||
utils.addProperty(Assertion.prototype, name, function () {
|
||||
assertIsAboutPromise(this);
|
||||
return proxifyIfSupported(asserter.apply(this, arguments));
|
||||
});
|
||||
}
|
||||
|
||||
function doNotify(promise, done) {
|
||||
promise.then(() => done(), done);
|
||||
}
|
||||
|
||||
// These are for clarity and to bypass Chai refusing to allow `undefined` as actual when used with `assert`.
|
||||
function assertIfNegated(assertion, message, extra) {
|
||||
assertion.assert(true, null, message, extra.expected, extra.actual);
|
||||
}
|
||||
|
||||
function assertIfNotNegated(assertion, message, extra) {
|
||||
assertion.assert(false, message, null, extra.expected, extra.actual);
|
||||
}
|
||||
|
||||
function getBasePromise(assertion) {
|
||||
// We need to chain subsequent asserters on top of ones in the chain already (consider
|
||||
// `eventually.have.property("foo").that.equals("bar")`), only running them after the existing ones pass.
|
||||
// So the first base-promise is `assertion._obj`, but after that we use the assertions themselves, i.e.
|
||||
// previously derived promises, to chain off of.
|
||||
return typeof assertion.then === "function" ? assertion : assertion._obj;
|
||||
}
|
||||
|
||||
function getReasonName(reason) {
|
||||
return reason instanceof Error ? reason.toString() : checkError.getConstructorName(reason);
|
||||
}
|
||||
|
||||
// Grab these first, before we modify `Assertion.prototype`.
|
||||
|
||||
const propertyNames = Object.getOwnPropertyNames(Assertion.prototype);
|
||||
|
||||
const propertyDescs = {};
|
||||
for (const name of propertyNames) {
|
||||
propertyDescs[name] = Object.getOwnPropertyDescriptor(Assertion.prototype, name);
|
||||
}
|
||||
|
||||
property("fulfilled", function () {
|
||||
const derivedPromise = getBasePromise(this).then(
|
||||
value => {
|
||||
assertIfNegated(this,
|
||||
"expected promise not to be fulfilled but it was fulfilled with #{act}",
|
||||
{ actual: value });
|
||||
return value;
|
||||
},
|
||||
reason => {
|
||||
assertIfNotNegated(this,
|
||||
"expected promise to be fulfilled but it was rejected with #{act}",
|
||||
{ actual: getReasonName(reason) });
|
||||
return reason;
|
||||
}
|
||||
);
|
||||
|
||||
module.exports.transferPromiseness(this, derivedPromise);
|
||||
return this;
|
||||
});
|
||||
|
||||
property("rejected", function () {
|
||||
const derivedPromise = getBasePromise(this).then(
|
||||
value => {
|
||||
assertIfNotNegated(this,
|
||||
"expected promise to be rejected but it was fulfilled with #{act}",
|
||||
{ actual: value });
|
||||
return value;
|
||||
},
|
||||
reason => {
|
||||
assertIfNegated(this,
|
||||
"expected promise not to be rejected but it was rejected with #{act}",
|
||||
{ actual: getReasonName(reason) });
|
||||
|
||||
// Return the reason, transforming this into a fulfillment, to allow further assertions, e.g.
|
||||
// `promise.should.be.rejected.and.eventually.equal("reason")`.
|
||||
return reason;
|
||||
}
|
||||
);
|
||||
|
||||
module.exports.transferPromiseness(this, derivedPromise);
|
||||
return this;
|
||||
});
|
||||
|
||||
method("rejectedWith", function (errorLike, errMsgMatcher, message) {
|
||||
let errorLikeName = null;
|
||||
const negate = utils.flag(this, "negate") || false;
|
||||
|
||||
// rejectedWith with that is called without arguments is
|
||||
// the same as a plain ".rejected" use.
|
||||
if (errorLike === undefined && errMsgMatcher === undefined &&
|
||||
message === undefined) {
|
||||
/* eslint-disable no-unused-expressions */
|
||||
return this.rejected;
|
||||
/* eslint-enable no-unused-expressions */
|
||||
}
|
||||
|
||||
if (message !== undefined) {
|
||||
utils.flag(this, "message", message);
|
||||
}
|
||||
|
||||
if (errorLike instanceof RegExp || typeof errorLike === "string") {
|
||||
errMsgMatcher = errorLike;
|
||||
errorLike = null;
|
||||
} else if (errorLike && errorLike instanceof Error) {
|
||||
errorLikeName = errorLike.toString();
|
||||
} else if (typeof errorLike === "function") {
|
||||
errorLikeName = checkError.getConstructorName(errorLike);
|
||||
} else {
|
||||
errorLike = null;
|
||||
}
|
||||
const everyArgIsDefined = Boolean(errorLike && errMsgMatcher);
|
||||
|
||||
let matcherRelation = "including";
|
||||
if (errMsgMatcher instanceof RegExp) {
|
||||
matcherRelation = "matching";
|
||||
}
|
||||
|
||||
const derivedPromise = getBasePromise(this).then(
|
||||
value => {
|
||||
let assertionMessage = null;
|
||||
let expected = null;
|
||||
|
||||
if (errorLike) {
|
||||
assertionMessage = "expected promise to be rejected with #{exp} but it was fulfilled with #{act}";
|
||||
expected = errorLikeName;
|
||||
} else if (errMsgMatcher) {
|
||||
assertionMessage = `expected promise to be rejected with an error ${matcherRelation} #{exp} but ` +
|
||||
`it was fulfilled with #{act}`;
|
||||
expected = errMsgMatcher;
|
||||
}
|
||||
|
||||
assertIfNotNegated(this, assertionMessage, { expected, actual: value });
|
||||
return value;
|
||||
},
|
||||
reason => {
|
||||
const errorLikeCompatible = errorLike && (errorLike instanceof Error ?
|
||||
checkError.compatibleInstance(reason, errorLike) :
|
||||
checkError.compatibleConstructor(reason, errorLike));
|
||||
|
||||
const errMsgMatcherCompatible = errMsgMatcher && checkError.compatibleMessage(reason, errMsgMatcher);
|
||||
|
||||
const reasonName = getReasonName(reason);
|
||||
|
||||
if (negate && everyArgIsDefined) {
|
||||
if (errorLikeCompatible && errMsgMatcherCompatible) {
|
||||
this.assert(true,
|
||||
null,
|
||||
"expected promise not to be rejected with #{exp} but it was rejected " +
|
||||
"with #{act}",
|
||||
errorLikeName,
|
||||
reasonName);
|
||||
}
|
||||
} else {
|
||||
if (errorLike) {
|
||||
this.assert(errorLikeCompatible,
|
||||
"expected promise to be rejected with #{exp} but it was rejected with #{act}",
|
||||
"expected promise not to be rejected with #{exp} but it was rejected " +
|
||||
"with #{act}",
|
||||
errorLikeName,
|
||||
reasonName);
|
||||
}
|
||||
|
||||
if (errMsgMatcher) {
|
||||
this.assert(errMsgMatcherCompatible,
|
||||
`expected promise to be rejected with an error ${matcherRelation} #{exp} but got ` +
|
||||
`#{act}`,
|
||||
`expected promise not to be rejected with an error ${matcherRelation} #{exp}`,
|
||||
errMsgMatcher,
|
||||
checkError.getMessage(reason));
|
||||
}
|
||||
}
|
||||
|
||||
return reason;
|
||||
}
|
||||
);
|
||||
|
||||
module.exports.transferPromiseness(this, derivedPromise);
|
||||
return this;
|
||||
});
|
||||
|
||||
property("eventually", function () {
|
||||
utils.flag(this, "eventually", true);
|
||||
return this;
|
||||
});
|
||||
|
||||
method("notify", function (done) {
|
||||
doNotify(getBasePromise(this), done);
|
||||
return this;
|
||||
});
|
||||
|
||||
method("become", function (value, message) {
|
||||
return this.eventually.deep.equal(value, message);
|
||||
});
|
||||
|
||||
// ### `eventually`
|
||||
|
||||
// We need to be careful not to trigger any getters, thus `Object.getOwnPropertyDescriptor` usage.
|
||||
const methodNames = propertyNames.filter(name => {
|
||||
return name !== "assert" && typeof propertyDescs[name].value === "function";
|
||||
});
|
||||
|
||||
methodNames.forEach(methodName => {
|
||||
Assertion.overwriteMethod(methodName, originalMethod => function () {
|
||||
return doAsserterAsyncAndAddThen(originalMethod, this, arguments);
|
||||
});
|
||||
});
|
||||
|
||||
const getterNames = propertyNames.filter(name => {
|
||||
return name !== "_obj" && typeof propertyDescs[name].get === "function";
|
||||
});
|
||||
|
||||
getterNames.forEach(getterName => {
|
||||
// Chainable methods are things like `an`, which can work both for `.should.be.an.instanceOf` and as
|
||||
// `should.be.an("object")`. We need to handle those specially.
|
||||
const isChainableMethod = Assertion.prototype.__methods.hasOwnProperty(getterName);
|
||||
|
||||
if (isChainableMethod) {
|
||||
Assertion.overwriteChainableMethod(
|
||||
getterName,
|
||||
originalMethod => function () {
|
||||
return doAsserterAsyncAndAddThen(originalMethod, this, arguments);
|
||||
},
|
||||
originalGetter => function () {
|
||||
return doAsserterAsyncAndAddThen(originalGetter, this);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
Assertion.overwriteProperty(getterName, originalGetter => function () {
|
||||
return proxifyIfSupported(doAsserterAsyncAndAddThen(originalGetter, this));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function doAsserterAsyncAndAddThen(asserter, assertion, args) {
|
||||
// Since we're intercepting all methods/properties, we need to just pass through if they don't want
|
||||
// `eventually`, or if we've already fulfilled the promise (see below).
|
||||
if (!utils.flag(assertion, "eventually")) {
|
||||
asserter.apply(assertion, args);
|
||||
return assertion;
|
||||
}
|
||||
|
||||
const derivedPromise = getBasePromise(assertion).then(value => {
|
||||
// Set up the environment for the asserter to actually run: `_obj` should be the fulfillment value, and
|
||||
// now that we have the value, we're no longer in "eventually" mode, so we won't run any of this code,
|
||||
// just the base Chai code that we get to via the short-circuit above.
|
||||
assertion._obj = value;
|
||||
utils.flag(assertion, "eventually", false);
|
||||
|
||||
return args ? module.exports.transformAsserterArgs(args) : args;
|
||||
}).then(newArgs => {
|
||||
asserter.apply(assertion, newArgs);
|
||||
|
||||
// Because asserters, for example `property`, can change the value of `_obj` (i.e. change the "object"
|
||||
// flag), we need to communicate this value change to subsequent chained asserters. Since we build a
|
||||
// promise chain paralleling the asserter chain, we can use it to communicate such changes.
|
||||
return assertion._obj;
|
||||
});
|
||||
|
||||
module.exports.transferPromiseness(assertion, derivedPromise);
|
||||
return assertion;
|
||||
}
|
||||
|
||||
// ### Now use the `Assertion` framework to build an `assert` interface.
|
||||
const originalAssertMethods = Object.getOwnPropertyNames(assert).filter(propName => {
|
||||
return typeof assert[propName] === "function";
|
||||
});
|
||||
|
||||
assert.isFulfilled = (promise, message) => (new Assertion(promise, message)).to.be.fulfilled;
|
||||
|
||||
assert.isRejected = (promise, errorLike, errMsgMatcher, message) => {
|
||||
const assertion = new Assertion(promise, message);
|
||||
return assertion.to.be.rejectedWith(errorLike, errMsgMatcher, message);
|
||||
};
|
||||
|
||||
assert.becomes = (promise, value, message) => assert.eventually.deepEqual(promise, value, message);
|
||||
|
||||
assert.doesNotBecome = (promise, value, message) => assert.eventually.notDeepEqual(promise, value, message);
|
||||
|
||||
assert.eventually = {};
|
||||
originalAssertMethods.forEach(assertMethodName => {
|
||||
assert.eventually[assertMethodName] = function (promise) {
|
||||
const otherArgs = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
let customRejectionHandler;
|
||||
const message = arguments[assert[assertMethodName].length - 1];
|
||||
if (typeof message === "string") {
|
||||
customRejectionHandler = reason => {
|
||||
throw new chai.AssertionError(`${message}\n\nOriginal reason: ${utils.inspect(reason)}`);
|
||||
};
|
||||
}
|
||||
|
||||
const returnedPromise = promise.then(
|
||||
fulfillmentValue => assert[assertMethodName].apply(assert, [fulfillmentValue].concat(otherArgs)),
|
||||
customRejectionHandler
|
||||
);
|
||||
|
||||
returnedPromise.notify = done => {
|
||||
doNotify(returnedPromise, done);
|
||||
};
|
||||
|
||||
return returnedPromise;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.transferPromiseness = (assertion, promise) => {
|
||||
assertion.then = promise.then.bind(promise);
|
||||
};
|
||||
|
||||
module.exports.transformAsserterArgs = values => values;
|
||||
|
||||
},{"check-error":2}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
/* !
|
||||
* Chai - checkError utility
|
||||
* Copyright(c) 2012-2016 Jake Luer <jake@alogicalparadox.com>
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/**
|
||||
* ### .checkError
|
||||
*
|
||||
* Checks that an error conforms to a given set of criteria and/or retrieves information about it.
|
||||
*
|
||||
* @api public
|
||||
*/
|
||||
|
||||
/**
|
||||
* ### .compatibleInstance(thrown, errorLike)
|
||||
*
|
||||
* Checks if two instances are compatible (strict equal).
|
||||
* Returns false if errorLike is not an instance of Error, because instances
|
||||
* can only be compatible if they're both error instances.
|
||||
*
|
||||
* @name compatibleInstance
|
||||
* @param {Error} thrown error
|
||||
* @param {Error|ErrorConstructor} errorLike object to compare against
|
||||
* @namespace Utils
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function compatibleInstance(thrown, errorLike) {
|
||||
return errorLike instanceof Error && thrown === errorLike;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### .compatibleConstructor(thrown, errorLike)
|
||||
*
|
||||
* Checks if two constructors are compatible.
|
||||
* This function can receive either an error constructor or
|
||||
* an error instance as the `errorLike` argument.
|
||||
* Constructors are compatible if they're the same or if one is
|
||||
* an instance of another.
|
||||
*
|
||||
* @name compatibleConstructor
|
||||
* @param {Error} thrown error
|
||||
* @param {Error|ErrorConstructor} errorLike object to compare against
|
||||
* @namespace Utils
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function compatibleConstructor(thrown, errorLike) {
|
||||
if (errorLike instanceof Error) {
|
||||
// If `errorLike` is an instance of any error we compare their constructors
|
||||
return thrown.constructor === errorLike.constructor || thrown instanceof errorLike.constructor;
|
||||
} else if (errorLike.prototype instanceof Error || errorLike === Error) {
|
||||
// If `errorLike` is a constructor that inherits from Error, we compare `thrown` to `errorLike` directly
|
||||
return thrown.constructor === errorLike || thrown instanceof errorLike;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### .compatibleMessage(thrown, errMatcher)
|
||||
*
|
||||
* Checks if an error's message is compatible with a matcher (String or RegExp).
|
||||
* If the message contains the String or passes the RegExp test,
|
||||
* it is considered compatible.
|
||||
*
|
||||
* @name compatibleMessage
|
||||
* @param {Error} thrown error
|
||||
* @param {String|RegExp} errMatcher to look for into the message
|
||||
* @namespace Utils
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function compatibleMessage(thrown, errMatcher) {
|
||||
var comparisonString = typeof thrown === 'string' ? thrown : thrown.message;
|
||||
if (errMatcher instanceof RegExp) {
|
||||
return errMatcher.test(comparisonString);
|
||||
} else if (typeof errMatcher === 'string') {
|
||||
return comparisonString.indexOf(errMatcher) !== -1; // eslint-disable-line no-magic-numbers
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### .getFunctionName(constructorFn)
|
||||
*
|
||||
* Returns the name of a function.
|
||||
* This also includes a polyfill function if `constructorFn.name` is not defined.
|
||||
*
|
||||
* @name getFunctionName
|
||||
* @param {Function} constructorFn
|
||||
* @namespace Utils
|
||||
* @api private
|
||||
*/
|
||||
|
||||
var functionNameMatch = /\s*function(?:\s|\s*\/\*[^(?:*\/)]+\*\/\s*)*([^\(\/]+)/;
|
||||
function getFunctionName(constructorFn) {
|
||||
var name = '';
|
||||
if (typeof constructorFn.name === 'undefined') {
|
||||
// Here we run a polyfill if constructorFn.name is not defined
|
||||
var match = String(constructorFn).match(functionNameMatch);
|
||||
if (match) {
|
||||
name = match[1];
|
||||
}
|
||||
} else {
|
||||
name = constructorFn.name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### .getConstructorName(errorLike)
|
||||
*
|
||||
* Gets the constructor name for an Error instance or constructor itself.
|
||||
*
|
||||
* @name getConstructorName
|
||||
* @param {Error|ErrorConstructor} errorLike
|
||||
* @namespace Utils
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function getConstructorName(errorLike) {
|
||||
var constructorName = errorLike;
|
||||
if (errorLike instanceof Error) {
|
||||
constructorName = getFunctionName(errorLike.constructor);
|
||||
} else if (typeof errorLike === 'function') {
|
||||
// If `err` is not an instance of Error it is an error constructor itself or another function.
|
||||
// If we've got a common function we get its name, otherwise we may need to create a new instance
|
||||
// of the error just in case it's a poorly-constructed error. Please see chaijs/chai/issues/45 to know more.
|
||||
constructorName = getFunctionName(errorLike).trim() ||
|
||||
getFunctionName(new errorLike()); // eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
return constructorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* ### .getMessage(errorLike)
|
||||
*
|
||||
* Gets the error message from an error.
|
||||
* If `err` is a String itself, we return it.
|
||||
* If the error has no message, we return an empty string.
|
||||
*
|
||||
* @name getMessage
|
||||
* @param {Error|String} errorLike
|
||||
* @namespace Utils
|
||||
* @api public
|
||||
*/
|
||||
|
||||
function getMessage(errorLike) {
|
||||
var msg = '';
|
||||
if (errorLike && errorLike.message) {
|
||||
msg = errorLike.message;
|
||||
} else if (typeof errorLike === 'string') {
|
||||
msg = errorLike;
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
compatibleInstance: compatibleInstance,
|
||||
compatibleConstructor: compatibleConstructor,
|
||||
compatibleMessage: compatibleMessage,
|
||||
getMessage: getMessage,
|
||||
getConstructorName: getConstructorName,
|
||||
};
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ApiKeyTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class NameTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
8
testing-server.js
Normal file
8
testing-server.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Used for running mocha tests
|
||||
|
||||
var connect = require('connect');
|
||||
var serveStatic = require('serve-static');
|
||||
var port = 7000;
|
||||
connect().use(serveStatic(__dirname)).listen(port, function(){
|
||||
console.log(`Server running on ${port}...`);
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @license
|
||||
* Lodash (Custom Build) <https://lodash.com/>
|
||||
* Build: `lodash include="includes,merge,filter,map,remove,find,omit,pull,cloneDeep,pick,uniq,sortedIndexBy"`
|
||||
* Build: `lodash include="includes,merge,filter,map,remove,find,omit,pull,cloneDeep,pick,uniq,sortedIndexBy,mergeWith"`
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
* Released under MIT license <https://lodash.com/license>
|
||||
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
||||
@@ -4890,6 +4890,41 @@
|
||||
baseMerge(object, source, srcIndex);
|
||||
});
|
||||
|
||||
/**
|
||||
* This method is like `_.merge` except that it accepts `customizer` which
|
||||
* is invoked to produce the merged values of the destination and source
|
||||
* properties. If `customizer` returns `undefined`, merging is handled by the
|
||||
* method instead. The `customizer` is invoked with six arguments:
|
||||
* (objValue, srcValue, key, object, source, stack).
|
||||
*
|
||||
* **Note:** This method mutates `object`.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 4.0.0
|
||||
* @category Object
|
||||
* @param {Object} object The destination object.
|
||||
* @param {...Object} sources The source objects.
|
||||
* @param {Function} customizer The function to customize assigned values.
|
||||
* @returns {Object} Returns `object`.
|
||||
* @example
|
||||
*
|
||||
* function customizer(objValue, srcValue) {
|
||||
* if (_.isArray(objValue)) {
|
||||
* return objValue.concat(srcValue);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* var object = { 'a': [1], 'b': [2] };
|
||||
* var other = { 'a': [3], 'b': [4] };
|
||||
*
|
||||
* _.mergeWith(object, other, customizer);
|
||||
* // => { 'a': [1, 3], 'b': [2, 4] }
|
||||
*/
|
||||
var mergeWith = createAssigner(function(object, source, srcIndex, customizer) {
|
||||
baseMerge(object, source, srcIndex, customizer);
|
||||
});
|
||||
|
||||
/**
|
||||
* The opposite of `_.pick`; this method creates an object composed of the
|
||||
* own and inherited enumerable property paths of `object` that are not omitted.
|
||||
@@ -5169,6 +5204,7 @@
|
||||
lodash.map = map;
|
||||
lodash.memoize = memoize;
|
||||
lodash.merge = merge;
|
||||
lodash.mergeWith = mergeWith;
|
||||
lodash.omit = omit;
|
||||
lodash.pick = pick;
|
||||
lodash.property = property;
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Lodash (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE
|
||||
* Build: `lodash include="includes,merge,filter,map,remove,find,omit,pull,cloneDeep,pick,uniq,sortedIndexBy"`
|
||||
* Build: `lodash include="includes,merge,filter,map,remove,find,omit,pull,cloneDeep,pick,uniq,sortedIndexBy,mergeWith"`
|
||||
*/
|
||||
;(function(){function t(t,n){return t.set(n[0],n[1]),t}function n(t,n){return t.add(n),t}function r(t,n,r){switch(r.length){case 0:return t.call(n);case 1:return t.call(n,r[0]);case 2:return t.call(n,r[0],r[1]);case 3:return t.call(n,r[0],r[1],r[2])}return t.apply(n,r)}function e(t,n){for(var r=-1,e=null==t?0:t.length;++r<e&&n(t[r],r,t)!==false;);return t}function u(t,n){for(var r=-1,e=null==t?0:t.length,u=0,o=[];++r<e;){var i=t[r];n(i,r,t)&&(o[u++]=i)}return o}function o(t,n){return!!(null==t?0:t.length)&&h(t,n,0)>-1;
|
||||
}function i(t,n,r){for(var e=-1,u=null==t?0:t.length;++e<u;)if(r(n,t[e]))return true;return false}function c(t,n){for(var r=-1,e=null==t?0:t.length,u=Array(e);++r<e;)u[r]=n(t[r],r,t);return u}function f(t,n){for(var r=-1,e=n.length,u=t.length;++r<e;)t[u+r]=n[r];return t}function a(t,n,r,e){var u=-1,o=null==t?0:t.length;for(e&&o&&(r=t[++u]);++u<o;)r=n(r,t[u],u,t);return r}function l(t,n){for(var r=-1,e=null==t?0:t.length;++r<e;)if(n(t[r],r,t))return true;return false}function s(t,n,r,e){for(var u=t.length,o=r+(e?1:-1);e?o--:++o<u;)if(n(t[o],o,t))return o;
|
||||
return-1}function h(t,n,r){return n===n?A(t,n,r):s(t,p,r)}function v(t,n,r,e){for(var u=r-1,o=t.length;++u<o;)if(e(t[u],n))return u;return-1}function p(t){return t!==t}function y(t){return function(n){return null==n?Or:n[t]}}function g(t,n){for(var r=-1,e=Array(t);++r<t;)e[r]=n(r);return e}function b(t){return function(n){return t(n)}}function _(t,n){return c(n,function(n){return t[n]})}function d(t,n){return t.has(n)}function j(t,n){return null==t?Or:t[n]}function w(t){var n=-1,r=Array(t.size);return t.forEach(function(t,e){
|
||||
r[++n]=[e,t]}),r}function O(t,n){return function(r){return t(n(r))}}function m(t){var n=-1,r=Array(t.size);return t.forEach(function(t){r[++n]=t}),r}function A(t,n,r){for(var e=r-1,u=t.length;++e<u;)if(t[e]===n)return e;return-1}function z(){}function x(t){var n=-1,r=null==t?0:t.length;for(this.clear();++n<r;){var e=t[n];this.set(e[0],e[1])}}function S(){this.__data__=Ou?Ou(null):{},this.size=0}function k(t){var n=this.has(t)&&delete this.__data__[t];return this.size-=n?1:0,n}function $(t){var n=this.__data__;
|
||||
if(Ou){var r=n[t];return r===xr?Or:r}return Je.call(n,t)?n[t]:Or}function I(t){var n=this.__data__;return Ou?n[t]!==Or:Je.call(n,t)}function L(t,n){var r=this.__data__;return this.size+=this.has(t)?0:1,r[t]=Ou&&n===Or?xr:n,this}function P(t){var n=-1,r=null==t?0:t.length;for(this.clear();++n<r;){var e=t[n];this.set(e[0],e[1])}}function E(){this.__data__=[],this.size=0}function F(t){var n=this.__data__,r=rt(n,t);return!(r<0)&&(r==n.length-1?n.pop():iu.call(n,r,1),--this.size,true)}function B(t){var n=this.__data__,r=rt(n,t);
|
||||
return r<0?Or:n[r][1]}function M(t){return rt(this.__data__,t)>-1}function T(t,n){var r=this.__data__,e=rt(r,t);return e<0?(++this.size,r.push([t,n])):r[e][1]=n,this}function U(t){var n=-1,r=null==t?0:t.length;for(this.clear();++n<r;){var e=t[n];this.set(e[0],e[1])}}function N(){this.size=0,this.__data__={hash:new x,map:new(_u||P),string:new x}}function C(t){var n=hn(this,t).delete(t);return this.size-=n?1:0,n}function D(t){return hn(this,t).get(t)}function R(t){return hn(this,t).has(t)}function V(t,n){
|
||||
var r=hn(this,t),e=r.size;return r.set(t,n),this.size+=r.size==e?0:1,this}function q(t){var n=-1,r=null==t?0:t.length;for(this.__data__=new U;++n<r;)this.add(t[n])}function W(t){return this.__data__.set(t,xr),this}function G(t){return this.__data__.has(t)}function H(t){this.size=(this.__data__=new P(t)).size}function J(){this.__data__=new P,this.size=0}function K(t){var n=this.__data__,r=n.delete(t);return this.size=n.size,r}function Q(t){return this.__data__.get(t)}function X(t){return this.__data__.has(t);
|
||||
}function Y(t,n){var r=this.__data__;if(r instanceof P){var e=r.__data__;if(!_u||e.length<Ar-1)return e.push([t,n]),this.size=++r.size,this;r=this.__data__=new U(e)}return r.set(t,n),this.size=r.size,this}function Z(t,n){var r=qu(t),e=!r&&Vu(t),u=!r&&!e&&Wu(t),o=!r&&!e&&!u&&Gu(t),i=r||e||u||o,c=i?g(t.length,String):[],f=c.length;for(var a in t)!n&&!Je.call(t,a)||i&&("length"==a||u&&("offset"==a||"parent"==a)||o&&("buffer"==a||"byteLength"==a||"byteOffset"==a)||wn(a,f))||c.push(a);return c}function tt(t,n,r){
|
||||
(r===Or||Kn(t[n],r))&&(r!==Or||n in t)||ot(t,n,r)}function nt(t,n,r){var e=t[n];Je.call(t,n)&&Kn(e,r)&&(r!==Or||n in t)||ot(t,n,r)}function rt(t,n){for(var r=t.length;r--;)if(Kn(t[r][0],n))return r;return-1}function et(t,n){return t&&Qt(n,hr(n),t)}function ut(t,n){return t&&Qt(n,vr(n),t)}function ot(t,n,r){"__proto__"==n&&au?au(t,n,{configurable:true,enumerable:true,value:r,writable:true}):t[n]=r}function it(t,n,r,u,o,i){var c,f=n&kr,a=n&$r,l=n&Ir;if(r&&(c=o?r(t,u,o,i):r(t)),c!==Or)return c;if(!tr(t))return t;
|
||||
var s=qu(t);if(s){if(c=bn(t),!f)return Kt(t,c)}else{var h=Uu(t),v=h==Hr||h==Jr;if(Wu(t))return Dt(t,f);if(h==Yr||h==Dr||v&&!o){if(c=a||v?{}:_n(t),!f)return a?Yt(t,ut(c,t)):Xt(t,et(c,t))}else{if(!Pe[h])return o?t:{};c=dn(t,h,it,f)}}i||(i=new H);var p=i.get(t);if(p)return p;i.set(t,c);var y=l?a?ln:an:a?vr:hr,g=s?Or:y(t);return e(g||t,function(e,u){g&&(u=e,e=t[u]),nt(c,u,it(e,n,r,u,t,i))}),c}function ct(t,n){var r=[];return Pu(t,function(t,e,u){n(t,e,u)&&r.push(t)}),r}function ft(t,n,r,e,u){var o=-1,i=t.length;
|
||||
for(r||(r=jn),u||(u=[]);++o<i;){var c=t[o];n>0&&r(c)?n>1?ft(c,n-1,r,e,u):f(u,c):e||(u[u.length]=c)}return u}function at(t,n){return t&&Eu(t,n,hr)}function lt(t,n){n=Ct(n,t);for(var r=0,e=n.length;null!=t&&r<e;)t=t[Bn(n[r++])];return r&&r==e?t:Or}function st(t,n,r){var e=n(t);return qu(t)?e:f(e,r(t))}function ht(t){return null==t?t===Or?oe:Xr:fu&&fu in Object(t)?yn(t):Ln(t)}function vt(t,n){return null!=t&&n in Object(t)}function pt(t){return nr(t)&&ht(t)==Dr}function yt(t,n,r,e,u){return t===n||(null==t||null==n||!nr(t)&&!nr(n)?t!==t&&n!==n:gt(t,n,r,e,yt,u));
|
||||
}function gt(t,n,r,e,u,o){var i=qu(t),c=qu(n),f=i?Rr:Uu(t),a=c?Rr:Uu(n);f=f==Dr?Yr:f,a=a==Dr?Yr:a;var l=f==Yr,s=a==Yr,h=f==a;if(h&&Wu(t)){if(!Wu(n))return false;i=true,l=false}if(h&&!l)return o||(o=new H),i||Gu(t)?un(t,n,r,e,u,o):on(t,n,f,r,e,u,o);if(!(r&Lr)){var v=l&&Je.call(t,"__wrapped__"),p=s&&Je.call(n,"__wrapped__");if(v||p){var y=v?t.value():t,g=p?n.value():n;return o||(o=new H),u(y,g,r,e,o)}}return!!h&&(o||(o=new H),cn(t,n,r,e,u,o))}function bt(t,n,r,e){var u=r.length,o=u,i=!e;if(null==t)return!o;for(t=Object(t);u--;){
|
||||
var c=r[u];if(i&&c[2]?c[1]!==t[c[0]]:!(c[0]in t))return false}for(;++u<o;){c=r[u];var f=c[0],a=t[f],l=c[1];if(i&&c[2]){if(a===Or&&!(f in t))return false}else{var s=new H;if(e)var h=e(a,l,f,t,n,s);if(!(h===Or?yt(l,a,Lr|Pr,e,s):h))return false}}return true}function _t(t){return!(!tr(t)||zn(t))&&(Yn(t)?Ye:ke).test(Mn(t))}function dt(t){return nr(t)&&Zn(t.length)&&!!Le[ht(t)]}function jt(t){return typeof t=="function"?t:null==t?gr:typeof t=="object"?qu(t)?zt(t[0],t[1]):At(t):dr(t)}function wt(t){if(!xn(t))return vu(t);
|
||||
var n=[];for(var r in Object(t))Je.call(t,r)&&"constructor"!=r&&n.push(r);return n}function Ot(t){if(!tr(t))return In(t);var n=xn(t),r=[];for(var e in t)("constructor"!=e||!n&&Je.call(t,e))&&r.push(e);return r}function mt(t,n){var r=-1,e=Qn(t)?Array(t.length):[];return Pu(t,function(t,u,o){e[++r]=n(t,u,o)}),e}function At(t){var n=vn(t);return 1==n.length&&n[0][2]?kn(n[0][0],n[0][1]):function(r){return r===t||bt(r,t,n)}}function zt(t,n){return mn(t)&&Sn(n)?kn(Bn(t),n):function(r){var e=lr(r,t);return e===Or&&e===n?sr(r,t):yt(n,e,Lr|Pr);
|
||||
}}function xt(t,n,r,e,u){t!==n&&Eu(n,function(o,i){if(tr(o))u||(u=new H),St(t,n,i,r,xt,e,u);else{var c=e?e(t[i],o,i+"",t,n,u):Or;c===Or&&(c=o),tt(t,i,c)}},vr)}function St(t,n,r,e,u,o,i){var c=t[r],f=n[r],a=i.get(f);if(a)return tt(t,r,a),Or;var l=o?o(c,f,r+"",t,n,i):Or,s=l===Or;if(s){var h=qu(f),v=!h&&Wu(f),p=!h&&!v&&Gu(f);l=f,h||v||p?qu(c)?l=c:Xn(c)?l=Kt(c):v?(s=false,l=Dt(f,true)):p?(s=false,l=Jt(f,true)):l=[]:rr(f)||Vu(f)?(l=c,Vu(c)?l=fr(c):(!tr(c)||e&&Yn(c))&&(l=_n(f))):s=false}s&&(i.set(f,l),u(l,f,e,o,i),
|
||||
i.delete(f)),tt(t,r,l)}function kt(t,n){return $t(t,n,function(n,r){return sr(t,r)})}function $t(t,n,r){for(var e=-1,u=n.length,o={};++e<u;){var i=n[e],c=lt(t,i);r(c,i)&&Ft(o,Ct(i,t),c)}return o}function It(t){return function(n){return lt(n,t)}}function Lt(t,n,r,e){var u=e?v:h,o=-1,i=n.length,f=t;for(t===n&&(n=Kt(n)),r&&(f=c(t,b(r)));++o<i;)for(var a=0,l=n[o],s=r?r(l):l;(a=u(f,s,a,e))>-1;)f!==t&&iu.call(f,a,1),iu.call(t,a,1);return t}function Pt(t,n){for(var r=t?n.length:0,e=r-1;r--;){var u=n[r];if(r==e||u!==o){
|
||||
var o=u;wn(u)?iu.call(t,u,1):Nt(t,u)}}return t}function Et(t,n){return Nu(Pn(t,n,gr),t+"")}function Ft(t,n,r,e){if(!tr(t))return t;n=Ct(n,t);for(var u=-1,o=n.length,i=o-1,c=t;null!=c&&++u<o;){var f=Bn(n[u]),a=r;if(u!=i){var l=c[f];a=e?e(l,f,c):Or,a===Or&&(a=tr(l)?l:wn(n[u+1])?[]:{})}nt(c,f,a),c=c[f]}return t}function Bt(t,n,r){var e=-1,u=t.length;n<0&&(n=-n>u?0:u+n),r=r>u?u:r,r<0&&(r+=u),u=n>r?0:r-n>>>0,n>>>=0;for(var o=Array(u);++e<u;)o[e]=t[e+n];return o}function Mt(t,n,r,e){n=r(n);for(var u=0,o=null==t?0:t.length,i=n!==n,c=null===n,f=ur(n),a=n===Or;u<o;){
|
||||
var l=lu((u+o)/2),s=r(t[l]),h=s!==Or,v=null===s,p=s===s,y=ur(s);if(i)var g=e||p;else g=a?p&&(e||h):c?p&&h&&(e||!v):f?p&&h&&!v&&(e||!y):!v&&!y&&(e?s<=n:s<n);g?u=l+1:o=l}return yu(o,Cr)}function Tt(t){if(typeof t=="string")return t;if(qu(t))return c(t,Tt)+"";if(ur(t))return Iu?Iu.call(t):"";var n=t+"";return"0"==n&&1/t==-Br?"-0":n}function Ut(t,n,r){var e=-1,u=o,c=t.length,f=true,a=[],l=a;if(r)f=false,u=i;else if(c>=Ar){var s=n?null:Bu(t);if(s)return m(s);f=false,u=d,l=new q}else l=n?[]:a;t:for(;++e<c;){var h=t[e],v=n?n(h):h;
|
||||
if(h=r||0!==h?h:0,f&&v===v){for(var p=l.length;p--;)if(l[p]===v)continue t;n&&l.push(v),a.push(h)}else u(l,v,r)||(l!==a&&l.push(v),a.push(h))}return a}function Nt(t,n){return n=Ct(n,t),t=En(t,n),null==t||delete t[Bn(Nn(n))]}function Ct(t,n){return qu(t)?t:mn(t,n)?[t]:Cu(ar(t))}function Dt(t,n){if(n)return t.slice();var r=t.length,e=ru?ru(r):new t.constructor(r);return t.copy(e),e}function Rt(t){var n=new t.constructor(t.byteLength);return new nu(n).set(new nu(t)),n}function Vt(t,n){return new t.constructor(n?Rt(t.buffer):t.buffer,t.byteOffset,t.byteLength);
|
||||
}function qt(n,r,e){return a(r?e(w(n),kr):w(n),t,new n.constructor)}function Wt(t){var n=new t.constructor(t.source,ze.exec(t));return n.lastIndex=t.lastIndex,n}function Gt(t,r,e){return a(r?e(m(t),kr):m(t),n,new t.constructor)}function Ht(t){return $u?Object($u.call(t)):{}}function Jt(t,n){return new t.constructor(n?Rt(t.buffer):t.buffer,t.byteOffset,t.length)}function Kt(t,n){var r=-1,e=t.length;for(n||(n=Array(e));++r<e;)n[r]=t[r];return n}function Qt(t,n,r,e){var u=!r;r||(r={});for(var o=-1,i=n.length;++o<i;){
|
||||
var c=n[o],f=e?e(r[c],t[c],c,r,t):Or;f===Or&&(f=t[c]),u?ot(r,c,f):nt(r,c,f)}return r}function Xt(t,n){return Qt(t,Mu(t),n)}function Yt(t,n){return Qt(t,Tu(t),n)}function Zt(t){return Et(function(n,r){var e=-1,u=r.length,o=u>1?r[u-1]:Or,i=u>2?r[2]:Or;for(o=t.length>3&&typeof o=="function"?(u--,o):Or,i&&On(r[0],r[1],i)&&(o=u<3?Or:o,u=1),n=Object(n);++e<u;){var c=r[e];c&&t(n,c,e,o)}return n})}function tn(t,n){return function(r,e){if(null==r)return r;if(!Qn(r))return t(r,e);for(var u=r.length,o=n?u:-1,i=Object(r);(n?o--:++o<u)&&e(i[o],o,i)!==false;);
|
||||
return r}}function nn(t){return function(n,r,e){for(var u=-1,o=Object(n),i=e(n),c=i.length;c--;){var f=i[t?c:++u];if(r(o[f],f,o)===false)break}return n}}function rn(t){return function(n,r,e){var u=Object(n);if(!Qn(n)){var o=sn(r,3);n=hr(n),r=function(t){return o(u[t],t,u)}}var i=t(n,r,e);return i>-1?u[o?n[i]:i]:Or}}function en(t){return rr(t)?Or:t}function un(t,n,r,e,u,o){var i=r&Lr,c=t.length,f=n.length;if(c!=f&&!(i&&f>c))return false;var a=o.get(t);if(a&&o.get(n))return a==n;var s=-1,h=true,v=r&Pr?new q:Or;
|
||||
for(o.set(t,n),o.set(n,t);++s<c;){var p=t[s],y=n[s];if(e)var g=i?e(y,p,s,n,t,o):e(p,y,s,t,n,o);if(g!==Or){if(g)continue;h=false;break}if(v){if(!l(n,function(t,n){if(!d(v,n)&&(p===t||u(p,t,r,e,o)))return v.push(n)})){h=false;break}}else if(p!==y&&!u(p,y,r,e,o)){h=false;break}}return o.delete(t),o.delete(n),h}function on(t,n,r,e,u,o,i){switch(r){case fe:if(t.byteLength!=n.byteLength||t.byteOffset!=n.byteOffset)return false;t=t.buffer,n=n.buffer;case ce:return!(t.byteLength!=n.byteLength||!o(new nu(t),new nu(n)));
|
||||
case qr:case Wr:case Qr:return Kn(+t,+n);case Gr:return t.name==n.name&&t.message==n.message;case ne:case ee:return t==n+"";case Kr:var c=w;case re:var f=e&Lr;if(c||(c=m),t.size!=n.size&&!f)return false;var a=i.get(t);if(a)return a==n;e|=Pr,i.set(t,n);var l=un(c(t),c(n),e,u,o,i);return i.delete(t),l;case ue:if($u)return $u.call(t)==$u.call(n)}return false}function cn(t,n,r,e,u,o){var i=r&Lr,c=an(t),f=c.length;if(f!=an(n).length&&!i)return false;for(var a=f;a--;){var l=c[a];if(!(i?l in n:Je.call(n,l)))return false;
|
||||
}var s=o.get(t);if(s&&o.get(n))return s==n;var h=true;o.set(t,n),o.set(n,t);for(var v=i;++a<f;){l=c[a];var p=t[l],y=n[l];if(e)var g=i?e(y,p,l,n,t,o):e(p,y,l,t,n,o);if(!(g===Or?p===y||u(p,y,r,e,o):g)){h=false;break}v||(v="constructor"==l)}if(h&&!v){var b=t.constructor,_=n.constructor;b!=_&&"constructor"in t&&"constructor"in n&&!(typeof b=="function"&&b instanceof b&&typeof _=="function"&&_ instanceof _)&&(h=false)}return o.delete(t),o.delete(n),h}function fn(t){return Nu(Pn(t,Or,Un),t+"")}function an(t){return st(t,hr,Mu);
|
||||
}function ln(t){return st(t,vr,Tu)}function sn(){var t=z.iteratee||br;return t=t===br?jt:t,arguments.length?t(arguments[0],arguments[1]):t}function hn(t,n){var r=t.__data__;return An(n)?r[typeof n=="string"?"string":"hash"]:r.map}function vn(t){for(var n=hr(t),r=n.length;r--;){var e=n[r],u=t[e];n[r]=[e,u,Sn(u)]}return n}function pn(t,n){var r=j(t,n);return _t(r)?r:Or}function yn(t){var n=Je.call(t,fu),r=t[fu];try{t[fu]=Or;var e=true}catch(t){}var u=Qe.call(t);return e&&(n?t[fu]=r:delete t[fu]),u}function gn(t,n,r){
|
||||
n=Ct(n,t);for(var e=-1,u=n.length,o=false;++e<u;){var i=Bn(n[e]);if(!(o=null!=t&&r(t,i)))break;t=t[i]}return o||++e!=u?o:(u=null==t?0:t.length,!!u&&Zn(u)&&wn(i,u)&&(qu(t)||Vu(t)))}function bn(t){var n=t.length,r=t.constructor(n);return n&&"string"==typeof t[0]&&Je.call(t,"index")&&(r.index=t.index,r.input=t.input),r}function _n(t){return typeof t.constructor!="function"||xn(t)?{}:Lu(eu(t))}function dn(t,n,r,e){var u=t.constructor;switch(n){case ce:return Rt(t);case qr:case Wr:return new u(+t);case fe:
|
||||
return Vt(t,e);case ae:case le:case se:case he:case ve:case pe:case ye:case ge:case be:return Jt(t,e);case Kr:return qt(t,e,r);case Qr:case ee:return new u(t);case ne:return Wt(t);case re:return Gt(t,e,r);case ue:return Ht(t)}}function jn(t){return qu(t)||Vu(t)||!!(cu&&t&&t[cu])}function wn(t,n){return n=null==n?Mr:n,!!n&&(typeof t=="number"||Ie.test(t))&&t>-1&&t%1==0&&t<n}function On(t,n,r){if(!tr(r))return false;var e=typeof n;return!!("number"==e?Qn(r)&&wn(n,r.length):"string"==e&&n in r)&&Kn(r[n],t);
|
||||
}function mn(t,n){if(qu(t))return false;var r=typeof t;return!("number"!=r&&"symbol"!=r&&"boolean"!=r&&null!=t&&!ur(t))||(de.test(t)||!_e.test(t)||null!=n&&t in Object(n))}function An(t){var n=typeof t;return"string"==n||"number"==n||"symbol"==n||"boolean"==n?"__proto__"!==t:null===t}function zn(t){return!!Ke&&Ke in t}function xn(t){var n=t&&t.constructor;return t===(typeof n=="function"&&n.prototype||We)}function Sn(t){return t===t&&!tr(t)}function kn(t,n){return function(r){return null!=r&&(r[t]===n&&(n!==Or||t in Object(r)));
|
||||
}}function $n(t){var n=Hn(t,function(t){return r.size===Sr&&r.clear(),t}),r=n.cache;return n}function In(t){var n=[];if(null!=t)for(var r in Object(t))n.push(r);return n}function Ln(t){return Qe.call(t)}function Pn(t,n,e){return n=pu(n===Or?t.length-1:n,0),function(){for(var u=arguments,o=-1,i=pu(u.length-n,0),c=Array(i);++o<i;)c[o]=u[n+o];o=-1;for(var f=Array(n+1);++o<n;)f[o]=u[o];return f[n]=e(c),r(t,this,f)}}function En(t,n){return n.length<2?t:lt(t,Bt(n,0,-1))}function Fn(t){var n=0,r=0;return function(){
|
||||
var e=gu(),u=Fr-(e-r);if(r=e,u>0){if(++n>=Er)return arguments[0]}else n=0;return t.apply(Or,arguments)}}function Bn(t){if(typeof t=="string"||ur(t))return t;var n=t+"";return"0"==n&&1/t==-Br?"-0":n}function Mn(t){if(null!=t){try{return He.call(t)}catch(t){}try{return t+""}catch(t){}}return""}function Tn(t,n,r){var e=null==t?0:t.length;if(!e)return-1;var u=null==r?0:ir(r);return u<0&&(u=pu(e+u,0)),s(t,sn(n,3),u)}function Un(t){return(null==t?0:t.length)?ft(t,1):[]}function Nn(t){var n=null==t?0:t.length;
|
||||
return n?t[n-1]:Or}function Cn(t,n){return t&&t.length&&n&&n.length?Lt(t,n):t}function Dn(t,n){var r=[];if(!t||!t.length)return r;var e=-1,u=[],o=t.length;for(n=sn(n,3);++e<o;){var i=t[e];n(i,e,t)&&(r.push(i),u.push(e))}return Pt(t,u),r}function Rn(t,n,r){return Mt(t,n,sn(r,2))}function Vn(t){return t&&t.length?Ut(t):[]}function qn(t,n){return(qu(t)?u:ct)(t,sn(n,3))}function Wn(t,n,r,e){t=Qn(t)?t:pr(t),r=r&&!e?ir(r):0;var u=t.length;return r<0&&(r=pu(u+r,0)),er(t)?r<=u&&t.indexOf(n,r)>-1:!!u&&h(t,n,r)>-1;
|
||||
}function Gn(t,n){return(qu(t)?c:mt)(t,sn(n,3))}function Hn(t,n){if(typeof t!="function"||null!=n&&typeof n!="function")throw new TypeError(zr);var r=function(){var e=arguments,u=n?n.apply(this,e):e[0],o=r.cache;if(o.has(u))return o.get(u);var i=t.apply(this,e);return r.cache=o.set(u,i)||o,i};return r.cache=new(Hn.Cache||U),r}function Jn(t){return it(t,kr|Ir)}function Kn(t,n){return t===n||t!==t&&n!==n}function Qn(t){return null!=t&&Zn(t.length)&&!Yn(t)}function Xn(t){return nr(t)&&Qn(t)}function Yn(t){
|
||||
if(!tr(t))return false;var n=ht(t);return n==Hr||n==Jr||n==Vr||n==te}function Zn(t){return typeof t=="number"&&t>-1&&t%1==0&&t<=Mr}function tr(t){var n=typeof t;return null!=t&&("object"==n||"function"==n)}function nr(t){return null!=t&&typeof t=="object"}function rr(t){if(!nr(t)||ht(t)!=Yr)return false;var n=eu(t);if(null===n)return true;var r=Je.call(n,"constructor")&&n.constructor;return typeof r=="function"&&r instanceof r&&He.call(r)==Xe}function er(t){return typeof t=="string"||!qu(t)&&nr(t)&&ht(t)==ee}function ur(t){
|
||||
return typeof t=="symbol"||nr(t)&&ht(t)==ue}function or(t){if(!t)return 0===t?t:0;if(t=cr(t),t===Br||t===-Br){return(t<0?-1:1)*Tr}return t===t?t:0}function ir(t){var n=or(t),r=n%1;return n===n?r?n-r:n:0}function cr(t){if(typeof t=="number")return t;if(ur(t))return Ur;if(tr(t)){var n=typeof t.valueOf=="function"?t.valueOf():t;t=tr(n)?n+"":n}if(typeof t!="string")return 0===t?t:+t;t=t.replace(me,"");var r=Se.test(t);return r||$e.test(t)?Ee(t.slice(2),r?2:8):xe.test(t)?Ur:+t}function fr(t){return Qt(t,vr(t));
|
||||
}function ar(t){return null==t?"":Tt(t)}function lr(t,n,r){var e=null==t?Or:lt(t,n);return e===Or?r:e}function sr(t,n){return null!=t&&gn(t,n,vt)}function hr(t){return Qn(t)?Z(t):wt(t)}function vr(t){return Qn(t)?Z(t,true):Ot(t)}function pr(t){return null==t?[]:_(t,hr(t))}function yr(t){return function(){return t}}function gr(t){return t}function br(t){return jt(typeof t=="function"?t:it(t,kr))}function _r(){}function dr(t){return mn(t)?y(Bn(t)):It(t)}function jr(){return[]}function wr(){return false}var Or,mr="4.17.4",Ar=200,zr="Expected a function",xr="__lodash_hash_undefined__",Sr=500,kr=1,$r=2,Ir=4,Lr=1,Pr=2,Er=800,Fr=16,Br=1/0,Mr=9007199254740991,Tr=1.7976931348623157e308,Ur=NaN,Nr=4294967295,Cr=Nr-1,Dr="[object Arguments]",Rr="[object Array]",Vr="[object AsyncFunction]",qr="[object Boolean]",Wr="[object Date]",Gr="[object Error]",Hr="[object Function]",Jr="[object GeneratorFunction]",Kr="[object Map]",Qr="[object Number]",Xr="[object Null]",Yr="[object Object]",Zr="[object Promise]",te="[object Proxy]",ne="[object RegExp]",re="[object Set]",ee="[object String]",ue="[object Symbol]",oe="[object Undefined]",ie="[object WeakMap]",ce="[object ArrayBuffer]",fe="[object DataView]",ae="[object Float32Array]",le="[object Float64Array]",se="[object Int8Array]",he="[object Int16Array]",ve="[object Int32Array]",pe="[object Uint8Array]",ye="[object Uint8ClampedArray]",ge="[object Uint16Array]",be="[object Uint32Array]",_e=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,de=/^\w*$/,je=/^\./,we=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Oe=/[\\^$.*+?()[\]{}|]/g,me=/^\s+|\s+$/g,Ae=/\\(\\)?/g,ze=/\w*$/,xe=/^[-+]0x[0-9a-f]+$/i,Se=/^0b[01]+$/i,ke=/^\[object .+?Constructor\]$/,$e=/^0o[0-7]+$/i,Ie=/^(?:0|[1-9]\d*)$/,Le={};
|
||||
Le[ae]=Le[le]=Le[se]=Le[he]=Le[ve]=Le[pe]=Le[ye]=Le[ge]=Le[be]=true,Le[Dr]=Le[Rr]=Le[ce]=Le[qr]=Le[fe]=Le[Wr]=Le[Gr]=Le[Hr]=Le[Kr]=Le[Qr]=Le[Yr]=Le[ne]=Le[re]=Le[ee]=Le[ie]=false;var Pe={};Pe[Dr]=Pe[Rr]=Pe[ce]=Pe[fe]=Pe[qr]=Pe[Wr]=Pe[ae]=Pe[le]=Pe[se]=Pe[he]=Pe[ve]=Pe[Kr]=Pe[Qr]=Pe[Yr]=Pe[ne]=Pe[re]=Pe[ee]=Pe[ue]=Pe[pe]=Pe[ye]=Pe[ge]=Pe[be]=true,Pe[Gr]=Pe[Hr]=Pe[ie]=false;var Ee=parseInt,Fe=typeof global=="object"&&global&&global.Object===Object&&global,Be=typeof self=="object"&&self&&self.Object===Object&&self,Me=Fe||Be||Function("return this")(),Te=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Ue=Te&&typeof module=="object"&&module&&!module.nodeType&&module,Ne=Ue&&Ue.exports===Te,Ce=Ne&&Fe.process,De=function(){
|
||||
try{return Ce&&Ce.binding&&Ce.binding("util")}catch(t){}}(),Re=De&&De.isTypedArray,Ve=Array.prototype,qe=Function.prototype,We=Object.prototype,Ge=Me["__core-js_shared__"],He=qe.toString,Je=We.hasOwnProperty,Ke=function(){var t=/[^.]+$/.exec(Ge&&Ge.keys&&Ge.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}(),Qe=We.toString,Xe=He.call(Object),Ye=RegExp("^"+He.call(Je).replace(Oe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),Ze=Ne?Me.Buffer:Or,tu=Me.Symbol,nu=Me.Uint8Array,ru=Ze?Ze.allocUnsafe:Or,eu=O(Object.getPrototypeOf,Object),uu=Object.create,ou=We.propertyIsEnumerable,iu=Ve.splice,cu=tu?tu.isConcatSpreadable:Or,fu=tu?tu.toStringTag:Or,au=function(){
|
||||
try{var t=pn(Object,"defineProperty");return t({},"",{}),t}catch(t){}}(),lu=Math.floor,su=Object.getOwnPropertySymbols,hu=Ze?Ze.isBuffer:Or,vu=O(Object.keys,Object),pu=Math.max,yu=Math.min,gu=Date.now,bu=pn(Me,"DataView"),_u=pn(Me,"Map"),du=pn(Me,"Promise"),ju=pn(Me,"Set"),wu=pn(Me,"WeakMap"),Ou=pn(Object,"create"),mu=Mn(bu),Au=Mn(_u),zu=Mn(du),xu=Mn(ju),Su=Mn(wu),ku=tu?tu.prototype:Or,$u=ku?ku.valueOf:Or,Iu=ku?ku.toString:Or,Lu=function(){function t(){}return function(n){if(!tr(n))return{};if(uu)return uu(n);
|
||||
t.prototype=n;var r=new t;return t.prototype=Or,r}}();x.prototype.clear=S,x.prototype.delete=k,x.prototype.get=$,x.prototype.has=I,x.prototype.set=L,P.prototype.clear=E,P.prototype.delete=F,P.prototype.get=B,P.prototype.has=M,P.prototype.set=T,U.prototype.clear=N,U.prototype.delete=C,U.prototype.get=D,U.prototype.has=R,U.prototype.set=V,q.prototype.add=q.prototype.push=W,q.prototype.has=G,H.prototype.clear=J,H.prototype.delete=K,H.prototype.get=Q,H.prototype.has=X,H.prototype.set=Y;var Pu=tn(at),Eu=nn(),Fu=au?function(t,n){
|
||||
return au(t,"toString",{configurable:true,enumerable:false,value:yr(n),writable:true})}:gr,Bu=ju&&1/m(new ju([,-0]))[1]==Br?function(t){return new ju(t)}:_r,Mu=su?function(t){return null==t?[]:(t=Object(t),u(su(t),function(n){return ou.call(t,n)}))}:jr,Tu=su?function(t){for(var n=[];t;)f(n,Mu(t)),t=eu(t);return n}:jr,Uu=ht;(bu&&Uu(new bu(new ArrayBuffer(1)))!=fe||_u&&Uu(new _u)!=Kr||du&&Uu(du.resolve())!=Zr||ju&&Uu(new ju)!=re||wu&&Uu(new wu)!=ie)&&(Uu=function(t){var n=ht(t),r=n==Yr?t.constructor:Or,e=r?Mn(r):"";
|
||||
if(e)switch(e){case mu:return fe;case Au:return Kr;case zu:return Zr;case xu:return re;case Su:return ie}return n});var Nu=Fn(Fu),Cu=$n(function(t){var n=[];return je.test(t)&&n.push(""),t.replace(we,function(t,r,e,u){n.push(e?u.replace(Ae,"$1"):r||t)}),n}),Du=Et(Cn),Ru=rn(Tn);Hn.Cache=U;var Vu=pt(function(){return arguments}())?pt:function(t){return nr(t)&&Je.call(t,"callee")&&!ou.call(t,"callee")},qu=Array.isArray,Wu=hu||wr,Gu=Re?b(Re):dt,Hu=Zt(function(t,n,r){xt(t,n,r)}),Ju=fn(function(t,n){var r={};
|
||||
if(null==t)return r;var e=false;n=c(n,function(n){return n=Ct(n,t),e||(e=n.length>1),n}),Qt(t,ln(t),r),e&&(r=it(r,kr|$r|Ir,en));for(var u=n.length;u--;)Nt(r,n[u]);return r}),Ku=fn(function(t,n){return null==t?{}:kt(t,n)});z.constant=yr,z.filter=qn,z.flatten=Un,z.iteratee=br,z.keys=hr,z.keysIn=vr,z.map=Gn,z.memoize=Hn,z.merge=Hu,z.omit=Ju,z.pick=Ku,z.property=dr,z.pull=Du,z.pullAll=Cn,z.remove=Dn,z.toPlainObject=fr,z.uniq=Vn,z.values=pr,z.cloneDeep=Jn,z.eq=Kn,z.find=Ru,z.findIndex=Tn,z.get=lr,z.hasIn=sr,
|
||||
z.identity=gr,z.includes=Wn,z.isArguments=Vu,z.isArray=qu,z.isArrayLike=Qn,z.isArrayLikeObject=Xn,z.isBuffer=Wu,z.isFunction=Yn,z.isLength=Zn,z.isObject=tr,z.isObjectLike=nr,z.isPlainObject=rr,z.isString=er,z.isSymbol=ur,z.isTypedArray=Gu,z.last=Nn,z.stubArray=jr,z.stubFalse=wr,z.noop=_r,z.sortedIndexBy=Rn,z.toFinite=or,z.toInteger=ir,z.toNumber=cr,z.toString=ar,z.VERSION=mr,typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Me._=z, define(function(){return z})):Ue?((Ue.exports=z)._=z,
|
||||
Te._=z):Me._=z}).call(this);
|
||||
;(function(){function t(t,e){return t.set(e[0],e[1]),t}function e(t,e){return t.add(e),t}function n(t,e,n){switch(n.length){case 0:return t.call(e);case 1:return t.call(e,n[0]);case 2:return t.call(e,n[0],n[1]);case 3:return t.call(e,n[0],n[1],n[2])}return t.apply(e,n)}function r(t,e){for(var n=-1,r=null==t?0:t.length;++n<r&&false!==e(t[n],n,t););}function o(t,e){for(var n=-1,r=null==t?0:t.length,o=0,u=[];++n<r;){var c=t[n];e(c,n,t)&&(u[o++]=c)}return u}function u(t,e){return!(null==t||!t.length)&&-1<s(t,e,0);
|
||||
}function c(t,e){for(var n=-1,r=null==t?0:t.length,o=Array(r);++n<r;)o[n]=e(t[n],n,t);return o}function i(t,e){for(var n=-1,r=e.length,o=t.length;++n<r;)t[o+n]=e[n];return t}function a(t,e,n){for(var r=-1,o=null==t?0:t.length;++r<o;)n=e(n,t[r],r,t);return n}function f(t,e){for(var n=-1,r=null==t?0:t.length;++n<r;)if(e(t[n],n,t))return true;return false}function l(t,e,n){var r=t.length;for(n+=-1;++n<r;)if(e(t[n],n,t))return n;return-1}function s(t,e,n){if(e===e)t:{--n;for(var r=t.length;++n<r;)if(t[n]===e){
|
||||
t=n;break t}t=-1}else t=l(t,b,n);return t}function b(t){return t!==t}function h(t){return function(e){return null==e?ae:e[t]}}function p(t){return function(e){return t(e)}}function y(t,e){return c(e,function(e){return t[e]})}function j(t,e){return t.has(e)}function v(t){var e=-1,n=Array(t.size);return t.forEach(function(t,r){n[++e]=[r,t]}),n}function g(t){var e=Object;return function(n){return t(e(n))}}function _(t){var e=-1,n=Array(t.size);return t.forEach(function(t){n[++e]=t}),n}function d(){}
|
||||
function A(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function w(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function m(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e<n;){var r=t[e];this.set(r[0],r[1])}}function O(t){var e=-1,n=null==t?0:t.length;for(this.__data__=new m;++e<n;)this.add(t[e])}function S(t){this.size=(this.__data__=new w(t)).size}function k(t,e){var n=Dn(t),r=!n&&Bn(t),o=!n&&!r&&Pn(t),u=!n&&!r&&!o&&Ln(t);
|
||||
if(n=n||r||o||u){for(var r=t.length,c=String,i=-1,a=Array(r);++i<r;)a[i]=c(i);r=a}else r=[];var f,c=r.length;for(f in t)!e&&!Ne.call(t,f)||n&&("length"==f||o&&("offset"==f||"parent"==f)||u&&("buffer"==f||"byteLength"==f||"byteOffset"==f)||mt(f,c))||r.push(f);return r}function z(t,e,n){(n===ae||Bt(t[e],n))&&(n!==ae||e in t)||M(t,e,n)}function x(t,e,n){var r=t[e];Ne.call(t,e)&&Bt(r,n)&&(n!==ae||e in t)||M(t,e,n)}function I(t,e){for(var n=t.length;n--;)if(Bt(t[n][0],e))return n;return-1}function F(t,e){
|
||||
return t&&ut(e,Yt(e),t)}function E(t,e){return t&&ut(e,Zt(e),t)}function M(t,e,n){"__proto__"==e&&tn?tn(t,e,{configurable:true,enumerable:true,value:n,writable:true}):t[e]=n}function $(t,e,n,o,u,c){var i,a=1&e,f=2&e,l=4&e;if(n&&(i=u?n(t,o,u,c):n(t)),i!==ae)return i;if(!Vt(t))return t;if(o=Dn(t)){if(i=_t(t),!a)return ot(t,i)}else{var s=Fn(t),b="[object Function]"==s||"[object GeneratorFunction]"==s;if(Pn(t))return et(t,a);if("[object Object]"==s||"[object Arguments]"==s||b&&!u){if(i=f||b?{}:dt(t),!a)return f?it(t,E(i,t)):ct(t,F(i,t));
|
||||
}else{if(!Oe[s])return u?t:{};i=At(t,s,$,a)}}if(c||(c=new S),u=c.get(t))return u;c.set(t,i);var f=l?f?pt:ht:f?Zt:Yt,h=o?ae:f(t);return r(h||t,function(r,o){h&&(o=r,r=t[o]),x(i,o,$(r,e,n,o,t,c))}),i}function U(t,e){var n=[];return On(t,function(t,r,o){e(t,r,o)&&n.push(t)}),n}function B(t,e,n,r,o){var u=-1,c=t.length;for(n||(n=wt),o||(o=[]);++u<c;){var a=t[u];0<e&&n(a)?1<e?B(a,e-1,n,r,o):i(o,a):r||(o[o.length]=a)}return o}function D(t,e){e=tt(e,t);for(var n=0,r=e.length;null!=t&&n<r;)t=t[xt(e[n++])];
|
||||
return n&&n==r?t:ae}function P(t,e,n){return e=e(t),Dn(t)?e:i(e,n(t))}function L(t){if(null==t)t=t===ae?"[object Undefined]":"[object Null]";else if(Ze&&Ze in Object(t)){var e=Ne.call(t,Ze),n=t[Ze];try{t[Ze]=ae;var r=true}catch(t){}var o=Ce.call(t);r&&(e?t[Ze]=n:delete t[Ze]),t=o}else t=Ce.call(t);return t}function N(t){return Ct(t)&&"[object Arguments]"==L(t)}function V(t,e,n,r,o){if(t===e)e=true;else if(null==t||null==e||!Ct(t)&&!Ct(e))e=t!==t&&e!==e;else t:{var u=Dn(t),c=Dn(e),i=u?"[object Array]":Fn(t),a=c?"[object Array]":Fn(e),i="[object Arguments]"==i?"[object Object]":i,a="[object Arguments]"==a?"[object Object]":a,f="[object Object]"==i,c="[object Object]"==a;
|
||||
if((a=i==a)&&Pn(t)){if(!Pn(e)){e=false;break t}u=true,f=false}if(a&&!f)o||(o=new S),e=u||Ln(t)?lt(t,e,n,r,V,o):st(t,e,i,n,r,V,o);else{if(!(1&n)&&(u=f&&Ne.call(t,"__wrapped__"),i=c&&Ne.call(e,"__wrapped__"),u||i)){t=u?t.value():t,e=i?e.value():e,o||(o=new S),e=V(t,e,n,r,o);break t}if(a)e:if(o||(o=new S),u=1&n,i=ht(t),c=i.length,a=ht(e).length,c==a||u){for(f=c;f--;){var l=i[f];if(!(u?l in e:Ne.call(e,l))){e=false;break e}}if((a=o.get(t))&&o.get(e))e=a==e;else{a=true,o.set(t,e),o.set(e,t);for(var s=u;++f<c;){var l=i[f],b=t[l],h=e[l];
|
||||
if(r)var p=u?r(h,b,l,e,t,o):r(b,h,l,t,e,o);if(p===ae?b!==h&&!V(b,h,n,r,o):!p){a=false;break}s||(s="constructor"==l)}a&&!s&&(n=t.constructor,r=e.constructor,n!=r&&"constructor"in t&&"constructor"in e&&!(typeof n=="function"&&n instanceof n&&typeof r=="function"&&r instanceof r)&&(a=false)),o.delete(t),o.delete(e),e=a}}else e=false;else e=false}}return e}function C(t,e){var n=e.length,r=n;if(null==t)return!r;for(t=Object(t);n--;){var o=e[n];if(o[2]?o[1]!==t[o[0]]:!(o[0]in t))return false}for(;++n<r;){var o=e[n],u=o[0],c=t[u],i=o[1];
|
||||
if(o[2]){if(c===ae&&!(u in t))return false}else if(o=new S,void 0===ae?!V(i,c,3,void 0,o):1)return false}return true}function R(t){return Ct(t)&&Nt(t.length)&&!!me[L(t)]}function T(t){return typeof t=="function"?t:null==t?ne:typeof t=="object"?Dn(t)?G(t[0],t[1]):q(t):ue(t)}function W(t,e){var n=-1,r=Dt(t)?Array(t.length):[];return On(t,function(t,o,u){r[++n]=e(t,o,u)}),r}function q(t){var e=vt(t);return 1==e.length&&e[0][2]?kt(e[0][0],e[0][1]):function(n){return n===t||C(n,e)}}function G(t,e){return Ot(t)&&e===e&&!Vt(e)?kt(xt(t),e):function(n){
|
||||
var r=Qt(n,t);return r===ae&&r===e?Xt(n,t):V(e,r,3)}}function H(t,e,n,r,o){t!==e&&Sn(e,function(u,c){if(Vt(u)){o||(o=new S);var i=o,a=t[c],f=e[c],l=i.get(f);if(l)z(t,c,l);else{var l=r?r(a,f,c+"",t,e,i):ae,s=l===ae;if(s){var b=Dn(f),h=!b&&Pn(f),p=!b&&!h&&Ln(f),l=f;b||h||p?Dn(a)?l=a:Pt(a)?l=ot(a):h?(s=false,l=et(f,true)):p?(s=false,l=rt(f,true)):l=[]:Rt(f)||Bn(f)?(l=a,Bn(a)?l=Jt(a):(!Vt(a)||n&&Lt(a))&&(l=dt(f))):s=false}s&&(i.set(f,l),H(l,f,n,r,i),i.delete(f)),z(t,c,l)}}else i=r?r(t[c],u,c+"",t,e,o):ae,i===ae&&(i=u),
|
||||
z(t,c,i)},Zt)}function J(t,e){return K(t,e,function(e,n){return Xt(t,n)})}function K(t,e,n){for(var r=-1,o=e.length,u={};++r<o;){var c=e[r],i=D(t,c);if(n(i,c)){var a=u,c=tt(c,t);if(Vt(a))for(var c=tt(c,a),f=-1,l=c.length,s=l-1;null!=a&&++f<l;){var b=xt(c[f]),h=i;if(f!=s){var p=a[b],h=ae;h===ae&&(h=Vt(p)?p:mt(c[f+1])?[]:{})}x(a,b,h),a=a[b]}}}return u}function Q(t){return function(e){return D(e,t)}}function X(t){return En(zt(t,void 0,ne),t+"")}function Y(t){if(typeof t=="string")return t;if(Dn(t))return c(t,Y)+"";
|
||||
if(Wt(t))return wn?wn.call(t):"";var e=t+"";return"0"==e&&1/t==-fe?"-0":e}function Z(t,e){e=tt(e,t);var n;if(2>e.length)n=t;else{n=e;var r=0,o=-1,u=-1,c=n.length;for(0>r&&(r=-r>c?0:c+r),o=o>c?c:o,0>o&&(o+=c),c=r>o?0:o-r>>>0,r>>>=0,o=Array(c);++u<c;)o[u]=n[u+r];n=D(t,o)}t=n,null==t||delete t[xt(Mt(e))]}function tt(t,e){return Dn(t)?t:Ot(t,e)?[t]:Mn(Kt(t))}function et(t,e){if(e)return t.slice();var n=t.length,n=He?He(n):new t.constructor(n);return t.copy(n),n}function nt(t){var e=new t.constructor(t.byteLength);
|
||||
return new Ge(e).set(new Ge(t)),e}function rt(t,e){return new t.constructor(e?nt(t.buffer):t.buffer,t.byteOffset,t.length)}function ot(t,e){var n=-1,r=t.length;for(e||(e=Array(r));++n<r;)e[n]=t[n];return e}function ut(t,e,n){var r=!n;n||(n={});for(var o=-1,u=e.length;++o<u;){var c=e[o],i=ae;i===ae&&(i=t[c]),r?M(n,c,i):x(n,c,i)}return n}function ct(t,e){return ut(t,xn(t),e)}function it(t,e){return ut(t,In(t),e)}function at(t){return X(function(e,n){var r,o=-1,u=n.length,c=1<u?n[u-1]:ae,i=2<u?n[2]:ae,c=3<t.length&&typeof c=="function"?(u--,
|
||||
c):ae;if(r=i){r=n[0];var a=n[1];if(Vt(i)){var f=typeof a;r=!!("number"==f?Dt(i)&&mt(a,i.length):"string"==f&&a in i)&&Bt(i[a],r)}else r=false}for(r&&(c=3>u?ae:c,u=1),e=Object(e);++o<u;)(i=n[o])&&t(e,i,o,c);return e})}function ft(t){return Rt(t)?ae:t}function lt(t,e,n,r,o,u){var c=1&n,i=t.length,a=e.length;if(i!=a&&!(c&&a>i))return false;if((a=u.get(t))&&u.get(e))return a==e;var a=-1,l=true,s=2&n?new O:ae;for(u.set(t,e),u.set(e,t);++a<i;){var b=t[a],h=e[a];if(r)var p=c?r(h,b,a,e,t,u):r(b,h,a,t,e,u);if(p!==ae){
|
||||
if(p)continue;l=false;break}if(s){if(!f(e,function(t,e){if(!j(s,e)&&(b===t||o(b,t,n,r,u)))return s.push(e)})){l=false;break}}else if(b!==h&&!o(b,h,n,r,u)){l=false;break}}return u.delete(t),u.delete(e),l}function st(t,e,n,r,o,u,c){switch(n){case"[object DataView]":if(t.byteLength!=e.byteLength||t.byteOffset!=e.byteOffset)break;t=t.buffer,e=e.buffer;case"[object ArrayBuffer]":if(t.byteLength!=e.byteLength||!u(new Ge(t),new Ge(e)))break;return true;case"[object Boolean]":case"[object Date]":case"[object Number]":
|
||||
return Bt(+t,+e);case"[object Error]":return t.name==e.name&&t.message==e.message;case"[object RegExp]":case"[object String]":return t==e+"";case"[object Map]":var i=v;case"[object Set]":if(i||(i=_),t.size!=e.size&&!(1&r))break;return(n=c.get(t))?n==e:(r|=2,c.set(t,e),e=lt(i(t),i(e),r,o,u,c),c.delete(t),e);case"[object Symbol]":if(An)return An.call(t)==An.call(e)}return false}function bt(t){return En(zt(t,ae,Et),t+"")}function ht(t){return P(t,Yt,xn)}function pt(t){return P(t,Zt,In)}function yt(){var t=d.iteratee||re,t=t===re?T:t;
|
||||
return arguments.length?t(arguments[0],arguments[1]):t}function jt(t,e){var n=t.__data__,r=typeof e;return("string"==r||"number"==r||"symbol"==r||"boolean"==r?"__proto__"!==e:null===e)?n[typeof e=="string"?"string":"hash"]:n.map}function vt(t){for(var e=Yt(t),n=e.length;n--;){var r=e[n],o=t[r];e[n]=[r,o,o===o&&!Vt(o)]}return e}function gt(t,e){var n=null==t?ae:t[e];return(!Vt(n)||Ve&&Ve in n?0:(Lt(n)?Te:de).test(It(n)))?n:ae}function _t(t){var e=t.length,n=t.constructor(e);return e&&"string"==typeof t[0]&&Ne.call(t,"index")&&(n.index=t.index,
|
||||
n.input=t.input),n}function dt(t){return typeof t.constructor!="function"||St(t)?{}:mn(Je(t))}function At(n,r,o,u){var c=n.constructor;switch(r){case"[object ArrayBuffer]":return nt(n);case"[object Boolean]":case"[object Date]":return new c(+n);case"[object DataView]":return r=u?nt(n.buffer):n.buffer,new n.constructor(r,n.byteOffset,n.byteLength);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":
|
||||
case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return rt(n,u);case"[object Map]":return r=u?o(v(n),1):v(n),a(r,t,new n.constructor);case"[object Number]":case"[object String]":return new c(n);case"[object RegExp]":return r=new n.constructor(n.source,ve.exec(n)),r.lastIndex=n.lastIndex,r;case"[object Set]":return r=u?o(_(n),1):_(n),a(r,e,new n.constructor);case"[object Symbol]":return An?Object(An.call(n)):{}}}function wt(t){return Dn(t)||Bn(t)||!!(Ye&&t&&t[Ye]);
|
||||
}function mt(t,e){return e=null==e?9007199254740991:e,!!e&&(typeof t=="number"||we.test(t))&&-1<t&&0==t%1&&t<e}function Ot(t,e){if(Dn(t))return false;var n=typeof t;return!("number"!=n&&"symbol"!=n&&"boolean"!=n&&null!=t&&!Wt(t))||(be.test(t)||!se.test(t)||null!=e&&t in Object(e))}function St(t){var e=t&&t.constructor;return t===(typeof e=="function"&&e.prototype||De)}function kt(t,e){return function(n){return null!=n&&(n[t]===e&&(e!==ae||t in Object(n)))}}function zt(t,e,r){return e=un(e===ae?t.length-1:e,0),
|
||||
function(){for(var o=arguments,u=-1,c=un(o.length-e,0),i=Array(c);++u<c;)i[u]=o[e+u];for(u=-1,c=Array(e+1);++u<e;)c[u]=o[u];return c[e]=r(i),n(t,this,c)}}function xt(t){if(typeof t=="string"||Wt(t))return t;var e=t+"";return"0"==e&&1/t==-fe?"-0":e}function It(t){if(null!=t){try{return Le.call(t)}catch(t){}return t+""}return""}function Ft(t,e,n){var r=null==t?0:t.length;return r?(n=null==n?0:Gt(n),0>n&&(n=un(r+n,0)),l(t,yt(e,3),n)):-1}function Et(t){return(null==t?0:t.length)?B(t,1):[]}function Mt(t){
|
||||
var e=null==t?0:t.length;return e?t[e-1]:ae}function $t(t,e){var n;if(t&&t.length&&e&&e.length){n=e;var r=s,o=-1,u=n.length;for(t===n&&(n=ot(n));++o<u;)for(var c=0,i=n[o];-1<(c=r(t,i,c,void 0));)t!==t&&Xe.call(t,c,1),Xe.call(t,c,1);n=t}else n=t;return n}function Ut(t,e){function n(){var r=arguments,o=e?e.apply(this,r):r[0],u=n.cache;return u.has(o)?u.get(o):(r=t.apply(this,r),n.cache=u.set(o,r)||u,r)}if(typeof t!="function"||null!=e&&typeof e!="function")throw new TypeError("Expected a function");
|
||||
return n.cache=new(Ut.Cache||m),n}function Bt(t,e){return t===e||t!==t&&e!==e}function Dt(t){return null!=t&&Nt(t.length)&&!Lt(t)}function Pt(t){return Ct(t)&&Dt(t)}function Lt(t){return!!Vt(t)&&(t=L(t),"[object Function]"==t||"[object GeneratorFunction]"==t||"[object AsyncFunction]"==t||"[object Proxy]"==t)}function Nt(t){return typeof t=="number"&&-1<t&&0==t%1&&9007199254740991>=t}function Vt(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function Ct(t){return null!=t&&typeof t=="object";
|
||||
}function Rt(t){return!(!Ct(t)||"[object Object]"!=L(t))&&(t=Je(t),null===t||(t=Ne.call(t,"constructor")&&t.constructor,typeof t=="function"&&t instanceof t&&Le.call(t)==Re))}function Tt(t){return typeof t=="string"||!Dn(t)&&Ct(t)&&"[object String]"==L(t)}function Wt(t){return typeof t=="symbol"||Ct(t)&&"[object Symbol]"==L(t)}function qt(t){return t?(t=Ht(t),t===fe||t===-fe?1.7976931348623157e308*(0>t?-1:1):t===t?t:0):0===t?t:0}function Gt(t){t=qt(t);var e=t%1;return t===t?e?t-e:t:0}function Ht(t){
|
||||
if(typeof t=="number")return t;if(Wt(t))return le;if(Vt(t)&&(t=typeof t.valueOf=="function"?t.valueOf():t,t=Vt(t)?t+"":t),typeof t!="string")return 0===t?t:+t;t=t.replace(ye,"");var e=_e.test(t);return e||Ae.test(t)?ke(t.slice(2),e?2:8):ge.test(t)?le:+t}function Jt(t){return ut(t,Zt(t))}function Kt(t){return null==t?"":Y(t)}function Qt(t,e,n){return t=null==t?ae:D(t,e),t===ae?n:t}function Xt(t,e){var n;if(n=null!=t){n=t;var r;r=tt(e,n);for(var o=-1,u=r.length,c=false;++o<u;){var i=xt(r[o]);if(!(c=null!=n&&null!=n&&i in Object(n)))break;
|
||||
n=n[i]}c||++o!=u?n=c:(u=null==n?0:n.length,n=!!u&&Nt(u)&&mt(i,u)&&(Dn(n)||Bn(n)))}return n}function Yt(t){if(Dt(t))t=k(t);else if(St(t)){var e,n=[];for(e in Object(t))Ne.call(t,e)&&"constructor"!=e&&n.push(e);t=n}else t=on(t);return t}function Zt(t){if(Dt(t))t=k(t,true);else if(Vt(t)){var e,n=St(t),r=[];for(e in t)("constructor"!=e||!n&&Ne.call(t,e))&&r.push(e);t=r}else{if(e=[],null!=t)for(n in Object(t))e.push(n);t=e}return t}function te(t){return null==t?[]:y(t,Yt(t))}function ee(t){return function(){
|
||||
return t}}function ne(t){return t}function re(t){return T(typeof t=="function"?t:$(t,1))}function oe(){}function ue(t){return Ot(t)?h(xt(t)):Q(t)}function ce(){return[]}function ie(){return false}var ae,fe=1/0,le=NaN,se=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,be=/^\w*$/,he=/^\./,pe=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,ye=/^\s+|\s+$/g,je=/\\(\\)?/g,ve=/\w*$/,ge=/^[-+]0x[0-9a-f]+$/i,_e=/^0b[01]+$/i,de=/^\[object .+?Constructor\]$/,Ae=/^0o[0-7]+$/i,we=/^(?:0|[1-9]\d*)$/,me={};
|
||||
me["[object Float32Array]"]=me["[object Float64Array]"]=me["[object Int8Array]"]=me["[object Int16Array]"]=me["[object Int32Array]"]=me["[object Uint8Array]"]=me["[object Uint8ClampedArray]"]=me["[object Uint16Array]"]=me["[object Uint32Array]"]=true,me["[object Arguments]"]=me["[object Array]"]=me["[object ArrayBuffer]"]=me["[object Boolean]"]=me["[object DataView]"]=me["[object Date]"]=me["[object Error]"]=me["[object Function]"]=me["[object Map]"]=me["[object Number]"]=me["[object Object]"]=me["[object RegExp]"]=me["[object Set]"]=me["[object String]"]=me["[object WeakMap]"]=false;
|
||||
var Oe={};Oe["[object Arguments]"]=Oe["[object Array]"]=Oe["[object ArrayBuffer]"]=Oe["[object DataView]"]=Oe["[object Boolean]"]=Oe["[object Date]"]=Oe["[object Float32Array]"]=Oe["[object Float64Array]"]=Oe["[object Int8Array]"]=Oe["[object Int16Array]"]=Oe["[object Int32Array]"]=Oe["[object Map]"]=Oe["[object Number]"]=Oe["[object Object]"]=Oe["[object RegExp]"]=Oe["[object Set]"]=Oe["[object String]"]=Oe["[object Symbol]"]=Oe["[object Uint8Array]"]=Oe["[object Uint8ClampedArray]"]=Oe["[object Uint16Array]"]=Oe["[object Uint32Array]"]=true,
|
||||
Oe["[object Error]"]=Oe["[object Function]"]=Oe["[object WeakMap]"]=false;var Se,ke=parseInt,ze=typeof global=="object"&&global&&global.Object===Object&&global,xe=typeof self=="object"&&self&&self.Object===Object&&self,Ie=ze||xe||Function("return this")(),Fe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Ee=Fe&&typeof module=="object"&&module&&!module.nodeType&&module,Me=Ee&&Ee.exports===Fe,$e=Me&&ze.process;t:{try{Se=$e&&$e.binding&&$e.binding("util");break t}catch(t){}Se=void 0}var Ue=Se&&Se.isTypedArray,Be=Array.prototype,De=Object.prototype,Pe=Ie["__core-js_shared__"],Le=Function.prototype.toString,Ne=De.hasOwnProperty,Ve=function(){
|
||||
var t=/[^.]+$/.exec(Pe&&Pe.keys&&Pe.keys.IE_PROTO||"");return t?"Symbol(src)_1."+t:""}(),Ce=De.toString,Re=Le.call(Object),Te=RegExp("^"+Le.call(Ne).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),We=Me?Ie.Buffer:ae,qe=Ie.Symbol,Ge=Ie.Uint8Array,He=We?We.a:ae,Je=g(Object.getPrototypeOf),Ke=Object.create,Qe=De.propertyIsEnumerable,Xe=Be.splice,Ye=qe?qe.isConcatSpreadable:ae,Ze=qe?qe.toStringTag:ae,tn=function(){try{var t=gt(Object,"defineProperty");
|
||||
return t({},"",{}),t}catch(t){}}(),en=Math.floor,nn=Object.getOwnPropertySymbols,rn=We?We.isBuffer:ae,on=g(Object.keys),un=Math.max,cn=Math.min,an=Date.now,fn=gt(Ie,"DataView"),ln=gt(Ie,"Map"),sn=gt(Ie,"Promise"),bn=gt(Ie,"Set"),hn=gt(Ie,"WeakMap"),pn=gt(Object,"create"),yn=It(fn),jn=It(ln),vn=It(sn),gn=It(bn),_n=It(hn),dn=qe?qe.prototype:ae,An=dn?dn.valueOf:ae,wn=dn?dn.toString:ae,mn=function(){function t(){}return function(e){return Vt(e)?Ke?Ke(e):(t.prototype=e,e=new t,t.prototype=ae,e):{}}}();
|
||||
A.prototype.clear=function(){this.__data__=pn?pn(null):{},this.size=0},A.prototype.delete=function(t){return t=this.has(t)&&delete this.__data__[t],this.size-=t?1:0,t},A.prototype.get=function(t){var e=this.__data__;return pn?(t=e[t],"__lodash_hash_undefined__"===t?ae:t):Ne.call(e,t)?e[t]:ae},A.prototype.has=function(t){var e=this.__data__;return pn?e[t]!==ae:Ne.call(e,t)},A.prototype.set=function(t,e){var n=this.__data__;return this.size+=this.has(t)?0:1,n[t]=pn&&e===ae?"__lodash_hash_undefined__":e,
|
||||
this},w.prototype.clear=function(){this.__data__=[],this.size=0},w.prototype.delete=function(t){var e=this.__data__;return t=I(e,t),!(0>t)&&(t==e.length-1?e.pop():Xe.call(e,t,1),--this.size,true)},w.prototype.get=function(t){var e=this.__data__;return t=I(e,t),0>t?ae:e[t][1]},w.prototype.has=function(t){return-1<I(this.__data__,t)},w.prototype.set=function(t,e){var n=this.__data__,r=I(n,t);return 0>r?(++this.size,n.push([t,e])):n[r][1]=e,this},m.prototype.clear=function(){this.size=0,this.__data__={
|
||||
hash:new A,map:new(ln||w),string:new A}},m.prototype.delete=function(t){return t=jt(this,t).delete(t),this.size-=t?1:0,t},m.prototype.get=function(t){return jt(this,t).get(t)},m.prototype.has=function(t){return jt(this,t).has(t)},m.prototype.set=function(t,e){var n=jt(this,t),r=n.size;return n.set(t,e),this.size+=n.size==r?0:1,this},O.prototype.add=O.prototype.push=function(t){return this.__data__.set(t,"__lodash_hash_undefined__"),this},O.prototype.has=function(t){return this.__data__.has(t)},S.prototype.clear=function(){
|
||||
this.__data__=new w,this.size=0},S.prototype.delete=function(t){var e=this.__data__;return t=e.delete(t),this.size=e.size,t},S.prototype.get=function(t){return this.__data__.get(t)},S.prototype.has=function(t){return this.__data__.has(t)},S.prototype.set=function(t,e){var n=this.__data__;if(n instanceof w){var r=n.__data__;if(!ln||199>r.length)return r.push([t,e]),this.size=++n.size,this;n=this.__data__=new m(r)}return n.set(t,e),this.size=n.size,this};var On=function(t,e){return function(n,r){if(null==n)return n;
|
||||
if(!Dt(n))return t(n,r);for(var o=n.length,u=e?o:-1,c=Object(n);(e?u--:++u<o)&&false!==r(c[u],u,c););return n}}(function(t,e){return t&&Sn(t,e,Yt)}),Sn=function(t){return function(e,n,r){var o=-1,u=Object(e);r=r(e);for(var c=r.length;c--;){var i=r[t?c:++o];if(false===n(u[i],i,u))break}return e}}(),kn=tn?function(t,e){return tn(t,"toString",{configurable:true,enumerable:false,value:ee(e),writable:true})}:ne,zn=bn&&1/_(new bn([,-0]))[1]==fe?function(t){return new bn(t)}:oe,xn=nn?function(t){return null==t?[]:(t=Object(t),
|
||||
o(nn(t),function(e){return Qe.call(t,e)}))}:ce,In=nn?function(t){for(var e=[];t;)i(e,xn(t)),t=Je(t);return e}:ce,Fn=L;(fn&&"[object DataView]"!=Fn(new fn(new ArrayBuffer(1)))||ln&&"[object Map]"!=Fn(new ln)||sn&&"[object Promise]"!=Fn(sn.resolve())||bn&&"[object Set]"!=Fn(new bn)||hn&&"[object WeakMap]"!=Fn(new hn))&&(Fn=function(t){var e=L(t);if(t=(t="[object Object]"==e?t.constructor:ae)?It(t):"")switch(t){case yn:return"[object DataView]";case jn:return"[object Map]";case vn:return"[object Promise]";
|
||||
case gn:return"[object Set]";case _n:return"[object WeakMap]"}return e});var En=function(t){var e=0,n=0;return function(){var r=an(),o=16-(r-n);if(n=r,0<o){if(800<=++e)return arguments[0]}else e=0;return t.apply(ae,arguments)}}(kn),Mn=function(t){t=Ut(t,function(t){return 500===e.size&&e.clear(),t});var e=t.cache;return t}(function(t){var e=[];return he.test(t)&&e.push(""),t.replace(pe,function(t,n,r,o){e.push(r?o.replace(je,"$1"):n||t)}),e}),$n=X($t),Un=function(t){return function(e,n,r){var o=Object(e);
|
||||
if(!Dt(e)){var u=yt(n,3);e=Yt(e),n=function(t){return u(o[t],t,o)}}return n=t(e,n,r),-1<n?o[u?e[n]:n]:ae}}(Ft);Ut.Cache=m;var Bn=N(function(){return arguments}())?N:function(t){return Ct(t)&&Ne.call(t,"callee")&&!Qe.call(t,"callee")},Dn=Array.isArray,Pn=rn||ie,Ln=Ue?p(Ue):R,Nn=at(function(t,e,n){H(t,e,n)}),Vn=at(function(t,e,n,r){H(t,e,n,r)}),Cn=bt(function(t,e){var n={};if(null==t)return n;var r=false;e=c(e,function(e){return e=tt(e,t),r||(r=1<e.length),e}),ut(t,pt(t),n),r&&(n=$(n,7,ft));for(var o=e.length;o--;)Z(n,e[o]);
|
||||
return n}),Rn=bt(function(t,e){return null==t?{}:J(t,e)});d.constant=ee,d.filter=function(t,e){return(Dn(t)?o:U)(t,yt(e,3))},d.flatten=Et,d.iteratee=re,d.keys=Yt,d.keysIn=Zt,d.map=function(t,e){return(Dn(t)?c:W)(t,yt(e,3))},d.memoize=Ut,d.merge=Nn,d.mergeWith=Vn,d.omit=Cn,d.pick=Rn,d.property=ue,d.pull=$n,d.pullAll=$t,d.remove=function(t,e){var n=[];if(!t||!t.length)return n;var r=-1,o=[],u=t.length;for(e=yt(e,3);++r<u;){var c=t[r];e(c,r,t)&&(n.push(c),o.push(r))}for(r=t?o.length:0,u=r-1;r--;)if(c=o[r],
|
||||
r==u||c!==i){var i=c;mt(c)?Xe.call(t,c,1):Z(t,c)}return n},d.toPlainObject=Jt,d.uniq=function(t){if(t&&t.length)t:{var e=-1,n=u,r=t.length,o=true,c=[],i=c;if(200<=r){if(n=zn(t)){t=_(n);break t}o=false,n=j,i=new O}else i=c;e:for(;++e<r;){var a=t[e],f=a,a=0!==a?a:0;if(o&&f===f){for(var l=i.length;l--;)if(i[l]===f)continue e;c.push(a)}else n(i,f,void 0)||(i!==c&&i.push(f),c.push(a))}t=c}else t=[];return t},d.values=te,d.cloneDeep=function(t){return $(t,5)},d.eq=Bt,d.find=Un,d.findIndex=Ft,d.get=Qt,d.hasIn=Xt,
|
||||
d.identity=ne,d.includes=function(t,e,n,r){return t=Dt(t)?t:te(t),n=n&&!r?Gt(n):0,r=t.length,0>n&&(n=un(r+n,0)),Tt(t)?n<=r&&-1<t.indexOf(e,n):!!r&&-1<s(t,e,n)},d.isArguments=Bn,d.isArray=Dn,d.isArrayLike=Dt,d.isArrayLikeObject=Pt,d.isBuffer=Pn,d.isFunction=Lt,d.isLength=Nt,d.isObject=Vt,d.isObjectLike=Ct,d.isPlainObject=Rt,d.isString=Tt,d.isSymbol=Wt,d.isTypedArray=Ln,d.last=Mt,d.stubArray=ce,d.stubFalse=ie,d.noop=oe,d.sortedIndexBy=function(t,e,n){n=yt(n,2),e=n(e);for(var r=0,o=null==t?0:t.length,u=e!==e,c=null===e,i=Wt(e),a=e===ae;r<o;){
|
||||
var f=en((r+o)/2),l=n(t[f]),s=l!==ae,b=null===l,h=l===l,p=Wt(l);(u?h:a?h&&s:c?h&&s&&!b:i?h&&s&&!b&&!p:b||p?0:l<e)?r=f+1:o=f}return cn(o,4294967294)},d.toFinite=qt,d.toInteger=Gt,d.toNumber=Ht,d.toString=Kt,d.VERSION="4.17.4",typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Ie._=d, define(function(){return d})):Ee?((Ee.exports=d)._=d,Fe._=d):Ie._=d}).call(this);
|
||||
Reference in New Issue
Block a user