Merge pull request #12 from standardnotes/extensions

Extensions
This commit is contained in:
Mo Bitar
2017-01-07 19:00:26 -06:00
committed by GitHub
44 changed files with 2718 additions and 990 deletions

View File

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

View File

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

View File

@@ -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'
}

View File

@@ -11,8 +11,6 @@ if(window.crypto.subtle) {
angular.module('app.frontend', [
'ui.router',
'restangular',
'oc.lazyLoad',
'angularLazyImg',
'ngDialog'
])

View File

@@ -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.");

View File

@@ -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();
}

View File

@@ -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);
}
/*

View File

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

View File

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

View File

@@ -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() {

View 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;
}
}

View File

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

View File

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

View File

@@ -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))
}
}
}
}

View File

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

View 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);

View File

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

View File

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

View File

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

View 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;
}
}
}
}
}
}

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -7,6 +7,7 @@ $dark-gray: #2e2e2e;
@import "app/tags";
@import "app/notes";
@import "app/editor";
@import "app/directives";
@font-face {
font-family: 'icomoon';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()"}

View File

@@ -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"}

View File

@@ -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'}}

View File

@@ -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)}}

View File

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

View File

@@ -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"
}
}
}

View File

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

View File

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

View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File