diff --git a/Gruntfile.js b/Gruntfile.js index 97a381471..d12e1d7dc 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,7 +13,7 @@ module.exports = function(grunt) { js: { files: ['app/assets/javascripts/**/*.js'], - tasks: ['concat'], + tasks: ['concat', 'babel'], options: { spawn: false, }, @@ -114,6 +114,18 @@ module.exports = function(grunt) { } }, + babel: { + options: { + sourceMap: true, + presets: ['es2015'] + }, + dist: { + files: { + 'vendor/assets/javascripts/app.js': 'vendor/assets/javascripts/app.js' + } + } + } + ngAnnotate: { options: { singleQuotes: true, @@ -145,5 +157,5 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-ng-annotate'); grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat', 'ngAnnotate', 'uglify']); + grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat', 'babel', 'ngAnnotate', 'uglify']); }; diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index 94f541334..fc169e6f9 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -9,7 +9,7 @@ angular.module('app.frontend') $scope.allGroup = new Group({name: "All", all: true}); $scope.groups = $scope.defaultUser.groups; - apiController.verifyEncryptionStatusOfAllNotes($scope.defaultUser, function(success){ + apiController.verifyEncryptionStatusOfAllItems($scope.defaultUser, function(success){ }); } @@ -53,7 +53,7 @@ angular.module('app.frontend') } $scope.groupsSave = function(group, callback) { - apiController.saveGroup($scope.defaultUser, group, callback); + apiController.saveItem($scope.defaultUser, group, callback); } /* @@ -64,22 +64,15 @@ angular.module('app.frontend') var originalNote = _.find($scope.defaultUser.notes, {id: noteCopy.id}); - if(newGroup.all) { - // going to new group, nil out group_id - originalNote.group_id = null; - } else { - originalNote.group_id = newGroup.id - originalNote.group = newGroup; + $scope.defaultUser.itemManager.removeReferencesBetweenItems(oldGroup, originalNote); - newGroup.notes.unshift(originalNote); - newGroup.notes.sort(function(a,b){ - //subtract to get a value that is either negative, positive, or zero. - return new Date(b.created_at) - new Date(a.created_at); - }); + if(!newGroup.all) { + $scope.defaultUser.itemManager.createReferencesBetweenItems(newGroup, originalNote); + newGroup.updateReferencesLocalMapping(); } - apiController.saveNote($scope.defaultUser, originalNote, function(note){ - _.merge(originalNote, note); + apiController.saveBatchItems($scope.defaultUser, [originalNote, newGroup, oldGroup], function(){ + }); } @@ -91,7 +84,7 @@ angular.module('app.frontend') var validNotes = Note.filterDummyNotes(group.notes); if(validNotes == 0) { // if no more notes, delete group - apiController.deleteGroup($scope.defaultUser, group, function(){ + apiController.deleteItem($scope.defaultUser, group, function(){ // force scope groups to update on sub directives $scope.groups = []; $timeout(function(){ diff --git a/app/assets/javascripts/app/frontend/models/group.js b/app/assets/javascripts/app/frontend/models/group.js index 957642717..c8b5c9bed 100644 --- a/app/assets/javascripts/app/frontend/models/group.js +++ b/app/assets/javascripts/app/frontend/models/group.js @@ -1,3 +1,13 @@ -var Group = function (json_obj) { - _.merge(this, json_obj); -}; +class Group extends Item { + constructor(json_obj) { + _.merge(this, json_obj); + } + + updateReferencesLocalMapping() { + super.updateReferencesLocalMapping(); + this.notes = this.referencesMatchingContentType("Note"); + this.notes.sort(function(a,b){ + return new Date(b.created_at) - new Date(a.created_at); + }); + } +} diff --git a/app/assets/javascripts/app/frontend/models/item.js b/app/assets/javascripts/app/frontend/models/item.js new file mode 100644 index 000000000..4f4f7bd1e --- /dev/null +++ b/app/assets/javascripts/app/frontend/models/item.js @@ -0,0 +1,29 @@ +class Item { + + referencesMatchingContentType(contentType) { + return this.references.filter(function(reference){ + return reference.content_type == content_type; + }); + } + + updateReferencesLocalMapping() { + // should be overriden to manage local properties + } + + /* Returns true if note is shared individually or via group */ + isPublic() { + return this.presentation; + } + + isEncrypted() { + return this.encryptionEnabled() && typeof this.content === 'string' ? true : false; + } + + encryptionEnabled() { + return this.loc_eek; + } + + presentationURL() { + return this.presentation.url; + } +} diff --git a/app/assets/javascripts/app/frontend/models/itemManager.js b/app/assets/javascripts/app/frontend/models/itemManager.js new file mode 100644 index 000000000..af92c6bb3 --- /dev/null +++ b/app/assets/javascripts/app/frontend/models/itemManager.js @@ -0,0 +1,55 @@ +class ItemManager() { + + set items(items) { + this.items = items; + resolveReferences(); + } + + referencesForItemId(itemId) { + return _.find(this.items, {uuid: itemId}); + } + + resolveReferences() { + this.items.forEach(function(item){ + // build out references + _.map(item.references, function(reference){ + return referencesForItemId(reference.uuid); + }) + }); + } + + itemsForContentType(contentType) { + this.items.filter(function(item){ + return item.content_type == contentType; + }); + } + + // returns dirty item references that need saving + deleteItem(item) { + _.remove(this.items, item); + item.references.forEach(function(reference){ + removeReferencesFromItem(reference, item); + }) + + return item.references; + } + + removeReferencesBetweenItems(itemOne, itemTwo) { + _.remove(itemOne.references, _.find(itemOne.references, {uuid: itemTwo.uuid})); + _.remove(itemTwo.references, _.find(itemTwo.references, {uuid: itemOne.uuid})); + return [itemOne, itemTwo]; + } + + removeReferencesBetweenItems(itemOne, itemTwo) { + itemOne.references.push(itemTwo); + itemTwo.references.push(itemOne); + return [itemOne, itemTwo]; + } + + createReferencesBetweenItems(itemOne, itemTwo) { + itemOne.references.push(itemTwo); + itemTwo.references.push(itemOne); + return [itemOne, itemTwo]; + } + +} diff --git a/app/assets/javascripts/app/frontend/models/note.js b/app/assets/javascripts/app/frontend/models/note.js index 53d5cb0bf..0c935bf20 100644 --- a/app/assets/javascripts/app/frontend/models/note.js +++ b/app/assets/javascripts/app/frontend/models/note.js @@ -1,57 +1,63 @@ -var Note = function (json_obj) { - var content; +class Note extends Item { + constructor(json_obj) { + var content; - Object.defineProperty(this, "content", { - get: function() { - return content; - }, - set: function(value) { - var finalValue = value; + Object.defineProperty(this, "content", { + get: function() { + return content; + }, + set: function(value) { + var finalValue = value; - if(typeof value === 'string') { - try { - decodedValue = JSON.parse(value); - finalValue = decodedValue; - } - catch(e) { - finalValue = value; + if(typeof value === 'string') { + try { + decodedValue = JSON.parse(value); + finalValue = decodedValue; + } + catch(e) { + finalValue = value; + } } + + content = finalValue; + }, + enumerable: true, + }); + + this.setContentRaw = function(rawContent) { + content = rawContent; + } + + _.merge(this, json_obj); + + if(!this.content) { + this.content = {title: "", text: ""}; + } + } + + filterDummyNotes(notes) { + var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null}); + return filtered; + } + + updateReferencesLocalMapping() { + super.updateReferencesLocalMapping(); + this.groups = this.referencesMatchingContentType("Group"); + } + + get hasOnePublicGroup() { + var hasPublicGroup = false; + this.groups.forEach(function(group){ + if(group.isPublic()) { + hasPublicGroup = true; + return; } + }) - content = finalValue; - }, - enumerable: true, - }); - - this.setContentRaw = function(rawContent) { - content = rawContent; + return hasPublicGroup; } - _.merge(this, json_obj); - - if(!this.content) { - this.content = {title: "", text: ""}; + function isPublic() { + return super.isPublic() || this.hasOnePublicGroup; } -}; - -Note.filterDummyNotes = function(notes) { - var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null}); - return filtered; -} - -/* Returns true if note is shared individually or via group */ -Note.prototype.isPublic = function() { - return this.presentation || (this.group && this.group.presentation); -}; - -Note.prototype.isEncrypted = function() { - return this.encryptionEnabled() && typeof this.content === 'string' ? true : false; -} - -Note.prototype.encryptionEnabled = function() { - return this.loc_eek; -} - -Note.prototype.presentationURL = function() { - return this.presentation.url; } diff --git a/app/assets/javascripts/app/frontend/models/user.js b/app/assets/javascripts/app/frontend/models/user.js index a6ff61349..4e6ddac6a 100644 --- a/app/assets/javascripts/app/frontend/models/user.js +++ b/app/assets/javascripts/app/frontend/models/user.js @@ -1,23 +1,23 @@ -var User = function (json_obj) { - _.merge(this, json_obj); +class User { + constructor(json_obj) { + _.merge(this, json_obj); - this.notes = _.map(this.notes, function(json_obj) { - return new Note(json_obj); - }); + this.itemManager = new ItemManager(); + this.itemManager.items = this.items; + this.items = null; - this.groups = _.map(this.groups, function(json_obj) { - return new Group(json_obj); - }); - - this.groups.forEach(function(group){ - var notes = this.notes.filter(function(note){return note.group_id && note.group_id == group.id}); - notes.forEach(function(note){ - note.group = group; + this.notes = _.map(this.itemManager.itemsForContentType("Note"), function(json_obj) { + return new Note(json_obj); }) - group.notes = notes; - }.bind(this)) -}; -User.prototype.filteredNotes = function() { - return Note.filterDummyNotes(this.notes); + this.groups = _.map(this.itemManager.itemsForContentType("Group"), function(json_obj) { + var group = Group(json_obj); + group.updateReferencesLocalMapping(); + return group; + }) + } + + filteredNotes() { + return Note.filterDummyNotes(this.notes); + } } diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index 41a1c2d96..1f7a13247 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -144,7 +144,7 @@ angular.module('app.services') Ensures that if encryption is disabled, all local notes are uncrypted, and that if it's enabled, that all local notes are encrypted */ - this.verifyEncryptionStatusOfAllNotes = function(user, callback) { + this.verifyEncryptionStatusOfAllItems = function(user, callback) { var allNotes = user.filteredNotes(); var notesNeedingUpdate = []; allNotes.forEach(function(note){ @@ -168,83 +168,13 @@ angular.module('app.services') /* - Groups + Items */ - this.restangularizeGroup = function(group, user) { - var request = Restangular.one("users", user.id).one("groups", group.id); - _.merge(request, group); - return request; - } - - this.saveGroup = function(user, group, callback) { - if(user.id) { - if(!group.route) { - group = this.restangularizeGroup(group, user); - } - group.customOperation(group.id ? "put" : "post").then(function(response) { - callback(response.plain()); - }) - } else { - this.writeUserToLocalStorage(user); - callback(group); - } - } - - this.deleteGroup = function(user, group, callback) { - if(!user.id) { - _.remove(user.groups, group); - this.writeUserToLocalStorage(user); - callback(true); - } else { - Restangular.one("users", user.id).one("groups", group.id).remove() - .then(function(response) { - _.remove(user.groups, group); - callback(true); - }) - } - } - - this.shareGroup = function(user, group, callback) { - Restangular.one("users", user.id).one("groups", group.id).one("presentations").post() - .then(function(response){ - var presentation = response.plain(); - _.merge(group, {presentation: presentation}); - callback(presentation); - - if(group.notes.length > 0) { - // decrypt notes - this.saveBatchNotes(user, group.notes, function(success){}) - } - }.bind(this)) - } - - this.unshareGroup = function(user, group, callback) { - var request = Restangular.one("users", user.id).one("groups", group.id).one("presentations", group.presentation.id); - request.remove().then(function(response){ - group.presentation = null; - callback(null); - - if(group.notes.length > 0) { - // encrypt notes - var notes = group.notes; - this.saveBatchNotes(user, notes, function(success){}) - } - }.bind(this)) - } - - - - - - /* - Notes - */ - - this.saveBatchNotes = function(user, notes, callback) { - var request = Restangular.one("users", user.id).one("notes/batch_update"); - request.notes = _.map(notes, function(note){ - return this.createRequestParamsFromNote(note, user); + this.saveBatchItems = function(user, items, callback) { + var request = Restangular.one("users", user.uuid).one("items/batch_update"); + request.items = _.map(items, function(item){ + return this.createRequestParamsFromItem(item, user); }.bind(this)); request.put().then(function(response){ var success = response.plain().success; @@ -252,95 +182,88 @@ angular.module('app.services') }) } - this.saveNote = function(user, note, callback) { + this.saveItem = function(user, item, callback) { if(!user.id) { this.writeUserToLocalStorage(user); - callback(note); + callback(item); return; } - var params = this.createRequestParamsFromNote(note, user); + var params = this.createRequestParamsForItem(item, user); - var request = Restangular.one("users", user.id).one("notes", note.id); + var request = Restangular.one("users", user.uuid).one("item", item.uuid); _.merge(request, params); - request.customOperation(request.id ? "put" : "post") + request.customOperation(request.uuid ? "put" : "post") .then(function(response) { var responseObject = response.plain(); - responseObject.content = note.content; - _.merge(note, responseObject); - callback(note); + responseObject.content = item.content; + _.merge(item, responseObject); + callback(item); }) .catch(function(response){ callback(null); }) } - this.createRequestParamsFromNote = function(note, user) { - var params = {id: note.id}; + this.createRequestParamsForItem = function(item, user) { + var params = {uuid: item.uuid}; - if(!note.pending_share && !note.isPublic()) { + if(!item.isPublic()) { // encrypted - var noteCopy = _.cloneDeep(note); - this.encryptSingleNote(noteCopy, this.retrieveGk()); - params.content = noteCopy.content; - params.loc_eek = noteCopy.loc_eek; + var itemCopy = _.cloneDeep(item); + this.encryptSingleNote(itemCopy, this.retrieveGk()); + params.content = itemCopy.content; + params.loc_eek = itemCopy.loc_eek; } else { // decrypted - params.content = JSON.stringify(note.content); + params.content = JSON.stringify(item.content); params.loc_eek = null; } return params; } - this.deleteNote = function(user, note, callback) { + this.deleteItem = function(user, item, callback) { if(!user.id) { this.writeUserToLocalStorage(user); callback(true); } else { - Restangular.one("users", user.id).one("notes", note.id).remove() + Restangular.one("users", user.uuid).one("items", item.uuid).remove() .then(function(response) { callback(true); }) } } - this.shareNote = function(user, note, callback) { + this.shareItem = function(user, item, callback) { if(!user.id) { - if(confirm("Note: You are not signed in. Any note you share cannot be edited or unshared.")) { - var request = Restangular.one("notes").one("share"); - _.merge(request, {name: note.content.title, content: note.content}); - request.post().then(function(response){ - var presentation = response.plain(); - _.merge(note, {presentation: presentation}); - note.locked = true; - this.writeUserToLocalStorage(user); - callback(note); - }.bind(this)) - } + alert("You must be signed in to share."); } else { - var shareFn = function(note, callback) { - Restangular.one("users", user.id).one("notes", note.id).one("presentations").post() - .then(function(response){ - var presentation = response.plain(); - _.merge(note, {presentation: presentation}); - callback(note); - }) - } + Restangular.one("users", user.uuid).one("items", item.uuid).one("presentations").post() + .then(function(response){ + var presentation = response.plain(); + _.merge(item, {presentation: presentation}); + callback(item); - note.pending_share = true; - this.saveNote(user, note, function(saved_note){ - shareFn(saved_note, callback); + // decrypt references + if(item.references.length > 0) { + this.saveBatchItems(user, item.references, function(success){}) + } }) } } - this.unshareNote = function(user, note, callback) { - var request = Restangular.one("users", user.id).one("notes", note.id).one("presentations", note.presentation.id); + this.unshareItem = function(user, item, callback) { + var request = Restangular.one("users", user.uuid).one("notes", item.uuid).one("presentations", item.presentation.uuid); request.remove().then(function(response){ - note.presentation = null; + item.presentation = null; callback(null); + + // encrypt references + if(item.references.length > 0) { + this.saveBatchItems(user, item.references, function(success){}) + } }) } @@ -351,7 +274,7 @@ angular.module('app.services') this.updatePresentation = function(resource, presentation, callback) { var request = Restangular.one("users", user.id) - .one(resource.constructor.name.toLowerCase() + "s", resource.id) + .one("items", resource.id) .one("presentations", resource.presentation.id); _.merge(request, presentation); request.patch().then(function(response){ diff --git a/package.json b/package.json index f409561d4..e90025918 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,10 @@ "name": "neeto", "version": "1.0.0", "devDependencies": { + "babel-preset-es2015": "^6.18.0", "grunt": "^1.0.1", "grunt-angular-templates": "^1.1.0", + "grunt-babel": "^6.0.0", "grunt-contrib-concat": "^1.0.1", "grunt-contrib-cssmin": "^1.0.2", "grunt-contrib-jshint": "~0.10.0",