4
Gemfile
4
Gemfile
@@ -9,16 +9,12 @@ gem "non-stupid-digest-assets"
|
||||
|
||||
gem 'uglifier'
|
||||
|
||||
gem 'activemodel-serializers-xml'
|
||||
|
||||
gem 'rack-cors', :require => 'rack/cors'
|
||||
|
||||
gem 'dotenv-rails', '~> 2.1.1'
|
||||
|
||||
gem 'bower-rails', '~> 0.10.0'
|
||||
|
||||
gem 'devise_token_auth', '~> 0.1.39'
|
||||
|
||||
# bundle exec rake doc:rails generates the API under doc/api.
|
||||
gem 'sdoc', '~> 0.4.0', group: :doc
|
||||
|
||||
|
||||
20
Gemfile.lock
20
Gemfile.lock
@@ -29,11 +29,6 @@ GEM
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
activemodel-serializers-xml (1.0.1)
|
||||
activemodel (> 5.x)
|
||||
activerecord (> 5.x)
|
||||
activesupport (> 5.x)
|
||||
builder (~> 3.1)
|
||||
activerecord (5.0.0.1)
|
||||
activemodel (= 5.0.0.1)
|
||||
activesupport (= 5.0.0.1)
|
||||
@@ -46,7 +41,6 @@ GEM
|
||||
airbrussh (1.1.1)
|
||||
sshkit (>= 1.6.1, != 1.7.0)
|
||||
arel (7.1.4)
|
||||
bcrypt (3.1.11)
|
||||
binding_of_caller (0.7.2)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bower-rails (0.10.0)
|
||||
@@ -78,15 +72,6 @@ GEM
|
||||
concurrent-ruby (1.0.2)
|
||||
connection_pool (2.2.1)
|
||||
debug_inspector (0.0.2)
|
||||
devise (4.2.0)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 4.1.0, < 5.1)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise_token_auth (0.1.39)
|
||||
devise (> 3.5.2, <= 4.2)
|
||||
rails (< 6)
|
||||
dotenv (2.1.1)
|
||||
dotenv-rails (2.1.1)
|
||||
dotenv (= 2.1.1)
|
||||
@@ -115,7 +100,6 @@ GEM
|
||||
mini_portile2 (~> 2.1.0)
|
||||
non-stupid-digest-assets (1.0.9)
|
||||
sprockets (>= 2.0)
|
||||
orm_adapter (0.5.0)
|
||||
puma (3.6.2)
|
||||
rack (2.0.1)
|
||||
rack-cors (0.4.0)
|
||||
@@ -178,8 +162,6 @@ GEM
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.3)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
warden (1.2.6)
|
||||
rack (>= 1.0)
|
||||
web-console (2.3.0)
|
||||
activemodel (>= 4.0)
|
||||
binding_of_caller (>= 0.7.2)
|
||||
@@ -193,7 +175,6 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activemodel-serializers-xml
|
||||
bower-rails (~> 0.10.0)
|
||||
byebug
|
||||
capistrano
|
||||
@@ -203,7 +184,6 @@ DEPENDENCIES
|
||||
capistrano-rails
|
||||
capistrano-rvm
|
||||
capistrano-sidekiq
|
||||
devise_token_auth (~> 0.1.39)
|
||||
dotenv-rails (~> 2.1.1)
|
||||
non-stupid-digest-assets
|
||||
puma
|
||||
|
||||
@@ -76,7 +76,6 @@ module.exports = function(grunt) {
|
||||
'app/assets/javascripts/app/frontend/controllers/*.js',
|
||||
'app/assets/javascripts/app/frontend/models/**/*.js',
|
||||
'app/assets/javascripts/app/services/**/*.js',
|
||||
// 'app/assets/javascripts/app/services/directives/*.js',
|
||||
],
|
||||
dest: 'vendor/assets/javascripts/app.js',
|
||||
},
|
||||
@@ -85,13 +84,9 @@ module.exports = function(grunt) {
|
||||
src: [
|
||||
'vendor/assets/bower_components/angular/angular.js',
|
||||
'vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js',
|
||||
'vendor/assets/bower_components/ng-token-auth/dist/ng-token-auth.js',
|
||||
'vendor/assets/bower_components/angular-cookie/angular-cookie.js',
|
||||
'vendor/assets/bower_components/lodash/dist/lodash.min.js',
|
||||
'vendor/assets/bower_components/restangular/dist/restangular.js',
|
||||
'vendor/assets/bower_components/marked/lib/marked.js',
|
||||
'vendor/assets/bower_components/oclazyload/dist/ocLazyLoad.js',
|
||||
'vendor/assets/bower_components/angular-lazy-img/release/angular-lazy-img.js',
|
||||
'vendor/assets/bower_components/ng-dialog/js/ngDialog.min.js',
|
||||
'vendor/assets/javascripts/crypto/*.js'
|
||||
],
|
||||
@@ -108,7 +103,6 @@ module.exports = function(grunt) {
|
||||
'vendor/assets/stylesheets/app.css',
|
||||
'vendor/assets/bower_components/ng-dialog/css/ngDialog.css',
|
||||
'vendor/assets/bower_components/ng-dialog/css/ngDialog-theme-default.css',
|
||||
'vendor/assets/bower_components/angular-typewrite/dist/angular-typewrite.css',
|
||||
],
|
||||
dest: 'vendor/assets/stylesheets/app.css'
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ if(window.crypto.subtle) {
|
||||
angular.module('app.frontend', [
|
||||
'ui.router',
|
||||
'restangular',
|
||||
'oc.lazyLoad',
|
||||
'angularLazyImg',
|
||||
'ngDialog'
|
||||
])
|
||||
|
||||
|
||||
@@ -63,12 +63,12 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, modelManager, markdownRenderer, $rootScope) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) {
|
||||
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.editorMode = 'edit';
|
||||
|
||||
if(note.content.text.length == 0 && note.dummy) {
|
||||
if(note.safeText().length == 0 && note.dummy) {
|
||||
this.focusTitle(100);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
this.hasAvailableExtensions = function() {
|
||||
return extensionManager.extensionsInContextOfItem(this.note).length > 0;
|
||||
}
|
||||
|
||||
this.onPreviewDoubleClick = function() {
|
||||
this.editorMode = 'edit';
|
||||
this.focusEditor(100);
|
||||
@@ -104,7 +108,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.renderedContent = function() {
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.content.text));
|
||||
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.safeText()));
|
||||
}
|
||||
|
||||
var statusTimeout;
|
||||
@@ -207,10 +211,10 @@ angular.module('app.frontend')
|
||||
|
||||
var original = this.note.presentation_name;
|
||||
this.note.presentation_name = this.url.token;
|
||||
modelManager.addDirtyItems([this.note]);
|
||||
this.note.setDirty(true);
|
||||
|
||||
apiController.sync(function(response){
|
||||
if(!response) {
|
||||
if(response && response.error) {
|
||||
this.note.presentation_name = original;
|
||||
this.url.token = original;
|
||||
alert("This URL is not available.");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("header", function(apiController){
|
||||
.directive("header", function(apiController, extensionManager){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@@ -19,7 +19,9 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('HeaderCtrl', function ($state, apiController, modelManager, serverSideValidation, $timeout) {
|
||||
.controller('HeaderCtrl', function ($state, apiController, modelManager, serverSideValidation, $timeout, extensionManager) {
|
||||
|
||||
this.extensionManager = extensionManager;
|
||||
|
||||
this.changePasswordPressed = function() {
|
||||
this.showNewPasswordForm = !this.showNewPasswordForm;
|
||||
@@ -30,6 +32,56 @@ angular.module('app.frontend')
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.showFaq = false;
|
||||
this.showNewPasswordForm = false;
|
||||
this.showExtensionsMenu = false;
|
||||
}
|
||||
|
||||
this.toggleExtensions = function() {
|
||||
this.showAccountMenu = false;
|
||||
this.showExtensionsMenu = !this.showExtensionsMenu;
|
||||
}
|
||||
|
||||
this.toggleExtensionForm = function() {
|
||||
this.newExtensionData = {};
|
||||
this.showNewExtensionForm = !this.showNewExtensionForm;
|
||||
}
|
||||
|
||||
this.submitNewExtensionForm = function() {
|
||||
if(this.newExtensionData.url) {
|
||||
extensionManager.addExtension(this.newExtensionData.url, function(response){
|
||||
if(!response) {
|
||||
alert("Unable to register this extension. Make sure the link is valid and try again.");
|
||||
} else {
|
||||
this.newExtensionData.url = "";
|
||||
this.showNewExtensionForm = false;
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedAction = function(action, extension) {
|
||||
action.running = true;
|
||||
extensionManager.executeAction(action, extension, null, function(response){
|
||||
action.running = false;
|
||||
if(response && response.error) {
|
||||
action.error = true;
|
||||
alert("There was an error performing this action. Please try again.");
|
||||
} else {
|
||||
action.error = false;
|
||||
apiController.sync(null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.deleteExtension = function(extension) {
|
||||
if(confirm("Are you sure you want to delete this extension?")) {
|
||||
extensionManager.deleteExtension(extension);
|
||||
}
|
||||
}
|
||||
|
||||
this.reloadExtensionsPressed = function() {
|
||||
if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) {
|
||||
extensionManager.refreshExtensionsFromServer();
|
||||
}
|
||||
}
|
||||
|
||||
this.changeServer = function() {
|
||||
@@ -77,7 +129,7 @@ angular.module('app.frontend')
|
||||
$timeout(function(){
|
||||
this.isRefreshing = false;
|
||||
}.bind(this), 200)
|
||||
if(!response) {
|
||||
if(response && response.error) {
|
||||
alert("There was an error syncing. Please try again. If all else fails, log out and log back in.");
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
@@ -120,18 +172,6 @@ angular.module('app.frontend')
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
this.forgotPasswordSubmit = function() {
|
||||
// $auth.requestPasswordReset(this.resetData)
|
||||
// .then(function(resp) {
|
||||
// this.resetData.response = "Success";
|
||||
// // handle success response
|
||||
// }.bind(this))
|
||||
// .catch(function(resp) {
|
||||
// // handle error response
|
||||
// this.resetData.response = "Error";
|
||||
// }.bind(this));
|
||||
}
|
||||
|
||||
this.encryptionStatusForNotes = function() {
|
||||
var allNotes = modelManager.filteredNotes;
|
||||
var countEncrypted = 0;
|
||||
@@ -144,10 +184,12 @@ angular.module('app.frontend')
|
||||
return countEncrypted + "/" + allNotes.length + " notes encrypted";
|
||||
}
|
||||
|
||||
this.archiveEncryptionFormat = {encrypted: true};
|
||||
|
||||
this.downloadDataArchive = function() {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', 'notes.json');
|
||||
link.href = apiController.itemsDataFile();
|
||||
link.href = apiController.itemsDataFile(this.archiveEncryptionFormat.encrypted);
|
||||
link.click();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ angular.module('app.frontend')
|
||||
var onUserSet = function() {
|
||||
apiController.setUser($scope.defaultUser);
|
||||
$scope.allTag = new Tag({all: true});
|
||||
$scope.allTag.content.title = "All";
|
||||
$scope.allTag.title = "All";
|
||||
$scope.tags = modelManager.tags;
|
||||
$scope.allTag.notes = modelManager.notes;
|
||||
|
||||
@@ -14,8 +14,6 @@ angular.module('app.frontend')
|
||||
setInterval(function () {
|
||||
apiController.sync(null);
|
||||
}, 30000);
|
||||
|
||||
// apiController.verifyEncryptionStatusOfAllItems($scope.defaultUser, function(success){});
|
||||
}
|
||||
|
||||
apiController.getCurrentUser(function(user){
|
||||
@@ -45,14 +43,18 @@ angular.module('app.frontend')
|
||||
|
||||
$scope.tagsSelectionMade = function(tag) {
|
||||
$scope.selectedTag = tag;
|
||||
|
||||
if($scope.selectedNote && $scope.selectedNote.dummy) {
|
||||
modelManager.removeItemLocally($scope.selectedNote);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.tagsAddNew = function(tag) {
|
||||
modelManager.addTag(tag);
|
||||
modelManager.addItem(tag);
|
||||
}
|
||||
|
||||
$scope.tagsSave = function(tag, callback) {
|
||||
modelManager.addDirtyItems([tag]);
|
||||
tag.setDirty(true);
|
||||
apiController.sync(callback);
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ angular.module('app.frontend')
|
||||
|
||||
var originalNote = _.find(modelManager.notes, {uuid: noteCopy.uuid});
|
||||
if(!newTag.all) {
|
||||
modelManager.addTagToNote(newTag, originalNote);
|
||||
modelManager.createRelationshipBetweenItems(newTag, originalNote);
|
||||
}
|
||||
|
||||
apiController.sync(function(){});
|
||||
@@ -77,9 +79,9 @@ angular.module('app.frontend')
|
||||
$scope.notesRemoveTag = function(tag) {
|
||||
var validNotes = Note.filterDummyNotes(tag.notes);
|
||||
if(validNotes == 0) {
|
||||
modelManager.deleteTag(tag);
|
||||
modelManager.setItemToBeDeleted(tag);
|
||||
// if no more notes, delete tag
|
||||
apiController.deleteItem(tag, function(){
|
||||
apiController.sync(function(){
|
||||
// force scope tags to update on sub directives
|
||||
$scope.tags = [];
|
||||
$timeout(function(){
|
||||
@@ -96,10 +98,10 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
$scope.notesAddNew = function(note) {
|
||||
modelManager.addNote(note);
|
||||
modelManager.addItem(note);
|
||||
|
||||
if(!$scope.selectedTag.all) {
|
||||
modelManager.addTagToNote($scope.selectedTag, note);
|
||||
modelManager.createRelationshipBetweenItems($scope.selectedTag, note);
|
||||
$scope.updateAllTag();
|
||||
}
|
||||
}
|
||||
@@ -109,30 +111,35 @@ angular.module('app.frontend')
|
||||
*/
|
||||
|
||||
$scope.saveNote = function(note, callback) {
|
||||
modelManager.addDirtyItems(note);
|
||||
note.setDirty(true);
|
||||
|
||||
apiController.sync(function(){
|
||||
note.hasChanges = false;
|
||||
|
||||
if(callback) {
|
||||
callback(true);
|
||||
apiController.sync(function(response){
|
||||
if(response && response.error) {
|
||||
alert("There was an error saving your note. Please try again.");
|
||||
callback(false);
|
||||
} else {
|
||||
note.hasChanges = false;
|
||||
if(callback) {
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteNote = function(note) {
|
||||
|
||||
modelManager.deleteNote(note);
|
||||
modelManager.setItemToBeDeleted(note);
|
||||
|
||||
if(note == $scope.selectedNote) {
|
||||
$scope.selectedNote = null;
|
||||
}
|
||||
|
||||
if(note.dummy) {
|
||||
modelManager.removeItemLocally(note);
|
||||
return;
|
||||
}
|
||||
|
||||
apiController.deleteItem(note, function(success){})
|
||||
apiController.sync(null);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -25,7 +25,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('NotesCtrl', function (apiController, $timeout, $rootScope) {
|
||||
.controller('NotesCtrl', function (apiController, $timeout, $rootScope, modelManager) {
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
@@ -110,8 +110,8 @@ angular.module('app.frontend')
|
||||
|
||||
this.createNewNote = function() {
|
||||
var title = "New Note" + (this.tag.notes ? (" " + (this.tag.notes.length + 1)) : "");
|
||||
this.newNote = new Note({dummy: true});
|
||||
this.newNote.content.title = title;
|
||||
this.newNote = modelManager.createItem({content_type: "Note", dummy: true, text: ""});
|
||||
this.newNote.title = title;
|
||||
this.selectNote(this.newNote);
|
||||
this.addNew()(this.newNote);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ angular.module('app.frontend')
|
||||
if(this.noteFilter.text.length == 0) {
|
||||
note.visible = true;
|
||||
} else {
|
||||
note.visible = note.content.title.toLowerCase().includes(this.noteFilter.text) || note.content.text.toLowerCase().includes(this.noteFilter.text);
|
||||
note.visible = note.title.toLowerCase().includes(this.noteFilter.text) || note.text.toLowerCase().includes(this.noteFilter.text);
|
||||
}
|
||||
return note.visible;
|
||||
}.bind(this)
|
||||
|
||||
@@ -33,7 +33,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('TagsCtrl', function () {
|
||||
.controller('TagsCtrl', function (modelManager) {
|
||||
|
||||
var initialLoad = true;
|
||||
|
||||
@@ -62,8 +62,8 @@ angular.module('app.frontend')
|
||||
if(this.editingTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.newTag = new Tag();
|
||||
|
||||
this.newTag = modelManager.createItem({content_type: "Tag"});
|
||||
this.selectedTag = this.newTag;
|
||||
this.editingTag = this.newTag;
|
||||
this.addNew()(this.newTag);
|
||||
@@ -71,7 +71,7 @@ angular.module('app.frontend')
|
||||
|
||||
var originalTagName = "";
|
||||
this.onTagTitleFocus = function(tag) {
|
||||
originalTagName = tag.content.title;
|
||||
originalTagName = tag.title;
|
||||
}
|
||||
|
||||
this.tagTitleDidChange = function(tag) {
|
||||
@@ -80,14 +80,14 @@ angular.module('app.frontend')
|
||||
|
||||
this.saveTag = function($event, tag) {
|
||||
this.editingTag = null;
|
||||
if(tag.content.title.length == 0) {
|
||||
tag.content.title = originalTagName;
|
||||
if(tag.title.length == 0) {
|
||||
tag.title = originalTagName;
|
||||
originalTagName = "";
|
||||
return;
|
||||
}
|
||||
|
||||
$event.target.blur();
|
||||
if(!tag.content.title || tag.content.title.length == 0) {
|
||||
if(!tag.title || tag.title.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,14 @@
|
||||
class Item {
|
||||
|
||||
constructor(json_obj) {
|
||||
|
||||
var content;
|
||||
this.updateFromJSON(json_obj);
|
||||
|
||||
Object.defineProperty(this, "content", {
|
||||
get: function() {
|
||||
return content;
|
||||
},
|
||||
set: function(value) {
|
||||
var finalValue = value;
|
||||
|
||||
if(typeof value === 'string') {
|
||||
try {
|
||||
var decodedValue = JSON.parse(value);
|
||||
finalValue = decodedValue;
|
||||
}
|
||||
catch(e) {
|
||||
finalValue = value;
|
||||
}
|
||||
}
|
||||
content = finalValue;
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
_.merge(this, json_obj);
|
||||
|
||||
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();
|
||||
}
|
||||
this.observers = [];
|
||||
|
||||
if(!this.uuid) {
|
||||
this.uuid = Neeto.crypto.generateUUID();
|
||||
}
|
||||
|
||||
this.setContentRaw = function(rawContent) {
|
||||
content = rawContent;
|
||||
}
|
||||
|
||||
if(!this.content) {
|
||||
this.content = {};
|
||||
}
|
||||
|
||||
if(!this.content.references) {
|
||||
this.content.references = [];
|
||||
}
|
||||
}
|
||||
|
||||
static sortItemsByDate(items) {
|
||||
@@ -57,31 +17,90 @@ class Item {
|
||||
});
|
||||
}
|
||||
|
||||
addReference(reference) {
|
||||
this.content.references.push(reference);
|
||||
this.content.references = _.uniq(this.content.references);
|
||||
this.updateReferencesLocalMapping();
|
||||
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;
|
||||
}
|
||||
|
||||
return JSON.parse(this.content);
|
||||
}
|
||||
|
||||
removeReference(reference) {
|
||||
_.remove(this.content.references, _.find(this.content.references, {uuid: reference.uuid}));
|
||||
this.updateReferencesLocalMapping();
|
||||
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();
|
||||
}
|
||||
|
||||
if(json.content) {
|
||||
this.mapContentToLocalProperties(this.contentObject);
|
||||
}
|
||||
}
|
||||
|
||||
referencesMatchingContentType(contentType) {
|
||||
return this.content.references.filter(function(reference){
|
||||
return reference.content_type == contentType;
|
||||
});
|
||||
setDirty(dirty) {
|
||||
this.dirty = dirty;
|
||||
|
||||
if(dirty) {
|
||||
this.notifyObserversOfChange();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
}
|
||||
|
||||
createContentJSONFromProperties() {
|
||||
return this.structureParams();
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
// must override
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
return {references: this.referenceParams()}
|
||||
}
|
||||
|
||||
addItemAsRelationship(item) {
|
||||
// must override
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
// must override
|
||||
}
|
||||
|
||||
removeAllRelationships() {
|
||||
// must override
|
||||
}
|
||||
|
||||
mergeMetadataFromItem(item) {
|
||||
_.merge(this, _.omit(item, ["content"]));
|
||||
}
|
||||
|
||||
updateReferencesLocalMapping() {
|
||||
// should be overriden to manage local properties
|
||||
}
|
||||
|
||||
referencesAffectedBySharingChange() {
|
||||
// should be overriden to determine which references should be decrypted/encrypted
|
||||
return null;
|
||||
@@ -92,7 +111,7 @@ class Item {
|
||||
}
|
||||
|
||||
isEncrypted() {
|
||||
return this.encryptionEnabled() && typeof this.content === 'string' ? true : false;
|
||||
return this.encryptionEnabled() && this.content.substring(0, 3) === '001' ? true : false;
|
||||
}
|
||||
|
||||
encryptionEnabled() {
|
||||
|
||||
102
app/assets/javascripts/app/frontend/models/app/extension.js
Normal file
102
app/assets/javascripts/app/frontend/models/app/extension.js
Normal file
@@ -0,0 +1,102 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
get permissionsString() {
|
||||
if(!this.permissions) {
|
||||
return "";
|
||||
}
|
||||
var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter
|
||||
permission += ": ";
|
||||
for(var contentType of this.content_types) {
|
||||
if(contentType == "*") {
|
||||
permission += "All items";
|
||||
} else {
|
||||
permission += contentType;
|
||||
}
|
||||
|
||||
permission += " ";
|
||||
}
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
get encryptionModeString() {
|
||||
if(this.verb != "post") {
|
||||
return null;
|
||||
}
|
||||
var encryptionMode = "This action accepts data ";
|
||||
if(this.accepts_encrypted && this.accepts_decrypted) {
|
||||
encryptionMode += "encrypted or decrypted.";
|
||||
} else {
|
||||
if(this.accepts_encrypted) {
|
||||
encryptionMode += "encrypted.";
|
||||
} else {
|
||||
encryptionMode += "decrypted.";
|
||||
}
|
||||
}
|
||||
return encryptionMode;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Extension extends Item {
|
||||
constructor(json) {
|
||||
super(json);
|
||||
_.merge(this, json);
|
||||
|
||||
this.encrypted = true;
|
||||
this.content_type = "Extension";
|
||||
}
|
||||
|
||||
actionsInGlobalContext() {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == "global";
|
||||
})
|
||||
}
|
||||
|
||||
actionsWithContextForItem(item) {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == item.content_type || action.context == "Item";
|
||||
})
|
||||
}
|
||||
|
||||
mapContentToLocalProperties(contentObject) {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.name = contentObject.name;
|
||||
this.url = contentObject.url;
|
||||
this.actions = contentObject.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
|
||||
updateFromExternalResponseItem(externalResponseItem) {
|
||||
_.merge(this, externalResponseItem);
|
||||
this.actions = externalResponseItem.actions.map(function(action){
|
||||
return new Action(action);
|
||||
})
|
||||
}
|
||||
|
||||
referenceParams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
structureParams() {
|
||||
var params = {
|
||||
name: this.name,
|
||||
url: this.url,
|
||||
actions: this.actions
|
||||
};
|
||||
|
||||
_.merge(params, super.structureParams());
|
||||
return params;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,14 +6,54 @@ class Note extends Item {
|
||||
if(!this.tags) {
|
||||
this.tags = [];
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.content.title) {
|
||||
this.content.title = "";
|
||||
}
|
||||
mapContentToLocalProperties(contentObject) {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.title = contentObject.title;
|
||||
this.text = contentObject.text;
|
||||
}
|
||||
|
||||
if(!this.content.text) {
|
||||
this.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) {
|
||||
if(item.content_type == "Tag") {
|
||||
if(!_.find(this.tags, item)) {
|
||||
this.tags.push(item);
|
||||
}
|
||||
}
|
||||
super.addItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeItemAsRelationship(item) {
|
||||
if(item.content_type == "Tag") {
|
||||
_.pull(this.tags, item);
|
||||
}
|
||||
super.removeItemAsRelationship(item);
|
||||
}
|
||||
|
||||
removeAllRelationships() {
|
||||
this.tags.forEach(function(tag){
|
||||
_.pull(tag.notes, this);
|
||||
tag.setDirty(true);
|
||||
}.bind(this))
|
||||
this.tags = [];
|
||||
}
|
||||
|
||||
static filterDummyNotes(notes) {
|
||||
@@ -21,11 +61,6 @@ class Note extends Item {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
updateReferencesLocalMapping() {
|
||||
super.updateReferencesLocalMapping();
|
||||
this.tags = this.referencesMatchingContentType("Tag");
|
||||
}
|
||||
|
||||
referencesAffectedBySharingChange() {
|
||||
return super.referencesAffectedBySharingChange();
|
||||
}
|
||||
@@ -39,6 +74,14 @@ class Note extends Item {
|
||||
return false;
|
||||
}
|
||||
|
||||
safeText() {
|
||||
return this.text || "";
|
||||
}
|
||||
|
||||
safeTitle() {
|
||||
return this.title || "";
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {uuid: this.uuid}
|
||||
}
|
||||
|
||||
@@ -6,22 +6,60 @@ class Tag extends Item {
|
||||
if(!this.notes) {
|
||||
this.notes = [];
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.content.title) {
|
||||
this.content.title = "";
|
||||
mapContentToLocalProperties(contentObject) {
|
||||
super.mapContentToLocalProperties(contentObject)
|
||||
this.title = contentObject.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);
|
||||
}
|
||||
|
||||
removeAllRelationships() {
|
||||
this.notes.forEach(function(note){
|
||||
_.pull(note.tags, this);
|
||||
note.setDirty(true);
|
||||
}.bind(this))
|
||||
|
||||
this.notes = [];
|
||||
}
|
||||
|
||||
get content_type() {
|
||||
return "Tag";
|
||||
}
|
||||
|
||||
updateReferencesLocalMapping() {
|
||||
super.updateReferencesLocalMapping();
|
||||
this.notes = this.referencesMatchingContentType("Note");
|
||||
}
|
||||
|
||||
referencesAffectedBySharingChange() {
|
||||
return this.referencesMatchingContentType("Note");
|
||||
return this.notes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ angular.module('app.frontend')
|
||||
Auth
|
||||
*/
|
||||
|
||||
this.isUserSignedIn = function() {
|
||||
return this.user.email && this.retrieveMk();
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(email, callback) {
|
||||
var request = Restangular.one("auth", "params");
|
||||
request.get({email: email}).then(function(response){
|
||||
@@ -218,19 +222,23 @@ angular.module('app.frontend')
|
||||
|
||||
this.syncWithOptions = function(callback, options = {}) {
|
||||
if(!this.user.uuid) {
|
||||
this.writeItemsToLocalStorage();
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
this.writeItemsToLocalStorage(function(responseItems){
|
||||
modelManager.clearDirtyItems();
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
}.bind(this))
|
||||
return;
|
||||
}
|
||||
|
||||
var dirtyItems = modelManager.dirtyItems;
|
||||
var dirtyItems = modelManager.getDirtyItems();
|
||||
var request = Restangular.one("items/sync");
|
||||
request.items = _.map(dirtyItems, function(item){
|
||||
return this.createRequestParamsForItem(item, options.additionalFields);
|
||||
}.bind(this));
|
||||
|
||||
// console.log("syncing items", request.items);
|
||||
|
||||
if(this.syncToken) {
|
||||
request.sync_token = this.syncToken;
|
||||
}
|
||||
@@ -251,7 +259,7 @@ angular.module('app.frontend')
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
console.log("Sync error: ", response);
|
||||
callback(null);
|
||||
callback({error: "Sync error"});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -268,14 +276,21 @@ angular.module('app.frontend')
|
||||
return this.paramsForItem(item, !item.isPublic(), additionalFields, false);
|
||||
}
|
||||
|
||||
this.paramsForExportFile = function(item, encrypted) {
|
||||
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
|
||||
}
|
||||
|
||||
this.paramsForExtension = function(item, encrypted) {
|
||||
return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]);
|
||||
}
|
||||
|
||||
this.paramsForItem = function(item, encrypted, additionalFields, forExportFile) {
|
||||
var itemCopy = _.cloneDeep(item);
|
||||
|
||||
var params = {uuid: item.uuid, content_type: item.content_type, presentation_name: item.presentation_name, deleted: item.deleted};
|
||||
console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy)
|
||||
|
||||
itemCopy.content.references = _.map(itemCopy.content.references, function(reference){
|
||||
return {uuid: reference.uuid, content_type: reference.content_type};
|
||||
})
|
||||
var params = {uuid: item.uuid, content_type: item.content_type,
|
||||
presentation_name: item.presentation_name, deleted: item.deleted};
|
||||
|
||||
if(encrypted) {
|
||||
this.encryptSingleItem(itemCopy, this.retrieveMk());
|
||||
@@ -284,7 +299,7 @@ angular.module('app.frontend')
|
||||
params.auth_hash = itemCopy.auth_hash;
|
||||
}
|
||||
else {
|
||||
params.content = forExportFile ? itemCopy.content : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.content));
|
||||
params.content = forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties()));
|
||||
if(!forExportFile) {
|
||||
params.enc_item_key = null;
|
||||
params.auth_hash = null;
|
||||
@@ -298,13 +313,6 @@ angular.module('app.frontend')
|
||||
return params;
|
||||
}
|
||||
|
||||
|
||||
this.deleteItem = function(item, callback) {
|
||||
item.deleted = true;
|
||||
modelManager.addDirtyItems([item]);
|
||||
this.sync(callback);
|
||||
}
|
||||
|
||||
this.shareItem = function(item, callback) {
|
||||
if(!this.user.uuid) {
|
||||
alert("You must be signed in to share.");
|
||||
@@ -314,7 +322,9 @@ angular.module('app.frontend')
|
||||
var shareFn = function() {
|
||||
item.presentation_name = "_auto_";
|
||||
var needsUpdate = [item].concat(item.referencesAffectedBySharingChange() || []);
|
||||
modelManager.addDirtyItems(needsUpdate);
|
||||
needsUpdate.forEach(function(needingUpdate){
|
||||
needingUpdate.setDirty(true);
|
||||
})
|
||||
this.sync();
|
||||
}.bind(this)
|
||||
|
||||
@@ -339,7 +349,9 @@ angular.module('app.frontend')
|
||||
this.unshareItem = function(item, callback) {
|
||||
item.presentation_name = null;
|
||||
var needsUpdate = [item].concat(item.referencesAffectedBySharingChange() || []);
|
||||
modelManager.addDirtyItems(needsUpdate);
|
||||
needsUpdate.forEach(function(needingUpdate){
|
||||
needingUpdate.setDirty(true);
|
||||
})
|
||||
this.sync(null);
|
||||
}
|
||||
|
||||
@@ -349,8 +361,12 @@ angular.module('app.frontend')
|
||||
|
||||
this.importJSONData = function(jsonString, callback) {
|
||||
var data = JSON.parse(jsonString);
|
||||
console.log("importing data", data);
|
||||
this.decryptItems(data.items);
|
||||
modelManager.mapResponseItemsToLocalModels(data.items);
|
||||
modelManager.addDirtyItems(modelManager.items);
|
||||
modelManager.allItems.forEach(function(item){
|
||||
item.setDirty(true);
|
||||
})
|
||||
this.syncWithOptions(callback, {additionalFields: ["created_at", "updated_at"]});
|
||||
}
|
||||
|
||||
@@ -358,7 +374,7 @@ angular.module('app.frontend')
|
||||
Export
|
||||
*/
|
||||
|
||||
this.itemsDataFile = function() {
|
||||
this.itemsDataFile = function(encrypted) {
|
||||
var textFile = null;
|
||||
var makeTextFile = function (text) {
|
||||
var data = new Blob([text], {type: 'text/json'});
|
||||
@@ -375,8 +391,8 @@ angular.module('app.frontend')
|
||||
return textFile;
|
||||
}.bind(this);
|
||||
|
||||
var items = _.map(modelManager.items, function(item){
|
||||
return _.omit(this.paramsForItem(item, false, ["created_at", "updated_at"], true), ["deleted"]);
|
||||
var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){
|
||||
return this.paramsForExportFile(item, encrypted);
|
||||
}.bind(this));
|
||||
|
||||
var data = {
|
||||
@@ -398,7 +414,7 @@ angular.module('app.frontend')
|
||||
request.items.forEach(function(item){
|
||||
if(item.tag_id) {
|
||||
var tag = tags.filter(function(tag){return tag.uuid == item.tag_id})[0];
|
||||
item.tag_name = tag.content.title;
|
||||
item.tag_name = tag.title;
|
||||
}
|
||||
})
|
||||
request.post().then(function(response){
|
||||
@@ -416,11 +432,13 @@ angular.module('app.frontend')
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
this.writeItemsToLocalStorage = function() {
|
||||
var items = _.map(modelManager.items, function(item){
|
||||
return this.paramsForItem(item, false, ["created_at", "updated_at"], true)
|
||||
this.writeItemsToLocalStorage = function(callback) {
|
||||
var items = _.map(modelManager.allItems, function(item){
|
||||
return this.paramsForItem(item, false, ["created_at", "updated_at"], false)
|
||||
}.bind(this));
|
||||
console.log("Writing items to local", items);
|
||||
this.writeToLocalStorage('items', items);
|
||||
callback(items);
|
||||
}
|
||||
|
||||
this.writeToLocalStorage = function(key, value) {
|
||||
@@ -430,7 +448,7 @@ angular.module('app.frontend')
|
||||
this.loadLocalItemsAndUser = function() {
|
||||
var user = {};
|
||||
var items = JSON.parse(localStorage.getItem('items')) || [];
|
||||
items = modelManager.mapResponseItemsToLocalModels(items);
|
||||
items = this.handleItemsResponse(items, null);
|
||||
Item.sortItemsByDate(items);
|
||||
user.items = items;
|
||||
user.shouldMerge = true;
|
||||
@@ -454,7 +472,8 @@ angular.module('app.frontend')
|
||||
if(!draftString || draftString == 'undefined') {
|
||||
return null;
|
||||
}
|
||||
return new Note(JSON.parse(draftString));
|
||||
var jsonObj = _.merge({content_type: "Note"}, JSON.parse(draftString));
|
||||
return modelManager.createItem(jsonObj);
|
||||
}
|
||||
|
||||
|
||||
@@ -489,7 +508,7 @@ angular.module('app.frontend')
|
||||
|
||||
var ek = Neeto.crypto.firstHalfOfKey(item_key);
|
||||
var ak = Neeto.crypto.secondHalfOfKey(item_key);
|
||||
var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.content), ek);
|
||||
var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek);
|
||||
var authHash = Neeto.crypto.hmac256(encryptedContent, ak);
|
||||
|
||||
item.content = encryptedContent;
|
||||
@@ -518,13 +537,15 @@ angular.module('app.frontend')
|
||||
if(item.deleted == true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
||||
// is encrypted
|
||||
this.decryptSingleItem(item, masterKey);
|
||||
} else {
|
||||
// is base64 encoded
|
||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||
var isString = typeof item.content === 'string' || item.content instanceof String;
|
||||
if(isString) {
|
||||
if(item.content.substring(0, 3) == "001" && item.enc_item_key) {
|
||||
// is encrypted
|
||||
this.decryptSingleItem(item, masterKey);
|
||||
} else {
|
||||
// is base64 encoded
|
||||
item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
class ContextualExtensionsMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/contextual-menu.html";
|
||||
this.scope = {
|
||||
item: "="
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, extensionManager) {
|
||||
$scope.extensions = extensionManager.extensionsInContextOfItem($scope.item);
|
||||
|
||||
$scope.executeAction = function(action, extension) {
|
||||
action.running = true;
|
||||
extensionManager.executeAction(action, extension, $scope.item, function(response){
|
||||
action.running = false;
|
||||
})
|
||||
}
|
||||
|
||||
$scope.accessTypeForExtension = function(extension) {
|
||||
return extensionManager.extensionUsesEncryptedData(extension) ? "encrypted" : "decrypted";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('contextualExtensionsMenu', () => new ContextualExtensionsMenu);
|
||||
285
app/assets/javascripts/app/services/extensionManager.js
Normal file
285
app/assets/javascripts/app/services/extensionManager.js
Normal file
@@ -0,0 +1,285 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(Restangular, modelManager, apiController) {
|
||||
this.Restangular = Restangular;
|
||||
this.modelManager = modelManager;
|
||||
this.apiController = apiController;
|
||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
||||
|
||||
modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){
|
||||
for (var ext of items) {
|
||||
|
||||
ext.encrypted = this.extensionUsesEncryptedData(ext);
|
||||
|
||||
for (var action of ext.actions) {
|
||||
if(this.enabledRepeatActionUrls.includes(action.url)) {
|
||||
this.enableRepeatAction(action, ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
return this.modelManager.extensions;
|
||||
}
|
||||
|
||||
extensionsInContextOfItem(item) {
|
||||
return this.extensions.filter(function(ext){
|
||||
return ext.actionsWithContextForItem(item).length > 0;
|
||||
})
|
||||
}
|
||||
|
||||
actionWithURL(url) {
|
||||
for (var extension of this.extensions) {
|
||||
return _.find(extension.actions, {url: url})
|
||||
}
|
||||
}
|
||||
|
||||
extensionUsesEncryptedData(extension) {
|
||||
return !this.decryptedExtensions.includes(extension.url);
|
||||
}
|
||||
|
||||
changeExtensionEncryptionFormat(encrypted, extension) {
|
||||
if(encrypted) {
|
||||
_.pull(this.decryptedExtensions, extension.url);
|
||||
} else {
|
||||
this.decryptedExtensions.push(extension.url);
|
||||
}
|
||||
|
||||
localStorage.setItem("decryptedExtensions", JSON.stringify(this.decryptedExtensions))
|
||||
|
||||
extension.encrypted = this.extensionUsesEncryptedData(extension);
|
||||
|
||||
console.log("ext with dec", this.decryptedExtensions);
|
||||
}
|
||||
|
||||
addExtension(url, callback) {
|
||||
this.retrieveExtensionFromServer(url, callback);
|
||||
}
|
||||
|
||||
deleteExtension(extension) {
|
||||
for(var action of extension.actions) {
|
||||
_.pull(this.decryptedExtensions, extension);
|
||||
if(action.repeat_mode) {
|
||||
if(this.isRepeatActionEnabled(action)) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.modelManager.setItemToBeDeleted(extension);
|
||||
this.apiController.sync(null);
|
||||
}
|
||||
|
||||
retrieveExtensionFromServer(url, callback) {
|
||||
console.log("Registering URL", url);
|
||||
this.Restangular.oneUrl(url, url).get().then(function(response){
|
||||
var ext = this.handleExtensionLoadExternalResponseItem(url, response.plain());
|
||||
if(callback) {
|
||||
callback(ext);
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
console.log("Error registering extension", response);
|
||||
callback(null);
|
||||
})
|
||||
}
|
||||
|
||||
handleExtensionLoadExternalResponseItem(url, externalResponseItem) {
|
||||
var extension = _.find(this.extensions, {url: url});
|
||||
if(extension) {
|
||||
extension.updateFromExternalResponseItem(externalResponseItem);
|
||||
} else {
|
||||
extension = new Extension(externalResponseItem);
|
||||
extension.url = url;
|
||||
extension.setDirty(true);
|
||||
this.modelManager.addItem(extension);
|
||||
this.apiController.sync(null);
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
refreshExtensionsFromServer() {
|
||||
for (var url of this.enabledRepeatActionUrls) {
|
||||
var action = this.actionWithURL(url);
|
||||
if(action) {
|
||||
this.disableRepeatAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
for(var ext of this.extensions) {
|
||||
this.retrieveExtensionFromServer(ext.url, function(extension){
|
||||
extension.setDirty(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
|
||||
if(this.extensionUsesEncryptedData(extension) && !this.apiController.isUserSignedIn()) {
|
||||
alert("To send data encrypted, you must have an encryption key, and must therefore be signed in.");
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
this.Restangular.oneUrl(action.url, action.url).get().then(function(response){
|
||||
console.log("Execute action response", response);
|
||||
action.error = false;
|
||||
var items = response.items;
|
||||
this.modelManager.mapResponseItemsToLocalModels(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
action.error = true;
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "show": {
|
||||
var win = window.open(action.url, '_blank');
|
||||
win.focus();
|
||||
callback();
|
||||
break;
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.item = this.outgoingParamsForItem(item, extension);
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
callback(response);
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
isRepeatActionEnabled(action) {
|
||||
return this.enabledRepeatActionUrls.includes(action.url);
|
||||
}
|
||||
|
||||
disableRepeatAction(action, extension) {
|
||||
console.log("Disabling action", action);
|
||||
|
||||
_.pull(this.enabledRepeatActionUrls, action.url);
|
||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
this.modelManager.removeItemChangeObserver(action.url);
|
||||
|
||||
console.assert(this.isRepeatActionEnabled(action) == false);
|
||||
}
|
||||
|
||||
enableRepeatAction(action, extension) {
|
||||
// console.log("Enabling repeat action", action);
|
||||
|
||||
if(!_.find(this.enabledRepeatActionUrls, action.url)) {
|
||||
this.enabledRepeatActionUrls.push(action.url);
|
||||
localStorage.setItem("enabledRepeatActionUrls", JSON.stringify(this.enabledRepeatActionUrls));
|
||||
}
|
||||
|
||||
if(action.repeat_mode) {
|
||||
|
||||
if(action.repeat_mode == "watch") {
|
||||
this.modelManager.addItemChangeObserver(action.url, action.content_types, function(changedItems){
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
if(action.repeat_mode == "loop") {
|
||||
// todo
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
queueAction(action, extension, delay, changedItems) {
|
||||
this.actionQueue = this.actionQueue || [];
|
||||
if(_.find(this.actionQueue, {url: action.url})) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Successfully queued", action, this.actionQueue.length);
|
||||
this.actionQueue.push(action);
|
||||
|
||||
setTimeout(function () {
|
||||
console.log("Performing queued action", action);
|
||||
this.triggerWatchAction(action, extension, changedItems);
|
||||
_.pull(this.actionQueue, action);
|
||||
}.bind(this), delay * 1000);
|
||||
}
|
||||
|
||||
triggerWatchAction(action, extension, changedItems) {
|
||||
if(action.repeat_timeout > 0) {
|
||||
var lastExecuted = action.lastExecuted;
|
||||
var diffInSeconds = (new Date() - lastExecuted)/1000;
|
||||
if(diffInSeconds < action.repeat_timeout) {
|
||||
var delay = action.repeat_timeout - diffInSeconds;
|
||||
this.queueAction(action, extension, delay, changedItems);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
action.lastExecuted = new Date();
|
||||
|
||||
console.log("Performing action immediately", action);
|
||||
|
||||
if(action.verb == "post") {
|
||||
var params = {};
|
||||
params.items = changedItems.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension);
|
||||
return params;
|
||||
}.bind(this))
|
||||
this.performPost(action, extension, params, null);
|
||||
} else {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension) {
|
||||
return this.apiController.paramsForExtension(item, this.extensionUsesEncryptedData(extension));
|
||||
}
|
||||
|
||||
performPost(action, extension, params, callback) {
|
||||
var request = this.Restangular.oneUrl(action.url, action.url);
|
||||
_.merge(request, params);
|
||||
|
||||
request.post().then(function(response){
|
||||
action.error = false;
|
||||
if(callback) {
|
||||
callback(response.plain());
|
||||
}
|
||||
})
|
||||
.catch(function(response){
|
||||
action.error = true;
|
||||
console.log("Action error response:", response);
|
||||
if(callback) {
|
||||
callback({error: "Request error"});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('extensionManager', ExtensionManager);
|
||||
@@ -1,112 +0,0 @@
|
||||
class ItemManager {
|
||||
|
||||
constructor() {
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
findItem(itemId) {
|
||||
return _.find(this.items, {uuid: itemId});
|
||||
}
|
||||
|
||||
addItems(items) {
|
||||
this._items = _.uniq(this.items.concat(items));
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModels(items) {
|
||||
return this.mapResponseItemsToLocalModelsOmittingFields(items, null)
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
|
||||
var models = []
|
||||
for (var json_obj of items) {
|
||||
json_obj = _.omit(json_obj, omitFields || [])
|
||||
var item = this.findItem(json_obj["uuid"]);
|
||||
if(json_obj["deleted"] == true) {
|
||||
if(item) {
|
||||
this.deleteItem(item)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(item) {
|
||||
_.merge(item, json_obj);
|
||||
} else {
|
||||
item = this.createItem(json_obj);
|
||||
}
|
||||
|
||||
models.push(item)
|
||||
}
|
||||
this.addItems(models)
|
||||
this.resolveReferences()
|
||||
return models;
|
||||
}
|
||||
|
||||
createItem(json_obj) {
|
||||
if(json_obj.content_type == "Note") {
|
||||
return new Note(json_obj);
|
||||
} else if(json_obj.content_type == "Tag") {
|
||||
return new Tag(json_obj);
|
||||
} else {
|
||||
return new Item(json_obj);
|
||||
}
|
||||
}
|
||||
|
||||
resolveReferences() {
|
||||
this.items.forEach(function(item){
|
||||
// build out references, safely handle broken references
|
||||
item.content.references = _.reduce(item.content.references, function(accumulator, reference){
|
||||
var item = this.findItem(reference.uuid);
|
||||
if(item) {
|
||||
accumulator.push(item);
|
||||
}
|
||||
return accumulator;
|
||||
}.bind(this), []);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
|
||||
addItem(item) {
|
||||
if(!this.findItem(item.uuid)) {
|
||||
this.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// returns dirty item references that need saving
|
||||
deleteItem(item) {
|
||||
var dirty = [];
|
||||
_.remove(this.items, item);
|
||||
var length = item.content.references.length;
|
||||
// note that references are deleted in this for loop, so you have to be careful how you iterate
|
||||
for(var i = 0; i < length; i++) {
|
||||
var referencedItem = item.content.references[0];
|
||||
// console.log("removing references between items", referencedItem, item);
|
||||
var _dirty = this.removeReferencesBetweenItems(referencedItem, item);
|
||||
dirty = dirty.concat(_dirty);
|
||||
}
|
||||
|
||||
return dirty;
|
||||
}
|
||||
|
||||
removeReferencesBetweenItems(itemOne, itemTwo) {
|
||||
itemOne.removeReference(itemTwo);
|
||||
itemTwo.removeReference(itemOne);
|
||||
return [itemOne, itemTwo];
|
||||
}
|
||||
|
||||
createReferencesBetweenItems(itemOne, itemTwo) {
|
||||
itemOne.addReference(itemTwo);
|
||||
itemTwo.addReference(itemOne);
|
||||
return [itemOne, itemTwo];
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('itemManager', ItemManager);
|
||||
@@ -1,101 +1,239 @@
|
||||
class ModelManager extends ItemManager {
|
||||
class ModelManager {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.notes = [];
|
||||
this.tags = [];
|
||||
this.dirtyItems = [];
|
||||
this.itemSyncObservers = [];
|
||||
this.itemChangeObservers = [];
|
||||
this.items = [];
|
||||
this._extensions = [];
|
||||
}
|
||||
|
||||
resolveReferences() {
|
||||
super.resolveReferences()
|
||||
|
||||
this.notes.push.apply(this.notes, _.difference(this.itemsForContentType("Note"), this.notes));
|
||||
Item.sortItemsByDate(this.notes);
|
||||
this.notes.forEach(function(note){
|
||||
note.updateReferencesLocalMapping();
|
||||
})
|
||||
|
||||
this.tags.push.apply(this.tags, _.difference(this.itemsForContentType("Tag"), this.tags));
|
||||
this.tags.forEach(function(tag){
|
||||
tag.updateReferencesLocalMapping();
|
||||
get allItems() {
|
||||
return this.items.filter(function(item){
|
||||
return !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
addDirtyItems(items) {
|
||||
if(!(items instanceof Array)) {
|
||||
items = [items];
|
||||
get extensions() {
|
||||
return this._extensions.filter(function(ext){
|
||||
return !ext.deleted;
|
||||
})
|
||||
}
|
||||
|
||||
allItemsMatchingTypes(contentTypes) {
|
||||
return this.items.filter(function(item){
|
||||
return (contentTypes.includes(item.content_type) || contentTypes.includes("*")) && !item.dummy;
|
||||
})
|
||||
}
|
||||
|
||||
findItem(itemId) {
|
||||
return _.find(this.items, {uuid: itemId});
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModels(items) {
|
||||
return this.mapResponseItemsToLocalModelsOmittingFields(items, null);
|
||||
}
|
||||
|
||||
mapResponseItemsToLocalModelsOmittingFields(items, omitFields) {
|
||||
var models = []
|
||||
for (var json_obj of items) {
|
||||
json_obj = _.omit(json_obj, omitFields || [])
|
||||
var item = this.findItem(json_obj["uuid"]);
|
||||
if(json_obj["deleted"] == true) {
|
||||
if(item) {
|
||||
this.removeItemLocally(item)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
_.omit(json_obj, omitFields);
|
||||
|
||||
if(!item) {
|
||||
item = this.createItem(json_obj);
|
||||
} else {
|
||||
item.updateFromJSON(json_obj);
|
||||
}
|
||||
|
||||
this.addItem(item);
|
||||
|
||||
if(json_obj.content) {
|
||||
this.resolveReferencesForItem(item)
|
||||
}
|
||||
|
||||
models.push(item)
|
||||
}
|
||||
|
||||
this.dirtyItems = this.dirtyItems.concat(items);
|
||||
this.dirtyItems = _.uniq(this.dirtyItems);
|
||||
this.notifySyncObserversOfModels(models);
|
||||
|
||||
this.sortItems();
|
||||
return models;
|
||||
}
|
||||
|
||||
notifySyncObserversOfModels(models) {
|
||||
for(var observer of this.itemSyncObservers) {
|
||||
var relevantItems = models.filter(function(item){return item.content_type == observer.type});
|
||||
if(relevantItems.length > 0) {
|
||||
observer.callback(relevantItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemChangeObserversOfModels(models) {
|
||||
for(var observer of this.itemChangeObservers) {
|
||||
var relevantItems = models.filter(function(item){
|
||||
return observer.content_types.includes(item.content_type) || observer.content_types.includes("*");
|
||||
});
|
||||
|
||||
if(relevantItems.length > 0) {
|
||||
observer.callback(relevantItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createItem(json_obj) {
|
||||
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 {
|
||||
item = new Item(json_obj);
|
||||
}
|
||||
|
||||
item.addObserver(this, function(changedItem){
|
||||
this.notifyItemChangeObserversOfModels([changedItem]);
|
||||
}.bind(this));
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
addItems(items) {
|
||||
this.items = _.uniq(this.items.concat(items));
|
||||
|
||||
items.forEach(function(item){
|
||||
if(item.content_type == "Tag") {
|
||||
if(!_.find(this.tags, {uuid: item.uuid})) {
|
||||
this.tags.unshift(item);
|
||||
}
|
||||
} else if(item.content_type == "Note") {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
addItem(item) {
|
||||
this.addItems([item])
|
||||
}
|
||||
|
||||
itemsForContentType(contentType) {
|
||||
return this.items.filter(function(item){
|
||||
return item.content_type == contentType;
|
||||
});
|
||||
}
|
||||
|
||||
resolveReferencesForItem(item) {
|
||||
var contentObject = item.contentObject;
|
||||
if(!contentObject.references) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var reference of contentObject.references) {
|
||||
var referencedItem = this.findItem(reference.uuid);
|
||||
if(referencedItem) {
|
||||
item.addItemAsRelationship(referencedItem);
|
||||
referencedItem.addItemAsRelationship(item);
|
||||
} else {
|
||||
// console.log("Unable to find item:", reference.uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sortItems() {
|
||||
Item.sortItemsByDate(this.notes);
|
||||
|
||||
this.tags.forEach(function(tag){
|
||||
Item.sortItemsByDate(tag.notes);
|
||||
})
|
||||
}
|
||||
|
||||
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(function(item){return item.dirty == true && !item.dummy})
|
||||
}
|
||||
|
||||
clearDirtyItems() {
|
||||
this.dirtyItems = [];
|
||||
this.getDirtyItems().forEach(function(item){
|
||||
item.setDirty(false);
|
||||
})
|
||||
}
|
||||
|
||||
addNote(note) {
|
||||
if(!_.find(this.notes, {uuid: note.uuid})) {
|
||||
this.notes.unshift(note);
|
||||
this.addItem(note);
|
||||
setItemToBeDeleted(item) {
|
||||
item.deleted = true;
|
||||
if(!item.dummy) {
|
||||
item.setDirty(true);
|
||||
}
|
||||
item.removeAllRelationships();
|
||||
}
|
||||
|
||||
removeItemLocally(item) {
|
||||
_.pull(this.items, item);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
this.tags.unshift(tag);
|
||||
this.addItem(tag);
|
||||
/*
|
||||
Relationships
|
||||
*/
|
||||
|
||||
createRelationshipBetweenItems(itemOne, itemTwo) {
|
||||
itemOne.addItemAsRelationship(itemTwo);
|
||||
itemTwo.addItemAsRelationship(itemOne);
|
||||
|
||||
itemOne.setDirty(true);
|
||||
itemTwo.setDirty(true);
|
||||
}
|
||||
|
||||
addTagToNote(tag, note) {
|
||||
var dirty = this.createReferencesBetweenItems(tag, note);
|
||||
this.refreshRelationshipsForTag(tag);
|
||||
this.refreshRelationshipsForNote(note);
|
||||
this.addDirtyItems(dirty);
|
||||
}
|
||||
removeRelationshipBetweenItems(itemOne, itemTwo) {
|
||||
itemOne.removeItemAsRelationship(itemTwo);
|
||||
itemTwo.removeItemAsRelationship(itemOne);
|
||||
|
||||
refreshRelationshipsForTag(tag) {
|
||||
tag.notes = tag.referencesMatchingContentType("Note");
|
||||
Item.sortItemsByDate(tag.notes);
|
||||
itemOne.setDirty(true);
|
||||
itemTwo.setDirty(true);
|
||||
}
|
||||
|
||||
refreshRelationshipsForNote(note) {
|
||||
note.tags = note.referencesMatchingContentType("Tag");
|
||||
}
|
||||
|
||||
removeTagFromNote(tag, note) {
|
||||
var dirty = this.removeReferencesBetweenItems(tag, note);
|
||||
this.addDirtyItems(dirty);
|
||||
}
|
||||
|
||||
deleteItem(item) {
|
||||
var dirty = super.deleteItem(item);
|
||||
if(item.content_type == "Note") {
|
||||
_.remove(this.notes, item);
|
||||
} else if(item.content_type == "Tag") {
|
||||
_.remove(this.tags, item);
|
||||
}
|
||||
return dirty;
|
||||
}
|
||||
|
||||
deleteNote(note) {
|
||||
var dirty = this.deleteItem(note);
|
||||
_.remove(this.notes, note);
|
||||
if(!note.dummy) {
|
||||
this.addDirtyItems(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
deleteTag(tag) {
|
||||
var dirty = this.deleteItem(tag);
|
||||
_.remove(this.tags, tag);
|
||||
this.addDirtyItems(dirty);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('modelManager', ModelManager);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.nt-dropdown-menu.dark {
|
||||
background-color: $selection-color;
|
||||
background-color: white;
|
||||
color: $selected-text-color;
|
||||
|
||||
li {
|
||||
@@ -85,18 +85,27 @@
|
||||
bottom: 0px;
|
||||
background-color: #f1f1f1;
|
||||
color: $selected-text-color;
|
||||
// padding-top: 5px;
|
||||
height: 28px;
|
||||
cursor: default;
|
||||
|
||||
ol, ul {
|
||||
margin-top: 7px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&.dropdown-menu {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
text-align: left;
|
||||
|
||||
&.sep {
|
||||
margin: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
@@ -128,6 +137,7 @@
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: black;
|
||||
|
||||
.url {
|
||||
text-align: right;
|
||||
|
||||
64
app/assets/stylesheets/app/_directives.scss
Normal file
64
app/assets/stylesheets/app/_directives.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
.dropdown-menu.contextual-menu {
|
||||
.extension {
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
|
||||
.ext-header {
|
||||
background-color: #ededed;
|
||||
border-bottom: 1px solid #d3d3d3;
|
||||
padding-top: 12px;
|
||||
padding-left: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
||||
> .name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .access {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
font-weight: normal;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
list-style:none;
|
||||
padding-left:0;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
height: auto;
|
||||
|
||||
&.action {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid rgba(black, 0.1);
|
||||
background-color: rgba(white, 0.9);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(gray, 0.05);
|
||||
}
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
> .desc {
|
||||
font-weight: normal;
|
||||
opacity: 0.5;
|
||||
margin-top: 1px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,21 @@
|
||||
|
||||
.section-title-bar {
|
||||
border-bottom: none !important;
|
||||
|
||||
&.fullscreen {
|
||||
opacity: 0.0;
|
||||
-webkit-transition: all 300ms ease-in-out;
|
||||
-moz-transition: all 300ms ease-in-out;
|
||||
-ms-transition: all 300ms ease-in-out;
|
||||
-o-transition: all 300ms ease-in-out;
|
||||
transition: all 300ms ease-in-out;
|
||||
|
||||
&:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
$heading-height: 90px;
|
||||
@@ -65,6 +80,26 @@
|
||||
|
||||
}
|
||||
|
||||
.fullscreen-ghost-bar {
|
||||
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
height: 200px;
|
||||
z-index: 100;
|
||||
//
|
||||
// &:hover + .section-title-bar {
|
||||
// // show section title bar when hover over ghost bar
|
||||
// opacity: 1.0;
|
||||
// z-index: 100;
|
||||
// }
|
||||
|
||||
// &:not(:hover) + .section-title-bar {
|
||||
// // display: none;
|
||||
// opacity: 1.0;
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
.editor-content {
|
||||
max-height: 100%;
|
||||
|
||||
@@ -74,9 +109,14 @@
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
overflow: auto;
|
||||
|
||||
text-align: center; // centers children div horizontally
|
||||
z-index: 10;
|
||||
padding-top: $heading-height;
|
||||
|
||||
&.fullscreen {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.sampler-container {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
@@ -101,6 +141,12 @@
|
||||
padding-top: 17px;
|
||||
font-size: 17px;
|
||||
resize: none;
|
||||
|
||||
&.fullscreen {
|
||||
padding: 85px 10%;
|
||||
max-width: 1200px;
|
||||
display: inline-block;;
|
||||
}
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -110,6 +156,13 @@
|
||||
line-height: 23px;
|
||||
overflow-y: scroll;
|
||||
padding: 0px 15px;
|
||||
text-align: left;
|
||||
|
||||
&.fullscreen {
|
||||
padding: 85px 10%;
|
||||
max-width: 1200px;
|
||||
display: inline-block;;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,10 @@
|
||||
.email {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.server {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -260,3 +264,131 @@ a.disabled {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/**
|
||||
Extensions
|
||||
*/
|
||||
|
||||
.extensions-panel {
|
||||
font-size: 14px;
|
||||
|
||||
.extension-link {
|
||||
margin-top: 6px;
|
||||
|
||||
a {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.extension-form {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.registered-extensions {
|
||||
|
||||
|
||||
.extension {
|
||||
margin-bottom: 18px;
|
||||
background-color: #f6f6f6;
|
||||
border: 1px solid #f2f2f2;
|
||||
padding: 14px 6px;
|
||||
padding-bottom: 8px;
|
||||
color: black;
|
||||
|
||||
a {
|
||||
color: $blue-color !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.encryption-format {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
> .title {
|
||||
font-size: 13px;
|
||||
// font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
> .subtitle {
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> .actions {
|
||||
margin-top: 15px;
|
||||
font-size: 12px;
|
||||
|
||||
.action {
|
||||
padding: 13px;
|
||||
margin-bottom: 10px;
|
||||
background-color: rgba(white, 0.9);
|
||||
border: 1px solid rgba(gray, 0.15);
|
||||
|
||||
> .name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .permissions {
|
||||
margin-top: 2px;
|
||||
a {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
|
||||
> .execute {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0px;
|
||||
font-size: 12px;
|
||||
height: 30px;
|
||||
padding-top: 7px;
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
border: 1px solid rgba(gray, 0.15);
|
||||
cursor: pointer;
|
||||
color: $blue-color;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(gray, 0.10);
|
||||
}
|
||||
|
||||
.execution-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
> .execute-type {
|
||||
font-size: 12px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
> .error {
|
||||
color: red;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
> .last-run {
|
||||
opacity: 0.5;
|
||||
font-size: 11px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ $secondary-text-color: rgba($main-text-color, 0.8);
|
||||
$bg-color: #e3e3e3;
|
||||
$selection-color: $bg-color;
|
||||
$selected-text-color: black;
|
||||
$blue-color: #086dd6;
|
||||
|
||||
@mixin MQ-Small() {
|
||||
@media (max-width: $screen-xs-max) {
|
||||
|
||||
@@ -53,8 +53,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
$note-selection-color: #086dd6;
|
||||
|
||||
.note {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
@@ -75,13 +73,9 @@
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: $note-selection-color;
|
||||
background-color: $blue-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
// &:hover:not(.selected) {
|
||||
// background-color: $note-selection-color;
|
||||
// color: white;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ $dark-gray: #2e2e2e;
|
||||
@import "app/tags";
|
||||
@import "app/notes";
|
||||
@import "app/editor";
|
||||
@import "app/directives";
|
||||
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Forgot Your Password?
|
||||
.panel-body
|
||||
%p We'll send reset instructions to your email.
|
||||
%form{'ng-submit' => 'requestPasswordReset(forgotData)', 'ng-init' => 'forgotData = {}'}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'forgotData.email'}/
|
||||
%span.glyphicon.glyphicon-envelope.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Reset Password
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.login'} Go back to the login page
|
||||
@@ -1,18 +0,0 @@
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
%h3.panel-title Sign in to start your session
|
||||
.panel-body
|
||||
%form{'ng-submit' => 'submitLogin(loginData)', 'ng-init' => 'loginData = {}'}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Username', :required => true, :type => 'email', 'ng-model' => 'loginData.email'}/
|
||||
%span.glyphicon.glyphicon-user.form-control-feedback
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-8.hidden-xs
|
||||
%a.btn.btn-link{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
.col-sm-4
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Sign in
|
||||
.col-xs-12.visible-xs
|
||||
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.forgot'} I forgot my password
|
||||
@@ -1,15 +0,0 @@
|
||||
.panel.panel-default.panel-centered
|
||||
.panel-heading
|
||||
%h3.panel-title Reset Password
|
||||
.panel-body
|
||||
%p Type your new password.
|
||||
%form{'ng-submit' => 'resetPasswordSubmit()'}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'resetData.password'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:placeholder => 'Password confirmation', :name => 'password_confirmation', :required => true, :type => 'password', 'ng-model' => 'resetData.password_confirmation'}/
|
||||
%span.glyphicon.glyphicon-lock.form-control-feedback
|
||||
.row
|
||||
.col-sm-4.col-sm-offset-8
|
||||
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Update Password
|
||||
@@ -1,6 +0,0 @@
|
||||
.login-box.margin-auto{"style" => "margin-top: 150px;"}
|
||||
|
||||
%uib-alert{:type => '{{data.authAlert.type}}', :close => 'data.authAlert = null', 'ng-if' => 'data.authAlert'}
|
||||
{{data.authAlert.msg}}
|
||||
|
||||
%ui-view
|
||||
@@ -0,0 +1,13 @@
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark.contextual-menu
|
||||
.extension{"ng-repeat" => "extension in extensions"}
|
||||
.ext-header
|
||||
.name {{extension.name}}
|
||||
.access
|
||||
Can access your data
|
||||
%strong {{accessTypeForExtension(extension)}}
|
||||
%ul
|
||||
%li.action{"ng-repeat" => "action in extension.actionsWithContextForItem(item)", "ng-click" => "executeAction(action, extension)"}
|
||||
.name {{action.label}}
|
||||
.desc {{action.desc}}
|
||||
%span{"ng-if" => "action.running"}
|
||||
.spinner{"style" => "margin-top: 3px;"}
|
||||
@@ -1,15 +1,16 @@
|
||||
.section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"}
|
||||
.content
|
||||
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
-# %span.fullscreen-ghost-bar{"ng-if" => "ctrl.fullscreen"}
|
||||
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic(), 'fullscreen' : ctrl.fullscreen }"}
|
||||
.title
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.content.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
|
||||
"select-on-click" => "true"}
|
||||
.save-status {{ctrl.noteStatus}}
|
||||
.section-menu
|
||||
%ul.nav.nav-pills
|
||||
%li.dropdown
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.clickedMenu()"}
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.clickedMenu(); ctrl.showExtensions = false"}
|
||||
File
|
||||
%span.caret{"ng-if" => "!ctrl.note.locked"}
|
||||
%span{"ng-if" => " ctrl.note.locked"}
|
||||
@@ -17,20 +18,28 @@
|
||||
%span.sr-only
|
||||
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu && !ctrl.note.locked"}
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"} Toggle Fullscreen
|
||||
%li{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"}
|
||||
.text Toggle Fullscreen
|
||||
.shortcut Cmd + O
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleMarkdown()"} Toggle Markdown Preview
|
||||
%li{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleMarkdown()"}
|
||||
.text Toggle Markdown Preview
|
||||
.shortcut Cmd + M
|
||||
%li{"ng-if" => "!ctrl.note.isSharedIndividually()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.shareNote()"} Share
|
||||
%li{"ng-if" => "ctrl.note.isSharedIndividually()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.editUrlPressed()"} Edit URL
|
||||
%li{"ng-if" => "ctrl.note.isSharedIndividually()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.unshareNote()"} Unshare
|
||||
%li
|
||||
%a.text{"ng-click" => "ctrl.deleteNote()"} Delete
|
||||
%li{"ng-if" => "!ctrl.note.isSharedIndividually()", "ng-click" => "ctrl.selectedMenuItem(); ctrl.shareNote()"}
|
||||
.text Share
|
||||
%li{"ng-if" => "ctrl.note.isSharedIndividually()", "ng-click" => "ctrl.selectedMenuItem(); ctrl.editUrlPressed()"}
|
||||
.text Edit URL
|
||||
%li{"ng-if" => "ctrl.note.isSharedIndividually()", "ng-click" => "ctrl.selectedMenuItem(); ctrl.unshareNote()"}
|
||||
.text Unshare
|
||||
%li{"ng-click" => "ctrl.deleteNote()"}
|
||||
.text Delete
|
||||
%li.sep
|
||||
%li.dropdown{"ng-if" => "ctrl.hasAvailableExtensions()"}
|
||||
%a.dropdown-toggle{"ng-click" => "ctrl.showExtensions = !ctrl.showExtensions; ctrl.showMenu = false"}
|
||||
Extensions
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
%contextual-extensions-menu{"ng-if" => "ctrl.showExtensions", "item" => "ctrl.note"}
|
||||
|
||||
.markdown.icon{"ng-if" => "ctrl.editorMode == 'preview'", "ng-click" => "ctrl.showMarkdown = !ctrl.showMarkdown"}
|
||||
.icon-markdown
|
||||
.panel.panel-default.info-panel{"ng-if" => "ctrl.showMarkdown"}
|
||||
@@ -47,12 +56,10 @@
|
||||
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
|
||||
"select-on-click" => "true", "autofocus" => "true"}
|
||||
|
||||
.editor-content{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
|
||||
.editor-content{"ng-class" => "{'shared' : ctrl.note.isPublic(), 'fullscreen' : ctrl.fullscreen }"}
|
||||
.sampler-container{"ng-if" => "ctrl.showSampler", "ng-click" => "ctrl.focusEditor()"}
|
||||
%strong.name-sampler.sampler{"typewrite" => "true", "text" => "ctrl.demoNoteNames", "type-delay" => "30", "initial-delay" => "1.5s",
|
||||
"iteration-callback" => "ctrl.callback", "prebegin-fn" => "ctrl.prebeginFn", "iteration-delay" => "2000", "cursor" => ""}
|
||||
%code{"ng-if" => "ctrl.currentDemoContent.text"}
|
||||
.content-sampler.sampler{"typewrite" => "true", "text" => "ctrl.currentDemoContent.text", "type-delay" => "10", "iteration-callback" => "ctrl.contentCallback"}
|
||||
%textarea.editable#note-text-editor{"ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.content.text",
|
||||
%textarea.editable#note-text-editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.text",
|
||||
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
|
||||
.preview{"ng-if" => "ctrl.editorMode == 'preview'", "ng-bind-html" => "ctrl.renderedContent()", "ng-dblclick" => "ctrl.onPreviewDoubleClick()"}
|
||||
.preview{"ng-class" => "{'fullscreen' : ctrl.fullscreen }", "ng-if" => "ctrl.editorMode == 'preview'", "ng-bind-html" => "ctrl.renderedContent()", "ng-dblclick" => "ctrl.onPreviewDoubleClick()"}
|
||||
|
||||
@@ -49,20 +49,21 @@
|
||||
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.email {{ctrl.user.email}}
|
||||
.server {{ctrl.serverData.url}}
|
||||
.links{"ng-if" => "ctrl.user.email"}
|
||||
.link-item
|
||||
%a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
|
||||
%form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
|
||||
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span.ladda-label Change Password
|
||||
.panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
|
||||
{{ctrl.passwordChangeData.status}}
|
||||
-# .link-item
|
||||
-# %a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
|
||||
-# %form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
|
||||
-# .form-tag.has-feedback
|
||||
-# %input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
|
||||
-# .form-tag.has-feedback
|
||||
-# %input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
|
||||
-# .form-tag.has-feedback
|
||||
-# %input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
|
||||
-# %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
-# %span.ladda-label Change Password
|
||||
-# .panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
|
||||
-# {{ctrl.passwordChangeData.status}}
|
||||
.link-item
|
||||
%a{"ng-click" => "ctrl.signOutPressed()"} Sign Out
|
||||
.meta-container
|
||||
@@ -74,9 +75,15 @@
|
||||
.account-item{"ng-if" => "ctrl.user.email"}
|
||||
.meta-container
|
||||
.title Data Archives
|
||||
.desc Note: data archives that you download using the link below are decrypted before save. You should take care to store them in a safe location.
|
||||
.options{"style" => "font-size: 12px; margin-top: 4px;"}
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "true", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = true"}
|
||||
Encrypted
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "false", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = false"}
|
||||
Decrypted
|
||||
.action-container
|
||||
%a{"ng-click" => "ctrl.downloadDataArchive()"} Download Latest Data Archive
|
||||
%a{"ng-click" => "ctrl.downloadDataArchive()"} Download Data Archive
|
||||
%br
|
||||
%label#import-archive
|
||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"}
|
||||
@@ -84,6 +91,61 @@
|
||||
%span
|
||||
Import Data from Archive
|
||||
|
||||
.item
|
||||
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||
.panel.panel-default.account-panel.panel-right.extensions-panel{"ng-if" => "ctrl.showExtensionsMenu"}
|
||||
.panel-body
|
||||
.registered-extensions{"ng-if" => "ctrl.extensionManager.extensions.length"}
|
||||
.extension{"ng-repeat" => "extension in ctrl.extensionManager.extensions"}
|
||||
.name {{extension.name}}
|
||||
.encryption-format
|
||||
.title Send data:
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(true, extension)"}
|
||||
Encrypted
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(false, extension)"}
|
||||
Decrypted
|
||||
.actions
|
||||
.action{"ng-repeat" => "action in extension.actionsInGlobalContext()"}
|
||||
.name {{action.label}}
|
||||
.desc{"style" => "font-style: italic;"} {{action.desc}}
|
||||
.execute-type{"ng-if" => "action.repeat_mode == 'watch'"}
|
||||
Repeats when a change is made to your items.
|
||||
.execute-type{"ng-if" => "action.repeat_mode == 'loop'"}
|
||||
Repeats at most once every {{action.repeat_timeout}} seconds
|
||||
.permissions
|
||||
%a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}}
|
||||
%div{"ng-if" => "action.showPermissions"}
|
||||
{{action.permissionsString}}
|
||||
.encryption-type
|
||||
%span {{action.encryptionModeString}}
|
||||
.execute
|
||||
%div{"ng-if" => "action.repeat_mode"}
|
||||
%div{"ng-if" => "ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.disableRepeatAction(action, extension)"} Disable
|
||||
%div{"ng-if" => "!ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.enableRepeatAction(action, extension)"} Enable
|
||||
%div{"ng-if" => "!action.repeat_mode", "ng-click" => "ctrl.selectedAction(action, extension)"}
|
||||
%div{"ng-if" => "!action.running"}
|
||||
Perform Action
|
||||
%div{"ng-if" => "action.running"}
|
||||
.spinner.execution-spinner
|
||||
.last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"}
|
||||
Last run {{action.lastExecuted | appDateTime}}
|
||||
.error{"ng-if" => "action.error"}
|
||||
Error performing action.
|
||||
%a{"ng-click" => "ctrl.deleteExtension(extension)", "style" => "margin-top: 22px; display: block;"} Remove extension
|
||||
|
||||
.extension-link
|
||||
%a{"ng-click" => "ctrl.toggleExtensionForm()"} Add new extension
|
||||
|
||||
%form.extension-form{"ng-if" => "ctrl.showNewExtensionForm"}
|
||||
.form-tag.has-feedback
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'ctrl.newExtensionData.url'}
|
||||
%button.btn.dark-button.btn-block{"ng-click" => "ctrl.submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span.ladda-label Add Extension
|
||||
|
||||
.extension-link
|
||||
%a{"ng-click" => "ctrl.reloadExtensionsPressed()", "ng-if" => "ctrl.extensionManager.extensions.length > 0"} Reload all extensions
|
||||
|
||||
.item
|
||||
%a{"href" => "https://standardnotes.org", "target" => "_blank"}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.section.notes
|
||||
.content
|
||||
.section-title-bar.notes-title-bar
|
||||
.title {{ctrl.tag.content.title}} notes
|
||||
.title {{ctrl.tag.title}} notes
|
||||
.add-button{"ng-click" => "ctrl.createNewNote()"} +
|
||||
%br
|
||||
.filter-section
|
||||
@@ -13,6 +13,7 @@
|
||||
File
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
|
||||
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"}
|
||||
%li{"ng-if" => "!ctrl.tag.isPublic()"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagShare($event)"} Share Tag
|
||||
@@ -20,6 +21,7 @@
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagUnshare()"} Unshare Tag
|
||||
%li{"ng-if" => "!ctrl.tag.all"}
|
||||
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag
|
||||
|
||||
.menu-right-container
|
||||
.public-link{"ng-if" => "ctrl.tag.isPublic()"}
|
||||
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.tag.presentationURL()}}", "target" => "_blank"}
|
||||
@@ -34,5 +36,5 @@
|
||||
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}",
|
||||
"ng-attr-draggable" => "{{note.dummy ? undefined : 'true'}}", "note" => "note"}
|
||||
.name
|
||||
{{note.content.title}}
|
||||
{{note.title}}
|
||||
.date {{(note.created_at | appDateTime) || 'Now'}}
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
{{ctrl.test}}
|
||||
.tag{"ng-if" => "ctrl.allTag", "ng-click" => "ctrl.selectTag(ctrl.allTag)", "ng-class" => "{'selected' : ctrl.selectedTag == ctrl.allTag}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "tag" => "ctrl.allTag"}
|
||||
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allTag.content.title"}
|
||||
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allTag.title"}
|
||||
.count {{ctrl.noteCount(ctrl.allTag)}}
|
||||
|
||||
.tag{"ng-repeat" => "tag in ctrl.tags", "ng-click" => "ctrl.selectTag(tag)", "ng-class" => "{'selected' : ctrl.selectedTag == tag}",
|
||||
"droppable" => true, "drop" => "ctrl.handleDrop", "tag" => "tag"}
|
||||
.icon.icon-rss{"ng-if" => "tag.isPublic()"}
|
||||
%input.title{"ng-disabled" => "tag != ctrl.selectedTag", "ng-model" => "tag.content.title",
|
||||
%input.title{"ng-disabled" => "tag != ctrl.selectedTag", "ng-model" => "tag.title",
|
||||
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveTag($event, tag)", "mb-autofocus" => "true", "should-focus" => "ctrl.newTag",
|
||||
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-focus" => "ctrl.onTagTitleFocus(tag)"}
|
||||
.count {{ctrl.noteCount(tag)}}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||
# Prevent CSRF attacks by raising an exception.
|
||||
# For APIs, you may want to use :null_session instead.
|
||||
|
||||
|
||||
15
bower.json
15
bower.json
@@ -7,17 +7,14 @@
|
||||
"vendor": {
|
||||
"name": "bower-rails generated vendor assets",
|
||||
"dependencies": {
|
||||
"angular": "1.6.0",
|
||||
"angular-ui-router": "^0.2.18",
|
||||
"restangular": "^1.5.2",
|
||||
"oclazyload": "^1.0.9",
|
||||
"font-awesome": "^4.6.2",
|
||||
"marked": "0.3.4",
|
||||
"angular-lazy-img": "1.1.0",
|
||||
"ng-dialog" : "0.6.4"
|
||||
"angular": "1.6.1",
|
||||
"angular-ui-router": "^0.3.2",
|
||||
"restangular": "^1.6.1",
|
||||
"marked": "0.3.6",
|
||||
"ng-dialog" : "0.6.6"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.6.0"
|
||||
"angular": "1.6.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ Rails.application.configure do
|
||||
config.eager_load = true
|
||||
|
||||
# Full error reports are disabled and caching is turned on.
|
||||
config.consider_all_requests_local = true
|
||||
config.consider_all_requests_local = false
|
||||
config.action_controller.perform_caching = true
|
||||
|
||||
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
DeviseTokenAuth.setup do |config|
|
||||
# By default the authorization headers will change after each request. The
|
||||
# client is responsible for keeping track of the changing tokens. Change
|
||||
# this to false to prevent the Authorization header from changing after
|
||||
# each request.
|
||||
# config.change_headers_on_each_request = true
|
||||
|
||||
# By default, users will need to re-authenticate after 2 weeks. This setting
|
||||
# determines how long tokens will remain valid after they are issued.
|
||||
# config.token_lifespan = 2.weeks
|
||||
|
||||
# Sets the max number of concurrent devices per user, which is 10 by default.
|
||||
# After this limit is reached, the oldest tokens will be removed.
|
||||
# config.max_number_of_devices = 10
|
||||
|
||||
# Sometimes it's necessary to make several requests to the API at the same
|
||||
# time. In this case, each request in the batch will need to share the same
|
||||
# auth token. This setting determines how far apart the requests can be while
|
||||
# still using the same auth token.
|
||||
# config.batch_request_buffer_throttle = 5.seconds
|
||||
|
||||
# This route will be the prefix for all oauth2 redirect callbacks. For
|
||||
# example, using the default '/omniauth', the github oauth2 provider will
|
||||
# redirect successful authentications to '/omniauth/github/callback'
|
||||
# config.omniauth_prefix = "/omniauth"
|
||||
|
||||
# By default sending current password is not needed for the password update.
|
||||
# Uncomment to enforce current_password param to be checked before all
|
||||
# attribute updates. Set it to :password if you want it to be checked only if
|
||||
# password is updated.
|
||||
config.check_current_password_before_update = :password
|
||||
|
||||
# By default we will use callbacks for single omniauth.
|
||||
# It depends on fields like email, provider and uid.
|
||||
# config.default_callbacks = true
|
||||
|
||||
# Makes it possible to change the headers names
|
||||
# config.headers_names = {:'access-token' => 'access-token',
|
||||
# :'client' => 'client',
|
||||
# :'expiry' => 'expiry',
|
||||
# :'uid' => 'uid',
|
||||
# :'token-type' => 'token-type' }
|
||||
|
||||
# By default, only Bearer Token authentication is implemented out of the box.
|
||||
# If, however, you wish to integrate with legacy Devise authentication, you can
|
||||
# do so by enabling this flag. NOTE: This feature is highly experimental!
|
||||
# config.enable_standard_devise_support = false
|
||||
end
|
||||
0
vendor/assets/javascripts/.keep
vendored
0
vendor/assets/javascripts/.keep
vendored
1
vendor/assets/javascripts/app.js.map
vendored
1
vendor/assets/javascripts/app.js.map
vendored
File diff suppressed because one or more lines are too long
1
vendor/assets/javascripts/compiled.js.map
vendored
1
vendor/assets/javascripts/compiled.js.map
vendored
File diff suppressed because one or more lines are too long
1799
vendor/assets/javascripts/transpiled.js
vendored
1799
vendor/assets/javascripts/transpiled.js
vendored
File diff suppressed because it is too large
Load Diff
2
vendor/assets/javascripts/transpiled.js.map
vendored
2
vendor/assets/javascripts/transpiled.js.map
vendored
File diff suppressed because one or more lines are too long
0
vendor/assets/stylesheets/.keep
vendored
0
vendor/assets/stylesheets/.keep
vendored
Reference in New Issue
Block a user