From eaffc346fd72307122958cf930d13fcf6b3e874a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 14:30:09 +0800 Subject: [PATCH 001/378] v2.9.1 --- app/assets/javascripts/src/Metamaps.js.erb | 1 + config/initializers/version.rb | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 839d701e..438bedf9 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -10,6 +10,7 @@ Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null Metamaps.VERSION = '<%= METAMAPS_VERSION %>' +Metamaps.LAST_UPDATED = '<%= METAMAPS_LAST_UPDATED %>' /* erb variables from rails */ Metamaps.Erb = {} diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 19139a4a..0cabe4da 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,2 @@ -METAMAPS_VERSION = '2.9.0'.freeze -METAMAPS_LAST_UPDATED = 'Sept 1, 2016'.freeze +METAMAPS_VERSION = '2.9.1'.freeze +METAMAPS_LAST_UPDATED = 'Sept 13, 2016'.freeze From 4723c62b2025ffdb833bb918760bd05af9c74245 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 15 Sep 2016 07:18:15 +0800 Subject: [PATCH 002/378] fix password reset error --- app/controllers/users/passwords_controller.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index b6fa2acb..ee7b8667 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -1,7 +1,11 @@ class Users::PasswordsController < Devise::PasswordsController protected - def after_resetting_password_path_for(resource) - signed_in_root_path(resource) - end + def after_resetting_password_path_for(resource) + signed_in_root_path(resource) + end + + def after_sending_reset_password_instructions_path_for(resource_name) + new_user_session_path if is_navigational_format? + end end From 95151523154117770fc8d07dd6c2e018d1e8efae Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 21:01:36 +0800 Subject: [PATCH 003/378] move auto layout function into its own file --- .../javascripts/src/Metamaps.AutoLayout.js | 75 +++++++++++++++++++ app/assets/javascripts/src/Metamaps.Map.js | 72 +----------------- app/assets/javascripts/src/Metamaps.Topic.js | 2 +- 3 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.AutoLayout.js diff --git a/app/assets/javascripts/src/Metamaps.AutoLayout.js b/app/assets/javascripts/src/Metamaps.AutoLayout.js new file mode 100644 index 00000000..51e105c2 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.AutoLayout.js @@ -0,0 +1,75 @@ +/* global Metamaps */ + +/* + * Metmaaps.AutoLayout.js + * + * Dependencies: none! + */ + +Metamaps.AutoLayout = { + nextX: 0, + nextY: 0, + sideLength: 1, + turnCount: 0, + nextXshift: 1, + nextYshift: 0, + timeToTurn: 0, + + getNextCoord: function () { + var self = Metamaps.AutoLayout + var nextX = self.nextX + var nextY = self.nextY + + var DISTANCE_BETWEEN = 120 + + self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift + self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift + + self.timeToTurn += 1 + // if true, it's time to turn + if (self.timeToTurn === self.sideLength) { + self.turnCount += 1 + // if true, it's time to increase side length + if (self.turnCount % 2 === 0) { + self.sideLength += 1 + } + self.timeToTurn = 0 + + // going right? turn down + if (self.nextXshift == 1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = 1 + } + // going down? turn left + else if (self.nextXshift == 0 && self.nextYshift == 1) { + self.nextXshift = -1 + self.nextYshift = 0 + } + // going left? turn up + else if (self.nextXshift == -1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = -1 + } + // going up? turn right + else if (self.nextXshift == 0 && self.nextYshift == -1) { + self.nextXshift = 1 + self.nextYshift = 0 + } + } + + return { + x: nextX, + y: nextY + } + }, + resetSpiral: function () { + var self = Metamaps.AutoLayout + self.nextX = 0 + self.nextY = 0 + self.nextXshift = 1 + self.nextYshift = 0 + self.sideLength = 1 + self.timeToTurn = 0 + self.turnCount = 0 + } +} diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/app/assets/javascripts/src/Metamaps.Map.js index 1c3c638d..264e3c48 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js +++ b/app/assets/javascripts/src/Metamaps.Map.js @@ -4,6 +4,7 @@ * Metamaps.Map.js.erb * * Dependencies: + * - Metamaps.AutoLayout * - Metamaps.Create * - Metamaps.Erb * - Metamaps.Filter @@ -34,13 +35,6 @@ Metamaps.Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' }, - nextX: 0, - nextY: 0, - sideLength: 1, - turnCount: 0, - nextXshift: 1, - nextYshift: 0, - timeToTurn: 0, init: function () { var self = Metamaps.Map @@ -131,7 +125,7 @@ Metamaps.Map = { end: function () { if (Metamaps.Active.Map) { $('.wrapper').removeClass('canEditMap commonsMap') - Metamaps.Map.resetSpiral() + Metamaps.AutoLayout.resetSpiral() $('.rightclickmenu').remove() Metamaps.TopicCard.hideCard() @@ -242,68 +236,6 @@ Metamaps.Map = { Metamaps.Mappers.add(Metamaps.Active.Mapper) } }, - getNextCoord: function () { - var self = Metamaps.Map - var nextX = self.nextX - var nextY = self.nextY - - var DISTANCE_BETWEEN = 120 - - self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift - self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift - - self.timeToTurn += 1 - // if true, it's time to turn - if (self.timeToTurn === self.sideLength) { - self.turnCount += 1 - // if true, it's time to increase side length - if (self.turnCount % 2 === 0) { - self.sideLength += 1 - } - self.timeToTurn = 0 - - // going right? turn down - if (self.nextXshift == 1 && self.nextYshift == 0) { - self.nextXshift = 0 - self.nextYshift = 1 - } - // going down? turn left - else if (self.nextXshift == 0 && self.nextYshift == 1) { - self.nextXshift = -1 - self.nextYshift = 0 - } - // going left? turn up - else if (self.nextXshift == -1 && self.nextYshift == 0) { - self.nextXshift = 0 - self.nextYshift = -1 - } - // going up? turn right - else if (self.nextXshift == 0 && self.nextYshift == -1) { - self.nextXshift = 1 - self.nextYshift = 0 - } - } - - // this is so that if someone has relied on the auto-placement feature on this map, - // it will at least start placing nodes at the first empty spot - // this will only work up to the point in the spiral at which someone manually moved a node - if (Metamaps.Mappings.findWhere({ xloc: nextX, yloc: nextY })) { - return self.getNextCoord() - } - else return { - x: nextX, - y: nextY - } - }, - resetSpiral: function () { - Metamaps.Map.nextX = 0 - Metamaps.Map.nextY = 0 - Metamaps.Map.nextXshift = 1 - Metamaps.Map.nextYshift = 0 - Metamaps.Map.sideLength = 1 - Metamaps.Map.timeToTurn = 0 - Metamaps.Map.turnCount = 0 - }, exportImage: function () { var canvas = {} diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 40a4cd42..52fabed3 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -370,7 +370,7 @@ Metamaps.Topic = { var topic = self.get(id) - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords.x, yloc: nextCoords.y, From ec96d69876da3792a775b507ccd3bb3f3dcb0413 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 21:02:55 +0800 Subject: [PATCH 004/378] refactor import view: -Paste Input wrapper class to abstract away getting input -Add ability to drop files in PasteInput -Add ability to drop .webloc files or paste a link to create a new topic with that link in the link and desc fields --- app/assets/javascripts/application.js | 2 + app/assets/javascripts/src/Metamaps.Import.js | 64 +++++----- .../javascripts/src/Metamaps.PasteInput.js | 120 ++++++++++++++++++ 3 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.PasteInput.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6ed8278d..14f565fa 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -43,5 +43,7 @@ //= require ./src/Metamaps.Mobile //= require ./src/Metamaps.Admin //= require ./src/Metamaps.Import +//= require ./src/Metamaps.AutoLayout +//= require ./src/Metamaps.PasteInput //= require ./src/Metamaps.JIT //= require ./src/Metamaps.Debug diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index d7771988..7ebadb37 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -6,7 +6,6 @@ * Dependencies: * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Famous // TODO remove dependency * - Metamaps.Map * - Metamaps.Mappings * - Metamaps.Metacodes @@ -24,38 +23,30 @@ Metamaps.Import = { ], cidMappings: {}, // to be filled by import_id => cid mappings - init: function () { + handleTSV: function (text) { var self = Metamaps.Import + results = self.parseTabbedString(text) + self.handle(results) + }, - $('body').bind('paste', function (e) { - if (e.target.tagName === 'INPUT') return - if (e.target.tagName === 'TEXTAREA') return + handleJSON: function (text) { + var self = Metamaps.Import + results = JSON.parse(text) + self.handle(results) + }, - var text = e.originalEvent.clipboardData.getData('text/plain') + handle: function(results) { + var self = Metamaps.Import + var topics = results.topics + var synapses = results.synapses - var results - if (text.trimLeft()[0] === '{') { - try { - results = JSON.parse(text) - } catch (e) { - results = false - } - } else { - results = self.parseTabbedString(text) - } - if (results === false) return - - var topics = results.topics - var synapses = results.synapses - - if (topics.length > 0 || synapses.length > 0) { - if (window.confirm('Are you sure you want to create ' + topics.length + - ' new topics and ' + synapses.length + ' new synapses?')) { - self.importTopics(topics) - self.importSynapses(synapses) - } // if + if (topics.length > 0 || synapses.length > 0) { + if (window.confirm('Are you sure you want to create ' + topics.length + + ' new topics and ' + synapses.length + ' new synapses?')) { + self.importTopics(topics) + self.importSynapses(synapses) } // if - }) + } // if }, abort: function (message) { @@ -272,15 +263,22 @@ Metamaps.Import = { console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') } + var topic_permission = permission || Metamaps.Active.Map.get('permission') + var defer_to_map_id = permission === topic_permission ? Metamaps.Active.Map.get('id') : null var topic = new Metamaps.Backbone.Topic({ name: name, metacode_id: metacode.id, - permission: permission || Metamaps.Active.Map.get('permission'), - desc: desc || "", - link: link + permission: topic_permission, + defer_to_map_id: defer_to_map_id, + desc: desc || "" }) + topic.set('desc', desc || '') // TODO why is this necessary? + topic.set('link', link) // TODO why is this necessary? Metamaps.Topics.add(topic) - self.cidMappings[import_id] = topic.cid + + if (import_id !== null && import_id !== undefined) { + self.cidMappings[import_id] = topic.cid + } var mapping = new Metamaps.Backbone.Mapping({ xloc: xloc, @@ -293,7 +291,7 @@ Metamaps.Import = { // this function also includes the creation of the topic in the database Metamaps.Topic.renderTopic(mapping, topic, true, true) - Metamaps.Famous.viz.hideInstructions() + Metamaps.GlobalUI.hideDiv('#instructions') }, createSynapseWithParameters: function (desc, category, permission, diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js new file mode 100644 index 00000000..1b89b0af --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -0,0 +1,120 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.PasteInput.js.erb + * + * Dependencies: + * - Metamaps.Import + * - Metamaps.AutoLayout + */ + +Metamaps.PasteInput = { + init: function () { + var self = Metamaps.PasteInput + + // intercept dragged files + // see http://stackoverflow.com/questions/6756583 + window.addEventListener("dragover", function(e){ + e = e || event; + e.preventDefault(); + }, false); + window.addEventListener("drop", function(e){ + e = e || event; + e.preventDefault(); + var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) + if (e.dataTransfer.files.length > 0) { + var fileReader = new FileReader() + var text = fileReader.readAsText(e.dataTransfer.files[0]) + fileReader.onload = function(e) { + var text = e.currentTarget.result + if (text.substring(0,5) === '(.*)<\/string>[\s\S]*/m, '$1') + } + self.handle(text, coords) + } + } + }, false); + + // allow pasting onto canvas (but don't break existing inputs/textareas) + $('body').bind('paste', function (e) { + if (e.target.tagName === 'INPUT') return + if (e.target.tagName === 'TEXTAREA') return + + var text = e.originalEvent.clipboardData.getData('text/plain').trim() + self.handle(text) + }) + }, + + handle: function(text, coords) { + var self = Metamaps.PasteInput + // thanks to https://github.com/kevva/url-regex + const URL_REGEX = new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$') + + if (text.match(URL_REGEX)) { + self.handleURL(text, coords) + } else if (text[0] === '{') { + self.handleJSON(text) + } else if (text.match(/\t/)) { + self.handleTSV(text) + } else { + // fail silently + } + }, + + handleURL: function (text, coords) { + var title = 'Link' + if (!coords || !coords.x || !coords.y) { + coords = Metamaps.AutoLayout.getNextCoord() + } + + var import_id = null // don't store a cidMapping + var permission = null // use default + + // try { + // // fetch title in 150ms or less + // Promise.race([ + // new Promise(function(resolve, reject) { + // fetch(text).then(function(response) { + // return response.text() + // }).then(function(html) { + // title = html.replace(/[\s\S]*(.*)<\/title>[\s\S]*/m, '$1') + // resolve() + // }) + // }), new Promise(function(resolve, reject) { + // window.setTimeout(function() { + // resolve() + // }, 150) + // }) + // ]).then(function() { + // finish() + // }).catch(function(error) { + // throw error + // }) + // } catch (err) { + // console.warn("Your browser can't fetch the title") // TODO move to webpack to avoid this error + // } + finish() + + function finish() { + Metamaps.Import.createTopicWithParameters( + title, + 'Reference', // metacode - todo fix + permission, + text, // desc - todo load from url? + text, // link - todo fix because this isn't being POSTed + coords.x, + coords.y, + import_id + ) + } + }, + + handleJSON: function (text) { + Metamaps.Import.handleJSON(text) + }, + + handleTSV: function (text) { + Metamaps.Import.handleTSV(text) + } +} From fac59f346f0f5320b7753157ae1297462d48e58d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 10:24:57 +0800 Subject: [PATCH 005/378] fix topic init function --- app/assets/javascripts/src/Metamaps.Backbone.js | 4 ++-- app/assets/javascripts/src/Metamaps.Import.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js index d05ffe3a..a37229d2 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -321,8 +321,8 @@ Metamaps.Backbone.init = function () { if (this.isNew()) { this.set({ 'user_id': Metamaps.Active.Mapper.id, - 'desc': '', - 'link': '', + 'desc': this.get('desc') || '', + 'link': this.get('link') || '', 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' }) } diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index 7ebadb37..2ed7e00a 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -270,10 +270,9 @@ Metamaps.Import = { metacode_id: metacode.id, permission: topic_permission, defer_to_map_id: defer_to_map_id, - desc: desc || "" + desc: desc || "", + link: link || "" }) - topic.set('desc', desc || '') // TODO why is this necessary? - topic.set('link', link) // TODO why is this necessary? Metamaps.Topics.add(topic) if (import_id !== null && import_id !== undefined) { From 49084b98dd140c07bb393bec821399e240947353 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 10:48:47 +0800 Subject: [PATCH 006/378] =?UTF-8?q?omg=20import=20bookmarks=20=F0=9F=98=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javascripts/src/Metamaps.PasteInput.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js index 1b89b0af..55798587 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -9,16 +9,19 @@ */ Metamaps.PasteInput = { + // thanks to https://github.com/kevva/url-regex + URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'), + init: function () { var self = Metamaps.PasteInput // intercept dragged files // see http://stackoverflow.com/questions/6756583 - window.addEventListener("dragover", function(e){ + window.addEventListener("dragover", function(e) { e = e || event; e.preventDefault(); }, false); - window.addEventListener("drop", function(e){ + window.addEventListener("drop", function(e) { e = e || event; e.preventDefault(); var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) @@ -34,6 +37,14 @@ Metamaps.PasteInput = { self.handle(text, coords) } } + // OMG import bookmarks 😍 + if (e.dataTransfer.items.length > 0) { + e.dataTransfer.items[0].getAsString(function(text) { + if (text.match(self.URL_REGEX)) { + self.handle(text, coords) + } + }) + } }, false); // allow pasting onto canvas (but don't break existing inputs/textareas) @@ -48,10 +59,8 @@ Metamaps.PasteInput = { handle: function(text, coords) { var self = Metamaps.PasteInput - // thanks to https://github.com/kevva/url-regex - const URL_REGEX = new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$') - if (text.match(URL_REGEX)) { + if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) } else if (text[0] === '{') { self.handleJSON(text) From 1efd78ad7bbff6fcc761df3ee38b92e39b58eae7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 14:27:49 +0800 Subject: [PATCH 007/378] initial attempt at focussing input field when entering multiple topics --- .../javascripts/src/Metamaps.Backbone.js | 8 +-- app/assets/javascripts/src/Metamaps.Import.js | 9 ++- .../javascripts/src/Metamaps.PasteInput.js | 55 ++++++------------- app/assets/javascripts/src/Metamaps.Topic.js | 19 ++++--- .../javascripts/src/Metamaps.TopicCard.js | 12 ++-- 5 files changed, 48 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js index a37229d2..2c1f58af 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -63,7 +63,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ authorizeToEdit: function (mapper) { if (mapper && ( this.get('permission') === 'commons' || - this.get('collaborator_ids').includes(mapper.get('id')) || + (this.get('collaborator_ids') || []).includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) { return true } else { @@ -350,9 +350,9 @@ Metamaps.Backbone.init = function () { }, authorizeToEdit: function (mapper) { if (mapper && - (this.get('calculated_permission') === 'commons' || - this.get('collaborator_ids').includes(mapper.get('id')) || - this.get('user_id') === mapper.get('id'))) { + (this.get('user_id') === mapper.get('id') || + this.get('calculated_permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')))) { return true } else { return false diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index 2ed7e00a..2dee51d0 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -254,7 +254,7 @@ Metamaps.Import = { }, createTopicWithParameters: function (name, metacode_name, permission, desc, - link, xloc, yloc, import_id) { + link, xloc, yloc, import_id, opts) { var self = Metamaps.Import $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null @@ -271,7 +271,8 @@ Metamaps.Import = { permission: topic_permission, defer_to_map_id: defer_to_map_id, desc: desc || "", - link: link || "" + link: link || "", + calculated_permission: Metamaps.Active.Map.get('permission') }) Metamaps.Topics.add(topic) @@ -288,7 +289,9 @@ Metamaps.Import = { Metamaps.Mappings.add(mapping) // this function also includes the creation of the topic in the database - Metamaps.Topic.renderTopic(mapping, topic, true, true) + Metamaps.Topic.renderTopic(mapping, topic, true, true, { + success: opts.success + }) Metamaps.GlobalUI.hideDiv('#instructions') }, diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js index 55798587..aaf848d0 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -80,43 +80,24 @@ Metamaps.PasteInput = { var import_id = null // don't store a cidMapping var permission = null // use default - // try { - // // fetch title in 150ms or less - // Promise.race([ - // new Promise(function(resolve, reject) { - // fetch(text).then(function(response) { - // return response.text() - // }).then(function(html) { - // title = html.replace(/[\s\S]*<title>(.*)<\/title>[\s\S]*/m, '$1') - // resolve() - // }) - // }), new Promise(function(resolve, reject) { - // window.setTimeout(function() { - // resolve() - // }, 150) - // }) - // ]).then(function() { - // finish() - // }).catch(function(error) { - // throw error - // }) - // } catch (err) { - // console.warn("Your browser can't fetch the title") // TODO move to webpack to avoid this error - // } - finish() - - function finish() { - Metamaps.Import.createTopicWithParameters( - title, - 'Reference', // metacode - todo fix - permission, - text, // desc - todo load from url? - text, // link - todo fix because this isn't being POSTed - coords.x, - coords.y, - import_id - ) - } + Metamaps.Import.createTopicWithParameters( + title, + 'Reference', // metacode - todo fix + permission, + text, // desc - todo load from url? + text, // link - todo fix because this isn't being POSTed + coords.x, + coords.y, + import_id, + { + success: function(topic) { + Metamaps.TopicCard.showCard(topic.get('node'), function() { + $('#showcard #titleActivator').click() + .find('textarea, input').focus() + }) + } + } + ) }, handleJSON: function (text) { diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 52fabed3..9e6782cb 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -186,11 +186,10 @@ Metamaps.Topic = { error: function () {} }) }, - /* - * - * - */ - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter) { + + // opts is additional options in a hash + // TODO: move createNewInDB and permitCerateSYnapseAfter into opts + renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { var self = Metamaps.Topic var nodeOnViz, tempPos @@ -265,18 +264,24 @@ Metamaps.Topic = { }) } - var mappingSuccessCallback = function (mappingModel, response) { + var mappingSuccessCallback = function (mappingModel, response, topicModel) { var newTopicData = { mappingid: mappingModel.id, mappableid: mappingModel.get('mappable_id') } $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + // call a success callback if provided + if (opts.success) { + opts.success(topicModel) + } } var topicSuccessCallback = function (topicModel, response) { if (Metamaps.Active.Map) { mapping.save({ mappable_id: topicModel.id }, { - success: mappingSuccessCallback, + success: function (model, response) { + mappingSuccessCallback(model, response, topicModel) + }, error: function (model, response) { console.log('error saving mapping to database') } diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/app/assets/javascripts/src/Metamaps.TopicCard.js index f1424ed9..1453104d 100644 --- a/app/assets/javascripts/src/Metamaps.TopicCard.js +++ b/app/assets/javascripts/src/Metamaps.TopicCard.js @@ -37,7 +37,7 @@ Metamaps.TopicCard = { * Will open the Topic Card for the node that it's passed * @param {$jit.Graph.Node} node */ - showCard: function (node) { + showCard: function (node, opts) { var self = Metamaps.TopicCard var topic = node.getData('topic') @@ -46,7 +46,11 @@ Metamaps.TopicCard = { self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) // populate the card that's about to show with the right topics data self.populateShowCard(topic) - $('.showcard').fadeIn('fast') + return $('.showcard').fadeIn('fast', function() { + if (opts.complete) { + opts.complete() + } + }) }, hideCard: function () { var self = Metamaps.TopicCard @@ -413,8 +417,8 @@ Metamaps.TopicCard = { nodeValues.attachments = '' } - var inmapsAr = topic.get('inmaps') - var inmapsLinks = topic.get('inmapsLinks') + var inmapsAr = topic.get('inmaps') || [] + var inmapsLinks = topic.get('inmapsLinks') || [] nodeValues.inmaps = '' if (inmapsAr.length < 6) { for (i = 0; i < inmapsAr.length; i++) { From 3843cab6439d90a16e49418bea289f269cf42d7c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 01:22:40 +0800 Subject: [PATCH 008/378] rails 5 + api v2 + raml api docs (#593) * update Gemfile to rails 5 and ruby 2.3.0 * fiddle with javascripts and add sprockets manifest file * update config directory for rails 5 * fix some errors with controllers/serializers * fix travis and rspec * new serializers renamed to serializers * module Api::V1 * reusable embedding code * add index/collections/paging. overriding most of snorlax now |:) * raml api documentation + rspec tests to verify schemas/examples * add sorting by ?sort and searching by ?q. Add pagination Link headers * api v1 => v2 * fill out synapse api * alphabetize map policy * fix page thing * fill out maps api * formParameters => properties, and fiddle with map api * more raml 1.0 stuff i'm learning about * deprecate v1 api * rails 5 uses ApplicationRecord class for app-wide model config * Update topic spec for api v2 * workaround for user_preference.rb issue * get ready for token api docs. also TODO is mapping api docs * spec out mapping api * map/mapping/synapse spec, plus other bugs * awesome, token specs/apis are done * add sanity checks to the api tests * more cleanup * devise fix * fix starred map error --- Gemfile | 20 +- Gemfile.lock | 211 +++++++++--------- app/assets/config/manifest.js | 6 + app/controllers/api/mappings_controller.rb | 2 - app/controllers/api/maps_controller.rb | 2 - app/controllers/api/restful_controller.rb | 50 ----- app/controllers/api/synapses_controller.rb | 2 - app/controllers/api/tokens_controller.rb | 17 -- app/controllers/api/topics_controller.rb | 2 - .../api/v1/deprecated_controller.rb | 9 + app/controllers/api/v1/mappings_controller.rb | 6 + app/controllers/api/v1/maps_controller.rb | 6 + app/controllers/api/v1/synapses_controller.rb | 6 + app/controllers/api/v1/tokens_controller.rb | 6 + app/controllers/api/v1/topics_controller.rb | 6 + app/controllers/api/v2/mappings_controller.rb | 6 + app/controllers/api/v2/maps_controller.rb | 9 + app/controllers/api/v2/restful_controller.rb | 179 +++++++++++++++ app/controllers/api/v2/sessions_controller.rb | 20 ++ app/controllers/api/v2/synapses_controller.rb | 9 + app/controllers/api/v2/tokens_controller.rb | 11 + app/controllers/api/v2/topics_controller.rb | 6 + app/controllers/main_controller.rb | 4 +- app/controllers/maps_controller.rb | 2 +- .../users/registrations_controller.rb | 5 +- app/models/application_record.rb | 3 + app/models/event.rb | 2 +- app/models/in_metacode_set.rb | 2 +- app/models/map.rb | 2 +- app/models/mapping.rb | 2 +- app/models/message.rb | 2 +- app/models/metacode.rb | 2 +- app/models/metacode_set.rb | 2 +- app/models/synapse.rb | 2 +- app/models/token.rb | 2 +- app/models/topic.rb | 2 +- app/models/user.rb | 4 +- app/models/user_map.rb | 2 +- app/models/user_preference.rb | 10 +- app/models/webhook.rb | 2 +- app/policies/map_policy.rb | 58 ++--- app/policies/mapping_policy.rb | 8 +- app/policies/synapse_policy.rb | 4 + app/policies/topic_policy.rb | 4 + .../api/v2/application_serializer.rb | 29 +++ app/serializers/api/v2/event_serializer.rb | 18 ++ app/serializers/api/v2/map_serializer.rb | 28 +++ app/serializers/api/v2/mapping_serializer.rb | 25 +++ app/serializers/api/v2/metacode_serializer.rb | 11 + app/serializers/api/v2/synapse_serializer.rb | 24 ++ app/serializers/api/v2/token_serializer.rb | 10 + app/serializers/api/v2/topic_serializer.rb | 24 ++ app/serializers/api/v2/user_serializer.rb | 19 ++ app/serializers/api/v2/webhook_serializer.rb | 7 + app/serializers/event_serializer.rb | 15 -- app/serializers/new_map_serializer.rb | 16 -- app/serializers/new_mapping_serializer.rb | 19 -- app/serializers/new_metacode_serializer.rb | 7 - app/serializers/new_synapse_serializer.rb | 14 -- app/serializers/new_topic_serializer.rb | 13 -- app/serializers/new_user_serializer.rb | 15 -- app/serializers/token_serializer.rb | 7 - app/serializers/webhook_serializer.rb | 3 - config/application.rb | 23 +- config/boot.rb | 5 +- config/cable.yml | 9 + config/environment.rb | 8 +- config/environments/production.rb | 51 +---- config/environments/test.rb | 6 +- config/initializers/access_codes.rb | 2 +- .../initializers/active_model_serializers.rb | 1 + .../application_controller_renderer.rb | 6 + config/initializers/assets.rb | 11 + config/initializers/cookies_serializer.rb | 5 + .../initializers/filter_parameter_logging.rb | 4 + config/initializers/inflections.rb | 11 +- config/initializers/kaminari_config.rb | 10 + config/initializers/mime_types.rb | 1 - config/initializers/new_framework_defaults.rb | 24 ++ config/initializers/secret_token.rb | 2 +- config/initializers/session_store.rb | 4 +- config/initializers/wrap_parameters.rb | 8 +- config/puma.rb | 47 ++++ config/routes.rb | 27 ++- config/spring.rb | 7 + db/schema.rb | 92 +++----- doc/api/api.raml | 39 ++++ doc/api/apis/mappings.raml | 68 ++++++ doc/api/apis/maps.raml | 82 +++++++ doc/api/apis/synapses.raml | 82 +++++++ doc/api/apis/tokens.raml | 25 +++ doc/api/apis/topics.raml | 72 ++++++ doc/api/examples/map.json | 27 +++ doc/api/examples/mapping.json | 11 + doc/api/examples/mappings.json | 54 +++++ doc/api/examples/maps.json | 37 +++ doc/api/examples/synapse.json | 13 ++ doc/api/examples/synapses.json | 34 +++ doc/api/examples/token.json | 8 + doc/api/examples/tokens.json | 18 ++ doc/api/examples/topic.json | 13 ++ doc/api/examples/topics.json | 34 +++ doc/api/resourceTypes/base.raml | 35 +++ doc/api/resourceTypes/collection.raml | 22 ++ doc/api/resourceTypes/item.raml | 29 +++ doc/api/schemas/_datetimestamp.json | 4 + doc/api/schemas/_id.json | 4 + doc/api/schemas/_map.json | 67 ++++++ doc/api/schemas/_mapping.json | 41 ++++ doc/api/schemas/_page.json | 38 ++++ doc/api/schemas/_permission.json | 4 + doc/api/schemas/_synapse.json | 42 ++++ doc/api/schemas/_token.json | 24 ++ doc/api/schemas/_topic.json | 43 ++++ doc/api/schemas/map.json | 12 + doc/api/schemas/mapping.json | 12 + doc/api/schemas/mappings.json | 19 ++ doc/api/schemas/maps.json | 19 ++ doc/api/schemas/synapse.json | 12 + doc/api/schemas/synapses.json | 19 ++ doc/api/schemas/token.json | 12 + doc/api/schemas/tokens.json | 19 ++ doc/api/schemas/topic.json | 12 + doc/api/schemas/topics.json | 19 ++ doc/api/traits/orderable.raml | 3 + doc/api/traits/pageable.raml | 7 + doc/api/traits/searchable.raml | 4 + spec/api/v2/mappings_api_spec.rb | 59 +++++ spec/api/v2/maps_api_spec.rb | 59 +++++ spec/api/v2/synapses_api_spec.rb | 59 +++++ spec/api/v2/tokens_api_spec.rb | 44 ++++ spec/api/v2/topics_api_spec.rb | 59 +++++ spec/controllers/mappings_controller_spec.rb | 56 ++--- spec/controllers/maps_controller_spec.rb | 73 +++--- spec/controllers/metacodes_controller_spec.rb | 45 ++-- spec/controllers/synapses_controller_spec.rb | 53 +++-- spec/controllers/topics_controller_spec.rb | 58 ++--- spec/factories/maps.rb | 1 + spec/factories/synapses.rb | 1 + spec/factories/tokens.rb | 6 + spec/factories/topics.rb | 6 +- spec/mailers/map_mailer_spec.rb | 5 - spec/rails_helper.rb | 27 +-- spec/spec_helper.rb | 4 - spec/support/controller_helpers.rb | 23 +- spec/support/pundit.rb | 1 + spec/support/schema_matcher.rb | 35 ++- spec/support/simplecov.rb | 2 + 148 files changed, 2506 insertions(+), 694 deletions(-) create mode 100644 app/assets/config/manifest.js delete mode 100644 app/controllers/api/mappings_controller.rb delete mode 100644 app/controllers/api/maps_controller.rb delete mode 100644 app/controllers/api/restful_controller.rb delete mode 100644 app/controllers/api/synapses_controller.rb delete mode 100644 app/controllers/api/tokens_controller.rb delete mode 100644 app/controllers/api/topics_controller.rb create mode 100644 app/controllers/api/v1/deprecated_controller.rb create mode 100644 app/controllers/api/v1/mappings_controller.rb create mode 100644 app/controllers/api/v1/maps_controller.rb create mode 100644 app/controllers/api/v1/synapses_controller.rb create mode 100644 app/controllers/api/v1/tokens_controller.rb create mode 100644 app/controllers/api/v1/topics_controller.rb create mode 100644 app/controllers/api/v2/mappings_controller.rb create mode 100644 app/controllers/api/v2/maps_controller.rb create mode 100644 app/controllers/api/v2/restful_controller.rb create mode 100644 app/controllers/api/v2/sessions_controller.rb create mode 100644 app/controllers/api/v2/synapses_controller.rb create mode 100644 app/controllers/api/v2/tokens_controller.rb create mode 100644 app/controllers/api/v2/topics_controller.rb create mode 100644 app/models/application_record.rb create mode 100644 app/serializers/api/v2/application_serializer.rb create mode 100644 app/serializers/api/v2/event_serializer.rb create mode 100644 app/serializers/api/v2/map_serializer.rb create mode 100644 app/serializers/api/v2/mapping_serializer.rb create mode 100644 app/serializers/api/v2/metacode_serializer.rb create mode 100644 app/serializers/api/v2/synapse_serializer.rb create mode 100644 app/serializers/api/v2/token_serializer.rb create mode 100644 app/serializers/api/v2/topic_serializer.rb create mode 100644 app/serializers/api/v2/user_serializer.rb create mode 100644 app/serializers/api/v2/webhook_serializer.rb delete mode 100644 app/serializers/event_serializer.rb delete mode 100644 app/serializers/new_map_serializer.rb delete mode 100644 app/serializers/new_mapping_serializer.rb delete mode 100644 app/serializers/new_metacode_serializer.rb delete mode 100644 app/serializers/new_synapse_serializer.rb delete mode 100644 app/serializers/new_topic_serializer.rb delete mode 100644 app/serializers/new_user_serializer.rb delete mode 100644 app/serializers/token_serializer.rb delete mode 100644 app/serializers/webhook_serializer.rb create mode 100644 config/cable.yml create mode 100644 config/initializers/active_model_serializers.rb create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/kaminari_config.rb create mode 100644 config/initializers/new_framework_defaults.rb create mode 100644 config/puma.rb create mode 100644 config/spring.rb create mode 100644 doc/api/api.raml create mode 100644 doc/api/apis/mappings.raml create mode 100644 doc/api/apis/maps.raml create mode 100644 doc/api/apis/synapses.raml create mode 100644 doc/api/apis/tokens.raml create mode 100644 doc/api/apis/topics.raml create mode 100644 doc/api/examples/map.json create mode 100644 doc/api/examples/mapping.json create mode 100644 doc/api/examples/mappings.json create mode 100644 doc/api/examples/maps.json create mode 100644 doc/api/examples/synapse.json create mode 100644 doc/api/examples/synapses.json create mode 100644 doc/api/examples/token.json create mode 100644 doc/api/examples/tokens.json create mode 100644 doc/api/examples/topic.json create mode 100644 doc/api/examples/topics.json create mode 100644 doc/api/resourceTypes/base.raml create mode 100644 doc/api/resourceTypes/collection.raml create mode 100644 doc/api/resourceTypes/item.raml create mode 100644 doc/api/schemas/_datetimestamp.json create mode 100644 doc/api/schemas/_id.json create mode 100644 doc/api/schemas/_map.json create mode 100644 doc/api/schemas/_mapping.json create mode 100644 doc/api/schemas/_page.json create mode 100644 doc/api/schemas/_permission.json create mode 100644 doc/api/schemas/_synapse.json create mode 100644 doc/api/schemas/_token.json create mode 100644 doc/api/schemas/_topic.json create mode 100644 doc/api/schemas/map.json create mode 100644 doc/api/schemas/mapping.json create mode 100644 doc/api/schemas/mappings.json create mode 100644 doc/api/schemas/maps.json create mode 100644 doc/api/schemas/synapse.json create mode 100644 doc/api/schemas/synapses.json create mode 100644 doc/api/schemas/token.json create mode 100644 doc/api/schemas/tokens.json create mode 100644 doc/api/schemas/topic.json create mode 100644 doc/api/schemas/topics.json create mode 100644 doc/api/traits/orderable.raml create mode 100644 doc/api/traits/pageable.raml create mode 100644 doc/api/traits/searchable.raml create mode 100644 spec/api/v2/mappings_api_spec.rb create mode 100644 spec/api/v2/maps_api_spec.rb create mode 100644 spec/api/v2/synapses_api_spec.rb create mode 100644 spec/api/v2/tokens_api_spec.rb create mode 100644 spec/api/v2/topics_api_spec.rb create mode 100644 spec/factories/tokens.rb delete mode 100644 spec/mailers/map_mailer_spec.rb create mode 100644 spec/support/pundit.rb create mode 100644 spec/support/simplecov.rb diff --git a/Gemfile b/Gemfile index 6be2271b..4c58772c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,28 +1,27 @@ source 'https://rubygems.org' ruby '2.3.0' -gem 'rails' +gem 'rails', '~> 5.0.0' -gem 'active_model_serializers', '~> 0.8.1' +gem 'active_model_serializers' gem 'aws-sdk', '< 2.0' -gem 'best_in_place' # in-place editing -gem 'delayed_job', '~> 4.0.2' -gem 'delayed_job_active_record', '~> 4.0.1' +gem 'best_in_place' +gem 'delayed_job' +gem 'delayed_job_active_record' gem 'devise' -gem 'doorkeeper' +gem 'doorkeeper', '~> 4.0.0.rc4' gem 'dotenv-rails' gem 'exception_notification' gem 'formtastic' gem 'formula' gem 'httparty' gem 'json' -gem 'kaminari' # pagination -gem 'paperclip' +gem 'kaminari' +gem 'paperclip', '~> 4.3.6' gem 'pg' gem 'pundit' gem 'pundit_extra' gem 'rack-cors' -gem 'rails3-jquery-autocomplete' gem 'redis' gem 'slack-notifier' gem 'snorlax' @@ -31,12 +30,12 @@ gem 'uservoice-ruby' gem 'jquery-rails' gem 'jquery-ui-rails' gem 'jbuilder' +gem 'rails3-jquery-autocomplete' group :assets do gem 'coffee-rails' gem 'sass-rails' gem 'uglifier' - # gem 'therubyracer' end group :production do @@ -57,7 +56,6 @@ group :development, :test do gem 'binding_of_caller' gem 'pry-byebug' gem 'pry-rails' - gem 'quiet_assets' gem 'tunemygc' gem 'rubocop' end diff --git a/Gemfile.lock b/Gemfile.lock index c385bd81..c2fd0d28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,45 +1,50 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) + actioncable (5.0.0) + actionpack (= 5.0.0) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.6) - actionview (= 4.2.6) - activesupport (= 4.2.6) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.0.0) + actionview (= 5.0.0) + activesupport (= 5.0.0) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.6) - activesupport (= 4.2.6) + actionview (5.0.0) + activesupport (= 5.0.0) builder (~> 3.1) erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - active_model_serializers (0.8.3) - activemodel (>= 3.0) - activejob (4.2.6) - activesupport (= 4.2.6) - globalid (>= 0.3.0) - activemodel (4.2.6) - activesupport (= 4.2.6) - builder (~> 3.1) - activerecord (4.2.6) - activemodel (= 4.2.6) - activesupport (= 4.2.6) - arel (~> 6.0) - activesupport (4.2.6) + active_model_serializers (0.10.1) + actionpack (>= 4.1, < 6) + activemodel (>= 4.1, < 6) + jsonapi (~> 0.1.1.beta2) + railties (>= 4.1, < 6) + activejob (5.0.0) + activesupport (= 5.0.0) + globalid (>= 0.3.6) + activemodel (5.0.0) + activesupport (= 5.0.0) + activerecord (5.0.0) + activemodel (= 5.0.0) + activesupport (= 5.0.0) + arel (~> 7.0) + activesupport (5.0.0) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.3.8) - arel (6.0.3) + arel (7.1.1) ast (2.3.0) aws-sdk (1.66.0) aws-sdk-v1 (= 1.66.0) @@ -56,7 +61,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - brakeman (3.3.2) + brakeman (3.3.3) builder (3.2.2) byebug (9.0.5) climate_control (0.0.3) @@ -64,21 +69,21 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) - coffee-rails (4.1.1) + coffee-rails (4.2.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.1.x) + railties (>= 4.0.0, < 5.2.x) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.10.0) concurrent-ruby (1.0.2) debug_inspector (0.0.2) - delayed_job (4.0.6) - activesupport (>= 3.0, < 5.0) - delayed_job_active_record (4.0.3) - activerecord (>= 3.0, < 5.0) - delayed_job (>= 3.0, < 4.1) - devise (4.1.1) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) + devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.1) @@ -86,16 +91,16 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - doorkeeper (3.1.0) - railties (>= 3.2) + doorkeeper (4.0.0) + railties (>= 4.2) dotenv (2.1.1) dotenv-rails (2.1.1) dotenv (= 2.1.1) railties (>= 4.0, < 5.1) erubis (2.7.0) - exception_notification (4.1.4) - actionmailer (~> 4.0) - activesupport (~> 4.0) + exception_notification (4.2.1) + actionmailer (>= 4.0, < 6) + activesupport (>= 4.0, < 6) execjs (2.7.0) ezcrypto (0.7.2) factory_girl (4.7.0) @@ -107,13 +112,12 @@ GEM actionpack (>= 3.2.13) formula (1.1.1) rails (> 3.0.0) - globalid (0.3.6) + globalid (0.3.7) activesupport (>= 4.1.0) - httparty (0.13.7) - json (~> 1.8) + httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.7.0) - jbuilder (2.5.0) + jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) jquery-rails (4.1.1) @@ -125,6 +129,8 @@ GEM json (1.8.3) json-schema (2.6.2) addressable (~> 2.3.8) + jsonapi (0.1.1.beta2) + json (~> 1.8) kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -141,6 +147,7 @@ GEM minitest (5.9.0) multi_json (1.12.1) multi_xml (0.5.5) + nio4r (1.2.1) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) @@ -157,7 +164,7 @@ GEM pg (0.18.4) pkg-config (1.1.7) powerpack (0.1.1) - pry (0.10.3) + pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -169,29 +176,25 @@ GEM pundit (1.1.0) activesupport (>= 3.0.0) pundit_extra (0.2.0) - quiet_assets (1.1.0) - railties (>= 3.1, < 5.0) - rack (1.6.4) + rack (2.0.1) rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.6) - actionmailer (= 4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) - activemodel (= 4.2.6) - activerecord (= 4.2.6) - activesupport (= 4.2.6) + rails (5.0.0) + actioncable (= 5.0.0) + actionmailer (= 5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) + activemodel (= 5.0.0) + activerecord (= 5.0.0) + activesupport (= 5.0.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.6) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) - activesupport (>= 4.2.0.beta, < 5.0) + railties (= 5.0.0) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.1) + activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6.0) - rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails3-jquery-autocomplete (1.0.15) @@ -201,34 +204,35 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (4.2.6) - actionpack (= 4.2.6) - activesupport (= 4.2.6) + railties (5.0.0) + actionpack (= 5.0.0) + activesupport (= 5.0.0) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) rake (11.2.2) - redis (3.3.0) + redis (3.3.1) responders (2.2.0) railties (>= 4.2.0, < 5.1) - rspec-core (3.4.4) - rspec-support (~> 3.4.0) - rspec-expectations (3.4.0) + rspec-core (3.5.2) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-mocks (3.4.1) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-rails (3.4.2) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - rubocop (0.41.1) + rspec-support (~> 3.5.0) + rspec-rails (3.5.1) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) + rubocop (0.42.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -236,8 +240,8 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) sass (3.4.22) - sass-rails (5.0.4) - railties (>= 4.0.0, < 5.0) + sass-rails (5.0.5) + railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) @@ -253,20 +257,20 @@ GEM slop (3.6.0) snorlax (0.1.6) rails (> 4.1) - sprockets (3.6.0) + sprockets (3.6.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.0.4) + sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) thor (0.19.1) thread_safe (0.3.5) tilt (2.0.5) - tunemygc (1.0.65) + tunemygc (1.0.68) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.0) + uglifier (3.0.1) execjs (>= 0.3.0, < 3) unicode-display_width (1.1.0) uservoice-ruby (0.0.11) @@ -275,22 +279,25 @@ GEM oauth (>= 0.4.7) warden (1.2.6) rack (>= 1.0) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) PLATFORMS ruby DEPENDENCIES - active_model_serializers (~> 0.8.1) + active_model_serializers aws-sdk (< 2.0) best_in_place better_errors binding_of_caller brakeman coffee-rails - delayed_job (~> 4.0.2) - delayed_job_active_record (~> 4.0.1) + delayed_job + delayed_job_active_record devise - doorkeeper + doorkeeper (~> 4.0.0.rc4) dotenv-rails exception_notification factory_girl_rails @@ -303,15 +310,14 @@ DEPENDENCIES json json-schema kaminari - paperclip + paperclip (~> 4.3.6) pg pry-byebug pry-rails pundit pundit_extra - quiet_assets rack-cors - rails + rails (~> 5.0.0) rails3-jquery-autocomplete rails_12factor redis @@ -326,5 +332,8 @@ DEPENDENCIES uglifier uservoice-ruby +RUBY VERSION + ruby 2.3.0p0 + BUNDLED WITH - 1.11.2 + 1.12.5 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 00000000..72a7189c --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,6 @@ +// JS and CSS bundles +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css + +// Other +//= link_tree ../images diff --git a/app/controllers/api/mappings_controller.rb b/app/controllers/api/mappings_controller.rb deleted file mode 100644 index 15fde6bc..00000000 --- a/app/controllers/api/mappings_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::MappingsController < API::RestfulController -end diff --git a/app/controllers/api/maps_controller.rb b/app/controllers/api/maps_controller.rb deleted file mode 100644 index bb2d553d..00000000 --- a/app/controllers/api/maps_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::MapsController < API::RestfulController -end diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb deleted file mode 100644 index 5b6c41da..00000000 --- a/app/controllers/api/restful_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -class API::RestfulController < ActionController::Base - include Pundit - include PunditExtra - - snorlax_used_rest! - - load_and_authorize_resource only: [:show, :update, :destroy] - - def create - instantiate_resource - resource.user = current_user - authorize resource - create_action - respond_with_resource - end - - private - - def resource_serializer - "new_#{resource_name}_serializer".camelize.constantize - end - - def accessible_records - if current_user - visible_records - else - public_records - end - end - - def current_user - super || token_user || doorkeeper_user || nil - end - - def token_user - token = params[:access_token] - access_token = Token.find_by_token(token) - @token_user ||= access_token.user if access_token - end - - def doorkeeper_user - return unless doorkeeper_token.present? - doorkeeper_render_error unless valid_doorkeeper_token? - @doorkeeper_user ||= User.find(doorkeeper_token.resource_owner_id) - end - - def permitted_params - @permitted_params ||= PermittedParams.new(params) - end -end diff --git a/app/controllers/api/synapses_controller.rb b/app/controllers/api/synapses_controller.rb deleted file mode 100644 index 47cb6056..00000000 --- a/app/controllers/api/synapses_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::SynapsesController < API::RestfulController -end diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb deleted file mode 100644 index cea6ac5f..00000000 --- a/app/controllers/api/tokens_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Api::TokensController < API::RestfulController - def my_tokens - raise Pundit::NotAuthorizedError unless current_user - instantiate_collection page_collection: false, timeframe_collection: false - respond_with_collection - end - - private - - def resource_serializer - "#{resource_name}_serializer".camelize.constantize - end - - def visible_records - current_user.tokens - end -end diff --git a/app/controllers/api/topics_controller.rb b/app/controllers/api/topics_controller.rb deleted file mode 100644 index 4ccc619c..00000000 --- a/app/controllers/api/topics_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::TopicsController < API::RestfulController -end diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb new file mode 100644 index 00000000..ed68b897 --- /dev/null +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -0,0 +1,9 @@ +module Api + module V1 + class DeprecatedController < ApplicationController + def method_missing + render json: { error: "/api/v1 is deprecated! Please use /api/v2 instead." } + end + end + end +end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb new file mode 100644 index 00000000..35c7d6bd --- /dev/null +++ b/app/controllers/api/v1/mappings_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class MappingsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb new file mode 100644 index 00000000..056810f1 --- /dev/null +++ b/app/controllers/api/v1/maps_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class MapsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb new file mode 100644 index 00000000..e2111e95 --- /dev/null +++ b/app/controllers/api/v1/synapses_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class SynapsesController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb new file mode 100644 index 00000000..c96b1065 --- /dev/null +++ b/app/controllers/api/v1/tokens_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class TokensController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb new file mode 100644 index 00000000..e974fff3 --- /dev/null +++ b/app/controllers/api/v1/topics_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class TopicsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb new file mode 100644 index 00000000..7f0d9513 --- /dev/null +++ b/app/controllers/api/v2/mappings_controller.rb @@ -0,0 +1,6 @@ +module Api + module V2 + class MappingsController < RestfulController + end + end +end diff --git a/app/controllers/api/v2/maps_controller.rb b/app/controllers/api/v2/maps_controller.rb new file mode 100644 index 00000000..fd54fa7b --- /dev/null +++ b/app/controllers/api/v2/maps_controller.rb @@ -0,0 +1,9 @@ +module Api + module V2 + class MapsController < RestfulController + def searchable_columns + [:name, :desc] + end + end + end +end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb new file mode 100644 index 00000000..e73f21b8 --- /dev/null +++ b/app/controllers/api/v2/restful_controller.rb @@ -0,0 +1,179 @@ +module Api + module V2 + class RestfulController < ActionController::Base + include Pundit + include PunditExtra + + snorlax_used_rest! + + before_action :load_resource, only: [:show, :update, :destroy] + after_action :verify_authorized + + def index + authorize resource_class + instantiate_collection + respond_with_collection + end + + def create + instantiate_resource + resource.user = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + + def destroy + destroy_action + head :no_content + end + + private + + def accessible_records + if current_user + visible_records + else + public_records + end + end + + def current_user + super || token_user || doorkeeper_user || nil + end + + def load_resource + super + authorize resource + end + + def resource_serializer + "Api::V2::#{resource_name.camelize}Serializer".constantize + end + + def respond_with_resource(scope: default_scope, serializer: resource_serializer, root: serializer_root) + if resource.errors.empty? + render json: resource, scope: scope, serializer: serializer, root: root + else + respond_with_errors + end + end + + def respond_with_collection(resources: collection, scope: default_scope, serializer: resource_serializer, root: serializer_root) + render json: resources, scope: scope, each_serializer: serializer, root: root, meta: pagination(resources), meta_key: :page + end + + def default_scope + { + embeds: embeds + } + end + + def embeds + (params[:embed] || '').split(',').map(&:to_sym) + end + + def token_user + token = params[:access_token] + access_token = Token.find_by_token(token) + @token_user ||= access_token.user if access_token + end + + def doorkeeper_user + return unless doorkeeper_token.present? + doorkeeper_render_error unless valid_doorkeeper_token? + @doorkeeper_user ||= User.find(doorkeeper_token.resource_owner_id) + end + + def permitted_params + @permitted_params ||= PermittedParams.new(params) + end + + def serializer_root + 'data' + end + + def pagination(collection) + per = (params[:per] || 25).to_i + current_page = (params[:page] || 1).to_i + total_pages = (collection.total_count.to_f / per).ceil + prev_page = current_page > 1 ? current_page - 1 : 0 + next_page = current_page < total_pages ? current_page + 1 : 0 + + base_url = request.base_url + request.path + nxt = request.query_parameters.merge(page: next_page).map{|x| x.join('=')}.join('&') + prev = request.query_parameters.merge(page: prev_page).map{|x| x.join('=')}.join('&') + last = request.query_parameters.merge(page: total_pages).map{|x| x.join('=')}.join('&') + response.headers['Link'] = [ + %(<#{base_url}?#{nxt}>; rel="next"), + %(<#{base_url}?#{prev}>; rel="prev"), + %(<#{base_url}?#{last}>; rel="last") + ].join(',') + response.headers['X-Total-Pages'] = collection.total_pages.to_s + response.headers['X-Total-Count'] = collection.total_count.to_s + response.headers['X-Per-Page'] = per.to_s + + { + current_page: current_page, + next_page: next_page, + prev_page: prev_page, + total_pages: total_pages, + total_count: collection.total_count, + per: per + } + end + + def instantiate_collection + collection = accessible_records + collection = yield collection if block_given? + collection = search_by_q(collection) if params[:q] + collection = order_by_sort(collection) if params[:sort] + collection = collection.page(params[:page]).per(params[:per]) + self.collection = collection + end + + # override this method to explicitly set searchable columns + def searchable_columns + columns = resource_class.columns.select do |column| + column.type == :text || column.type == :string + end + columns.map(&:name) + end + + # thanks to http://stackoverflow.com/questions/4430578 + def search_by_q(collection) + table = resource_class.arel_table + safe_query = "%#{params[:q].gsub(/[%_]/, '\\\\\0')}%" + search_column = -> (column) { table[column].matches(safe_query) } + + condition = searchable_columns.reduce(nil) do |prev, column| + next search_column.(column) if prev.nil? + search_column.(column).or(prev) + end + puts collection.where(condition).to_sql + collection.where(condition) + end + + def order_by_sort(collection) + builder = collection + sorts = params[:sort].split(',') + sorts.each do |sort| + direction = sort.starts_with?('-') ? 'desc' : 'asc' + sort = sort.sub(/^-/, '') + if resource_class.columns.map(&:name).include?(sort) + builder = builder.order(sort => direction) + end + end + return builder + end + + def visible_records + policy_scope(resource_class) + end + + def public_records + policy_scope(resource_class) + end + end + end +end diff --git a/app/controllers/api/v2/sessions_controller.rb b/app/controllers/api/v2/sessions_controller.rb new file mode 100644 index 00000000..3aefa214 --- /dev/null +++ b/app/controllers/api/v2/sessions_controller.rb @@ -0,0 +1,20 @@ +module Api + module V2 + class SessionsController < ApplicationController + def create + @user = User.find_by(email: params[:email]) + if @user && @user.valid_password(params[:password]) + sign_in(@user) + render json: @user + else + render json: { error: 'Error' } + end + end + + def destroy + sign_out + head :no_content + end + end + end +end diff --git a/app/controllers/api/v2/synapses_controller.rb b/app/controllers/api/v2/synapses_controller.rb new file mode 100644 index 00000000..6572997d --- /dev/null +++ b/app/controllers/api/v2/synapses_controller.rb @@ -0,0 +1,9 @@ +module Api + module V2 + class SynapsesController < RestfulController + def searchable_columns + [:desc] + end + end + end +end diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb new file mode 100644 index 00000000..6eeb102b --- /dev/null +++ b/app/controllers/api/v2/tokens_controller.rb @@ -0,0 +1,11 @@ +module Api + module V2 + class TokensController < RestfulController + def my_tokens + authorize resource_class + instantiate_collection + respond_with_collection + end + end + end +end diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb new file mode 100644 index 00000000..74fa7105 --- /dev/null +++ b/app/controllers/api/v2/topics_controller.rb @@ -0,0 +1,6 @@ +module Api + module V2 + class TopicsController < RestfulController + end + end +end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 1164d42e..01304328 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -163,8 +163,8 @@ class MainController < ApplicationController @synapses = [] end - # limit to 5 results - @synapses = @synapses.slice(0, 5) + #limit to 5 results + @synapses = @synapses.to_a.slice(0,5) render json: autocomplete_synapse_array_json(@synapses) end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index ee3e6549..7c4a74a7 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,6 +1,6 @@ class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps, :events] + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] respond_to :html, :json, :csv diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index efd6b42d..8895cfd2 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -15,11 +15,10 @@ class Users::RegistrationsController < Devise::RegistrationsController private def configure_sign_up_params - devise_parameter_sanitizer.for(:sign_up) << [:name, :joinedwithcode] + devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode]) end def configure_account_update_params - puts devise_parameter_sanitizer_for(:account_update) - devise_parameter_sanitizer.for(:account_update) << [:image] + devise_parameter_sanitizer.permit(:account_update, keys: [:image]) end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..10a4cba8 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/event.rb b/app/models/event.rb index 67606aa2..90407314 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,4 +1,4 @@ -class Event < ActiveRecord::Base +class Event < ApplicationRecord KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze # has_many :notifications, dependent: :destroy diff --git a/app/models/in_metacode_set.rb b/app/models/in_metacode_set.rb index c1b1ca33..de1f2514 100644 --- a/app/models/in_metacode_set.rb +++ b/app/models/in_metacode_set.rb @@ -1,4 +1,4 @@ -class InMetacodeSet < ActiveRecord::Base +class InMetacodeSet < ApplicationRecord belongs_to :metacode, class_name: 'Metacode', foreign_key: 'metacode_id' belongs_to :metacode_set, class_name: 'MetacodeSet', foreign_key: 'metacode_set_id' end diff --git a/app/models/map.rb b/app/models/map.rb index bf5757d3..f59eb790 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,4 +1,4 @@ -class Map < ActiveRecord::Base +class Map < ApplicationRecord belongs_to :user has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy diff --git a/app/models/mapping.rb b/app/models/mapping.rb index ceb15538..eba7a6d2 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,4 +1,4 @@ -class Mapping < ActiveRecord::Base +class Mapping < ApplicationRecord scope :topicmapping, -> { where(mappable_type: :Topic) } scope :synapsemapping, -> { where(mappable_type: :Synapse) } diff --git a/app/models/message.rb b/app/models/message.rb index 597caeb7..348c5d4e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,4 +1,4 @@ -class Message < ActiveRecord::Base +class Message < ApplicationRecord belongs_to :user belongs_to :resource, polymorphic: true diff --git a/app/models/metacode.rb b/app/models/metacode.rb index bc0bef7b..9b05bee5 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -1,4 +1,4 @@ -class Metacode < ActiveRecord::Base +class Metacode < ApplicationRecord has_many :in_metacode_sets has_many :metacode_sets, through: :in_metacode_sets has_many :topics diff --git a/app/models/metacode_set.rb b/app/models/metacode_set.rb index cc672784..c52811fd 100644 --- a/app/models/metacode_set.rb +++ b/app/models/metacode_set.rb @@ -1,4 +1,4 @@ -class MetacodeSet < ActiveRecord::Base +class MetacodeSet < ApplicationRecord belongs_to :user has_many :in_metacode_sets has_many :metacodes, through: :in_metacode_sets diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 710cb029..afd40a25 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,4 +1,4 @@ -class Synapse < ActiveRecord::Base +class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' diff --git a/app/models/token.rb b/app/models/token.rb index 1dac3fde..9103aebc 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,4 +1,4 @@ -class Token < ActiveRecord::Base +class Token < ApplicationRecord belongs_to :user before_create :assign_token diff --git a/app/models/topic.rb b/app/models/topic.rb index a91c75fc..c250338b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,4 +1,4 @@ -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord include TopicsHelper belongs_to :user diff --git a/app/models/user.rb b/app/models/user.rb index 1f091499..876e10cd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ require 'open-uri' -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :topics has_many :synapses has_many :maps @@ -80,7 +80,7 @@ class User < ActiveRecord::Base end def starred_map?(map) - return !!self.stars.index{|s| s.map_id == map.id } + return self.stars.where(map_id: map.id).exists? end def settings diff --git a/app/models/user_map.rb b/app/models/user_map.rb index 5e91ecc2..c48cfb96 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -1,4 +1,4 @@ -class UserMap < ActiveRecord::Base +class UserMap < ApplicationRecord belongs_to :map belongs_to :user end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index a87dc679..3aadbdb3 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -4,8 +4,14 @@ class UserPreference def initialize array = [] %w(Action Aim Idea Question Note Wildcard Subject).each do |m| - metacode = Metacode.find_by_name(m) - array.push(metacode.id.to_s) if metacode + begin + metacode = Metacode.find_by_name(m) + array.push(metacode.id.to_s) if metacode + rescue ActiveRecord::StatementInvalid + if m == 'Action' + Rails.logger.warn("TODO: remove this travis workaround in user_preference.rb") + end + end end @metacodes = array end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 86d2333d..6389398e 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,4 +1,4 @@ -class Webhook < ActiveRecord::Base +class Webhook < ApplicationRecord belongs_to :hookable, polymorphic: true validates :uri, presence: true diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index bf511869..0a2b33ce 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -12,19 +12,7 @@ class MapPolicy < ApplicationPolicy end end - def activemaps? - user.blank? # redirect to root url if authenticated for some reason - end - - def featuredmaps? - true - end - - def mymaps? - user.present? - end - - def usermaps? + def index? true end @@ -32,18 +20,6 @@ class MapPolicy < ApplicationPolicy record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user end - def export? - show? - end - - def events? - show? - end - - def contains? - show? - end - def create? user.present? end @@ -52,11 +28,39 @@ class MapPolicy < ApplicationPolicy user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) end + def destroy? + record.user == user || admin_override + end + def access? # note that this is to edit access user.present? && record.user == user end + def activemaps? + user.blank? # redirect to root url if authenticated for some reason + end + + def contains? + show? + end + + def events? + show? + end + + def export? + show? + end + + def featuredmaps? + true + end + + def mymaps? + user.present? + end + def star? unstar? end @@ -69,7 +73,7 @@ class MapPolicy < ApplicationPolicy update? end - def destroy? - record.user == user || admin_override + def usermaps? + true end end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 07b6d0c5..1cd99783 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -8,13 +8,17 @@ class MappingPolicy < ApplicationPolicy visible = %w(public commons) permission = 'maps.permission IN (?)' if user - scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) + scope.joins(:map).where(permission, visible).or(scope.joins(:map).where(user_id: user.id)) else - scope.where(permission, visible) + scope.joins(:map).where(permission, visible) end end end + def index? + true + end + def show? map_policy.show? && mappable_policy.try(:show?) end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 97d993f5..310b3947 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -11,6 +11,10 @@ class SynapsePolicy < ApplicationPolicy end end + def index? + true # really only for the API. should be policy scoped! + end + def create? user.present? # TODO: add validation against whether you can see both topics diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index a8d5df2a..7bca6770 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -11,6 +11,10 @@ class TopicPolicy < ApplicationPolicy end end + def index? + user.present? + end + def create? user.present? end diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb new file mode 100644 index 00000000..f943646c --- /dev/null +++ b/app/serializers/api/v2/application_serializer.rb @@ -0,0 +1,29 @@ +module Api + module V2 + class ApplicationSerializer < ActiveModel::Serializer + def self.embeddable + {} + end + + def embeds + @embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) } + end + + def self.embed_dat + embeddable.each_pair do |key, opts| + attr = opts.delete(:attr) || key + if attr.to_s.pluralize == attr.to_s + attribute "#{attr.to_s.singularize}_ids".to_sym, opts.merge(unless: -> { embeds.include?(key) }) do + object.send(attr).map(&:id) + end + has_many attr, opts.merge(if: -> { embeds.include?(key) }) + else + id_opts = opts.merge(key: "#{key}_id") + attribute "#{attr}_id".to_sym, id_opts.merge(unless: -> { embeds.include?(key) }) + attribute key, opts.merge(if: -> { embeds.include?(key) }) + end + end + end + end + end +end diff --git a/app/serializers/api/v2/event_serializer.rb b/app/serializers/api/v2/event_serializer.rb new file mode 100644 index 00000000..644598cf --- /dev/null +++ b/app/serializers/api/v2/event_serializer.rb @@ -0,0 +1,18 @@ +module Api + module V2 + class EventSerializer < ApplicationSerializer + attributes :id, :sequence_id, :kind, :map_id, :created_at + + has_one :actor, serializer: UserSerializer, root: 'users' + has_one :map, serializer: MapSerializer + + def actor + object.user || object.eventable.try(:user) + end + + def map + object.eventable.try(:map) || object.eventable.map + end + end + end +end diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb new file mode 100644 index 00000000..438f97ee --- /dev/null +++ b/app/serializers/api/v2/map_serializer.rb @@ -0,0 +1,28 @@ +module Api + module V2 + class MapSerializer < ApplicationSerializer + attributes :id, + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at + + def self.embeddable + { + user: {}, + topics: {}, + synapses: {}, + mappings: {}, + contributors: { serializer: UserSerializer }, + collaborators: { serializer: UserSerializer } + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb new file mode 100644 index 00000000..dc36421e --- /dev/null +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -0,0 +1,25 @@ +module Api + module V2 + class MappingSerializer < ApplicationSerializer + attributes :id, + :created_at, + :updated_at, + :mappable_id, + :mappable_type + + attribute :xloc, if: -> { object.mappable_type == 'Topic' } + attribute :yloc, if: -> { object.mappable_type == 'Topic' } + + def self.embeddable + { + user: {}, + map: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb new file mode 100644 index 00000000..4f4daa35 --- /dev/null +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -0,0 +1,11 @@ +module Api + module V2 + class MetacodeSerializer < ApplicationSerializer + attributes :id, + :name, + :manual_icon, + :color, + :aws_icon + end + end +end diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb new file mode 100644 index 00000000..9ef86660 --- /dev/null +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -0,0 +1,24 @@ +module Api + module V2 + class SynapseSerializer < ApplicationSerializer + attributes :id, + :desc, + :category, + :permission, + :created_at, + :updated_at + + def self.embeddable + { + topic1: { attr: :node1, serializer: TopicSerializer }, + topic2: { attr: :node2, serializer: TopicSerializer }, + user: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/token_serializer.rb b/app/serializers/api/v2/token_serializer.rb new file mode 100644 index 00000000..18d15d15 --- /dev/null +++ b/app/serializers/api/v2/token_serializer.rb @@ -0,0 +1,10 @@ +module Api + module V2 + class TokenSerializer < ApplicationSerializer + attributes :id, + :token, + :description, + :created_at + end + end +end diff --git a/app/serializers/api/v2/topic_serializer.rb b/app/serializers/api/v2/topic_serializer.rb new file mode 100644 index 00000000..48d1d6de --- /dev/null +++ b/app/serializers/api/v2/topic_serializer.rb @@ -0,0 +1,24 @@ +module Api + module V2 + class TopicSerializer < ApplicationSerializer + attributes :id, + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at + + def self.embeddable + { + user: {}, + metacode: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb new file mode 100644 index 00000000..fdfffae0 --- /dev/null +++ b/app/serializers/api/v2/user_serializer.rb @@ -0,0 +1,19 @@ +module Api + module V2 + class UserSerializer < ApplicationSerializer + attributes :id, + :name, + :avatar, + :is_admin, + :generation + + def avatar + object.image.url(:sixtyfour) + end + + def is_admin + object.admin + end + end + end +end diff --git a/app/serializers/api/v2/webhook_serializer.rb b/app/serializers/api/v2/webhook_serializer.rb new file mode 100644 index 00000000..59d60283 --- /dev/null +++ b/app/serializers/api/v2/webhook_serializer.rb @@ -0,0 +1,7 @@ +module Api + module V2 + class WebhookSerializer < ApplicationSerializer + attributes :text, :username, :icon_url # , :attachments + end + end +end diff --git a/app/serializers/event_serializer.rb b/app/serializers/event_serializer.rb deleted file mode 100644 index 0e87cd44..00000000 --- a/app/serializers/event_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class EventSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, :sequence_id, :kind, :map_id, :created_at - - has_one :actor, serializer: NewUserSerializer, root: 'users' - has_one :map, serializer: NewMapSerializer - - def actor - object.user || object.eventable.try(:user) - end - - def map - object.eventable.try(:map) || object.eventable.map - end -end diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb deleted file mode 100644 index c323b09d..00000000 --- a/app/serializers/new_map_serializer.rb +++ /dev/null @@ -1,16 +0,0 @@ -class NewMapSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :name, - :desc, - :permission, - :screenshot, - :created_at, - :updated_at - - has_many :topics, serializer: NewTopicSerializer - has_many :synapses, serializer: NewSynapseSerializer - has_many :mappings, serializer: NewMappingSerializer - has_many :contributors, root: :users, serializer: NewUserSerializer - has_many :collaborators, root: :users, serializer: NewUserSerializer -end diff --git a/app/serializers/new_mapping_serializer.rb b/app/serializers/new_mapping_serializer.rb deleted file mode 100644 index 3ef9a8b6..00000000 --- a/app/serializers/new_mapping_serializer.rb +++ /dev/null @@ -1,19 +0,0 @@ -class NewMappingSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :xloc, - :yloc, - :created_at, - :updated_at, - :mappable_id, - :mappable_type - - has_one :user, serializer: NewUserSerializer - has_one :map, serializer: NewMapSerializer - - def filter(keys) - keys.delete(:xloc) unless object.mappable_type == 'Topic' - keys.delete(:yloc) unless object.mappable_type == 'Topic' - keys - end -end diff --git a/app/serializers/new_metacode_serializer.rb b/app/serializers/new_metacode_serializer.rb deleted file mode 100644 index b20f25b6..00000000 --- a/app/serializers/new_metacode_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class NewMetacodeSerializer < ActiveModel::Serializer - attributes :id, - :name, - :manual_icon, - :color, - :aws_icon -end diff --git a/app/serializers/new_synapse_serializer.rb b/app/serializers/new_synapse_serializer.rb deleted file mode 100644 index 5cdf644d..00000000 --- a/app/serializers/new_synapse_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -class NewSynapseSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :desc, - :category, - :weight, - :permission, - :created_at, - :updated_at - - has_one :topic1, root: :topics, serializer: NewTopicSerializer - has_one :topic2, root: :topics, serializer: NewTopicSerializer - has_one :user, serializer: NewUserSerializer -end diff --git a/app/serializers/new_topic_serializer.rb b/app/serializers/new_topic_serializer.rb deleted file mode 100644 index 2eb718df..00000000 --- a/app/serializers/new_topic_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -class NewTopicSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :name, - :desc, - :link, - :permission, - :created_at, - :updated_at - - has_one :user, serializer: NewUserSerializer - has_one :metacode, serializer: NewMetacodeSerializer -end diff --git a/app/serializers/new_user_serializer.rb b/app/serializers/new_user_serializer.rb deleted file mode 100644 index 62796f31..00000000 --- a/app/serializers/new_user_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class NewUserSerializer < ActiveModel::Serializer - attributes :id, - :name, - :avatar, - :is_admin, - :generation - - def avatar - object.image.url(:sixtyfour) - end - - def is_admin - object.admin - end -end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb deleted file mode 100644 index 4d593c0e..00000000 --- a/app/serializers/token_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class TokenSerializer < ActiveModel::Serializer - attributes :id, - :token, - :description, - :created_at, - :updated_at -end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb deleted file mode 100644 index 8108c86c..00000000 --- a/app/serializers/webhook_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class WebhookSerializer < ActiveModel::Serializer - attributes :text, :username, :icon_url # , :attachments -end diff --git a/config/application.rb b/config/application.rb index afebfd6d..b80306c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' require 'csv' require 'rails/all' @@ -15,21 +15,6 @@ module Metamaps # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths << Rails.root.join('app', 'services') - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - - # Activate observers that should always be running. - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - # Configure the default encoding used in templates for Ruby 1.9. config.encoding = 'utf-8' @@ -43,11 +28,6 @@ module Metamaps # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] - # Use SQL instead of Active Record's schema dumper when creating the database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - # Enable the asset pipeline config.assets.initialize_on_precompile = false @@ -57,7 +37,6 @@ module Metamaps config.generators do |g| g.test_framework :rspec end - config.active_record.raise_in_transactional_callbacks = true # pundit errors return 403 FORBIDDEN config.action_dispatch.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden diff --git a/config/boot.rb b/config/boot.rb index 4add3ee3..e49b6649 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,5 +1,6 @@ require 'rubygems' require 'rails/commands/server' + module Rails class Server def default_options @@ -9,6 +10,6 @@ module Rails end # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..0bbde6f7 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,9 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/config/environment.rb b/config/environment.rb index 6e9ad9e4..426333bb 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ -# Load the rails application -require File.expand_path('../application', __FILE__) +# Load the Rails application. +require_relative 'application' -# Initialize the rails application -Metamaps::Application.initialize! +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/production.rb b/config/environments/production.rb index 5ad26ad3..24ceed21 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,9 +1,8 @@ -Metamaps::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb config.log_level = :warn config.eager_load = true - config.assets.js_compressor = :uglifier # Code is not reloaded between requests config.cache_classes = true @@ -13,12 +12,12 @@ Metamaps::Application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_files = true + config.public_file_server.enabled = false + # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false - # Compress JavaScripts and CSS - config.assets.compress = true + config.assets.js_compressor = :uglifier # S3 file storage config.paperclip_defaults = { @@ -37,7 +36,6 @@ Metamaps::Application.configure do port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USERNAME'], password: ENV['SMTP_PASSWORD'], - # domain: ENV['SMTP_DOMAIN'] authentication: 'plain', enable_starttls_auto: true, openssl_verify_mode: 'none' @@ -46,54 +44,13 @@ Metamaps::Application.configure do # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - # Generate digests for assets URLs config.assets.digest = true - # Defaults to Rails.root.join("public/assets") - # config.assets.manifest = YOUR_PATH - - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # See everything in the log (default is :info) - # config.log_level = :debug - - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile += %w( ) - - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - - # Enable threaded mode - # config.threadsafe! - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify - - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - # config.active_record.auto_explain_threshold_in_seconds = 0.5 end diff --git a/config/environments/test.rb b/config/environments/test.rb index 4980b34c..dac060f1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -10,8 +10,10 @@ Metamaps::Application.configure do config.cache_classes = true # Configure static asset server for tests with Cache-Control for performance - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } # Show full error reports and disable caching config.consider_all_requests_local = true diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 66ace9a1..4a220c97 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,4 +1,4 @@ $codes = [] -if ActiveRecord::Base.connection.table_exists? 'users' +if ActiveRecord::Base.connection.data_source_exists? 'users' $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } end diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb new file mode 100644 index 00000000..aba3586b --- /dev/null +++ b/config/initializers/active_model_serializers.rb @@ -0,0 +1 @@ +ActiveModelSerializers.config.adapter = :json diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..51639b67 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index b84c4e25..31897cf4 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '2.0' +Rails.application.config.assets.quiet = true + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. Rails.application.config.assets.precompile += %w( webpacked/metamaps.bundle.js ) diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000..f51a497e --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..4a994e1e --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 5d8d9be2..ac033bf9 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,15 +1,16 @@ # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end -# + # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 00000000..b1d87b01 --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,10 @@ +Kaminari.configure do |config| + # config.default_per_page = 25 + # config.max_per_page = nil + # config.window = 4 + # config.outer_window = 0 + # config.left = 0 + # config.right = 0 + # config.page_method_name = :page + # config.param_name = :page +end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 54890c6e..c7b0c86d 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,6 +2,5 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone Mime::Type.register 'application/xls', :xls diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 00000000..0706cafd --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,24 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Rails 5.0 release notes for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = true + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = false + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 83877c08..e7f18911 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Metamaps::Application.config.secret_key_base = ENV['SECRET_KEY_BASE'] +Rails.application.config.secret_key_base = ENV['SECRET_KEY_BASE'] diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 757d66cc..d2dc13b6 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,8 @@ # Be sure to restart your server when you modify this file. -Metamaps::Application.config.session_store :cookie_store, key: '_Metamaps_session' +Rails.application.config.session_store :cookie_store, key: '_Metamaps_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") -# Metamaps::Application.config.session_store :active_record_store +# Rails.application.config.session_store :active_record_store diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 999df201..36bb3e27 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -8,7 +8,7 @@ ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end -# Disable root element in JSON by default. -ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false -end +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..c7f311f8 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb index 83f03051..38fe274e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,13 +9,26 @@ Metamaps::Application.routes.draw do get 'search/mappers', to: 'main#searchmappers', as: :searchmappers get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses - namespace :api, path: '/api/v1', defaults: { format: :json } do - resources :maps, only: [:create, :show, :update, :destroy] - resources :synapses, only: [:create, :show, :update, :destroy] - resources :topics, only: [:create, :show, :update, :destroy] - resources :mappings, only: [:create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection + namespace :api, path: '/api', default: { format: :json } do + namespace :v2, path: '/v2' do + resources :maps, only: [:index, :create, :show, :update, :destroy] + resources :synapses, only: [:index, :create, :show, :update, :destroy] + resources :topics, only: [:index, :create, :show, :update, :destroy] + resources :mappings, only: [:index, :create, :show, :update, :destroy] + resources :tokens, only: [:create, :destroy] do + get :my_tokens, on: :collection + end + end + namespace :v1, path: '/v1' do + # api v1 routes all lead to a deprecation error method + # see app/controllers/api/v1/deprecated_controller.rb + resources :maps, only: [:create, :show, :update, :destroy] + resources :synapses, only: [:create, :show, :update, :destroy] + resources :topics, only: [:create, :show, :update, :destroy] + resources :mappings, only: [:create, :show, :update, :destroy] + resources :tokens, only: [:create, :destroy] do + get :my_tokens, on: :collection + end end end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..be72de67 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,7 @@ +%w( + .ruby-version + .ruby-gemset + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/db/schema.rb b/db/schema.rb index 6b586a0d..ea06b679 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -28,10 +27,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "queue" t.datetime "created_at" t.datetime "updated_at" + t.index ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree end - add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree - create_table "events", force: :cascade do |t| t.string "kind", limit: 255 t.integer "eventable_id" @@ -41,24 +39,22 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "sequence_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree + t.index ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree + t.index ["map_id"], name: "index_events_on_map_id", using: :btree + t.index ["sequence_id"], name: "index_events_on_sequence_id", using: :btree + t.index ["user_id"], name: "index_events_on_user_id", using: :btree end - add_index "events", ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree - add_index "events", ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree - add_index "events", ["map_id"], name: "index_events_on_map_id", using: :btree - add_index "events", ["sequence_id"], name: "index_events_on_sequence_id", using: :btree - add_index "events", ["user_id"], name: "index_events_on_user_id", using: :btree - create_table "in_metacode_sets", force: :cascade do |t| t.integer "metacode_id" t.integer "metacode_set_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["metacode_id"], name: "index_in_metacode_sets_on_metacode_id", using: :btree + t.index ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree end - add_index "in_metacode_sets", ["metacode_id"], name: "index_in_metacode_sets_on_metacode_id", using: :btree - add_index "in_metacode_sets", ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree - create_table "mappings", force: :cascade do |t| t.text "category" t.integer "xloc" @@ -71,14 +67,13 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "updated_at", null: false t.integer "mappable_id" t.string "mappable_type" + t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree + t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree + t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree + t.index ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree + t.index ["user_id"], name: "index_mappings_on_user_id", using: :btree end - add_index "mappings", ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree - add_index "mappings", ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree - add_index "mappings", ["map_id"], name: "index_mappings_on_map_id", using: :btree - add_index "mappings", ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree - add_index "mappings", ["user_id"], name: "index_mappings_on_user_id", using: :btree - create_table "maps", force: :cascade do |t| t.text "name" t.boolean "arranged" @@ -92,10 +87,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "screenshot_content_type" t.integer "screenshot_file_size" t.datetime "screenshot_updated_at" + t.index ["user_id"], name: "index_maps_on_user_id", using: :btree end - add_index "maps", ["user_id"], name: "index_maps_on_user_id", using: :btree - create_table "messages", force: :cascade do |t| t.text "message" t.integer "user_id" @@ -103,12 +97,11 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "resource_type" t.datetime "created_at" t.datetime "updated_at" + t.index ["resource_id"], name: "index_messages_on_resource_id", using: :btree + t.index ["resource_type"], name: "index_messages_on_resource_type", using: :btree + t.index ["user_id"], name: "index_messages_on_user_id", using: :btree end - add_index "messages", ["resource_id"], name: "index_messages_on_resource_id", using: :btree - add_index "messages", ["resource_type"], name: "index_messages_on_resource_type", using: :btree - add_index "messages", ["user_id"], name: "index_messages_on_user_id", using: :btree - create_table "metacode_sets", force: :cascade do |t| t.string "name" t.text "desc" @@ -116,10 +109,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.boolean "mapperContributed" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree end - add_index "metacode_sets", ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree - create_table "metacodes", force: :cascade do |t| t.text "name" t.string "manual_icon" @@ -141,10 +133,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "created_at", null: false t.datetime "revoked_at" t.string "scopes" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree end - add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree - create_table "oauth_access_tokens", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" @@ -154,12 +145,11 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "revoked_at" t.datetime "created_at", null: false t.string "scopes" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree end - add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree - add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree - add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree - create_table "oauth_applications", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false @@ -168,20 +158,18 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "scopes", default: "", null: false t.datetime "created_at" t.datetime "updated_at" + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree end - add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree - create_table "stars", force: :cascade do |t| t.integer "user_id" t.integer "map_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["map_id"], name: "index_stars_on_map_id", using: :btree + t.index ["user_id"], name: "index_stars_on_user_id", using: :btree end - add_index "stars", ["map_id"], name: "index_stars_on_map_id", using: :btree - add_index "stars", ["user_id"], name: "index_stars_on_user_id", using: :btree - create_table "synapses", force: :cascade do |t| t.text "desc" t.text "category" @@ -193,24 +181,22 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "defer_to_map_id" + t.index ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree + t.index ["node1_id"], name: "index_synapses_on_node1_id", using: :btree + t.index ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree + t.index ["node2_id"], name: "index_synapses_on_node2_id", using: :btree + t.index ["user_id"], name: "index_synapses_on_user_id", using: :btree end - add_index "synapses", ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree - add_index "synapses", ["node1_id"], name: "index_synapses_on_node1_id", using: :btree - add_index "synapses", ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree - add_index "synapses", ["node2_id"], name: "index_synapses_on_node2_id", using: :btree - add_index "synapses", ["user_id"], name: "index_synapses_on_user_id", using: :btree - create_table "tokens", force: :cascade do |t| t.string "token" t.string "description" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_tokens_on_user_id", using: :btree end - add_index "tokens", ["user_id"], name: "index_tokens_on_user_id", using: :btree - create_table "topics", force: :cascade do |t| t.text "name" t.text "desc" @@ -229,21 +215,19 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "audio_file_size" t.datetime "audio_updated_at" t.integer "defer_to_map_id" + t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree + t.index ["user_id"], name: "index_topics_on_user_id", using: :btree end - add_index "topics", ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree - add_index "topics", ["user_id"], name: "index_topics_on_user_id", using: :btree - create_table "user_maps", force: :cascade do |t| t.integer "user_id" t.integer "map_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["map_id"], name: "index_user_maps_on_map_id", using: :btree + t.index ["user_id"], name: "index_user_maps_on_user_id", using: :btree end - add_index "user_maps", ["map_id"], name: "index_user_maps_on_map_id", using: :btree - add_index "user_maps", ["user_id"], name: "index_user_maps_on_user_id", using: :btree - create_table "users", force: :cascade do |t| t.string "name" t.string "email" @@ -272,19 +256,17 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "image_file_size" t.datetime "image_updated_at" t.integer "generation" + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - create_table "webhooks", force: :cascade do |t| t.integer "hookable_id" t.string "hookable_type" t.string "kind", null: false t.string "uri", null: false t.text "event_types", default: [], array: true + t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree end - add_index "webhooks", ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree - add_foreign_key "tokens", "users" end diff --git a/doc/api/api.raml b/doc/api/api.raml new file mode 100644 index 00000000..d61e66ac --- /dev/null +++ b/doc/api/api.raml @@ -0,0 +1,39 @@ +#%RAML 1.0 +--- +title: Metamaps +version: v2 +baseUri: http://metamaps.cc/api/v2 +mediaType: application/json + +securitySchemes: + - oauth_2_0: + description: | + OAuth 2.0 implementation + type: OAuth 2.0 + settings: + authorizationUri: https://metamaps.cc/api/v2/oauth/authorize + accessTokenUri: https://metamaps.cc/api/v2/oauth/token + authorizationGrants: [ authorization_code, password, client_credentials, implicit, refresh_token ] + +traits: + - pageable: !include traits/pageable.raml + - orderable: !include traits/orderable.raml + - searchable: !include traits/searchable.raml + +schemas: + - topic: !include schemas/_topic.json + - synapse: !include schemas/_synapse.json + - map: !include schemas/_map.json + - mapping: !include schemas/_mapping.json + - token: !include schemas/_token.json + +resourceTypes: + - base: !include resourceTypes/base.raml + - item: !include resourceTypes/item.raml + - collection: !include resourceTypes/collection.raml + +/topics: !include apis/topics.raml +/synapses: !include apis/synapses.raml +/maps: !include apis/maps.raml +/mappings: !include apis/mappings.raml +/tokens: !include apis/tokens.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml new file mode 100644 index 00000000..8b72b4df --- /dev/null +++ b/doc/api/apis/mappings.raml @@ -0,0 +1,68 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/mappings.json +post: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + xloc: + description: (for Topic mappings only) x location on the canvas + yloc: + description: (for Topic mappings only) y location on the canvas + responses: + 201: + body: + application/json: + example: !include ../examples/mapping.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + put: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + patch: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml new file mode 100644 index 00000000..c5499a33 --- /dev/null +++ b/doc/api/apis/maps.raml @@ -0,0 +1,82 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/maps.json +post: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 201: + body: + application/json: + example: !include ../examples/map.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + put: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + patch: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml new file mode 100644 index 00000000..3169c712 --- /dev/null +++ b/doc/api/apis/synapses.raml @@ -0,0 +1,82 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/synapses.json +post: + body: + application/json: + properties: + desc: + description: name + category: + description: from to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 201: + body: + application/json: + example: !include ../examples/synapse.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + put: + body: + application/json: + properties: + desc: + description: name + category: + description: from-to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + patch: + body: + application/json: + properties: + desc: + description: name + category: + description: from-to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml new file mode 100644 index 00000000..9f471615 --- /dev/null +++ b/doc/api/apis/tokens.raml @@ -0,0 +1,25 @@ +type: collection +post: + body: + application/json: + properties: + description: + description: short string describing this token + responses: + 201: + body: + application/json: + example: !include ../examples/token.json +/my_tokens: + get: + responses: + 200: + body: + application/json: + example: !include ../examples/tokens.json +/{id}: + type: item + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml new file mode 100644 index 00000000..7c214dd2 --- /dev/null +++ b/doc/api/apis/topics.raml @@ -0,0 +1,72 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/topics.json +post: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + metacode_id: + description: Topic's metacode + responses: + 201: + body: + application/json: + example: !include ../examples/topic.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + put: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + patch: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/examples/map.json b/doc/api/examples/map.json new file mode 100644 index 00000000..fe3796ca --- /dev/null +++ b/doc/api/examples/map.json @@ -0,0 +1,27 @@ +{ + "data": { + "id": 2, + "name": "Emergent Network Phenomena", + "desc": "Example map for the API", + "permission": "commons", + "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "created_at": "2016-03-26T08:02:05.379Z", + "updated_at": "2016-03-27T07:20:18.047Z", + "topic_ids": [ + 58, + 59 + ], + "synapse_ids": [ + 2 + ], + "mapping_ids": [ + 94, + 95, + 96 + ], + "collaborator_ids": [], + "contributor_ids": [ + 2 + ] + } +} diff --git a/doc/api/examples/mapping.json b/doc/api/examples/mapping.json new file mode 100644 index 00000000..c4aa87bf --- /dev/null +++ b/doc/api/examples/mapping.json @@ -0,0 +1,11 @@ +{ + "data": { + "id": 4, + "created_at": "2016-03-25T08:44:21.337Z", + "updated_at": "2016-03-25T08:44:21.337Z", + "mappable_id": 1, + "mappable_type": "Synapse", + "user_id": 1, + "map_id": 1 + } +} diff --git a/doc/api/examples/mappings.json b/doc/api/examples/mappings.json new file mode 100644 index 00000000..5a4a99c3 --- /dev/null +++ b/doc/api/examples/mappings.json @@ -0,0 +1,54 @@ +{ + "data": [ + { + "created_at": "2016-03-25T08:44:07.152Z", + "id": 1, + "map_id": 1, + "mappable_id": 1, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:07.152Z", + "user_id": 1, + "xloc": -271, + "yloc": 22 + }, + { + "created_at": "2016-03-25T08:44:13.907Z", + "id": 2, + "map_id": 1, + "mappable_id": 2, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:13.907Z", + "user_id": 1, + "xloc": -12, + "yloc": 61 + }, + { + "created_at": "2016-03-25T08:44:19.333Z", + "id": 3, + "map_id": 1, + "mappable_id": 3, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:19.333Z", + "user_id": 1, + "xloc": -93, + "yloc": -90 + }, + { + "created_at": "2016-03-25T08:44:21.337Z", + "id": 4, + "map_id": 1, + "mappable_id": 1, + "mappable_type": "Synapse", + "updated_at": "2016-03-25T08:44:21.337Z", + "user_id": 1 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "per": 4, + "prev_page": 0, + "total_count": 303, + "total_pages": 76 + } +} diff --git a/doc/api/examples/maps.json b/doc/api/examples/maps.json new file mode 100644 index 00000000..8b963990 --- /dev/null +++ b/doc/api/examples/maps.json @@ -0,0 +1,37 @@ +{ + "data": [ + { + "id": 2, + "name": "Emergent Network Phenomena", + "desc": "Example map for the API", + "permission": "commons", + "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "created_at": "2016-03-26T08:02:05.379Z", + "updated_at": "2016-03-27T07:20:18.047Z", + "topic_ids": [ + 58, + 59 + ], + "synapse_ids": [ + 2 + ], + "mapping_ids": [ + 94, + 95, + 96 + ], + "collaborator_ids": [], + "contributor_ids": [ + 2 + ] + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 5, + "total_count": 5, + "per": 1 + } +} diff --git a/doc/api/examples/synapse.json b/doc/api/examples/synapse.json new file mode 100644 index 00000000..0de4acb3 --- /dev/null +++ b/doc/api/examples/synapse.json @@ -0,0 +1,13 @@ +{ + "data": { + "id": 2, + "desc": "hello", + "category": "from-to", + "permission": "commons", + "created_at": "2016-03-26T08:02:17.994Z", + "updated_at": "2016-03-26T08:02:17.994Z", + "topic1_id": 5, + "topic2_id": 6, + "user_id": 2 + } +} diff --git a/doc/api/examples/synapses.json b/doc/api/examples/synapses.json new file mode 100644 index 00000000..1bcb00c2 --- /dev/null +++ b/doc/api/examples/synapses.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": 2, + "desc": "hello", + "category": "from-to", + "permission": "commons", + "created_at": "2016-03-26T08:02:17.994Z", + "updated_at": "2016-03-26T08:02:17.994Z", + "topic1_id": 1, + "topic2_id": 2, + "user_id": 2 + }, + { + "id": 6, + "desc": "nice", + "category": "both", + "permission": "public", + "created_at": "2016-03-26T08:05:31.563Z", + "updated_at": "2016-03-26T08:05:31.563Z", + "topic1_id": 2, + "topic2_id": 3, + "user_id": 2 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 71, + "total_count": 142, + "per": 2 + } +} diff --git a/doc/api/examples/token.json b/doc/api/examples/token.json new file mode 100644 index 00000000..14f559ea --- /dev/null +++ b/doc/api/examples/token.json @@ -0,0 +1,8 @@ +{ + "data": { + "id": 1, + "token": "VeI0qAe2bf2ytnrTRxmywsH0VSwuyjK5", + "description": "Personal token for in-browser testing", + "created_at": "2016-09-06T03:47:56.553Z" + } +} diff --git a/doc/api/examples/tokens.json b/doc/api/examples/tokens.json new file mode 100644 index 00000000..6d05cffc --- /dev/null +++ b/doc/api/examples/tokens.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "id": 1, + "token": "VeI0qAe2bf2ytnrTRxmywsH0VSwuyjK5", + "description": "Personal token for in-browser testing", + "created_at": "2016-09-06T03:47:56.553Z" + } + ], + "page": { + "current_page": 1, + "next_page": 0, + "prev_page": 0, + "total_pages": 1, + "total_count": 1, + "per": 25 + } +} diff --git a/doc/api/examples/topic.json b/doc/api/examples/topic.json new file mode 100644 index 00000000..90e702a2 --- /dev/null +++ b/doc/api/examples/topic.json @@ -0,0 +1,13 @@ +{ + "data": { + "id": 670, + "name": "Junto feedback and enhancements map", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-07-02T09:23:30.397Z", + "updated_at": "2016-07-02T09:23:30.397Z", + "user_id": 2, + "metacode_id": 36 + } +} diff --git a/doc/api/examples/topics.json b/doc/api/examples/topics.json new file mode 100644 index 00000000..d4eba53e --- /dev/null +++ b/doc/api/examples/topics.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": 670, + "name": "Junto feedback and enhancements map", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-07-02T09:23:30.397Z", + "updated_at": "2016-07-02T09:23:30.397Z", + "user_id": 2, + "metacode_id": 36 + }, + { + "id": 60, + "name": "View others on map in realtime", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-03-31T01:20:26.734Z", + "updated_at": "2016-03-31T01:20:26.734Z", + "user_id": 2, + "metacode_id": 8 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 249, + "total_count": 497, + "per": 2 + } +} diff --git a/doc/api/resourceTypes/base.raml b/doc/api/resourceTypes/base.raml new file mode 100644 index 00000000..47ca56e3 --- /dev/null +++ b/doc/api/resourceTypes/base.raml @@ -0,0 +1,35 @@ +get?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +put?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +patch?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +post?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +delete?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error diff --git a/doc/api/resourceTypes/collection.raml b/doc/api/resourceTypes/collection.raml new file mode 100644 index 00000000..d54e6c0c --- /dev/null +++ b/doc/api/resourceTypes/collection.raml @@ -0,0 +1,22 @@ +type: base +get?: + description: Get all <<resourcePathName>> + queryParameters: + page: + description: The page number + type: integer + per: + description: Number of records per page + type: integer + responses: + 200: + body: + application/json: + schema: <<resourcePathName>> +post?: + description: Create a new <<resourcePathName | !singularize>> + responses: + 200: + body: + application/json: + schema: <<resourcePathName | !singularize>> diff --git a/doc/api/resourceTypes/item.raml b/doc/api/resourceTypes/item.raml new file mode 100644 index 00000000..1abf040e --- /dev/null +++ b/doc/api/resourceTypes/item.raml @@ -0,0 +1,29 @@ +get?: + description: Get a <<resourcePathName | !singularize>> + responses: + 200: + body: + application/json: + schema: <<resourcePathName | !singularize>> +put?: + description: Update a <<resourcePathName | !singularize>> + responses: + 201: + description: Update success + body: + application/json: + schema: <<resourcePathName | !singularize>> +patch?: + description: Update a <<resourcePathName | !singularize>> + responses: + 201: + description: Update success + body: + application/json: + schema: <<resourcePathName | !singularize>> +delete?: + description: Delete a <<resourcePathName | !singularize>> + responses: + 204: + description: Removed +type: base diff --git a/doc/api/schemas/_datetimestamp.json b/doc/api/schemas/_datetimestamp.json new file mode 100644 index 00000000..fd9a76a4 --- /dev/null +++ b/doc/api/schemas/_datetimestamp.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "format": "date-time" +} diff --git a/doc/api/schemas/_id.json b/doc/api/schemas/_id.json new file mode 100644 index 00000000..d94ff818 --- /dev/null +++ b/doc/api/schemas/_id.json @@ -0,0 +1,4 @@ +{ + "type": "integer", + "minimum": 1 +} diff --git a/doc/api/schemas/_map.json b/doc/api/schemas/_map.json new file mode 100644 index 00000000..469b4dbe --- /dev/null +++ b/doc/api/schemas/_map.json @@ -0,0 +1,67 @@ +{ + "name": "Map", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "permission": { + "$ref": "_permission.json" + }, + "screenshot": { + "format": "uri", + "type": "string" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "topic_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "synapse_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "mapping_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "contributor_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "collaborator_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + } + }, + "required": [ + "id", + "name", + "desc", + "permission", + "screenshot", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_mapping.json b/doc/api/schemas/_mapping.json new file mode 100644 index 00000000..5a3b06a6 --- /dev/null +++ b/doc/api/schemas/_mapping.json @@ -0,0 +1,41 @@ +{ + "name": "Mapping", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "mappable_id": { + "$ref": "_id.json" + }, + "mappable_type": { + "type": "string", + "pattern": "(Topic|Synapse)" + }, + "xloc": { + "type": "integer" + }, + "yloc": { + "type": "integer" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "map_id": { + "$ref": "_id.json" + }, + "user_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "mappable_id", + "mappable_type", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_page.json b/doc/api/schemas/_page.json new file mode 100644 index 00000000..635f0286 --- /dev/null +++ b/doc/api/schemas/_page.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "properties": { + "current_page": { + "type": "integer", + "minimum": 1 + }, + "next_page": { + "type": "integer", + "minimum": 0 + }, + "prev_page": { + "type": "integer", + "minimum": 0 + }, + "total_pages": { + "type": "integer", + "minimum": 0 + }, + "total_count": { + "type": "integer", + "minimum": 0 + }, + "per": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "current_page", + "next_page", + "prev_page", + "total_pages", + "total_count", + "per" + ] +} + diff --git a/doc/api/schemas/_permission.json b/doc/api/schemas/_permission.json new file mode 100644 index 00000000..5c94fc81 --- /dev/null +++ b/doc/api/schemas/_permission.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "pattern": "(commons|private|public)" +} diff --git a/doc/api/schemas/_synapse.json b/doc/api/schemas/_synapse.json new file mode 100644 index 00000000..dea238e2 --- /dev/null +++ b/doc/api/schemas/_synapse.json @@ -0,0 +1,42 @@ +{ + "name": "Synapse", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "desc": { + "type": "string" + }, + "category": { + "type": "string", + "pattern": "(from-to|both)" + }, + "permission": { + "$ref": "_permission.json" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "topic1_id": { + "$ref": "_id.json" + }, + "topic2_id": { + "$ref": "_id.json" + }, + "user_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "desc", + "category", + "permission", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_token.json b/doc/api/schemas/_token.json new file mode 100644 index 00000000..62a44b3c --- /dev/null +++ b/doc/api/schemas/_token.json @@ -0,0 +1,24 @@ +{ + "name": "Token", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "token": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "$ref": "_datetimestamp.json" + } + }, + "required": [ + "id", + "token", + "description", + "created_at" + ] +} diff --git a/doc/api/schemas/_topic.json b/doc/api/schemas/_topic.json new file mode 100644 index 00000000..e9ccf67b --- /dev/null +++ b/doc/api/schemas/_topic.json @@ -0,0 +1,43 @@ +{ + "name": "Topic", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "link": { + "format": "uri", + "type": "string" + }, + "permission": { + "$ref": "_permission.json" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "user_id": { + "$ref": "_id.json" + }, + "metacode_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "name", + "desc", + "link", + "permission", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/map.json b/doc/api/schemas/map.json new file mode 100644 index 00000000..0a7ece7e --- /dev/null +++ b/doc/api/schemas/map.json @@ -0,0 +1,12 @@ +{ + "name": "Map Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_map.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/mapping.json b/doc/api/schemas/mapping.json new file mode 100644 index 00000000..f0e91ace --- /dev/null +++ b/doc/api/schemas/mapping.json @@ -0,0 +1,12 @@ +{ + "name": "Mapping Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_mapping.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/mappings.json b/doc/api/schemas/mappings.json new file mode 100644 index 00000000..37976a70 --- /dev/null +++ b/doc/api/schemas/mappings.json @@ -0,0 +1,19 @@ +{ + "name": "Mappings", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_mapping.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/maps.json b/doc/api/schemas/maps.json new file mode 100644 index 00000000..39698b96 --- /dev/null +++ b/doc/api/schemas/maps.json @@ -0,0 +1,19 @@ +{ + "name": "Maps", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_map.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/synapse.json b/doc/api/schemas/synapse.json new file mode 100644 index 00000000..5f916976 --- /dev/null +++ b/doc/api/schemas/synapse.json @@ -0,0 +1,12 @@ +{ + "name": "Synapse Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_synapse.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/synapses.json b/doc/api/schemas/synapses.json new file mode 100644 index 00000000..dd41cc53 --- /dev/null +++ b/doc/api/schemas/synapses.json @@ -0,0 +1,19 @@ +{ + "name": "Synapses", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_synapse.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/token.json b/doc/api/schemas/token.json new file mode 100644 index 00000000..85be81a5 --- /dev/null +++ b/doc/api/schemas/token.json @@ -0,0 +1,12 @@ +{ + "name": "Token Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_token.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/tokens.json b/doc/api/schemas/tokens.json new file mode 100644 index 00000000..5ea5bdce --- /dev/null +++ b/doc/api/schemas/tokens.json @@ -0,0 +1,19 @@ +{ + "name": "Tokens", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_token.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/topic.json b/doc/api/schemas/topic.json new file mode 100644 index 00000000..170670b1 --- /dev/null +++ b/doc/api/schemas/topic.json @@ -0,0 +1,12 @@ +{ + "name": "Topic Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_topic.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/topics.json b/doc/api/schemas/topics.json new file mode 100644 index 00000000..643e7607 --- /dev/null +++ b/doc/api/schemas/topics.json @@ -0,0 +1,19 @@ +{ + "name": "Topics", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_topic.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml new file mode 100644 index 00000000..708736ab --- /dev/null +++ b/doc/api/traits/orderable.raml @@ -0,0 +1,3 @@ +queryParameters: + sort: + description: The name of the field to sort by, prefixed by "-" to sort descending diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml new file mode 100644 index 00000000..88165861 --- /dev/null +++ b/doc/api/traits/pageable.raml @@ -0,0 +1,7 @@ +queryParameters: + page: + description: The page number + type: integer + per: + description: Number of records per page + type: integer diff --git a/doc/api/traits/searchable.raml b/doc/api/traits/searchable.raml new file mode 100644 index 00000000..53ae8525 --- /dev/null +++ b/doc/api/traits/searchable.raml @@ -0,0 +1,4 @@ +queryParameters: + q: + description: The search string to query by + type: string diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb new file mode 100644 index 00000000..4a1e3298 --- /dev/null +++ b/spec/api/v2/mappings_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'mappings API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:mapping) { create(:mapping, user: user) } + + it 'GET /api/v2/mappings' do + create_list(:mapping, 5) + get '/api/v2/mappings', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mappings) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/mappings/:id' do + get "/api/v2/mappings/#{mapping.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + expect(JSON.parse(response.body)['data']['id']).to eq mapping.id + end + + it 'POST /api/v2/mappings' do + post '/api/v2/mappings', params: { mapping: mapping.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + expect(Mapping.count).to eq 2 + end + + it 'PATCH /api/v2/mappings/:id' do + patch "/api/v2/mappings/#{mapping.id}", params: { mapping: mapping.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + end + + it 'DELETE /api/v2/mappings/:id' do + delete "/api/v2/mappings/#{mapping.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Mapping.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:mapping) } + let(:collection) { get_json_example(:mappings) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:mapping) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:mappings) + end + end +end diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb new file mode 100644 index 00000000..7356ca72 --- /dev/null +++ b/spec/api/v2/maps_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'maps API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:map) { create(:map, user: user) } + + it 'GET /api/v2/maps' do + create_list(:map, 5) + get '/api/v2/maps', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:maps) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/maps/:id' do + get "/api/v2/maps/#{map.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(JSON.parse(response.body)['data']['id']).to eq map.id + end + + it 'POST /api/v2/maps' do + post '/api/v2/maps', params: { map: map.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(Map.count).to eq 2 + end + + it 'PATCH /api/v2/maps/:id' do + patch "/api/v2/maps/#{map.id}", params: { map: map.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + end + + it 'DELETE /api/v2/maps/:id' do + delete "/api/v2/maps/#{map.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Map.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:map) } + let(:collection) { get_json_example(:maps) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:map) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:maps) + end + end +end diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb new file mode 100644 index 00000000..f232b879 --- /dev/null +++ b/spec/api/v2/synapses_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'synapses API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:synapse) { create(:synapse, user: user) } + + it 'GET /api/v2/synapses' do + create_list(:synapse, 5) + get '/api/v2/synapses', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapses) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/synapses/:id' do + get "/api/v2/synapses/#{synapse.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + expect(JSON.parse(response.body)['data']['id']).to eq synapse.id + end + + it 'POST /api/v2/synapses' do + post '/api/v2/synapses', params: { synapse: synapse.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + expect(Synapse.count).to eq 2 + end + + it 'PATCH /api/v2/synapses/:id' do + patch "/api/v2/synapses/#{synapse.id}", params: { synapse: synapse.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + end + + it 'DELETE /api/v2/synapses/:id' do + delete "/api/v2/synapses/#{synapse.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Synapse.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:synapse) } + let(:collection) { get_json_example(:synapses) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:synapse) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:synapses) + end + end +end diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb new file mode 100644 index 00000000..c2e480a5 --- /dev/null +++ b/spec/api/v2/tokens_api_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe 'tokens API', type: :request do + let(:user) { create(:user, admin: true) } + let(:auth_token) { create(:token, user: user).token } + let(:token) { create(:token, user: user) } + + it 'GET /api/v2/tokens/my_tokens' do + create_list(:token, 5, user: user) + get '/api/v2/tokens/my_tokens', params: { access_token: auth_token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:tokens) + expect(Token.count).to eq 6 # 5 + the extra auth token; let(:token) wasn't used + end + + it 'POST /api/v2/tokens' do + post '/api/v2/tokens', params: { token: token.attributes, access_token: auth_token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:token) + expect(Token.count).to eq 3 # auth_token, token, and the new POST-ed token + end + + it 'DELETE /api/v2/tokens/:id' do + delete "/api/v2/tokens/#{token.id}", params: { access_token: auth_token } + + expect(response).to have_http_status(:no_content) + expect(Token.count).to eq 1 # the extra auth token + end + + context 'RAML example' do + let(:resource) { get_json_example(:token) } + let(:collection) { get_json_example(:tokens) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:token) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:tokens) + end + end +end diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb new file mode 100644 index 00000000..4781348a --- /dev/null +++ b/spec/api/v2/topics_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'topics API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:topic) { create(:topic, user: user) } + + it 'GET /api/v2/topics' do + create_list(:topic, 5) + get '/api/v2/topics', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topics) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/topics/:id' do + get "/api/v2/topics/#{topic.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + expect(JSON.parse(response.body)['data']['id']).to eq topic.id + end + + it 'POST /api/v2/topics' do + post '/api/v2/topics', params: { topic: topic.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + expect(Topic.count).to eq 2 + end + + it 'PATCH /api/v2/topics/:id' do + patch "/api/v2/topics/#{topic.id}", params: { topic: topic.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + end + + it 'DELETE /api/v2/topics/:id' do + delete "/api/v2/topics/#{topic.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Topic.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:topic) } + let(:collection) { get_json_example(:topics) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:topic) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:topics) + end + end +end diff --git a/spec/controllers/mappings_controller_spec.rb b/spec/controllers/mappings_controller_spec.rb index ecf726c2..bcd2b97f 100644 --- a/spec/controllers/mappings_controller_spec.rb +++ b/spec/controllers/mappings_controller_spec.rb @@ -1,39 +1,40 @@ require 'rails_helper' RSpec.describe MappingsController, type: :controller do - let!(:mapping) { create(:mapping) } + let(:user) { create(:user) } + let!(:mapping) { create(:mapping, user: user) } let(:valid_attributes) { mapping.attributes.except('id') } - let(:invalid_attributes) { { xloc: 0 } } + let(:invalid_attributes) { { id: mapping.id } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested mapping as @mapping' do - get :show, id: mapping.to_param - expect(assigns(:mapping)).to eq(mapping) - end + sign_in user end describe 'POST #create' do context 'with valid params' do it 'creates a new Mapping' do expect do - post :create, mapping: valid_attributes + post :create, params: { + mapping: valid_attributes + } end.to change(Mapping, :count).by(1) end it 'assigns a newly created mapping as @mapping' do - post :create, mapping: valid_attributes - expect(assigns(:mapping)).to be_a(Mapping) - expect(assigns(:mapping)).to be_persisted + post :create, params: { + mapping: valid_attributes + } + expect(comparable(Mapping.last)).to eq comparable(mapping) end end context 'with invalid params' do it 'assigns a newly created but unsaved mapping as @mapping' do - post :create, mapping: invalid_attributes - expect(assigns(:mapping)).to be_a_new(Mapping) + post :create, params: { + mapping: invalid_attributes + } + # for some reason, the first mapping is still persisted + # TODO: fixme?? + expect(Mapping.count).to eq 1 end end end @@ -43,23 +44,26 @@ RSpec.describe MappingsController, type: :controller do let(:new_attributes) { build(:mapping_random_location).attributes.except('id') } it 'updates the requested mapping' do - put :update, - id: mapping.to_param, mapping: new_attributes + put :update, params: { + id: mapping.to_param, mapping: new_attributes + } mapping.reload end it 'assigns the requested mapping as @mapping' do - put :update, - id: mapping.to_param, mapping: valid_attributes - expect(assigns(:mapping)).to eq(mapping) + put :update, params: { + id: mapping.to_param, mapping: valid_attributes + } + expect(Mapping.last).to eq mapping end end context 'with invalid params' do it 'assigns the mapping as @mapping' do - put :update, - id: mapping.to_param, mapping: invalid_attributes - expect(assigns(:mapping)).to eq(mapping) + put :update, params: { + id: mapping.to_param, mapping: invalid_attributes + } + expect(Mapping.last).to eq mapping end end end @@ -67,7 +71,9 @@ RSpec.describe MappingsController, type: :controller do describe 'DELETE #destroy' do it 'destroys the requested mapping' do expect do - delete :destroy, id: mapping.to_param + delete :destroy, params: { + id: mapping.to_param + } end.to change(Mapping, :count).by(-1) end end diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 60b52ec1..278ec559 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -5,50 +5,33 @@ RSpec.describe MapsController, type: :controller do let(:valid_attributes) { map.attributes.except(:id) } let(:invalid_attributes) { { permission: :commons } } before :each do - sign_in - end - - describe 'GET #index' do - it 'viewable maps as @maps' do - get :activemaps - expect(assigns(:maps)).to eq([map]) - end - end - - describe 'GET #contains' do - it 'returns json matching schema' do - get :contains, id: map.to_param, format: :json - expect(response.body).to match_json_schema(:map_contains) - end - end - - describe 'GET #show' do - it 'assigns the requested map as @map' do - get :show, id: map.to_param - expect(assigns(:map)).to eq(map) - end + sign_in create(:user) end describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do - map.reload expect do - post :create, valid_attributes.merge(format: :json) + post :create, format: :json, params: { + map: valid_attributes + } end.to change(Map, :count).by(1) end it 'assigns a newly created map as @map' do - post :create, valid_attributes.merge(format: :json) - expect(assigns(:map)).to be_a(Map) - expect(assigns(:map)).to be_persisted + post :create, format: :json, params: { + map: valid_attributes + } + expect(Map.last).to eq map end end context 'with invalid params' do it 'assigns a newly created but unsaved map as @map' do - post :create, invalid_attributes.merge(format: :json) - expect(assigns(:map)).to be_a_new(Map) + post :create, format: :json, params: { + map: invalid_attributes + } + expect(Map.count).to eq 0 end end end @@ -58,24 +41,28 @@ RSpec.describe MapsController, type: :controller do let(:new_attributes) { { name: 'Uncool map', permission: :private } } it 'updates the requested map' do - put :update, - id: map.to_param, map: new_attributes, format: :json - expect(assigns(:map).name).to eq 'Uncool map' - expect(assigns(:map).permission).to eq 'private' + put :update, format: :json, params: { + id: map.to_param, map: new_attributes + } + map.reload + expect(map.name).to eq 'Uncool map' + expect(map.permission).to eq 'private' end it 'assigns the requested map as @map' do - put :update, - id: map.to_param, map: valid_attributes, format: :json - expect(assigns(:map)).to eq(map) + put :update, format: :json, params: { + id: map.to_param, map: valid_attributes + } + expect(Map.last).to eq map end end context 'with invalid params' do it 'assigns the map as @map' do - put :update, - id: map.to_param, map: invalid_attributes, format: :json - expect(assigns(:map)).to eq(map) + put :update, format: :json, params: { + id: map.to_param, map: invalid_attributes + } + expect(Map.last).to eq map end end end @@ -87,7 +74,9 @@ RSpec.describe MapsController, type: :controller do it 'prevents deletion by non-owners' do unowned_map.reload expect do - delete :destroy, id: unowned_map.to_param, format: :json + delete :destroy, format: :json, params: { + id: unowned_map.to_param + } end.to change(Map, :count).by(0) expect(response.body).to eq '' expect(response.status).to eq 403 @@ -96,7 +85,9 @@ RSpec.describe MapsController, type: :controller do it 'deletes owned map' do owned_map.reload # ensure it's in the database expect do - delete :destroy, id: owned_map.to_param, format: :json + delete :destroy, format: :json, params: { + id: owned_map.to_param + } end.to change(Map, :count).by(-1) expect(response.body).to eq '' expect(response.status).to eq 204 diff --git a/spec/controllers/metacodes_controller_spec.rb b/spec/controllers/metacodes_controller_spec.rb index 06434d49..cb4116d4 100644 --- a/spec/controllers/metacodes_controller_spec.rb +++ b/spec/controllers/metacodes_controller_spec.rb @@ -10,43 +10,33 @@ RSpec.describe MetacodesController, type: :controller do describe 'GET #index' do it 'assigns all metacodes as @metacodes' do metacode.reload # ensure it's created - get :index, {} + get :index expect(Metacode.all.to_a).to eq([metacode]) end end - describe 'GET #new' do - it 'assigns a new metacode as @metacode' do - get :new, format: :json - expect(assigns(:metacode)).to be_a_new(Metacode) - end - end - - describe 'GET #edit' do - it 'assigns the requested metacode as @metacode' do - get :edit, id: metacode.to_param - expect(assigns(:metacode)).to eq(metacode) - end - end - describe 'POST #create' do context 'with valid params' do it 'creates a new Metacode' do metacode.reload # ensure it's present to start expect do - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } end.to change(Metacode, :count).by(1) end it 'has the correct attributes' do - post :create, metacode: valid_attributes - # expect(Metacode.last.attributes.expect(:id)).to eq(metacode.attributes.except(:id)) - expect(assigns(:metacode)).to be_a(Metacode) - expect(assigns(:metacode)).to be_persisted + post :create, params: { + metacode: valid_attributes + } + expect(comparable(Metacode.last)).to eq(comparable(metacode)) end it 'redirects to the metacode index' do - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } expect(response).to redirect_to(metacodes_url) end end @@ -62,8 +52,9 @@ RSpec.describe MetacodesController, type: :controller do end it 'updates the requested metacode' do - put :update, - id: metacode.to_param, metacode: new_attributes + put :update, params: { + id: metacode.to_param, metacode: new_attributes + } metacode.reload expect(metacode.icon).to eq 'https://newimages.ca/cool-image.jpg' expect(metacode.color).to eq '#ffffff' @@ -75,13 +66,17 @@ RSpec.describe MetacodesController, type: :controller do context 'not admin' do it 'denies access to create' do sign_in create(:user, admin: false) - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } expect(response).to redirect_to root_url end it 'denies access to update' do sign_in create(:user, admin: false) - post :update, id: metacode.to_param, metacode: valid_attributes + post :update, params: { + id: metacode.to_param, metacode: valid_attributes + } expect(response).to redirect_to root_url end end diff --git a/spec/controllers/synapses_controller_spec.rb b/spec/controllers/synapses_controller_spec.rb index 55b2addb..15d91250 100644 --- a/spec/controllers/synapses_controller_spec.rb +++ b/spec/controllers/synapses_controller_spec.rb @@ -5,14 +5,7 @@ RSpec.describe SynapsesController, type: :controller do let(:valid_attributes) { synapse.attributes.except('id') } let(:invalid_attributes) { { permission: :invalid_lol } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested synapse as @synapse' do - get :show, id: synapse.to_param, format: :json - expect(assigns(:synapse)).to eq(synapse) - end + sign_in create(:user) end describe 'POST #create' do @@ -20,25 +13,32 @@ RSpec.describe SynapsesController, type: :controller do it 'creates a new Synapse' do synapse.reload # ensure it's present expect do - post :create, synapse: valid_attributes, format: :json + post :create, format: :json, params: { + synapse: valid_attributes + } end.to change(Synapse, :count).by(1) end it 'assigns a newly created synapse as @synapse' do - post :create, synapse: valid_attributes, format: :json - expect(assigns(:synapse)).to be_a(Synapse) - expect(assigns(:synapse)).to be_persisted + post :create, format: :json, params: { + synapse: valid_attributes + } + expect(comparable(Synapse.last)).to eq comparable(synapse) end it 'returns 201 CREATED' do - post :create, synapse: valid_attributes, format: :json + post :create, format: :json, params: { + synapse: valid_attributes + } expect(response.status).to eq 201 end end context 'with invalid params' do it 'returns 422 UNPROCESSABLE ENTITY' do - post :create, synapse: invalid_attributes, format: :json + post :create, format: :json, params: { + synapse: invalid_attributes + } expect(response.status).to eq 422 end end @@ -53,8 +53,9 @@ RSpec.describe SynapsesController, type: :controller do end it 'updates the requested synapse' do - put :update, - id: synapse.to_param, synapse: new_attributes, format: :json + put :update, format: :json, params: { + id: synapse.to_param, synapse: new_attributes + } synapse.reload expect(synapse.desc).to eq 'My new description' expect(synapse.category).to eq 'both' @@ -62,17 +63,19 @@ RSpec.describe SynapsesController, type: :controller do end it 'returns 204 NO CONTENT' do - put :update, - id: synapse.to_param, synapse: valid_attributes, format: :json + put :update, format: :json, params: { + id: synapse.to_param, synapse: valid_attributes + } expect(response.status).to eq 204 end end context 'with invalid params' do it 'assigns the synapse as @synapse' do - put :update, - id: synapse.to_param, synapse: invalid_attributes, format: :json - expect(assigns(:synapse)).to eq(synapse) + put :update, format: :json, params: { + id: synapse.to_param, synapse: invalid_attributes + } + expect(Synapse.last).to eq synapse end end end @@ -83,12 +86,16 @@ RSpec.describe SynapsesController, type: :controller do it 'destroys the requested synapse' do synapse.reload # ensure it's present expect do - delete :destroy, id: synapse.to_param, format: :json + delete :destroy, format: :json, params: { + id: synapse.to_param + } end.to change(Synapse, :count).by(-1) end it 'returns 204 NO CONTENT' do - delete :destroy, id: synapse.to_param, format: :json + delete :destroy, format: :json, params: { + id: synapse.to_param + } expect(response.status).to eq 204 end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index b6081701..315b931f 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -5,14 +5,7 @@ RSpec.describe TopicsController, type: :controller do let(:valid_attributes) { topic.attributes.except('id') } let(:invalid_attributes) { { permission: :invalid_lol } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested topic as @topic' do - get :show, id: topic.to_param, format: :json - expect(assigns(:topic)).to eq(topic) - end + sign_in create(:user) end describe 'POST #create' do @@ -20,26 +13,33 @@ RSpec.describe TopicsController, type: :controller do it 'creates a new Topic' do topic.reload # ensure it's created expect do - post :create, topic: valid_attributes, format: :json + post :create, format: :json, params: { + topic: valid_attributes + } end.to change(Topic, :count).by(1) end it 'assigns a newly created topic as @topic' do - post :create, topic: valid_attributes, format: :json - expect(assigns(:topic)).to be_a(Topic) - expect(assigns(:topic)).to be_persisted + post :create, format: :json, params: { + topic: valid_attributes + } + expect(comparable(Topic.last)).to eq comparable(topic) end it 'returns 201 CREATED' do - post :create, topic: valid_attributes, format: :json + post :create, format: :json, params: { + topic: valid_attributes + } expect(response.status).to eq 201 end end context 'with invalid params' do it 'assigns a newly created but unsaved topic as @topic' do - post :create, topic: invalid_attributes, format: :json - expect(assigns(:topic)).to be_a_new(Topic) + post :create, format: :json, params: { + topic: invalid_attributes + } + expect(Topic.count).to eq 0 end end end @@ -54,8 +54,9 @@ RSpec.describe TopicsController, type: :controller do end it 'updates the requested topic' do - put :update, - id: topic.to_param, topic: new_attributes, format: :json + put :update, format: :json, params: { + id: topic.to_param, topic: new_attributes + } topic.reload expect(topic.name).to eq 'Cool Topic with no number' expect(topic.desc).to eq 'This is a cool topic.' @@ -64,23 +65,26 @@ RSpec.describe TopicsController, type: :controller do end it 'assigns the requested topic as @topic' do - put :update, - id: topic.to_param, topic: valid_attributes, format: :json - expect(assigns(:topic)).to eq(topic) + put :update, format: :json, params: { + id: topic.to_param, topic: valid_attributes + } + expect(Topic.last).to eq(topic) end it 'returns status of no content' do - put :update, - id: topic.to_param, topic: valid_attributes, format: :json + put :update, format: :json, params: { + id: topic.to_param, topic: valid_attributes + } expect(response.status).to eq 204 end end context 'with invalid params' do it 'assigns the topic as @topic' do - put :update, - id: topic.to_param, topic: invalid_attributes, format: :json - expect(assigns(:topic)).to eq(topic) + put :update, format: :json, params: { + id: topic.to_param, topic: invalid_attributes + } + expect(Topic.last).to eq topic end end end @@ -90,7 +94,9 @@ RSpec.describe TopicsController, type: :controller do it 'destroys the requested topic' do owned_topic.reload # ensure it's there expect do - delete :destroy, id: owned_topic.to_param, format: :json + delete :destroy, format: :json, params: { + id: owned_topic.to_param + } end.to change(Topic, :count).by(-1) expect(response.body).to eq '' expect(response.status).to eq 204 diff --git a/spec/factories/maps.rb b/spec/factories/maps.rb index a786d109..14450c00 100644 --- a/spec/factories/maps.rb +++ b/spec/factories/maps.rb @@ -3,6 +3,7 @@ FactoryGirl.define do sequence(:name) { |n| "Cool Map ##{n}" } permission :commons arranged { false } + desc '' user end end diff --git a/spec/factories/synapses.rb b/spec/factories/synapses.rb index 4454a7a4..db82fc39 100644 --- a/spec/factories/synapses.rb +++ b/spec/factories/synapses.rb @@ -6,5 +6,6 @@ FactoryGirl.define do association :topic1, factory: :topic association :topic2, factory: :topic user + weight 1 # todo drop this column end end diff --git a/spec/factories/tokens.rb b/spec/factories/tokens.rb new file mode 100644 index 00000000..3970d76f --- /dev/null +++ b/spec/factories/tokens.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :token do + user + description '' + end +end diff --git a/spec/factories/topics.rb b/spec/factories/topics.rb index 17c69a25..f4c73f4c 100644 --- a/spec/factories/topics.rb +++ b/spec/factories/topics.rb @@ -1,8 +1,10 @@ FactoryGirl.define do factory :topic do - sequence(:name) { |n| "Cool Topic ##{n}" } - permission :commons user metacode + permission :commons + sequence(:name) { |n| "Cool Topic ##{n}" } + sequence(:desc) { |n| "topic desc #{n}" } + sequence(:link) { |n| "https://metamaps.cc/maps/#{n}" } end end diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb deleted file mode 100644 index 9c398b20..00000000 --- a/spec/mailers/map_mailer_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe MapMailer, type: :mailer do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 23f21101..d14d6dbe 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,45 +1,26 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' ENV['RAILS_ENV'] ||= 'test' +require 'spec_helper' require File.expand_path('../../config/environment', __FILE__) +require 'rspec/rails' # Prevent database truncation if the environment is production if Rails.env.production? abort('The Rails environment is running in production mode!') end -require 'spec_helper' -require 'rspec/rails' -# Add additional requires below this line. Rails is not loaded until this point! - # require all support files Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +ActiveRecord::Migration.maintain_test_schema! + RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. config.use_transactional_fixtures = true - # RSpec Rails can automatically mix in different behaviours to your tests - # based on their file location, for example enabling you to call `get` and - # `post` in specs under `spec/controllers`. - # - # You can disable this behaviour by removing the line below, and instead - # explicitly tag your specs with their type, e.g.: - # - # RSpec.describe UsersController, :type => :controller do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! - config.include Devise::TestHelpers, type: :controller - config.include ControllerHelpers, type: :controller config.include Shoulda::Matchers::ActiveModel, type: :model config.include Shoulda::Matchers::ActiveRecord, type: :model end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4028602..a2b164b2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,3 @@ -require 'simplecov' -require 'support/controller_helpers' -require 'pundit/rspec' - RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index a8364f91..1672479f 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -3,24 +3,17 @@ require 'devise' module ControllerHelpers - # rubocop:disable Metrics/AbcSize - def sign_in(user = create(:user)) - if user.nil? # simulate unauthenticated - allow(request.env['warden']).to( - receive(:authenticate!).and_throw(:warden, scope: :user) - ) - else # simulate authenticated - allow_message_expectations_on_nil - allow(request.env['warden']).to( - receive(:authenticate!).and_return(user) - ) - end - allow(controller).to receive(:current_user).and_return(user) + extend ActiveSupport::Concern + + included do + include Devise::Test::ControllerHelpers + end + + def comparable(model) + model.attributes.except('id', 'created_at', 'updated_at') end - # rubocop:enable Metrics/AbcSize end RSpec.configure do |config| - config.include Devise::TestHelpers, type: :controller config.include ControllerHelpers, type: :controller end diff --git a/spec/support/pundit.rb b/spec/support/pundit.rb new file mode 100644 index 00000000..1fd8e296 --- /dev/null +++ b/spec/support/pundit.rb @@ -0,0 +1 @@ +require 'pundit/rspec' diff --git a/spec/support/schema_matcher.rb b/spec/support/schema_matcher.rb index b2a89352..207c5fa6 100644 --- a/spec/support/schema_matcher.rb +++ b/spec/support/schema_matcher.rb @@ -1,7 +1,32 @@ -RSpec::Matchers.define :match_json_schema do |schema| - match do |json| - schema_directory = Rails.root.join('spec', 'schemas').to_s - schema_path = "#{schema_directory}/#{schema}.json" - JSON::Validator.validate!(schema_path, json) +RSpec::Matchers.define :match_json_schema do |schema_name| + match do |response| + schema_directory = Rails.root.join('doc', 'api', 'schemas').to_s + schema = "#{schema_directory}/#{schema_name}.json" + + # schema customizations + schema = JSON.parse(File.read(schema)) + schema = update_file_refs(schema) + + data = JSON.parse(response.body) + JSON::Validator.validate!(schema, data, validate_schema: true) end end + +def get_json_example(resource) + filepath = "#{Rails.root}/doc/api/examples/#{resource}.json" + OpenStruct.new(body: File.read(filepath)) +end + +# add full paths to file references +def update_file_refs(schema) + schema.each_pair do |key, value| + schema[key] = if value.is_a? Hash + update_file_refs(value) + elsif key == '$ref' + "#{Rails.root}/doc/api/schemas/#{value}" + else + value + end + end + schema +end diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb new file mode 100644 index 00000000..8017e897 --- /dev/null +++ b/spec/support/simplecov.rb @@ -0,0 +1,2 @@ +require 'simplecov' + From 8b19c9e340482d16b703d1a089baa9c5b434e5bb Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 01:24:14 +0800 Subject: [PATCH 009/378] automatic versioning via git (#621) --- config/initializers/version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 19139a4a..94f54a37 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,2 @@ -METAMAPS_VERSION = '2.9.0'.freeze -METAMAPS_LAST_UPDATED = 'Sept 1, 2016'.freeze +METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad' --date=format:'%b %d, %Y'`.freeze From 2219e0d0dd7250ae6ca5630ea5a15ab45e8948fe Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 21 Sep 2016 14:53:17 -0400 Subject: [PATCH 010/378] Update Metamaps.Topic.js --- app/assets/javascripts/src/Metamaps.Topic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 9e6782cb..a0ebfa82 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -331,7 +331,7 @@ Metamaps.Topic = { Metamaps.Topics.add(topic) if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, @@ -356,7 +356,7 @@ Metamaps.Topic = { var topic = self.get(id) if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, From a4d31241a8cebbbe47579304360b5bf9173ade3a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 14:25:49 +0800 Subject: [PATCH 011/378] move Metamaps code into webpack --- app/assets/javascripts/application.js | 32 +- .../javascripts/src/Metamaps.Erb.js.erb | 18 + .../javascripts/src/Metamaps.GlobalUI.js | 717 ------------------ .../src/Metamaps}/Metamaps.Account.js | 1 + .../src/Metamaps}/Metamaps.Admin.js | 1 + .../src/Metamaps}/Metamaps.AutoLayout.js | 1 + .../src/Metamaps}/Metamaps.Backbone.js | 1 + .../src/Metamaps}/Metamaps.Control.js | 1 + .../src/Metamaps}/Metamaps.Create.js | 1 + .../src/Metamaps}/Metamaps.Debug.js | 2 + .../src/Metamaps}/Metamaps.Filter.js | 1 + frontend/src/Metamaps/Metamaps.GlobalUI.js | 680 +++++++++++++++++ .../src/Metamaps}/Metamaps.Import.js | 1 + .../src/Metamaps}/Metamaps.JIT.js | 3 +- .../src/Metamaps}/Metamaps.Listeners.js | 1 + .../src/Metamaps}/Metamaps.Map.js | 1 + .../src/Metamaps}/Metamaps.Mapper.js | 1 + .../src/Metamaps}/Metamaps.Mobile.js | 1 + .../src/Metamaps}/Metamaps.Organize.js | 1 + .../src/Metamaps}/Metamaps.PasteInput.js | 1 + .../src/Metamaps/Metamaps.ReactComponents.js | 7 + .../src/Metamaps}/Metamaps.Realtime.js | 2 + .../src/Metamaps}/Metamaps.Router.js | 1 + .../src/Metamaps}/Metamaps.Synapse.js | 1 + .../src/Metamaps}/Metamaps.SynapseCard.js | 1 + .../src/Metamaps}/Metamaps.Topic.js | 1 + .../src/Metamaps}/Metamaps.TopicCard.js | 7 +- .../src/Metamaps}/Metamaps.Util.js | 1 + .../src/Metamaps}/Metamaps.Views.js | 1 + .../src/Metamaps}/Metamaps.Visualize.js | 1 + .../src/Metamaps/index.js | 49 +- frontend/src/index.js | 13 +- 32 files changed, 775 insertions(+), 776 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.Erb.js.erb delete mode 100644 app/assets/javascripts/src/Metamaps.GlobalUI.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Account.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Admin.js (97%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.AutoLayout.js (97%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Backbone.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Control.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Create.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Debug.js (73%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Filter.js (99%) create mode 100644 frontend/src/Metamaps/Metamaps.GlobalUI.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Import.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.JIT.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Listeners.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Map.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Mapper.js (91%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Mobile.js (95%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Organize.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.PasteInput.js (98%) create mode 100644 frontend/src/Metamaps/Metamaps.ReactComponents.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Realtime.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Router.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Synapse.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.SynapseCard.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Topic.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.TopicCard.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Util.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Views.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Visualize.js (99%) rename app/assets/javascripts/src/Metamaps.js.erb => frontend/src/Metamaps/index.js (60%) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 14f565fa..03dac4fb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,37 +13,11 @@ //= require jquery //= require jquery-ui //= require jquery_ujs -//= require ./webpacked/metamaps.bundle //= require_directory ./lib -//= require ./src/Metamaps.GlobalUI -//= require ./src/Metamaps.Router -//= require ./src/Metamaps.Backbone -//= require ./src/Metamaps.Views +//= require ./src/JIT +//= require ./src/Metamaps.Erb +//= require ./webpacked/metamaps.bundle //= require ./src/views/chatView //= require ./src/views/videoView //= require ./src/views/room -//= require ./src/JIT //= require ./src/check-canvas-support -//= require ./src/Metamaps -//= require ./src/Metamaps.Create -//= require ./src/Metamaps.TopicCard -//= require ./src/Metamaps.SynapseCard -//= require ./src/Metamaps.Visualize -//= require ./src/Metamaps.Util -//= require ./src/Metamaps.Realtime -//= require ./src/Metamaps.Control -//= require ./src/Metamaps.Filter -//= require ./src/Metamaps.Listeners -//= require ./src/Metamaps.Organize -//= require ./src/Metamaps.Topic -//= require ./src/Metamaps.Synapse -//= require ./src/Metamaps.Map -//= require ./src/Metamaps.Account -//= require ./src/Metamaps.Mapper -//= require ./src/Metamaps.Mobile -//= require ./src/Metamaps.Admin -//= require ./src/Metamaps.Import -//= require ./src/Metamaps.AutoLayout -//= require ./src/Metamaps.PasteInput -//= require ./src/Metamaps.JIT -//= require ./src/Metamaps.Debug diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb new file mode 100644 index 00000000..90eba5e5 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -0,0 +1,18 @@ +/* global Metamaps */ + +/* + * Metamaps.Erb.js.erb + */ + +/* erb variables from rails */ +window.Metamaps = window.Metamaps || {} +Metamaps.Erb = {} +Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' +Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' +Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' +Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' +Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' +Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> +Metamaps.VERSION = '<%= METAMAPS_VERSION %>' diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js b/app/assets/javascripts/src/Metamaps.GlobalUI.js deleted file mode 100644 index a6466fdc..00000000 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js +++ /dev/null @@ -1,717 +0,0 @@ -var Metamaps = window.Metamaps || {}; // this variable declaration defines a Javascript object that will contain all the variables and functions used by us, broken down into 'sub-modules' that look something like this -/* - -* unless you are on a page with the Javascript InfoVis Toolkit (Topic or Map) the only section in the metamaps -* object will be these -GlobalUI -Active -Maps -Mappers -Backbone - -* all these get added when you are on a page with the Javascript Infovis Toolkit -Settings -Touch -Mouse -Selected -Metacodes -Topics -Synapses -Mappings -Create -TopicCard -SynapseCard -Visualize -Util -Realtime -Control -Filter -Listeners -Organize -Map -Mapper -Topic -Synapse -JIT -*/ - -Metamaps.Active = { - Map: null, - Topic: null, - Mapper: null -}; -Metamaps.Maps = {}; - -$(document).ready(function () { - // initialize all the modules - for (var prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } - } - // load whichever page you are on - if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() - } - else { - Metamaps.Views.exploreMaps.render() - } - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { - Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') - } -}); - -Metamaps.GlobalUI = { - notifyTimeout: null, - lightbox: null, - init: function () { - var self = Metamaps.GlobalUI; - - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); - - if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - - //bind lightbox clicks - $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); - - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); - - // initialize global backbone models and collections - if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); - - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; - if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; - } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); - // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); - }, - showDiv: function (selector) { - $(selector).show() - $(selector).animate({ - opacity: 1 - }, 200, 'easeOutCubic') - }, - hideDiv: function (selector) { - $(selector).animate({ - opacity: 0 - }, 200, 'easeInCubic', function () { $(this).hide() }) - }, - openLightbox: function (which) { - var self = Metamaps.GlobalUI; - - $('.lightboxContent').hide(); - $('#' + which).show(); - - self.lightbox = which; - - $('#lightbox_overlay').show(); - - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; - // animate the content in from the bottom - $('#lightbox_main').animate({ - 'top': '50%', - 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); - - // fade the black overlay in - $('#lightbox_screen').animate({ - 'opacity': '0.42' - }, 200); - - if (which == "switchMetacodes") { - Metamaps.Create.isSwitchingSet = true; - } - }, - - closeLightbox: function (event) { - var self = Metamaps.GlobalUI; - - if (event) event.preventDefault(); - - // animate the lightbox content offscreen - $('#lightbox_main').animate({ - 'top': '100%', - 'margin-top': '0' - }, 200, 'easeInCubic'); - - // fade the black overlay out - $('#lightbox_screen').animate({ - 'opacity': '0.0' - }, 200, function () { - $('#lightbox_overlay').hide(); - }); - - if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); - if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { - Metamaps.Create.cancelMetacodeSetSwitch(); - } - self.lightbox = null; - }, - notifyUser: function (message, leaveOpen) { - var self = Metamaps.GlobalUI; - - $('#toast').html(message) - self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); - if (!leaveOpen) { - self.notifyTimeOut = setTimeout(function () { - self.hideDiv('#toast') - }, 8000); - } - }, - clearNotify: function() { - var self = Metamaps.GlobalUI; - - clearTimeout(self.notifyTimeOut); - self.hideDiv('#toast') - }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); - } -}; - -Metamaps.GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = Metamaps.GlobalUI.CreateMap; - - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - - self.bindFormEvents(); - - self.emptyMapForm = $('#new_map').html(); - - }, - bindFormEvents: function () { - var self = Metamaps.GlobalUI.CreateMap; - - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { - if (event.keyCode === 13) self.submit() - }) - - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - Metamaps.GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); - - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Metamaps.Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; - }, - switchPermission: function () { - var self = Metamaps.GlobalUI.CreateMap; - - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); - - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); - }, - submit: function (event) { - if (event) event.preventDefault(); - - var self = Metamaps.GlobalUI.CreateMap; - - if (Metamaps.GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); - } - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); - - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); - - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; - } - - self.newMap.save(null, { - success: self.success - // TODO add error message - }); - - Metamaps.GlobalUI.closeLightbox(); - Metamaps.GlobalUI.notifyUser('Working...'); - }, - throwMapNameError: function () { - var self = Metamaps.GlobalUI.CreateMap; - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); - - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); - - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); - }, - success: function (model) { - var self = Metamaps.GlobalUI.CreateMap; - - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); - - Metamaps.GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); - - }, - reset: function (id) { - var self = Metamaps.GlobalUI.CreateMap; - - var form = $('#' + id); - - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); - } - - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - - return false; - }, -}; - - -Metamaps.GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = Metamaps.GlobalUI.Account; - - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); - }, - toggleBox: function (event) { - var self = Metamaps.GlobalUI.Account; - - if (self.isOpen) self.close(); - else self.open(); - - event.stopPropagation(); - }, - open: function () { - var self = Metamaps.GlobalUI.Account; - - Metamaps.Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); - - - if (!self.isOpen && !self.changing) { - self.changing = true; - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); - } - }, - close: function () { - var self = Metamaps.GlobalUI.Account; - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); - if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); - } - } -}; - - - -Metamaps.GlobalUI.Search = { - locked: false, - isOpen: false, - limitTopicsToMe: false, - limitMapsToMe: false, - timeOut: null, - changing: false, - optionsInitialized: false, - init: function () { - var self = Metamaps.GlobalUI.Search; - - var loader = new CanvasLoader('searchLoading'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(24); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - - // bind the hover events - $(".sidebarSearch").hover(function () { - self.open() - }, function () { - self.close(800, false) - }); - - $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus(); - }); - $('.sidebarSearch').click(function (e) { - e.stopPropagation(); - }); - $('body').click(function (e) { - self.close(0, false); - }); - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true); // true for focus - } - break; - case 27: - if (self.isOpen) { - self.close(0, true); - } - break; - - default: - break; //console.log(e.which); - } - }); - - self.startTypeahead(); - }, - lock: function() { - var self = Metamaps.GlobalUI.Search; - self.locked = true; - }, - unlock: function() { - var self = Metamaps.GlobalUI.Search; - self.locked = false; - }, - open: function (focus) { - var self = Metamaps.GlobalUI.Search; - - clearTimeout(self.timeOut); - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true; - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus(); - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }); - self.changing = false; - self.isOpen = true; - }); - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = Metamaps.GlobalUI.Search; - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { - self.changing = true; - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }); - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', ''); - $('.sidebarSearchField').blur(); - self.changing = false; - self.isOpen = false; - }); - } - }, closeAfter); - }, - startTypeahead: function () { - var self = Metamaps.GlobalUI.Search; - - var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; - - var topics = { - name: 'topics', - limit: 9999, - - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: "No results", - label: "No results", - typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: "noresult" - }); - }, - header: topicheader, - suggestion: function(s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/topics', - prepare: function(query, settings) { - settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return settings; - }, - }, - }), - }; - - var maps = { - name: 'maps', - limit: 9999, - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult" - }); - }, - header: mapheader, - suggestion: function(s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/maps', - prepare: function(query, settings) { - settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return settings; - }, - }, - }), - }; - - var mappers = { - name: 'mappers', - limit: 9999, - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult", - profile: Metamaps.Erb['user.png'] - }); - }, - header: mapperheader, - suggestion: function(s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY', - }, - }), - }; - - // Take all that crazy setup data and put it together into one beautiful typeahead call! - $('.sidebarSearchField').typeahead( - { - highlight: true, - }, - [topics, maps, mappers] - ); - - //Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions(); - self.hideLoader(); - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true); - } - if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true); - } - }); - $(window).resize(function () { - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - }); - - // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); - - // don't do it, if they clicked on a 'addToMap' button - $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation(); - }); - - // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function(){ - if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader(); - } else { - self.showLoader(); - } - }); - - }, - handleResultClick: function (event, datum, dataset) { - var self = Metamaps.GlobalUI.Search; - - self.hideLoader(); - - if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { - self.close(0, true); - var win; - if (datum.rtype == "topic") { - Metamaps.Router.topics(datum.id); - } else if (datum.rtype == "map") { - Metamaps.Router.maps(datum.id); - } else if (datum.rtype == "mapper") { - Metamaps.Router.explore("mapper", datum.id); - } - } - }, - initSearchOptions: function () { - var self = Metamaps.GlobalUI.Search; - - function toggleResultSet(set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); - if (s.is(':visible')) { - s.hide(); - $(this).removeClass('minimizeResults').addClass('maximizeResults'); - } else { - s.show(); - $(this).removeClass('maximizeResults').addClass('minimizeResults'); - } - } - - $('.limitToMe').unbind().bind("change", function (e) { - if ($(this).attr('id') == 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe; - } - if ($(this).attr('id') == 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe; - } - - // set the value of the search equal to itself to retrigger the - // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val(); - $(".sidebarSearchField").typeahead('val', '') - .typeahead('val', searchQuery); - }); - - // when the user clicks minimize section, hide the results for that section - $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers'); - }); - $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics'); - }); - $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps'); - }); - }, - hideLoader: function () { - $('#searchLoading').hide(); - }, - showLoader: function () { - $('#searchLoading').show(); - } -}; diff --git a/app/assets/javascripts/src/Metamaps.Account.js b/frontend/src/Metamaps/Metamaps.Account.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Account.js rename to frontend/src/Metamaps/Metamaps.Account.js index a2286ad8..66348481 100644 --- a/app/assets/javascripts/src/Metamaps.Account.js +++ b/frontend/src/Metamaps/Metamaps.Account.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Admin.js b/frontend/src/Metamaps/Metamaps.Admin.js similarity index 97% rename from app/assets/javascripts/src/Metamaps.Admin.js rename to frontend/src/Metamaps/Metamaps.Admin.js index a0192012..8dcaed5b 100644 --- a/app/assets/javascripts/src/Metamaps.Admin.js +++ b/frontend/src/Metamaps/Metamaps.Admin.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.AutoLayout.js b/frontend/src/Metamaps/Metamaps.AutoLayout.js similarity index 97% rename from app/assets/javascripts/src/Metamaps.AutoLayout.js rename to frontend/src/Metamaps/Metamaps.AutoLayout.js index 51e105c2..2360204b 100644 --- a/app/assets/javascripts/src/Metamaps.AutoLayout.js +++ b/frontend/src/Metamaps/Metamaps.AutoLayout.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/frontend/src/Metamaps/Metamaps.Backbone.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Backbone.js rename to frontend/src/Metamaps/Metamaps.Backbone.js index 2c1f58af..3c991e0b 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/frontend/src/Metamaps/Metamaps.Backbone.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, _, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Control.js b/frontend/src/Metamaps/Metamaps.Control.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Control.js rename to frontend/src/Metamaps/Metamaps.Control.js index da6854c2..33623927 100644 --- a/app/assets/javascripts/src/Metamaps.Control.js +++ b/frontend/src/Metamaps/Metamaps.Control.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/frontend/src/Metamaps/Metamaps.Create.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Create.js rename to frontend/src/Metamaps/Metamaps.Create.js index 6f3bbb62..1bd4216f 100644 --- a/app/assets/javascripts/src/Metamaps.Create.js +++ b/frontend/src/Metamaps/Metamaps.Create.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Debug.js b/frontend/src/Metamaps/Metamaps.Debug.js similarity index 73% rename from app/assets/javascripts/src/Metamaps.Debug.js rename to frontend/src/Metamaps/Metamaps.Debug.js index accd93a9..7bc71979 100644 --- a/app/assets/javascripts/src/Metamaps.Debug.js +++ b/frontend/src/Metamaps/Metamaps.Debug.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* * Metamaps.Debug.js.erb * @@ -10,4 +11,5 @@ Metamaps.Debug = function () { } Metamaps.debug = function () { Metamaps.Debug() +window.Metamaps = window.Metamaps || {} } diff --git a/app/assets/javascripts/src/Metamaps.Filter.js b/frontend/src/Metamaps/Metamaps.Filter.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Filter.js rename to frontend/src/Metamaps/Metamaps.Filter.js index 1dba099c..367918e6 100644 --- a/app/assets/javascripts/src/Metamaps.Filter.js +++ b/frontend/src/Metamaps/Metamaps.Filter.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/frontend/src/Metamaps/Metamaps.GlobalUI.js b/frontend/src/Metamaps/Metamaps.GlobalUI.js new file mode 100644 index 00000000..d5fe6caa --- /dev/null +++ b/frontend/src/Metamaps/Metamaps.GlobalUI.js @@ -0,0 +1,680 @@ +window.Metamaps = window.Metamaps || {}; + +Metamaps.Active = Metamaps.Active || { + Map: null, + Topic: null, + Mapper: null +}; +Metamaps.Maps = Metamaps.Maps || {} + +$(document).ready(function () { + // initialize all the modules + for (var prop in Metamaps) { + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) == 'function' + ) { + Metamaps[prop].init() + } + } + // load whichever page you are on + if (Metamaps.currentSection === "explore") { + var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + + Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + if (Metamaps.currentPage === "mapper") { + Metamaps.Views.exploreMaps.fetchUserThenRender() + } + else { + Metamaps.Views.exploreMaps.render() + } + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.exploreMaps.render() + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + Metamaps.Loading.show() + Metamaps.JIT.prepareVizData() + Metamaps.GlobalUI.showDiv('#infovis') + } +}); + +Metamaps.GlobalUI = { + notifyTimeout: null, + lightbox: null, + init: function () { + var self = Metamaps.GlobalUI; + + self.Search.init(); + self.CreateMap.init(); + self.Account.init(); + + if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) + + //bind lightbox clicks + $('.openLightbox').click(function (event) { + self.openLightbox($(this).attr('data-open')); + event.preventDefault(); + return false; + }); + + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + + // initialize global backbone models and collections + if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); + + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; + var mapperCollection = []; + var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + if (Metamaps.Maps.Mapper) { + mapperCollection = Metamaps.Maps.Mapper.models; + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; + } + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + // 'Mapper' refers to another mapper + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + }, + showDiv: function (selector) { + $(selector).show() + $(selector).animate({ + opacity: 1 + }, 200, 'easeOutCubic') + }, + hideDiv: function (selector) { + $(selector).animate({ + opacity: 0 + }, 200, 'easeInCubic', function () { $(this).hide() }) + }, + openLightbox: function (which) { + var self = Metamaps.GlobalUI; + + $('.lightboxContent').hide(); + $('#' + which).show(); + + self.lightbox = which; + + $('#lightbox_overlay').show(); + + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + // animate the content in from the bottom + $('#lightbox_main').animate({ + 'top': '50%', + 'margin-top': heightOfContent + }, 200, 'easeOutCubic'); + + // fade the black overlay in + $('#lightbox_screen').animate({ + 'opacity': '0.42' + }, 200); + + if (which == "switchMetacodes") { + Metamaps.Create.isSwitchingSet = true; + } + }, + + closeLightbox: function (event) { + var self = Metamaps.GlobalUI; + + if (event) event.preventDefault(); + + // animate the lightbox content offscreen + $('#lightbox_main').animate({ + 'top': '100%', + 'margin-top': '0' + }, 200, 'easeInCubic'); + + // fade the black overlay out + $('#lightbox_screen').animate({ + 'opacity': '0.0' + }, 200, function () { + $('#lightbox_overlay').hide(); + }); + + if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); + if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { + Metamaps.Create.cancelMetacodeSetSwitch(); + } + self.lightbox = null; + }, + notifyUser: function (message, leaveOpen) { + var self = Metamaps.GlobalUI; + + $('#toast').html(message) + self.showDiv('#toast') + clearTimeout(self.notifyTimeOut); + if (!leaveOpen) { + self.notifyTimeOut = setTimeout(function () { + self.hideDiv('#toast') + }, 8000); + } + }, + clearNotify: function() { + var self = Metamaps.GlobalUI; + + clearTimeout(self.notifyTimeOut); + self.hideDiv('#toast') + }, + shareInvite: function(inviteLink) { + window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + } +}; + +Metamaps.GlobalUI.CreateMap = { + newMap: null, + emptyMapForm: "", + emptyForkMapForm: "", + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = Metamaps.GlobalUI.CreateMap; + + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + + self.bindFormEvents(); + + self.emptyMapForm = $('#new_map').html(); + + }, + bindFormEvents: function () { + var self = Metamaps.GlobalUI.CreateMap; + + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + if (event.keyCode === 13) self.submit() + }) + + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault(); + Metamaps.GlobalUI.closeLightbox(); + }); + $('.new_map button.submitMap').unbind().bind('click', self.submit); + + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission); + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function(){ + $(this).remove(); + }); + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; + stringStart += id; + stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + var page = Metamaps.Active.Map ? 'map' : 'page'; + var stringEnd = "</a></div>"; + return stringStart + page + stringEnd; + }, + switchPermission: function () { + var self = Metamaps.GlobalUI.CreateMap; + + self.newMap.set('permission', $(this).attr('data-permission')); + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); + $(this).find('.mapPermIcon').addClass('selected'); + + var permText = $(this).find('.tip').html(); + $(this).parents('.new_map').find('.permText').html(permText); + }, + submit: function (event) { + if (event) event.preventDefault(); + + var self = Metamaps.GlobalUI.CreateMap; + + if (Metamaps.GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap); + self.newMap.set('synapsesToMap', self.synapsesToMap); + } + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); + + self.newMap.set('name', $form.find('#map_name').val()); + self.newMap.set('desc', $form.find('#map_desc').val()); + + if (self.newMap.get('name').length===0){ + self.throwMapNameError(); + return; + } + + self.newMap.save(null, { + success: self.success + // TODO add error message + }); + + Metamaps.GlobalUI.closeLightbox(); + Metamaps.GlobalUI.notifyUser('Working...'); + }, + throwMapNameError: function () { + var self = Metamaps.GlobalUI.CreateMap; + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); + + var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + + $form.find('#map_name').after(message); + setTimeout(function(){ + message.fadeOut('fast', function(){ + message.remove(); + }); + }, 5000); + }, + success: function (model) { + var self = Metamaps.GlobalUI.CreateMap; + + //push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model); + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var form = $(formId); + + Metamaps.GlobalUI.clearNotify(); + $('#wrapper').append(self.generateSuccessMessage(model.id)); + + }, + reset: function (id) { + var self = Metamaps.GlobalUI.CreateMap; + + var form = $('#' + id); + + if (id === "fork_map") { + self.topicsToMap = []; + self.synapsesToMap = []; + form.html(self.emptyForkMapForm); + } + else { + form.html(self.emptyMapForm); + } + + self.bindFormEvents(); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + + return false; + }, +}; + + +Metamaps.GlobalUI.Account = { + isOpen: false, + changing: false, + init: function () { + var self = Metamaps.GlobalUI.Account; + + $('.sidebarAccountIcon').click(self.toggleBox); + $('.sidebarAccountBox').click(function(event){ + event.stopPropagation(); + }); + $('body').click(self.close); + }, + toggleBox: function (event) { + var self = Metamaps.GlobalUI.Account; + + if (self.isOpen) self.close(); + else self.open(); + + event.stopPropagation(); + }, + open: function () { + var self = Metamaps.GlobalUI.Account; + + Metamaps.Filter.close(); + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + + + if (!self.isOpen && !self.changing) { + self.changing = true; + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false; + self.isOpen = true; + $('.sidebarAccountBox #user_email').focus(); + }); + } + }, + close: function () { + var self = Metamaps.GlobalUI.Account; + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + if (!self.changing) { + self.changing = true; + $('.sidebarAccountBox #user_email').blur(); + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false; + self.isOpen = false; + }); + } + } +}; + +Metamaps.GlobalUI.Search = { + locked: false, + isOpen: false, + limitTopicsToMe: false, + limitMapsToMe: false, + timeOut: null, + changing: false, + optionsInitialized: false, + init: function () { + var self = Metamaps.GlobalUI.Search; + + var loader = new CanvasLoader('searchLoading'); + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(24); // default is 40 + loader.setDensity(41); // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show(); // Hidden by default + + // bind the hover events + $(".sidebarSearch").hover(function () { + self.open() + }, function () { + self.close(800, false) + }); + + $('.sidebarSearchIcon').click(function (e) { + $('.sidebarSearchField').focus(); + }); + $('.sidebarSearch').click(function (e) { + e.stopPropagation(); + }); + $('body').click(function (e) { + self.close(0, false); + }); + + // open if the search is closed and user hits ctrl+/ + // close if they hit ESC + $('body').bind('keyup', function (e) { + switch (e.which) { + case 191: + if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { + self.open(true); // true for focus + } + break; + case 27: + if (self.isOpen) { + self.close(0, true); + } + break; + + default: + break; //console.log(e.which); + } + }); + + self.startTypeahead(); + }, + lock: function() { + var self = Metamaps.GlobalUI.Search; + self.locked = true; + }, + unlock: function() { + var self = Metamaps.GlobalUI.Search; + self.locked = false; + }, + open: function (focus) { + var self = Metamaps.GlobalUI.Search; + + clearTimeout(self.timeOut); + if (!self.isOpen && !self.changing && !self.locked) { + self.changing = true; + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '400px' + }, 300, function () { + if (focus) $('.sidebarSearchField').focus(); + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 10px 3px 10px', + width: '380px' + }); + self.changing = false; + self.isOpen = true; + }); + } + }, + close: function (closeAfter, bypass) { + // for now + return + + var self = Metamaps.GlobalUI.Search; + + self.timeOut = setTimeout(function () { + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { + self.changing = true; + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 0 3px 0', + width: '400px' + }); + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '0' + }, 300, function () { + $('.sidebarSearchField').typeahead('val', ''); + $('.sidebarSearchField').blur(); + self.changing = false; + self.isOpen = false; + }); + } + }, closeAfter); + }, + startTypeahead: function () { + var self = Metamaps.GlobalUI.Search; + + var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; + var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; + + var topics = { + name: 'topics', + limit: 9999, + + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ + value: "No results", + label: "No results", + typeImageURL: Metamaps.Erb['icons/wildcard.png'], + rtype: "noresult" + }); + }, + header: topicheader, + suggestion: function(s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/topics', + prepare: function(query, settings) { + settings.url += '?term=' + query; + if (Metamaps.Active.Mapper && self.limitTopicsToMe) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), + }; + + var maps = { + name: 'maps', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult" + }); + }, + header: mapheader, + suggestion: function(s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps', + prepare: function(query, settings) { + settings.url += '?term=' + query; + if (Metamaps.Active.Mapper && self.limitMapsToMe) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), + }; + + var mappers = { + name: 'mappers', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult", + profile: Metamaps.Erb['user.png'] + }); + }, + header: mapperheader, + suggestion: function(s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY', + }, + }), + }; + + // Take all that crazy setup data and put it together into one beautiful typeahead call! + $('.sidebarSearchField').typeahead( + { + highlight: true, + }, + [topics, maps, mappers] + ); + + //Set max height of the search results box to prevent it from covering bottom left footer + $('.sidebarSearchField').bind('typeahead:render', function (event) { + self.initSearchOptions(); + self.hideLoader(); + var h = $(window).height(); + $(".tt-dropdown-menu").css('max-height', h - 100); + if (self.limitTopicsToMe) { + $('#limitTopicsToMe').prop('checked', true); + } + if (self.limitMapsToMe) { + $('#limitMapsToMe').prop('checked', true); + } + }); + $(window).resize(function () { + var h = $(window).height(); + $(".tt-dropdown-menu").css('max-height', h - 100); + }); + + // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); + + // don't do it, if they clicked on a 'addToMap' button + $('.sidebarSearch button.addToMap').click(function (event) { + event.stopPropagation(); + }); + + // make sure that when you click on 'limit to me' or 'toggle section' it works + $('.sidebarSearchField.tt-input').keyup(function(){ + if ($('.sidebarSearchField.tt-input').val() === '') { + self.hideLoader(); + } else { + self.showLoader(); + } + }); + + }, + handleResultClick: function (event, datum, dataset) { + var self = Metamaps.GlobalUI.Search; + + self.hideLoader(); + + if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { + self.close(0, true); + var win; + if (datum.rtype == "topic") { + Metamaps.Router.topics(datum.id); + } else if (datum.rtype == "map") { + Metamaps.Router.maps(datum.id); + } else if (datum.rtype == "mapper") { + Metamaps.Router.explore("mapper", datum.id); + } + } + }, + initSearchOptions: function () { + var self = Metamaps.GlobalUI.Search; + + function toggleResultSet(set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); + if (s.is(':visible')) { + s.hide(); + $(this).removeClass('minimizeResults').addClass('maximizeResults'); + } else { + s.show(); + $(this).removeClass('maximizeResults').addClass('minimizeResults'); + } + } + + $('.limitToMe').unbind().bind("change", function (e) { + if ($(this).attr('id') == 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe; + } + if ($(this).attr('id') == 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe; + } + + // set the value of the search equal to itself to retrigger the + // autocomplete event + var searchQuery = $('.sidebarSearchField.tt-input').val(); + $(".sidebarSearchField").typeahead('val', '') + .typeahead('val', searchQuery); + }); + + // when the user clicks minimize section, hide the results for that section + $('.minimizeMapperResults').unbind().click(function (e) { + toggleResultSet.call(this, 'mappers'); + }); + $('.minimizeTopicResults').unbind().click(function (e) { + toggleResultSet.call(this, 'topics'); + }); + $('.minimizeMapResults').unbind().click(function (e) { + toggleResultSet.call(this, 'maps'); + }); + }, + hideLoader: function () { + $('#searchLoading').hide(); + }, + showLoader: function () { + $('#searchLoading').show(); + } +} diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/frontend/src/Metamaps/Metamaps.Import.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Import.js rename to frontend/src/Metamaps/Metamaps.Import.js index 2dee51d0..426071f0 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/frontend/src/Metamaps/Metamaps.Import.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.JIT.js b/frontend/src/Metamaps/Metamaps.JIT.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.JIT.js rename to frontend/src/Metamaps/Metamaps.JIT.js index d5e82081..cfdd921d 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js +++ b/frontend/src/Metamaps/Metamaps.JIT.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} Metamaps.JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', @@ -819,7 +820,7 @@ Metamaps.JIT = { } } // - temp = eventInfo.getNode() + let temp = eventInfo.getNode() if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned Metamaps.tempNode2 = temp diff --git a/app/assets/javascripts/src/Metamaps.Listeners.js b/frontend/src/Metamaps/Metamaps.Listeners.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Listeners.js rename to frontend/src/Metamaps/Metamaps.Listeners.js index 948893cb..e6c4e1b9 100644 --- a/app/assets/javascripts/src/Metamaps.Listeners.js +++ b/frontend/src/Metamaps/Metamaps.Listeners.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/frontend/src/Metamaps/Metamaps.Map.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Map.js rename to frontend/src/Metamaps/Metamaps.Map.js index 264e3c48..e925a92c 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js +++ b/frontend/src/Metamaps/Metamaps.Map.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Mapper.js b/frontend/src/Metamaps/Metamaps.Mapper.js similarity index 91% rename from app/assets/javascripts/src/Metamaps.Mapper.js rename to frontend/src/Metamaps/Metamaps.Mapper.js index 7d565479..f8a530b8 100644 --- a/app/assets/javascripts/src/Metamaps.Mapper.js +++ b/frontend/src/Metamaps/Metamaps.Mapper.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Mobile.js b/frontend/src/Metamaps/Metamaps.Mobile.js similarity index 95% rename from app/assets/javascripts/src/Metamaps.Mobile.js rename to frontend/src/Metamaps/Metamaps.Mobile.js index 1a55f081..fcd76b2f 100644 --- a/app/assets/javascripts/src/Metamaps.Mobile.js +++ b/frontend/src/Metamaps/Metamaps.Mobile.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Organize.js b/frontend/src/Metamaps/Metamaps.Organize.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Organize.js rename to frontend/src/Metamaps/Metamaps.Organize.js index b2463280..220cb83a 100644 --- a/app/assets/javascripts/src/Metamaps.Organize.js +++ b/frontend/src/Metamaps/Metamaps.Organize.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/frontend/src/Metamaps/Metamaps.PasteInput.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.PasteInput.js rename to frontend/src/Metamaps/Metamaps.PasteInput.js index aaf848d0..3e933e41 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/frontend/src/Metamaps/Metamaps.PasteInput.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/frontend/src/Metamaps/Metamaps.ReactComponents.js b/frontend/src/Metamaps/Metamaps.ReactComponents.js new file mode 100644 index 00000000..a1de0f40 --- /dev/null +++ b/frontend/src/Metamaps/Metamaps.ReactComponents.js @@ -0,0 +1,7 @@ +window.Metamaps = window.Metamaps || {} + +import Maps from '../components/Maps' + +Metamaps.ReactComponents = { + Maps +} diff --git a/app/assets/javascripts/src/Metamaps.Realtime.js b/frontend/src/Metamaps/Metamaps.Realtime.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Realtime.js rename to frontend/src/Metamaps/Metamaps.Realtime.js index 620a561a..0b62648f 100644 --- a/app/assets/javascripts/src/Metamaps.Realtime.js +++ b/frontend/src/Metamaps/Metamaps.Realtime.js @@ -1,3 +1,5 @@ +window.Metamaps = window.Metamaps || {} + /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Router.js b/frontend/src/Metamaps/Metamaps.Router.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Router.js rename to frontend/src/Metamaps/Metamaps.Router.js index 3ef30986..417c9b9e 100644 --- a/app/assets/javascripts/src/Metamaps.Router.js +++ b/frontend/src/Metamaps/Metamaps.Router.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Synapse.js b/frontend/src/Metamaps/Metamaps.Synapse.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Synapse.js rename to frontend/src/Metamaps/Metamaps.Synapse.js index ceed219d..20cf0f9c 100644 --- a/app/assets/javascripts/src/Metamaps.Synapse.js +++ b/frontend/src/Metamaps/Metamaps.Synapse.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.SynapseCard.js b/frontend/src/Metamaps/Metamaps.SynapseCard.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.SynapseCard.js rename to frontend/src/Metamaps/Metamaps.SynapseCard.js index f71601e5..aff207a9 100644 --- a/app/assets/javascripts/src/Metamaps.SynapseCard.js +++ b/frontend/src/Metamaps/Metamaps.SynapseCard.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/frontend/src/Metamaps/Metamaps.Topic.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Topic.js rename to frontend/src/Metamaps/Metamaps.Topic.js index a0ebfa82..faa8b336 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/frontend/src/Metamaps/Metamaps.Topic.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/frontend/src/Metamaps/Metamaps.TopicCard.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.TopicCard.js rename to frontend/src/Metamaps/Metamaps.TopicCard.js index 1453104d..fc007f3b 100644 --- a/app/assets/javascripts/src/Metamaps.TopicCard.js +++ b/frontend/src/Metamaps/Metamaps.TopicCard.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -421,18 +422,18 @@ Metamaps.TopicCard = { var inmapsLinks = topic.get('inmapsLinks') || [] nodeValues.inmaps = '' if (inmapsAr.length < 6) { - for (i = 0; i < inmapsAr.length; i++) { + for (let i = 0; i < inmapsAr.length; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } } else { - for (i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } extra = inmapsAr.length - 5 nodeValues.inmaps += '<li><span class="showMore">See ' + extra + ' more...</span></li>' - for (i = 5; i < inmapsAr.length; i++) { + for (let i = 5; i < inmapsAr.length; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li class="hideExtra extraText"><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } diff --git a/app/assets/javascripts/src/Metamaps.Util.js b/frontend/src/Metamaps/Metamaps.Util.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Util.js rename to frontend/src/Metamaps/Metamaps.Util.js index e150d3bb..9ff9c470 100644 --- a/app/assets/javascripts/src/Metamaps.Util.js +++ b/frontend/src/Metamaps/Metamaps.Util.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* diff --git a/app/assets/javascripts/src/Metamaps.Views.js b/frontend/src/Metamaps/Metamaps.Views.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Views.js rename to frontend/src/Metamaps/Metamaps.Views.js index d027d22c..eb5fdb7c 100644 --- a/app/assets/javascripts/src/Metamaps.Views.js +++ b/frontend/src/Metamaps/Metamaps.Views.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Visualize.js b/frontend/src/Metamaps/Metamaps.Visualize.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Visualize.js rename to frontend/src/Metamaps/Metamaps.Visualize.js index 7168c03a..f5ce8c79 100644 --- a/app/assets/javascripts/src/Metamaps.Visualize.js +++ b/frontend/src/Metamaps/Metamaps.Visualize.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* * Metamaps.Visualize diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/frontend/src/Metamaps/index.js similarity index 60% rename from app/assets/javascripts/src/Metamaps.js.erb rename to frontend/src/Metamaps/index.js index 839d701e..bf54483d 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/frontend/src/Metamaps/index.js @@ -1,26 +1,10 @@ -/* global Metamaps */ - -/* - * Metamaps.js.erb - */ +window.Metamaps = window.Metamaps || {} // TODO eliminate these 5 top-level variables Metamaps.panningInt = null Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null -Metamaps.VERSION = '<%= METAMAPS_VERSION %>' - -/* erb variables from rails */ -Metamaps.Erb = {} -Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' -Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' -Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' -Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' -Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' -Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' -Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' -Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages @@ -65,10 +49,39 @@ Metamaps.Mouse = { Metamaps.Selected = { reset: function () { var self = Metamaps.Selected - self.Nodes = [] self.Edges = [] }, Nodes: [], Edges: [] } + +require('./Metamaps.Account') +require('./Metamaps.Admin') +require('./Metamaps.AutoLayout') +require('./Metamaps.Backbone') +require('./Metamaps.Control') +require('./Metamaps.Create') +require('./Metamaps.Debug') +require('./Metamaps.Filter') +require('./Metamaps.GlobalUI') +require('./Metamaps.Import') +require('./Metamaps.JIT') +require('./Metamaps.Listeners') +require('./Metamaps.Map') +require('./Metamaps.Mapper') +require('./Metamaps.Mobile') +require('./Metamaps.Organize') +require('./Metamaps.PasteInput') +require('./Metamaps.Realtime') +require('./Metamaps.Router') +require('./Metamaps.Synapse') +require('./Metamaps.SynapseCard') +require('./Metamaps.Topic') +require('./Metamaps.TopicCard') +require('./Metamaps.Util') +require('./Metamaps.Views') +require('./Metamaps.Visualize') +require('./Metamaps.ReactComponents') + +export default window.Metamaps diff --git a/frontend/src/index.js b/frontend/src/index.js index 0556f4c1..e5705512 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -2,17 +2,14 @@ import React from 'react' import ReactDOM from 'react-dom' import Backbone from 'backbone' import _ from 'underscore' -import Maps from './components/Maps.js' -// this is optional really, if we import components directly React will be -// in the bundle, so we won't need a global reference +import Metamaps from './Metamaps' + +// create global references to some libraries window.React = React window.ReactDOM = ReactDOM -Backbone.$ = window.$ +Backbone.$ = window.$ // jquery from rails window.Backbone = Backbone window._ = _ -window.Metamaps = window.Metamaps || {} -window.Metamaps.ReactComponents = { - Maps -} +window.Metamaps = Metamaps From d02c836805e36b0611872a04f481a759e902b58a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 14:35:14 +0800 Subject: [PATCH 012/378] remove Metamaps from filenames --- .../{Metamaps.Account.js => Account.js} | 0 .../Metamaps/{Metamaps.Admin.js => Admin.js} | 0 .../{Metamaps.AutoLayout.js => AutoLayout.js} | 0 .../{Metamaps.Backbone.js => Backbone.js} | 0 frontend/src/Metamaps/Constants.js | 57 +++++++++ .../{Metamaps.Control.js => Control.js} | 0 .../{Metamaps.Create.js => Create.js} | 0 .../Metamaps/{Metamaps.Debug.js => Debug.js} | 0 .../{Metamaps.Filter.js => Filter.js} | 0 .../{Metamaps.GlobalUI.js => GlobalUI.js} | 0 .../{Metamaps.Import.js => Import.js} | 0 .../src/Metamaps/{Metamaps.JIT.js => JIT.js} | 1 + .../{Metamaps.Listeners.js => Listeners.js} | 0 .../src/Metamaps/{Metamaps.Map.js => Map.js} | 0 .../{Metamaps.Mapper.js => Mapper.js} | 0 .../{Metamaps.Mobile.js => Mobile.js} | 0 .../{Metamaps.Organize.js => Organize.js} | 0 .../{Metamaps.PasteInput.js => PasteInput.js} | 0 ....ReactComponents.js => ReactComponents.js} | 0 .../{Metamaps.Realtime.js => Realtime.js} | 0 .../{Metamaps.Router.js => Router.js} | 0 .../{Metamaps.Synapse.js => Synapse.js} | 0 ...Metamaps.SynapseCard.js => SynapseCard.js} | 0 .../Metamaps/{Metamaps.Topic.js => Topic.js} | 0 .../{Metamaps.TopicCard.js => TopicCard.js} | 0 .../Metamaps/{Metamaps.Util.js => Util.js} | 0 .../Metamaps/{Metamaps.Views.js => Views.js} | 0 .../{Metamaps.Visualize.js => Visualize.js} | 0 frontend/src/Metamaps/index.js | 110 +++++------------- 29 files changed, 86 insertions(+), 82 deletions(-) rename frontend/src/Metamaps/{Metamaps.Account.js => Account.js} (100%) rename frontend/src/Metamaps/{Metamaps.Admin.js => Admin.js} (100%) rename frontend/src/Metamaps/{Metamaps.AutoLayout.js => AutoLayout.js} (100%) rename frontend/src/Metamaps/{Metamaps.Backbone.js => Backbone.js} (100%) create mode 100644 frontend/src/Metamaps/Constants.js rename frontend/src/Metamaps/{Metamaps.Control.js => Control.js} (100%) rename frontend/src/Metamaps/{Metamaps.Create.js => Create.js} (100%) rename frontend/src/Metamaps/{Metamaps.Debug.js => Debug.js} (100%) rename frontend/src/Metamaps/{Metamaps.Filter.js => Filter.js} (100%) rename frontend/src/Metamaps/{Metamaps.GlobalUI.js => GlobalUI.js} (100%) rename frontend/src/Metamaps/{Metamaps.Import.js => Import.js} (100%) rename frontend/src/Metamaps/{Metamaps.JIT.js => JIT.js} (99%) rename frontend/src/Metamaps/{Metamaps.Listeners.js => Listeners.js} (100%) rename frontend/src/Metamaps/{Metamaps.Map.js => Map.js} (100%) rename frontend/src/Metamaps/{Metamaps.Mapper.js => Mapper.js} (100%) rename frontend/src/Metamaps/{Metamaps.Mobile.js => Mobile.js} (100%) rename frontend/src/Metamaps/{Metamaps.Organize.js => Organize.js} (100%) rename frontend/src/Metamaps/{Metamaps.PasteInput.js => PasteInput.js} (100%) rename frontend/src/Metamaps/{Metamaps.ReactComponents.js => ReactComponents.js} (100%) rename frontend/src/Metamaps/{Metamaps.Realtime.js => Realtime.js} (100%) rename frontend/src/Metamaps/{Metamaps.Router.js => Router.js} (100%) rename frontend/src/Metamaps/{Metamaps.Synapse.js => Synapse.js} (100%) rename frontend/src/Metamaps/{Metamaps.SynapseCard.js => SynapseCard.js} (100%) rename frontend/src/Metamaps/{Metamaps.Topic.js => Topic.js} (100%) rename frontend/src/Metamaps/{Metamaps.TopicCard.js => TopicCard.js} (100%) rename frontend/src/Metamaps/{Metamaps.Util.js => Util.js} (100%) rename frontend/src/Metamaps/{Metamaps.Views.js => Views.js} (100%) rename frontend/src/Metamaps/{Metamaps.Visualize.js => Visualize.js} (100%) diff --git a/frontend/src/Metamaps/Metamaps.Account.js b/frontend/src/Metamaps/Account.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Account.js rename to frontend/src/Metamaps/Account.js diff --git a/frontend/src/Metamaps/Metamaps.Admin.js b/frontend/src/Metamaps/Admin.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Admin.js rename to frontend/src/Metamaps/Admin.js diff --git a/frontend/src/Metamaps/Metamaps.AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.AutoLayout.js rename to frontend/src/Metamaps/AutoLayout.js diff --git a/frontend/src/Metamaps/Metamaps.Backbone.js b/frontend/src/Metamaps/Backbone.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Backbone.js rename to frontend/src/Metamaps/Backbone.js diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js new file mode 100644 index 00000000..f56c463a --- /dev/null +++ b/frontend/src/Metamaps/Constants.js @@ -0,0 +1,57 @@ +window.Metamaps = window.Metamaps || {} + +// TODO eliminate these 5 top-level variables +Metamaps.panningInt = null +Metamaps.tempNode = null +Metamaps.tempInit = false +Metamaps.tempNode2 = null + +Metamaps.Settings = { + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' + }, + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} + +Metamaps.Touch = { + touchPos: null, // this stores the x and y values of a current touch event + touchDragNode: null // this stores a reference to a JIT node that is being dragged +} + +Metamaps.Mouse = { + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} + +Metamaps.Selected = { + reset: function () { + var self = Metamaps.Selected + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} diff --git a/frontend/src/Metamaps/Metamaps.Control.js b/frontend/src/Metamaps/Control.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Control.js rename to frontend/src/Metamaps/Control.js diff --git a/frontend/src/Metamaps/Metamaps.Create.js b/frontend/src/Metamaps/Create.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Create.js rename to frontend/src/Metamaps/Create.js diff --git a/frontend/src/Metamaps/Metamaps.Debug.js b/frontend/src/Metamaps/Debug.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Debug.js rename to frontend/src/Metamaps/Debug.js diff --git a/frontend/src/Metamaps/Metamaps.Filter.js b/frontend/src/Metamaps/Filter.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Filter.js rename to frontend/src/Metamaps/Filter.js diff --git a/frontend/src/Metamaps/Metamaps.GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.GlobalUI.js rename to frontend/src/Metamaps/GlobalUI.js diff --git a/frontend/src/Metamaps/Metamaps.Import.js b/frontend/src/Metamaps/Import.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Import.js rename to frontend/src/Metamaps/Import.js diff --git a/frontend/src/Metamaps/Metamaps.JIT.js b/frontend/src/Metamaps/JIT.js similarity index 99% rename from frontend/src/Metamaps/Metamaps.JIT.js rename to frontend/src/Metamaps/JIT.js index cfdd921d..57ab60bf 100644 --- a/frontend/src/Metamaps/Metamaps.JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,5 @@ window.Metamaps = window.Metamaps || {} + Metamaps.JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', diff --git a/frontend/src/Metamaps/Metamaps.Listeners.js b/frontend/src/Metamaps/Listeners.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Listeners.js rename to frontend/src/Metamaps/Listeners.js diff --git a/frontend/src/Metamaps/Metamaps.Map.js b/frontend/src/Metamaps/Map.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Map.js rename to frontend/src/Metamaps/Map.js diff --git a/frontend/src/Metamaps/Metamaps.Mapper.js b/frontend/src/Metamaps/Mapper.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Mapper.js rename to frontend/src/Metamaps/Mapper.js diff --git a/frontend/src/Metamaps/Metamaps.Mobile.js b/frontend/src/Metamaps/Mobile.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Mobile.js rename to frontend/src/Metamaps/Mobile.js diff --git a/frontend/src/Metamaps/Metamaps.Organize.js b/frontend/src/Metamaps/Organize.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Organize.js rename to frontend/src/Metamaps/Organize.js diff --git a/frontend/src/Metamaps/Metamaps.PasteInput.js b/frontend/src/Metamaps/PasteInput.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.PasteInput.js rename to frontend/src/Metamaps/PasteInput.js diff --git a/frontend/src/Metamaps/Metamaps.ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.ReactComponents.js rename to frontend/src/Metamaps/ReactComponents.js diff --git a/frontend/src/Metamaps/Metamaps.Realtime.js b/frontend/src/Metamaps/Realtime.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Realtime.js rename to frontend/src/Metamaps/Realtime.js diff --git a/frontend/src/Metamaps/Metamaps.Router.js b/frontend/src/Metamaps/Router.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Router.js rename to frontend/src/Metamaps/Router.js diff --git a/frontend/src/Metamaps/Metamaps.Synapse.js b/frontend/src/Metamaps/Synapse.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Synapse.js rename to frontend/src/Metamaps/Synapse.js diff --git a/frontend/src/Metamaps/Metamaps.SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.SynapseCard.js rename to frontend/src/Metamaps/SynapseCard.js diff --git a/frontend/src/Metamaps/Metamaps.Topic.js b/frontend/src/Metamaps/Topic.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Topic.js rename to frontend/src/Metamaps/Topic.js diff --git a/frontend/src/Metamaps/Metamaps.TopicCard.js b/frontend/src/Metamaps/TopicCard.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.TopicCard.js rename to frontend/src/Metamaps/TopicCard.js diff --git a/frontend/src/Metamaps/Metamaps.Util.js b/frontend/src/Metamaps/Util.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Util.js rename to frontend/src/Metamaps/Util.js diff --git a/frontend/src/Metamaps/Metamaps.Views.js b/frontend/src/Metamaps/Views.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Views.js rename to frontend/src/Metamaps/Views.js diff --git a/frontend/src/Metamaps/Metamaps.Visualize.js b/frontend/src/Metamaps/Visualize.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Visualize.js rename to frontend/src/Metamaps/Visualize.js diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index bf54483d..ef50b564 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,87 +1,33 @@ window.Metamaps = window.Metamaps || {} -// TODO eliminate these 5 top-level variables -Metamaps.panningInt = null -Metamaps.tempNode = null -Metamaps.tempInit = false -Metamaps.tempNode2 = null +import './Constants' -Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } - }, -} - -Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -} - -Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -} - -Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected - self.Nodes = [] - self.Edges = [] - }, - Nodes: [], - Edges: [] -} - -require('./Metamaps.Account') -require('./Metamaps.Admin') -require('./Metamaps.AutoLayout') -require('./Metamaps.Backbone') -require('./Metamaps.Control') -require('./Metamaps.Create') -require('./Metamaps.Debug') -require('./Metamaps.Filter') -require('./Metamaps.GlobalUI') -require('./Metamaps.Import') -require('./Metamaps.JIT') -require('./Metamaps.Listeners') -require('./Metamaps.Map') -require('./Metamaps.Mapper') -require('./Metamaps.Mobile') -require('./Metamaps.Organize') -require('./Metamaps.PasteInput') -require('./Metamaps.Realtime') -require('./Metamaps.Router') -require('./Metamaps.Synapse') -require('./Metamaps.SynapseCard') -require('./Metamaps.Topic') -require('./Metamaps.TopicCard') -require('./Metamaps.Util') -require('./Metamaps.Views') -require('./Metamaps.Visualize') -require('./Metamaps.ReactComponents') +import './Account' +import './Admin' +import './AutoLayout' +import './Backbone' +import './Control' +import './Create' +import './Debug' +import './Filter' +import './GlobalUI' +import './Import' +import './JIT' +import './Listeners' +import './Map' +import './Mapper' +import './Mobile' +import './Organize' +import './PasteInput' +import './Realtime' +import './Router' +import './Synapse' +import './SynapseCard' +import './Topic' +import './TopicCard' +import './Util' +import './Views' +import './Visualize' +import './ReactComponents' export default window.Metamaps From 03446f548aad8f6956d4339f35a12dec2b88007f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:21:59 +0800 Subject: [PATCH 013/378] start making the code modular. many files still need global scape --- frontend/src/Metamaps/Account.js | 2 + frontend/src/Metamaps/Admin.js | 13 +- frontend/src/Metamaps/AutoLayout.js | 13 +- frontend/src/Metamaps/Backbone.js | 2 + frontend/src/Metamaps/Constants.js | 1 - frontend/src/Metamaps/Control.js | 44 +-- frontend/src/Metamaps/Create.js | 74 ++-- frontend/src/Metamaps/Debug.js | 19 +- frontend/src/Metamaps/Filter.js | 51 +-- frontend/src/Metamaps/GlobalUI.js | 2 + frontend/src/Metamaps/Import.js | 19 +- frontend/src/Metamaps/JIT.js | 92 ++--- frontend/src/Metamaps/Listeners.js | 7 +- frontend/src/Metamaps/Map.js | 3 + frontend/src/Metamaps/Mapper.js | 26 +- frontend/src/Metamaps/Mobile.js | 7 +- frontend/src/Metamaps/Organize.js | 8 +- frontend/src/Metamaps/PasteInput.js | 9 +- frontend/src/Metamaps/ReactComponents.js | 6 +- frontend/src/Metamaps/Realtime.js | 154 ++++---- frontend/src/Metamaps/Router.js | 430 +++++++++++------------ frontend/src/Metamaps/Synapse.js | 11 +- frontend/src/Metamaps/SynapseCard.js | 15 +- frontend/src/Metamaps/Topic.js | 7 +- frontend/src/Metamaps/TopicCard.js | 29 +- frontend/src/Metamaps/Util.js | 9 +- frontend/src/Metamaps/Views.js | 15 +- frontend/src/Metamaps/Visualize.js | 14 +- frontend/src/Metamaps/index.js | 82 +++-- 29 files changed, 598 insertions(+), 566 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 66348481..95a1a69f 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -121,3 +121,5 @@ Metamaps.Account = { $('#user_password_confirmation').val('') } } + +export default Metamaps.Account diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 8dcaed5b..10cbc6d8 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -1,13 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.Admin.js.erb - * - * Dependencies: none! - */ - -Metamaps.Admin = { +const Admin = { selectMetacodes: [], allMetacodes: [], init: function () { @@ -53,3 +46,5 @@ Metamaps.Admin = { } } } + +export default Admin diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index 2360204b..386b61ef 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,13 +1,4 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps */ - -/* - * Metmaaps.AutoLayout.js - * - * Dependencies: none! - */ - -Metamaps.AutoLayout = { +const AutoLayout = { nextX: 0, nextY: 0, sideLength: 1, @@ -74,3 +65,5 @@ Metamaps.AutoLayout = { self.turnCount = 0 } } + +export default AutoLayout diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index 3c991e0b..ce62c6be 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -695,3 +695,5 @@ Metamaps.Backbone.init = function () { } self.attachCollectionEvents() }; // end Metamaps.Backbone.init + +export default Metamaps.Backbone diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index f56c463a..a79054e6 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -1,7 +1,6 @@ window.Metamaps = window.Metamaps || {} // TODO eliminate these 5 top-level variables -Metamaps.panningInt = null Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 33623927..b9df0d2c 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,12 +1,10 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* - * Metamaps.Control.js.erb + * Metamaps.Control.js * * Dependencies: * - Metamaps.Active - * - Metamaps.Control * - Metamaps.Filter * - Metamaps.GlobalUI * - Metamaps.JIT @@ -20,7 +18,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Control = { +const Control = { init: function () {}, selectNode: function (node, e) { var filtered = node.getData('alpha') === 0 @@ -34,7 +32,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { var node = Metamaps.Selected.Nodes[i] - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } Metamaps.Visualize.mGraph.plot() }, @@ -64,8 +62,8 @@ Metamaps.Control = { var r = confirm(text + 'Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.') if (r == true) { - Metamaps.Control.deleteSelectedEdges() - Metamaps.Control.deleteSelectedNodes() + Control.deleteSelectedEdges() + Control.deleteSelectedNodes() } }, deleteSelectedNodes: function () { // refers to deleting topics permanently @@ -81,7 +79,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { var node = Metamaps.Selected.Nodes[i] - Metamaps.Control.deleteNode(node.id) + Control.deleteNode(node.id) } }, deleteNode: function (nodeid) { // refers to deleting topics permanently @@ -106,7 +104,7 @@ Metamaps.Control = { $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ mappableid: mappableid }]) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) } else { Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted') } @@ -120,7 +118,7 @@ Metamaps.Control = { _.each(nodeids, function(nodeid) { if (Metamaps.Active.Topic.id !== nodeid) { Metamaps.Topics.remove(nodeid) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) } }) return @@ -139,7 +137,7 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { node = Metamaps.Selected.Nodes[i] - Metamaps.Control.removeNode(node.id) + Control.removeNode(node.id) } }, removeNode: function (nodeid) { // refers to removing topics permanently from a map @@ -161,7 +159,7 @@ Metamaps.Control = { $(document).trigger(Metamaps.JIT.events.removeTopic, [{ mappableid: mappableid }]) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) }, hideSelectedNodes: function () { var l = Metamaps.Selected.Nodes.length, @@ -170,14 +168,14 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { node = Metamaps.Selected.Nodes[i] - Metamaps.Control.hideNode(node.id) + Control.hideNode(node.id) } }, hideNode: function (nodeid) { var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) var graph = Metamaps.Visualize.mGraph - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) node.setData('alpha', 0, 'end') node.eachAdjacency(function (adj) { @@ -218,7 +216,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { var edge = Metamaps.Selected.Edges[i] - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) } Metamaps.Visualize.mGraph.plot() }, @@ -258,7 +256,7 @@ Metamaps.Control = { for (var i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.deleteEdge(edge) + Control.deleteEdge(edge) } }, deleteEdge: function (edge) { @@ -279,7 +277,7 @@ Metamaps.Control = { var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin') if (permToDelete) { if (edge.getData('synapses').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var mappableid = synapse.id synapse.destroy() @@ -315,7 +313,7 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.removeEdge(edge) + Control.removeEdge(edge) } Metamaps.Selected.Edges = [ ] }, @@ -330,7 +328,7 @@ Metamaps.Control = { } if (edge.getData('mappings').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 @@ -357,7 +355,7 @@ Metamaps.Control = { i for (i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } Metamaps.Selected.Edges = [ ] }, @@ -365,7 +363,7 @@ Metamaps.Control = { var from = edge.nodeFrom.id var to = edge.nodeTo.id edge.setData('alpha', 0, 'end') - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) Metamaps.Visualize.mGraph.fx.animate({ modes: ['edge-property:alpha'], duration: 500 @@ -449,4 +447,6 @@ Metamaps.Control = { Metamaps.GlobalUI.notifyUser(message) Metamaps.Visualize.mGraph.plot() }, -}; // end Metamaps.Control +} + +export default Control diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1bd4216f..1348e9d2 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -15,7 +15,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Create = { +const Create = { isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open selectedMetacodeSet: null, selectedMetacodeSetIndex: null, @@ -24,7 +24,7 @@ Metamaps.Create = { selectedMetacodes: [], newSelectedMetacodes: [], init: function () { - var self = Metamaps.Create + var self = Create self.newTopic.init() self.newSynapse.init() @@ -37,7 +37,7 @@ Metamaps.Create = { $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab }, toggleMetacodeSelected: function () { - var self = Metamaps.Create + var self = Create if ($(this).attr('class') != 'toggledOff') { $(this).addClass('toggledOff') @@ -52,29 +52,29 @@ Metamaps.Create = { } }, updateMetacodeSet: function (set, index, custom) { - if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { + if (custom && Create.newSelectedMetacodes.length == 0) { alert('Please select at least one metacode to use!') return false } var codesToSwitchToIds var metacodeModels = new Metamaps.Backbone.MetacodeCollection() - Metamaps.Create.selectedMetacodeSetIndex = index - Metamaps.Create.selectedMetacodeSet = 'metacodeset-' + set + Create.selectedMetacodeSetIndex = index + Create.selectedMetacodeSet = 'metacodeset-' + set if (!custom) { codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(',') $('.customMetacodeList li').addClass('toggledOff') - Metamaps.Create.selectedMetacodes = [] - Metamaps.Create.selectedMetacodeNames = [] - Metamaps.Create.newSelectedMetacodes = [] - Metamaps.Create.newSelectedMetacodeNames = [] + Create.selectedMetacodes = [] + Create.selectedMetacodeNames = [] + Create.newSelectedMetacodes = [] + Create.newSelectedMetacodeNames = [] } else if (custom) { // uses .slice to avoid setting the two arrays to the same actual array - Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0) - Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0) - codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0) + Create.selectedMetacodes = Create.newSelectedMetacodes.slice(0) + Create.selectedMetacodeNames = Create.newSelectedMetacodeNames.slice(0) + codesToSwitchToIds = Create.selectedMetacodes.slice(0) } // sort by name @@ -106,7 +106,7 @@ Metamaps.Create = { var mdata = { 'metacodes': { - 'value': custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet + 'value': custom ? Create.selectedMetacodes.toString() : Create.selectedMetacodeSet } } $.ajax({ @@ -124,7 +124,7 @@ Metamaps.Create = { }, cancelMetacodeSetSwitch: function () { - var self = Metamaps.Create + var self = Create self.isSwitchingSet = false if (self.selectedMetacodeSet != 'metacodeset-custom') { @@ -149,17 +149,17 @@ Metamaps.Create = { newTopic: { init: function () { $('#topic_name').keyup(function () { - Metamaps.Create.newTopic.name = $(this).val() + Create.newTopic.name = $(this).val() }) $('.pinCarousel').click(function() { - if (Metamaps.Create.newTopic.pinned) { + if (Create.newTopic.pinned) { $('.pinCarousel').removeClass('isPinned') - Metamaps.Create.newTopic.pinned = false + Create.newTopic.pinned = false } else { $('.pinCarousel').addClass('isPinned') - Metamaps.Create.newTopic.pinned = true + Create.newTopic.pinned = true } }) @@ -221,24 +221,24 @@ Metamaps.Create = { $('#new_topic').fadeIn('fast', function () { $('#topic_name').focus() }) - Metamaps.Create.newTopic.beingCreated = true - Metamaps.Create.newTopic.name = '' + Create.newTopic.beingCreated = true + Create.newTopic.name = '' }, hide: function (force) { - if (force || !Metamaps.Create.newTopic.pinned) { + if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') - Metamaps.Create.newTopic.beingCreated = false + Create.newTopic.beingCreated = false } if (force) { $('.pinCarousel').removeClass('isPinned') - Metamaps.Create.newTopic.pinned = false + Create.newTopic.pinned = false } $('#topic_name').typeahead('val', '') } }, newSynapse: { init: function () { - var self = Metamaps.Create.newSynapse + var self = Create.newSynapse var synapseBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), @@ -254,7 +254,7 @@ Metamaps.Create = { remote: { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { - var self = Metamaps.Create.newSynapse + var self = Create.newSynapse if (Metamaps.Selected.Nodes.length < 2) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings @@ -300,13 +300,13 @@ Metamaps.Create = { if (e.keyCode === BACKSPACE && $(this).val() === '' || e.keyCode === DELETE && $(this).val() === '' || e.keyCode === ESC) { - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() } // if - Metamaps.Create.newSynapse.description = $(this).val() + Create.newSynapse.description = $(this).val() }) $('#synapse_desc').focusout(function () { - if (Metamaps.Create.newSynapse.beingCreated) { + if (Create.newSynapse.beingCreated) { Metamaps.Synapse.createSynapseLocally() } }) @@ -315,7 +315,7 @@ Metamaps.Create = { if (datum.id) { // if they clicked on an existing synapse get it Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) } else { - Metamaps.Create.newSynapse.description = datum.value + Create.newSynapse.description = datum.value Metamaps.Synapse.createSynapseLocally() } }) @@ -329,17 +329,19 @@ Metamaps.Create = { $('#new_synapse').fadeIn(100, function () { $('#synapse_desc').focus() }) - Metamaps.Create.newSynapse.beingCreated = true + Create.newSynapse.beingCreated = true }, hide: function () { $('#new_synapse').fadeOut('fast') $('#synapse_desc').typeahead('val', '') - Metamaps.Create.newSynapse.beingCreated = false - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = 0 - Metamaps.Create.newSynapse.topic2id = 0 + Create.newSynapse.beingCreated = false + Create.newTopic.addSynapse = false + Create.newSynapse.topic1id = 0 + Create.newSynapse.topic2id = 0 Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Visualize.mGraph.plot() }, } -}; // end Metamaps.Create +} + +export default Create diff --git a/frontend/src/Metamaps/Debug.js b/frontend/src/Metamaps/Debug.js index 7bc71979..e8e40e69 100644 --- a/frontend/src/Metamaps/Debug.js +++ b/frontend/src/Metamaps/Debug.js @@ -1,15 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* - * Metamaps.Debug.js.erb - * - * Dependencies: none! - */ +const Debug = () => { + console.debug(window.Metamaps) + console.debug(`Metamaps Version: ${window.Metamaps.VERSION}`) +} -Metamaps.Debug = function () { - console.debug(Metamaps) - console.debug('Metamaps Version: ' + Metamaps.VERSION) -} -Metamaps.debug = function () { - Metamaps.Debug() -window.Metamaps = window.Metamaps || {} -} +export default Debug diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 367918e6..cc21f7e2 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -16,7 +15,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Topics * - Metamaps.Visualize */ -Metamaps.Filter = { +const Filter = { filters: { name: '', metacodes: [], @@ -31,7 +30,7 @@ Metamaps.Filter = { isOpen: false, changing: false, init: function () { - var self = Metamaps.Filter + var self = Filter $('.sidebarFilterIcon').click(self.toggleBox) @@ -46,7 +45,7 @@ Metamaps.Filter = { self.getFilterData() }, toggleBox: function (event) { - var self = Metamaps.Filter + var self = Filter if (self.isOpen) self.close() else self.open() @@ -54,7 +53,7 @@ Metamaps.Filter = { event.stopPropagation() }, open: function () { - var self = Metamaps.Filter + var self = Filter Metamaps.GlobalUI.Account.close() $('.sidebarFilterIcon div').addClass('hide') @@ -70,7 +69,7 @@ Metamaps.Filter = { } }, close: function () { - var self = Metamaps.Filter + var self = Filter $('.sidebarFilterIcon div').removeClass('hide') if (!self.changing) { @@ -83,7 +82,7 @@ Metamaps.Filter = { } }, reset: function () { - var self = Metamaps.Filter + var self = Filter self.filters.metacodes = [] self.filters.mappers = [] @@ -103,7 +102,7 @@ Metamaps.Filter = { But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses */ getFilterData: function () { - var self = Metamaps.Filter + var self = Filter var metacode, mapper, synapse @@ -126,7 +125,7 @@ Metamaps.Filter = { }) }, bindLiClicks: function () { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode) $('#filter_by_mapper ul li').unbind().click(self.toggleMapper) $('#filter_by_synapse ul li').unbind().click(self.toggleSynapse) @@ -137,7 +136,7 @@ Metamaps.Filter = { @param */ updateFilters: function (collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { - var self = Metamaps.Filter + var self = Filter var newList = [] var removed = [] @@ -212,11 +211,11 @@ Metamaps.Filter = { self.bindLiClicks() }, checkMetacodes: function () { - var self = Metamaps.Filter + var self = Filter self.updateFilters('Topics', 'metacode_id', 'Metacodes', 'metacodes', 'metacode') }, checkMappers: function () { - var self = Metamaps.Filter + var self = Filter var onMap = Metamaps.Active.Map ? true : false if (onMap) { self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') @@ -226,11 +225,11 @@ Metamaps.Filter = { } }, checkSynapses: function () { - var self = Metamaps.Filter + var self = Filter self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') }, filterAllMetacodes: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').addClass('toggledOff') $('.showAllMetacodes').removeClass('active') $('.hideAllMetacodes').addClass('active') @@ -238,7 +237,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoMetacodes: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').removeClass('toggledOff') $('.showAllMetacodes').addClass('active') $('.hideAllMetacodes').removeClass('active') @@ -246,7 +245,7 @@ Metamaps.Filter = { self.passFilters() }, filterAllMappers: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_mapper ul li').addClass('toggledOff') $('.showAllMappers').removeClass('active') $('.hideAllMappers').addClass('active') @@ -254,7 +253,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoMappers: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_mapper ul li').removeClass('toggledOff') $('.showAllMappers').addClass('active') $('.hideAllMappers').removeClass('active') @@ -262,7 +261,7 @@ Metamaps.Filter = { self.passFilters() }, filterAllSynapses: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_synapse ul li').addClass('toggledOff') $('.showAllSynapses').removeClass('active') $('.hideAllSynapses').addClass('active') @@ -270,7 +269,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoSynapses: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_synapse ul li').removeClass('toggledOff') $('.showAllSynapses').addClass('active') $('.hideAllSynapses').removeClass('active') @@ -281,7 +280,7 @@ Metamaps.Filter = { // to reduce code redundancy // gets called in the context of a list item in a filter box toggleLi: function (whichToFilter) { - var self = Metamaps.Filter, index + var self = Filter, index var id = $(this).attr('data-id') if (self.visible[whichToFilter].indexOf(id) == -1) { self.visible[whichToFilter].push(id) @@ -294,7 +293,7 @@ Metamaps.Filter = { self.passFilters() }, toggleMetacode: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'metacodes') if (self.visible.metacodes.length === self.filters.metacodes.length) { @@ -310,7 +309,7 @@ Metamaps.Filter = { } }, toggleMapper: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'mappers') if (self.visible.mappers.length === self.filters.mappers.length) { @@ -326,7 +325,7 @@ Metamaps.Filter = { } }, toggleSynapse: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'synapses') if (self.visible.synapses.length === self.filters.synapses.length) { @@ -342,7 +341,7 @@ Metamaps.Filter = { } }, passFilters: function () { - var self = Metamaps.Filter + var self = Filter var visible = self.visible var passesMetacode, passesMapper, passesSynapse @@ -464,4 +463,6 @@ Metamaps.Filter = { duration: 200 }) } -}; // end Metamaps.Filter +} + +export default Filter diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index d5fe6caa..e4522c2f 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -678,3 +678,5 @@ Metamaps.GlobalUI.Search = { $('#searchLoading').show(); } } + +export default Metamaps.GlobalUI diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 426071f0..e963ca32 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -14,7 +13,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Topics */ -Metamaps.Import = { +const Import = { // note that user is not imported topicWhitelist: [ 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission' @@ -25,19 +24,19 @@ Metamaps.Import = { cidMappings: {}, // to be filled by import_id => cid mappings handleTSV: function (text) { - var self = Metamaps.Import + var self = Import results = self.parseTabbedString(text) self.handle(results) }, handleJSON: function (text) { - var self = Metamaps.Import + var self = Import results = JSON.parse(text) self.handle(results) }, handle: function(results) { - var self = Metamaps.Import + var self = Import var topics = results.topics var synapses = results.synapses @@ -61,7 +60,7 @@ Metamaps.Import = { }, parseTabbedString: function (text) { - var self = Metamaps.Import + var self = Import // determine line ending and split lines var delim = '\n' @@ -187,7 +186,7 @@ Metamaps.Import = { }, importTopics: function (parsedTopics) { - var self = Metamaps.Import + var self = Import // up to 25 topics: scale 100 // up to 81 topics: scale 200 @@ -220,7 +219,7 @@ Metamaps.Import = { }, importSynapses: function (parsedSynapses) { - var self = Metamaps.Import + var self = Import parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted @@ -256,7 +255,7 @@ Metamaps.Import = { createTopicWithParameters: function (name, metacode_name, permission, desc, link, xloc, yloc, import_id, opts) { - var self = Metamaps.Import + var self = Import $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null if (metacode === null) { @@ -325,3 +324,5 @@ Metamaps.Import = { Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true) } } + +export default Import diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 57ab60bf..a93e3341 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,6 +1,8 @@ -window.Metamaps = window.Metamaps || {} +/* global Metamaps */ -Metamaps.JIT = { +let panningInt + +const JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', newTopic: 'Metamaps:JIT:events:newTopic', @@ -18,7 +20,7 @@ Metamaps.JIT = { * This method will bind the event handlers it is interested and initialize the class. */ init: function () { - var self = Metamaps.JIT + var self = JIT $('.zoomIn').click(self.zoomIn) $('.zoomOut').click(self.zoomOut) @@ -94,7 +96,7 @@ Metamaps.JIT = { return [jitReady, synapsesToRemove] }, prepareVizData: function () { - var self = Metamaps.JIT + var self = JIT var mapping // reset/empty vizData @@ -148,7 +150,7 @@ Metamaps.JIT = { var color = Metamaps.Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } - Metamaps.JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) + JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) // check for edge label in data var desc = synapse.get('desc') @@ -253,7 +255,7 @@ Metamaps.JIT = { duration: 800, onComplete: function () { Metamaps.Visualize.mGraph.busy = false - $(document).trigger(Metamaps.JIT.events.animationDone) + $(document).trigger(JIT.events.animationDone) } }, animateFDLayout: { @@ -323,26 +325,26 @@ Metamaps.JIT = { enable: true, enableForEdges: true, onMouseMove: function (node, eventInfo, e) { - Metamaps.JIT.onMouseMoveHandler(node, eventInfo, e) + JIT.onMouseMoveHandler(node, eventInfo, e) // console.log('called mouse move handler') }, // Update node positions when dragged onDragMove: function (node, eventInfo, e) { - Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e) + JIT.onDragMoveTopicHandler(node, eventInfo, e) // console.log('called drag move handler') }, onDragEnd: function (node, eventInfo, e) { - Metamaps.JIT.onDragEndTopicHandler(node, eventInfo, e, false) + JIT.onDragEndTopicHandler(node, eventInfo, e, false) // console.log('called drag end handler') }, onDragCancel: function (node, eventInfo, e) { - Metamaps.JIT.onDragCancelHandler(node, eventInfo, e, false) + JIT.onDragCancelHandler(node, eventInfo, e, false) }, // Implement the same handler for touchscreens onTouchStart: function (node, eventInfo, e) {}, // Implement the same handler for touchscreens onTouchMove: function (node, eventInfo, e) { - Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e) + JIT.onDragMoveTopicHandler(node, eventInfo, e) }, // Implement the same handler for touchscreens onTouchEnd: function (node, eventInfo, e) {}, @@ -361,7 +363,7 @@ Metamaps.JIT = { var bS = Metamaps.Mouse.boxStartCoordinates var bE = Metamaps.Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { - Metamaps.JIT.zoomToBox(e) + JIT.zoomToBox(e) return } else { Metamaps.Mouse.boxStartCoordinates = null @@ -373,7 +375,7 @@ Metamaps.JIT = { if (e.shiftKey) { Metamaps.Visualize.mGraph.busy = false Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() - Metamaps.JIT.selectWithBox(e) + JIT.selectWithBox(e) // console.log('called select with box') return } @@ -383,13 +385,13 @@ Metamaps.JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnClickHandler(node, e) + JIT.selectEdgeOnClickHandler(node, e) // console.log('called selectEdgeOnClickHandler') } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnClickHandler(node, e) + JIT.selectNodeOnClickHandler(node, e) // console.log('called selectNodeOnClickHandler') } else { - Metamaps.JIT.canvasClickHandler(eventInfo.getPos(), e) + JIT.canvasClickHandler(eventInfo.getPos(), e) // console.log('called canvasClickHandler') } // if }, @@ -401,7 +403,7 @@ Metamaps.JIT = { if (Metamaps.Mouse.boxStartCoordinates) { Metamaps.Visualize.mGraph.busy = false Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() - Metamaps.JIT.selectWithBox(e) + JIT.selectWithBox(e) return } @@ -409,9 +411,9 @@ Metamaps.JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnRightClickHandler(node, e) + JIT.selectEdgeOnRightClickHandler(node, e) } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnRightClickHandler(node, e) + JIT.selectNodeOnRightClickHandler(node, e) } else { // console.log('right clicked on open space') } @@ -455,7 +457,7 @@ Metamaps.JIT = { // if the topic has a link, draw a small image to indicate that var hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null - var linkImage = Metamaps.JIT.topicLinkImage + var linkImage = JIT.topicLinkImage var linkImageLoaded = linkImage.complete || (typeof linkImage.naturalWidth !== 'undefined' && linkImage.naturalWidth !== 0) @@ -465,7 +467,7 @@ Metamaps.JIT = { // if the topic has a desc, draw a small image to indicate that var hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null - var descImage = Metamaps.JIT.topicDescImage + var descImage = JIT.topicDescImage var descImageLoaded = descImage.complete || (typeof descImage.naturalWidth !== 'undefined' && descImage.naturalWidth !== 0) @@ -500,7 +502,7 @@ Metamaps.JIT = { edgeSettings: { 'customEdge': { 'render': function (adj, canvas) { - Metamaps.JIT.edgeRender(adj, canvas) + JIT.edgeRender(adj, canvas) }, 'contains': function (adj, pos) { var from = adj.nodeFrom.pos.getc(), @@ -667,7 +669,7 @@ Metamaps.JIT = { Metamaps.Visualize.mGraph.plot() }, // onMouseLeave onMouseMoveHandler: function (node, eventInfo, e) { - var self = Metamaps.JIT + var self = JIT if (Metamaps.Visualize.mGraph.busy) return @@ -721,7 +723,7 @@ Metamaps.JIT = { Metamaps.Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { - var self = Metamaps.JIT + var self = JIT // this is used to send nodes that are moving to // other realtime collaborators on the same map @@ -751,7 +753,7 @@ Metamaps.JIT = { // to be the same as on other collaborators // maps positionsToSend[topic.id] = pos - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { var len = Metamaps.Selected.Nodes.length @@ -782,7 +784,7 @@ Metamaps.JIT = { } // for if (Metamaps.Active.Map) { - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } // if @@ -1201,7 +1203,7 @@ Metamaps.JIT = { selectNodeOnClickHandler: function (node, e) { if (Metamaps.Visualize.mGraph.busy) return - var self = Metamaps.JIT + var self = JIT // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { @@ -1222,7 +1224,7 @@ Metamaps.JIT = { } else { // wait a certain length of time, then check again, then run this code setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { + if (!JIT.nodeWasDoubleClicked()) { var nodeAlreadySelected = node.selected if (!e.shiftKey) { @@ -1403,7 +1405,7 @@ Metamaps.JIT = { var fetch_sent = false $('.rc-siblings').hover(function () { if (!fetch_sent) { - Metamaps.JIT.populateRightClickSiblings(node) + JIT.populateRightClickSiblings(node) fetch_sent = true } }) @@ -1414,7 +1416,7 @@ Metamaps.JIT = { }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { - var self = Metamaps.JIT + var self = JIT // depending on how many topics are selected, do different things @@ -1456,7 +1458,7 @@ Metamaps.JIT = { selectEdgeOnClickHandler: function (adj, e) { if (Metamaps.Visualize.mGraph.busy) return - var self = Metamaps.JIT + var self = JIT // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { @@ -1471,7 +1473,7 @@ Metamaps.JIT = { } else { // wait a certain length of time, then check again, then run this code setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { + if (!JIT.nodeWasDoubleClicked()) { var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { @@ -1611,17 +1613,17 @@ Metamaps.JIT = { easing = 1 // frictional value easing = 1 - window.clearInterval(Metamaps.panningInt) - Metamaps.panningInt = setInterval(function () { + window.clearInterval(panningInt) + panningInt = setInterval(function () { myTimer() }, 1) function myTimer () { Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) - $(document).trigger(Metamaps.JIT.events.pan) + $(document).trigger(JIT.events.pan) easing = easing * 0.75 - if (easing < 0.1) window.clearInterval(Metamaps.panningInt) + if (easing < 0.1) window.clearInterval(panningInt) } }, // SmoothPanning renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { @@ -1666,7 +1668,7 @@ Metamaps.JIT = { ctx.stroke() }, // renderMidArrow renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { - var self = Metamaps.JIT + var self = JIT var directionCat = synapse.get('category') var direction = synapse.getDirection() @@ -1720,11 +1722,11 @@ Metamaps.JIT = { }, // renderEdgeArrows zoomIn: function (event) { Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }, zoomOut: function (event) { Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { var offsetScale = canvas.scaleOffsetX @@ -1743,7 +1745,7 @@ Metamaps.JIT = { eY = Metamaps.Mouse.boxEndCoordinates.y var canvas = Metamaps.Visualize.mGraph.canvas - Metamaps.JIT.centerMap(canvas) + JIT.centerMap(canvas) var height = $(document).height(), width = $(document).width() @@ -1770,14 +1772,14 @@ Metamaps.JIT = { var cogY = (sY + eY) / 2 canvas.translate(-1 * cogX, -1 * cogY) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) Metamaps.Mouse.boxStartCoordinates = false Metamaps.Mouse.boxEndCoordinates = false Metamaps.Visualize.mGraph.plot() }, zoomExtents: function (event, canvas, denySelected) { - Metamaps.JIT.centerMap(canvas) + JIT.centerMap(canvas) var height = canvas.getSize().height, width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 @@ -1847,7 +1849,7 @@ Metamaps.JIT = { canvas.scale(scaleMultiplier, scaleMultiplier) } - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) } else if (nodes.length == 1) { nodes.forEach(function (n) { @@ -1855,8 +1857,10 @@ Metamaps.JIT = { y = n.pos.y canvas.translate(-1 * x, -1 * y) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }) } } } + +export default JIT diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index e6c4e1b9..af244961 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,7 +9,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.JIT * - Metamaps.Visualize */ -Metamaps.Listeners = { +const Listeners = { init: function () { var self = this $(document).on('keydown', function (e) { @@ -120,4 +119,6 @@ Metamaps.Listeners = { Metamaps.Topic.fetchRelatives(nodes) } } -}; // end Metamaps.Listeners +} + +export default Listeners diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map.js index e925a92c..690c2a6d 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map.js @@ -1,4 +1,5 @@ window.Metamaps = window.Metamaps || {} + /* global Metamaps, $ */ /* @@ -753,3 +754,5 @@ Metamaps.Map.InfoBox = { } } }; // end Metamaps.Map.InfoBox + +export default Metamaps.Map diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index f8a530b8..114d4f8c 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,21 +1,21 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* global Metamaps */ /* - * Metamaps.Mapper.js.erb - * - * Dependencies: none! + * Dependencies: + * - Metamaps.Backbone */ - -Metamaps.Mapper = { +const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve get: function (id, callback) { - return $.ajax({ - url: '/users/' + id + '.json', - success: function (data) { - callback(new Metamaps.Backbone.Mapper(data)) - } + return fetch(`/users/${id}.json`, { + }).then(response => { + if (!response.ok) throw response + return response.json() + }).then(payload => { + callback(new Metamaps.Backbone.Mapper(payload)) }) } -}; // end Metamaps.Mapper +} + +export default Mapper diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index fcd76b2f..e062ca45 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -9,7 +8,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Map */ -Metamaps.Mobile = { +const Mobile = { init: function () { var self = Metamaps.Mobile @@ -23,7 +22,7 @@ Metamaps.Mobile = { $('#header_content').width($(document).width() - 70) }, liClick: function () { - var self = Metamaps.Mobile + var self = Mobile $('#header_content').html($(this).text()) self.toggleMenu() }, @@ -36,3 +35,5 @@ Metamaps.Mobile = { } } } + +export default Mobile diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index 220cb83a..71905568 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -7,8 +6,7 @@ window.Metamaps = window.Metamaps || {} * Dependencies: * - Metamaps.Visualize */ -Metamaps.Organize = { - init: function () {}, +const Organize = { arrange: function (layout, centerNode) { // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin if (layout == 'grid') { @@ -115,4 +113,6 @@ Metamaps.Organize = { var newOriginY = (lowY + highY) / 2 } else alert('please call function with a valid layout dammit!') } -}; // end Metamaps.Organize +} + +export default Organize diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 3e933e41..9676e783 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -9,12 +8,12 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.AutoLayout */ -Metamaps.PasteInput = { +const PasteInput = { // thanks to https://github.com/kevva/url-regex URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'), init: function () { - var self = Metamaps.PasteInput + var self = PasteInput // intercept dragged files // see http://stackoverflow.com/questions/6756583 @@ -59,7 +58,7 @@ Metamaps.PasteInput = { }, handle: function(text, coords) { - var self = Metamaps.PasteInput + var self = PasteInput if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) @@ -109,3 +108,5 @@ Metamaps.PasteInput = { Metamaps.Import.handleTSV(text) } } + +export default PasteInput diff --git a/frontend/src/Metamaps/ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js index a1de0f40..a2495245 100644 --- a/frontend/src/Metamaps/ReactComponents.js +++ b/frontend/src/Metamaps/ReactComponents.js @@ -1,7 +1,7 @@ -window.Metamaps = window.Metamaps || {} - import Maps from '../components/Maps' -Metamaps.ReactComponents = { +const ReactComponents = { Maps } + +export default ReactComponents diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 0b62648f..35d00f06 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,5 +1,3 @@ -window.Metamaps = window.Metamaps || {} - /* global Metamaps, $ */ /* @@ -26,7 +24,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Realtime = { +const Realtime = { videoId: 'video-wrapper', socket: null, webrtc: null, @@ -39,7 +37,7 @@ Metamaps.Realtime = { inConversation: false, localVideo: null, init: function () { - var self = Metamaps.Realtime + var self = Realtime self.addJuntoListeners() @@ -102,7 +100,7 @@ Metamaps.Realtime = { } // if Metamaps.Active.Mapper }, addJuntoListeners: function () { - var self = Metamaps.Realtime + var self = Realtime $(document).on(Metamaps.Views.chatView.events.openTray, function () { $('.main').addClass('compressed') @@ -128,7 +126,7 @@ Metamaps.Realtime = { }) }, handleVideoAdded: function (v, id) { - var self = Metamaps.Realtime + var self = Realtime self.positionVideos() v.setParent($('#wrapper')) v.$container.find('.video-cutoff').css({ @@ -137,7 +135,7 @@ Metamaps.Realtime = { $('#wrapper').append(v.$container) }, positionVideos: function () { - var self = Metamaps.Realtime + var self = Realtime var videoIds = Object.keys(self.room.videos) var numOfVideos = videoIds.length var numOfVideosToPosition = _.filter(videoIds, function (id) { @@ -169,7 +167,7 @@ Metamaps.Realtime = { } // do self first - var myVideo = Metamaps.Realtime.localVideo.view + var myVideo = Realtime.localVideo.view if (!myVideo.manuallyPositioned) { myVideo.$container.css({ top: yFormula() + 'px', @@ -187,7 +185,7 @@ Metamaps.Realtime = { }) }, startActiveMap: function () { - var self = Metamaps.Realtime + var self = Realtime if (Metamaps.Active.Map && Metamaps.Active.Mapper) { if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { @@ -200,7 +198,7 @@ Metamaps.Realtime = { } }, endActiveMap: function () { - var self = Metamaps.Realtime + var self = Realtime $(document).off('.map') self.socket.removeAllListeners() @@ -215,7 +213,7 @@ Metamaps.Realtime = { } }, turnOn: function (notify) { - var self = Metamaps.Realtime + var self = Realtime if (notify) self.sendRealtimeOn() self.status = true @@ -238,11 +236,11 @@ Metamaps.Realtime = { self.room.chat.addParticipant(self.activeMapper) }, checkForACallToJoin: function () { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) }, promptToJoin: function () { - var self = Metamaps.Realtime + var self = Realtime var notifyText = "There's a conversation happening, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' @@ -251,7 +249,7 @@ Metamaps.Realtime = { self.room.conversationInProgress() }, conversationHasBegun: function () { - var self = Metamaps.Realtime + var self = Realtime if (self.inConversation) return var notifyText = "There's a conversation starting, want to join?" @@ -261,7 +259,7 @@ Metamaps.Realtime = { self.room.conversationInProgress() }, countOthersInConversation: function () { - var self = Metamaps.Realtime + var self = Realtime var count = 0 for (var key in self.mappersOnMap) { @@ -270,7 +268,7 @@ Metamaps.Realtime = { return count }, mapperJoinedCall: function (id) { - var self = Metamaps.Realtime + var self = Realtime var mapper = self.mappersOnMap[id] if (mapper) { @@ -285,7 +283,7 @@ Metamaps.Realtime = { } }, mapperLeftCall: function (id) { - var self = Metamaps.Realtime + var self = Realtime var mapper = self.mappersOnMap[id] if (mapper) { @@ -305,7 +303,7 @@ Metamaps.Realtime = { } }, callEnded: function () { - var self = Metamaps.Realtime + var self = Realtime self.room.conversationEnding() self.room.leaveVideoOnly() @@ -324,7 +322,7 @@ Metamaps.Realtime = { self.webrtc.webrtc.localStreams = [] }, invitedToCall: function (inviter) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.room.chat.sound.play('sessioninvite') @@ -337,7 +335,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(notifyText, true) }, invitedToJoin: function (inviter) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.room.chat.sound.play('sessioninvite') @@ -349,7 +347,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(notifyText, true) }, acceptCall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callAccepted', { mapid: Metamaps.Active.Map.id, @@ -361,7 +359,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, denyCall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callDenied', { mapid: Metamaps.Active.Map.id, @@ -371,7 +369,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, denyInvite: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('inviteDenied', { mapid: Metamaps.Active.Map.id, @@ -381,7 +379,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, inviteACall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('inviteACall', { mapid: Metamaps.Active.Map.id, inviter: Metamaps.Active.Mapper.id, @@ -391,7 +389,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, inviteToJoin: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('inviteToJoin', { mapid: Metamaps.Active.Map.id, inviter: Metamaps.Active.Mapper.id, @@ -400,7 +398,7 @@ Metamaps.Realtime = { self.room.chat.invitationPending(userid) }, callAccepted: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser('Conversation starting...') @@ -408,21 +406,21 @@ Metamaps.Realtime = { self.room.chat.invitationAnswered(userid) }, callDenied: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, inviteDenied: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, joinCall: function () { - var self = Metamaps.Realtime + var self = Realtime self.webrtc.off('readyToCall') self.webrtc.once('readyToCall', function () { @@ -446,7 +444,7 @@ Metamaps.Realtime = { self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) }, leaveCall: function () { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('mapperLeftCall', { mapid: Metamaps.Active.Map.id, @@ -465,7 +463,7 @@ Metamaps.Realtime = { } }, turnOff: function (silent) { - var self = Metamaps.Realtime + var self = Realtime if (self.status) { if (!silent) self.sendRealtimeOff() @@ -479,8 +477,8 @@ Metamaps.Realtime = { } }, setupSocket: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var myId = Metamaps.Active.Mapper.id socket.emit('newMapperNotify', { @@ -614,14 +612,14 @@ Metamaps.Realtime = { $(document).on(Metamaps.Views.room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket socket.on('mapChangeFromServer', self.mapChange) }, sendRealtimeOn: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // send this new mapper back your details, and the awareness that you're online var update = { @@ -632,8 +630,8 @@ Metamaps.Realtime = { socket.emit('notifyStartRealtime', update) }, sendRealtimeOff: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // send this new mapper back your details, and the awareness that you're online var update = { @@ -644,8 +642,8 @@ Metamaps.Realtime = { socket.emit('notifyStopRealtime', update) }, updateMapperList: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -675,8 +673,8 @@ Metamaps.Realtime = { } }, newPeerOnMap: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -743,8 +741,8 @@ Metamaps.Realtime = { }) }, lostPeerOnMap: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -763,8 +761,8 @@ Metamaps.Realtime = { } }, newCollaborator: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -777,8 +775,8 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') }, lostCollaborator: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -791,15 +789,15 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') }, updatePeerCoords: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket self.mappersOnMap[data.userid].coords = {x: data.usercoords.x,y: data.usercoords.y} self.positionPeerIcon(data.userid) }, positionPeerIcons: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket if (self.status) { // if i have realtime turned on for (var key in self.mappersOnMap) { @@ -811,8 +809,8 @@ Metamaps.Realtime = { } }, positionPeerIcon: function (id) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var boundary = self.chatOpen ? '#wrapper' : document var mapper = self.mappersOnMap[id] @@ -848,8 +846,8 @@ Metamaps.Realtime = { } }, limitPixelsToScreen: function (pixels) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var boundary = self.chatOpen ? '#wrapper' : document var xLimit, yLimit @@ -866,8 +864,8 @@ Metamaps.Realtime = { return {x: xLimit,y: yLimit} }, sendCoords: function (coords) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var map = Metamaps.Active.Map var mapper = Metamaps.Active.Mapper @@ -882,7 +880,7 @@ Metamaps.Realtime = { } }, sendTopicDrag: function (positions) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map && self.status) { @@ -891,7 +889,7 @@ Metamaps.Realtime = { } }, topicDrag: function (positions) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var topic @@ -907,7 +905,7 @@ Metamaps.Realtime = { } }, sendTopicChange: function (topic) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -929,7 +927,7 @@ Metamaps.Realtime = { } }, sendSynapseChange: function (synapse) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -952,7 +950,7 @@ Metamaps.Realtime = { } }, sendMapChange: function (map) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -989,7 +987,7 @@ Metamaps.Realtime = { }, // newMessage sendNewMessage: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var message = data.attributes @@ -997,14 +995,14 @@ Metamaps.Realtime = { socket.emit('newMessage', message) }, newMessage: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)) }, // newTopic sendNewTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map && self.status) { @@ -1016,7 +1014,7 @@ Metamaps.Realtime = { newTopic: function (data) { var topic, mapping, mapper, mapperCallback, cancel - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1063,7 +1061,7 @@ Metamaps.Realtime = { }, // removeTopic sendDeleteTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1072,7 +1070,7 @@ Metamaps.Realtime = { }, // removeTopic sendRemoveTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1081,7 +1079,7 @@ Metamaps.Realtime = { } }, removeTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1097,7 +1095,7 @@ Metamaps.Realtime = { }, // newSynapse sendNewSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1109,7 +1107,7 @@ Metamaps.Realtime = { newSynapse: function (data) { var topic1, topic2, node1, node2, synapse, mapping, cancel - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1160,7 +1158,7 @@ Metamaps.Realtime = { }, // deleteSynapse sendDeleteSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1170,7 +1168,7 @@ Metamaps.Realtime = { }, // removeSynapse sendRemoveSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1179,7 +1177,7 @@ Metamaps.Realtime = { } }, removeSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1202,4 +1200,6 @@ Metamaps.Realtime = { Metamaps.Mappings.remove(mapping) } }, -}; // end Metamaps.Realtime +} + +export default Realtime diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 417c9b9e..8aacadd1 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -16,231 +16,231 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -;(function () { - var Router = Backbone.Router.extend({ - routes: { - '': 'home', // #home - 'explore/:section': 'explore', // #explore/active - 'explore/:section/:id': 'explore', // #explore/mapper/1234 - 'maps/:id': 'maps' // #maps/7 - }, - home: function () { - clearTimeout(Metamaps.Router.timeoutId) +const _Router = Backbone.Router.extend({ + routes: { + '': 'home', // #home + 'explore/:section': 'explore', // #explore/active + 'explore/:section/:id': 'explore', // #explore/mapper/1234 + 'maps/:id': 'maps' // #maps/7 + }, + home: function () { + clearTimeout(Metamaps.Router.timeoutId) - if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' - else document.title = 'Home | Metamaps' + if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' + else document.title = 'Home | Metamaps' - Metamaps.Router.currentSection = '' - Metamaps.Router.currentPage = '' - $('.wrapper').removeClass('mapPage topicPage') + Metamaps.Router.currentSection = '' + Metamaps.Router.currentPage = '' + $('.wrapper').removeClass('mapPage topicPage') - var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' - $('.wrapper').addClass(classes) + var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' + $('.wrapper').addClass(classes) - var navigate = function () { - Metamaps.Router.timeoutId = setTimeout(function () { - Metamaps.Router.navigate('') - }, 300) - } + var navigate = function () { + Metamaps.Router.timeoutId = setTimeout(function () { + Metamaps.Router.navigate('') + }, 300) + } - // all this only for the logged in home page - if (Metamaps.Active.Mapper) { - $('.homeButton a').attr('href', '/') - Metamaps.GlobalUI.hideDiv('#yield') - - Metamaps.GlobalUI.showDiv('#explore') - - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - if (Metamaps.Maps.Active.length === 0) { - Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render - } else { - Metamaps.Views.exploreMaps.render(navigate) - } - } else { - // logged out home page - Metamaps.GlobalUI.hideDiv('#explore') - Metamaps.GlobalUI.showDiv('#yield') - Metamaps.Router.timeoutId = setTimeout(navigate, 500) - } - - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null - }, - explore: function (section, id) { - clearTimeout(Metamaps.Router.timeoutId) - - // just capitalize the variable section - // either 'featured', 'mapper', or 'active' - var capitalize = section.charAt(0).toUpperCase() + section.slice(1) - - if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') { - document.title = 'Explore ' + capitalize + ' Maps | Metamaps' - } else if (section === 'mapper') { - $.ajax({ - url: '/users/' + id + '.json', - success: function (response) { - document.title = response.name + ' | Metamaps' - }, - error: function () {} - }) - } else if (section === 'mine') { - document.title = 'Explore My Maps | Metamaps' - } - - if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) - $('.wrapper').removeClass('homePage mapPage topicPage') - $('.wrapper').addClass('explorePage') - - Metamaps.Router.currentSection = 'explore' - Metamaps.Router.currentPage = section - - // this will mean it's a mapper page being loaded - if (id) { - if (Metamaps.Maps.Mapper.mapperId !== id) { - // empty the collection if we are trying to load the maps - // collection of a different mapper than we had previously - Metamaps.Maps.Mapper.reset() - Metamaps.Maps.Mapper.page = 1 - } - Metamaps.Maps.Mapper.mapperId = id - } - - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) - - var navigate = function () { - var path = '/explore/' + Metamaps.Router.currentPage - - // alter url if for mapper profile page - if (Metamaps.Router.currentPage === 'mapper') { - path += '/' + Metamaps.Maps.Mapper.mapperId - } - - Metamaps.Router.navigate(path) - } - var navigateTimeout = function () { - Metamaps.Router.timeoutId = setTimeout(navigate, 300) - } - if (Metamaps.Maps[capitalize].length === 0) { - Metamaps.Loading.show() - setTimeout(function () { - Metamaps.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render - }, 300) // wait 300 milliseconds till the other animations are done to do the fetch - } else { - if (id) { - Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) - } else { - Metamaps.Views.exploreMaps.render(navigateTimeout) - } - } + // all this only for the logged in home page + if (Metamaps.Active.Mapper) { + $('.homeButton a').attr('href', '/') + Metamaps.GlobalUI.hideDiv('#yield') Metamaps.GlobalUI.showDiv('#explore') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null - }, - maps: function (id) { - clearTimeout(Metamaps.Router.timeoutId) - document.title = 'Map ' + id + ' | Metamaps' - - Metamaps.Router.currentSection = 'map' - Metamaps.Router.currentPage = id - - $('.wrapper').removeClass('homePage explorePage topicPage') - $('.wrapper').addClass('mapPage') - // another class will be added to wrapper if you - // can edit this map '.canEditMap' - - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) - } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Topic.end() - Metamaps.Active.Topic = null - - Metamaps.Loading.show() - Metamaps.Map.end() - Metamaps.Map.launch(id) - }, - topics: function (id) { - clearTimeout(Metamaps.Router.timeoutId) - - document.title = 'Topic ' + id + ' | Metamaps' - - Metamaps.Router.currentSection = 'topic' - Metamaps.Router.currentPage = id - - $('.wrapper').removeClass('homePage explorePage mapPage') - $('.wrapper').addClass('topicPage') - - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) - } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Map.end() - Metamaps.Active.Map = null - - Metamaps.Topic.end() - Metamaps.Topic.launch(id) - } - }) - - Metamaps.Router = new Router() - Metamaps.Router.currentPage = '' - Metamaps.Router.currentSection = undefined - Metamaps.Router.timeoutId = undefined - - Metamaps.Router.intercept = function (evt) { - var segments - - var href = { - prop: $(this).prop('href'), - attr: $(this).attr('href') - } - var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root - - if (href.prop && href.prop === root) href.attr = '' - - if (href.prop && href.prop.slice(0, root.length) === root) { - evt.preventDefault() - - segments = href.attr.split('/') - segments.splice(0, 1) // pop off the element created by the first / - - if (href.attr === '') { - Metamaps.Router.home() + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + if (Metamaps.Maps.Active.length === 0) { + Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Metamaps.Router[segments[0]](segments[1], segments[2]) + Metamaps.Views.exploreMaps.render(navigate) + } + } else { + // logged out home page + Metamaps.GlobalUI.hideDiv('#explore') + Metamaps.GlobalUI.showDiv('#yield') + Metamaps.Router.timeoutId = setTimeout(navigate, 500) + } + + Metamaps.GlobalUI.hideDiv('#infovis') + Metamaps.GlobalUI.hideDiv('#instructions') + Metamaps.Map.end() + Metamaps.Topic.end() + Metamaps.Active.Map = null + Metamaps.Active.Topic = null + }, + explore: function (section, id) { + clearTimeout(Metamaps.Router.timeoutId) + + // just capitalize the variable section + // either 'featured', 'mapper', or 'active' + var capitalize = section.charAt(0).toUpperCase() + section.slice(1) + + if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') { + document.title = 'Explore ' + capitalize + ' Maps | Metamaps' + } else if (section === 'mapper') { + $.ajax({ + url: '/users/' + id + '.json', + success: function (response) { + document.title = response.name + ' | Metamaps' + }, + error: function () {} + }) + } else if (section === 'mine') { + document.title = 'Explore My Maps | Metamaps' + } + + if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) + $('.wrapper').removeClass('homePage mapPage topicPage') + $('.wrapper').addClass('explorePage') + + Metamaps.Router.currentSection = 'explore' + Metamaps.Router.currentPage = section + + // this will mean it's a mapper page being loaded + if (id) { + if (Metamaps.Maps.Mapper.mapperId !== id) { + // empty the collection if we are trying to load the maps + // collection of a different mapper than we had previously + Metamaps.Maps.Mapper.reset() + Metamaps.Maps.Mapper.page = 1 + } + Metamaps.Maps.Mapper.mapperId = id + } + + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + + var navigate = function () { + var path = '/explore/' + Metamaps.Router.currentPage + + // alter url if for mapper profile page + if (Metamaps.Router.currentPage === 'mapper') { + path += '/' + Metamaps.Maps.Mapper.mapperId + } + + Metamaps.Router.navigate(path) + } + var navigateTimeout = function () { + Metamaps.Router.timeoutId = setTimeout(navigate, 300) + } + if (Metamaps.Maps[capitalize].length === 0) { + Metamaps.Loading.show() + setTimeout(function () { + Metamaps.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render + }, 300) // wait 300 milliseconds till the other animations are done to do the fetch + } else { + if (id) { + Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) + } else { + Metamaps.Views.exploreMaps.render(navigateTimeout) } } - } - Metamaps.Router.init = function () { - Backbone.history.start({ - silent: true, - pushState: true, - root: '/' - }) - $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) + Metamaps.GlobalUI.showDiv('#explore') + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#infovis') + Metamaps.GlobalUI.hideDiv('#instructions') + Metamaps.Map.end() + Metamaps.Topic.end() + Metamaps.Active.Map = null + Metamaps.Active.Topic = null + }, + maps: function (id) { + clearTimeout(Metamaps.Router.timeoutId) + + document.title = 'Map ' + id + ' | Metamaps' + + Metamaps.Router.currentSection = 'map' + Metamaps.Router.currentPage = id + + $('.wrapper').removeClass('homePage explorePage topicPage') + $('.wrapper').addClass('mapPage') + // another class will be added to wrapper if you + // can edit this map '.canEditMap' + + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#explore') + + // clear the visualization, if there was one, before showing its div again + if (Metamaps.Visualize.mGraph) { + Metamaps.Visualize.mGraph.graph.empty() + Metamaps.Visualize.mGraph.plot() + Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + } + Metamaps.GlobalUI.showDiv('#infovis') + Metamaps.Topic.end() + Metamaps.Active.Topic = null + + Metamaps.Loading.show() + Metamaps.Map.end() + Metamaps.Map.launch(id) + }, + topics: function (id) { + clearTimeout(Metamaps.Router.timeoutId) + + document.title = 'Topic ' + id + ' | Metamaps' + + Metamaps.Router.currentSection = 'topic' + Metamaps.Router.currentPage = id + + $('.wrapper').removeClass('homePage explorePage mapPage') + $('.wrapper').addClass('topicPage') + + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#explore') + + // clear the visualization, if there was one, before showing its div again + if (Metamaps.Visualize.mGraph) { + Metamaps.Visualize.mGraph.graph.empty() + Metamaps.Visualize.mGraph.plot() + Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + } + Metamaps.GlobalUI.showDiv('#infovis') + Metamaps.Map.end() + Metamaps.Active.Map = null + + Metamaps.Topic.end() + Metamaps.Topic.launch(id) } -})() +}) + +const Router = new _Router() +Router.currentPage = '' +Router.currentSection = undefined +Router.timeoutId = undefined + +Router.intercept = function (evt) { + var segments + + var href = { + prop: $(this).prop('href'), + attr: $(this).attr('href') + } + var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root + + if (href.prop && href.prop === root) href.attr = '' + + if (href.prop && href.prop.slice(0, root.length) === root) { + evt.preventDefault() + + segments = href.attr.split('/') + segments.splice(0, 1) // pop off the element created by the first / + + if (href.attr === '') { + Metamaps.Router.home() + } else { + Metamaps.Router[segments[0]](segments[1], segments[2]) + } + } +} + +Router.init = function () { + Backbone.history.start({ + silent: true, + pushState: true, + root: '/' + }) + $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) +} + +export default Router diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 20cf0f9c..5258de3b 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -18,7 +17,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Synapse = { +const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve get: function (id, callback) { @@ -98,7 +97,7 @@ Metamaps.Synapse = { } }, createSynapseLocally: function () { - var self = Metamaps.Synapse, + var self = Synapse, topic1, topic2, node1, @@ -145,7 +144,7 @@ Metamaps.Synapse = { Metamaps.Create.newSynapse.hide() }, getSynapseFromAutocomplete: function (id) { - var self = Metamaps.Synapse, + var self = Synapse, topic1, topic2, node1, @@ -167,4 +166,6 @@ Metamaps.Synapse = { self.renderSynapse(mapping, synapse, node1, node2, true) } -}; // end Metamaps.Synapse +} + +export default Synapse diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index aff207a9..93ebb646 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,10 +9,10 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Mapper * - Metamaps.Visualize */ -Metamaps.SynapseCard = { +const SynapseCard = { openSynapseCard: null, showCard: function (edge, e) { - var self = Metamaps.SynapseCard + var self = SynapseCard // reset so we don't interfere with other edges, but first, save its x and y var myX = $('#edit_synapse').css('left') @@ -59,11 +58,11 @@ Metamaps.SynapseCard = { hideCard: function () { $('#edit_synapse').remove() - Metamaps.SynapseCard.openSynapseCard = null + SynapseCard.openSynapseCard = null }, populateShowCard: function (edge, synapse) { - var self = Metamaps.SynapseCard + var self = SynapseCard self.add_synapse_count(edge) self.add_desc_form(synapse) @@ -154,7 +153,7 @@ Metamaps.SynapseCard = { var index = parseInt($(this).attr('data-synapse-index')) edge.setData('displayIndex', index) Metamaps.Visualize.mGraph.plot() - Metamaps.SynapseCard.showCard(edge, false) + SynapseCard.showCard(edge, false) }) } }, @@ -286,4 +285,6 @@ Metamaps.SynapseCard = { }) } // if } // add_direction_form -}; // end Metamaps.SynapseCard +} + +export default SynapseCard diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index faa8b336..8de14c34 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -28,7 +27,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.tempNode2 */ -Metamaps.Topic = { +const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve get: function (id, callback) { @@ -393,4 +392,6 @@ Metamaps.Topic = { event.preventDefault() return false } -}; // end Metamaps.Topic +} + +export default Topic diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index fc007f3b..5a7f1920 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -13,16 +12,16 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Util * - Metamaps.Visualize */ -Metamaps.TopicCard = { +const TopicCard = { openTopicCard: null, // stores the topic that's currently open authorizedToEdit: false, // stores boolean for edit permission for open topic card init: function () { - var self = Metamaps.TopicCard + var self = TopicCard // initialize best_in_place editing $('.authenticated div.permission.canEdit .best_in_place').best_in_place() - Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) + TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) // initialize topic card draggability and resizability $('.showcard').draggable({ @@ -39,7 +38,7 @@ Metamaps.TopicCard = { * @param {$jit.Graph.Node} node */ showCard: function (node, opts) { - var self = Metamaps.TopicCard + var self = TopicCard var topic = node.getData('topic') @@ -54,14 +53,14 @@ Metamaps.TopicCard = { }) }, hideCard: function () { - var self = Metamaps.TopicCard + var self = TopicCard $('.showcard').fadeOut('fast') self.openTopicCard = null self.authorizedToEdit = false }, embedlyCardRendered: function (iframe) { - var self = Metamaps.TopicCard + var self = TopicCard $('#embedlyLinkLoader').hide() @@ -78,7 +77,7 @@ Metamaps.TopicCard = { } }, removeLink: function () { - var self = Metamaps.TopicCard + var self = TopicCard self.openTopicCard.save({ link: null }) @@ -88,7 +87,7 @@ Metamaps.TopicCard = { $('.CardOnGraph').removeClass('hasAttachment') }, bindShowCardListeners: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var showCard = document.getElementById('showcard') var authorized = self.authorizedToEdit @@ -350,13 +349,13 @@ Metamaps.TopicCard = { }) }, handleInvalidLink: function () { - var self = Metamaps.TopicCard + var self = TopicCard self.removeLink() Metamaps.GlobalUI.notifyUser('Invalid link') }, populateShowCard: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var showCard = document.getElementById('showcard') @@ -380,12 +379,12 @@ Metamaps.TopicCard = { showCard.appendChild(perm) } - Metamaps.TopicCard.bindShowCardListeners(topic) + TopicCard.bindShowCardListeners(topic) }, generateShowcardHTML: null, // will be initialized into a Hogan template within init function // generateShowcardHTML buildObject: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var nodeValues = {} @@ -456,4 +455,6 @@ Metamaps.TopicCard = { nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') return nodeValues } -}; // end Metamaps.TopicCard +} + +export default TopicCard diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 9ff9c470..e835a15b 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* @@ -8,7 +7,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Util = { +const Util = { // helper function to determine how many lines are needed // Line Splitter Function // copyright Stephen Chapman, 19th April 2006 @@ -92,7 +91,7 @@ Metamaps.Util = { var r = (Math.round(Math.random() * 127) + 127).toString(16) var g = (Math.round(Math.random() * 127) + 127).toString(16) var b = (Math.round(Math.random() * 127) + 127).toString(16) - return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4) + return Util.colorLuminance('#' + r + g + b, -0.4) }, // darkens a hex value by 'lum' percentage colorLuminance: function (hex, lum) { @@ -128,4 +127,6 @@ Metamaps.Util = { checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) } -}; // end Metamaps.Util +} + +export default Util diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js index eb5fdb7c..90cd466d 100644 --- a/frontend/src/Metamaps/Views.js +++ b/frontend/src/Metamaps/Views.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,10 +9,10 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.ReactComponents */ -Metamaps.Views = { +const Views = { exploreMaps: { setCollection: function (collection) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection) { self.collection.off('add', self.render) @@ -26,7 +25,7 @@ Metamaps.Views = { self.collection.on('errorOnFetch', self.handleError) }, render: function (mapperObj, cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (typeof mapperObj === 'function') { cb = mapperObj @@ -51,7 +50,7 @@ Metamaps.Views = { Metamaps.Loading.hide() }, loadMore: function () { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection.page != "loadedAll") { self.collection.getMaps() @@ -59,7 +58,7 @@ Metamaps.Views = { else self.render() }, handleSuccess: function (cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection && self.collection.id === 'mapper') { self.fetchUserThenRender(cb) @@ -71,7 +70,7 @@ Metamaps.Views = { console.log('error loading maps!') // TODO }, fetchUserThenRender: function (cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps // first load the mapper object and then call the render function $.ajax({ @@ -86,3 +85,5 @@ Metamaps.Views = { } } } + +export default Views diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index f5ce8c79..5e99519f 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* * Metamaps.Visualize @@ -13,16 +12,15 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.TopicCard * - Metamaps.Topics * - Metamaps.Touch - * - Metamaps.Visualize */ -Metamaps.Visualize = { +const Visualize = { mGraph: null, // a reference to the graph object. cameraPosition: null, // stores the camera position when using a 3D visualization type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created init: function () { - var self = Metamaps.Visualize + var self = Visualize // disable awkward dragging of the canvas element that would sometimes happen $('#infovis-canvas').on('dragstart', function (event) { event.preventDefault() @@ -48,7 +46,7 @@ Metamaps.Visualize = { }) }, computePositions: function () { - var self = Metamaps.Visualize, + var self = Visualize, mapping if (self.type == 'RGraph') { @@ -112,7 +110,7 @@ Metamaps.Visualize = { * */ render: function () { - var self = Metamaps.Visualize, RGraphSettings, FDSettings + var self = Visualize, RGraphSettings, FDSettings if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { // clear the previous canvas from #infovis @@ -217,4 +215,6 @@ Metamaps.Visualize = { } }, 800) } -}; // end Metamaps.Visualize +} + +export default Visualize diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index ef50b564..926529ca 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -2,32 +2,60 @@ window.Metamaps = window.Metamaps || {} import './Constants' -import './Account' -import './Admin' -import './AutoLayout' -import './Backbone' -import './Control' -import './Create' -import './Debug' -import './Filter' -import './GlobalUI' -import './Import' -import './JIT' -import './Listeners' -import './Map' -import './Mapper' -import './Mobile' -import './Organize' -import './PasteInput' -import './Realtime' -import './Router' -import './Synapse' -import './SynapseCard' -import './Topic' -import './TopicCard' -import './Util' -import './Views' -import './Visualize' -import './ReactComponents' +import Account from './Account' +import Admin from './Admin' +import AutoLayout from './AutoLayout' +import Backbone from './Backbone' +import Control from './Control' +import Create from './Create' +import Debug from './Debug' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import Import from './Import' +import JIT from './JIT' +import Listeners from './Listeners' +import Map from './Map' +import Mapper from './Mapper' +import Mobile from './Mobile' +import Organize from './Organize' +import PasteInput from './PasteInput' +import Realtime from './Realtime' +import Router from './Router' +import Synapse from './Synapse' +import SynapseCard from './SynapseCard' +import Topic from './Topic' +import TopicCard from './TopicCard' +import Util from './Util' +import Views from './Views' +import Visualize from './Visualize' +import ReactComponents from './ReactComponents' + +Metamaps.Account = Account +Metamaps.Admin = Admin +Metamaps.AutoLayout = AutoLayout +Metamaps.Backbone = Backbone +Metamaps.Control = Control +Metamaps.Create = Create +Metamaps.Debug = Debug +Metamaps.Filter = Filter +Metamaps.GlobalUI = GlobalUI +Metamaps.Import = Import +Metamaps.JIT = JIT +Metamaps.Listeners = Listeners +Metamaps.Map = Map +Metamaps.Mapper = Mapper +Metamaps.Mobile = Mobile +Metamaps.Organize = Organize +Metamaps.PasteInput = PasteInput +Metamaps.Realtime = Realtime +Metamaps.ReactComponents = ReactComponents +Metamaps.Router = Router +Metamaps.Synapse = Synapse +Metamaps.SynapseCard = SynapseCard +Metamaps.Topic = Topic +Metamaps.TopicCard = TopicCard +Metamaps.Util = Util +Metamaps.Views = Views +Metamaps.Visualize = Visualize export default window.Metamaps From 7f83f86460d9c9b3eb5e1dc201d0689ea04b8084 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:29:55 +0800 Subject: [PATCH 014/378] refactor a bit, make a comment about the Constants file --- frontend/src/Metamaps/Constants.js | 12 ++- frontend/src/Metamaps/GlobalUI.js | 133 ++++++++++------------------- frontend/src/Metamaps/index.js | 38 +++++++++ 3 files changed, 93 insertions(+), 90 deletions(-) diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index a79054e6..ab62ba14 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -1,10 +1,20 @@ window.Metamaps = window.Metamaps || {} -// TODO eliminate these 5 top-level variables +// TODO everything in this file should be moved into one of the other modules +// Either as a local constant, or as a local constant with a globally available getter/setter + Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null +Metamaps.Active = Metamaps.Active || { + Map: null, + Topic: null, + Mapper: null +}; + +Metamaps.Maps = Metamaps.Maps || {} + Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index e4522c2f..5abf25ee 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,54 +1,10 @@ window.Metamaps = window.Metamaps || {}; -Metamaps.Active = Metamaps.Active || { - Map: null, - Topic: null, - Mapper: null -}; -Metamaps.Maps = Metamaps.Maps || {} - -$(document).ready(function () { - // initialize all the modules - for (var prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } - } - // load whichever page you are on - if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() - } - else { - Metamaps.Views.exploreMaps.render() - } - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { - Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') - } -}); - -Metamaps.GlobalUI = { +const GlobalUI = { notifyTimeout: null, lightbox: null, init: function () { - var self = Metamaps.GlobalUI; + var self = GlobalUI; self.Search.init(); self.CreateMap.init(); @@ -99,7 +55,7 @@ Metamaps.GlobalUI = { }, 200, 'easeInCubic', function () { $(this).hide() }) }, openLightbox: function (which) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; $('.lightboxContent').hide(); $('#' + which).show(); @@ -126,7 +82,7 @@ Metamaps.GlobalUI = { }, closeLightbox: function (event) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; if (event) event.preventDefault(); @@ -143,15 +99,15 @@ Metamaps.GlobalUI = { $('#lightbox_overlay').hide(); }); - if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { Metamaps.Create.cancelMetacodeSetSwitch(); } self.lightbox = null; }, notifyUser: function (message, leaveOpen) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; $('#toast').html(message) self.showDiv('#toast') @@ -163,7 +119,7 @@ Metamaps.GlobalUI = { } }, clearNotify: function() { - var self = Metamaps.GlobalUI; + var self = GlobalUI; clearTimeout(self.notifyTimeOut); self.hideDiv('#toast') @@ -171,16 +127,16 @@ Metamaps.GlobalUI = { shareInvite: function(inviteLink) { window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); } -}; +} -Metamaps.GlobalUI.CreateMap = { +GlobalUI.CreateMap = { newMap: null, emptyMapForm: "", emptyForkMapForm: "", topicsToMap: [], synapsesToMap: [], init: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); @@ -190,7 +146,7 @@ Metamaps.GlobalUI.CreateMap = { }, bindFormEvents: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { if (event.keyCode === 13) self.submit() @@ -198,7 +154,7 @@ Metamaps.GlobalUI.CreateMap = { $('.new_map button.cancel').unbind().bind('click', function (event) { event.preventDefault(); - Metamaps.GlobalUI.closeLightbox(); + GlobalUI.closeLightbox(); }); $('.new_map button.submitMap').unbind().bind('click', self.submit); @@ -213,14 +169,14 @@ Metamaps.GlobalUI.CreateMap = { generateSuccessMessage: function (id) { var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; stringStart += id; - stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; var page = Metamaps.Active.Map ? 'map' : 'page'; var stringEnd = "</a></div>"; return stringStart + page + stringEnd; }, switchPermission: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; self.newMap.set('permission', $(this).attr('data-permission')); $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); @@ -232,14 +188,14 @@ Metamaps.GlobalUI.CreateMap = { submit: function (event) { if (event) event.preventDefault(); - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - if (Metamaps.GlobalUI.lightbox === 'forkmap') { + if (GlobalUI.lightbox === 'forkmap') { self.newMap.set('topicsToMap', self.topicsToMap); self.newMap.set('synapsesToMap', self.synapsesToMap); } - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var $form = $(formId); self.newMap.set('name', $form.find('#map_name').val()); @@ -255,13 +211,13 @@ Metamaps.GlobalUI.CreateMap = { // TODO add error message }); - Metamaps.GlobalUI.closeLightbox(); - Metamaps.GlobalUI.notifyUser('Working...'); + GlobalUI.closeLightbox(); + GlobalUI.notifyUser('Working...'); }, throwMapNameError: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var $form = $(formId); var message = $("<div class='feedback_message'>Please enter a map name...</div>"); @@ -274,20 +230,20 @@ Metamaps.GlobalUI.CreateMap = { }, 5000); }, success: function (model) { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; //push the new map onto the collection of 'my maps' Metamaps.Maps.Mine.add(model); - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var form = $(formId); - Metamaps.GlobalUI.clearNotify(); + GlobalUI.clearNotify(); $('#wrapper').append(self.generateSuccessMessage(model.id)); }, reset: function (id) { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; var form = $('#' + id); @@ -305,14 +261,13 @@ Metamaps.GlobalUI.CreateMap = { return false; }, -}; +} - -Metamaps.GlobalUI.Account = { +GlobalUI.Account = { isOpen: false, changing: false, init: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; $('.sidebarAccountIcon').click(self.toggleBox); $('.sidebarAccountBox').click(function(event){ @@ -321,7 +276,7 @@ Metamaps.GlobalUI.Account = { $('body').click(self.close); }, toggleBox: function (event) { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; if (self.isOpen) self.close(); else self.open(); @@ -329,7 +284,7 @@ Metamaps.GlobalUI.Account = { event.stopPropagation(); }, open: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; Metamaps.Filter.close(); $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); @@ -345,7 +300,7 @@ Metamaps.GlobalUI.Account = { } }, close: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); if (!self.changing) { @@ -357,9 +312,9 @@ Metamaps.GlobalUI.Account = { }); } } -}; +} -Metamaps.GlobalUI.Search = { +GlobalUI.Search = { locked: false, isOpen: false, limitTopicsToMe: false, @@ -368,7 +323,7 @@ Metamaps.GlobalUI.Search = { changing: false, optionsInitialized: false, init: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; var loader = new CanvasLoader('searchLoading'); loader.setColor('#4fb5c0'); // default is '#000000' @@ -417,15 +372,15 @@ Metamaps.GlobalUI.Search = { self.startTypeahead(); }, lock: function() { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.locked = true; }, unlock: function() { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.locked = false; }, open: function (focus) { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; clearTimeout(self.timeOut); if (!self.isOpen && !self.changing && !self.locked) { @@ -447,7 +402,7 @@ Metamaps.GlobalUI.Search = { // for now return - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.timeOut = setTimeout(function () { if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { @@ -468,7 +423,7 @@ Metamaps.GlobalUI.Search = { }, closeAfter); }, startTypeahead: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; @@ -615,7 +570,7 @@ Metamaps.GlobalUI.Search = { }, handleResultClick: function (event, datum, dataset) { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.hideLoader(); @@ -632,7 +587,7 @@ Metamaps.GlobalUI.Search = { } }, initSearchOptions: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; function toggleResultSet(set) { var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); @@ -679,4 +634,4 @@ Metamaps.GlobalUI.Search = { } } -export default Metamaps.GlobalUI +export default GlobalUI diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 926529ca..21d2af3a 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,3 +1,4 @@ +/* global $ */ window.Metamaps = window.Metamaps || {} import './Constants' @@ -58,4 +59,41 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize +$(document).ready(function () { + // initialize all the modules + for (var prop in Metamaps) { + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) == 'function' + ) { + Metamaps[prop].init() + } + } + // load whichever page you are on + if (Metamaps.currentSection === "explore") { + var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + + Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + if (Metamaps.currentPage === "mapper") { + Metamaps.Views.exploreMaps.fetchUserThenRender() + } + else { + Metamaps.Views.exploreMaps.render() + } + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.exploreMaps.render() + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + Metamaps.Loading.show() + Metamaps.JIT.prepareVizData() + Metamaps.GlobalUI.showDiv('#infovis') + } +}); + export default window.Metamaps From d97b5c297729bb9420fac3ce61dd3659d78bd5a8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:32:08 +0800 Subject: [PATCH 015/378] make Util modular --- frontend/src/Metamaps/Util.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index e835a15b..9eb715de 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,11 +1,4 @@ -/* global Metamaps */ - -/* - * Metamaps.Util.js - * - * Dependencies: - * - Metamaps.Visualize - */ +import Visualize from './Visualize' const Util = { // helper function to determine how many lines are needed @@ -45,8 +38,8 @@ const Util = { return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)) }, coordsToPixels: function (coords) { - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, + if (Visualize.mGraph) { + var canvas = Visualize.mGraph.canvas, s = canvas.getSize(), p = canvas.getPos(), ox = canvas.translateOffsetX, @@ -67,8 +60,8 @@ const Util = { }, pixelsToCoords: function (pixels) { var coords - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, + if (Visualize.mGraph) { + var canvas = Visualize.mGraph.canvas, s = canvas.getSize(), p = canvas.getPos(), ox = canvas.translateOffsetX, From c0f63abc59eaf2cd4d105e6bb766c4105beafc04 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 16:30:20 +0800 Subject: [PATCH 016/378] upgrade testing to es6 --- frontend/test/Metamaps.Import.spec.js | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index 8dcf8e97..68946bea 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -1,13 +1,13 @@ /* global describe, it */ -const chai = require('chai') -const expect = chai.expect -Metamaps = {} -require('../../app/assets/javascripts/src/Metamaps.Import') +import chai from 'chai' +import Import from '../src/Metamaps/Import' + +const { expect } = chai describe('Metamaps.Import.js', function () { it('has a topic whitelist', function () { - expect(Metamaps.Import.topicWhitelist).to.deep.equal( + expect(Import.topicWhitelist).to.deep.equal( ['id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission'] ) }) diff --git a/package.json b/package.json index a2227300..495b4f4b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "webpack", "build:watch": "webpack --watch", - "test": "mocha frontend/test || (echo 'Run `npm install` to setup testing' && false)" + "test": "mocha --compilers js:babel-core/register frontend/test || (echo 'Run `npm install` to setup testing' && false)" }, "repository": { "type": "git", From 0a109895f77ca0bae9c6f5065e464aa5c06adead Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 16:32:10 +0800 Subject: [PATCH 017/378] merge realtime/package.json into top level package.json --- doc/production/first-deploy.md | 2 -- doc/production/pull-changes.md | 4 +--- package.json | 2 ++ realtime/package.json | 10 ---------- 4 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 realtime/package.json diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md index cf98eda4..cc3a1f4a 100644 --- a/doc/production/first-deploy.md +++ b/doc/production/first-deploy.md @@ -87,8 +87,6 @@ server to see what problems show up: sudo npm install -g forever (crontab -u metamaps -l 2>/dev/null; echo "@reboot $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps - - cd /home/metamaps/metamaps/realtime - npm install mkdir -p /home/metamaps/logs forever --append -l /home/metamaps/logs/forever.realtime.log \ start /home/metamaps/metamaps/realtime/realtime-server.js diff --git a/doc/production/pull-changes.md b/doc/production/pull-changes.md index 0fb5d568..30f41cf5 100644 --- a/doc/production/pull-changes.md +++ b/doc/production/pull-changes.md @@ -29,9 +29,7 @@ Now that you have the code, run these commands: rake perms:fix passenger-config restart-app . - cd realtime - npm install - forever list #find the uid, e.g. xQKv + forever list #find the uid of the realtime server, e.g. xQKv forever restart xQKv sudo service metamaps_delayed_job restart diff --git a/package.json b/package.json index 495b4f4b..925323ac 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "chai": "^3.5.0", "jquery": "1.12.1", "mocha": "^3.0.2", + "node-uuid": "1.2.0", "react": "^15.3.0", "react-dom": "^15.3.0", "requirejs": "^2.1.1", + "socket.io": "0.9.12", "underscore": "^1.4.4", "webpack": "^1.13.1" } diff --git a/realtime/package.json b/realtime/package.json deleted file mode 100644 index 5b5b08f4..00000000 --- a/realtime/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "RoR-real-time", - "description": "providing real-time sychronization for ruby on rails", - "version": "0.0.1", - "private": true, - "dependencies": { - "socket.io": "0.9.12", - "node-uuid": "1.2.0" - } -} From 056213415772c38b738e64223502bc5cb6b493c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:00:12 +0800 Subject: [PATCH 018/378] low hanging fruit Here is my TODO list: already done ==> Account.js <== ==> Admin.js <== ==> AutoLayout.js <== ==> Listeners.js <== ==> Mapper.js <== ==> Organize.js <== ==> PasteInput.js <== ==> ReactComponents.js <== ==> Util.js <== TODO (I think) simple to make modular ==> Backbone.js <== ==> Control.js <== ==> Create.js <== ==> Filter.js <== ==> Import.js <== ==> Mobile.js <== ==> Synapse.js <== ==> SynapseCard.js <== ==> Topic.js <== ==> TopicCard.js <== ==> Views.js <== ==> Visualize.js <== TODO hard to make modular ==> Constants.js <== ==> Debug.js <== ==> GlobalUI.js <== ==> JIT.js <== ==> Map.js <== ==> Realtime.js <== ==> Router.js <== --- frontend/src/Metamaps/Account.js | 28 +++++++++-------------- frontend/src/Metamaps/Admin.js | 10 ++++----- frontend/src/Metamaps/Listeners.js | 2 -- frontend/src/Metamaps/Mapper.js | 8 ++----- frontend/src/Metamaps/Mobile.js | 2 +- frontend/src/Metamaps/Organize.js | 35 +++++++++++++---------------- frontend/src/Metamaps/PasteInput.js | 19 ++++++---------- 7 files changed, 41 insertions(+), 63 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 95a1a69f..f424019f 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,14 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* uses window.Metamaps.Erb */ -/* - * Metamaps.Account.js.erb - * - * Dependencies: - * - Metamaps.Erb - */ - -Metamaps.Account = { +const Account = { listenersInitialized: false, init: function () { var self = Metamaps.Account @@ -20,24 +12,24 @@ Metamaps.Account = { self.listenersInitialized = true }, toggleChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').toggle() if (!self.listenersInitialized) self.initListeners() }, openChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').show() if (!self.listenersInitialized) self.initListeners() }, closeChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').hide() }, showLoading: function () { - var self = Metamaps.Account + var self = Account var loader = new CanvasLoader('accountPageLoading') loader.setColor('#4FC059'); // default is '#000000' @@ -48,7 +40,7 @@ Metamaps.Account = { $('#accountPageLoading').show() }, showImagePreview: function () { - var self = Metamaps.Account + var self = Account var file = $('#user_image')[0].files[0] @@ -94,10 +86,10 @@ Metamaps.Account = { } }, removePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageDiv canvas').remove() - $('.userImageDiv img').attr('src', Metamaps.Erb['user.png']).show() + $('.userImageDiv img').attr('src', window.Metamaps.Erb['user.png']).show() $('.userImageMenu').hide() var input = $('#user_image') @@ -122,4 +114,4 @@ Metamaps.Account = { } } -export default Metamaps.Account +export default Account diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 10cbc6d8..5d080c2e 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -4,26 +4,26 @@ const Admin = { selectMetacodes: [], allMetacodes: [], init: function () { - var self = Metamaps.Admin + var self = Admin $('#metacodes_value').val(self.selectMetacodes.toString()) }, selectAll: function () { - var self = Metamaps.Admin + var self = Admin $('.editMetacodes li').removeClass('toggledOff') self.selectMetacodes = self.allMetacodes.slice(0) $('#metacodes_value').val(self.selectMetacodes.toString()) }, deselectAll: function () { - var self = Metamaps.Admin + var self = Admin $('.editMetacodes li').addClass('toggledOff') self.selectMetacodes = [] $('#metacodes_value').val(0) }, liClickHandler: function () { - var self = Metamaps.Admin + var self = Admin if ($(this).attr('class') != 'toggledOff') { $(this).addClass('toggledOff') @@ -38,7 +38,7 @@ const Admin = { } }, validate: function () { - var self = Metamaps.Admin + var self = Admin if (self.selectMetacodes.length == 0) { alert('Would you pretty please select at least one metacode for the set?') diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index af244961..1c56b679 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,8 +1,6 @@ /* global Metamaps, $ */ /* - * Metamaps.Listeners.js.erb - * * Dependencies: * - Metamaps.Active * - Metamaps.Control diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 114d4f8c..ac93c34d 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,9 +1,5 @@ -/* global Metamaps */ +import Backbone from './Backbone' -/* - * Dependencies: - * - Metamaps.Backbone - */ const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve @@ -13,7 +9,7 @@ const Mapper = { if (!response.ok) throw response return response.json() }).then(payload => { - callback(new Metamaps.Backbone.Mapper(payload)) + callback(new Backbone.Mapper(payload)) }) } } diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index e062ca45..9074f521 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -10,7 +10,7 @@ const Mobile = { init: function () { - var self = Metamaps.Mobile + var self = Mobile $('#menu_icon').click(self.toggleMenu) $('#mobile_menu li a').click(self.liClick) diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index 71905568..ee29c2b8 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,21 +1,18 @@ -/* global Metamaps, $ */ +/* global $ */ + +import Visualize from './Visualize' +import JIT from './JIT' -/* - * Metamaps.Organize.js.erb - * - * Dependencies: - * - Metamaps.Visualize - */ const Organize = { arrange: function (layout, centerNode) { // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin if (layout == 'grid') { - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization + var numNodes = _.size(Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid var GRIDSPACE = 400 var row = 0 var column = 0 - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 row += 1 @@ -26,14 +23,14 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'grid_full') { // this will always be an integer, the # of nodes on your graph visualization - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes) + var numNodes = _.size(Visualize.mGraph.graph.nodes) // var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid // var GRIDSPACE = 400 - var height = Metamaps.Visualize.mGraph.canvas.getSize(0).height - var width = Metamaps.Visualize.mGraph.canvas.getSize(0).width + var height = Visualize.mGraph.canvas.getSize(0).height + var width = Visualize.mGraph.canvas.getSize(0).width var totalArea = height * width var cellArea = totalArea / numNodes var ratio = height / width @@ -44,7 +41,7 @@ const Organize = { var totalCells = row * column if (totalCells) - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 row += 1 @@ -55,7 +52,7 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'radial') { var centerX = centerNode.getPos().x var centerY = centerNode.getPos().y @@ -87,16 +84,16 @@ const Organize = { }) } radial(centerNode, 1, 0) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'center_viewport') { var lowX = 0, lowY = 0, highX = 0, highY = 0 - var oldOriginX = Metamaps.Visualize.mGraph.canvas.translateOffsetX - var oldOriginY = Metamaps.Visualize.mGraph.canvas.translateOffsetY + var oldOriginX = Visualize.mGraph.canvas.translateOffsetX + var oldOriginY = Visualize.mGraph.canvas.translateOffsetY - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (n.id === 1) { lowX = n.getPos().x lowY = n.getPos().y diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 9676e783..ebe1d944 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,12 +1,7 @@ -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.PasteInput.js.erb - * - * Dependencies: - * - Metamaps.Import - * - Metamaps.AutoLayout - */ +import AutoLayout from './AutoLayout' +import Import from './Import' const PasteInput = { // thanks to https://github.com/kevva/url-regex @@ -74,13 +69,13 @@ const PasteInput = { handleURL: function (text, coords) { var title = 'Link' if (!coords || !coords.x || !coords.y) { - coords = Metamaps.AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord() } var import_id = null // don't store a cidMapping var permission = null // use default - Metamaps.Import.createTopicWithParameters( + Import.createTopicWithParameters( title, 'Reference', // metacode - todo fix permission, @@ -101,11 +96,11 @@ const PasteInput = { }, handleJSON: function (text) { - Metamaps.Import.handleJSON(text) + Import.handleJSON(text) }, handleTSV: function (text) { - Metamaps.Import.handleTSV(text) + Import.handleTSV(text) } } From 8f100d99cb82bb3204f264fd6212619d7dbf9603 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:05:28 +0800 Subject: [PATCH 019/378] start to do stuff that may/may not work --- frontend/src/Metamaps/Active.js | 7 +++++++ frontend/src/Metamaps/Constants.js | 5 ----- frontend/src/Metamaps/index.js | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 frontend/src/Metamaps/Active.js diff --git a/frontend/src/Metamaps/Active.js b/frontend/src/Metamaps/Active.js new file mode 100644 index 00000000..c61a8bb9 --- /dev/null +++ b/frontend/src/Metamaps/Active.js @@ -0,0 +1,7 @@ +const Active = { + Map: null, + Topic: null, + Mapper: null +}; + +export default Active diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index ab62ba14..e887f24c 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -7,11 +7,6 @@ Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null -Metamaps.Active = Metamaps.Active || { - Map: null, - Topic: null, - Mapper: null -}; Metamaps.Maps = Metamaps.Maps || {} diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 21d2af3a..598533c0 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,9 +1,9 @@ /* global $ */ -window.Metamaps = window.Metamaps || {} import './Constants' import Account from './Account' +import Active from './Active' import Admin from './Admin' import AutoLayout from './AutoLayout' import Backbone from './Backbone' @@ -32,6 +32,7 @@ import Visualize from './Visualize' import ReactComponents from './ReactComponents' Metamaps.Account = Account +Metamaps.Active = Active Metamaps.Admin = Admin Metamaps.AutoLayout = AutoLayout Metamaps.Backbone = Backbone @@ -59,9 +60,9 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize -$(document).ready(function () { +document.addEventListener("DOMContentLoaded", function() { // initialize all the modules - for (var prop in Metamaps) { + for (const prop in Metamaps) { // this runs the init function within each sub-object on the Metamaps one if (Metamaps.hasOwnProperty(prop) && Metamaps[prop] != null && @@ -73,7 +74,7 @@ $(document).ready(function () { } // load whichever page you are on if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { @@ -96,4 +97,4 @@ $(document).ready(function () { } }); -export default window.Metamaps +export default Metamaps From 9c1543de6467fb5eab9d059e111d4e533b6436c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:08:53 +0800 Subject: [PATCH 020/378] move some variables into JIT --- frontend/src/Metamaps/Constants.js | 5 --- frontend/src/Metamaps/JIT.js | 58 ++++++++++++++++-------------- frontend/src/Metamaps/Topic.js | 15 ++++---- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index e887f24c..9d0818ab 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -3,11 +3,6 @@ window.Metamaps = window.Metamaps || {} // TODO everything in this file should be moved into one of the other modules // Either as a local constant, or as a local constant with a globally available getter/setter -Metamaps.tempNode = null -Metamaps.tempInit = false -Metamaps.tempNode2 = null - - Metamaps.Maps = Metamaps.Maps || {} Metamaps.Settings = { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index a93e3341..ec8195de 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -3,6 +3,10 @@ let panningInt const JIT = { + tempInit: false, + tempNode: null, + tempNode2: null, + events: { topicDrag: 'Metamaps:JIT:events:topicDrag', newTopic: 'Metamaps:JIT:events:newTopic', @@ -795,9 +799,9 @@ const JIT = { } // if it's a right click or holding down alt, start synapse creation ->third option is for firefox else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { - if (Metamaps.tempInit == false) { - Metamaps.tempNode = node - Metamaps.tempInit = true + if (JIT.tempInit == false) { + JIT.tempNode = node + JIT.tempInit = true Metamaps.Create.newTopic.hide() Metamaps.Create.newSynapse.hide() @@ -813,8 +817,8 @@ const JIT = { } } else { Metamaps.Mouse.synapseStartCoordinates = [{ - x: Metamaps.tempNode.pos.getc().x, - y: Metamaps.tempNode.pos.getc().y + x: JIT.tempNode.pos.getc().x, + y: JIT.tempNode.pos.getc().y }] } Metamaps.Mouse.synapseEndCoordinates = { @@ -825,11 +829,11 @@ const JIT = { // let temp = eventInfo.getNode() if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned - Metamaps.tempNode2 = temp + JIT.tempNode2 = temp Metamaps.Mouse.synapseEndCoordinates = { - x: Metamaps.tempNode2.pos.getc().x, - y: Metamaps.tempNode2.pos.getc().y + x: JIT.tempNode2.pos.getc().x, + y: JIT.tempNode2.pos.getc().y } // before making the highlighted one bigger, make sure all the others are regular size @@ -839,7 +843,7 @@ const JIT = { temp.setData('dim', 35, 'current') Metamaps.Visualize.mGraph.plot() } else if (!temp) { - Metamaps.tempNode2 = null + JIT.tempNode2 = null Metamaps.Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) @@ -867,10 +871,10 @@ const JIT = { } }, // onDragMoveTopicHandler onDragCancelHandler: function (node, eventInfo, e) { - Metamaps.tempNode = null - if (Metamaps.tempNode2) Metamaps.tempNode2.setData('dim', 25, 'current') - Metamaps.tempNode2 = null - Metamaps.tempInit = false + JIT.tempNode = null + if (JIT.tempNode2) JIT.tempNode2.setData('dim', 25, 'current') + JIT.tempNode2 = null + JIT.tempInit = false // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Mouse.synapseEndCoordinates = null @@ -879,27 +883,27 @@ const JIT = { onDragEndTopicHandler: function (node, eventInfo, e) { var midpoint = {}, pixelPos, mapping - if (Metamaps.tempInit && Metamaps.tempNode2 == null) { + if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse Metamaps.Create.newTopic.addSynapse = true Metamaps.Create.newTopic.open() - } else if (Metamaps.tempInit && Metamaps.tempNode2 != null) { + } else if (JIT.tempInit && JIT.tempNode2 != null) { // this means you want to create a synapse between two existing topics Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id - Metamaps.Create.newSynapse.topic2id = Metamaps.tempNode2.getData('topic').id - Metamaps.tempNode2.setData('dim', 25, 'current') + Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + Metamaps.Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id + JIT.tempNode2.setData('dim', 25, 'current') Metamaps.Visualize.mGraph.plot() - midpoint.x = Metamaps.tempNode.pos.getc().x + (Metamaps.tempNode2.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 - midpoint.y = Metamaps.tempNode.pos.getc().y + (Metamaps.tempNode2.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + midpoint.x = JIT.tempNode.pos.getc().x + (JIT.tempNode2.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 + midpoint.y = JIT.tempNode.pos.getc().y + (JIT.tempNode2.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 pixelPos = Metamaps.Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') Metamaps.Create.newSynapse.open() - Metamaps.tempNode = null - Metamaps.tempNode2 = null - Metamaps.tempInit = false - } else if (!Metamaps.tempInit && node && !node.nodeFrom) { + JIT.tempNode = null + JIT.tempNode2 = null + JIT.tempInit = false + } else if (!JIT.tempInit && node && !node.nodeFrom) { // this means you dragged an existing node, autosave that to the database // check whether to save mappings @@ -977,9 +981,9 @@ const JIT = { // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Mouse.synapseEndCoordinates = null - Metamaps.tempInit = false - Metamaps.tempNode = null - Metamaps.tempNode2 = null + JIT.tempInit = false + JIT.tempNode = null + JIT.tempNode2 = null if (!e.ctrlKey && !e.shiftKey) { Metamaps.Control.deselectAllEdges() Metamaps.Control.deselectAllNodes() diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 8de14c34..0ebcc118 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -22,9 +22,6 @@ * - Metamaps.Topics * - Metamaps.Util * - Metamaps.Visualize - * - Metamaps.tempInit - * - Metamaps.tempNode - * - Metamaps.tempNode2 */ const Topic = { @@ -218,11 +215,11 @@ const Topic = { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') } if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id + Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id // position the form - midpoint.x = Metamaps.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 - midpoint.y = Metamaps.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + midpoint.x = JIT.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 + midpoint.y = JIT.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 pixelPos = Metamaps.Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') @@ -232,9 +229,9 @@ const Topic = { modes: ['node-property:dim'], duration: 500, onComplete: function () { - Metamaps.tempNode = null - Metamaps.tempNode2 = null - Metamaps.tempInit = false + JIT.tempNode = null + JIT.tempNode2 = null + JIT.tempInit = false } }) } else { From 8ed2b3ffc17d7206672853d0dedac7b8dacb64b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:14:34 +0800 Subject: [PATCH 021/378] remove Constants.js --- frontend/src/Metamaps/Constants.js | 56 ------------------------------ frontend/src/Metamaps/Mouse.js | 16 +++++++++ frontend/src/Metamaps/Selected.js | 11 ++++++ frontend/src/Metamaps/Settings.js | 21 +++++++++++ frontend/src/Metamaps/Visualize.js | 6 ++-- frontend/src/Metamaps/index.js | 9 +++-- 6 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 frontend/src/Metamaps/Constants.js create mode 100644 frontend/src/Metamaps/Mouse.js create mode 100644 frontend/src/Metamaps/Selected.js create mode 100644 frontend/src/Metamaps/Settings.js diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js deleted file mode 100644 index 9d0818ab..00000000 --- a/frontend/src/Metamaps/Constants.js +++ /dev/null @@ -1,56 +0,0 @@ -window.Metamaps = window.Metamaps || {} - -// TODO everything in this file should be moved into one of the other modules -// Either as a local constant, or as a local constant with a globally available getter/setter - -Metamaps.Maps = Metamaps.Maps || {} - -Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } - }, -} - -Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -} - -Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -} - -Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected - self.Nodes = [] - self.Edges = [] - }, - Nodes: [], - Edges: [] -} diff --git a/frontend/src/Metamaps/Mouse.js b/frontend/src/Metamaps/Mouse.js new file mode 100644 index 00000000..9989bc20 --- /dev/null +++ b/frontend/src/Metamaps/Mouse.js @@ -0,0 +1,16 @@ +const Mouse = { + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} + +export default Mouse diff --git a/frontend/src/Metamaps/Selected.js b/frontend/src/Metamaps/Selected.js new file mode 100644 index 00000000..396270ab --- /dev/null +++ b/frontend/src/Metamaps/Selected.js @@ -0,0 +1,11 @@ +const Selected = { + reset: function () { + var self = Metamaps.Selected + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} + +export default Selected diff --git a/frontend/src/Metamaps/Settings.js b/frontend/src/Metamaps/Settings.js new file mode 100644 index 00000000..687a6629 --- /dev/null +++ b/frontend/src/Metamaps/Settings.js @@ -0,0 +1,21 @@ +const Settings = { + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' + }, + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} + +export default Settings diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 5e99519f..4aa65772 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -11,7 +11,6 @@ * - Metamaps.Synapses * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Touch */ const Visualize = { @@ -19,6 +18,7 @@ const Visualize = { cameraPosition: null, // stores the camera position when using a 3D visualization type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created + touchDragNode: null, init: function () { var self = Visualize // disable awkward dragging of the canvas element that would sometimes happen @@ -40,9 +40,9 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchend touchcancel', function (event) { lastDist = 0 - if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) Metamaps.TopicCard.hideCurrentCard() self.mGraph.events.touched = self.mGraph.events.touchMoved = false - Metamaps.Touch.touchDragNode = false + Visualize.touchDragNode = false }) }, computePositions: function () { diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 598533c0..37e93492 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,7 +1,5 @@ /* global $ */ -import './Constants' - import Account from './Account' import Active from './Active' import Admin from './Admin' @@ -18,10 +16,13 @@ import Listeners from './Listeners' import Map from './Map' import Mapper from './Mapper' import Mobile from './Mobile' +import Mouse from './Mouse' import Organize from './Organize' import PasteInput from './PasteInput' import Realtime from './Realtime' import Router from './Router' +import Selected from './Selected' +import Settings from './Settings' import Synapse from './Synapse' import SynapseCard from './SynapseCard' import Topic from './Topic' @@ -45,13 +46,17 @@ Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners Metamaps.Map = Map +Metamaps.Maps = {} Metamaps.Mapper = Mapper Metamaps.Mobile = Mobile +Metamaps.Mouse = Mouse Metamaps.Organize = Organize Metamaps.PasteInput = PasteInput Metamaps.Realtime = Realtime Metamaps.ReactComponents = ReactComponents Metamaps.Router = Router +Metamaps.Selected = Selected +Metamaps.Settings = Settings Metamaps.Synapse = Synapse Metamaps.SynapseCard = SynapseCard Metamaps.Topic = Topic From 0065b201c7b12c7eb7d76db7a33ee225e6e87094 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:36:47 +0800 Subject: [PATCH 022/378] make more code modular --- frontend/src/Metamaps/Control.js | 187 ++++++++++++++------------- frontend/src/Metamaps/Create.js | 26 ++-- frontend/src/Metamaps/Filter.js | 25 ++-- frontend/src/Metamaps/Import.js | 13 +- frontend/src/Metamaps/Mobile.js | 15 +-- frontend/src/Metamaps/SynapseCard.js | 35 +++-- frontend/src/Metamaps/Topic.js | 37 +++--- frontend/src/Metamaps/TopicCard.js | 25 ++-- frontend/src/Metamaps/Views.js | 10 +- frontend/src/Metamaps/Visualize.js | 48 +++---- 10 files changed, 209 insertions(+), 212 deletions(-) diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index b9df0d2c..3eecb126 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,21 +1,22 @@ /* global Metamaps, $ */ +import Active from './Active' +import Filter from './Filter' +import JIT from './JIT' +import Mouse from './Mouse' +import Selected from './Selected' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Control.js * * Dependencies: - * - Metamaps.Active - * - Metamaps.Filter * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Mappings * - Metamaps.Metacodes - * - Metamaps.Mouse - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Control = { @@ -23,37 +24,37 @@ const Control = { selectNode: function (node, e) { var filtered = node.getData('alpha') === 0 - if (filtered || Metamaps.Selected.Nodes.indexOf(node) != -1) return + if (filtered || Selected.Nodes.indexOf(node) != -1) return node.selected = true node.setData('dim', 30, 'current') - Metamaps.Selected.Nodes.push(node) + Selected.Nodes.push(node) }, deselectAllNodes: function () { - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i] + var node = Selected.Nodes[i] Control.deselectNode(node) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, deselectNode: function (node) { delete node.selected node.setData('dim', 25, 'current') // remove the node - Metamaps.Selected.Nodes.splice( - Metamaps.Selected.Nodes.indexOf(node), 1) + Selected.Nodes.splice( + Selected.Nodes.indexOf(node), 1) }, deleteSelected: function () { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var n = Metamaps.Selected.Nodes.length - var e = Metamaps.Selected.Edges.length + var n = Selected.Nodes.length + var e = Selected.Edges.length var ntext = n == 1 ? '1 topic' : n + ' topics' var etext = e == 1 ? '1 synapse' : e + ' synapses' var text = 'You have ' + ntext + ' and ' + etext + ' selected. ' - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -67,41 +68,41 @@ const Control = { } }, deleteSelectedNodes: function () { // refers to deleting topics permanently - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') return } - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i] + var node = Selected.Nodes[i] Control.deleteNode(node.id) } }, deleteNode: function (nodeid) { // refers to deleting topics permanently - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') return } - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var node = Visualize.mGraph.graph.getNode(nodeid) var topic = node.getData('topic') - var permToDelete = Metamaps.Active.Mapper.id === topic.get('user_id') || Metamaps.Active.Mapper.get('admin') + var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin') if (permToDelete) { var mappableid = topic.id var mapping = node.getData('mapping') topic.destroy() Metamaps.Mappings.remove(mapping) - $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ + $(document).trigger(JIT.events.deleteTopic, [{ mappableid: mappableid }]) Control.hideNode(nodeid) @@ -110,25 +111,25 @@ const Control = { } }, removeSelectedNodes: function () { // refers to removing topics permanently from a map - if (Metamaps.Active.Topic) { + if (Active.Topic) { // hideNode will handle synapses as well - var nodeids = _.map(Metamaps.Selected.Nodes, function(node) { + var nodeids = _.map(Selected.Nodes, function(node) { return node.id }) _.each(nodeids, function(nodeid) { - if (Metamaps.Active.Topic.id !== nodeid) { + if (Active.Topic.id !== nodeid) { Metamaps.Topics.remove(nodeid) Control.hideNode(nodeid) } }) return } - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var l = Metamaps.Selected.Nodes.length, + var l = Selected.Nodes.length, i, node, - authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -136,15 +137,15 @@ const Control = { } for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] Control.removeNode(node.id) } }, removeNode: function (nodeid) { // refers to removing topics permanently from a map - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) + var node = Visualize.mGraph.graph.getNode(nodeid) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -156,24 +157,24 @@ const Control = { var mapping = node.getData('mapping') mapping.destroy() Metamaps.Topics.remove(topic) - $(document).trigger(Metamaps.JIT.events.removeTopic, [{ + $(document).trigger(JIT.events.removeTopic, [{ mappableid: mappableid }]) Control.hideNode(nodeid) }, hideSelectedNodes: function () { - var l = Metamaps.Selected.Nodes.length, + var l = Selected.Nodes.length, i, node for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] Control.hideNode(node.id) } }, hideNode: function (nodeid) { - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) - var graph = Metamaps.Visualize.mGraph + var node = Visualize.mGraph.graph.getNode(nodeid) + var graph = Visualize.mGraph Control.deselectNode(node) @@ -181,73 +182,73 @@ const Control = { node.eachAdjacency(function (adj) { adj.setData('alpha', 0, 'end') }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['node-property:alpha', 'edge-property:alpha' ], duration: 500 }) setTimeout(function () { - if (nodeid == Metamaps.Visualize.mGraph.root) { // && Metamaps.Visualize.type === "RGraph" + if (nodeid == Visualize.mGraph.root) { // && Visualize.type === "RGraph" var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) graph.root = newroot ? newroot.id : null } - Metamaps.Visualize.mGraph.graph.removeNode(nodeid) + Visualize.mGraph.graph.removeNode(nodeid) }, 500) - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkMappers() }, selectEdge: function (edge) { var filtered = edge.getData('alpha') === 0; // don't select if the edge is filtered - if (filtered || Metamaps.Selected.Edges.indexOf(edge) != -1) return + if (filtered || Selected.Edges.indexOf(edge) != -1) return - var width = Metamaps.Mouse.edgeHoveringOver === edge ? 4 : 2 + var width = Mouse.edgeHoveringOver === edge ? 4 : 2 edge.setDataset('current', { showDesc: true, lineWidth: width, - color: Metamaps.Settings.colors.synapses.selected + color: Settings.colors.synapses.selected }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() - Metamaps.Selected.Edges.push(edge) + Selected.Edges.push(edge) }, deselectAllEdges: function () { - var l = Metamaps.Selected.Edges.length + var l = Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { - var edge = Metamaps.Selected.Edges[i] + var edge = Selected.Edges[i] Control.deselectEdge(edge) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, deselectEdge: function (edge) { edge.setData('showDesc', false, 'current') edge.setDataset('current', { lineWidth: 2, - color: Metamaps.Settings.colors.synapses.normal + color: Settings.colors.synapses.normal }) - if (Metamaps.Mouse.edgeHoveringOver == edge) { + if (Mouse.edgeHoveringOver == edge) { edge.setDataset('current', { showDesc: true, lineWidth: 4 }) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() // remove the edge - Metamaps.Selected.Edges.splice( - Metamaps.Selected.Edges.indexOf(edge), 1) + Selected.Edges.splice( + Selected.Edges.indexOf(edge), 1) }, deleteSelectedEdges: function () { // refers to deleting topics permanently var edge, - l = Metamaps.Selected.Edges.length + l = Selected.Edges.length - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -255,14 +256,14 @@ const Control = { } for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.deleteEdge(edge) } }, deleteEdge: function (edge) { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -274,7 +275,7 @@ const Control = { var synapse = edge.getData('synapses')[index] var mapping = edge.getData('mappings')[index] - var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin') + var permToDelete = Active.Mapper.id === synapse.get('user_id') || Active.Mapper.get('admin') if (permToDelete) { if (edge.getData('synapses').length - 1 === 0) { Control.hideEdge(edge) @@ -289,7 +290,7 @@ const Control = { if (edge.getData('displayIndex')) { delete edge.data.$displayIndex } - $(document).trigger(Metamaps.JIT.events.deleteSynapse, [{ + $(document).trigger(JIT.events.deleteSynapse, [{ mappableid: mappableid }]) } else { @@ -298,13 +299,13 @@ const Control = { }, removeSelectedEdges: function () { // Topic view is handled by removeSelectedNodes - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var l = Metamaps.Selected.Edges.length, + var l = Selected.Edges.length, i, edge - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -312,15 +313,15 @@ const Control = { } for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.removeEdge(edge) } - Metamaps.Selected.Edges = [ ] + Selected.Edges = [ ] }, removeEdge: function (edge) { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -345,34 +346,34 @@ const Control = { if (edge.getData('displayIndex')) { delete edge.data.$displayIndex } - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + $(document).trigger(JIT.events.removeSynapse, [{ mappableid: mappableid }]) }, hideSelectedEdges: function () { var edge, - l = Metamaps.Selected.Edges.length, + l = Selected.Edges.length, i for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.hideEdge(edge) } - Metamaps.Selected.Edges = [ ] + Selected.Edges = [ ] }, hideEdge: function (edge) { var from = edge.nodeFrom.id var to = edge.nodeTo.id edge.setData('alpha', 0, 'end') Control.deselectEdge(edge) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:alpha'], duration: 500 }) setTimeout(function () { - Metamaps.Visualize.mGraph.graph.removeAdjacence(from, to) + Visualize.mGraph.graph.removeAdjacence(from, to) }, 500) - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkSynapses() + Filter.checkMappers() }, updateSelectedPermissions: function (permission) { var edge, synapse, node, topic @@ -384,12 +385,12 @@ const Control = { sCount = 0 // change the permission of the selected synapses, if logged in user is the original creator - var l = Metamaps.Selected.Edges.length + var l = Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] synapse = edge.getData('synapses')[0] - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (synapse.authorizePermissionChange(Active.Mapper)) { synapse.save({ permission: permission }) @@ -398,12 +399,12 @@ const Control = { } // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] topic = node.getData('topic') - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (topic.authorizePermissionChange(Active.Mapper)) { topic.save({ permission: permission }) @@ -428,12 +429,12 @@ const Control = { var nCount = 0 // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] topic = node.getData('topic') - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + if (topic.authorizeToEdit(Active.Mapper)) { topic.save({ 'metacode_id': metacode_id }) @@ -445,7 +446,7 @@ const Control = { var message = nString + ' you can edit updated to ' + metacode.get('name') Metamaps.GlobalUI.notifyUser(message) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, } diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1348e9d2..49267d6d 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,6 +1,11 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ +import Mouse from './Mouse' +import Selected from './Selected' +import Synapse from './Synapse' +import Topic from './Topic' +import Visualize from './Visualize' + /* * Metamaps.Create.js * @@ -8,11 +13,6 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Backbone * - Metamaps.GlobalUI * - Metamaps.Metacodes - * - Metamaps.Mouse - * - Metamaps.Selected - * - Metamaps.Synapse - * - Metamaps.Topic - * - Metamaps.Visualize */ const Create = { @@ -193,7 +193,7 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Metamaps.Topic.getTopicFromAutocomplete(datum.id) + Topic.getTopicFromAutocomplete(datum.id) }) // initialize metacode spinner and then hide it @@ -255,7 +255,7 @@ const Create = { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { var self = Create.newSynapse - if (Metamaps.Selected.Nodes.length < 2) { + if (Selected.Nodes.length < 2) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings } else { @@ -307,16 +307,16 @@ const Create = { $('#synapse_desc').focusout(function () { if (Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally() + Synapse.createSynapseLocally() } }) $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { if (datum.id) { // if they clicked on an existing synapse get it - Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) + Synapse.getSynapseFromAutocomplete(datum.id) } else { Create.newSynapse.description = datum.value - Metamaps.Synapse.createSynapseLocally() + Synapse.createSynapseLocally() } }) }, @@ -338,8 +338,8 @@ const Create = { Create.newTopic.addSynapse = false Create.newSynapse.topic1id = 0 Create.newSynapse.topic2id = 0 - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Visualize.mGraph.plot() + Mouse.synapseStartCoordinates = [] + Visualize.mGraph.plot() }, } } diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index cc21f7e2..aed9964d 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,19 +1,20 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Filter.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.Control * - Metamaps.Creators * - Metamaps.GlobalUI * - Metamaps.Mappers * - Metamaps.Metacodes - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Filter = { filters: { @@ -216,7 +217,7 @@ const Filter = { }, checkMappers: function () { var self = Filter - var onMap = Metamaps.Active.Map ? true : false + var onMap = Active.Map ? true : false if (onMap) { self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') } else { @@ -347,10 +348,10 @@ const Filter = { var passesMetacode, passesMapper, passesSynapse var onMap - if (Metamaps.Active.Map) { + if (Active.Map) { onMap = true } - else if (Metamaps.Active.Topic) { + else if (Active.Topic) { onMap = false } @@ -386,10 +387,10 @@ const Filter = { else console.log(topic) } else { if (n) { - Metamaps.Control.deselectNode(n, true) + Control.deselectNode(n, true) n.setData('alpha', opacityForFilter, 'end') n.eachAdjacency(function (e) { - Metamaps.Control.deselectEdge(e, true) + Control.deselectEdge(e, true) }) } else console.log(topic) @@ -442,12 +443,12 @@ const Filter = { if (visible.mappers.indexOf(user_id) == -1) passesMapper = false else passesMapper = true - var color = Metamaps.Settings.colors.synapses.normal + var color = Settings.colors.synapses.normal if (passesSynapse && passesMapper) { e.setData('alpha', 1, 'end') e.setData('color', color, 'end') } else { - Metamaps.Control.deselectEdge(e, true) + Control.deselectEdge(e, true) e.setData('alpha', opacityForFilter, 'end') } @@ -457,7 +458,7 @@ const Filter = { }) // run the animation - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['node-property:alpha', 'edge-property:alpha'], duration: 200 diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index e963ca32..bc0bab30 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,12 +1,13 @@ /* global Metamaps, $ */ +import Active from './Active' +import Map from './Map' + /* * Metamaps.Import.js.erb * * Dependencies: - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Map * - Metamaps.Mappings * - Metamaps.Metacodes * - Metamaps.Synapses @@ -256,15 +257,15 @@ const Import = { createTopicWithParameters: function (name, metacode_name, permission, desc, link, xloc, yloc, import_id, opts) { var self = Import - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null if (metacode === null) { metacode = Metamaps.Metacodes.where({ name: 'Wildcard' })[0] console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') } - var topic_permission = permission || Metamaps.Active.Map.get('permission') - var defer_to_map_id = permission === topic_permission ? Metamaps.Active.Map.get('id') : null + var topic_permission = permission || Active.Map.get('permission') + var defer_to_map_id = permission === topic_permission ? Active.Map.get('id') : null var topic = new Metamaps.Backbone.Topic({ name: name, metacode_id: metacode.id, @@ -272,7 +273,7 @@ const Import = { defer_to_map_id: defer_to_map_id, desc: desc || "", link: link || "", - calculated_permission: Metamaps.Active.Map.get('permission') + calculated_permission: Active.Map.get('permission') }) Metamaps.Topics.add(topic) diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index 9074f521..fddd90a4 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -1,12 +1,7 @@ -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.Mobile.js - * - * Dependencies: - * - Metamaps.Active - * - Metamaps.Map - */ +import Active from './Active' +import Map from './Map' const Mobile = { init: function () { @@ -30,8 +25,8 @@ const Mobile = { $('#mobile_menu').toggle() }, titleClick: function () { - if (Metamaps.Active.Map) { - Metamaps.Map.InfoBox.open() + if (Active.Map) { + Map.InfoBox.open() } } } diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 93ebb646..e0315486 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,14 +1,9 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Mapper from './Mapper' +import Visualize from './Visualize' -/* - * Metamaps.SynapseCard.js - * - * Dependencies: - * - Metamaps.Active - * - Metamaps.Control - * - Metamaps.Mapper - * - Metamaps.Visualize - */ const SynapseCard = { openSynapseCard: null, showCard: function (edge, e) { @@ -20,7 +15,7 @@ const SynapseCard = { $('#edit_synapse').remove() // so label is missing while editing - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse @@ -30,9 +25,9 @@ const SynapseCard = { var edit_div = document.createElement('div') edit_div.innerHTML = '<div id="editSynUpperBar"></div><div id="editSynLowerBar"></div>' edit_div.setAttribute('id', 'edit_synapse') - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { edit_div.className = 'permission canEdit' - edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : '' + edit_div.className += synapse.authorizePermissionChange(Active.Mapper) ? ' yourEdge' : '' } else { edit_div.className = 'permission cannotEdit' } @@ -94,7 +89,7 @@ const SynapseCard = { // if edge data is blank or just whitespace, populate it with data_nil if ($('#edit_synapse_desc').html().trim() == '') { - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { $('#edit_synapse_desc').html(data_nil) } else { $('#edit_synapse_desc').html('(no description)') @@ -109,8 +104,8 @@ const SynapseCard = { synapse.set('desc', desc) } synapse.trigger('saved') - Metamaps.Control.selectEdge(synapse.get('edge')) - Metamaps.Visualize.mGraph.plot() + Control.selectEdge(synapse.get('edge')) + Visualize.mGraph.plot() }) }, add_drop_down: function (edge, synapse) { @@ -152,7 +147,7 @@ const SynapseCard = { e.stopPropagation() var index = parseInt($(this).attr('data-synapse-index')) edge.setData('displayIndex', index) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() SynapseCard.showCard(edge, false) }) } @@ -167,7 +162,7 @@ const SynapseCard = { var setMapperImage = function (mapper) { $('#edgeUser img').attr('src', mapper.get('image')) } - Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage) + Mapper.get(synapse.get('user_id'), setMapperImage) }, add_perms_form: function (synapse) { @@ -210,7 +205,7 @@ const SynapseCard = { $('#edit_synapse .permissionSelect').remove() } - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (synapse.authorizePermissionChange(Active.Mapper)) { $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect) $('#edit_synapse').click(hidePermissionSelect) } @@ -257,7 +252,7 @@ const SynapseCard = { $('#edit_synapse_right').addClass('checked') } - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { $('#edit_synapse_left, #edit_synapse_right').click(function () { $(this).toggleClass('checked') @@ -281,7 +276,7 @@ const SynapseCard = { node1_id: dir[0], node2_id: dir[1] }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }) } // if } // add_direction_form diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 0ebcc118..ab93e419 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,26 +1,25 @@ /* global Metamaps, $ */ +import Active from './Active' +import JIT from './JIT' +import Selected from './Selected' +import Settings from './Settings' +import Util from './Util' + /* * Metamaps.Topic.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.Backbone * - Metamaps.Backbone * - Metamaps.Create * - Metamaps.Creators - * - Metamaps.Famous * - Metamaps.Filter * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Mappings - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.SynapseCard * - Metamaps.Synapses * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Util * - Metamaps.Visualize */ @@ -58,7 +57,7 @@ const Topic = { launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { - Metamaps.Active.Topic = new bb.Topic(data.topic) + Active.Topic = new bb.Topic(data.topic) Metamaps.Creators = new bb.MapperCollection(data.creators) Metamaps.Topics = new bb.TopicCollection([data.topic].concat(data.relatives)) Metamaps.Synapses = new bb.SynapseCollection(data.synapses) @@ -69,13 +68,13 @@ const Topic = { // build and render the visualization Metamaps.Visualize.type = 'RGraph' - Metamaps.JIT.prepareVizData() + JIT.prepareVizData() // update filters Metamaps.Filter.reset() // reset selected arrays - Metamaps.Selected.reset() + Selected.reset() // these three update the actual filter box with the right list items Metamaps.Filter.checkMetacodes() @@ -83,7 +82,7 @@ const Topic = { Metamaps.Filter.checkMappers() // for mobile - $('#header_content').html(Metamaps.Active.Topic.get('name')) + $('#header_content').html(Active.Topic.get('name')) } $.ajax({ @@ -92,7 +91,7 @@ const Topic = { }) }, end: function () { - if (Metamaps.Active.Topic) { + if (Active.Topic) { $('.rightclickmenu').remove() Metamaps.TopicCard.hideCard() Metamaps.SynapseCard.hideCard() @@ -110,7 +109,7 @@ const Topic = { } }) Metamaps.Router.navigate('/topics/' + nodeid) - Metamaps.Active.Topic = Metamaps.Topics.get(nodeid) + Active.Topic = Metamaps.Topics.get(nodeid) } }, fetchRelatives: function (nodes, metacode_id) { @@ -141,7 +140,7 @@ const Topic = { topicColl.add(topic) var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses) - var graph = Metamaps.JIT.convertModelsToJIT(topicColl, synapseColl)[0] + var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] Metamaps.Visualize.mGraph.op.sum(graph, { type: 'fade', duration: 500, @@ -267,14 +266,14 @@ const Topic = { mappableid: mappingModel.get('mappable_id') } - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + $(document).trigger(JIT.events.newTopic, [newTopicData]) // call a success callback if provided if (opts.success) { opts.success(topicModel) } } var topicSuccessCallback = function (topicModel, response) { - if (Metamaps.Active.Map) { + if (Active.Map) { mapping.save({ mappable_id: topicModel.id }, { success: function (model, response) { mappingSuccessCallback(model, response, topicModel) @@ -290,7 +289,7 @@ const Topic = { } } - if (!Metamaps.Settings.sandbox && createNewInDB) { + if (!Settings.sandbox && createNewInDB) { if (topic.isNew()) { topic.save(null, { success: topicSuccessCallback, @@ -298,7 +297,7 @@ const Topic = { console.log('error saving topic to database') } }) - } else if (!topic.isNew() && Metamaps.Active.Map) { + } else if (!topic.isNew() && Active.Map) { mapping.save(null, { success: mappingSuccessCallback }) @@ -323,7 +322,7 @@ const Topic = { var topic = new Metamaps.Backbone.Topic({ name: Metamaps.Create.newTopic.name, metacode_id: metacode.id, - defer_to_map_id: Metamaps.Active.Map.id + defer_to_map_id: Active.Map.id }) Metamaps.Topics.add(topic) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 5a7f1920..ebc79575 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,16 +1,17 @@ /* global Metamaps, $ */ +import Active from './Active' +import Mapper from './Mapper' +import Util from './Util' +import Visualize from './Visualize' + /* * Metamaps.TopicCard.js * * Dependencies: - * - Metamaps.Active * - Metamaps.GlobalUI - * - Metamaps.Mapper * - Metamaps.Metacodes * - Metamaps.Router - * - Metamaps.Util - * - Metamaps.Visualize */ const TopicCard = { openTopicCard: null, // stores the topic that's currently open @@ -43,7 +44,7 @@ const TopicCard = { var topic = node.getData('topic') self.openTopicCard = topic - self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) + self.authorizedToEdit = topic.authorizeToEdit(Active.Mapper) // populate the card that's about to show with the right topics data self.populateShowCard(topic) return $('.showcard').fadeIn('fast', function() { @@ -96,7 +97,7 @@ const TopicCard = { var setMapperImage = function (mapper) { $('.contributorIcon').attr('src', mapper.get('image')) } - Metamaps.Mapper.get(topic.get('user_id'), setMapperImage) + Mapper.get(topic.get('user_id'), setMapperImage) // starting embed.ly var resetFunc = function () { @@ -179,7 +180,7 @@ const TopicCard = { topic.save({ metacode_id: metacode.id }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') $('.metacodeTitle').hide() $('.showcard .icon').css('z-index', '1') @@ -265,7 +266,7 @@ const TopicCard = { // bind best_in_place ajax callbacks bipName.bind('ajax:success', function () { - var name = Metamaps.Util.decodeEntities($(this).html()) + var name = Util.decodeEntities($(this).html()) topic.set('name', name) topic.trigger('saved') }) @@ -313,7 +314,7 @@ const TopicCard = { } // ability to change permission var selectingPermission = false - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (topic.authorizePermissionChange(Active.Mapper)) { $('.showcard .yourTopic .mapPerm').click(openPermissionSelect) $('.showcard').click(hidePermissionSelect) } @@ -364,11 +365,11 @@ const TopicCard = { var topicForTemplate = self.buildObject(topic) var html = self.generateShowcardHTML.render(topicForTemplate) - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + if (topic.authorizeToEdit(Active.Mapper)) { var perm = document.createElement('div') var string = 'permission canEdit' - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic' + if (topic.authorizePermissionChange(Active.Mapper)) string += ' yourTopic' perm.className = string perm.innerHTML = html showCard.appendChild(perm) @@ -388,7 +389,7 @@ const TopicCard = { var nodeValues = {} - var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = topic.authorizeToEdit(Active.Mapper) if (!authorized) { } else { diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js index 90cd466d..aee0fdf0 100644 --- a/frontend/src/Metamaps/Views.js +++ b/frontend/src/Metamaps/Views.js @@ -1,12 +1,14 @@ /* global Metamaps, $ */ +import Active from './Active' +import ReactComponents from './ReactComponents' +import ReactDOM from 'react-dom' // TODO ensure this isn't a double import + /* * Metamaps.Views.js.erb * * Dependencies: * - Metamaps.Loading - * - Metamaps.Active - * - Metamaps.ReactComponents */ const Views = { @@ -33,7 +35,7 @@ const Views = { } var exploreObj = { - currentUser: Metamaps.Active.Mapper, + currentUser: Active.Mapper, section: self.collection.id, displayStyle: 'grid', maps: self.collection, @@ -42,7 +44,7 @@ const Views = { loadMore: self.loadMore } ReactDOM.render( - React.createElement(Metamaps.ReactComponents.Maps, exploreObj), + React.createElement(ReactComponents.Maps, exploreObj), document.getElementById('explore') ) diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 4aa65772..9e44e8e8 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,10 +1,12 @@ /* global Metamaps, $ */ + +import Active from './Active' +import JIT from './JIT' + /* * Metamaps.Visualize * * Dependencies: - * - Metamaps.Active - * - Metamaps.JIT * - Metamaps.Loading * - Metamaps.Metacodes * - Metamaps.Router @@ -34,7 +36,7 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchmove', function (event) { - // Metamaps.JIT.touchPanZoomHandler(event) + // JIT.touchPanZoomHandler(event) }) // prevent touch events on the canvas from default behaviour @@ -116,25 +118,25 @@ const Visualize = { // clear the previous canvas from #infovis $('#infovis').empty() - RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + RGraphSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) - $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) - $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + $jit.RGraph.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) + $jit.RGraph.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) RGraphSettings.width = $(document).width() RGraphSettings.height = $(document).height() - RGraphSettings.background = Metamaps.JIT.RGraph.background - RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance + RGraphSettings.background = JIT.RGraph.background + RGraphSettings.levelDistance = JIT.RGraph.levelDistance self.mGraph = new $jit.RGraph(RGraphSettings) } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { // clear the previous canvas from #infovis $('#infovis').empty() - FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + FDSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) - $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) - $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + $jit.ForceDirected.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) + $jit.ForceDirected.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) FDSettings.width = $('body').width() FDSettings.height = $('body').height() @@ -145,14 +147,14 @@ const Visualize = { $('#infovis').empty() // init ForceDirected3D - self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings) + self.mGraph = new $jit.ForceDirected3D(JIT.ForceDirected3D.graphSettings) self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position } else { self.mGraph.graph.empty() } - if (self.type == 'ForceDirected' && Metamaps.Active.Mapper) $.post('/maps/' + Metamaps.Active.Map.id + '/events/user_presence') + if (self.type == 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence') function runAnimation () { Metamaps.Loading.hide() @@ -160,22 +162,22 @@ const Visualize = { if (!self.loadLater) { // load JSON data. var rootIndex = 0 - if (Metamaps.Active.Topic) { - var node = _.find(Metamaps.JIT.vizData, function (node) { - return node.id === Metamaps.Active.Topic.id + if (Active.Topic) { + var node = _.find(JIT.vizData, function (node) { + return node.id === Active.Topic.id }) - rootIndex = _.indexOf(Metamaps.JIT.vizData, node) + rootIndex = _.indexOf(JIT.vizData, node) } - self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex) + self.mGraph.loadJSON(JIT.vizData, rootIndex) // compute positions and plot. self.computePositions() self.mGraph.busy = true if (self.type == 'RGraph') { - self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate) + self.mGraph.fx.animate(JIT.RGraph.animate) } else if (self.type == 'ForceDirected') { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + self.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (self.type == 'ForceDirected3D') { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout) + self.mGraph.animate(JIT.ForceDirected.animateFDLayout) } } } @@ -204,8 +206,8 @@ const Visualize = { // update the url now that the map is ready clearTimeout(Metamaps.Router.timeoutId) Metamaps.Router.timeoutId = setTimeout(function () { - var m = Metamaps.Active.Map - var t = Metamaps.Active.Topic + var m = Active.Map + var t = Active.Topic if (m && window.location.pathname !== '/maps/' + m.id) { Metamaps.Router.navigate('/maps/' + m.id) From 120c2c0b673d21b32e372820fce78b16b3d68c4b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 18:31:56 +0800 Subject: [PATCH 023/378] finish most except Backbone --- frontend/src/Metamaps/Account.js | 9 +- frontend/src/Metamaps/AutoLayout.js | 4 +- frontend/src/Metamaps/Backbone.js | 30 +- frontend/src/Metamaps/Control.js | 32 +- frontend/src/Metamaps/Create.js | 4 +- frontend/src/Metamaps/Debug.js | 6 +- frontend/src/Metamaps/Filter.js | 4 +- frontend/src/Metamaps/GlobalUI.js | 557 ++++++++++++++------------- frontend/src/Metamaps/Import.js | 9 +- frontend/src/Metamaps/JIT.js | 485 ++++++++++++----------- frontend/src/Metamaps/Listeners.js | 77 ++-- frontend/src/Metamaps/Map.js | 180 ++++----- frontend/src/Metamaps/Mapper.js | 6 +- frontend/src/Metamaps/PasteInput.js | 6 +- frontend/src/Metamaps/Realtime.js | 320 ++++++++------- frontend/src/Metamaps/Router.js | 158 ++++---- frontend/src/Metamaps/Synapse.js | 52 +-- frontend/src/Metamaps/SynapseCard.js | 2 +- frontend/src/Metamaps/Topic.js | 127 +++--- frontend/src/Metamaps/TopicCard.js | 8 +- frontend/src/Metamaps/Visualize.js | 14 +- frontend/src/Metamaps/index.js | 20 +- 22 files changed, 1077 insertions(+), 1033 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index f424019f..10311cbd 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,12 +1,11 @@ -/* uses window.Metamaps.Erb */ +/* + * Metamaps.Erb + */ const Account = { listenersInitialized: false, - init: function () { - var self = Metamaps.Account - }, initListeners: function () { - var self = Metamaps.Account + var self = Account $('#user_image').change(self.showImagePreview) self.listenersInitialized = true diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index 386b61ef..ee9dc33c 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -8,7 +8,7 @@ const AutoLayout = { timeToTurn: 0, getNextCoord: function () { - var self = Metamaps.AutoLayout + var self = AutoLayout var nextX = self.nextX var nextY = self.nextY @@ -55,7 +55,7 @@ const AutoLayout = { } }, resetSpiral: function () { - var self = Metamaps.AutoLayout + var self = AutoLayout self.nextX = 0 self.nextY = 0 self.nextXshift = 1 diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index ce62c6be..9f18ef32 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -26,9 +26,9 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Backbone = {} +const _Backbone = {} -Metamaps.Backbone.Map = Backbone.Model.extend({ +_Backbone.Map = Backbone.Model.extend({ urlRoot: '/maps', blacklist: ['created_at', 'updated_at', 'created_at_clean', 'updated_at_clean', 'user_name', 'contributor_count', 'topic_count', 'synapse_count', 'topics', 'synapses', 'mappings', 'mappers'], toJSON: function (options) { @@ -82,7 +82,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ return Metamaps.Mapper.get(this.get('user_id')) }, fetchContained: function () { - var bb = Metamaps.Backbone + var bb = _Backbone var that = this var start = function (data) { that.set('mappers', new bb.MapperCollection(data.mappers)) @@ -143,8 +143,8 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ } } }) -Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Map, +_Backbone.MapsCollection = Backbone.Collection.extend({ + model: _Backbone.Map, initialize: function (models, options) { this.id = options.id this.sortBy = options.sortBy @@ -211,7 +211,7 @@ Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ } }) -Metamaps.Backbone.Message = Backbone.Model.extend({ +_Backbone.Message = Backbone.Model.extend({ urlRoot: '/messages', blacklist: ['created_at', 'updated_at'], toJSON: function (options) { @@ -227,12 +227,12 @@ Metamaps.Backbone.Message = Backbone.Model.extend({ */ } }) -Metamaps.Backbone.MessageCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Message, +_Backbone.MessageCollection = Backbone.Collection.extend({ + model: _Backbone.Message, url: '/messages' }) -Metamaps.Backbone.Mapper = Backbone.Model.extend({ +_Backbone.Mapper = Backbone.Model.extend({ urlRoot: '/users', blacklist: ['created_at', 'updated_at'], toJSON: function (options) { @@ -248,13 +248,13 @@ Metamaps.Backbone.Mapper = Backbone.Model.extend({ } }) -Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Mapper, +_Backbone.MapperCollection = Backbone.Collection.extend({ + model: _Backbone.Mapper, url: '/users' }) -Metamaps.Backbone.init = function () { - var self = Metamaps.Backbone +_Backbone.init = function () { + var self = _Backbone self.Metacode = Backbone.Model.extend({ initialize: function () { @@ -694,6 +694,6 @@ Metamaps.Backbone.init = function () { } } self.attachCollectionEvents() -}; // end Metamaps.Backbone.init +}; // end _Backbone.init -export default Metamaps.Backbone +export default _Backbone diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 3eecb126..9e13e40c 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -2,6 +2,7 @@ import Active from './Active' import Filter from './Filter' +import GlobalUI from './GlobalUI' import JIT from './JIT' import Mouse from './Mouse' import Selected from './Selected' @@ -12,7 +13,6 @@ import Visualize from './Visualize' * Metamaps.Control.js * * Dependencies: - * - Metamaps.GlobalUI * - Metamaps.Mappings * - Metamaps.Metacodes * - Metamaps.Synapses @@ -57,7 +57,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -73,7 +73,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -89,7 +89,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -107,7 +107,7 @@ const Control = { }]) Control.hideNode(nodeid) } else { - Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted') + GlobalUI.notifyUser('Only topics you created can be deleted') } }, removeSelectedNodes: function () { // refers to removing topics permanently from a map @@ -132,7 +132,7 @@ const Control = { authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -148,7 +148,7 @@ const Control = { var node = Visualize.mGraph.graph.getNode(nodeid) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -251,7 +251,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -266,7 +266,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -294,7 +294,7 @@ const Control = { mappableid: mappableid }]) } else { - Metamaps.GlobalUI.notifyUser('Only synapses you created can be deleted') + GlobalUI.notifyUser('Only synapses you created can be deleted') } }, removeSelectedEdges: function () { @@ -308,7 +308,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -324,7 +324,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -378,7 +378,7 @@ const Control = { updateSelectedPermissions: function (permission) { var edge, synapse, node, topic - Metamaps.GlobalUI.notifyUser('Working...') + GlobalUI.notifyUser('Working...') // variables to keep track of how many nodes and synapses you had the ability to change the permission of var nCount = 0, @@ -416,12 +416,12 @@ const Control = { var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') var message = nString + sString + ' you created updated to ' + permission - Metamaps.GlobalUI.notifyUser(message) + GlobalUI.notifyUser(message) }, updateSelectedMetacodes: function (metacode_id) { var node, topic - Metamaps.GlobalUI.notifyUser('Working...') + GlobalUI.notifyUser('Working...') var metacode = Metamaps.Metacodes.get(metacode_id) @@ -445,7 +445,7 @@ const Control = { var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') var message = nString + ' you can edit updated to ' + metacode.get('name') - Metamaps.GlobalUI.notifyUser(message) + GlobalUI.notifyUser(message) Visualize.mGraph.plot() }, } diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 49267d6d..c9252aba 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -5,13 +5,13 @@ import Selected from './Selected' import Synapse from './Synapse' import Topic from './Topic' import Visualize from './Visualize' +import GlobalUI from './GlobalUI' /* * Metamaps.Create.js * * Dependencies: * - Metamaps.Backbone - * - Metamaps.GlobalUI * - Metamaps.Metacodes */ @@ -101,7 +101,7 @@ const Create = { bringToFront: true }) - Metamaps.GlobalUI.closeLightbox() + GlobalUI.closeLightbox() $('#topic_name').focus() var mdata = { diff --git a/frontend/src/Metamaps/Debug.js b/frontend/src/Metamaps/Debug.js index e8e40e69..0fe5f769 100644 --- a/frontend/src/Metamaps/Debug.js +++ b/frontend/src/Metamaps/Debug.js @@ -1,6 +1,6 @@ -const Debug = () => { - console.debug(window.Metamaps) - console.debug(`Metamaps Version: ${window.Metamaps.VERSION}`) +const Debug = (arg = window.Metamaps) => { + console.debug(arg) + console.debug(`Metamaps Version: ${arg.VERSION}`) } export default Debug diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index aed9964d..38c4f369 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -2,6 +2,7 @@ import Active from './Active' import Control from './Control' +import GlobalUI from './GlobalUI' import Settings from './Settings' import Visualize from './Visualize' @@ -10,7 +11,6 @@ import Visualize from './Visualize' * * Dependencies: * - Metamaps.Creators - * - Metamaps.GlobalUI * - Metamaps.Mappers * - Metamaps.Metacodes * - Metamaps.Synapses @@ -56,7 +56,7 @@ const Filter = { open: function () { var self = Filter - Metamaps.GlobalUI.Account.close() + GlobalUI.Account.close() $('.sidebarFilterIcon div').addClass('hide') if (!self.isOpen && !self.changing) { diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index 5abf25ee..b24b31c7 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,317 +1,326 @@ -window.Metamaps = window.Metamaps || {}; +import Active from './Active' +import Create from './Create' +import Filter from './Filter' +import Router from './Router' + +/* + * Metamaps.Backbone + * Metamaps.Erb + * Metamaps.Maps + */ const GlobalUI = { - notifyTimeout: null, - lightbox: null, - init: function () { - var self = GlobalUI; + notifyTimeout: null, + lightbox: null, + init: function () { + var self = GlobalUI; - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); - - if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) + self.Search.init(); + self.CreateMap.init(); + self.Account.init(); - //bind lightbox clicks - $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); + if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + //bind lightbox clicks + $('.openLightbox').click(function (event) { + self.openLightbox($(this).attr('data-open')); + event.preventDefault(); + return false; + }); - // initialize global backbone models and collections - if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; - if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; - } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); - // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); - }, - showDiv: function (selector) { - $(selector).show() - $(selector).animate({ - opacity: 1 - }, 200, 'easeOutCubic') - }, - hideDiv: function (selector) { - $(selector).animate({ - opacity: 0 - }, 200, 'easeInCubic', function () { $(this).hide() }) - }, - openLightbox: function (which) { - var self = GlobalUI; + // initialize global backbone models and collections + if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper); - $('.lightboxContent').hide(); - $('#' + which).show(); - - self.lightbox = which; - - $('#lightbox_overlay').show(); - - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; - // animate the content in from the bottom - $('#lightbox_main').animate({ - 'top': '50%', - 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); - - // fade the black overlay in - $('#lightbox_screen').animate({ - 'opacity': '0.42' - }, 200); - - if (which == "switchMetacodes") { - Metamaps.Create.isSwitchingSet = true; - } - }, - - closeLightbox: function (event) { - var self = GlobalUI; - - if (event) event.preventDefault(); - - // animate the lightbox content offscreen - $('#lightbox_main').animate({ - 'top': '100%', - 'margin-top': '0' - }, 200, 'easeInCubic'); - - // fade the black overlay out - $('#lightbox_screen').animate({ - 'opacity': '0.0' - }, 200, function () { - $('#lightbox_overlay').hide(); - }); - - if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); - if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { - Metamaps.Create.cancelMetacodeSetSwitch(); - } - self.lightbox = null; - }, - notifyUser: function (message, leaveOpen) { - var self = GlobalUI; - - $('#toast').html(message) - self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); - if (!leaveOpen) { - self.notifyTimeOut = setTimeout(function () { - self.hideDiv('#toast') - }, 8000); - } - }, - clearNotify: function() { - var self = GlobalUI; - - clearTimeout(self.notifyTimeOut); - self.hideDiv('#toast') - }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; + var mapperCollection = []; + var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + if (Metamaps.Maps.Mapper) { + mapperCollection = Metamaps.Maps.Mapper.models; + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; } + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + // 'Mapper' refers to another mapper + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + }, + showDiv: function (selector) { + $(selector).show() + $(selector).animate({ + opacity: 1 + }, 200, 'easeOutCubic') + }, + hideDiv: function (selector) { + $(selector).animate({ + opacity: 0 + }, 200, 'easeInCubic', function () { $(this).hide() }) + }, + openLightbox: function (which) { + var self = GlobalUI; + + $('.lightboxContent').hide(); + $('#' + which).show(); + + self.lightbox = which; + + $('#lightbox_overlay').show(); + + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + // animate the content in from the bottom + $('#lightbox_main').animate({ + 'top': '50%', + 'margin-top': heightOfContent + }, 200, 'easeOutCubic'); + + // fade the black overlay in + $('#lightbox_screen').animate({ + 'opacity': '0.42' + }, 200); + + if (which == "switchMetacodes") { + Create.isSwitchingSet = true; + } + }, + + closeLightbox: function (event) { + var self = GlobalUI; + + if (event) event.preventDefault(); + + // animate the lightbox content offscreen + $('#lightbox_main').animate({ + 'top': '100%', + 'margin-top': '0' + }, 200, 'easeInCubic'); + + // fade the black overlay out + $('#lightbox_screen').animate({ + 'opacity': '0.0' + }, 200, function () { + $('#lightbox_overlay').hide(); + }); + + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); + if (Create && Create.isSwitchingSet) { + Create.cancelMetacodeSetSwitch(); + } + self.lightbox = null; + }, + notifyUser: function (message, leaveOpen) { + var self = GlobalUI; + + $('#toast').html(message) + self.showDiv('#toast') + clearTimeout(self.notifyTimeOut); + if (!leaveOpen) { + self.notifyTimeOut = setTimeout(function () { + self.hideDiv('#toast') + }, 8000); + } + }, + clearNotify: function() { + var self = GlobalUI; + + clearTimeout(self.notifyTimeOut); + self.hideDiv('#toast') + }, + shareInvite: function(inviteLink) { + window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + } } GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = GlobalUI.CreateMap; + newMap: null, + emptyMapForm: "", + emptyForkMapForm: "", + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = GlobalUI.CreateMap; - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - self.bindFormEvents(); + self.bindFormEvents(); - self.emptyMapForm = $('#new_map').html(); + self.emptyMapForm = $('#new_map').html(); - }, - bindFormEvents: function () { - var self = GlobalUI.CreateMap; + }, + bindFormEvents: function () { + var self = GlobalUI.CreateMap; - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { - if (event.keyCode === 13) self.submit() - }) + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + if (event.keyCode === 13) self.submit() + }) - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault(); + GlobalUI.closeLightbox(); + }); + $('.new_map button.submitMap').unbind().bind('click', self.submit); - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Metamaps.Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; - }, - switchPermission: function () { - var self = GlobalUI.CreateMap; + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission); + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function(){ + $(this).remove(); + }); + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; + stringStart += id; + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + var page = Active.Map ? 'map' : 'page'; + var stringEnd = "</a></div>"; + return stringStart + page + stringEnd; + }, + switchPermission: function () { + var self = GlobalUI.CreateMap; - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); + self.newMap.set('permission', $(this).attr('data-permission')); + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); + $(this).find('.mapPermIcon').addClass('selected'); - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); - }, - submit: function (event) { - if (event) event.preventDefault(); + var permText = $(this).find('.tip').html(); + $(this).parents('.new_map').find('.permText').html(permText); + }, + submit: function (event) { + if (event) event.preventDefault(); - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); - } + if (GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap); + self.newMap.set('synapsesToMap', self.synapsesToMap); + } - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); + self.newMap.set('name', $form.find('#map_name').val()); + self.newMap.set('desc', $form.find('#map_desc').val()); - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; - } + if (self.newMap.get('name').length===0){ + self.throwMapNameError(); + return; + } - self.newMap.save(null, { - success: self.success - // TODO add error message - }); + self.newMap.save(null, { + success: self.success + // TODO add error message + }); - GlobalUI.closeLightbox(); - GlobalUI.notifyUser('Working...'); - }, - throwMapNameError: function () { - var self = GlobalUI.CreateMap; + GlobalUI.closeLightbox(); + GlobalUI.notifyUser('Working...'); + }, + throwMapNameError: function () { + var self = GlobalUI.CreateMap; - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + var message = $("<div class='feedback_message'>Please enter a map name...</div>"); - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); - }, - success: function (model) { - var self = GlobalUI.CreateMap; + $form.find('#map_name').after(message); + setTimeout(function(){ + message.fadeOut('fast', function(){ + message.remove(); + }); + }, 5000); + }, + success: function (model) { + var self = GlobalUI.CreateMap; - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); + //push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model); - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var form = $(formId); - GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); + GlobalUI.clearNotify(); + $('#wrapper').append(self.generateSuccessMessage(model.id)); - }, - reset: function (id) { - var self = GlobalUI.CreateMap; + }, + reset: function (id) { + var self = GlobalUI.CreateMap; - var form = $('#' + id); + var form = $('#' + id); - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); - } + if (id === "fork_map") { + self.topicsToMap = []; + self.synapsesToMap = []; + form.html(self.emptyForkMapForm); + } + else { + form.html(self.emptyMapForm); + } - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.bindFormEvents(); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - return false; - }, + return false; + }, } GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = GlobalUI.Account; + isOpen: false, + changing: false, + init: function () { + var self = GlobalUI.Account; - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); - }, - toggleBox: function (event) { - var self = GlobalUI.Account; + $('.sidebarAccountIcon').click(self.toggleBox); + $('.sidebarAccountBox').click(function(event){ + event.stopPropagation(); + }); + $('body').click(self.close); + }, + toggleBox: function (event) { + var self = GlobalUI.Account; - if (self.isOpen) self.close(); - else self.open(); + if (self.isOpen) self.close(); + else self.open(); - event.stopPropagation(); - }, - open: function () { - var self = GlobalUI.Account; + event.stopPropagation(); + }, + open: function () { + var self = GlobalUI.Account; - Metamaps.Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + Filter.close(); + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); - if (!self.isOpen && !self.changing) { - self.changing = true; - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); - } - }, - close: function () { - var self = GlobalUI.Account; - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); - if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); - } + if (!self.isOpen && !self.changing) { + self.changing = true; + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false; + self.isOpen = true; + $('.sidebarAccountBox #user_email').focus(); + }); } + }, + close: function () { + var self = GlobalUI.Account; + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + if (!self.changing) { + self.changing = true; + $('.sidebarAccountBox #user_email').blur(); + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false; + self.isOpen = false; + }); + } + } } GlobalUI.Search = { @@ -425,8 +434,8 @@ GlobalUI.Search = { startTypeahead: function () { var self = GlobalUI.Search; - var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; var topics = { @@ -455,8 +464,8 @@ GlobalUI.Search = { url: '/search/topics', prepare: function(query, settings) { settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + if (Active.Mapper && self.limitTopicsToMe) { + settings.url += "&user=" + Active.Mapper.id.toString(); } return settings; }, @@ -488,8 +497,8 @@ GlobalUI.Search = { url: '/search/maps', prepare: function(query, settings) { settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + if (Active.Mapper && self.limitMapsToMe) { + settings.url += "&user=" + Active.Mapper.id.toString(); } return settings; }, @@ -578,11 +587,11 @@ GlobalUI.Search = { self.close(0, true); var win; if (datum.rtype == "topic") { - Metamaps.Router.topics(datum.id); + Router.topics(datum.id); } else if (datum.rtype == "map") { - Metamaps.Router.maps(datum.id); + Router.maps(datum.id); } else if (datum.rtype == "mapper") { - Metamaps.Router.explore("mapper", datum.id); + Router.explore("mapper", datum.id); } } }, diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index bc0bab30..d5a4b4e1 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,7 +1,10 @@ /* global Metamaps, $ */ import Active from './Active' +import GlobalUI from './GlobalUI' import Map from './Map' +import Synapse from './Synapse' +import Topic from './Topic' /* * Metamaps.Import.js.erb @@ -290,11 +293,11 @@ const Import = { Metamaps.Mappings.add(mapping) // this function also includes the creation of the topic in the database - Metamaps.Topic.renderTopic(mapping, topic, true, true, { + Topic.renderTopic(mapping, topic, true, true, { success: opts.success }) - Metamaps.GlobalUI.hideDiv('#instructions') + GlobalUI.hideDiv('#instructions') }, createSynapseWithParameters: function (desc, category, permission, @@ -322,7 +325,7 @@ const Import = { }) Metamaps.Mappings.add(mapping) - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true) + Synapse.renderSynapse(mapping, synapse, node1, node2, true) } } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index ec8195de..50c48985 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,5 +1,30 @@ /* global Metamaps */ +import Active from './Active' +import Control from './Control' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import Map from './Map' +import Mouse from './Mouse' +import Realtime from './Realtime' +import Selected from './Selected' +import Settings from './Settings' +import Synapse from './Synapse' +import SynapseCard from './SynapseCard' +import Topic from './Topic' +import TopicCard from './TopicCard' +import Util from './Util' +import Visualize from './Visualize' + +/* + * Metamaps.Erb + * Metamaps.Mappings + * Metamaps.Metacodes + * Metamaps.Synapses + * Metamaps.Topics + */ + let panningInt const JIT = { @@ -30,11 +55,11 @@ const JIT = { $('.zoomOut').click(self.zoomOut) var zoomExtents = function (event) { - self.zoomExtents(event, Metamaps.Visualize.mGraph.canvas) + self.zoomExtents(event, Visualize.mGraph.canvas) } $('.zoomExtents').click(zoomExtents) - $('.takeScreenshot').click(Metamaps.Map.exportImage) + $('.takeScreenshot').click(Map.exportImage) self.topicDescImage = new Image() self.topicDescImage.src = Metamaps.Erb['topic_description_signifier.png'] @@ -80,7 +105,7 @@ const JIT = { if (existingEdge) { // for when you're dealing with multiple relationships between the same two topics - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = s.getMapping() existingEdge.data['$mappingIDs'].push(mapping.id) } @@ -105,7 +130,7 @@ const JIT = { // reset/empty vizData self.vizData = [] - Metamaps.Visualize.loadLater = false + Visualize.loadLater = false var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) @@ -121,12 +146,12 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() $('#instructions div.addTopic').show() - Metamaps.GlobalUI.showDiv('#instructions') - Metamaps.Visualize.loadLater = true + GlobalUI.showDiv('#instructions') + Visualize.loadLater = true } - else Metamaps.GlobalUI.hideDiv('#instructions') + else GlobalUI.hideDiv('#instructions') - Metamaps.Visualize.render() + Visualize.render() }, // prepareVizData edgeRender: function (adj, canvas) { // get nodes cartesian coordinates @@ -151,7 +176,7 @@ const JIT = { // label placement on edges if (canvas.denySelected) { - var color = Metamaps.Settings.colors.synapses.normal + var color = Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) @@ -191,7 +216,7 @@ const JIT = { if (!canvas.denySelected && desc != '' && showDesc) { // '&' to '&' - desc = Metamaps.Util.decodeEntities(desc) + desc = Util.decodeEntities(desc) // now adjust the label placement var ctx = canvas.getCtx() @@ -199,7 +224,7 @@ const JIT = { ctx.fillStyle = '#FFF' ctx.textBaseline = 'alphabetic' - var arrayOfLabelLines = Metamaps.Util.splitLine(desc, 30).split('\n') + var arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') var index, lineWidths = [] for (index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) @@ -258,7 +283,7 @@ const JIT = { transition: $jit.Trans.Quad.easeInOut, duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false $(document).trigger(JIT.events.animationDone) } }, @@ -267,7 +292,7 @@ const JIT = { transition: $jit.Trans.Elastic.easeOut, duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, graphSettings: { @@ -306,7 +331,7 @@ const JIT = { }, Edge: { overridable: true, - color: Metamaps.Settings.colors.synapses.normal, + color: Settings.colors.synapses.normal, type: 'customEdge', lineWidth: 2, alpha: 1 @@ -317,7 +342,7 @@ const JIT = { size: 20, family: 'arial', textBaseline: 'alphabetic', - color: Metamaps.Settings.colors.labels.text + color: Settings.colors.labels.text }, // Add Tips Tips: { @@ -359,26 +384,26 @@ const JIT = { // remove the rightclickmenu $('.rightclickmenu').remove() - if (Metamaps.Mouse.boxStartCoordinates) { + if (Mouse.boxStartCoordinates) { if (e.ctrlKey) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() - var bS = Metamaps.Mouse.boxStartCoordinates - var bE = Metamaps.Mouse.boxEndCoordinates + var bS = Mouse.boxStartCoordinates + var bE = Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { JIT.zoomToBox(e) return } else { - Metamaps.Mouse.boxStartCoordinates = null - Metamaps.Mouse.boxEndCoordinates = null + Mouse.boxStartCoordinates = null + Mouse.boxEndCoordinates = null } // console.log('called zoom to box') } if (e.shiftKey) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) // console.log('called select with box') return @@ -404,9 +429,9 @@ const JIT = { // remove the rightclickmenu $('.rightclickmenu').remove() - if (Metamaps.Mouse.boxStartCoordinates) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + if (Mouse.boxStartCoordinates) { + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) return } @@ -441,7 +466,7 @@ const JIT = { if (!canvas.denySelected && node.selected) { ctx.beginPath() ctx.arc(pos.x, pos.y, dim + 3, 0, 2 * Math.PI, false) - ctx.strokeStyle = Metamaps.Settings.colors.topics.selected + ctx.strokeStyle = Settings.colors.topics.selected ctx.lineWidth = 2 ctx.stroke() } @@ -482,8 +507,8 @@ const JIT = { 'contains': function (node, pos) { var npos = node.pos.getc(true), dim = node.getData('dim'), - arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 30).split('\n'), - ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n'), + ctx = Visualize.mGraph.canvas.getCtx() var height = 25 * arrayOfLabelLines.length @@ -528,7 +553,7 @@ const JIT = { transition: $jit.Trans.Elastic.easeOut, duration: 2500, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, graphSettings: { @@ -589,13 +614,13 @@ const JIT = { onMouseMove: function (node, eventInfo, e) { // if(this.i++ % 3) return var pos = eventInfo.getPos() - Metamaps.Visualize.cameraPosition.x += (pos.x - Metamaps.Visualize.cameraPosition.x) * 0.5 - Metamaps.Visualize.cameraPosition.y += (-pos.y - Metamaps.Visualize.cameraPosition.y) * 0.5 - Metamaps.Visualize.mGraph.plot() + Visualize.cameraPosition.x += (pos.x - Visualize.cameraPosition.x) * 0.5 + Visualize.cameraPosition.y += (-pos.y - Visualize.cameraPosition.y) * 0.5 + Visualize.mGraph.plot() }, onMouseWheel: function (delta) { - Metamaps.Visualize.cameraPosition.z += -delta * 20 - Metamaps.Visualize.mGraph.plot() + Visualize.cameraPosition.z += -delta * 20 + Visualize.mGraph.plot() }, onClick: function () {} }, @@ -616,7 +641,7 @@ const JIT = { modes: ['polar'], duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ @@ -636,10 +661,10 @@ const JIT = { // don't do anything if the edge is filtered // or if the canvas is animating - if (filtered || Metamaps.Visualize.mGraph.busy) return + if (filtered || Visualize.mGraph.busy) return $('canvas').css('cursor', 'pointer') - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + var edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', true, 'current') @@ -648,16 +673,16 @@ const JIT = { edge.setDataset('end', { lineWidth: 4 }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, // onMouseEnter onMouseLeave: function (edge) { if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered $('canvas').css('cursor', 'default') - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + var edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', false, 'current') @@ -666,65 +691,65 @@ const JIT = { edge.setDataset('end', { lineWidth: 2 }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, // onMouseLeave onMouseMoveHandler: function (node, eventInfo, e) { var self = JIT - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var node = eventInfo.getNode() var edge = eventInfo.getEdge() // if we're on top of a node object, act like there aren't edges under it if (node != false) { - if (Metamaps.Mouse.edgeHoveringOver) { - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + if (Mouse.edgeHoveringOver) { + self.onMouseLeave(Mouse.edgeHoveringOver) } $('canvas').css('cursor', 'pointer') return } - if (edge == false && Metamaps.Mouse.edgeHoveringOver != false) { + if (edge == false && Mouse.edgeHoveringOver != false) { // mouse not on an edge, but we were on an edge previously - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver == false) { + self.onMouseLeave(Mouse.edgeHoveringOver) + } else if (edge != false && Mouse.edgeHoveringOver == false) { // mouse is on an edge, but there isn't a stored edge self.onMouseEnter(edge) - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver != edge) { + } else if (edge != false && Mouse.edgeHoveringOver != edge) { // mouse is on an edge, but a different edge is stored - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + self.onMouseLeave(Mouse.edgeHoveringOver) self.onMouseEnter(edge) } // could be false - Metamaps.Mouse.edgeHoveringOver = edge + Mouse.edgeHoveringOver = edge if (!node && !edge) { $('canvas').css('cursor', 'default') } }, // onMouseMoveHandler enterKeyHandler: function () { - var creatingMap = Metamaps.GlobalUI.lightbox + var creatingMap = GlobalUI.lightbox if (creatingMap === 'newmap' || creatingMap === 'forkmap') { - Metamaps.GlobalUI.CreateMap.submit() + GlobalUI.CreateMap.submit() } // this is to submit new topic creation - else if (Metamaps.Create.newTopic.beingCreated) { - Metamaps.Topic.createTopicLocally() + else if (Create.newTopic.beingCreated) { + Topic.createTopicLocally() } // to submit new synapse creation - else if (Metamaps.Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally() + else if (Create.newSynapse.beingCreated) { + Synapse.createSynapseLocally() } }, // enterKeyHandler escKeyHandler: function () { - Metamaps.Control.deselectAllEdges() - Metamaps.Control.deselectAllNodes() + Control.deselectAllEdges() + Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { var self = JIT @@ -734,7 +759,7 @@ const JIT = { var positionsToSend = {} var topic - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (node && !node.nodeFrom) { var pos = eventInfo.getPos() @@ -750,7 +775,7 @@ const JIT = { } else if (whatToDo == 'only-drag-this-one') { node.pos.setc(pos.x, pos.y) - if (Metamaps.Active.Map) { + if (Active.Map) { topic = node.getData('topic') // we use the topic ID not the node id // because we can't depend on the node id @@ -760,24 +785,24 @@ const JIT = { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { - var len = Metamaps.Selected.Nodes.length + var len = Selected.Nodes.length // first define offset for each node var xOffset = [] var yOffset = [] for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] xOffset[i] = n.pos.x - node.pos.x yOffset[i] = n.pos.y - node.pos.y } // for for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] var x = pos.x + xOffset[i] var y = pos.y + yOffset[i] n.pos.setc(x, y) - if (Metamaps.Active.Map) { + if (Active.Map) { topic = n.getData('topic') // we use the topic ID not the node id // because we can't depend on the node id @@ -787,15 +812,15 @@ const JIT = { } } // for - if (Metamaps.Active.Map) { + if (Active.Map) { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } // if if (whatToDo == 'deselect') { - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } // if it's a right click or holding down alt, start synapse creation ->third option is for firefox else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { @@ -803,48 +828,48 @@ const JIT = { JIT.tempNode = node JIT.tempInit = true - Metamaps.Create.newTopic.hide() - Metamaps.Create.newSynapse.hide() + Create.newTopic.hide() + Create.newSynapse.hide() // set the draw synapse start positions - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length if (l > 0) { for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i] - Metamaps.Mouse.synapseStartCoordinates.push({ + var n = Selected.Nodes[i] + Mouse.synapseStartCoordinates.push({ x: n.pos.getc().x, y: n.pos.getc().y }) } } else { - Metamaps.Mouse.synapseStartCoordinates = [{ + Mouse.synapseStartCoordinates = [{ x: JIT.tempNode.pos.getc().x, y: JIT.tempNode.pos.getc().y }] } - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y } } // let temp = eventInfo.getNode() - if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned + if (temp != false && temp.id != node.id && Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned JIT.tempNode2 = temp - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: JIT.tempNode2.pos.getc().x, y: JIT.tempNode2.pos.getc().y } // before making the highlighted one bigger, make sure all the others are regular size - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) temp.setData('dim', 35, 'current') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } else if (!temp) { JIT.tempNode2 = null - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) // pop up node creation :) @@ -852,21 +877,21 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') - Metamaps.Create.newTopic.x = eventInfo.getPos().x - Metamaps.Create.newTopic.y = eventInfo.getPos().y - Metamaps.Visualize.mGraph.plot() + Create.newTopic.x = eventInfo.getPos().x + Create.newTopic.y = eventInfo.getPos().y + Visualize.mGraph.plot() - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y } } } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Active.Topic) { + GlobalUI.notifyUser('Cannot create in Topic view.') } else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') } } }, // onDragMoveTopicHandler @@ -876,30 +901,30 @@ const JIT = { JIT.tempNode2 = null JIT.tempInit = false // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Mouse.synapseEndCoordinates = null - Metamaps.Visualize.mGraph.plot() + Mouse.synapseStartCoordinates = [] + Mouse.synapseEndCoordinates = null + Visualize.mGraph.plot() }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { var midpoint = {}, pixelPos, mapping if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse - Metamaps.Create.newTopic.addSynapse = true - Metamaps.Create.newTopic.open() + Create.newTopic.addSynapse = true + Create.newTopic.open() } else if (JIT.tempInit && JIT.tempNode2 != null) { // this means you want to create a synapse between two existing topics - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id - Metamaps.Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id + Create.newTopic.addSynapse = false + Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id JIT.tempNode2.setData('dim', 25, 'current') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() midpoint.x = JIT.tempNode.pos.getc().x + (JIT.tempNode2.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 midpoint.y = JIT.tempNode.pos.getc().y + (JIT.tempNode2.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 - pixelPos = Metamaps.Util.coordsToPixels(midpoint) + pixelPos = Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') - Metamaps.Create.newSynapse.open() + Create.newSynapse.open() JIT.tempNode = null JIT.tempNode2 = null JIT.tempInit = false @@ -908,17 +933,17 @@ const JIT = { // check whether to save mappings var checkWhetherToSave = function () { - var map = Metamaps.Active.Map + var map = Active.Map if (!map) return false - var mapper = Metamaps.Active.Mapper + var mapper = Active.Mapper // this case // covers when it is a public map owned by you // and also when it's a private map var activeMappersMap = map.authorizePermissionChange(mapper) var commonsMap = map.get('permission') === 'commons' - var realtimeOn = Metamaps.Realtime.status + var realtimeOn = Realtime.status // don't save if commons map, and you have realtime off, // even if you're map creator @@ -932,9 +957,9 @@ const JIT = { yloc: node.getPos().y }) // also save any other selected nodes that also got dragged along - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] if (n !== node) { mapping = n.getData('mapping') mapping.save({ @@ -948,61 +973,61 @@ const JIT = { }, // onDragEndTopicHandler canvasClickHandler: function (canvasLoc, e) { // grab the location and timestamp of the click - var storedTime = Metamaps.Mouse.lastCanvasClick + var storedTime = Mouse.lastCanvasClick var now = Date.now() // not compatible with IE8 FYI - Metamaps.Mouse.lastCanvasClick = now + Mouse.lastCanvasClick = now - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE && !Metamaps.Mouse.didPan) { - if (Metamaps.Active.Map && !authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE && !Mouse.didPan) { + if (Active.Map && !authorized) { + GlobalUI.notifyUser('Cannot edit Public map.') return } - else if (Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + else if (Active.Topic) { + GlobalUI.notifyUser('Cannot create in Topic view.') return } // DOUBLE CLICK // pop up node creation :) - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newTopic.x = canvasLoc.x - Metamaps.Create.newTopic.y = canvasLoc.y + Create.newTopic.addSynapse = false + Create.newTopic.x = canvasLoc.x + Create.newTopic.y = canvasLoc.y $('#new_topic').css('left', e.clientX + 'px') $('#new_topic').css('top', e.clientY + 'px') - Metamaps.Create.newTopic.open() - } else if (!Metamaps.Mouse.didPan) { + Create.newTopic.open() + } else if (!Mouse.didPan) { // SINGLE CLICK, no pan - Metamaps.Filter.close() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Create.newTopic.hide() + Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide() $('.rightclickmenu').remove() // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Mouse.synapseEndCoordinates = null + Mouse.synapseStartCoordinates = [] + Mouse.synapseEndCoordinates = null JIT.tempInit = false JIT.tempNode = null JIT.tempNode2 = null if (!e.ctrlKey && !e.shiftKey) { - Metamaps.Control.deselectAllEdges() - Metamaps.Control.deselectAllNodes() + Control.deselectAllEdges() + Control.deselectAllNodes() } } }, // canvasClickHandler nodeDoubleClickHandler: function (node, e) { - Metamaps.TopicCard.showCard(node) + TopicCard.showCard(node) }, // nodeDoubleClickHandler edgeDoubleClickHandler: function (adj, e) { - Metamaps.SynapseCard.showCard(adj, e) + SynapseCard.showCard(adj, e) }, // nodeDoubleClickHandler nodeWasDoubleClicked: function () { // grab the timestamp of the click - var storedTime = Metamaps.Mouse.lastNodeClick + var storedTime = Mouse.lastNodeClick var now = Date.now() // not compatible with IE8 FYI - Metamaps.Mouse.lastNodeClick = now + Mouse.lastNodeClick = now - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) { + if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE) { return true } else { return false @@ -1015,12 +1040,12 @@ const JIT = { // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) // return value: deselect node again after? - if (Metamaps.Selected.Nodes.length == 0) { + if (Selected.Nodes.length == 0) { return 'only-drag-this-one' } - if (Metamaps.Selected.Nodes.indexOf(node) == -1) { + if (Selected.Nodes.indexOf(node) == -1) { if (e.shiftKey) { - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) return 'nothing' } else { return 'only-drag-this-one' @@ -1040,18 +1065,18 @@ const JIT = { }, selectWithBox: function (e) { var self = this - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y + var sX = Mouse.boxStartCoordinates.x, + sY = Mouse.boxStartCoordinates.y, + eX = Mouse.boxEndCoordinates.x, + eY = Mouse.boxEndCoordinates.y if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } // select all nodes that are within the box - Metamaps.Visualize.mGraph.graph.eachNode(function(n) { + Visualize.mGraph.graph.eachNode(function(n) { var pos = self.getNodeXY(n) var x = pos.x, y = pos.y @@ -1064,12 +1089,12 @@ const JIT = { (sX < x && x < eX && sY > y && y > eY)) { if (e.shiftKey) { if (n.selected) { - Metamaps.Control.deselectNode(n) + Control.deselectNode(n) } else { - Metamaps.Control.selectNode(n, e) + Control.selectNode(n, e) } } else { - Metamaps.Control.selectNode(n, e) + Control.selectNode(n, e) } } }) @@ -1170,30 +1195,30 @@ const JIT = { if (selectTest) { // shiftKey = toggleSelect, otherwise if (e.shiftKey) { - if (Metamaps.Selected.Edges.indexOf(edge) != -1) { - Metamaps.Control.deselectEdge(edge) + if (Selected.Edges.indexOf(edge) != -1) { + Control.deselectEdge(edge) } else { - Metamaps.Control.selectEdge(edge) + Control.selectEdge(edge) } } else { - Metamaps.Control.selectEdge(edge) + Control.selectEdge(edge) } } }) - Metamaps.Mouse.boxStartCoordinates = false - Metamaps.Mouse.boxEndCoordinates = false - Metamaps.Visualize.mGraph.plot() + Mouse.boxStartCoordinates = false + Mouse.boxEndCoordinates = false + Visualize.mGraph.plot() }, // selectWithBox drawSelectBox: function (eventInfo, e) { - var ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + var ctx = Visualize.mGraph.canvas.getCtx() - var startX = Metamaps.Mouse.boxStartCoordinates.x, - startY = Metamaps.Mouse.boxStartCoordinates.y, + var startX = Mouse.boxStartCoordinates.x, + startY = Mouse.boxStartCoordinates.y, currX = eventInfo.getPos().x, currY = eventInfo.getPos().y - Metamaps.Visualize.mGraph.canvas.clear() - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.canvas.clear() + Visualize.mGraph.plot() ctx.beginPath() ctx.moveTo(startX, startY) @@ -1205,7 +1230,7 @@ const JIT = { ctx.stroke() }, // drawSelectBox selectNodeOnClickHandler: function (node, e) { - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var self = JIT @@ -1216,8 +1241,8 @@ const JIT = { } // if on a topic page, let alt+click center you on a new topic - if (Metamaps.Active.Topic && e.altKey) { - Metamaps.RGraph.centerOn(node.id) + if (Active.Topic && e.altKey) { + JIT.RGraph.centerOn(node.id) return } @@ -1232,24 +1257,24 @@ const JIT = { var nodeAlreadySelected = node.selected if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } if (nodeAlreadySelected) { - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } else { - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) } // trigger animation to final styles - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth:color:alpha'], duration: 500 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectNodeOnClickHandler selectNodeOnRightClickHandler: function (node, e) { @@ -1259,10 +1284,10 @@ const JIT = { e.preventDefault() e.stopPropagation() - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return // select the node - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) // delete old right click menu $('.rightclickmenu').remove() @@ -1272,20 +1297,20 @@ const JIT = { // add the proper options to the menu var menustring = '<ul>' - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) var disabled = authorized ? '' : 'disabled' - if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' + if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' - if (Metamaps.Active.Topic) { + if (Active.Topic) { menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>' } menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { var options = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ @@ -1299,8 +1324,8 @@ const JIT = { menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>' } - if (Metamaps.Active.Topic) { - if (!Metamaps.Active.Mapper) { + if (Active.Topic) { + if (!Active.Mapper) { menustring += '<li class="rc-spacer"></li>' } @@ -1358,30 +1383,30 @@ const JIT = { if (authorized) { $('.rc-delete').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.deleteSelected() + Control.deleteSelected() }) } // remove the selected things from the map - if (Metamaps.Active.Topic || authorized) { + if (Active.Topic || authorized) { $('.rc-remove').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.removeSelectedEdges() - Metamaps.Control.removeSelectedNodes() + Control.removeSelectedEdges() + Control.removeSelectedNodes() }) } // hide selected nodes and synapses until refresh $('.rc-hide').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.hideSelectedEdges() - Metamaps.Control.hideSelectedNodes() + Control.hideSelectedEdges() + Control.hideSelectedNodes() }) // when in radial, center on the topic you picked $('.rc-center').click(function () { $('.rightclickmenu').remove() - Metamaps.Topic.centerOn(node.id) + Topic.centerOn(node.id) }) // open the entity in a new tab @@ -1395,14 +1420,14 @@ const JIT = { $('.rc-permission li').click(function () { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()) + Control.updateSelectedPermissions($(this).text()) }) // change the metacode of all the selected nodes that you have edit permission for $('.rc-metacode li li').click(function () { $('.rightclickmenu').remove() // - Metamaps.Control.updateSelectedMetacodes($(this).attr('data-id')) + Control.updateSelectedMetacodes($(this).attr('data-id')) }) // fetch relatives @@ -1416,7 +1441,7 @@ const JIT = { $('.rc-siblings .fetchAll').click(function () { $('.rightclickmenu').remove() // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node) + Topic.fetchRelatives(node) }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { @@ -1448,7 +1473,7 @@ const JIT = { $('.rc-siblings .getSiblings').click(function () { $('.rightclickmenu').remove() // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node, $(this).attr('data-id')) + Topic.fetchRelatives(node, $(this).attr('data-id')) }) } @@ -1460,7 +1485,7 @@ const JIT = { }) }, selectEdgeOnClickHandler: function (adj, e) { - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var self = JIT @@ -1478,22 +1503,22 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1 + var edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } if (edgeAlreadySelected) { - Metamaps.Control.deselectEdge(adj) + Control.deselectEdge(adj) } else { - Metamaps.Control.selectEdge(adj) + Control.selectEdge(adj) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectEdgeOnClickHandler selectEdgeOnRightClickHandler: function (adj, e) { @@ -1507,9 +1532,9 @@ const JIT = { e.preventDefault() e.stopPropagation() - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return - Metamaps.Control.selectEdge(adj) + Control.selectEdge(adj) // delete old right click menu $('.rightclickmenu').remove() @@ -1520,18 +1545,18 @@ const JIT = { // add the proper options to the menu var menustring = '<ul>' - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) var disabled = authorized ? '' : 'disabled' - if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' + if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-spacer"></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { var permOptions = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ @@ -1582,7 +1607,7 @@ const JIT = { if (authorized) { $('.rc-delete').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.deleteSelected() + Control.deleteSelected() }) } @@ -1590,30 +1615,30 @@ const JIT = { if (authorized) { $('.rc-remove').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.removeSelectedEdges() - Metamaps.Control.removeSelectedNodes() + Control.removeSelectedEdges() + Control.removeSelectedNodes() }) } // hide selected nodes and synapses until refresh $('.rc-hide').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.hideSelectedEdges() - Metamaps.Control.hideSelectedNodes() + Control.hideSelectedEdges() + Control.hideSelectedNodes() }) // change the permission of all the selected nodes and synapses that you were the originator of $('.rc-permission li').click(function () { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()) + Control.updateSelectedPermissions($(this).text()) }) }, // selectEdgeOnRightClickHandler SmoothPanning: function () { - var sx = Metamaps.Visualize.mGraph.canvas.scaleOffsetX, - sy = Metamaps.Visualize.mGraph.canvas.scaleOffsetY, - y_velocity = Metamaps.Mouse.changeInY, // initial y velocity - x_velocity = Metamaps.Mouse.changeInX, // initial x velocity + var sx = Visualize.mGraph.canvas.scaleOffsetX, + sy = Visualize.mGraph.canvas.scaleOffsetY, + y_velocity = Mouse.changeInY, // initial y velocity + x_velocity = Mouse.changeInX, // initial x velocity easing = 1 // frictional value easing = 1 @@ -1623,7 +1648,7 @@ const JIT = { }, 1) function myTimer () { - Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) + Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) $(document).trigger(JIT.events.pan) easing = easing * 0.75 @@ -1725,11 +1750,11 @@ const JIT = { } }, // renderEdgeArrows zoomIn: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25) + Visualize.mGraph.canvas.scale(1.25, 1.25) $(document).trigger(JIT.events.zoom, [event]) }, zoomOut: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8) + Visualize.mGraph.canvas.scale(0.8, 0.8) $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { @@ -1743,12 +1768,12 @@ const JIT = { canvas.translate(-1 * offsetX, -1 * offsetY) }, zoomToBox: function (event) { - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y + var sX = Mouse.boxStartCoordinates.x, + sY = Mouse.boxStartCoordinates.y, + eX = Mouse.boxEndCoordinates.x, + eY = Mouse.boxEndCoordinates.y - var canvas = Metamaps.Visualize.mGraph.canvas + var canvas = Visualize.mGraph.canvas JIT.centerMap(canvas) var height = $(document).height(), @@ -1778,9 +1803,9 @@ const JIT = { canvas.translate(-1 * cogX, -1 * cogY) $(document).trigger(JIT.events.zoom, [event]) - Metamaps.Mouse.boxStartCoordinates = false - Metamaps.Mouse.boxEndCoordinates = false - Metamaps.Visualize.mGraph.plot() + Mouse.boxStartCoordinates = false + Mouse.boxEndCoordinates = false + Visualize.mGraph.plot() }, zoomExtents: function (event, canvas, denySelected) { JIT.centerMap(canvas) @@ -1788,10 +1813,10 @@ const JIT = { width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 - if (!denySelected && Metamaps.Selected.Nodes.length > 0) { - var nodes = Metamaps.Selected.Nodes + if (!denySelected && Selected.Nodes.length > 0) { + var nodes = Selected.Nodes } else { - var nodes = _.values(Metamaps.Visualize.mGraph.graph.nodes) + var nodes = _.values(Visualize.mGraph.graph.nodes) } if (nodes.length > 1) { @@ -1806,7 +1831,7 @@ const JIT = { minY = y } - var arrayOfLabelLines = Metamaps.Util.splitLine(n.name, 30).split('\n'), + var arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n'), dim = n.getData('dim'), ctx = canvas.getCtx() diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 1c56b679..78e881d4 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,57 +1,59 @@ -/* global Metamaps, $ */ +/* global $ */ + +import Active from './Active' +import Control from './Control' +import JIT from './JIT' +import Mobile from './Mobile' +import Realtime from './Realtime' +import Selected from './Selected' +import Topic from './Topic' +import Visualize from './Visualize' -/* - * Dependencies: - * - Metamaps.Active - * - Metamaps.Control - * - Metamaps.JIT - * - Metamaps.Visualize - */ const Listeners = { init: function () { var self = this $(document).on('keydown', function (e) { - if (!(Metamaps.Active.Map || Metamaps.Active.Topic)) return + if (!(Active.Map || Active.Topic)) return switch (e.which) { case 13: // if enter key is pressed - Metamaps.JIT.enterKeyHandler() + JIT.enterKeyHandler() e.preventDefault() break case 27: // if esc key is pressed - Metamaps.JIT.escKeyHandler() + JIT.escKeyHandler() break case 65: // if a or A is pressed if (e.ctrlKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() e.preventDefault() - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - Metamaps.Control.selectNode(n, e) + Visualize.mGraph.graph.eachNode(function (n) { + Control.selectNode(n, e) }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } break case 68: // if d or D is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.deleteSelected() + Control.deleteSelected() } break case 69: // if e or E is pressed - if (e.ctrlKey && Metamaps.Active.Map) { + if (e.ctrlKey && Active.Map) { e.preventDefault() - Metamaps.JIT.zoomExtents(null, Metamaps.Visualize.mGraph.canvas) + JIT.zoomExtents(null, Visualize.mGraph.canvas) break } - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - if (Metamaps.Active.Topic) { - self.centerAndReveal(Metamaps.Selected.Nodes, { + if (Active.Topic) { + self.centerAndReveal(Selected.Nodes, { center: true, reveal: false }) @@ -62,30 +64,30 @@ const Listeners = { case 72: // if h or H is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.hideSelectedNodes() - Metamaps.Control.hideSelectedEdges() + Control.hideSelectedNodes() + Control.hideSelectedEdges() } break case 77: // if m or M is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.removeSelectedNodes() - Metamaps.Control.removeSelectedEdges() + Control.removeSelectedNodes() + Control.removeSelectedEdges() } break case 82: // if r or R is pressed - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - self.centerAndReveal(Metamaps.Selected.Nodes, { + self.centerAndReveal(Selected.Nodes, { center: false, reveal: true }) } break case 84: // if t or T is pressed - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - self.centerAndReveal(Metamaps.Selected.Nodes, { + self.centerAndReveal(Selected.Nodes, { center: true, reveal: true }) @@ -98,23 +100,22 @@ const Listeners = { }) $(window).resize(function () { - if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) - if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition() - if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos() - Metamaps.Mobile.resizeTitle() + if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() + Mobile.resizeTitle() }) }, centerAndReveal: function(nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Metamaps.Topic.centerOn(node.id, function() { - Metamaps.Topic.fetchRelatives(nodes) + Topic.centerOn(node.id, function() { + Topic.fetchRelatives(nodes) }) } else if (opts.center) { - Metamaps.Topic.centerOn(node.id) + Topic.centerOn(node.id) } else if (opts.reveal) { - Metamaps.Topic.fetchRelatives(nodes) + Topic.fetchRelatives(nodes) } } } diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map.js index 690c2a6d..cd2c3d2e 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map.js @@ -1,30 +1,29 @@ -window.Metamaps = window.Metamaps || {} - /* global Metamaps, $ */ +import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Realtime from './Realtime' +import Selected from './Selected' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' +import Visualize from './Visualize' + /* * Metamaps.Map.js.erb * * Dependencies: - * - Metamaps.AutoLayout - * - Metamaps.Create - * - Metamaps.Erb - * - Metamaps.Filter - * - Metamaps.JIT - * - Metamaps.Loading - * - Metamaps.Maps - * - Metamaps.Realtime - * - Metamaps.Router - * - Metamaps.Selected - * - Metamaps.SynapseCard - * - Metamaps.TopicCard - * - Metamaps.Visualize - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.GlobalUI + * - Metamaps.Erb + * - Metamaps.Loading * - Metamaps.Mappers * - Metamaps.Mappings + * - Metamaps.Maps * - Metamaps.Messages + * - Metamaps.Router * - Metamaps.Synapses * - Metamaps.Topics * @@ -33,6 +32,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Map.InfoBox */ +window.Metamaps = window.Metamaps || {} Metamaps.Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' @@ -54,7 +54,7 @@ Metamaps.Map = { self.fork() }) - Metamaps.GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() + GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() self.updateStar() self.InfoBox.init() @@ -65,7 +65,7 @@ Metamaps.Map = { launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { - Metamaps.Active.Map = new bb.Map(data.map) + Active.Map = new bb.Map(data.map) Metamaps.Mappers = new bb.MapperCollection(data.mappers) Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) Metamaps.Topics = new bb.TopicCollection(data.topics) @@ -75,8 +75,8 @@ Metamaps.Map = { Metamaps.Stars = data.stars Metamaps.Backbone.attachCollectionEvents() - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { @@ -95,24 +95,24 @@ Metamaps.Map = { $('#filter_by_mapper h3').html('MAPPERS') // build and render the visualization - Metamaps.Visualize.type = 'ForceDirected' - Metamaps.JIT.prepareVizData() + Visualize.type = 'ForceDirected' + JIT.prepareVizData() // update filters - Metamaps.Filter.reset() + Filter.reset() // reset selected arrays - Metamaps.Selected.reset() + Selected.reset() // set the proper mapinfobox content Metamaps.Map.InfoBox.load() // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() - Metamaps.Realtime.startActiveMap() + Realtime.startActiveMap() Metamaps.Loading.hide() // for mobile @@ -125,24 +125,24 @@ Metamaps.Map = { }) }, end: function () { - if (Metamaps.Active.Map) { + if (Active.Map) { $('.wrapper').removeClass('canEditMap commonsMap') - Metamaps.AutoLayout.resetSpiral() + AutoLayout.resetSpiral() $('.rightclickmenu').remove() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Create.newTopic.hide(true) // true means force (and override pinned) - Metamaps.Create.newSynapse.hide() - Metamaps.Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide(true) // true means force (and override pinned) + Create.newSynapse.hide() + Filter.close() Metamaps.Map.InfoBox.close() - Metamaps.Realtime.endActiveMap() + Realtime.endActiveMap() } }, updateStar: function () { - if (!Metamaps.Active.Mapper || !Metamaps.Stars) return + if (!Active.Mapper || !Metamaps.Stars) return // update the star/unstar icon - if (Metamaps.Stars.find(function (s) { return s.user_id === Metamaps.Active.Mapper.id })) { + if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { $('.starMap').addClass('starred') $('.starMap .tooltipsAbove').html('Unstar') } else { @@ -153,31 +153,31 @@ Metamaps.Map = { star: function () { var self = Metamaps.Map - if (!Metamaps.Active.Map) return - $.post('/maps/' + Metamaps.Active.Map.id + '/star') - Metamaps.Stars.push({ user_id: Metamaps.Active.Mapper.id, map_id: Metamaps.Active.Map.id }) - Metamaps.Maps.Starred.add(Metamaps.Active.Map) - Metamaps.GlobalUI.notifyUser('Map is now starred') + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/star') + Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) + Metamaps.Maps.Starred.add(Active.Map) + GlobalUI.notifyUser('Map is now starred') self.updateStar() }, unstar: function () { var self = Metamaps.Map - if (!Metamaps.Active.Map) return - $.post('/maps/' + Metamaps.Active.Map.id + '/unstar') - Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Metamaps.Active.Mapper.id }) - Metamaps.Maps.Starred.remove(Metamaps.Active.Map) + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/unstar') + Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + Metamaps.Maps.Starred.remove(Active.Map) self.updateStar() }, fork: function () { - Metamaps.GlobalUI.openLightbox('forkmap') + GlobalUI.openLightbox('forkmap') var nodes_data = '', synapses_data = '' var nodes_array = [] var synapses_array = [] // collect the unfiltered topics - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { // if the opacity is less than 1 then it's filtered if (n.getData('alpha') === 1) { var id = n.getData('topic').id @@ -197,7 +197,7 @@ Metamaps.Map = { Metamaps.Synapses.each(function (synapse) { var desc = synapse.get('desc') - var descNotFiltered = Metamaps.Filter.visible.synapses.indexOf(desc) > -1 + var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 // make sure that both topics are being added, otherwise, it // doesn't make sense to add the synapse var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 @@ -210,32 +210,32 @@ Metamaps.Map = { synapses_data = synapses_array.join() nodes_data = nodes_data.slice(0, -1) - Metamaps.GlobalUI.CreateMap.topicsToMap = nodes_data - Metamaps.GlobalUI.CreateMap.synapsesToMap = synapses_data + GlobalUI.CreateMap.topicsToMap = nodes_data + GlobalUI.CreateMap.synapsesToMap = synapses_data }, leavePrivateMap: function () { - var map = Metamaps.Active.Map + var map = Active.Map Metamaps.Maps.Active.remove(map) Metamaps.Maps.Featured.remove(map) Metamaps.Router.home() - Metamaps.GlobalUI.notifyUser('Sorry! That map has been changed to Private.') + GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, cantEditNow: function () { - Metamaps.Realtime.turnOff(true); // true is for 'silence' - Metamaps.GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') - Metamaps.Active.Map.trigger('changeByOther') + Realtime.turnOff(true); // true is for 'silence' + GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') + Active.Map.trigger('changeByOther') }, canEditNow: function () { var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' var c = confirm(confirmString) if (c) { - Metamaps.Router.maps(Metamaps.Active.Map.id) + Metamaps.Router.maps(Active.Map.id) } }, editedByActiveMapper: function () { - if (Metamaps.Active.Mapper) { - Metamaps.Mappers.add(Metamaps.Active.Mapper) + if (Active.Mapper) { + Metamaps.Mappers.add(Active.Mapper) } }, exportImage: function () { @@ -282,14 +282,14 @@ Metamaps.Map = { // center it canvas.getCtx().translate(1880 / 2, 1260 / 2) - var mGraph = Metamaps.Visualize.mGraph + var mGraph = Visualize.mGraph var id = mGraph.root var root = mGraph.graph.getNode(id) var T = !!root.visited // pass true to avoid basing it on a selection - Metamaps.JIT.zoomExtents(null, canvas, true) + JIT.zoomExtents(null, canvas, true) var c = canvas.canvas, ctx = canvas.getCtx(), @@ -327,7 +327,7 @@ Metamaps.Map = { encoded_image: canvas.canvas.toDataURL() } - var map = Metamaps.Active.Map + var map = Active.Map var today = new Date() var dd = today.getDate() @@ -346,12 +346,12 @@ Metamaps.Map = { downloadMessage += 'Captured map screenshot! ' downloadMessage += "<a href='" + imageData.encoded_image + "' " downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" - Metamaps.GlobalUI.notifyUser(downloadMessage) + GlobalUI.notifyUser(downloadMessage) $.ajax({ type: 'POST', dataType: 'json', - url: '/maps/' + Metamaps.Active.Map.id + '/upload_screenshot', + url: '/maps/' + Active.Map.id + '/upload_screenshot', data: imageData, success: function (data) { console.log('successfully uploaded map screenshot') @@ -461,12 +461,12 @@ Metamaps.Map.InfoBox = { load: function () { var self = Metamaps.Map.InfoBox - var map = Metamaps.Active.Map + var map = Active.Map var obj = map.pick('permission', 'topic_count', 'synapse_count') - var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) - var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) + var isCreator = map.authorizePermissionChange(Active.Mapper) + var canEdit = map.authorizeToEdit(Active.Mapper) var relevantPeople = map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var shareable = map.get('permission') !== 'private' @@ -519,8 +519,8 @@ Metamaps.Map.InfoBox = { $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function () { var name = $(this).html() - Metamaps.Active.Map.set('name', name) - Metamaps.Active.Map.trigger('saved') + Active.Map.set('name', name) + Active.Map.trigger('saved') // mobile menu $('#header_content').html(name) $('.mapInfoBox').removeClass('mapRequestTitle') @@ -529,8 +529,8 @@ Metamaps.Map.InfoBox = { $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { var desc = $(this).html() - Metamaps.Active.Map.set('desc', desc) - Metamaps.Active.Map.trigger('saved') + Active.Map.set('desc', desc) + Active.Map.trigger('saved') }) $('.yourMap .mapPermission').unbind().click(self.onPermissionClick) @@ -558,7 +558,7 @@ Metamaps.Map.InfoBox = { addTypeahead: function () { var self = Metamaps.Map.InfoBox - if (!Metamaps.Active.Map) return + if (!Active.Map) return // for autocomplete var collaborators = { @@ -589,7 +589,7 @@ Metamaps.Map.InfoBox = { } // for adding map collaborators, who will have edit rights - if (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) { + if (Active.Mapper && Active.Mapper.id === Active.Map.get('user_id')) { $('.collaboratorSearchField').typeahead( { highlight: false, @@ -606,23 +606,23 @@ Metamaps.Map.InfoBox = { var self = Metamaps.Map.InfoBox Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) - $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, addCollaborator: function (newCollaboratorId) { var self = Metamaps.Map.InfoBox if (Metamaps.Collaborators.get(newCollaboratorId)) { - Metamaps.GlobalUI.notifyUser('That user already has access') + GlobalUI.notifyUser('That user already has access') return } function callback(mapper) { Metamaps.Collaborators.add(mapper) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) - $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') - Metamaps.GlobalUI.notifyUser(name + ' will be notified by email') + GlobalUI.notifyUser(name + ' will be notified by email') self.updateNumbers() } @@ -642,13 +642,13 @@ Metamaps.Map.InfoBox = { }, createContributorList: function () { var self = Metamaps.Map.InfoBox - var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators - var activeMapperIsCreator = Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id') + var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' string += '<ul>' relevantPeople.each(function (m) { - var isCreator = Metamaps.Active.Map.get('user_id') === m.get('id') + var isCreator = Active.Map.get('user_id') === m.get('id') string += '<li><a href="/explore/mapper/' + m.get('id') + '">' + '<img class="rtUserImage" width="25" height="25" src="' + m.get('image') + '" />' + m.get('name') if (isCreator) string += ' (creator)' string += '</a>' @@ -664,11 +664,11 @@ Metamaps.Map.InfoBox = { return string }, updateNumbers: function () { - if (!Metamaps.Active.Map) return + if (!Active.Map) return var self = Metamaps.Map.InfoBox - var mapper = Metamaps.Active.Mapper - var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var mapper = Active.Mapper + var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var contributors_class = '' if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' @@ -720,10 +720,10 @@ Metamaps.Map.InfoBox = { self.selectingPermission = false var permission = $(this).attr('class') - Metamaps.Active.Map.save({ + Active.Map.save({ permission: permission }) - Metamaps.Active.Map.updateMapWrapper() + Active.Map.updateMapWrapper() shareable = permission === 'private' ? '' : 'shareable' $('.mapPermission').removeClass('commons public private minimize').addClass(permission) $('.mapPermission .permissionSelect').remove() @@ -735,8 +735,8 @@ Metamaps.Map.InfoBox = { confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' var doIt = confirm(confirmString) - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper var authorized = map.authorizePermissionChange(mapper) if (doIt && authorized) { @@ -747,7 +747,7 @@ Metamaps.Map.InfoBox = { Metamaps.Maps.Shared.remove(map) map.destroy() Metamaps.Router.home() - Metamaps.GlobalUI.notifyUser('Map eliminated!') + GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index ac93c34d..3858101d 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,4 +1,6 @@ -import Backbone from './Backbone' +/* + * Metamaps.Backbone + */ const Mapper = { // this function is to retrieve a mapper JSON object from the database @@ -9,7 +11,7 @@ const Mapper = { if (!response.ok) throw response return response.json() }).then(payload => { - callback(new Backbone.Mapper(payload)) + callback(new Metamaps.Backbone.Mapper(payload)) }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index ebe1d944..e0620329 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -2,6 +2,8 @@ import AutoLayout from './AutoLayout' import Import from './Import' +import TopicCard from './TopicCard' +import Util from './Util' const PasteInput = { // thanks to https://github.com/kevva/url-regex @@ -19,7 +21,7 @@ const PasteInput = { window.addEventListener("drop", function(e) { e = e || event; e.preventDefault(); - var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) + var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { var fileReader = new FileReader() var text = fileReader.readAsText(e.dataTransfer.files[0]) @@ -86,7 +88,7 @@ const PasteInput = { import_id, { success: function(topic) { - Metamaps.TopicCard.showCard(topic.get('node'), function() { + TopicCard.showCard(topic.get('node'), function() { $('#showcard #titleActivator').click() .find('textarea, input').focus() }) diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 35d00f06..1eef6408 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,27 +1,27 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Map from './Map' +import Mapper from './Mapper' +import Topic from './Topic' +import Util from './Util' +import Views from './Views' +import Visualize from './Visualize' + /* * Metamaps.Realtime.js * * Dependencies: - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Backbone - * - Metamaps.Control * - Metamaps.Erb - * - Metamaps.GlobalUI - * - Metamaps.JIT - * - Metamaps.Map - * - Metamaps.Mapper * - Metamaps.Mappers * - Metamaps.Mappings * - Metamaps.Messages * - Metamaps.Synapses - * - Metamaps.Topic * - Metamaps.Topics - * - Metamaps.Util - * - Metamaps.Views - * - Metamaps.Visualize */ const Realtime = { @@ -52,7 +52,7 @@ const Realtime = { self.disconnected = true }) - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { self.webrtc = new SimpleWebRTC({ connection: self.socket, localVideoEl: self.videoId, @@ -69,23 +69,23 @@ const Realtime = { video: true, audio: true }, - nick: Metamaps.Active.Mapper.id + nick: Active.Mapper.id }) var $video = $('<video></video>').attr('id', self.videoId) self.localVideo = { $video: $video, - view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { + view: new Views.videoView($video[0], $('body'), 'me', true, { DOUBLE_CLICK_TOLERANCE: 200, - avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' + avatar: Active.Mapper ? Active.Mapper.get('image') : '' }) } - self.room = new Metamaps.Views.room({ + self.room = new Views.room({ webrtc: self.webrtc, socket: self.socket, - username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', - image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', + username: Active.Mapper ? Active.Mapper.get('name') : '', + image: Active.Mapper ? Active.Mapper.get('image') : '', room: 'global', $video: self.localVideo.$video, myVideoView: self.localVideo.view, @@ -93,35 +93,35 @@ const Realtime = { }) self.room.videoAdded(self.handleVideoAdded) - if (!Metamaps.Active.Map) { + if (!Active.Map) { self.room.chat.$container.hide() } $('body').prepend(self.room.chat.$container) - } // if Metamaps.Active.Mapper + } // if Active.Mapper }, addJuntoListeners: function () { var self = Realtime - $(document).on(Metamaps.Views.chatView.events.openTray, function () { + $(document).on(Views.chatView.events.openTray, function () { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Metamaps.Views.chatView.events.closeTray, function () { + $(document).on(Views.chatView.events.closeTray, function () { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Metamaps.Views.chatView.events.videosOn, function () { + $(document).on(Views.chatView.events.videosOn, function () { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Metamaps.Views.chatView.events.videosOff, function () { + $(document).on(Views.chatView.events.videosOff, function () { $('#wrapper').addClass('hideVideos') }) - $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { + $(document).on(Views.chatView.events.cursorsOn, function () { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { + $(document).on(Views.chatView.events.cursorsOff, function () { $('#wrapper').addClass('hideCursors') }) }, @@ -187,8 +187,8 @@ const Realtime = { startActiveMap: function () { var self = Realtime - if (Metamaps.Active.Map && Metamaps.Active.Mapper) { - if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { + if (Active.Map && Active.Mapper) { + if (Active.Map.authorizeToEdit(Active.Mapper)) { self.turnOn() self.setupSocket() } else { @@ -219,15 +219,15 @@ const Realtime = { self.status = true $('.collabCompass').show() self.room.chat.$container.show() - self.room.room = 'map-' + Metamaps.Active.Map.id + self.room.room = 'map-' + Active.Map.id self.checkForACallToJoin() self.activeMapper = { - id: Metamaps.Active.Mapper.id, - name: Metamaps.Active.Mapper.get('name'), - username: Metamaps.Active.Mapper.get('name'), - image: Metamaps.Active.Mapper.get('image'), - color: Metamaps.Util.getPastelColor(), + id: Active.Mapper.id, + name: Active.Mapper.get('name'), + username: Active.Mapper.get('name'), + image: Active.Mapper.get('image'), + color: Util.getPastelColor(), self: true } self.localVideo.view.$container.find('.video-cutoff').css({ @@ -237,7 +237,7 @@ const Realtime = { }, checkForACallToJoin: function () { var self = Realtime - self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) + self.socket.emit('checkForCall', { room: self.room.room, mapid: Active.Map.id }) }, promptToJoin: function () { var self = Realtime @@ -245,7 +245,7 @@ const Realtime = { var notifyText = "There's a conversation happening, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) self.room.conversationInProgress() }, conversationHasBegun: function () { @@ -255,7 +255,7 @@ const Realtime = { var notifyText = "There's a conversation starting, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) self.room.conversationInProgress() }, countOthersInConversation: function () { @@ -275,7 +275,7 @@ const Realtime = { if (self.inConversation) { var username = mapper.name var notifyText = username + ' joined the call' - Metamaps.GlobalUI.notifyUser(notifyText) + GlobalUI.notifyUser(notifyText) } mapper.inConversation = true @@ -290,7 +290,7 @@ const Realtime = { if (self.inConversation) { var username = mapper.name var notifyText = username + ' left the call' - Metamaps.GlobalUI.notifyUser(notifyText) + GlobalUI.notifyUser(notifyText) } mapper.inConversation = false @@ -332,7 +332,7 @@ const Realtime = { notifyText += username + ' is inviting you to a conversation. Join live?' notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.acceptCall(' + inviter + ')">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyCall(' + inviter + ')">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) }, invitedToJoin: function (inviter) { var self = Realtime @@ -344,55 +344,55 @@ const Realtime = { var notifyText = username + ' is inviting you to the conversation. Join?' notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyInvite(' + inviter + ')">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) }, acceptCall: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callAccepted', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation') + $.post('/maps/' + Active.Map.id + '/events/conversation') self.joinCall() - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, denyCall: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, denyInvite: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('inviteDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, inviteACall: function (userid) { var self = Realtime self.socket.emit('inviteACall', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + inviter: Active.Mapper.id, invited: userid }) self.room.chat.invitationPending(userid) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, inviteToJoin: function (userid) { var self = Realtime self.socket.emit('inviteToJoin', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + inviter: Active.Mapper.id, invited: userid }) self.room.chat.invitationPending(userid) @@ -401,7 +401,7 @@ const Realtime = { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser('Conversation starting...') + GlobalUI.notifyUser('Conversation starting...') self.joinCall() self.room.chat.invitationAnswered(userid) }, @@ -409,14 +409,14 @@ const Realtime = { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, inviteDenied: function (userid) { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, joinCall: function () { @@ -436,22 +436,22 @@ const Realtime = { }) self.inConversation = true self.socket.emit('mapperJoinedCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id + mapid: Active.Map.id, + id: Active.Mapper.id }) self.webrtc.startLocalVideo() - Metamaps.GlobalUI.clearNotify() - self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) + GlobalUI.clearNotify() + self.room.chat.mapperJoinedCall(Active.Mapper.id) }, leaveCall: function () { var self = Realtime self.socket.emit('mapperLeftCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id + mapid: Active.Map.id, + id: Active.Mapper.id }) - self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id) + self.room.chat.mapperLeftCall(Active.Mapper.id) self.room.leaveVideoOnly() self.inConversation = false self.localVideo.view.$container.hide() @@ -479,63 +479,63 @@ const Realtime = { setupSocket: function () { var self = Realtime var socket = Realtime.socket - var myId = Metamaps.Active.Mapper.id + var myId = Active.Mapper.id socket.emit('newMapperNotify', { userid: myId, - username: Metamaps.Active.Mapper.get('name'), - userimage: Metamaps.Active.Mapper.get('image'), - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userimage: Active.Mapper.get('image'), + mapid: Active.Map.id }) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall) // new call - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied) + socket.on(myId + '-' + Active.Map.id + '-invitedToCall', self.invitedToCall) // new call + socket.on(myId + '-' + Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress + socket.on(myId + '-' + Active.Map.id + '-callAccepted', self.callAccepted) + socket.on(myId + '-' + Active.Map.id + '-callDenied', self.callDenied) + socket.on(myId + '-' + Active.Map.id + '-inviteDenied', self.inviteDenied) // receive word that there's a conversation in progress - socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin) - socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun) + socket.on('maps-' + Active.Map.id + '-callInProgress', self.promptToJoin) + socket.on('maps-' + Active.Map.id + '-callStarting', self.conversationHasBegun) - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) + socket.on('maps-' + Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) + socket.on('maps-' + Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) // if you're the 'new guy' update your list with who's already online - socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList) + socket.on(myId + '-' + Active.Map.id + '-UpdateMapperList', self.updateMapperList) // receive word that there's a new mapper on the map - socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap) + socket.on('maps-' + Active.Map.id + '-newmapper', self.newPeerOnMap) // receive word that a mapper left the map - socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap) + socket.on('maps-' + Active.Map.id + '-lostmapper', self.lostPeerOnMap) // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator) + socket.on('maps-' + Active.Map.id + '-newrealtime', self.newCollaborator) // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator) + socket.on('maps-' + Active.Map.id + '-lostrealtime', self.lostCollaborator) // - socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag) + socket.on('maps-' + Active.Map.id + '-topicDrag', self.topicDrag) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic) + socket.on('maps-' + Active.Map.id + '-newTopic', self.newTopic) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage) + socket.on('maps-' + Active.Map.id + '-newMessage', self.newMessage) // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic) + socket.on('maps-' + Active.Map.id + '-removeTopic', self.removeTopic) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse) + socket.on('maps-' + Active.Map.id + '-newSynapse', self.newSynapse) // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse) + socket.on('maps-' + Active.Map.id + '-removeSynapse', self.removeSynapse) // update mapper compass position - socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) + socket.on('maps-' + Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) // deletions socket.on('deleteTopicFromServer', self.removeTopic) @@ -551,7 +551,7 @@ const Realtime = { x: event.pageX, y: event.pageY } - var coords = Metamaps.Util.pixelsToCoords(pixels) + var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } $(document).on('mousemove.map', sendCoords) @@ -562,54 +562,54 @@ const Realtime = { x: e.pageX, y: e.pageY } - var coords = Metamaps.Util.pixelsToCoords(pixels) + var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } self.positionPeerIcons() } - $(document).on(Metamaps.JIT.events.zoom + '.map', zoom) + $(document).on(JIT.events.zoom + '.map', zoom) - $(document).on(Metamaps.JIT.events.pan + '.map', self.positionPeerIcons) + $(document).on(JIT.events.pan + '.map', self.positionPeerIcons) var sendTopicDrag = function (event, positions) { self.sendTopicDrag(positions) } - $(document).on(Metamaps.JIT.events.topicDrag + '.map', sendTopicDrag) + $(document).on(JIT.events.topicDrag + '.map', sendTopicDrag) var sendNewTopic = function (event, data) { self.sendNewTopic(data) } - $(document).on(Metamaps.JIT.events.newTopic + '.map', sendNewTopic) + $(document).on(JIT.events.newTopic + '.map', sendNewTopic) var sendDeleteTopic = function (event, data) { self.sendDeleteTopic(data) } - $(document).on(Metamaps.JIT.events.deleteTopic + '.map', sendDeleteTopic) + $(document).on(JIT.events.deleteTopic + '.map', sendDeleteTopic) var sendRemoveTopic = function (event, data) { self.sendRemoveTopic(data) } - $(document).on(Metamaps.JIT.events.removeTopic + '.map', sendRemoveTopic) + $(document).on(JIT.events.removeTopic + '.map', sendRemoveTopic) var sendNewSynapse = function (event, data) { self.sendNewSynapse(data) } - $(document).on(Metamaps.JIT.events.newSynapse + '.map', sendNewSynapse) + $(document).on(JIT.events.newSynapse + '.map', sendNewSynapse) var sendDeleteSynapse = function (event, data) { self.sendDeleteSynapse(data) } - $(document).on(Metamaps.JIT.events.deleteSynapse + '.map', sendDeleteSynapse) + $(document).on(JIT.events.deleteSynapse + '.map', sendDeleteSynapse) var sendRemoveSynapse = function (event, data) { self.sendRemoveSynapse(data) } - $(document).on(Metamaps.JIT.events.removeSynapse + '.map', sendRemoveSynapse) + $(document).on(JIT.events.removeSynapse + '.map', sendRemoveSynapse) var sendNewMessage = function (event, data) { self.sendNewMessage(data) } - $(document).on(Metamaps.Views.room.events.newMessage + '.map', sendNewMessage) + $(document).on(Views.room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { var self = Realtime @@ -623,9 +623,9 @@ const Realtime = { // send this new mapper back your details, and the awareness that you're online var update = { - username: Metamaps.Active.Mapper.get('name'), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('notifyStartRealtime', update) }, @@ -635,9 +635,9 @@ const Realtime = { // send this new mapper back your details, and the awareness that you're online var update = { - username: Metamaps.Active.Mapper.get('name'), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('notifyStopRealtime', update) }, @@ -655,7 +655,7 @@ const Realtime = { name: data.username, username: data.username, image: data.userimage, - color: Metamaps.Util.getPastelColor(), + color: Util.getPastelColor(), realtime: data.userrealtime, inConversation: data.userinconversation, coords: { @@ -664,7 +664,7 @@ const Realtime = { } } - if (data.userid !== Metamaps.Active.Mapper.id) { + if (data.userid !== Active.Mapper.id) { self.room.chat.addParticipant(self.mappersOnMap[data.userid]) if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) @@ -687,7 +687,7 @@ const Realtime = { name: data.username, username: data.username, image: data.userimage, - color: Metamaps.Util.getPastelColor(), + color: Util.getPastelColor(), realtime: true, coords: { x: 0, @@ -696,7 +696,7 @@ const Realtime = { } // create an item for them in the realtime box - if (data.userid !== Metamaps.Active.Mapper.id && self.status) { + if (data.userid !== Active.Mapper.id && self.status) { self.room.chat.sound.play('joinmap') self.room.chat.addParticipant(self.mappersOnMap[data.userid]) @@ -707,17 +707,17 @@ const Realtime = { if (firstOtherPerson) { notifyMessage += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.inviteACall(' + data.userid + ')">Suggest A Video Call</button>' } - Metamaps.GlobalUI.notifyUser(notifyMessage) + GlobalUI.notifyUser(notifyMessage) // send this new mapper back your details, and the awareness that you've loaded the map var update = { userToNotify: data.userid, - username: Metamaps.Active.Mapper.get('name'), - userimage: Metamaps.Active.Mapper.get('image'), - userid: Metamaps.Active.Mapper.id, + username: Active.Mapper.get('name'), + userimage: Active.Mapper.get('image'), + userid: Active.Mapper.id, userrealtime: self.status, userinconversation: self.inConversation, - mapid: Metamaps.Active.Map.id + mapid: Active.Map.id } socket.emit('updateNewMapperList', update) } @@ -753,7 +753,7 @@ const Realtime = { $('#compass' + data.userid).remove() self.room.chat.removeParticipant(data.username) - Metamaps.GlobalUI.notifyUser(data.username + ' just left the map') + GlobalUI.notifyUser(data.username + ' just left the map') if ((self.inConversation && self.countOthersInConversation() === 0) || (!self.inConversation && self.countOthersInConversation() === 1)) { @@ -772,7 +772,7 @@ const Realtime = { // $('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn') $('#compass' + data.userid).show() - Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') + GlobalUI.notifyUser(data.username + ' just turned on realtime') }, lostCollaborator: function (data) { var self = Realtime @@ -786,7 +786,7 @@ const Realtime = { // $('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff') $('#compass' + data.userid).hide() - Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') + GlobalUI.notifyUser(data.username + ' just turned off realtime') }, updatePeerCoords: function (data) { var self = Realtime @@ -819,7 +819,7 @@ const Realtime = { var compassDiameter = 56 var compassArrowSize = 24 - var origPixels = Metamaps.Util.coordsToPixels(mapper.coords) + var origPixels = Util.coordsToPixels(mapper.coords) var pixels = self.limitPixelsToScreen(origPixels) $('#compass' + id).css({ left: pixels.x + 'px', @@ -867,14 +867,14 @@ const Realtime = { var self = Realtime var socket = Realtime.socket - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper if (self.status && map.authorizeToEdit(mapper) && socket) { var update = { usercoords: coords, - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('updateMapperCoords', update) } @@ -883,8 +883,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map && self.status) { - positions.mapid = Metamaps.Active.Map.id + if (Active.Map && self.status) { + positions.mapid = Active.Map.id socket.emit('topicDrag', positions) } }, @@ -895,13 +895,13 @@ const Realtime = { var topic var node - if (Metamaps.Active.Map && self.status) { + if (Active.Map && self.status) { for (var key in positions) { topic = Metamaps.Topics.get(key) if (topic) node = topic.get('node') if (node) node.pos.setc(positions[key].x, positions[key].y) } // for - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } }, sendTopicChange: function (topic) { @@ -960,23 +960,23 @@ const Realtime = { socket.emit('mapChangeFromClient', data) }, mapChange: function (data) { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = map && data.mapId === map.id if (isActiveMap) { - var couldEditBefore = map.authorizeToEdit(Metamaps.Active.Mapper) + var couldEditBefore = map.authorizeToEdit(Active.Mapper) var idBefore = map.id map.fetch({ success: function (model, response) { var idNow = model.id - var canEditNow = model.authorizeToEdit(Metamaps.Active.Mapper) + var canEditNow = model.authorizeToEdit(Active.Mapper) if (idNow !== idBefore) { - Metamaps.Map.leavePrivateMap() // this means the map has been changed to private + Map.leavePrivateMap() // this means the map has been changed to private } else if (couldEditBefore && !canEditNow) { - Metamaps.Map.cantEditNow() + Map.cantEditNow() } else if (!couldEditBefore && canEditNow) { - Metamaps.Map.canEditNow() + Map.canEditNow() } else { model.fetchContained() model.trigger('changeByOther') @@ -991,7 +991,7 @@ const Realtime = { var socket = self.socket var message = data.attributes - message.mapid = Metamaps.Active.Map.id + message.mapid = Active.Map.id socket.emit('newMessage', message) }, newMessage: function (data) { @@ -1005,14 +1005,14 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map && self.status) { - data.mapperid = Metamaps.Active.Mapper.id - data.mapid = Metamaps.Active.Map.id + if (Active.Map && self.status) { + data.mapperid = Active.Mapper.id + data.mapid = Active.Map.id socket.emit('newTopic', data) } }, newTopic: function (data) { - var topic, mapping, mapper, mapperCallback, cancel + var topic, mapping, mapper, cancel var self = Realtime var socket = self.socket @@ -1021,7 +1021,7 @@ const Realtime = { function waitThenRenderTopic () { if (topic && mapping && mapper) { - Metamaps.Topic.renderTopic(mapping, topic, false, false) + Topic.renderTopic(mapping, topic, false, false) } else if (!cancel) { setTimeout(waitThenRenderTopic, 10) @@ -1030,11 +1030,10 @@ const Realtime = { mapper = Metamaps.Mappers.get(data.mapperid) if (mapper === undefined) { - mapperCallback = function (m) { + Mapper.get(data.mapperid, function(m) { Metamaps.Mappers.add(m) mapper = m - } - Metamaps.Mapper.get(data.mapperid, mapperCallback) + }) } $.ajax({ url: '/topics/' + data.mappableid + '.json', @@ -1064,7 +1063,7 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { + if (Active.Map) { socket.emit('deleteTopicFromClient', data) } }, @@ -1073,8 +1072,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('removeTopic', data) } }, @@ -1088,7 +1087,7 @@ const Realtime = { if (topic) { var node = topic.get('node') var mapping = topic.getMapping() - Metamaps.Control.hideNode(node.id) + Control.hideNode(node.id) Metamaps.Topics.remove(topic) Metamaps.Mappings.remove(mapping) } @@ -1098,9 +1097,9 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapperid = Metamaps.Active.Mapper.id - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapperid = Active.Mapper.id + data.mapid = Active.Map.id socket.emit('newSynapse', data) } }, @@ -1119,7 +1118,7 @@ const Realtime = { topic2 = synapse.getTopic2() node2 = topic2.get('node') - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false) + Synapse.renderSynapse(mapping, synapse, node1, node2, false) } else if (!cancel) { setTimeout(waitThenRenderSynapse, 10) @@ -1128,11 +1127,10 @@ const Realtime = { mapper = Metamaps.Mappers.get(data.mapperid) if (mapper === undefined) { - mapperCallback = function (m) { + Mapper.get(data.mapperid, function(m) { Metamaps.Mappers.add(m) mapper = m - } - Metamaps.Mapper.get(data.mapperid, mapperCallback) + }) } $.ajax({ url: '/synapses/' + data.mappableid + '.json', @@ -1161,8 +1159,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('deleteSynapseFromClient', data) } }, @@ -1171,8 +1169,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('removeSynapse', data) } }, @@ -1187,7 +1185,7 @@ const Realtime = { var edge = synapse.get('edge') var mapping = synapse.getMapping() if (edge.getData('mappings').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var index = _.indexOf(edge.getData('synapses'), synapse) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 8aacadd1..d5c07e12 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,19 +1,19 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, $ */ +import Active from './Active' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Map from './Map' +import Topic from './Topic' +import Views from './Views' +import Visualize from './Visualize' + /* * Metamaps.Router.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Loading - * - Metamaps.Map * - Metamaps.Maps - * - Metamaps.Topic - * - Metamaps.Views - * - Metamaps.Visualize */ const _Router = Backbone.Router.extend({ @@ -24,53 +24,53 @@ const _Router = Backbone.Router.extend({ 'maps/:id': 'maps' // #maps/7 }, home: function () { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) - if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' + if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps' else document.title = 'Home | Metamaps' - Metamaps.Router.currentSection = '' - Metamaps.Router.currentPage = '' + this.currentSection = '' + this.currentPage = '' $('.wrapper').removeClass('mapPage topicPage') - var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' + var classes = Active.Mapper ? 'homePage explorePage' : 'homePage' $('.wrapper').addClass(classes) var navigate = function () { - Metamaps.Router.timeoutId = setTimeout(function () { - Metamaps.Router.navigate('') + this.timeoutId = setTimeout(function () { + this.navigate('') }, 300) } // all this only for the logged in home page - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { $('.homeButton a').attr('href', '/') - Metamaps.GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.showDiv('#explore') + GlobalUI.showDiv('#explore') - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.exploreMaps.setCollection(Metamaps.Maps.Active) if (Metamaps.Maps.Active.length === 0) { Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Metamaps.Views.exploreMaps.render(navigate) + Views.exploreMaps.render(navigate) } } else { // logged out home page - Metamaps.GlobalUI.hideDiv('#explore') - Metamaps.GlobalUI.showDiv('#yield') - Metamaps.Router.timeoutId = setTimeout(navigate, 500) + GlobalUI.hideDiv('#explore') + GlobalUI.showDiv('#yield') + this.timeoutId = setTimeout(navigate, 500) } - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null + GlobalUI.hideDiv('#infovis') + GlobalUI.hideDiv('#instructions') + Map.end() + Topic.end() + Active.Map = null + Active.Topic = null }, explore: function (section, id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) // just capitalize the variable section // either 'featured', 'mapper', or 'active' @@ -90,12 +90,12 @@ const _Router = Backbone.Router.extend({ document.title = 'Explore My Maps | Metamaps' } - if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) + if (Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) $('.wrapper').removeClass('homePage mapPage topicPage') $('.wrapper').addClass('explorePage') - Metamaps.Router.currentSection = 'explore' - Metamaps.Router.currentPage = section + this.currentSection = 'explore' + this.currentPage = section // this will mean it's a mapper page being loaded if (id) { @@ -108,20 +108,20 @@ const _Router = Backbone.Router.extend({ Metamaps.Maps.Mapper.mapperId = id } - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { - var path = '/explore/' + Metamaps.Router.currentPage + var path = '/explore/' + this.currentPage // alter url if for mapper profile page - if (Metamaps.Router.currentPage === 'mapper') { + if (this.currentPage === 'mapper') { path += '/' + Metamaps.Maps.Mapper.mapperId } - Metamaps.Router.navigate(path) + this.navigate(path) } var navigateTimeout = function () { - Metamaps.Router.timeoutId = setTimeout(navigate, 300) + this.timeoutId = setTimeout(navigate, 300) } if (Metamaps.Maps[capitalize].length === 0) { Metamaps.Loading.show() @@ -130,77 +130,77 @@ const _Router = Backbone.Router.extend({ }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { if (id) { - Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) + Views.exploreMaps.fetchUserThenRender(navigateTimeout) } else { - Metamaps.Views.exploreMaps.render(navigateTimeout) + Views.exploreMaps.render(navigateTimeout) } } - Metamaps.GlobalUI.showDiv('#explore') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null + GlobalUI.showDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#infovis') + GlobalUI.hideDiv('#instructions') + Map.end() + Topic.end() + Active.Map = null + Active.Topic = null }, maps: function (id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) document.title = 'Map ' + id + ' | Metamaps' - Metamaps.Router.currentSection = 'map' - Metamaps.Router.currentPage = id + this.currentSection = 'map' + this.currentPage = id $('.wrapper').removeClass('homePage explorePage topicPage') $('.wrapper').addClass('mapPage') // another class will be added to wrapper if you // can edit this map '.canEditMap' - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#explore') // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + if (Visualize.mGraph) { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Topic.end() - Metamaps.Active.Topic = null + GlobalUI.showDiv('#infovis') + Topic.end() + Active.Topic = null Metamaps.Loading.show() - Metamaps.Map.end() - Metamaps.Map.launch(id) + Map.end() + Map.launch(id) }, topics: function (id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) document.title = 'Topic ' + id + ' | Metamaps' - Metamaps.Router.currentSection = 'topic' - Metamaps.Router.currentPage = id + this.currentSection = 'topic' + this.currentPage = id $('.wrapper').removeClass('homePage explorePage mapPage') $('.wrapper').addClass('topicPage') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#explore') // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + if (Visualize.mGraph) { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Map.end() - Metamaps.Active.Map = null + GlobalUI.showDiv('#infovis') + Map.end() + Active.Map = null - Metamaps.Topic.end() - Metamaps.Topic.launch(id) + Topic.end() + Topic.launch(id) } }) @@ -227,9 +227,9 @@ Router.intercept = function (evt) { segments.splice(0, 1) // pop off the element created by the first / if (href.attr === '') { - Metamaps.Router.home() + Router.home() } else { - Metamaps.Router[segments[0]](segments[1], segments[2]) + Router[segments[0]](segments[1], segments[2]) } } } @@ -240,7 +240,7 @@ Router.init = function () { pushState: true, root: '/' }) - $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) + $(document).on('click', 'a[data-router="true"]', Router.intercept) } export default Router diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 5258de3b..b50e50e6 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -1,20 +1,22 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Create from './Create' +import JIT from './JIT' +import Map from './Map' +import Selected from './Selected' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Synapse.js.erb * * Dependencies: * - Metamaps.Backbone - * - Metamaps.Control - * - Metamaps.Create - * - Metamaps.JIT - * - Metamaps.Map * - Metamaps.Mappings - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Synapse = { @@ -52,18 +54,18 @@ const Synapse = { * */ renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { - var self = Metamaps.Synapse + var self = Synapse var edgeOnViz var newedge = synapse.createEdge(mapping) - Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data) - edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id) + Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data) + edgeOnViz = Visualize.mGraph.graph.getAdjacence(node1.id, node2.id) synapse.set('edge', edgeOnViz) synapse.updateEdge() // links the synapse and the mapping to the edge - Metamaps.Control.selectEdge(edgeOnViz) + Control.selectEdge(edgeOnViz) var mappingSuccessCallback = function (mappingModel, response) { var newSynapseData = { @@ -71,17 +73,17 @@ const Synapse = { mappableid: mappingModel.get('mappable_id') } - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + $(document).trigger(JIT.events.newSynapse, [newSynapseData]) } var synapseSuccessCallback = function (synapseModel, response) { - if (Metamaps.Active.Map) { + if (Active.Map) { mapping.save({ mappable_id: synapseModel.id }, { success: mappingSuccessCallback }) } } - if (!Metamaps.Settings.sandbox && createNewInDB) { + if (!Settings.sandbox && createNewInDB) { if (synapse.isNew()) { synapse.save(null, { success: synapseSuccessCallback, @@ -89,7 +91,7 @@ const Synapse = { console.log('error saving synapse to database') } }) - } else if (!synapse.isNew() && Metamaps.Active.Map) { + } else if (!synapse.isNew() && Active.Map) { mapping.save(null, { success: mappingSuccessCallback }) @@ -105,27 +107,27 @@ const Synapse = { synapse, mapping - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) // for each node in this array we will create a synapse going to the position2 node. var synapsesToCreate = [] - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') - var len = Metamaps.Selected.Nodes.length + var len = Selected.Nodes.length if (len == 0) { - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) synapsesToCreate[0] = topic1.get('node') } else if (len > 0) { - synapsesToCreate = Metamaps.Selected.Nodes + synapsesToCreate = Selected.Nodes } for (var i = 0; i < synapsesToCreate.length; i++) { node1 = synapsesToCreate[i] topic1 = node1.getData('topic') synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description, + desc: Create.newSynapse.description, node1_id: topic1.isNew() ? topic1.cid : topic1.id, node2_id: topic2.isNew() ? topic2.cid : topic2.id, }) @@ -141,7 +143,7 @@ const Synapse = { self.renderSynapse(mapping, synapse, node1, node2, true) } // for each in synapsesToCreate - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() }, getSynapseFromAutocomplete: function (id) { var self = Synapse, @@ -158,11 +160,11 @@ const Synapse = { }) Metamaps.Mappings.add(mapping) - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) node1 = topic1.get('node') - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() self.renderSynapse(mapping, synapse, node1, node2, true) } diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index e0315486..28ff1e32 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import Active from './Active' import Control from './Control' import Mapper from './Mapper' diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index ab93e419..412f7ef2 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,26 +1,29 @@ /* global Metamaps, $ */ import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' import JIT from './JIT' +import Map from './Map' +import Router from './Router' import Selected from './Selected' import Settings from './Settings' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' import Util from './Util' +import Visualize from './Visualize' /* * Metamaps.Topic.js.erb * * Dependencies: * - Metamaps.Backbone - * - Metamaps.Create * - Metamaps.Creators - * - Metamaps.Filter - * - Metamaps.GlobalUI * - Metamaps.Mappings - * - Metamaps.SynapseCard * - Metamaps.Synapses - * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Visualize */ const Topic = { @@ -67,19 +70,19 @@ const Topic = { $('#filter_by_mapper h3').html('CREATORS') // build and render the visualization - Metamaps.Visualize.type = 'RGraph' + Visualize.type = 'RGraph' JIT.prepareVizData() // update filters - Metamaps.Filter.reset() + Filter.reset() // reset selected arrays Selected.reset() // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() // for mobile $('#header_content').html(Active.Topic.get('name')) @@ -93,22 +96,22 @@ const Topic = { end: function () { if (Active.Topic) { $('.rightclickmenu').remove() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Filter.close() } }, centerOn: function (nodeid, callback) { // don't clash with fetchRelatives - if (!Metamaps.Visualize.mGraph.busy) { - Metamaps.Visualize.mGraph.onClick(nodeid, { + if (!Visualize.mGraph.busy) { + Visualize.mGraph.onClick(nodeid, { hideLabels: false, duration: 1000, onComplete: function () { if (callback) callback() } }) - Metamaps.Router.navigate('/topics/' + nodeid) + Router.navigate('/topics/' + nodeid) Active.Topic = Metamaps.Topics.get(nodeid) } }, @@ -127,7 +130,7 @@ const Topic = { var successCallback; successCallback = function (data) { - if (Metamaps.Visualize.mGraph.busy) { + if (Visualize.mGraph.busy) { // don't clash with centerOn window.setTimeout(function() { successCallback(data) }, 100) return @@ -141,7 +144,7 @@ const Topic = { var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses) var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] - Metamaps.Visualize.mGraph.op.sum(graph, { + Visualize.mGraph.op.sum(graph, { type: 'fade', duration: 500, hideLabels: false @@ -149,7 +152,7 @@ const Topic = { var i, l, t, s - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { t = Metamaps.Topics.get(n.id) t.set({ node: n }, { silent: true }) t.updateNode() @@ -186,7 +189,7 @@ const Topic = { // opts is additional options in a hash // TODO: move createNewInDB and permitCerateSYnapseAfter into opts renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { - var self = Metamaps.Topic + var self = Topic var nodeOnViz, tempPos @@ -194,37 +197,37 @@ const Topic = { var midpoint = {}, pixelPos - if (!$.isEmptyObject(Metamaps.Visualize.mGraph.graph.nodes)) { - Metamaps.Visualize.mGraph.graph.addNode(newnode) - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + if (!$.isEmptyObject(Visualize.mGraph.graph.nodes)) { + Visualize.mGraph.graph.addNode(newnode) + nodeOnViz = Visualize.mGraph.graph.getNode(newnode.id) topic.set('node', nodeOnViz, {silent: true}) topic.updateNode() // links the topic and the mapping to the node nodeOnViz.setData('dim', 1, 'start') nodeOnViz.setData('dim', 25, 'end') - if (Metamaps.Visualize.type === 'RGraph') { + if (Visualize.type === 'RGraph') { tempPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) tempPos = tempPos.toPolar() nodeOnViz.setPos(tempPos, 'current') nodeOnViz.setPos(tempPos, 'start') nodeOnViz.setPos(tempPos, 'end') - } else if (Metamaps.Visualize.type === 'ForceDirected') { + } else if (Visualize.type === 'ForceDirected') { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') } - if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + if (Create.newTopic.addSynapse && permitCreateSynapseAfter) { + Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id // position the form midpoint.x = JIT.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 midpoint.y = JIT.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 - pixelPos = Metamaps.Util.coordsToPixels(midpoint) + pixelPos = Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') // show the form - Metamaps.Create.newSynapse.open() - Metamaps.Visualize.mGraph.fx.animate({ + Create.newSynapse.open() + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () { @@ -234,16 +237,16 @@ const Topic = { } }) } else { - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.plotNode(nodeOnViz, Visualize.mGraph.canvas) + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () {} }) } } else { - Metamaps.Visualize.mGraph.loadJSON(newnode) - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + Visualize.mGraph.loadJSON(newnode) + nodeOnViz = Visualize.mGraph.graph.getNode(newnode.id) topic.set('node', nodeOnViz, {silent: true}) topic.updateNode() // links the topic and the mapping to the node @@ -252,8 +255,8 @@ const Topic = { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.plotNode(nodeOnViz, Visualize.mGraph.canvas) + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () {} @@ -284,8 +287,8 @@ const Topic = { }) } - if (Metamaps.Create.newTopic.addSynapse) { - Metamaps.Create.newSynapse.topic2id = topicModel.id + if (Create.newTopic.addSynapse) { + Create.newSynapse.topic2id = topicModel.id } } @@ -305,58 +308,58 @@ const Topic = { } }, createTopicLocally: function () { - var self = Metamaps.Topic + var self = Topic - if (Metamaps.Create.newTopic.name === '') { - Metamaps.GlobalUI.notifyUser('Please enter a topic title...') + if (Create.newTopic.name === '') { + GlobalUI.notifyUser('Please enter a topic title...') return } // hide the 'double-click to add a topic' message - Metamaps.GlobalUI.hideDiv('#instructions') + GlobalUI.hideDiv('#instructions') - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) - var metacode = Metamaps.Metacodes.get(Metamaps.Create.newTopic.metacode) + var metacode = Metamaps.Metacodes.get(Create.newTopic.metacode) var topic = new Metamaps.Backbone.Topic({ - name: Metamaps.Create.newTopic.name, + name: Create.newTopic.name, metacode_id: metacode.id, defer_to_map_id: Active.Map.id }) Metamaps.Topics.add(topic) - if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.AutoLayout.getNextCoord() + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_id: topic.cid, mappable_type: 'Topic', }) Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Metamaps.Create.newTopic.hide() + Create.newTopic.hide() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database }, getTopicFromAutocomplete: function (id) { - var self = Metamaps.Topic + var self = Topic - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) - Metamaps.Create.newTopic.hide() + Create.newTopic.hide() var topic = self.get(id) - if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.AutoLayout.getNextCoord() + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_type: 'Topic', mappable_id: topic.id, }) @@ -365,13 +368,13 @@ const Topic = { self.renderTopic(mapping, topic, true, true) }, getTopicFromSearch: function (event, id) { - var self = Metamaps.Topic + var self = Topic - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) var topic = self.get(id) - var nextCoords = Metamaps.AutoLayout.getNextCoord() + var nextCoords = AutoLayout.getNextCoord() var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords.x, yloc: nextCoords.y, @@ -382,7 +385,7 @@ const Topic = { self.renderTopic(mapping, topic, true, true) - Metamaps.GlobalUI.notifyUser('Topic was added to your map!') + GlobalUI.notifyUser('Topic was added to your map!') event.stopPropagation() event.preventDefault() diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index ebc79575..7320d285 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,7 +1,9 @@ /* global Metamaps, $ */ import Active from './Active' +import GlobalUI from './GlobalUI' import Mapper from './Mapper' +import Router from './Router' import Util from './Util' import Visualize from './Visualize' @@ -9,9 +11,7 @@ import Visualize from './Visualize' * Metamaps.TopicCard.js * * Dependencies: - * - Metamaps.GlobalUI * - Metamaps.Metacodes - * - Metamaps.Router */ const TopicCard = { openTopicCard: null, // stores the topic that's currently open @@ -332,7 +332,7 @@ const TopicCard = { $('.showcard .hoverTip').removeClass('hide') }) - $('.mapCount .tip li a').click(Metamaps.Router.intercept) + $('.mapCount .tip li a').click(Router.intercept) var originalText = $('.showMore').html() $('.mapCount .tip .showMore').unbind().toggle( @@ -353,7 +353,7 @@ const TopicCard = { var self = TopicCard self.removeLink() - Metamaps.GlobalUI.notifyUser('Invalid link') + GlobalUI.notifyUser('Invalid link') }, populateShowCard: function (topic) { var self = TopicCard diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 9e44e8e8..678c7c64 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -2,6 +2,8 @@ import Active from './Active' import JIT from './JIT' +import Router from './Router' +import TopicCard from './TopicCard' /* * Metamaps.Visualize @@ -9,9 +11,7 @@ import JIT from './JIT' * Dependencies: * - Metamaps.Loading * - Metamaps.Metacodes - * - Metamaps.Router * - Metamaps.Synapses - * - Metamaps.TopicCard * - Metamaps.Topics */ @@ -42,7 +42,7 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchend touchcancel', function (event) { lastDist = 0 - if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) TopicCard.hideCurrentCard() self.mGraph.events.touched = self.mGraph.events.touchMoved = false Visualize.touchDragNode = false }) @@ -204,16 +204,16 @@ const Visualize = { hold() // update the url now that the map is ready - clearTimeout(Metamaps.Router.timeoutId) - Metamaps.Router.timeoutId = setTimeout(function () { + clearTimeout(Router.timeoutId) + Router.timeoutId = setTimeout(function () { var m = Active.Map var t = Active.Topic if (m && window.location.pathname !== '/maps/' + m.id) { - Metamaps.Router.navigate('/maps/' + m.id) + Router.navigate('/maps/' + m.id) } else if (t && window.location.pathname !== '/topics/' + t.id) { - Metamaps.Router.navigate('/topics/' + t.id) + Router.navigate('/topics/' + t.id) } }, 800) } diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 37e93492..9fe8925b 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -83,22 +83,22 @@ document.addEventListener("DOMContentLoaded", function() { Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() + Views.exploreMaps.fetchUserThenRender() } else { - Metamaps.Views.exploreMaps.render() + Views.exploreMaps.render() } - Metamaps.GlobalUI.showDiv('#explore') + GlobalUI.showDiv('#explore') } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') + else if (Metamaps.currentSection === "" && Active.Mapper) { + Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.exploreMaps.render() + GlobalUI.showDiv('#explore') } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + else if (Active.Map || Active.Topic) { Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') + JIT.prepareVizData() + GlobalUI.showDiv('#infovis') } }); From 59b471ac62a75da175a8a17bee11c9f4b22f8624 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 23:51:13 +0800 Subject: [PATCH 024/378] break Map into three files --- frontend/src/Metamaps/Map/CheatSheet.js | 27 ++ .../src/Metamaps/{Map.js => Map/InfoBox.js} | 446 ++---------------- frontend/src/Metamaps/Map/index.js | 365 ++++++++++++++ frontend/src/Metamaps/index.js | 4 +- 4 files changed, 425 insertions(+), 417 deletions(-) create mode 100644 frontend/src/Metamaps/Map/CheatSheet.js rename frontend/src/Metamaps/{Map.js => Map/InfoBox.js} (51%) create mode 100644 frontend/src/Metamaps/Map/index.js diff --git a/frontend/src/Metamaps/Map/CheatSheet.js b/frontend/src/Metamaps/Map/CheatSheet.js new file mode 100644 index 00000000..969ee159 --- /dev/null +++ b/frontend/src/Metamaps/Map/CheatSheet.js @@ -0,0 +1,27 @@ +const CheatSheet = { + init: function () { + // tab the cheatsheet + $('#cheatSheet').tabs() + $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') + $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') + + // id = the id of a vimeo video + var switchVideo = function (element, id) { + $('.tutorialItem').removeClass('active') + $(element).addClass('active') + $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) + } + + $('#gettingStarted').click(function () { + // switchVideo(this,'88334167') + }) + $('#upYourSkillz').click(function () { + // switchVideo(this,'100118167') + }) + $('#advancedMapping').click(function () { + // switchVideo(this,'88334167') + }) + } +} + +export default CheatSheet diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map/InfoBox.js similarity index 51% rename from frontend/src/Metamaps/Map.js rename to frontend/src/Metamaps/Map/InfoBox.js index cd2c3d2e..eaceba29 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,405 +1,19 @@ /* global Metamaps, $ */ import Active from './Active' -import AutoLayout from './AutoLayout' -import Create from './Create' -import Filter from './Filter' -import GlobalUI from './GlobalUI' -import JIT from './JIT' -import Realtime from './Realtime' -import Selected from './Selected' -import SynapseCard from './SynapseCard' -import TopicCard from './TopicCard' -import Visualize from './Visualize' +import GlobalUI from '../GlobalUI' +import Router from '../Router' /* - * Metamaps.Map.js.erb - * - * Dependencies: - * - Metamaps.Backbone - * - Metamaps.Erb - * - Metamaps.Loading - * - Metamaps.Mappers - * - Metamaps.Mappings - * - Metamaps.Maps - * - Metamaps.Messages - * - Metamaps.Router - * - Metamaps.Synapses - * - Metamaps.Topics - * - * Major sub-modules: - * - Metamaps.Map.CheatSheet - * - Metamaps.Map.InfoBox + * Metamaps.Collaborators + * Metamaps.Erb + * Metamaps.Mappers + * Metamaps.Maps + * Metamaps.Synapses + * Metamaps.Topics */ -window.Metamaps = window.Metamaps || {} -Metamaps.Map = { - events: { - editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' - }, - init: function () { - var self = Metamaps.Map - - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#center-container').bind('contextmenu', function (e) { - return false - }) - - $('.starMap').click(function () { - if ($(this).is('.starred')) self.unstar() - else self.star() - }) - - $('.sidebarFork').click(function () { - self.fork() - }) - - GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() - - self.updateStar() - self.InfoBox.init() - self.CheatSheet.init() - - $(document).on(Metamaps.Map.events.editedByActiveMapper, self.editedByActiveMapper) - }, - launch: function (id) { - var bb = Metamaps.Backbone - var start = function (data) { - Active.Map = new bb.Map(data.map) - Metamaps.Mappers = new bb.MapperCollection(data.mappers) - Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) - Metamaps.Topics = new bb.TopicCollection(data.topics) - Metamaps.Synapses = new bb.SynapseCollection(data.synapses) - Metamaps.Mappings = new bb.MappingCollection(data.mappings) - Metamaps.Messages = data.messages - Metamaps.Stars = data.stars - Metamaps.Backbone.attachCollectionEvents() - - var map = Active.Map - var mapper = Active.Mapper - - // add class to .wrapper for specifying whether you can edit the map - if (map.authorizeToEdit(mapper)) { - $('.wrapper').addClass('canEditMap') - } - - // add class to .wrapper for specifying if the map can - // be collaborated on - if (map.get('permission') === 'commons') { - $('.wrapper').addClass('commonsMap') - } - - Metamaps.Map.updateStar() - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('MAPPERS') - - // build and render the visualization - Visualize.type = 'ForceDirected' - JIT.prepareVizData() - - // update filters - Filter.reset() - - // reset selected arrays - Selected.reset() - - // set the proper mapinfobox content - Metamaps.Map.InfoBox.load() - - // these three update the actual filter box with the right list items - Filter.checkMetacodes() - Filter.checkSynapses() - Filter.checkMappers() - - Realtime.startActiveMap() - Metamaps.Loading.hide() - - // for mobile - $('#header_content').html(map.get('name')) - } - - $.ajax({ - url: '/maps/' + id + '/contains.json', - success: start - }) - }, - end: function () { - if (Active.Map) { - $('.wrapper').removeClass('canEditMap commonsMap') - AutoLayout.resetSpiral() - - $('.rightclickmenu').remove() - TopicCard.hideCard() - SynapseCard.hideCard() - Create.newTopic.hide(true) // true means force (and override pinned) - Create.newSynapse.hide() - Filter.close() - Metamaps.Map.InfoBox.close() - Realtime.endActiveMap() - } - }, - updateStar: function () { - if (!Active.Mapper || !Metamaps.Stars) return - // update the star/unstar icon - if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { - $('.starMap').addClass('starred') - $('.starMap .tooltipsAbove').html('Unstar') - } else { - $('.starMap').removeClass('starred') - $('.starMap .tooltipsAbove').html('Star') - } - }, - star: function () { - var self = Metamaps.Map - - if (!Active.Map) return - $.post('/maps/' + Active.Map.id + '/star') - Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) - Metamaps.Maps.Starred.add(Active.Map) - GlobalUI.notifyUser('Map is now starred') - self.updateStar() - }, - unstar: function () { - var self = Metamaps.Map - - if (!Active.Map) return - $.post('/maps/' + Active.Map.id + '/unstar') - Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) - Metamaps.Maps.Starred.remove(Active.Map) - self.updateStar() - }, - fork: function () { - GlobalUI.openLightbox('forkmap') - - var nodes_data = '', - synapses_data = '' - var nodes_array = [] - var synapses_array = [] - // collect the unfiltered topics - Visualize.mGraph.graph.eachNode(function (n) { - // if the opacity is less than 1 then it's filtered - if (n.getData('alpha') === 1) { - var id = n.getData('topic').id - nodes_array.push(id) - var x, y - if (n.pos.x && n.pos.y) { - x = n.pos.x - y = n.pos.y - } else { - var x = Math.cos(n.pos.theta) * n.pos.rho - var y = Math.sin(n.pos.theta) * n.pos.rho - } - nodes_data += id + '/' + x + '/' + y + ',' - } - }) - // collect the unfiltered synapses - Metamaps.Synapses.each(function (synapse) { - var desc = synapse.get('desc') - - var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 - // make sure that both topics are being added, otherwise, it - // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 - if (descNotFiltered && topicsNotFiltered) { - synapses_array.push(synapse.id) - } - }) - - synapses_data = synapses_array.join() - nodes_data = nodes_data.slice(0, -1) - - GlobalUI.CreateMap.topicsToMap = nodes_data - GlobalUI.CreateMap.synapsesToMap = synapses_data - }, - leavePrivateMap: function () { - var map = Active.Map - Metamaps.Maps.Active.remove(map) - Metamaps.Maps.Featured.remove(map) - Metamaps.Router.home() - GlobalUI.notifyUser('Sorry! That map has been changed to Private.') - }, - cantEditNow: function () { - Realtime.turnOff(true); // true is for 'silence' - GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') - Active.Map.trigger('changeByOther') - }, - canEditNow: function () { - var confirmString = "You've been granted permission to edit this map. " - confirmString += 'Do you want to reload and enable realtime collaboration?' - var c = confirm(confirmString) - if (c) { - Metamaps.Router.maps(Active.Map.id) - } - }, - editedByActiveMapper: function () { - if (Active.Mapper) { - Metamaps.Mappers.add(Active.Mapper) - } - }, - exportImage: function () { - var canvas = {} - - canvas.canvas = document.createElement('canvas') - canvas.canvas.width = 1880 // 960 - canvas.canvas.height = 1260 // 630 - - canvas.scaleOffsetX = 1 - canvas.scaleOffsetY = 1 - canvas.translateOffsetY = 0 - canvas.translateOffsetX = 0 - canvas.denySelected = true - - canvas.getSize = function () { - if (this.size) return this.size - var canvas = this.canvas - return this.size = { - width: canvas.width, - height: canvas.height - } - } - canvas.scale = function (x, y) { - var px = this.scaleOffsetX * x, - py = this.scaleOffsetY * y - var dx = this.translateOffsetX * (x - 1) / px, - dy = this.translateOffsetY * (y - 1) / py - this.scaleOffsetX = px - this.scaleOffsetY = py - this.getCtx().scale(x, y) - this.translate(dx, dy) - } - canvas.translate = function (x, y) { - var sx = this.scaleOffsetX, - sy = this.scaleOffsetY - this.translateOffsetX += x * sx - this.translateOffsetY += y * sy - this.getCtx().translate(x, y) - } - canvas.getCtx = function () { - return this.canvas.getContext('2d') - } - // center it - canvas.getCtx().translate(1880 / 2, 1260 / 2) - - var mGraph = Visualize.mGraph - - var id = mGraph.root - var root = mGraph.graph.getNode(id) - var T = !!root.visited - - // pass true to avoid basing it on a selection - JIT.zoomExtents(null, canvas, true) - - var c = canvas.canvas, - ctx = canvas.getCtx(), - scale = canvas.scaleOffsetX - - // draw a grey background - ctx.fillStyle = '#d8d9da' - var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), - yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) - ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) - - // draw the graph - mGraph.graph.eachNode(function (node) { - var nodeAlpha = node.getData('alpha') - node.eachAdjacency(function (adj) { - var nodeTo = adj.nodeTo - if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { - mGraph.fx.plotLine(adj, canvas) - } - }) - if (node.drawn) { - mGraph.fx.plotNode(node, canvas) - } - if (!mGraph.labelsHidden) { - if (node.drawn && nodeAlpha >= 0.95) { - mGraph.labels.plotLabel(canvas, node) - } else { - mGraph.labels.hideLabel(node, false) - } - } - node.visited = !T - }) - - var imageData = { - encoded_image: canvas.canvas.toDataURL() - } - - var map = Active.Map - - var today = new Date() - var dd = today.getDate() - var mm = today.getMonth() + 1; // January is 0! - var yyyy = today.getFullYear() - if (dd < 10) { - dd = '0' + dd - } - if (mm < 10) { - mm = '0' + mm - } - today = mm + '/' + dd + '/' + yyyy - - var mapName = map.get('name').split(' ').join([separator = '-']) - var downloadMessage = '' - downloadMessage += 'Captured map screenshot! ' - downloadMessage += "<a href='" + imageData.encoded_image + "' " - downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" - GlobalUI.notifyUser(downloadMessage) - - $.ajax({ - type: 'POST', - dataType: 'json', - url: '/maps/' + Active.Map.id + '/upload_screenshot', - data: imageData, - success: function (data) { - console.log('successfully uploaded map screenshot') - }, - error: function () { - console.log('failed to save map screenshot') - } - }) - } -} - -/* - * - * CHEATSHEET - * - */ -Metamaps.Map.CheatSheet = { - init: function () { - // tab the cheatsheet - $('#cheatSheet').tabs() - $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') - $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') - - // id = the id of a vimeo video - var switchVideo = function (element, id) { - $('.tutorialItem').removeClass('active') - $(element).addClass('active') - $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) - } - - $('#gettingStarted').click(function () { - // switchVideo(this,'88334167') - }) - $('#upYourSkillz').click(function () { - // switchVideo(this,'100118167') - }) - $('#advancedMapping').click(function () { - // switchVideo(this,'88334167') - }) - } -}; // end Metamaps.Map.CheatSheet - -/* - * - * INFOBOX - * - */ -Metamaps.Map.InfoBox = { +const InfoBox = { isOpen: false, changing: false, selectingPermission: false, @@ -407,7 +21,7 @@ Metamaps.Map.InfoBox = { nameHTML: '<span class="best_in_place best_in_place_name" id="best_in_place_map_{{id}}_name" data-url="/maps/{{id}}" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName">{{name}}</span>', descHTML: '<span class="best_in_place best_in_place_desc" id="best_in_place_map_{{id}}_desc" data-url="/maps/{{id}}" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc">{{desc}}</span>', init: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon').click(self.toggleBox) $('.mapInfoBox').click(function (event) { @@ -426,7 +40,7 @@ Metamaps.Map.InfoBox = { } }, toggleBox: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (self.isOpen) self.close() else self.open() @@ -434,7 +48,7 @@ Metamaps.Map.InfoBox = { event.stopPropagation() }, open: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon div').addClass('hide') if (!self.isOpen && !self.changing) { self.changing = true @@ -445,7 +59,7 @@ Metamaps.Map.InfoBox = { } }, close: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon div').removeClass('hide') if (!self.changing) { @@ -459,7 +73,7 @@ Metamaps.Map.InfoBox = { } }, load: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox var map = Active.Map @@ -494,7 +108,7 @@ Metamaps.Map.InfoBox = { self.attachEventListeners() }, attachEventListeners: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoBox.canEdit .best_in_place').best_in_place() @@ -547,7 +161,7 @@ Metamaps.Map.InfoBox = { $('.mapContributors .tip').unbind().click(function (event) { event.stopPropagation() }) - $('.mapContributors .tip li a').click(Metamaps.Router.intercept) + $('.mapContributors .tip li a').click(Router.intercept) $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { $('.mapContributors .tip').hide() @@ -556,7 +170,7 @@ Metamaps.Map.InfoBox = { self.addTypeahead() }, addTypeahead: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (!Active.Map) return @@ -603,14 +217,14 @@ Metamaps.Map.InfoBox = { } }, removeCollaborator: function (collaboratorId) { - var self = Metamaps.Map.InfoBox + var self = InfoBox Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, addCollaborator: function (newCollaboratorId) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (Metamaps.Collaborators.get(newCollaboratorId)) { GlobalUI.notifyUser('That user already has access') @@ -629,7 +243,7 @@ Metamaps.Map.InfoBox = { $.getJSON('/users/' + newCollaboratorId + '.json', callback) }, handleResultClick: function (event, item) { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.addCollaborator(item.id) $('.collaboratorSearchField').typeahead('val', '') @@ -641,7 +255,7 @@ Metamaps.Map.InfoBox = { $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm) }, createContributorList: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' @@ -666,7 +280,7 @@ Metamaps.Map.InfoBox = { updateNumbers: function () { if (!Active.Map) return - var self = Metamaps.Map.InfoBox + var self = InfoBox var mapper = Active.Mapper var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators @@ -689,10 +303,10 @@ Metamaps.Map.InfoBox = { $('.mapTopics').text(Metamaps.Topics.length) $('.mapSynapses').text(Metamaps.Synapses.length) - $('.mapEditedAt').html('<span>Last edited: </span>' + Metamaps.Util.nowDateFormatted()) + $('.mapEditedAt').html('<span>Last edited: </span>' + Util.nowDateFormatted()) }, onPermissionClick: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (!self.selectingPermission) { self.selectingPermission = true @@ -709,14 +323,14 @@ Metamaps.Map.InfoBox = { } }, hidePermissionSelect: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.selectingPermission = false $('.mapPermission').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow $('.mapPermission .permissionSelect').remove() }, selectPermission: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.selectingPermission = false var permission = $(this).attr('class') @@ -740,19 +354,19 @@ Metamaps.Map.InfoBox = { var authorized = map.authorizePermissionChange(mapper) if (doIt && authorized) { - Metamaps.Map.InfoBox.close() + InfoBox.close() Metamaps.Maps.Active.remove(map) Metamaps.Maps.Featured.remove(map) Metamaps.Maps.Mine.remove(map) Metamaps.Maps.Shared.remove(map) map.destroy() - Metamaps.Router.home() + Router.home() GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") } } -}; // end Metamaps.Map.InfoBox +} -export default Metamaps.Map +export default InfoBox diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js new file mode 100644 index 00000000..84ee8b39 --- /dev/null +++ b/frontend/src/Metamaps/Map/index.js @@ -0,0 +1,365 @@ +/* global Metamaps, $ */ + +import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Realtime from './Realtime' +import Router from './Router' +import Selected from './Selected' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' +import Visualize from './Visualize' + +import CheatSheet from './CheatSheet' +import InfoBox from './InfoBox' + +/* + * Metamaps.Map.js.erb + * + * Dependencies: + * - Metamaps.Backbone + * - Metamaps.Erb + * - Metamaps.Loading + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Maps + * - Metamaps.Messages + * - Metamaps.Synapses + * - Metamaps.Topics + */ + +const Map = { + events: { + editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' + }, + init: function () { + var self = Map + + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#center-container').bind('contextmenu', function (e) { + return false + }) + + $('.starMap').click(function () { + if ($(this).is('.starred')) self.unstar() + else self.star() + }) + + $('.sidebarFork').click(function () { + self.fork() + }) + + GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() + + self.updateStar() + self.InfoBox.init() + CheatSheet.init() + + $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) + }, + launch: function (id) { + var bb = Metamaps.Backbone + var start = function (data) { + Active.Map = new bb.Map(data.map) + Metamaps.Mappers = new bb.MapperCollection(data.mappers) + Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) + Metamaps.Topics = new bb.TopicCollection(data.topics) + Metamaps.Synapses = new bb.SynapseCollection(data.synapses) + Metamaps.Mappings = new bb.MappingCollection(data.mappings) + Metamaps.Messages = data.messages + Metamaps.Stars = data.stars + Metamaps.Backbone.attachCollectionEvents() + + var map = Active.Map + var mapper = Active.Mapper + + // add class to .wrapper for specifying whether you can edit the map + if (map.authorizeToEdit(mapper)) { + $('.wrapper').addClass('canEditMap') + } + + // add class to .wrapper for specifying if the map can + // be collaborated on + if (map.get('permission') === 'commons') { + $('.wrapper').addClass('commonsMap') + } + + Map.updateStar() + + // set filter mapper H3 text + $('#filter_by_mapper h3').html('MAPPERS') + + // build and render the visualization + Visualize.type = 'ForceDirected' + JIT.prepareVizData() + + // update filters + Filter.reset() + + // reset selected arrays + Selected.reset() + + // set the proper mapinfobox content + Map.InfoBox.load() + + // these three update the actual filter box with the right list items + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() + + Realtime.startActiveMap() + Metamaps.Loading.hide() + + // for mobile + $('#header_content').html(map.get('name')) + } + + $.ajax({ + url: '/maps/' + id + '/contains.json', + success: start + }) + }, + end: function () { + if (Active.Map) { + $('.wrapper').removeClass('canEditMap commonsMap') + AutoLayout.resetSpiral() + + $('.rightclickmenu').remove() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide(true) // true means force (and override pinned) + Create.newSynapse.hide() + Filter.close() + Map.InfoBox.close() + Realtime.endActiveMap() + } + }, + updateStar: function () { + if (!Active.Mapper || !Metamaps.Stars) return + // update the star/unstar icon + if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { + $('.starMap').addClass('starred') + $('.starMap .tooltipsAbove').html('Unstar') + } else { + $('.starMap').removeClass('starred') + $('.starMap .tooltipsAbove').html('Star') + } + }, + star: function () { + var self = Map + + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/star') + Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) + Metamaps.Maps.Starred.add(Active.Map) + GlobalUI.notifyUser('Map is now starred') + self.updateStar() + }, + unstar: function () { + var self = Map + + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/unstar') + Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + Metamaps.Maps.Starred.remove(Active.Map) + self.updateStar() + }, + fork: function () { + GlobalUI.openLightbox('forkmap') + + var nodes_data = '', + synapses_data = '' + var nodes_array = [] + var synapses_array = [] + // collect the unfiltered topics + Visualize.mGraph.graph.eachNode(function (n) { + // if the opacity is less than 1 then it's filtered + if (n.getData('alpha') === 1) { + var id = n.getData('topic').id + nodes_array.push(id) + var x, y + if (n.pos.x && n.pos.y) { + x = n.pos.x + y = n.pos.y + } else { + var x = Math.cos(n.pos.theta) * n.pos.rho + var y = Math.sin(n.pos.theta) * n.pos.rho + } + nodes_data += id + '/' + x + '/' + y + ',' + } + }) + // collect the unfiltered synapses + Metamaps.Synapses.each(function (synapse) { + var desc = synapse.get('desc') + + var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 + // make sure that both topics are being added, otherwise, it + // doesn't make sense to add the synapse + var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 + if (descNotFiltered && topicsNotFiltered) { + synapses_array.push(synapse.id) + } + }) + + synapses_data = synapses_array.join() + nodes_data = nodes_data.slice(0, -1) + + GlobalUI.CreateMap.topicsToMap = nodes_data + GlobalUI.CreateMap.synapsesToMap = synapses_data + }, + leavePrivateMap: function () { + var map = Active.Map + Metamaps.Maps.Active.remove(map) + Metamaps.Maps.Featured.remove(map) + Router.home() + GlobalUI.notifyUser('Sorry! That map has been changed to Private.') + }, + cantEditNow: function () { + Realtime.turnOff(true); // true is for 'silence' + GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') + Active.Map.trigger('changeByOther') + }, + canEditNow: function () { + var confirmString = "You've been granted permission to edit this map. " + confirmString += 'Do you want to reload and enable realtime collaboration?' + var c = confirm(confirmString) + if (c) { + Router.maps(Active.Map.id) + } + }, + editedByActiveMapper: function () { + if (Active.Mapper) { + Metamaps.Mappers.add(Active.Mapper) + } + }, + exportImage: function () { + var canvas = {} + + canvas.canvas = document.createElement('canvas') + canvas.canvas.width = 1880 // 960 + canvas.canvas.height = 1260 // 630 + + canvas.scaleOffsetX = 1 + canvas.scaleOffsetY = 1 + canvas.translateOffsetY = 0 + canvas.translateOffsetX = 0 + canvas.denySelected = true + + canvas.getSize = function () { + if (this.size) return this.size + var canvas = this.canvas + return this.size = { + width: canvas.width, + height: canvas.height + } + } + canvas.scale = function (x, y) { + var px = this.scaleOffsetX * x, + py = this.scaleOffsetY * y + var dx = this.translateOffsetX * (x - 1) / px, + dy = this.translateOffsetY * (y - 1) / py + this.scaleOffsetX = px + this.scaleOffsetY = py + this.getCtx().scale(x, y) + this.translate(dx, dy) + } + canvas.translate = function (x, y) { + var sx = this.scaleOffsetX, + sy = this.scaleOffsetY + this.translateOffsetX += x * sx + this.translateOffsetY += y * sy + this.getCtx().translate(x, y) + } + canvas.getCtx = function () { + return this.canvas.getContext('2d') + } + // center it + canvas.getCtx().translate(1880 / 2, 1260 / 2) + + var mGraph = Visualize.mGraph + + var id = mGraph.root + var root = mGraph.graph.getNode(id) + var T = !!root.visited + + // pass true to avoid basing it on a selection + JIT.zoomExtents(null, canvas, true) + + var c = canvas.canvas, + ctx = canvas.getCtx(), + scale = canvas.scaleOffsetX + + // draw a grey background + ctx.fillStyle = '#d8d9da' + var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), + yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) + ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) + + // draw the graph + mGraph.graph.eachNode(function (node) { + var nodeAlpha = node.getData('alpha') + node.eachAdjacency(function (adj) { + var nodeTo = adj.nodeTo + if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { + mGraph.fx.plotLine(adj, canvas) + } + }) + if (node.drawn) { + mGraph.fx.plotNode(node, canvas) + } + if (!mGraph.labelsHidden) { + if (node.drawn && nodeAlpha >= 0.95) { + mGraph.labels.plotLabel(canvas, node) + } else { + mGraph.labels.hideLabel(node, false) + } + } + node.visited = !T + }) + + var imageData = { + encoded_image: canvas.canvas.toDataURL() + } + + var map = Active.Map + + var today = new Date() + var dd = today.getDate() + var mm = today.getMonth() + 1; // January is 0! + var yyyy = today.getFullYear() + if (dd < 10) { + dd = '0' + dd + } + if (mm < 10) { + mm = '0' + mm + } + today = mm + '/' + dd + '/' + yyyy + + var mapName = map.get('name').split(' ').join([separator = '-']) + var downloadMessage = '' + downloadMessage += 'Captured map screenshot! ' + downloadMessage += "<a href='" + imageData.encoded_image + "' " + downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" + GlobalUI.notifyUser(downloadMessage) + + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/maps/' + Active.Map.id + '/upload_screenshot', + data: imageData, + success: function (data) { + console.log('successfully uploaded map screenshot') + }, + error: function () { + console.log('failed to save map screenshot') + } + }) + } +} + +export CheatSheet, InfoBox +export default Map diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 9fe8925b..7b431d1f 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -13,7 +13,7 @@ import GlobalUI from './GlobalUI' import Import from './Import' import JIT from './JIT' import Listeners from './Listeners' -import Map from './Map' +import Map, { CheatSheet, InfoBox } from './Map' import Mapper from './Mapper' import Mobile from './Mobile' import Mouse from './Mouse' @@ -46,6 +46,8 @@ Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners Metamaps.Map = Map +Metamaps.Map.CheatSheet = CheatSheet +Metamaps.Map.InfoBox = InfoBox Metamaps.Maps = {} Metamaps.Mapper = Mapper Metamaps.Mobile = Mobile From fe3012136da1a674d87cd0cb0bb48d73dd33c08c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 23:51:33 +0800 Subject: [PATCH 025/378] import _ --- frontend/src/Metamaps/Backbone.js | 3 ++- frontend/src/Metamaps/Control.js | 2 ++ frontend/src/Metamaps/Filter.js | 2 ++ frontend/src/Metamaps/JIT.js | 2 ++ frontend/src/Metamaps/Organize.js | 2 ++ frontend/src/Metamaps/Realtime.js | 2 ++ frontend/src/Metamaps/Visualize.js | 2 ++ 7 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index 9f18ef32..bc303df4 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -1,6 +1,7 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, _, $ */ +import _ from 'lodash' + /* * Metamaps.Backbone.js.erb * diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 9e13e40c..2c14cfca 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Filter from './Filter' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 38c4f369..f67c6ec8 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 50c48985..0fe5a224 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,5 +1,7 @@ /* global Metamaps */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import Create from './Create' diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index ee29c2b8..c05f870e 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,5 +1,7 @@ /* global $ */ +import _ from 'lodash' + import Visualize from './Visualize' import JIT from './JIT' diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 1eef6408..80143f25 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 678c7c64..047cb81d 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import JIT from './JIT' import Router from './Router' From 30894a313fbde92faabcc5ed37124cf995a34967 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:07:30 +0800 Subject: [PATCH 026/378] move views to their own frontend folder --- .../javascripts/src/Metamaps.Erb.js.erb | 2 + .../javascripts/src/views/chatView.js.erb | 343 ------------------ app/assets/javascripts/src/views/room.js | 195 ---------- app/assets/javascripts/src/views/videoView.js | 207 ----------- frontend/src/Metamaps/Realtime.js | 18 +- frontend/src/Metamaps/Router.js | 10 +- frontend/src/Metamaps/Views.js | 91 ----- frontend/src/Metamaps/Views/ChatView.js | 337 +++++++++++++++++ frontend/src/Metamaps/Views/ExploreMaps.js | 86 +++++ frontend/src/Metamaps/Views/Room.js | 198 ++++++++++ frontend/src/Metamaps/Views/VideoView.js | 202 +++++++++++ frontend/src/Metamaps/Views/index.js | 6 + frontend/src/Metamaps/index.js | 12 +- 13 files changed, 851 insertions(+), 856 deletions(-) delete mode 100644 app/assets/javascripts/src/views/chatView.js.erb delete mode 100644 app/assets/javascripts/src/views/room.js delete mode 100644 app/assets/javascripts/src/views/videoView.js delete mode 100644 frontend/src/Metamaps/Views.js create mode 100644 frontend/src/Metamaps/Views/ChatView.js create mode 100644 frontend/src/Metamaps/Views/ExploreMaps.js create mode 100644 frontend/src/Metamaps/Views/Room.js create mode 100644 frontend/src/Metamaps/Views/VideoView.js create mode 100644 frontend/src/Metamaps/Views/index.js diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index 90eba5e5..60b64e46 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -14,5 +14,7 @@ Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' +Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.VERSION = '<%= METAMAPS_VERSION %>' diff --git a/app/assets/javascripts/src/views/chatView.js.erb b/app/assets/javascripts/src/views/chatView.js.erb deleted file mode 100644 index 7a1e7f8e..00000000 --- a/app/assets/javascripts/src/views/chatView.js.erb +++ /dev/null @@ -1,343 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.chatView = (function () { - var - chatView, - linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); - - var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - "</div>", - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - "</div>", - templates: function() { - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - this.messageTemplate = _.template(Private.messageHTML); - - this.participantTemplate = _.template(Private.participantHTML); - }, - createElements: function() { - this.$unread = $('<div class="chat-unread"></div>'); - this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); - this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); - this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); - this.$videoToggle = $('<div class="video-toggle"></div>'); - this.$cursorToggle = $('<div class="cursor-toggle"></div>'); - this.$participants = $('<div class="participants"></div>'); - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); - this.$chatHeader = $('<div class="chat-header">CHAT</div>'); - this.$soundToggle = $('<div class="sound-toggle"></div>'); - this.$messages = $('<div class="chat-messages"></div>'); - this.$container = $('<div class="chat-box"></div>'); - }, - attachElements: function() { - this.$button.append(this.$unread); - - this.$juntoHeader.append(this.$videoToggle); - this.$juntoHeader.append(this.$cursorToggle); - - this.$chatHeader.append(this.$soundToggle); - - this.$participants.append(this.$conversationInProgress); - - this.$container.append(this.$juntoHeader); - this.$container.append(this.$participants); - this.$container.append(this.$chatHeader); - this.$container.append(this.$button); - this.$container.append(this.$messages); - this.$container.append(this.$messageInput); - }, - addEventListeners: function() { - var self = this; - - this.participants.on('add', function (participant) { - Private.addParticipant.call(self, participant); - }); - - this.participants.on('remove', function (participant) { - Private.removeParticipant.call(self, participant); - }); - - this.$button.on('click', function () { - Handlers.buttonClick.call(self); - }); - this.$videoToggle.on('click', function () { - Handlers.videoToggleClick.call(self); - }); - this.$cursorToggle.on('click', function () { - Handlers.cursorToggleClick.call(self); - }); - this.$soundToggle.on('click', function () { - Handlers.soundToggleClick.call(self); - }); - this.$messageInput.on('keyup', function (event) { - Handlers.keyUp.call(self, event); - }); - this.$messageInput.on('focus', function () { - Handlers.inputFocus.call(self); - }); - this.$messageInput.on('blur', function () { - Handlers.inputBlur.call(self); - }); - }, - initializeSounds: function() { - this.sound = new Howl({ - urls: ["<%= asset_path 'sounds/MM_sounds.mp3' %>", "<%= asset_path 'sounds/MM_sounds.ogg' %>"], - sprite: { - joinmap: [0, 561], - leavemap: [1000, 592], - receivechat: [2000, 318], - sendchat: [3000, 296], - sessioninvite: [4000, 5393, true] - } - }); - }, - incrementUnread: function() { - this.unreadMessages++; - this.$unread.html(this.unreadMessages); - this.$unread.show(); - }, - addMessage: function(message, isInitial, wasMe) { - - if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); - - function addZero(i) { - if (i < 10) { - i = "0" + i; - } - return i; - } - var m = _.clone(message.attributes); - - var today = new Date(); - m.timestamp = new Date(m.created_at); - - var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); - date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); - m.timestamp = date; - m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove - m.message = linker.link(m.message); - var $html = $(this.messageTemplate(m)); - this.$messages.append($html); - if (!isInitial) this.scrollMessages(200); - - if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); - }, - initialMessages: function() { - var messages = this.messages.models; - for (var i = 0; i < messages.length; i++) { - Private.addMessage.call(this, messages[i], true); - } - }, - handleInputMessage: function() { - var message = { - message: this.$messageInput.val(), - }; - this.$messageInput.val(''); - $(document).trigger(chatView.events.message + '-' + this.room, [message]); - }, - addParticipant: function(participant) { - var p = _.clone(participant.attributes); - if (p.self) { - p.selfClass = 'is-self'; - p.selfName = '(me)'; - } else { - p.selfClass = ''; - p.selfName = ''; - } - var html = this.participantTemplate(p); - this.$participants.append(html); - }, - removeParticipant: function(participant) { - this.$container.find('.participant-' + participant.get('id')).remove(); - } - }; - - var Handlers = { - buttonClick: function() { - if (this.isOpen) this.close(); - else if (!this.isOpen) this.open(); - }, - videoToggleClick: function() { - this.$videoToggle.toggleClass('active'); - this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); - }, - cursorToggleClick: function() { - this.$cursorToggle.toggleClass('active'); - this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); - }, - soundToggleClick: function() { - this.alertSound = !this.alertSound; - this.$soundToggle.toggleClass('active'); - }, - keyUp: function(event) { - switch(event.which) { - case 13: // enter - Private.handleInputMessage.call(this); - break; - } - }, - inputFocus: function() { - $(document).trigger(chatView.events.inputFocus); - }, - inputBlur: function() { - $(document).trigger(chatView.events.inputBlur); - } - }; - - chatView = function(messages, mapper, room) { - var self = this; - - this.room = room; - this.mapper = mapper; - this.messages = messages; // backbone collection - - this.isOpen = false; - this.alertSound = true; // whether to play sounds on arrival of new messages or not - this.cursorsShowing = true; - this.videosShowing = true; - this.unreadMessages = 0; - this.participants = new Backbone.Collection(); - - Private.templates.call(this); - Private.createElements.call(this); - Private.attachElements.call(this); - Private.addEventListeners.call(this); - Private.initialMessages.call(this); - Private.initializeSounds.call(this); - this.$container.css({ - right: '-300px' - }); - }; - - chatView.prototype.conversationInProgress = function (participating) { - this.$conversationInProgress.show(); - this.$participants.addClass('is-live'); - if (participating) this.$participants.addClass('is-participating'); - this.$button.addClass('active'); - - // hide invite to call buttons - } - - chatView.prototype.conversationEnded = function () { - this.$conversationInProgress.hide(); - this.$participants.removeClass('is-live'); - this.$participants.removeClass('is-participating'); - this.$button.removeClass('active'); - this.$participants.find('.participant').removeClass('active'); - this.$participants.find('.participant').removeClass('pending'); - } - - chatView.prototype.leaveConversation = function () { - this.$participants.removeClass('is-participating'); - } - - chatView.prototype.mapperJoinedCall = function (id) { - this.$participants.find('.participant-' + id).addClass('active'); - } - - chatView.prototype.mapperLeftCall = function (id) { - this.$participants.find('.participant-' + id).removeClass('active'); - } - - chatView.prototype.invitationPending = function (id) { - this.$participants.find('.participant-' + id).addClass('pending'); - } - - chatView.prototype.invitationAnswered = function (id) { - this.$participants.find('.participant-' + id).removeClass('pending'); - } - - chatView.prototype.addParticipant = function (participant) { - this.participants.add(participant); - } - - chatView.prototype.removeParticipant = function (username) { - var p = this.participants.find(function (p) { return p.get('username') === username; }); - if (p) { - this.participants.remove(p); - } - } - - chatView.prototype.removeParticipants = function () { - this.participants.remove(this.participants.models); - } - - chatView.prototype.open = function () { - this.$container.css({ - right: '0' - }); - this.$messageInput.focus(); - this.isOpen = true; - this.unreadMessages = 0; - this.$unread.hide(); - this.scrollMessages(0); - $(document).trigger(chatView.events.openTray); - } - - chatView.prototype.addMessage = function(message, isInitial, wasMe) { - this.messages.add(message); - Private.addMessage.call(this, message, isInitial, wasMe); - } - - chatView.prototype.scrollMessages = function(duration) { - duration = duration || 0; - - this.$messages.animate({ - scrollTop: this.$messages[0].scrollHeight - }, duration); - } - - chatView.prototype.clearMessages = function () { - this.unreadMessages = 0; - this.$unread.hide(); - this.$messages.empty(); - } - - chatView.prototype.close = function () { - this.$container.css({ - right: '-300px' - }); - this.$messageInput.blur(); - this.isOpen = false; - $(document).trigger(chatView.events.closeTray); - } - - chatView.prototype.remove = function () { - this.$button.off(); - this.$container.remove(); - } - - /** - * @class - * @static - */ - chatView.events = { - message: 'ChatView:message', - openTray: 'ChatView:openTray', - closeTray: 'ChatView:closeTray', - inputFocus: 'ChatView:inputFocus', - inputBlur: 'ChatView:inputBlur', - cursorsOff: 'ChatView:cursorsOff', - cursorsOn: 'ChatView:cursorsOn', - videosOff: 'ChatView:videosOff', - videosOn: 'ChatView:videosOn' - }; - - return chatView; - -})(); diff --git a/app/assets/javascripts/src/views/room.js b/app/assets/javascripts/src/views/room.js deleted file mode 100644 index 4595c3cb..00000000 --- a/app/assets/javascripts/src/views/room.js +++ /dev/null @@ -1,195 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.room = (function () { - - var ChatView = Metamaps.Views.chatView; - var VideoView = Metamaps.Views.videoView; - - var room = function(opts) { - var self = this; - - this.isActiveRoom = false; - this.socket = opts.socket; - this.webrtc = opts.webrtc; - //this.roomRef = opts.firebase; - this.room = opts.room; - this.config = opts.config; - this.peopleCount = 0; - - this.$myVideo = opts.$video; - this.myVideo = opts.myVideoView; - - this.messages = new Backbone.Collection(); - this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }); - this.chat = new ChatView(this.messages, this.currentMapper, this.room); - - this.videos = {}; - - this.init(); - }; - - room.prototype.join = function(cb) { - this.isActiveRoom = true; - this.webrtc.joinRoom(this.room, cb); - this.chat.conversationInProgress(true); // true indicates participation - } - - room.prototype.conversationInProgress = function() { - this.chat.conversationInProgress(false); // false indicates not participating - } - - room.prototype.conversationEnding = function() { - this.chat.conversationEnded(); - } - - room.prototype.leaveVideoOnly = function() { - this.chat.leaveConversation(); // the conversation will carry on without you - for (var id in this.videos) { - this.removeVideo(id); - } - this.isActiveRoom = false; - this.webrtc.leaveRoom(); - } - - room.prototype.leave = function() { - for (var id in this.videos) { - this.removeVideo(id); - } - this.isActiveRoom = false; - this.webrtc.leaveRoom(); - this.chat.conversationEnded(); - this.chat.removeParticipants(); - this.chat.clearMessages(); - this.messages.reset(); - } - - room.prototype.setPeopleCount = function(count) { - this.peopleCount = count; - } - - room.prototype.init = function () { - var self = this; - - $(document).on(VideoView.events.audioControlClick, function (event, videoView) { - if (!videoView.audioStatus) self.webrtc.mute(); - else if (videoView.audioStatus) self.webrtc.unmute(); - }); - $(document).on(VideoView.events.videoControlClick, function (event, videoView) { - if (!videoView.videoStatus) self.webrtc.pauseVideo(); - else if (videoView.videoStatus) self.webrtc.resumeVideo(); - }); - - this.webrtc.webrtc.off('peerStreamAdded'); - this.webrtc.webrtc.off('peerStreamRemoved'); - this.webrtc.on('peerStreamAdded', function (peer) { - var mapper = Metamaps.Realtime.mappersOnMap[peer.nick]; - peer.avatar = mapper.image; - peer.username = mapper.name; - if (self.isActiveRoom) { - self.addVideo(peer); - } - }); - - this.webrtc.on('peerStreamRemoved', function (peer) { - if (self.isActiveRoom) { - self.removeVideo(peer); - } - }); - - this.webrtc.on('mute', function (data) { - var v = self.videos[data.id]; - if (!v) return; - - if (data.name === 'audio') { - v.audioStatus = false; - } - else if (data.name === 'video') { - v.videoStatus = false; - v.$avatar.show(); - } - if (!v.audioStatus && !v.videoStatus) v.$container.hide(); - }); - this.webrtc.on('unmute', function (data) { - var v = self.videos[data.id]; - if (!v) return; - - if (data.name === 'audio') { - v.audioStatus = true; - } - else if (data.name === 'video') { - v.videoStatus = true; - v.$avatar.hide(); - } - v.$container.show(); - }); - - var sendChatMessage = function (event, data) { - self.sendChatMessage(data); - }; - $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage); - } - - room.prototype.videoAdded = function (callback) { - this._videoAdded = callback; - } - - room.prototype.addVideo = function (peer) { - var - id = this.webrtc.getDomId(peer), - video = attachMediaStream(peer.stream); - - var - v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }); - - this.videos[peer.id] = v; - if (this._videoAdded) this._videoAdded(v, peer.nick); - } - - room.prototype.removeVideo = function (peer) { - var id = typeof peer == 'string' ? peer : peer.id; - if (this.videos[id]) { - this.videos[id].remove(); - delete this.videos[id]; - } - } - - room.prototype.sendChatMessage = function (data) { - var self = this; - //this.roomRef.child('messages').push(data); - if (self.chat.alertSound) self.chat.sound.play('sendchat'); - var m = new Metamaps.Backbone.Message({ - message: data.message, - resource_id: Metamaps.Active.Map.id, - resource_type: "Map" - }); - m.save(null, { - success: function (model, response) { - self.addMessages(new Metamaps.Backbone.MessageCollection(model), false, true); - $(document).trigger(room.events.newMessage, [model]); - }, - error: function (model, response) { - console.log('error!', response); - } - }); - } - - // they should be instantiated as backbone models before they get - // passed to this function - room.prototype.addMessages = function (messages, isInitial, wasMe) { - var self = this; - - messages.models.forEach(function (message) { - self.chat.addMessage(message, isInitial, wasMe); - }); - } - - /** - * @class - * @static - */ - room.events = { - newMessage: "Room:newMessage" - }; - - return room; -})(); diff --git a/app/assets/javascripts/src/views/videoView.js b/app/assets/javascripts/src/views/videoView.js deleted file mode 100644 index b9d39c06..00000000 --- a/app/assets/javascripts/src/views/videoView.js +++ /dev/null @@ -1,207 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.videoView = (function () { - - var videoView; - - var Private = { - addControls: function() { - var self = this; - - this.$audioControl = $('<div class="video-audio"></div>'); - this.$videoControl = $('<div class="video-video"></div>'); - - this.$audioControl.on('click', function () { - Handlers.audioControlClick.call(self); - }); - - this.$videoControl.on('click', function () { - Handlers.videoControlClick.call(self); - }); - - this.$container.append(this.$audioControl); - this.$container.append(this.$videoControl); - }, - cancelClick: function() { - this.mouseIsDown = false; - - if (this.hasMoved) { - - } - - $(document).trigger(videoView.events.dragEnd); - } - }; - - var Handlers = { - mousedown: function(event) { - this.mouseIsDown = true; - this.hasMoved = false; - this.mouseMoveStart = { - x: event.pageX, - y: event.pageY - }; - this.posStart = { - x: parseInt(this.$container.css('left'), '10'), - y: parseInt(this.$container.css('top'), '10') - } - - $(document).trigger(videoView.events.mousedown); - }, - mouseup: function(event) { - $(document).trigger(videoView.events.mouseup, [this]); - - var storedTime = this.lastClick; - var now = Date.now(); - this.lastClick = now; - - if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { - $(document).trigger(videoView.events.doubleClick, [this]); - } - }, - mousemove: function(event) { - var - diffX, - diffY, - newX, - newY; - - if (this.$parent && this.mouseIsDown) { - this.manuallyPositioned = true; - this.hasMoved = true; - diffX = event.pageX - this.mouseMoveStart.x; - diffY = this.mouseMoveStart.y - event.pageY; - newX = this.posStart.x + diffX; - newY = this.posStart.y - diffY; - this.$container.css({ - top: newY, - left: newX - }); - } - }, - audioControlClick: function() { - if (this.audioStatus) { - this.audioOff(); - } else { - this.audioOn(); - } - $(document).trigger(videoView.events.audioControlClick, [this]); - }, - videoControlClick: function() { - if (this.videoStatus) { - this.videoOff(); - } else { - this.videoOn(); - } - $(document).trigger(videoView.events.videoControlClick, [this]); - }, - }; - - var videoView = function(video, $parent, id, isMyself, config) { - var self = this; - - this.$parent = $parent; // mapView - - this.video = video; - this.id = id; - - this.config = config; - - this.mouseIsDown = false; - this.mouseDownOffset = { x: 0, y: 0 }; - this.lastClick = null; - this.hasMoved = false; - - this.audioStatus = true; - this.videoStatus = true; - - this.$container = $('<div></div>'); - this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')); - this.$container.attr('id', 'container_' + id); - - - var $vidContainer = $('<div></div>'); - $vidContainer.addClass('video-cutoff'); - $vidContainer.append(this.video); - - this.avatar = config.avatar; - this.$avatar = $('<img draggable="false" class="collaborator-video-avatar" src="' + config.avatar + '" width="150" height="150" />'); - $vidContainer.append(this.$avatar); - - this.$container.append($vidContainer); - - this.$container.on('mousedown', function (event) { - Handlers.mousedown.call(self, event); - }); - - if (isMyself) { - Private.addControls.call(this); - } - - // suppress contextmenu - this.video.oncontextmenu = function () { return false; }; - - if (this.$parent) this.setParent(this.$parent); - }; - - videoView.prototype.setParent = function($parent) { - var self = this; - this.$parent = $parent; - this.$parent.off('.video' + this.id); - this.$parent.on('mouseup.video' + this.id, function (event) { - Handlers.mouseup.call(self, event); - Private.cancelClick.call(self); - }); - this.$parent.on('mousemove.video' + this.id, function (event) { - Handlers.mousemove.call(self, event); - }); - } - - videoView.prototype.setAvatar = function (src) { - this.$avatar.attr('src', src); - this.avatar = src; - } - - videoView.prototype.remove = function () { - this.$container.off(); - if (this.$parent) this.$parent.off('.video' + this.id); - this.$container.remove(); - } - - videoView.prototype.videoOff = function () { - this.$videoControl.addClass('active'); - this.$avatar.show(); - this.videoStatus = false; - } - - videoView.prototype.videoOn = function () { - this.$videoControl.removeClass('active'); - this.$avatar.hide(); - this.videoStatus = true; - } - - videoView.prototype.audioOff = function () { - this.$audioControl.addClass('active'); - this.audioStatus = false; - } - - videoView.prototype.audioOn = function () { - this.$audioControl.removeClass('active'); - this.audioStatus = true; - } - - /** - * @class - * @static - */ - videoView.events = { - mousedown: "VideoView:mousedown", - mouseup: "VideoView:mouseup", - doubleClick: "VideoView:doubleClick", - dragEnd: "VideoView:dragEnd", - audioControlClick: "VideoView:audioControlClick", - videoControlClick: "VideoView:videoControlClick", - }; - - return videoView; -})(); diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 80143f25..355e73f8 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -77,13 +77,13 @@ const Realtime = { var $video = $('<video></video>').attr('id', self.videoId) self.localVideo = { $video: $video, - view: new Views.videoView($video[0], $('body'), 'me', true, { + view: new Views.VideoView($video[0], $('body'), 'me', true, { DOUBLE_CLICK_TOLERANCE: 200, avatar: Active.Mapper ? Active.Mapper.get('image') : '' }) } - self.room = new Views.room({ + self.room = new Views.Room({ webrtc: self.webrtc, socket: self.socket, username: Active.Mapper ? Active.Mapper.get('name') : '', @@ -104,26 +104,26 @@ const Realtime = { addJuntoListeners: function () { var self = Realtime - $(document).on(Views.chatView.events.openTray, function () { + $(document).on(Views.ChatView.events.openTray, function () { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Views.chatView.events.closeTray, function () { + $(document).on(Views.ChatView.events.closeTray, function () { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Views.chatView.events.videosOn, function () { + $(document).on(Views.ChatView.events.videosOn, function () { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Views.chatView.events.videosOff, function () { + $(document).on(Views.ChatView.events.videosOff, function () { $('#wrapper').addClass('hideVideos') }) - $(document).on(Views.chatView.events.cursorsOn, function () { + $(document).on(Views.ChatView.events.cursorsOn, function () { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Views.chatView.events.cursorsOff, function () { + $(document).on(Views.ChatView.events.cursorsOff, function () { $('#wrapper').addClass('hideCursors') }) }, @@ -611,7 +611,7 @@ const Realtime = { var sendNewMessage = function (event, data) { self.sendNewMessage(data) } - $(document).on(Views.room.events.newMessage + '.map', sendNewMessage) + $(document).on(Views.Room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { var self = Realtime diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index d5c07e12..6760edcc 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -49,11 +49,11 @@ const _Router = Backbone.Router.extend({ GlobalUI.showDiv('#explore') - Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) if (Metamaps.Maps.Active.length === 0) { Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Views.exploreMaps.render(navigate) + Views.ExploreMaps.render(navigate) } } else { // logged out home page @@ -108,7 +108,7 @@ const _Router = Backbone.Router.extend({ Metamaps.Maps.Mapper.mapperId = id } - Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { var path = '/explore/' + this.currentPage @@ -130,9 +130,9 @@ const _Router = Backbone.Router.extend({ }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { if (id) { - Views.exploreMaps.fetchUserThenRender(navigateTimeout) + Views.ExploreMaps.fetchUserThenRender(navigateTimeout) } else { - Views.exploreMaps.render(navigateTimeout) + Views.ExploreMaps.render(navigateTimeout) } } diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js deleted file mode 100644 index aee0fdf0..00000000 --- a/frontend/src/Metamaps/Views.js +++ /dev/null @@ -1,91 +0,0 @@ -/* global Metamaps, $ */ - -import Active from './Active' -import ReactComponents from './ReactComponents' -import ReactDOM from 'react-dom' // TODO ensure this isn't a double import - -/* - * Metamaps.Views.js.erb - * - * Dependencies: - * - Metamaps.Loading - */ - -const Views = { - exploreMaps: { - setCollection: function (collection) { - var self = Views.exploreMaps - - if (self.collection) { - self.collection.off('add', self.render) - self.collection.off('successOnFetch', self.handleSuccess) - self.collection.off('errorOnFetch', self.handleError) - } - self.collection = collection - self.collection.on('add', self.render) - self.collection.on('successOnFetch', self.handleSuccess) - self.collection.on('errorOnFetch', self.handleError) - }, - render: function (mapperObj, cb) { - var self = Views.exploreMaps - - if (typeof mapperObj === 'function') { - cb = mapperObj - mapperObj = null - } - - var exploreObj = { - currentUser: Active.Mapper, - section: self.collection.id, - displayStyle: 'grid', - maps: self.collection, - moreToLoad: self.collection.page != 'loadedAll', - user: mapperObj, - loadMore: self.loadMore - } - ReactDOM.render( - React.createElement(ReactComponents.Maps, exploreObj), - document.getElementById('explore') - ) - - if (cb) cb() - Metamaps.Loading.hide() - }, - loadMore: function () { - var self = Views.exploreMaps - - if (self.collection.page != "loadedAll") { - self.collection.getMaps() - } - else self.render() - }, - handleSuccess: function (cb) { - var self = Views.exploreMaps - - if (self.collection && self.collection.id === 'mapper') { - self.fetchUserThenRender(cb) - } else { - self.render(cb) - } - }, - handleError: function () { - console.log('error loading maps!') // TODO - }, - fetchUserThenRender: function (cb) { - var self = Views.exploreMaps - - // first load the mapper object and then call the render function - $.ajax({ - url: '/users/' + self.collection.mapperId + '/details.json', - success: function (response) { - self.render(response, cb) - }, - error: function () { - self.render(cb) - } - }) - } - } -} - -export default Views diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js new file mode 100644 index 00000000..5d8f5f65 --- /dev/null +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -0,0 +1,337 @@ +/* global Autolinker, $ */ +var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); + +var Private = { + messageHTML: "<div class='chat-message'>" + + "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + + "<div class='chat-message-text'>{{ message }}</div>" + + "<div class='chat-message-time'>{{ timestamp }}</div>" + + "<div class='clearfloat'></div>" + + "</div>", + participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + + "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + + "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + + "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + + "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + + "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + + "<div class='clearfloat'></div>" + + "</div>", + templates: function() { + _.templateSettings = { + interpolate: /\{\{(.+?)\}\}/g + }; + this.messageTemplate = _.template(Private.messageHTML); + + this.participantTemplate = _.template(Private.participantHTML); + }, + createElements: function() { + this.$unread = $('<div class="chat-unread"></div>'); + this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); + this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); + this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); + this.$videoToggle = $('<div class="video-toggle"></div>'); + this.$cursorToggle = $('<div class="cursor-toggle"></div>'); + this.$participants = $('<div class="participants"></div>'); + this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); + this.$chatHeader = $('<div class="chat-header">CHAT</div>'); + this.$soundToggle = $('<div class="sound-toggle"></div>'); + this.$messages = $('<div class="chat-messages"></div>'); + this.$container = $('<div class="chat-box"></div>'); + }, + attachElements: function() { + this.$button.append(this.$unread); + + this.$juntoHeader.append(this.$videoToggle); + this.$juntoHeader.append(this.$cursorToggle); + + this.$chatHeader.append(this.$soundToggle); + + this.$participants.append(this.$conversationInProgress); + + this.$container.append(this.$juntoHeader); + this.$container.append(this.$participants); + this.$container.append(this.$chatHeader); + this.$container.append(this.$button); + this.$container.append(this.$messages); + this.$container.append(this.$messageInput); + }, + addEventListeners: function() { + var self = this; + + this.participants.on('add', function (participant) { + Private.addParticipant.call(self, participant); + }); + + this.participants.on('remove', function (participant) { + Private.removeParticipant.call(self, participant); + }); + + this.$button.on('click', function () { + Handlers.buttonClick.call(self); + }); + this.$videoToggle.on('click', function () { + Handlers.videoToggleClick.call(self); + }); + this.$cursorToggle.on('click', function () { + Handlers.cursorToggleClick.call(self); + }); + this.$soundToggle.on('click', function () { + Handlers.soundToggleClick.call(self); + }); + this.$messageInput.on('keyup', function (event) { + Handlers.keyUp.call(self, event); + }); + this.$messageInput.on('focus', function () { + Handlers.inputFocus.call(self); + }); + this.$messageInput.on('blur', function () { + Handlers.inputBlur.call(self); + }); + }, + initializeSounds: function() { + this.sound = new Howl({ + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg'], + sprite: { + joinmap: [0, 561], + leavemap: [1000, 592], + receivechat: [2000, 318], + sendchat: [3000, 296], + sessioninvite: [4000, 5393, true] + } + }); + }, + incrementUnread: function() { + this.unreadMessages++; + this.$unread.html(this.unreadMessages); + this.$unread.show(); + }, + addMessage: function(message, isInitial, wasMe) { + + if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); + + function addZero(i) { + if (i < 10) { + i = "0" + i; + } + return i; + } + var m = _.clone(message.attributes); + + var today = new Date(); + m.timestamp = new Date(m.created_at); + + var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); + date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); + m.timestamp = date; + m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove + m.message = linker.link(m.message); + var $html = $(this.messageTemplate(m)); + this.$messages.append($html); + if (!isInitial) this.scrollMessages(200); + + if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); + }, + initialMessages: function() { + var messages = this.messages.models; + for (var i = 0; i < messages.length; i++) { + Private.addMessage.call(this, messages[i], true); + } + }, + handleInputMessage: function() { + var message = { + message: this.$messageInput.val(), + }; + this.$messageInput.val(''); + $(document).trigger(chatView.events.message + '-' + this.room, [message]); + }, + addParticipant: function(participant) { + var p = _.clone(participant.attributes); + if (p.self) { + p.selfClass = 'is-self'; + p.selfName = '(me)'; + } else { + p.selfClass = ''; + p.selfName = ''; + } + var html = this.participantTemplate(p); + this.$participants.append(html); + }, + removeParticipant: function(participant) { + this.$container.find('.participant-' + participant.get('id')).remove(); + } +}; + +var Handlers = { + buttonClick: function() { + if (this.isOpen) this.close(); + else if (!this.isOpen) this.open(); + }, + videoToggleClick: function() { + this.$videoToggle.toggleClass('active'); + this.videosShowing = !this.videosShowing; + $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); + }, + cursorToggleClick: function() { + this.$cursorToggle.toggleClass('active'); + this.cursorsShowing = !this.cursorsShowing; + $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); + }, + soundToggleClick: function() { + this.alertSound = !this.alertSound; + this.$soundToggle.toggleClass('active'); + }, + keyUp: function(event) { + switch(event.which) { + case 13: // enter + Private.handleInputMessage.call(this); + break; + } + }, + inputFocus: function() { + $(document).trigger(chatView.events.inputFocus); + }, + inputBlur: function() { + $(document).trigger(chatView.events.inputBlur); + } +}; + +const ChatView = function(messages, mapper, room) { + var self = this; + + this.room = room; + this.mapper = mapper; + this.messages = messages; // backbone collection + + this.isOpen = false; + this.alertSound = true; // whether to play sounds on arrival of new messages or not + this.cursorsShowing = true; + this.videosShowing = true; + this.unreadMessages = 0; + this.participants = new Backbone.Collection(); + + Private.templates.call(this); + Private.createElements.call(this); + Private.attachElements.call(this); + Private.addEventListeners.call(this); + Private.initialMessages.call(this); + Private.initializeSounds.call(this); + this.$container.css({ + right: '-300px' + }); +}; + +ChatView.prototype.conversationInProgress = function (participating) { + this.$conversationInProgress.show(); + this.$participants.addClass('is-live'); + if (participating) this.$participants.addClass('is-participating'); + this.$button.addClass('active'); + + // hide invite to call buttons +} + +ChatView.prototype.conversationEnded = function () { + this.$conversationInProgress.hide(); + this.$participants.removeClass('is-live'); + this.$participants.removeClass('is-participating'); + this.$button.removeClass('active'); + this.$participants.find('.participant').removeClass('active'); + this.$participants.find('.participant').removeClass('pending'); +} + +ChatView.prototype.leaveConversation = function () { + this.$participants.removeClass('is-participating'); +} + +ChatView.prototype.mapperJoinedCall = function (id) { + this.$participants.find('.participant-' + id).addClass('active'); +} + +ChatView.prototype.mapperLeftCall = function (id) { + this.$participants.find('.participant-' + id).removeClass('active'); +} + +ChatView.prototype.invitationPending = function (id) { + this.$participants.find('.participant-' + id).addClass('pending'); +} + +ChatView.prototype.invitationAnswered = function (id) { + this.$participants.find('.participant-' + id).removeClass('pending'); +} + +ChatView.prototype.addParticipant = function (participant) { + this.participants.add(participant); +} + +ChatView.prototype.removeParticipant = function (username) { + var p = this.participants.find(function (p) { return p.get('username') === username; }); + if (p) { + this.participants.remove(p); + } +} + +ChatView.prototype.removeParticipants = function () { + this.participants.remove(this.participants.models); +} + +ChatView.prototype.open = function () { + this.$container.css({ + right: '0' + }); + this.$messageInput.focus(); + this.isOpen = true; + this.unreadMessages = 0; + this.$unread.hide(); + this.scrollMessages(0); + $(document).trigger(ChatView.events.openTray); +} + +ChatView.prototype.addMessage = function(message, isInitial, wasMe) { + this.messages.add(message); + Private.addMessage.call(this, message, isInitial, wasMe); +} + +ChatView.prototype.scrollMessages = function(duration) { + duration = duration || 0; + + this.$messages.animate({ + scrollTop: this.$messages[0].scrollHeight + }, duration); +} + +ChatView.prototype.clearMessages = function () { + this.unreadMessages = 0; + this.$unread.hide(); + this.$messages.empty(); +} + +ChatView.prototype.close = function () { + this.$container.css({ + right: '-300px' + }); + this.$messageInput.blur(); + this.isOpen = false; + $(document).trigger(ChatView.events.closeTray); +} + +ChatView.prototype.remove = function () { + this.$button.off(); + this.$container.remove(); +} + +/** + * @class + * @static + */ +ChatView.events = { + message: 'ChatView:message', + openTray: 'ChatView:openTray', + closeTray: 'ChatView:closeTray', + inputFocus: 'ChatView:inputFocus', + inputBlur: 'ChatView:inputBlur', + cursorsOff: 'ChatView:cursorsOff', + cursorsOn: 'ChatView:cursorsOn', + videosOff: 'ChatView:videosOff', + videosOn: 'ChatView:videosOn' +}; + +export default ChatView diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js new file mode 100644 index 00000000..4ffbf9fb --- /dev/null +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -0,0 +1,86 @@ +/* global Metamaps, $ */ + +import Active from './Active' +import ReactComponents from './ReactComponents' +import ReactDOM from 'react-dom' // TODO ensure this isn't a double import + +/* + * - Metamaps.Loading + */ + +const ExploreMaps = { + setCollection: function (collection) { + var self = ExploreMaps + + if (self.collection) { + self.collection.off('add', self.render) + self.collection.off('successOnFetch', self.handleSuccess) + self.collection.off('errorOnFetch', self.handleError) + } + self.collection = collection + self.collection.on('add', self.render) + self.collection.on('successOnFetch', self.handleSuccess) + self.collection.on('errorOnFetch', self.handleError) + }, + render: function (mapperObj, cb) { + var self = ExploreMaps + + if (typeof mapperObj === 'function') { + cb = mapperObj + mapperObj = null + } + + var exploreObj = { + currentUser: Active.Mapper, + section: self.collection.id, + displayStyle: 'grid', + maps: self.collection, + moreToLoad: self.collection.page != 'loadedAll', + user: mapperObj, + loadMore: self.loadMore + } + ReactDOM.render( + React.createElement(ReactComponents.Maps, exploreObj), + document.getElementById('explore') + ) + + if (cb) cb() + Metamaps.Loading.hide() + }, + loadMore: function () { + var self = ExploreMaps + + if (self.collection.page != "loadedAll") { + self.collection.getMaps() + } + else self.render() + }, + handleSuccess: function (cb) { + var self = ExploreMaps + + if (self.collection && self.collection.id === 'mapper') { + self.fetchUserThenRender(cb) + } else { + self.render(cb) + } + }, + handleError: function () { + console.log('error loading maps!') // TODO + }, + fetchUserThenRender: function (cb) { + var self = ExploreMaps + + // first load the mapper object and then call the render function + $.ajax({ + url: '/users/' + self.collection.mapperId + '/details.json', + success: function (response) { + self.render(response, cb) + }, + error: function () { + self.render(cb) + } + }) + } +} + +export default ExploreMaps diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js new file mode 100644 index 00000000..014df61b --- /dev/null +++ b/frontend/src/Metamaps/Views/Room.js @@ -0,0 +1,198 @@ +/* global Metamaps, $ */ +import Active from '../Active' +import Realtime from '../Realtime' + +import ChatView from './ChatView' +import VideoView from './VideoView' + +/* + * Metamaps.Backbone + */ + +const Room = function(opts) { + var self = this + + this.isActiveRoom = false + this.socket = opts.socket + this.webrtc = opts.webrtc + //this.roomRef = opts.firebase + this.room = opts.room + this.config = opts.config + this.peopleCount = 0 + + this.$myVideo = opts.$video + this.myVideo = opts.myVideoView + + this.messages = new Backbone.Collection() + this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }) + this.chat = new ChatView(this.messages, this.currentMapper, this.room) + + this.videos = {} + + this.init() +} + +Room.prototype.join = function(cb) { + this.isActiveRoom = true + this.webrtc.joinRoom(this.room, cb) + this.chat.conversationInProgress(true) // true indicates participation +} + +Room.prototype.conversationInProgress = function() { + this.chat.conversationInProgress(false) // false indicates not participating +} + +Room.prototype.conversationEnding = function() { + this.chat.conversationEnded() +} + +Room.prototype.leaveVideoOnly = function() { + this.chat.leaveConversation() // the conversation will carry on without you + for (var id in this.videos) { + this.removeVideo(id) + } + this.isActiveRoom = false + this.webrtc.leaveRoom() +} + +Room.prototype.leave = function() { + for (var id in this.videos) { + this.removeVideo(id) + } + this.isActiveRoom = false + this.webrtc.leaveRoom() + this.chat.conversationEnded() + this.chat.removeParticipants() + this.chat.clearMessages() + this.messages.reset() +} + +Room.prototype.setPeopleCount = function(count) { + this.peopleCount = count +} + +Room.prototype.init = function () { + var self = this + + $(document).on(VideoView.events.audioControlClick, function (event, videoView) { + if (!videoView.audioStatus) self.webrtc.mute() + else if (videoView.audioStatus) self.webrtc.unmute() + }) + $(document).on(VideoView.events.videoControlClick, function (event, videoView) { + if (!videoView.videoStatus) self.webrtc.pauseVideo() + else if (videoView.videoStatus) self.webrtc.resumeVideo() + }) + + this.webrtc.webrtc.off('peerStreamAdded') + this.webrtc.webrtc.off('peerStreamRemoved') + this.webrtc.on('peerStreamAdded', function (peer) { + var mapper = Realtime.mappersOnMap[peer.nick] + peer.avatar = mapper.image + peer.username = mapper.name + if (self.isActiveRoom) { + self.addVideo(peer) + } + }) + + this.webrtc.on('peerStreamRemoved', function (peer) { + if (self.isActiveRoom) { + self.removeVideo(peer) + } + }) + + this.webrtc.on('mute', function (data) { + var v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = false + } + else if (data.name === 'video') { + v.videoStatus = false + v.$avatar.show() + } + if (!v.audioStatus && !v.videoStatus) v.$container.hide() + }) + this.webrtc.on('unmute', function (data) { + var v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = true + } + else if (data.name === 'video') { + v.videoStatus = true + v.$avatar.hide() + } + v.$container.show() + }) + + var sendChatMessage = function (event, data) { + self.sendChatMessage(data) + } + $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage) + } + + Room.prototype.videoAdded = function (callback) { + this._videoAdded = callback + } + + Room.prototype.addVideo = function (peer) { + var + id = this.webrtc.getDomId(peer), + video = attachMediaStream(peer.stream) + + var + v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }) + + this.videos[peer.id] = v + if (this._videoAdded) this._videoAdded(v, peer.nick) + } + + Room.prototype.removeVideo = function (peer) { + var id = typeof peer == 'string' ? peer : peer.id + if (this.videos[id]) { + this.videos[id].remove() + delete this.videos[id] + } + } + + Room.prototype.sendChatMessage = function (data) { + var self = this + //this.roomRef.child('messages').push(data) + if (self.chat.alertSound) self.chat.sound.play('sendchat') + var m = new Metamaps.Backbone.Message({ + message: data.message, + resource_id: Active.Map.id, + resource_type: "Map" + }) + m.save(null, { + success: function (model, response) { + self.addMessages(new Metamaps.Backbone.MessageCollection(model), false, true) + $(document).trigger(Room.events.newMessage, [model]) + }, + error: function (model, response) { + console.log('error!', response) + } + }) + } + + // they should be instantiated as backbone models before they get + // passed to this function + Room.prototype.addMessages = function (messages, isInitial, wasMe) { + var self = this + + messages.models.forEach(function (message) { + self.chat.addMessage(message, isInitial, wasMe) + }) + } + +/** + * @class + * @static + */ +Room.events = { + newMessage: "Room:newMessage" +} + +export default Room diff --git a/frontend/src/Metamaps/Views/VideoView.js b/frontend/src/Metamaps/Views/VideoView.js new file mode 100644 index 00000000..401ece54 --- /dev/null +++ b/frontend/src/Metamaps/Views/VideoView.js @@ -0,0 +1,202 @@ +/* global $ */ + +var Private = { + addControls: function() { + var self = this; + + this.$audioControl = $('<div class="video-audio"></div>'); + this.$videoControl = $('<div class="video-video"></div>'); + + this.$audioControl.on('click', function () { + Handlers.audioControlClick.call(self); + }); + + this.$videoControl.on('click', function () { + Handlers.videoControlClick.call(self); + }); + + this.$container.append(this.$audioControl); + this.$container.append(this.$videoControl); + }, + cancelClick: function() { + this.mouseIsDown = false; + + if (this.hasMoved) { + + } + + $(document).trigger(VideoView.events.dragEnd); + } +}; + +var Handlers = { + mousedown: function(event) { + this.mouseIsDown = true; + this.hasMoved = false; + this.mouseMoveStart = { + x: event.pageX, + y: event.pageY + }; + this.posStart = { + x: parseInt(this.$container.css('left'), '10'), + y: parseInt(this.$container.css('top'), '10') + } + + $(document).trigger(VideoView.events.mousedown); + }, + mouseup: function(event) { + $(document).trigger(VideoView.events.mouseup, [this]); + + var storedTime = this.lastClick; + var now = Date.now(); + this.lastClick = now; + + if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { + $(document).trigger(VideoView.events.doubleClick, [this]); + } + }, + mousemove: function(event) { + var + diffX, + diffY, + newX, + newY; + + if (this.$parent && this.mouseIsDown) { + this.manuallyPositioned = true; + this.hasMoved = true; + diffX = event.pageX - this.mouseMoveStart.x; + diffY = this.mouseMoveStart.y - event.pageY; + newX = this.posStart.x + diffX; + newY = this.posStart.y - diffY; + this.$container.css({ + top: newY, + left: newX + }); + } + }, + audioControlClick: function() { + if (this.audioStatus) { + this.audioOff(); + } else { + this.audioOn(); + } + $(document).trigger(VideoView.events.audioControlClick, [this]); + }, + videoControlClick: function() { + if (this.videoStatus) { + this.videoOff(); + } else { + this.videoOn(); + } + $(document).trigger(VideoView.events.videoControlClick, [this]); + }, +}; + +var VideoView = function(video, $parent, id, isMyself, config) { + var self = this; + + this.$parent = $parent; // mapView + + this.video = video; + this.id = id; + + this.config = config; + + this.mouseIsDown = false; + this.mouseDownOffset = { x: 0, y: 0 }; + this.lastClick = null; + this.hasMoved = false; + + this.audioStatus = true; + this.videoStatus = true; + + this.$container = $('<div></div>'); + this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')); + this.$container.attr('id', 'container_' + id); + + + var $vidContainer = $('<div></div>'); + $vidContainer.addClass('video-cutoff'); + $vidContainer.append(this.video); + + this.avatar = config.avatar; + this.$avatar = $('<img draggable="false" class="collaborator-video-avatar" src="' + config.avatar + '" width="150" height="150" />'); + $vidContainer.append(this.$avatar); + + this.$container.append($vidContainer); + + this.$container.on('mousedown', function (event) { + Handlers.mousedown.call(self, event); + }); + + if (isMyself) { + Private.addControls.call(this); + } + + // suppress contextmenu + this.video.oncontextmenu = function () { return false; }; + + if (this.$parent) this.setParent(this.$parent); +}; + +VideoView.prototype.setParent = function($parent) { + var self = this; + this.$parent = $parent; + this.$parent.off('.video' + this.id); + this.$parent.on('mouseup.video' + this.id, function (event) { + Handlers.mouseup.call(self, event); + Private.cancelClick.call(self); + }); + this.$parent.on('mousemove.video' + this.id, function (event) { + Handlers.mousemove.call(self, event); + }); +} + +VideoView.prototype.setAvatar = function (src) { + this.$avatar.attr('src', src); + this.avatar = src; +} + +VideoView.prototype.remove = function () { + this.$container.off(); + if (this.$parent) this.$parent.off('.video' + this.id); + this.$container.remove(); +} + +VideoView.prototype.videoOff = function () { + this.$videoControl.addClass('active'); + this.$avatar.show(); + this.videoStatus = false; +} + +VideoView.prototype.videoOn = function () { + this.$videoControl.removeClass('active'); + this.$avatar.hide(); + this.videoStatus = true; +} + +VideoView.prototype.audioOff = function () { + this.$audioControl.addClass('active'); + this.audioStatus = false; +} + +VideoView.prototype.audioOn = function () { + this.$audioControl.removeClass('active'); + this.audioStatus = true; +} + +/** + * @class + * @static + */ +VideoView.events = { + mousedown: "VideoView:mousedown", + mouseup: "VideoView:mouseup", + doubleClick: "VideoView:doubleClick", + dragEnd: "VideoView:dragEnd", + audioControlClick: "VideoView:audioControlClick", + videoControlClick: "VideoView:videoControlClick", +}; + +export default VideoView diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js new file mode 100644 index 00000000..ca0e751a --- /dev/null +++ b/frontend/src/Metamaps/Views/index.js @@ -0,0 +1,6 @@ +import ExploreMaps from './ExploreMaps' +import ChatView from './ChatView' +import VideoView from './VideoView' +import Room from './Room' + +export ExploreMaps, ChatView, VideoView, Room diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 7b431d1f..45283c89 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -28,7 +28,7 @@ import SynapseCard from './SynapseCard' import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' -import Views from './Views' +import * as Views from './Views' import Visualize from './Visualize' import ReactComponents from './ReactComponents' @@ -83,18 +83,18 @@ document.addEventListener("DOMContentLoaded", function() { if (Metamaps.currentSection === "explore") { const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + Metamaps.Views.ExploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { - Views.exploreMaps.fetchUserThenRender() + Views.ExploreMaps.fetchUserThenRender() } else { - Views.exploreMaps.render() + Views.ExploreMaps.render() } GlobalUI.showDiv('#explore') } else if (Metamaps.currentSection === "" && Active.Mapper) { - Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Views.exploreMaps.render() + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.render() GlobalUI.showDiv('#explore') } else if (Active.Map || Active.Topic) { From a996734c793aebd75eea8031e4a328f3e5a5a2b5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:16:15 +0800 Subject: [PATCH 027/378] remove Backbone from window --- .../src/Metamaps/{Backbone.js => Backbone/index.js} | 4 +++- frontend/src/Metamaps/Router.js | 6 +++++- frontend/src/Metamaps/Views/ChatView.js | 5 +++++ frontend/src/Metamaps/Views/Room.js | 8 +++++++- frontend/src/index.js | 11 ++--------- 5 files changed, 22 insertions(+), 12 deletions(-) rename frontend/src/Metamaps/{Backbone.js => Backbone/index.js} (99%) diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone/index.js similarity index 99% rename from frontend/src/Metamaps/Backbone.js rename to frontend/src/Metamaps/Backbone/index.js index bc303df4..7e6878b3 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -1,6 +1,8 @@ -/* global Metamaps, Backbone, _, $ */ +/* global Metamaps, Backbone, $ */ import _ from 'lodash' +import Backbone from 'backbone' +Backbone.$ = window.$ /* * Metamaps.Backbone.js.erb diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 6760edcc..0ad88efd 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,4 +1,8 @@ -/* global Metamaps, Backbone, $ */ +/* global Metamaps, $ */ + +import Backbone from 'backbone' +//TODO is this line good or bad? +//Backbone.$ = window.$ import Active from './Active' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 5d8f5f65..d1efdf74 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,4 +1,9 @@ /* global Autolinker, $ */ + +import Backbone from 'backbone' +// TODO is this line good or bad +// Backbone.$ = window.$ + var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); var Private = { diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index 014df61b..5b70ee7c 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -1,4 +1,9 @@ /* global Metamaps, $ */ + +import Backbone from 'backbone' +// TODO is this line good or bad +// Backbone.$ = window.$ + import Active from '../Active' import Realtime from '../Realtime' @@ -6,7 +11,8 @@ import ChatView from './ChatView' import VideoView from './VideoView' /* - * Metamaps.Backbone + * Dependencies: + * Metamaps.Backbone */ const Room = function(opts) { diff --git a/frontend/src/index.js b/frontend/src/index.js index e5705512..176ac329 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,15 +1,8 @@ -import React from 'react' +// create global references to some utility libraries import ReactDOM from 'react-dom' -import Backbone from 'backbone' import _ from 'underscore' - -import Metamaps from './Metamaps' - -// create global references to some libraries -window.React = React window.ReactDOM = ReactDOM -Backbone.$ = window.$ // jquery from rails -window.Backbone = Backbone window._ = _ +import Metamaps from './Metamaps' window.Metamaps = Metamaps From 30fc9438331620b9b27a0edf897581ccbf51e19a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:20:05 +0800 Subject: [PATCH 028/378] clean up backbone file imports --- frontend/src/Metamaps/Backbone/index.js | 141 ++++++++++++------------ 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 7e6878b3..2c7ae530 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -4,29 +4,30 @@ import _ from 'lodash' import Backbone from 'backbone' Backbone.$ = window.$ +import Active from '../Active' +import Filter from '../Filter' +import JIT from '../JIT' +import Map, { InfoBox } from '../Map' +import Mapper from '../Mapper' +import Realtime from '../Realtime' +import Synapse from '../Synapse' +import SynapseCard from '../SynapseCard' +import Topic from '../Topic' +import TopicCard from '../TopicCard' +import Visualize from '../Visualize' + /* * Metamaps.Backbone.js.erb * * Dependencies: - * - Metamaps.Active * - Metamaps.Collaborators * - Metamaps.Creators - * - Metamaps.Filter - * - Metamaps.JIT * - Metamaps.Loading - * - Metamaps.Map - * - Metamaps.Mapper * - Metamaps.Mappers * - Metamaps.Mappings * - Metamaps.Metacodes - * - Metamaps.Realtime - * - Metamaps.Synapse - * - Metamaps.SynapseCard * - Metamaps.Synapses - * - Metamaps.Topic - * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Visualize */ const _Backbone = {} @@ -62,7 +63,7 @@ _Backbone.Map = Backbone.Model.extend({ this.on('saved', this.savedEvent) }, savedEvent: function () { - Metamaps.Realtime.sendMapChange(this) + Realtime.sendMapChange(this) }, authorizeToEdit: function (mapper) { if (mapper && ( @@ -82,7 +83,7 @@ _Backbone.Map = Backbone.Model.extend({ } }, getUser: function () { - return Metamaps.Mapper.get(this.get('user_id')) + return Mapper.get(this.get('user_id')) }, fetchContained: function () { var bb = _Backbone @@ -126,10 +127,10 @@ _Backbone.Map = Backbone.Model.extend({ return this.get('mappers') }, updateView: function () { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = this.id === map.id if (isActiveMap) { - Metamaps.Map.InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) + InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) this.updateMapWrapper() // mobile menu $('#header_content').html(this.get('name')) @@ -137,9 +138,9 @@ _Backbone.Map = Backbone.Model.extend({ } }, updateMapWrapper: function () { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = this.id === map.id - var authorized = map && map.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEditMap' : '' + var authorized = map && map.authorizeToEdit(Active.Mapper) ? 'canEditMap' : '' var commonsMap = map && map.get('permission') === 'commons' ? 'commonsMap' : '' if (isActiveMap) { $('.wrapper').removeClass('canEditMap commonsMap').addClass(authorized + ' ' + commonsMap) @@ -324,10 +325,10 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, + 'user_id': Active.Mapper.id, 'desc': this.get('desc') || '', 'link': this.get('link') || '', - 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' + 'permission': Active.Map ? Active.Map.get('permission') : 'commons' }) } @@ -339,7 +340,7 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]) + $(document).trigger(JIT.events.removeTopic, [removeTopicData]) }) this.on('noLongerPrivate', function () { var newTopicData = { @@ -347,10 +348,10 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + $(document).trigger(JIT.events.newTopic, [newTopicData]) }) - this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this) + this.on('change:metacode_id', Filter.checkMetacodes, this) }, authorizeToEdit: function (mapper) { if (mapper && @@ -371,10 +372,10 @@ _Backbone.init = function () { return Metamaps.Metacodes.get(this.get('metacode_id')) }, getMapping: function () { - if (!Metamaps.Active.Map) return false + if (!Active.Map) return false return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, + map_id: Active.Map.id, mappable_type: 'Topic', mappable_id: this.isNew() ? this.cid : this.id }) @@ -387,7 +388,7 @@ _Backbone.init = function () { name: this.get('name') } - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() node.data = { $mapping: null, @@ -402,7 +403,7 @@ _Backbone.init = function () { var node = this.get('node') node.setData('topic', this) - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() node.setData('mapping', mapping) } @@ -410,38 +411,38 @@ _Backbone.init = function () { return node }, savedEvent: function () { - Metamaps.Realtime.sendTopicChange(this) + Realtime.sendTopicChange(this) }, updateViews: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node) + if (onPageWithTopicCard && this == TopicCard.openTopicCard) { + TopicCard.showCard(node) } // update the node on the map if (onPageWithTopicCard && node) { node.name = this.get('name') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } }, updateCardView: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node) + if (onPageWithTopicCard && this == TopicCard.openTopicCard) { + TopicCard.showCard(node) } }, updateNodeView: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update the node on the map if (onPageWithTopicCard && node) { node.name = this.get('name') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } } }) @@ -489,8 +490,8 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, - 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', + 'user_id': Active.Mapper.id, + 'permission': Active.Map ? Active.Map.get('permission') : 'commons', 'category': 'from-to' }) } @@ -504,15 +505,15 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + $(document).trigger(JIT.events.newSynapse, [newSynapseData]) }) this.on('nowPrivate', function () { - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + $(document).trigger(JIT.events.removeSynapse, [{ mappableid: this.id }]) }) - this.on('change:desc', Metamaps.Filter.checkSynapses, this) + this.on('change:desc', Filter.checkSynapses, this) }, prepareLiForFilter: function () { var li = '' @@ -546,10 +547,10 @@ _Backbone.init = function () { ] : false }, getMapping: function () { - if (!Metamaps.Active.Map) return false + if (!Active.Map) return false return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, + map_id: Active.Map.id, mappable_type: 'Synapse', mappable_id: this.isNew() ? this.cid : this.id }) @@ -567,7 +568,7 @@ _Backbone.init = function () { } } - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = providedMapping || this.getMapping() mappingID = mapping.isNew() ? mapping.cid : mapping.id edge.data.$mappings = [] @@ -581,7 +582,7 @@ _Backbone.init = function () { var edge = this.get('edge') edge.getData('synapses').push(this) - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() edge.getData('mappings').push(mapping) } @@ -589,28 +590,28 @@ _Backbone.init = function () { return edge }, savedEvent: function () { - Metamaps.Realtime.sendSynapseChange(this) + Realtime.sendSynapseChange(this) }, updateViews: function () { this.updateCardView() this.updateEdgeView() }, updateCardView: function () { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') // update synapse card, if this synapse is the one open there - if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { - Metamaps.SynapseCard.showCard(edge) + if (onPageWithSynapseCard && edge == SynapseCard.openSynapseCard) { + SynapseCard.showCard(edge) } }, updateEdgeView: function () { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') // update the edge on the map if (onPageWithSynapseCard && edge) { - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } } }) @@ -629,20 +630,20 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, - 'map_id': Metamaps.Active.Map ? Metamaps.Active.Map.id : null + 'user_id': Active.Mapper.id, + 'map_id': Active.Map ? Active.Map.id : null }) } }, getMap: function () { - return Metamaps.Map.get(this.get('map_id')) + return Map.get(this.get('map_id')) }, getTopic: function () { - if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')) + if (this.get('mappable_type') === 'Topic') return Topic.get(this.get('mappable_id')) else return false }, getSynapse: function () { - if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')) + if (this.get('mappable_type') === 'Synapse') return Synapse.get(this.get('mappable_id')) else return false } }) @@ -665,34 +666,34 @@ _Backbone.init = function () { // this is for topic view Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection() - if (Metamaps.Active.Map) { + if (Active.Map) { Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection() - Metamaps.Active.Map = new self.Map(Metamaps.Active.Map) + Active.Map = new self.Map(Active.Map) } - if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic) + if (Active.Topic) Active.Topic = new self.Topic(Active.Topic) // attach collection event listeners self.attachCollectionEvents = function () { Metamaps.Topics.on('add remove', function (topic) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkMetacodes() + Filter.checkMappers() }) Metamaps.Synapses.on('add remove', function (synapse) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkSynapses() + Filter.checkMappers() }) - if (Metamaps.Active.Map) { + if (Active.Map) { Metamaps.Mappings.on('add remove', function (mapping) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkSynapses() + Filter.checkMetacodes() + Filter.checkMappers() }) } } From 73e7c38873c21551f718478eedf8a40a81a831fc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 08:05:26 +0800 Subject: [PATCH 029/378] syntax fixes --- frontend/src/Metamaps/Map/InfoBox.js | 2 +- frontend/src/Metamaps/Map/index.js | 26 +++++++++++----------- frontend/src/Metamaps/Views/ChatView.js | 2 +- frontend/src/Metamaps/Views/ExploreMaps.js | 5 +++-- frontend/src/Metamaps/Views/index.js | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index eaceba29..a2cc5de2 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,6 +1,6 @@ /* global Metamaps, $ */ -import Active from './Active' +import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 84ee8b39..3dd1c531 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,17 +1,17 @@ /* global Metamaps, $ */ -import Active from './Active' -import AutoLayout from './AutoLayout' -import Create from './Create' -import Filter from './Filter' -import GlobalUI from './GlobalUI' -import JIT from './JIT' -import Realtime from './Realtime' -import Router from './Router' -import Selected from './Selected' -import SynapseCard from './SynapseCard' -import TopicCard from './TopicCard' -import Visualize from './Visualize' +import Active from '../Active' +import AutoLayout from '../AutoLayout' +import Create from '../Create' +import Filter from '../Filter' +import GlobalUI from '../GlobalUI' +import JIT from '../JIT' +import Realtime from '../Realtime' +import Router from '../Router' +import Selected from '../Selected' +import SynapseCard from '../SynapseCard' +import TopicCard from '../TopicCard' +import Visualize from '../Visualize' import CheatSheet from './CheatSheet' import InfoBox from './InfoBox' @@ -361,5 +361,5 @@ const Map = { } } -export CheatSheet, InfoBox +export { CheatSheet, InfoBox } export default Map diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index d1efdf74..9f800c4e 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -95,7 +95,7 @@ var Private = { }, initializeSounds: function() { this.sound = new Howl({ - urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg'], + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], sprite: { joinmap: [0, 561], leavemap: [1000, 592], diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 4ffbf9fb..d8ba5360 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,9 +1,10 @@ /* global Metamaps, $ */ -import Active from './Active' -import ReactComponents from './ReactComponents' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import +import Active from '../Active' +import ReactComponents from '../ReactComponents' + /* * - Metamaps.Loading */ diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index ca0e751a..9663ba98 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -3,4 +3,4 @@ import ChatView from './ChatView' import VideoView from './VideoView' import Room from './Room' -export ExploreMaps, ChatView, VideoView, Room +export { ExploreMaps, ChatView, VideoView, Room } From f59a5775ae6136a2bb274170204176da018e325e Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 20:16:18 -0400 Subject: [PATCH 030/378] tweaks to import/exports --- app/assets/javascripts/application.js | 3 --- frontend/src/Metamaps/Views/ExploreMaps.js | 1 + frontend/src/Metamaps/Views/index.js | 3 ++- frontend/src/Metamaps/index.js | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 03dac4fb..68f2179b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,7 +17,4 @@ //= require ./src/JIT //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle -//= require ./src/views/chatView -//= require ./src/views/videoView -//= require ./src/views/room //= require ./src/check-canvas-support diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index d8ba5360..155e8453 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,5 +1,6 @@ /* global Metamaps, $ */ +import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index 9663ba98..d13482d0 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -3,4 +3,5 @@ import ChatView from './ChatView' import VideoView from './VideoView' import Room from './Room' -export { ExploreMaps, ChatView, VideoView, Room } +const Views = { ExploreMaps, ChatView, VideoView, Room } +export default Views diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 45283c89..5d15559c 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -28,7 +28,7 @@ import SynapseCard from './SynapseCard' import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' -import * as Views from './Views' +import Views from './Views' import Visualize from './Visualize' import ReactComponents from './ReactComponents' From 499593fc82f7c8338ad2c9dbead7511829f8aef4 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 21:40:49 -0400 Subject: [PATCH 031/378] fixing references --- frontend/src/Metamaps/Router.js | 20 +++++++++++--------- frontend/src/Metamaps/Views/ChatView.js | 10 +++++----- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 0ad88efd..c5f1c9a7 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -21,6 +21,9 @@ import Visualize from './Visualize' */ const _Router = Backbone.Router.extend({ + currentPage: '', + currentSection: '', + timeoutId: undefined, routes: { '': 'home', // #home 'explore/:section': 'explore', // #explore/active @@ -28,6 +31,7 @@ const _Router = Backbone.Router.extend({ 'maps/:id': 'maps' // #maps/7 }, home: function () { + let self = this clearTimeout(this.timeoutId) if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps' @@ -41,8 +45,8 @@ const _Router = Backbone.Router.extend({ $('.wrapper').addClass(classes) var navigate = function () { - this.timeoutId = setTimeout(function () { - this.navigate('') + self.timeoutId = setTimeout(function () { + self.navigate('') }, 300) } @@ -74,6 +78,7 @@ const _Router = Backbone.Router.extend({ Active.Topic = null }, explore: function (section, id) { + var self = this clearTimeout(this.timeoutId) // just capitalize the variable section @@ -115,17 +120,17 @@ const _Router = Backbone.Router.extend({ Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { - var path = '/explore/' + this.currentPage + var path = '/explore/' + self.currentPage // alter url if for mapper profile page - if (this.currentPage === 'mapper') { + if (self.currentPage === 'mapper') { path += '/' + Metamaps.Maps.Mapper.mapperId } - this.navigate(path) + self.navigate(path) } var navigateTimeout = function () { - this.timeoutId = setTimeout(navigate, 300) + self.timeoutId = setTimeout(navigate, 300) } if (Metamaps.Maps[capitalize].length === 0) { Metamaps.Loading.show() @@ -209,9 +214,6 @@ const _Router = Backbone.Router.extend({ }) const Router = new _Router() -Router.currentPage = '' -Router.currentSection = undefined -Router.timeoutId = undefined Router.intercept = function (evt) { var segments diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 9f800c4e..cdcda4e5 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -147,7 +147,7 @@ var Private = { message: this.$messageInput.val(), }; this.$messageInput.val(''); - $(document).trigger(chatView.events.message + '-' + this.room, [message]); + $(document).trigger(ChatView.events.message + '-' + this.room, [message]); }, addParticipant: function(participant) { var p = _.clone(participant.attributes); @@ -174,12 +174,12 @@ var Handlers = { videoToggleClick: function() { this.$videoToggle.toggleClass('active'); this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); + $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff); }, cursorToggleClick: function() { this.$cursorToggle.toggleClass('active'); this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); + $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff); }, soundToggleClick: function() { this.alertSound = !this.alertSound; @@ -193,10 +193,10 @@ var Handlers = { } }, inputFocus: function() { - $(document).trigger(chatView.events.inputFocus); + $(document).trigger(ChatView.events.inputFocus); }, inputBlur: function() { - $(document).trigger(chatView.events.inputBlur); + $(document).trigger(ChatView.events.inputBlur); } }; From 07e4ac386530694ff2b185bb9231dd3893ad76c3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 10:37:59 +0800 Subject: [PATCH 032/378] attempt to get npm testing working; fail --- .travis.yml | 2 +- app/assets/javascripts/lib/Autolinker.js | 2756 ----------------- .../javascripts/src/{JIT.js.erb => JIT.js} | 2 +- frontend/src/Metamaps/JIT.js | 4 +- frontend/src/Metamaps/Views/ChatView.js | 5 +- frontend/test/Metamaps.Import.spec.js | 6 +- package.json | 2 + 7 files changed, 15 insertions(+), 2762 deletions(-) delete mode 100644 app/assets/javascripts/lib/Autolinker.js rename app/assets/javascripts/src/{JIT.js.erb => JIT.js} (99%) diff --git a/.travis.yml b/.travis.yml index 28559996..99c917c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,4 +18,4 @@ before_script: - nvm use stable - npm install script: - - bundle exec rspec && npm test && bundle exec brakeman -q -z + - bundle exec rspec && bundle exec brakeman -q -z || npm test diff --git a/app/assets/javascripts/lib/Autolinker.js b/app/assets/javascripts/lib/Autolinker.js deleted file mode 100644 index 6f363d4c..00000000 --- a/app/assets/javascripts/lib/Autolinker.js +++ /dev/null @@ -1,2756 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (root['Autolinker'] = factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - root['Autolinker'] = factory(); - } -}(this, function () { - -/*! - * Autolinker.js - * 0.17.1 - * - * Copyright(c) 2015 Gregory Jacobs <greg@greg-jacobs.com> - * MIT Licensed. http://www.opensource.org/licenses/mit-license.php - * - * https://github.com/gregjacobs/Autolinker.js - */ -/** - * @class Autolinker - * @extends Object - * - * Utility class used to process a given string of text, and wrap the matches in - * the appropriate anchor (<a>) tags to turn them into links. - * - * Any of the configuration options may be provided in an Object (map) provided - * to the Autolinker constructor, which will configure how the {@link #link link()} - * method will process the links. - * - * For example: - * - * var autolinker = new Autolinker( { - * newWindow : false, - * truncate : 30 - * } ); - * - * var html = autolinker.link( "Joe went to www.yahoo.com" ); - * // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' - * - * - * The {@link #static-link static link()} method may also be used to inline options into a single call, which may - * be more convenient for one-off uses. For example: - * - * var html = Autolinker.link( "Joe went to www.yahoo.com", { - * newWindow : false, - * truncate : 30 - * } ); - * // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' - * - * - * ## Custom Replacements of Links - * - * If the configuration options do not provide enough flexibility, a {@link #replaceFn} - * may be provided to fully customize the output of Autolinker. This function is - * called once for each URL/Email/Phone#/Twitter Handle/Hashtag match that is - * encountered. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, Phone #s, Twitter Handles, and Hashtags - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( autolinker, match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes - * tag.setAttr( 'rel', 'nofollow' ); - * tag.addClass( 'external-link' ); - * - * return tag; - * - * } else { - * return true; // let Autolinker perform its normal anchor tag replacement - * } - * - * case 'email' : - * var email = match.getEmail(); - * console.log( "email: ", email ); - * - * if( email === "my@own.address" ) { - * return false; // don't auto-link this particular email address; leave as-is - * } else { - * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) - * } - * - * case 'phone' : - * var phoneNumber = match.getPhoneNumber(); - * console.log( phoneNumber ); - * - * return '<a href="http://newplace.to.link.phone.numbers.to/">' + phoneNumber + '</a>'; - * - * case 'twitter' : - * var twitterHandle = match.getTwitterHandle(); - * console.log( twitterHandle ); - * - * return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>'; - * - * case 'hashtag' : - * var hashtag = match.getHashtag(); - * console.log( hashtag ); - * - * return '<a href="http://newplace.to.link.hashtag.handles.to/">' + hashtag + '</a>'; - * } - * } - * } ); - * - * - * The function may return the following values: - * - * - `true` (Boolean): Allow Autolinker to replace the match as it normally would. - * - `false` (Boolean): Do not replace the current match at all - leave as-is. - * - Any String: If a string is returned from the function, the string will be used directly as the replacement HTML for - * the match. - * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify an HTML tag before writing out its HTML text. - * - * @constructor - * @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). - */ -var Autolinker = function( cfg ) { - Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. - - // Validate the value of the `hashtag` cfg. - var hashtag = this.hashtag; - if( hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' ) { - throw new Error( "invalid `hashtag` cfg - see docs" ); - } -}; - -Autolinker.prototype = { - constructor : Autolinker, // fix constructor property - - /** - * @cfg {Boolean} urls - * - * `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. - */ - urls : true, - - /** - * @cfg {Boolean} email - * - * `true` if email addresses should be automatically linked, `false` if they should not be. - */ - email : true, - - /** - * @cfg {Boolean} twitter - * - * `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. - */ - twitter : true, - - /** - * @cfg {Boolean} phone - * - * `true` if Phone numbers ("(555)555-5555") should be automatically linked, `false` if they should not be. - */ - phone: true, - - /** - * @cfg {Boolean/String} hashtag - * - * A string for the service name to have hashtags (ex: "#myHashtag") - * auto-linked to. The currently-supported values are: - * - * - 'twitter' - * - 'facebook' - * - * Pass `false` to skip auto-linking of hashtags. - */ - hashtag : false, - - /** - * @cfg {Boolean} newWindow - * - * `true` if the links should open in a new window, `false` otherwise. - */ - newWindow : true, - - /** - * @cfg {Boolean} stripPrefix - * - * `true` if 'http://' or 'https://' and/or the 'www.' should be stripped - * from the beginning of URL links' text, `false` otherwise. - */ - stripPrefix : true, - - /** - * @cfg {Number} truncate - * - * A number for how many characters long matched text should be truncated to inside the text of - * a link. If the matched text is over this number of characters, it will be truncated to this length by - * adding a two period ellipsis ('..') to the end of the string. - * - * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look - * something like this: 'yahoo.com/some/long/pat..' - */ - truncate : undefined, - - /** - * @cfg {String} className - * - * A CSS class name to add to the generated links. This class will be added to all links, as well as this class - * plus match suffixes for styling url/email/phone/twitter/hashtag links differently. - * - * For example, if this config is provided as "myLink", then: - * - * - URL links will have the CSS classes: "myLink myLink-url" - * - Email links will have the CSS classes: "myLink myLink-email", and - * - Twitter links will have the CSS classes: "myLink myLink-twitter" - * - Phone links will have the CSS classes: "myLink myLink-phone" - * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" - */ - className : "", - - /** - * @cfg {Function} replaceFn - * - * A function to individually process each match found in the input string. - * - * See the class's description for usage. - * - * This function is called with the following parameters: - * - * @cfg {Autolinker} replaceFn.autolinker The Autolinker instance, which may be used to retrieve child objects from (such - * as the instance's {@link #getTagBuilder tag builder}). - * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which can be used to retrieve information about the - * match that the `replaceFn` is currently processing. See {@link Autolinker.match.Match} subclasses for details. - */ - - - /** - * @private - * @property {Autolinker.htmlParser.HtmlParser} htmlParser - * - * The HtmlParser instance used to skip over HTML tags, while finding text nodes to process. This is lazily instantiated - * in the {@link #getHtmlParser} method. - */ - htmlParser : undefined, - - /** - * @private - * @property {Autolinker.matchParser.MatchParser} matchParser - * - * The MatchParser instance used to find matches in the text nodes of an input string passed to - * {@link #link}. This is lazily instantiated in the {@link #getMatchParser} method. - */ - matchParser : undefined, - - /** - * @private - * @property {Autolinker.AnchorTagBuilder} tagBuilder - * - * The AnchorTagBuilder instance used to build match replacement anchor tags. Note: this is lazily instantiated - * in the {@link #getTagBuilder} method. - */ - tagBuilder : undefined, - - /** - * Automatically links URLs, Email addresses, Phone numbers, Twitter - * handles, and Hashtags found in the given chunk of HTML. Does not link - * URLs found within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to - * <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * This method finds the text around any HTML elements in the input - * `textOrHtml`, which will be the text that is processed. Any original HTML - * elements will be left as-is, as well as the text that is already wrapped - * in anchor (<a>) tags. - * - * @param {String} textOrHtml The HTML or text to autolink matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #twitter}, and {@link #hashtag} options are enabled). - * @return {String} The HTML, with matches automatically linked. - */ - link : function( textOrHtml ) { - var htmlParser = this.getHtmlParser(), - htmlNodes = htmlParser.parse( textOrHtml ), - anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have - resultHtml = []; - - for( var i = 0, len = htmlNodes.length; i < len; i++ ) { - var node = htmlNodes[ i ], - nodeType = node.getType(), - nodeText = node.getText(); - - if( nodeType === 'element' ) { - // Process HTML nodes in the input `textOrHtml` - if( node.getTagName() === 'a' ) { - if( !node.isClosing() ) { // it's the start <a> tag - anchorTagStackCount++; - } else { // it's the end </a> tag - anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous </a> tags by making sure the stack count never goes below 0 - } - } - resultHtml.push( nodeText ); // now add the text of the tag itself verbatim - - } else if( nodeType === 'entity' || nodeType === 'comment' ) { - resultHtml.push( nodeText ); // append HTML entity nodes (such as ' ') or HTML comments (such as '<!-- Comment -->') verbatim - - } else { - // Process text nodes in the input `textOrHtml` - if( anchorTagStackCount === 0 ) { - // If we're not within an <a> tag, process the text node to linkify - var linkifiedStr = this.linkifyStr( nodeText ); - resultHtml.push( linkifiedStr ); - - } else { - // `text` is within an <a> tag, simply append the text - we do not want to autolink anything - // already within an <a>...</a> tag - resultHtml.push( nodeText ); - } - } - } - - return resultHtml.join( "" ); - }, - - /** - * Process the text that lies in between HTML tags, performing the anchor - * tag replacements for the matches, and returns the string with the - * replacements made. - * - * This method does the actual wrapping of matches with anchor tags. - * - * @private - * @param {String} str The string of text to auto-link. - * @return {String} The text with anchor tags auto-filled. - */ - linkifyStr : function( str ) { - return this.getMatchParser().replace( str, this.createMatchReturnVal, this ); - }, - - - /** - * Creates the return string value for a given match in the input string, - * for the {@link #linkifyStr} method. - * - * This method handles the {@link #replaceFn}, if one was provided. - * - * @private - * @param {Autolinker.match.Match} match The Match object that represents the match. - * @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but - * may be the `matchStr` itself if the match is not to be replaced. - */ - createMatchReturnVal : function( match ) { - // Handle a custom `replaceFn` being provided - var replaceFnResult; - if( this.replaceFn ) { - replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg - } - - if( typeof replaceFnResult === 'string' ) { - return replaceFnResult; // `replaceFn` returned a string, use that - - } else if( replaceFnResult === false ) { - return match.getMatchedText(); // no replacement for the match - - } else if( replaceFnResult instanceof Autolinker.HtmlTag ) { - return replaceFnResult.toAnchorString(); - - } else { // replaceFnResult === true, or no/unknown return value from function - // Perform Autolinker's default anchor tag generation - var tagBuilder = this.getTagBuilder(), - anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance - - return anchorTag.toAnchorString(); - } - }, - - - /** - * Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. - * - * @protected - * @return {Autolinker.htmlParser.HtmlParser} - */ - getHtmlParser : function() { - var htmlParser = this.htmlParser; - - if( !htmlParser ) { - htmlParser = this.htmlParser = new Autolinker.htmlParser.HtmlParser(); - } - - return htmlParser; - }, - - - /** - * Lazily instantiates and returns the {@link #matchParser} instance for this Autolinker instance. - * - * @protected - * @return {Autolinker.matchParser.MatchParser} - */ - getMatchParser : function() { - var matchParser = this.matchParser; - - if( !matchParser ) { - matchParser = this.matchParser = new Autolinker.matchParser.MatchParser( { - urls : this.urls, - email : this.email, - twitter : this.twitter, - phone : this.phone, - hashtag : this.hashtag, - stripPrefix : this.stripPrefix - } ); - } - - return matchParser; - }, - - - /** - * Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it - * if it does not yet exist. - * - * This method may be used in a {@link #replaceFn} to generate the {@link Autolinker.HtmlTag HtmlTag} instance that - * Autolinker would normally generate, and then allow for modifications before returning it. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - * - * @return {Autolinker.AnchorTagBuilder} - */ - getTagBuilder : function() { - var tagBuilder = this.tagBuilder; - - if( !tagBuilder ) { - tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { - newWindow : this.newWindow, - truncate : this.truncate, - className : this.className - } ); - } - - return tagBuilder; - } - -}; - - -/** - * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, - * and Hashtags found in the given chunk of HTML. Does not link URLs found - * within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * Example: - * - * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); - * // Produces: "Go to <a href="http://google.com">google.com</a>" - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within (depending - * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #twitter}, - * and {@link #hashtag} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {String} The HTML text, with matches automatically linked. - */ -Autolinker.link = function( textOrHtml, options ) { - var autolinker = new Autolinker( options ); - return autolinker.link( textOrHtml ); -}; - - -// Autolinker Namespaces -Autolinker.match = {}; -Autolinker.htmlParser = {}; -Autolinker.matchParser = {}; - -/*global Autolinker */ -/*jshint eqnull:true, boss:true */ -/** - * @class Autolinker.Util - * @singleton - * - * A few utility methods for Autolinker. - */ -Autolinker.Util = { - - /** - * @property {Function} abstractMethod - * - * A function object which represents an abstract method. - */ - abstractMethod : function() { throw "abstract"; }, - - - /** - * @private - * @property {RegExp} trimRegex - * - * The regular expression used to trim the leading and trailing whitespace - * from a string. - */ - trimRegex : /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - - /** - * Assigns (shallow copies) the properties of `src` onto `dest`. - * - * @param {Object} dest The destination object. - * @param {Object} src The source object. - * @return {Object} The destination object (`dest`) - */ - assign : function( dest, src ) { - for( var prop in src ) { - if( src.hasOwnProperty( prop ) ) { - dest[ prop ] = src[ prop ]; - } - } - - return dest; - }, - - - /** - * Extends `superclass` to create a new subclass, adding the `protoProps` to the new subclass's prototype. - * - * @param {Function} superclass The constructor function for the superclass. - * @param {Object} protoProps The methods/properties to add to the subclass's prototype. This may contain the - * special property `constructor`, which will be used as the new subclass's constructor function. - * @return {Function} The new subclass function. - */ - extend : function( superclass, protoProps ) { - var superclassProto = superclass.prototype; - - var F = function() {}; - F.prototype = superclassProto; - - var subclass; - if( protoProps.hasOwnProperty( 'constructor' ) ) { - subclass = protoProps.constructor; - } else { - subclass = function() { superclassProto.constructor.apply( this, arguments ); }; - } - - var subclassProto = subclass.prototype = new F(); // set up prototype chain - subclassProto.constructor = subclass; // fix constructor property - subclassProto.superclass = superclassProto; - - delete protoProps.constructor; // don't re-assign constructor property to the prototype, since a new function may have been created (`subclass`), which is now already there - Autolinker.Util.assign( subclassProto, protoProps ); - - return subclass; - }, - - - /** - * Truncates the `str` at `len - ellipsisChars.length`, and adds the `ellipsisChars` to the - * end of the string (by default, two periods: '..'). If the `str` length does not exceed - * `len`, the string will be returned unchanged. - * - * @param {String} str The string to truncate and add an ellipsis to. - * @param {Number} truncateLen The length to truncate the string at. - * @param {String} [ellipsisChars=..] The ellipsis character(s) to add to the end of `str` - * when truncated. Defaults to '..' - */ - ellipsis : function( str, truncateLen, ellipsisChars ) { - if( str.length > truncateLen ) { - ellipsisChars = ( ellipsisChars == null ) ? '..' : ellipsisChars; - str = str.substring( 0, truncateLen - ellipsisChars.length ) + ellipsisChars; - } - return str; - }, - - - /** - * Supports `Array.prototype.indexOf()` functionality for old IE (IE8 and below). - * - * @param {Array} arr The array to find an element of. - * @param {*} element The element to find in the array, and return the index of. - * @return {Number} The index of the `element`, or -1 if it was not found. - */ - indexOf : function( arr, element ) { - if( Array.prototype.indexOf ) { - return arr.indexOf( element ); - - } else { - for( var i = 0, len = arr.length; i < len; i++ ) { - if( arr[ i ] === element ) return i; - } - return -1; - } - }, - - - - /** - * Performs the functionality of what modern browsers do when `String.prototype.split()` is called - * with a regular expression that contains capturing parenthesis. - * - * For example: - * - * // Modern browsers: - * "a,b,c".split( /(,)/ ); // --> [ 'a', ',', 'b', ',', 'c' ] - * - * // Old IE (including IE8): - * "a,b,c".split( /(,)/ ); // --> [ 'a', 'b', 'c' ] - * - * This method emulates the functionality of modern browsers for the old IE case. - * - * @param {String} str The string to split. - * @param {RegExp} splitRegex The regular expression to split the input `str` on. The splitting - * character(s) will be spliced into the array, as in the "modern browsers" example in the - * description of this method. - * Note #1: the supplied regular expression **must** have the 'g' flag specified. - * Note #2: for simplicity's sake, the regular expression does not need - * to contain capturing parenthesis - it will be assumed that any match has them. - * @return {String[]} The split array of strings, with the splitting character(s) included. - */ - splitAndCapture : function( str, splitRegex ) { - if( !splitRegex.global ) throw new Error( "`splitRegex` must have the 'g' flag set" ); - - var result = [], - lastIdx = 0, - match; - - while( match = splitRegex.exec( str ) ) { - result.push( str.substring( lastIdx, match.index ) ); - result.push( match[ 0 ] ); // push the splitting char(s) - - lastIdx = match.index + match[ 0 ].length; - } - result.push( str.substring( lastIdx ) ); - - return result; - }, - - - /** - * Trims the leading and trailing whitespace from a string. - * - * @param {String} str The string to trim. - * @return {String} - */ - trim : function( str ) { - return str.replace( this.trimRegex, '' ); - } - -}; -/*global Autolinker */ -/*jshint boss:true */ -/** - * @class Autolinker.HtmlTag - * @extends Object - * - * Represents an HTML tag, which can be used to easily build/modify HTML tags programmatically. - * - * Autolinker uses this abstraction to create HTML tags, and then write them out as strings. You may also use - * this class in your code, especially within a {@link Autolinker#replaceFn replaceFn}. - * - * ## Examples - * - * Example instantiation: - * - * var tag = new Autolinker.HtmlTag( { - * tagName : 'a', - * attrs : { 'href': 'http://google.com', 'class': 'external-link' }, - * innerHtml : 'Google' - * } ); - * - * tag.toAnchorString(); // <a href="http://google.com" class="external-link">Google</a> - * - * // Individual accessor methods - * tag.getTagName(); // 'a' - * tag.getAttr( 'href' ); // 'http://google.com' - * tag.hasClass( 'external-link' ); // true - * - * - * Using mutator methods (which may be used in combination with instantiation config properties): - * - * var tag = new Autolinker.HtmlTag(); - * tag.setTagName( 'a' ); - * tag.setAttr( 'href', 'http://google.com' ); - * tag.addClass( 'external-link' ); - * tag.setInnerHtml( 'Google' ); - * - * tag.getTagName(); // 'a' - * tag.getAttr( 'href' ); // 'http://google.com' - * tag.hasClass( 'external-link' ); // true - * - * tag.toAnchorString(); // <a href="http://google.com" class="external-link">Google</a> - * - * - * ## Example use within a {@link Autolinker#replaceFn replaceFn} - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance, configured with the Match's href and anchor text - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - * - * - * ## Example use with a new tag for the replacement - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = new Autolinker.HtmlTag( { - * tagName : 'button', - * attrs : { 'title': 'Load URL: ' + match.getAnchorHref() }, - * innerHtml : 'Load URL: ' + match.getAnchorText() - * } ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <button title="Load URL: http://google.com">Load URL: google.com</button> - */ -Autolinker.HtmlTag = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} tagName - * - * The tag name. Ex: 'a', 'button', etc. - * - * Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toAnchorString} - * is executed. - */ - - /** - * @cfg {Object.<String, String>} attrs - * - * An key/value Object (map) of attributes to create the tag with. The keys are the attribute names, and the - * values are the attribute values. - */ - - /** - * @cfg {String} innerHtml - * - * The inner HTML for the tag. - * - * Note the camel case name on `innerHtml`. Acronyms are camelCased in this utility (such as not to run into the acronym - * naming inconsistency that the DOM developers created with `XMLHttpRequest`). You may alternatively use {@link #innerHTML} - * if you prefer, but this one is recommended. - */ - - /** - * @cfg {String} innerHTML - * - * Alias of {@link #innerHtml}, accepted for consistency with the browser DOM api, but prefer the camelCased version - * for acronym names. - */ - - - /** - * @protected - * @property {RegExp} whitespaceRegex - * - * Regular expression used to match whitespace in a string of CSS classes. - */ - whitespaceRegex : /\s+/, - - - /** - * @constructor - * @param {Object} [cfg] The configuration properties for this class, in an Object (map) - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - - this.innerHtml = this.innerHtml || this.innerHTML; // accept either the camelCased form or the fully capitalized acronym - }, - - - /** - * Sets the tag name that will be used to generate the tag with. - * - * @param {String} tagName - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setTagName : function( tagName ) { - this.tagName = tagName; - return this; - }, - - - /** - * Retrieves the tag name. - * - * @return {String} - */ - getTagName : function() { - return this.tagName || ""; - }, - - - /** - * Sets an attribute on the HtmlTag. - * - * @param {String} attrName The attribute name to set. - * @param {String} attrValue The attribute value to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setAttr : function( attrName, attrValue ) { - var tagAttrs = this.getAttrs(); - tagAttrs[ attrName ] = attrValue; - - return this; - }, - - - /** - * Retrieves an attribute from the HtmlTag. If the attribute does not exist, returns `undefined`. - * - * @param {String} name The attribute name to retrieve. - * @return {String} The attribute's value, or `undefined` if it does not exist on the HtmlTag. - */ - getAttr : function( attrName ) { - return this.getAttrs()[ attrName ]; - }, - - - /** - * Sets one or more attributes on the HtmlTag. - * - * @param {Object.<String, String>} attrs A key/value Object (map) of the attributes to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setAttrs : function( attrs ) { - var tagAttrs = this.getAttrs(); - Autolinker.Util.assign( tagAttrs, attrs ); - - return this; - }, - - - /** - * Retrieves the attributes Object (map) for the HtmlTag. - * - * @return {Object.<String, String>} A key/value object of the attributes for the HtmlTag. - */ - getAttrs : function() { - return this.attrs || ( this.attrs = {} ); - }, - - - /** - * Sets the provided `cssClass`, overwriting any current CSS classes on the HtmlTag. - * - * @param {String} cssClass One or more space-separated CSS classes to set (overwrite). - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setClass : function( cssClass ) { - return this.setAttr( 'class', cssClass ); - }, - - - /** - * Convenience method to add one or more CSS classes to the HtmlTag. Will not add duplicate CSS classes. - * - * @param {String} cssClass One or more space-separated CSS classes to add. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - addClass : function( cssClass ) { - var classAttr = this.getClass(), - whitespaceRegex = this.whitespaceRegex, - indexOf = Autolinker.Util.indexOf, // to support IE8 and below - classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), - newClasses = cssClass.split( whitespaceRegex ), - newClass; - - while( newClass = newClasses.shift() ) { - if( indexOf( classes, newClass ) === -1 ) { - classes.push( newClass ); - } - } - - this.getAttrs()[ 'class' ] = classes.join( " " ); - return this; - }, - - - /** - * Convenience method to remove one or more CSS classes from the HtmlTag. - * - * @param {String} cssClass One or more space-separated CSS classes to remove. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - removeClass : function( cssClass ) { - var classAttr = this.getClass(), - whitespaceRegex = this.whitespaceRegex, - indexOf = Autolinker.Util.indexOf, // to support IE8 and below - classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), - removeClasses = cssClass.split( whitespaceRegex ), - removeClass; - - while( classes.length && ( removeClass = removeClasses.shift() ) ) { - var idx = indexOf( classes, removeClass ); - if( idx !== -1 ) { - classes.splice( idx, 1 ); - } - } - - this.getAttrs()[ 'class' ] = classes.join( " " ); - return this; - }, - - - /** - * Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when - * there are multiple. - * - * @return {String} - */ - getClass : function() { - return this.getAttrs()[ 'class' ] || ""; - }, - - - /** - * Convenience method to check if the tag has a CSS class or not. - * - * @param {String} cssClass The CSS class to check for. - * @return {Boolean} `true` if the HtmlTag has the CSS class, `false` otherwise. - */ - hasClass : function( cssClass ) { - return ( ' ' + this.getClass() + ' ' ).indexOf( ' ' + cssClass + ' ' ) !== -1; - }, - - - /** - * Sets the inner HTML for the tag. - * - * @param {String} html The inner HTML to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setInnerHtml : function( html ) { - this.innerHtml = html; - - return this; - }, - - - /** - * Retrieves the inner HTML for the tag. - * - * @return {String} - */ - getInnerHtml : function() { - return this.innerHtml || ""; - }, - - - /** - * Override of superclass method used to generate the HTML string for the tag. - * - * @return {String} - */ - toAnchorString : function() { - var tagName = this.getTagName(), - attrsStr = this.buildAttrsStr(); - - attrsStr = ( attrsStr ) ? ' ' + attrsStr : ''; // prepend a space if there are actually attributes - - return [ '<', tagName, attrsStr, '>', this.getInnerHtml(), '</', tagName, '>' ].join( "" ); - }, - - - /** - * Support method for {@link #toAnchorString}, returns the string space-separated key="value" pairs, used to populate - * the stringified HtmlTag. - * - * @protected - * @return {String} Example return: `attr1="value1" attr2="value2"` - */ - buildAttrsStr : function() { - if( !this.attrs ) return ""; // no `attrs` Object (map) has been set, return empty string - - var attrs = this.getAttrs(), - attrsArr = []; - - for( var prop in attrs ) { - if( attrs.hasOwnProperty( prop ) ) { - attrsArr.push( prop + '="' + attrs[ prop ] + '"' ); - } - } - return attrsArr.join( " " ); - } - -} ); - -/*global Autolinker */ -/*jshint sub:true */ -/** - * @protected - * @class Autolinker.AnchorTagBuilder - * @extends Object - * - * Builds anchor (<a>) tags for the Autolinker utility when a match is found. - * - * Normally this class is instantiated, configured, and used internally by an {@link Autolinker} instance, but may - * actually be retrieved in a {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} instances - * which may be modified before returning from the {@link Autolinker#replaceFn replaceFn}. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - */ -Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { - - /** - * @cfg {Boolean} newWindow - * @inheritdoc Autolinker#newWindow - */ - - /** - * @cfg {Number} truncate - * @inheritdoc Autolinker#truncate - */ - - /** - * @cfg {String} className - * @inheritdoc Autolinker#className - */ - - - /** - * @constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Generates the actual anchor (<a>) tag to use in place of the - * matched text, via its `match` object. - * - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. - */ - build : function( match ) { - var tag = new Autolinker.HtmlTag( { - tagName : 'a', - attrs : this.createAttrs( match.getType(), match.getAnchorHref() ), - innerHtml : this.processAnchorText( match.getAnchorText() ) - } ); - - return tag; - }, - - - /** - * Creates the Object (map) of the HTML attributes for the anchor (<a>) - * tag being generated. - * - * @protected - * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of - * match that an anchor tag is being generated for. - * @param {String} href The href for the anchor tag. - * @return {Object} A key/value Object (map) of the anchor tag's attributes. - */ - createAttrs : function( matchType, anchorHref ) { - var attrs = { - 'href' : anchorHref // we'll always have the `href` attribute - }; - - var cssClass = this.createCssClass( matchType ); - if( cssClass ) { - attrs[ 'class' ] = cssClass; - } - if( this.newWindow ) { - attrs[ 'target' ] = "_blank"; - } - - return attrs; - }, - - - /** - * Creates the CSS class that will be used for a given anchor tag, based on - * the `matchType` and the {@link #className} config. - * - * @private - * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of - * match that an anchor tag is being generated for. - * @return {String} The CSS class string for the link. Example return: - * "myLink myLink-url". If no {@link #className} was configured, returns - * an empty string. - */ - createCssClass : function( matchType ) { - var className = this.className; - - if( !className ) - return ""; - else - return className + " " + className + "-" + matchType; // ex: "myLink myLink-url", "myLink myLink-email", "myLink myLink-phone", "myLink myLink-twitter", or "myLink myLink-hashtag" - }, - - - /** - * Processes the `anchorText` by truncating the text according to the - * {@link #truncate} config. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The processed `anchorText`. - */ - processAnchorText : function( anchorText ) { - anchorText = this.doTruncate( anchorText ); - - return anchorText; - }, - - - /** - * Performs the truncation of the `anchorText`, if the `anchorText` is - * longer than the {@link #truncate} option. Truncates the text to 2 - * characters fewer than the {@link #truncate} option, and adds ".." to the - * end. - * - * @private - * @param {String} text The anchor tag's text (i.e. what will be displayed). - * @return {String} The truncated anchor text. - */ - doTruncate : function( anchorText ) { - return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); - } - -} ); -/*global Autolinker */ -/** - * @private - * @class Autolinker.htmlParser.HtmlParser - * @extends Object - * - * An HTML parser implementation which simply walks an HTML string and returns an array of - * {@link Autolinker.htmlParser.HtmlNode HtmlNodes} that represent the basic HTML structure of the input string. - * - * Autolinker uses this to only link URLs/emails/Twitter handles within text nodes, effectively ignoring / "walking - * around" HTML tags. - */ -Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, { - - /** - * @private - * @property {RegExp} htmlRegex - * - * The regular expression used to pull out HTML tags from a string. Handles namespaced HTML tags and - * attribute names, as specified by http://www.w3.org/TR/html-markup/syntax.html. - * - * Capturing groups: - * - * 1. The "!DOCTYPE" tag name, if a tag is a <!DOCTYPE> tag. - * 2. If it is an end tag, this group will have the '/'. - * 3. If it is a comment tag, this group will hold the comment text (i.e. - * the text inside the `<!--` and `-->`. - * 4. The tag name for all tags (other than the <!DOCTYPE> tag) - */ - htmlRegex : (function() { - var commentTagRegex = /!--([\s\S]+?)--/, - tagNameRegex = /[0-9a-zA-Z][0-9a-zA-Z:]*/, - attrNameRegex = /[^\s\0"'>\/=\x01-\x1F\x7F]+/, // the unicode range accounts for excluding control chars, and the delete char - attrValueRegex = /(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/, // double quoted, single quoted, or unquoted attribute values - nameEqualsValueRegex = attrNameRegex.source + '(?:\\s*=\\s*' + attrValueRegex.source + ')?'; // optional '=[value]' - - return new RegExp( [ - // for <!DOCTYPE> tag. Ex: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">) - '(?:', - '<(!DOCTYPE)', // *** Capturing Group 1 - If it's a doctype tag - - // Zero or more attributes following the tag name - '(?:', - '\\s+', // one or more whitespace chars before an attribute - - // Either: - // A. attr="value", or - // B. "value" alone (To cover example doctype tag: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">) - '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', - ')*', - '>', - ')', - - '|', - - // All other HTML tags (i.e. tags that are not <!DOCTYPE>) - '(?:', - '<(/)?', // Beginning of a tag or comment. Either '<' for a start tag, or '</' for an end tag. - // *** Capturing Group 2: The slash or an empty string. Slash ('/') for end tag, empty string for start or self-closing tag. - - '(?:', - commentTagRegex.source, // *** Capturing Group 3 - A Comment Tag's Text - - '|', - - '(?:', - - // *** Capturing Group 4 - The tag name - '(' + tagNameRegex.source + ')', - - // Zero or more attributes following the tag name - '(?:', - '\\s+', // one or more whitespace chars before an attribute - nameEqualsValueRegex, // attr="value" (with optional ="value" part) - ')*', - - '\\s*/?', // any trailing spaces and optional '/' before the closing '>' - - ')', - ')', - '>', - ')' - ].join( "" ), 'gi' ); - } )(), - - /** - * @private - * @property {RegExp} htmlCharacterEntitiesRegex - * - * The regular expression that matches common HTML character entities. - * - * Ignoring & as it could be part of a query string -- handling it separately. - */ - htmlCharacterEntitiesRegex: /( | |<|<|>|>|"|"|')/gi, - - - /** - * Parses an HTML string and returns a simple array of {@link Autolinker.htmlParser.HtmlNode HtmlNodes} - * to represent the HTML structure of the input string. - * - * @param {String} html The HTML to parse. - * @return {Autolinker.htmlParser.HtmlNode[]} - */ - parse : function( html ) { - var htmlRegex = this.htmlRegex, - currentResult, - lastIndex = 0, - textAndEntityNodes, - nodes = []; // will be the result of the method - - while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { - var tagText = currentResult[ 0 ], - commentText = currentResult[ 3 ], // if we've matched a comment - tagName = currentResult[ 1 ] || currentResult[ 4 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img") - isClosingTag = !!currentResult[ 2 ], - inBetweenTagsText = html.substring( lastIndex, currentResult.index ); - - // Push TextNodes and EntityNodes for any text found between tags - if( inBetweenTagsText ) { - textAndEntityNodes = this.parseTextAndEntityNodes( inBetweenTagsText ); - nodes.push.apply( nodes, textAndEntityNodes ); - } - - // Push the CommentNode or ElementNode - if( commentText ) { - nodes.push( this.createCommentNode( tagText, commentText ) ); - } else { - nodes.push( this.createElementNode( tagText, tagName, isClosingTag ) ); - } - - lastIndex = currentResult.index + tagText.length; - } - - // Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. - if( lastIndex < html.length ) { - var text = html.substring( lastIndex ); - - // Push TextNodes and EntityNodes for any text found between tags - if( text ) { - textAndEntityNodes = this.parseTextAndEntityNodes( text ); - nodes.push.apply( nodes, textAndEntityNodes ); - } - } - - return nodes; - }, - - - /** - * Parses text and HTML entity nodes from a given string. The input string - * should not have any HTML tags (elements) within it. - * - * @private - * @param {String} text The text to parse. - * @return {Autolinker.htmlParser.HtmlNode[]} An array of HtmlNodes to - * represent the {@link Autolinker.htmlParser.TextNode TextNodes} and - * {@link Autolinker.htmlParser.EntityNode EntityNodes} found. - */ - parseTextAndEntityNodes : function( text ) { - var nodes = [], - textAndEntityTokens = Autolinker.Util.splitAndCapture( text, this.htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array - - // Every even numbered token is a TextNode, and every odd numbered token is an EntityNode - // For example: an input `text` of "Test "this" today" would turn into the - // `textAndEntityTokens`: [ 'Test ', '"', 'this', '"', ' today' ] - for( var i = 0, len = textAndEntityTokens.length; i < len; i += 2 ) { - var textToken = textAndEntityTokens[ i ], - entityToken = textAndEntityTokens[ i + 1 ]; - - if( textToken ) nodes.push( this.createTextNode( textToken ) ); - if( entityToken ) nodes.push( this.createEntityNode( entityToken ) ); - } - return nodes; - }, - - - /** - * Factory method to create an {@link Autolinker.htmlParser.CommentNode CommentNode}. - * - * @private - * @param {String} tagText The full text of the tag (comment) that was - * matched, including its <!-- and -->. - * @param {String} comment The full text of the comment that was matched. - */ - createCommentNode : function( tagText, commentText ) { - return new Autolinker.htmlParser.CommentNode( { - text: tagText, - comment: Autolinker.Util.trim( commentText ) - } ); - }, - - - /** - * Factory method to create an {@link Autolinker.htmlParser.ElementNode ElementNode}. - * - * @private - * @param {String} tagText The full text of the tag (element) that was - * matched, including its attributes. - * @param {String} tagName The name of the tag. Ex: An <img> tag would - * be passed to this method as "img". - * @param {Boolean} isClosingTag `true` if it's a closing tag, false - * otherwise. - * @return {Autolinker.htmlParser.ElementNode} - */ - createElementNode : function( tagText, tagName, isClosingTag ) { - return new Autolinker.htmlParser.ElementNode( { - text : tagText, - tagName : tagName.toLowerCase(), - closing : isClosingTag - } ); - }, - - - /** - * Factory method to create a {@link Autolinker.htmlParser.EntityNode EntityNode}. - * - * @private - * @param {String} text The text that was matched for the HTML entity (such - * as '&nbsp;'). - * @return {Autolinker.htmlParser.EntityNode} - */ - createEntityNode : function( text ) { - return new Autolinker.htmlParser.EntityNode( { text: text } ); - }, - - - /** - * Factory method to create a {@link Autolinker.htmlParser.TextNode TextNode}. - * - * @private - * @param {String} text The text that was matched. - * @return {Autolinker.htmlParser.TextNode} - */ - createTextNode : function( text ) { - return new Autolinker.htmlParser.TextNode( { text: text } ); - } - -} ); -/*global Autolinker */ -/** - * @abstract - * @class Autolinker.htmlParser.HtmlNode - * - * Represents an HTML node found in an input string. An HTML node is one of the following: - * - * 1. An {@link Autolinker.htmlParser.ElementNode ElementNode}, which represents HTML tags. - * 2. A {@link Autolinker.htmlParser.TextNode TextNode}, which represents text outside or within HTML tags. - * 3. A {@link Autolinker.htmlParser.EntityNode EntityNode}, which represents one of the known HTML - * entities that Autolinker looks for. This includes common ones such as &quot; and &nbsp; - */ -Autolinker.htmlParser.HtmlNode = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} text (required) - * - * The original text that was matched for the HtmlNode. - * - * - In the case of an {@link Autolinker.htmlParser.ElementNode ElementNode}, this will be the tag's - * text. - * - In the case of a {@link Autolinker.htmlParser.TextNode TextNode}, this will be the text itself. - * - In the case of a {@link Autolinker.htmlParser.EntityNode EntityNode}, this will be the text of - * the HTML entity. - */ - text : "", - - - /** - * @constructor - * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Returns a string name for the type of node that this class represents. - * - * @abstract - * @return {String} - */ - getType : Autolinker.Util.abstractMethod, - - - /** - * Retrieves the {@link #text} for the HtmlNode. - * - * @return {String} - */ - getText : function() { - return this.text; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.CommentNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents an HTML comment node that has been parsed by the - * {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more - * details. - */ -Autolinker.htmlParser.CommentNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * @cfg {String} comment (required) - * - * The text inside the comment tag. This text is stripped of any leading or - * trailing whitespace. - */ - comment : '', - - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'comment'; - }, - - - /** - * Returns the comment inside the comment tag. - * - * @return {String} - */ - getComment : function() { - return this.comment; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.ElementNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents an HTML element node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.ElementNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * @cfg {String} tagName (required) - * - * The name of the tag that was matched. - */ - tagName : '', - - /** - * @cfg {Boolean} closing (required) - * - * `true` if the element (tag) is a closing tag, `false` if its an opening tag. - */ - closing : false, - - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'element'; - }, - - - /** - * Returns the HTML element's (tag's) name. Ex: for an <img> tag, returns "img". - * - * @return {String} - */ - getTagName : function() { - return this.tagName; - }, - - - /** - * Determines if the HTML element (tag) is a closing tag. Ex: <div> returns - * `false`, while </div> returns `true`. - * - * @return {Boolean} - */ - isClosing : function() { - return this.closing; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.EntityNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents a known HTML entity node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * Ex: '&nbsp;', or '&#160;' (which will be retrievable from the {@link #getText} method. - * - * Note that this class will only be returned from the HtmlParser for the set of checked HTML entity nodes - * defined by the {@link Autolinker.htmlParser.HtmlParser#htmlCharacterEntitiesRegex}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.EntityNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'entity'; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.TextNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents a text node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.TextNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'text'; - } - -} ); -/*global Autolinker */ -/** - * @private - * @class Autolinker.matchParser.MatchParser - * @extends Object - * - * Used by Autolinker to parse potential matches, given an input string of text. - * - * The MatchParser is fed a non-HTML string in order to search for matches. - * Autolinker first uses the {@link Autolinker.htmlParser.HtmlParser} to "walk - * around" HTML tags, and then the text around the HTML tags is passed into the - * MatchParser in order to find the actual matches. - */ -Autolinker.matchParser.MatchParser = Autolinker.Util.extend( Object, { - - /** - * @cfg {Boolean} urls - * @inheritdoc Autolinker#urls - */ - urls : true, - - /** - * @cfg {Boolean} email - * @inheritdoc Autolinker#email - */ - email : true, - - /** - * @cfg {Boolean} twitter - * @inheritdoc Autolinker#twitter - */ - twitter : true, - - /** - * @cfg {Boolean} phone - * @inheritdoc Autolinker#phone - */ - phone: true, - - /** - * @cfg {Boolean/String} hashtag - * @inheritdoc Autolinker#hashtag - */ - hashtag : false, - - /** - * @cfg {Boolean} stripPrefix - * @inheritdoc Autolinker#stripPrefix - */ - stripPrefix : true, - - - /** - * @private - * @property {RegExp} matcherRegex - * - * The regular expression that matches URLs, email addresses, phone #s, - * Twitter handles, and Hashtags. - * - * This regular expression has the following capturing groups: - * - * 1. Group that is used to determine if there is a Twitter handle match - * (i.e. \@someTwitterUser). Simply check for its existence to determine - * if there is a Twitter handle match. The next couple of capturing - * groups give information about the Twitter handle match. - * 2. The whitespace character before the \@sign in a Twitter handle. This - * is needed because there are no lookbehinds in JS regular expressions, - * and can be used to reconstruct the original string in a replace(). - * 3. The Twitter handle itself in a Twitter match. If the match is - * '@someTwitterUser', the handle is 'someTwitterUser'. - * 4. Group that matches an email address. Used to determine if the match - * is an email address, as well as holding the full address. Ex: - * 'me@my.com' - * 5. Group that matches a URL in the input text. Ex: 'http://google.com', - * 'www.google.com', or just 'google.com'. This also includes a path, - * url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor - * 6. Group that matches a protocol URL (i.e. 'http://google.com'). This is - * used to match protocol URLs with just a single word, like 'http://localhost', - * where we won't double check that the domain name has at least one '.' - * in it. - * 7. A protocol-relative ('//') match for the case of a 'www.' prefixed - * URL. Will be an empty string if it is not a protocol-relative match. - * We need to know the character before the '//' in order to determine - * if it is a valid match or the // was in a string we don't want to - * auto-link. - * 8. A protocol-relative ('//') match for the case of a known TLD prefixed - * URL. Will be an empty string if it is not a protocol-relative match. - * See #6 for more info. - * 9. Group that is used to determine if there is a phone number match. The - * next 3 groups give segments of the phone number. - * 10. Group that is used to determine if there is a Hashtag match - * (i.e. \#someHashtag). Simply check for its existence to determine if - * there is a Hashtag match. The next couple of capturing groups give - * information about the Hashtag match. - * 11. The whitespace character before the #sign in a Hashtag handle. This - * is needed because there are no look-behinds in JS regular - * expressions, and can be used to reconstruct the original string in a - * replace(). - * 12. The Hashtag itself in a Hashtag match. If the match is - * '#someHashtag', the hashtag is 'someHashtag'. - */ - matcherRegex : (function() { - var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs - - hashtagRegex = /(^|[^\w])#(\w{1,15})/, // For matching a Hashtag. Ex: #games - - emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) - phoneRegex = /(?:\+?\d{1,3}[-\s.])?\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}/, // ex: (123) 456-7890, 123 456 7890, 123-456-7890, etc. - protocolRegex = /(?:[A-Za-z][-.+A-Za-z0-9]+:(?![A-Za-z][-.+A-Za-z0-9]+:\/\/)(?!\d+\/?)(?:\/\/)?)/, // match protocol, allow in format "http://" or "mailto:". However, do not match the first part of something like 'link:http://www.google.com' (i.e. don't match "link:"). Also, make sure we don't interpret 'google.com:8000' as if 'google.com' was a protocol here (i.e. ignore a trailing port number in this regex) - wwwRegex = /(?:www\.)/, // starting with 'www.' - domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period - tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/, // match our known top level domains (TLDs) - - // Allow optional path, query string, and hash anchor, not ending in the following characters: "?!:,.;" - // http://blog.codinghorror.com/the-problem-with-urls/ - urlSuffixRegex = /[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]?!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]]/; - - return new RegExp( [ - '(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() - // *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and - // *** Capturing group $3, which matches the actual twitter handle - twitterRegex.source, - ')', - - '|', - - '(', // *** Capturing group $4, which is used to determine an email match - emailRegex.source, - domainNameRegex.source, - tldRegex.source, - ')', - - '|', - - '(', // *** Capturing group $5, which is used to match a URL - '(?:', // parens to cover match for protocol (optional), and domain - '(', // *** Capturing group $6, for a protocol-prefixed url (ex: http://google.com) - protocolRegex.source, - domainNameRegex.source, - ')', - - '|', - - '(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) - '(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character - wwwRegex.source, - domainNameRegex.source, - ')', - - '|', - - '(?:', // non-capturing paren for known a TLD url (ex: google.com) - '(.?//)?', // *** Capturing group $8 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character - domainNameRegex.source, - tldRegex.source, - ')', - ')', - - '(?:' + urlSuffixRegex.source + ')?', // match for path, query string, and/or hash anchor - optional - ')', - - '|', - - // this setup does not scale well for open extension :( Need to rethink design of autolinker... - // *** Capturing group $9, which matches a (USA for now) phone number - '(', - phoneRegex.source, - ')', - - '|', - - '(', // *** Capturing group $10, which can be used to check for a Hashtag match. Use group $12 for the actual Hashtag though. $11 may be used to reconstruct the original string in a replace() - // *** Capturing group $11, which matches the whitespace character before the '#' sign (needed because of no lookbehinds), and - // *** Capturing group $12, which matches the actual Hashtag - hashtagRegex.source, - ')' - ].join( "" ), 'gi' ); - } )(), - - /** - * @private - * @property {RegExp} charBeforeProtocolRelMatchRegex - * - * The regular expression used to retrieve the character before a - * protocol-relative URL match. - * - * This is used in conjunction with the {@link #matcherRegex}, which needs - * to grab the character before a protocol-relative '//' due to the lack of - * a negative look-behind in JavaScript regular expressions. The character - * before the match is stripped from the URL. - */ - charBeforeProtocolRelMatchRegex : /^(.)?\/\//, - - /** - * @private - * @property {Autolinker.MatchValidator} matchValidator - * - * The MatchValidator object, used to filter out any false positives from - * the {@link #matcherRegex}. See {@link Autolinker.MatchValidator} for details. - */ - - - /** - * @constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder - * instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - - this.matchValidator = new Autolinker.MatchValidator(); - }, - - - /** - * Parses the input `text` to search for matches, and calls the `replaceFn` - * to allow replacements of the matches. Returns the `text` with matches - * replaced. - * - * @param {String} text The text to search and repace matches in. - * @param {Function} replaceFn The iterator function to handle the - * replacements. The function takes a single argument, a {@link Autolinker.match.Match} - * object, and should return the text that should make the replacement. - * @param {Object} [contextObj=window] The context object ("scope") to run - * the `replaceFn` in. - * @return {String} - */ - replace : function( text, replaceFn, contextObj ) { - var me = this; // for closure - - return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) { - var matchDescObj = me.processCandidateMatch( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ); // "match description" object - - // Return out with no changes for match types that are disabled (url, - // email, phone, etc.), or for matches that are invalid (false - // positives from the matcherRegex, which can't use look-behinds - // since they are unavailable in JS). - if( !matchDescObj ) { - return matchStr; - - } else { - // Generate replacement text for the match from the `replaceFn` - var replaceStr = replaceFn.call( contextObj, matchDescObj.match ); - return matchDescObj.prefixStr + replaceStr + matchDescObj.suffixStr; - } - } ); - }, - - - /** - * Processes a candidate match from the {@link #matcherRegex}. - * - * Not all matches found by the regex are actual URL/Email/Phone/Twitter/Hashtag - * matches, as determined by the {@link #matchValidator}. In this case, the - * method returns `null`. Otherwise, a valid Object with `prefixStr`, - * `match`, and `suffixStr` is returned. - * - * @private - * @param {String} matchStr The full match that was found by the - * {@link #matcherRegex}. - * @param {String} twitterMatch The matched text of a Twitter handle, if the - * match is a Twitter match. - * @param {String} twitterHandlePrefixWhitespaceChar The whitespace char - * before the @ sign in a Twitter handle match. This is needed because of - * no lookbehinds in JS regexes, and is need to re-include the character - * for the anchor tag replacement. - * @param {String} twitterHandle The actual Twitter user (i.e the word after - * the @ sign in a Twitter match). - * @param {String} emailAddressMatch The matched email address for an email - * address match. - * @param {String} urlMatch The matched URL string for a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @param {String} wwwProtocolRelativeMatch The '//' for a protocol-relative - * match from a 'www' url, with the character that comes before the '//'. - * @param {String} tldProtocolRelativeMatch The '//' for a protocol-relative - * match from a TLD (top level domain) match, with the character that - * comes before the '//'. - * @param {String} phoneMatch The matched text of a phone number - * @param {String} hashtagMatch The matched text of a Twitter - * Hashtag, if the match is a Hashtag match. - * @param {String} hashtagPrefixWhitespaceChar The whitespace char - * before the # sign in a Hashtag match. This is needed because of no - * lookbehinds in JS regexes, and is need to re-include the character for - * the anchor tag replacement. - * @param {String} hashtag The actual Hashtag (i.e the word - * after the # sign in a Hashtag match). - * - * @return {Object} A "match description object". This will be `null` if the - * match was invalid, or if a match type is disabled. Otherwise, this will - * be an Object (map) with the following properties: - * @return {String} return.prefixStr The char(s) that should be prepended to - * the replacement string. These are char(s) that were needed to be - * included from the regex match that were ignored by processing code, and - * should be re-inserted into the replacement stream. - * @return {String} return.suffixStr The char(s) that should be appended to - * the replacement string. These are char(s) that were needed to be - * included from the regex match that were ignored by processing code, and - * should be re-inserted into the replacement stream. - * @return {Autolinker.match.Match} return.match The Match object that - * represents the match that was found. - */ - processCandidateMatch : function( - matchStr, twitterMatch, twitterHandlePrefixWhitespaceChar, twitterHandle, - emailAddressMatch, urlMatch, protocolUrlMatch, wwwProtocolRelativeMatch, - tldProtocolRelativeMatch, phoneMatch, hashtagMatch, - hashtagPrefixWhitespaceChar, hashtag - ) { - // Note: The `matchStr` variable wil be fixed up to remove characters that are no longer needed (which will - // be added to `prefixStr` and `suffixStr`). - - var protocolRelativeMatch = wwwProtocolRelativeMatch || tldProtocolRelativeMatch, - match, // Will be an Autolinker.match.Match object - - prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter and Hashtag matches. - suffixStr = ""; // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. - - // Return out with `null` for match types that are disabled (url, email, - // twitter, hashtag), or for matches that are invalid (false positives - // from the matcherRegex, which can't use look-behinds since they are - // unavailable in JS). - if( - ( urlMatch && !this.urls ) || - ( emailAddressMatch && !this.email ) || - ( phoneMatch && !this.phone ) || - ( twitterMatch && !this.twitter ) || - ( hashtagMatch && !this.hashtag ) || - !this.matchValidator.isValidMatch( urlMatch, protocolUrlMatch, protocolRelativeMatch ) - ) { - return null; - } - - // Handle a closing parenthesis at the end of the match, and exclude it - // if there is not a matching open parenthesis - // in the match itself. - if( this.matchHasUnbalancedClosingParen( matchStr ) ) { - matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" - suffixStr = ")"; // this will be added after the generated <a> tag - } - - if( emailAddressMatch ) { - match = new Autolinker.match.Email( { matchedText: matchStr, email: emailAddressMatch } ); - - } else if( twitterMatch ) { - // fix up the `matchStr` if there was a preceding whitespace char, - // which was needed to determine the match itself (since there are - // no look-behinds in JS regexes) - if( twitterHandlePrefixWhitespaceChar ) { - prefixStr = twitterHandlePrefixWhitespaceChar; - matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match - } - match = new Autolinker.match.Twitter( { matchedText: matchStr, twitterHandle: twitterHandle } ); - - } else if( phoneMatch ) { - // remove non-numeric values from phone number string - var cleanNumber = matchStr.replace( /\D/g, '' ); - match = new Autolinker.match.Phone( { matchedText: matchStr, number: cleanNumber } ); - - } else if( hashtagMatch ) { - // fix up the `matchStr` if there was a preceding whitespace char, - // which was needed to determine the match itself (since there are - // no look-behinds in JS regexes) - if( hashtagPrefixWhitespaceChar ) { - prefixStr = hashtagPrefixWhitespaceChar; - matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match - } - match = new Autolinker.match.Hashtag( { matchedText: matchStr, serviceName: this.hashtag, hashtag: hashtag } ); - - } else { // url match - // If it's a protocol-relative '//' match, remove the character - // before the '//' (which the matcherRegex needed to match due to - // the lack of a negative look-behind in JavaScript regular - // expressions) - if( protocolRelativeMatch ) { - var charBeforeMatch = protocolRelativeMatch.match( this.charBeforeProtocolRelMatchRegex )[ 1 ] || ""; - - if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) - prefixStr = charBeforeMatch; - matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match - } - } - - match = new Autolinker.match.Url( { - matchedText : matchStr, - url : matchStr, - protocolUrlMatch : !!protocolUrlMatch, - protocolRelativeMatch : !!protocolRelativeMatch, - stripPrefix : this.stripPrefix - } ); - } - - return { - prefixStr : prefixStr, - suffixStr : suffixStr, - match : match - }; - }, - - - /** - * Determines if a match found has an unmatched closing parenthesis. If so, - * this parenthesis will be removed from the match itself, and appended - * after the generated anchor tag in {@link #processCandidateMatch}. - * - * A match may have an extra closing parenthesis at the end of the match - * because the regular expression must include parenthesis for URLs such as - * "wikipedia.com/something_(disambiguation)", which should be auto-linked. - * - * However, an extra parenthesis *will* be included when the URL itself is - * wrapped in parenthesis, such as in the case of "(wikipedia.com/something_(disambiguation))". - * In this case, the last closing parenthesis should *not* be part of the - * URL itself, and this method will return `true`. - * - * @private - * @param {String} matchStr The full match string from the {@link #matcherRegex}. - * @return {Boolean} `true` if there is an unbalanced closing parenthesis at - * the end of the `matchStr`, `false` otherwise. - */ - matchHasUnbalancedClosingParen : function( matchStr ) { - var lastChar = matchStr.charAt( matchStr.length - 1 ); - - if( lastChar === ')' ) { - var openParensMatch = matchStr.match( /\(/g ), - closeParensMatch = matchStr.match( /\)/g ), - numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, - numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; - - if( numOpenParens < numCloseParens ) { - return true; - } - } - - return false; - } - -} ); -/*global Autolinker */ -/*jshint scripturl:true */ -/** - * @private - * @class Autolinker.MatchValidator - * @extends Object - * - * Used by Autolinker to filter out false positives from the - * {@link Autolinker.matchParser.MatchParser#matcherRegex}. - * - * Due to the limitations of regular expressions (including the missing feature - * of look-behinds in JS regular expressions), we cannot always determine the - * validity of a given match. This class applies a bit of additional logic to - * filter out any false positives that have been matched by the - * {@link Autolinker.matchParser.MatchParser#matcherRegex}. - */ -Autolinker.MatchValidator = Autolinker.Util.extend( Object, { - - /** - * @private - * @property {RegExp} invalidProtocolRelMatchRegex - * - * The regular expression used to check a potential protocol-relative URL - * match, coming from the {@link Autolinker.matchParser.MatchParser#matcherRegex}. - * A protocol-relative URL is, for example, "//yahoo.com" - * - * This regular expression checks to see if there is a word character before - * the '//' match in order to determine if we should actually autolink a - * protocol-relative URL. This is needed because there is no negative - * look-behind in JavaScript regular expressions. - * - * For instance, we want to autolink something like "Go to: //google.com", - * but we don't want to autolink something like "abc//google.com" - */ - invalidProtocolRelMatchRegex : /^[\w]\/\//, - - /** - * Regex to test for a full protocol, with the two trailing slashes. Ex: 'http://' - * - * @private - * @property {RegExp} hasFullProtocolRegex - */ - hasFullProtocolRegex : /^[A-Za-z][-.+A-Za-z0-9]+:\/\//, - - /** - * Regex to find the URI scheme, such as 'mailto:'. - * - * This is used to filter out 'javascript:' and 'vbscript:' schemes. - * - * @private - * @property {RegExp} uriSchemeRegex - */ - uriSchemeRegex : /^[A-Za-z][-.+A-Za-z0-9]+:/, - - /** - * Regex to determine if at least one word char exists after the protocol (i.e. after the ':') - * - * @private - * @property {RegExp} hasWordCharAfterProtocolRegex - */ - hasWordCharAfterProtocolRegex : /:[^\s]*?[A-Za-z]/, - - - /** - * Determines if a given match found by the {@link Autolinker.matchParser.MatchParser} - * is valid. Will return `false` for: - * - * 1) URL matches which do not have at least have one period ('.') in the - * domain name (effectively skipping over matches like "abc:def"). - * However, URL matches with a protocol will be allowed (ex: 'http://localhost') - * 2) URL matches which do not have at least one word character in the - * domain name (effectively skipping over matches like "git:1.0"). - * 3) A protocol-relative url match (a URL beginning with '//') whose - * previous character is a word character (effectively skipping over - * strings like "abc//google.com") - * - * Otherwise, returns `true`. - * - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @param {String} protocolRelativeMatch The protocol-relative string for a - * URL match (i.e. '//'), possibly with a preceding character (ex, a - * space, such as: ' //', or a letter, such as: 'a//'). The match is - * invalid if there is a word character preceding the '//'. - * @return {Boolean} `true` if the match given is valid and should be - * processed, or `false` if the match is invalid and/or should just not be - * processed. - */ - isValidMatch : function( urlMatch, protocolUrlMatch, protocolRelativeMatch ) { - if( - ( protocolUrlMatch && !this.isValidUriScheme( protocolUrlMatch ) ) || - this.urlMatchDoesNotHaveProtocolOrDot( urlMatch, protocolUrlMatch ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL, *unless* it was a full protocol match (like 'http://localhost') - this.urlMatchDoesNotHaveAtLeastOneWordChar( urlMatch, protocolUrlMatch ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" - this.isInvalidProtocolRelativeMatch( protocolRelativeMatch ) // A protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") - ) { - return false; - } - - return true; - }, - - - /** - * Determines if the URI scheme is a valid scheme to be autolinked. Returns - * `false` if the scheme is 'javascript:' or 'vbscript:' - * - * @private - * @param {String} uriSchemeMatch The match URL string for a full URI scheme - * match. Ex: 'http://yahoo.com' or 'mailto:a@a.com'. - * @return {Boolean} `true` if the scheme is a valid one, `false` otherwise. - */ - isValidUriScheme : function( uriSchemeMatch ) { - var uriScheme = uriSchemeMatch.match( this.uriSchemeRegex )[ 0 ].toLowerCase(); - - return ( uriScheme !== 'javascript:' && uriScheme !== 'vbscript:' ); - }, - - - /** - * Determines if a URL match does not have either: - * - * a) a full protocol (i.e. 'http://'), or - * b) at least one dot ('.') in the domain name (for a non-full-protocol - * match). - * - * Either situation is considered an invalid URL (ex: 'git:d' does not have - * either the '://' part, or at least one dot in the domain name. If the - * match was 'git:abc.com', we would consider this valid.) - * - * @private - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @return {Boolean} `true` if the URL match does not have a full protocol, - * or at least one dot ('.') in a non-full-protocol match. - */ - urlMatchDoesNotHaveProtocolOrDot : function( urlMatch, protocolUrlMatch ) { - return ( !!urlMatch && ( !protocolUrlMatch || !this.hasFullProtocolRegex.test( protocolUrlMatch ) ) && urlMatch.indexOf( '.' ) === -1 ); - }, - - - /** - * Determines if a URL match does not have at least one word character after - * the protocol (i.e. in the domain name). - * - * At least one letter character must exist in the domain name after a - * protocol match. Ex: skip over something like "git:1.0" - * - * @private - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to know whether or not we - * have a protocol in the URL string, in order to check for a word - * character after the protocol separator (':'). - * @return {Boolean} `true` if the URL match does not have at least one word - * character in it after the protocol, `false` otherwise. - */ - urlMatchDoesNotHaveAtLeastOneWordChar : function( urlMatch, protocolUrlMatch ) { - if( urlMatch && protocolUrlMatch ) { - return !this.hasWordCharAfterProtocolRegex.test( urlMatch ); - } else { - return false; - } - }, - - - /** - * Determines if a protocol-relative match is an invalid one. This method - * returns `true` if there is a `protocolRelativeMatch`, and that match - * contains a word character before the '//' (i.e. it must contain - * whitespace or nothing before the '//' in order to be considered valid). - * - * @private - * @param {String} protocolRelativeMatch The protocol-relative string for a - * URL match (i.e. '//'), possibly with a preceding character (ex, a - * space, such as: ' //', or a letter, such as: 'a//'). The match is - * invalid if there is a word character preceding the '//'. - * @return {Boolean} `true` if it is an invalid protocol-relative match, - * `false` otherwise. - */ - isInvalidProtocolRelativeMatch : function( protocolRelativeMatch ) { - return ( !!protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ); - } - -} ); -/*global Autolinker */ -/** - * @abstract - * @class Autolinker.match.Match - * - * Represents a match found in an input string which should be Autolinked. A Match object is what is provided in a - * {@link Autolinker#replaceFn replaceFn}, and may be used to query for details about the match. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, and Twitter Handles - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( autolinker, match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * case 'email' : - * console.log( "email: ", match.getEmail() ); - * - * case 'twitter' : - * console.log( "twitter: ", match.getTwitterHandle() ); - * } - * } - * } ); - * - * See the {@link Autolinker} class for more details on using the {@link Autolinker#replaceFn replaceFn}. - */ -Autolinker.match.Match = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} matchedText (required) - * - * The original text that was matched. - */ - - - /** - * @constructor - * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @abstract - * @return {String} - */ - getType : Autolinker.Util.abstractMethod, - - - /** - * Returns the original text that was matched. - * - * @return {String} - */ - getMatchedText : function() { - return this.matchedText; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @abstract - * @return {String} - */ - getAnchorHref : Autolinker.Util.abstractMethod, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @abstract - * @return {String} - */ - getAnchorText : Autolinker.Util.abstractMethod - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Email - * @extends Autolinker.match.Match - * - * Represents a Email match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} email (required) - * - * The email address that was matched. - */ - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'email'; - }, - - - /** - * Returns the email address that was matched. - * - * @return {String} - */ - getEmail : function() { - return this.email; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'mailto:' + this.email; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return this.email; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Hashtag - * @extends Autolinker.match.Match - * - * Represents a Hashtag match found in an input string which should be - * Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more - * details. - */ -Autolinker.match.Hashtag = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} serviceName (required) - * - * The service to point hashtag matches to. See {@link Autolinker#hashtag} - * for available values. - */ - - /** - * @cfg {String} hashtag (required) - * - * The Hashtag that was matched, without the '#'. - */ - - - /** - * Returns the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'hashtag'; - }, - - - /** - * Returns the matched hashtag. - * - * @return {String} - */ - getHashtag : function() { - return this.hashtag; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - var serviceName = this.serviceName, - hashtag = this.hashtag; - - switch( serviceName ) { - case 'twitter' : - return 'https://twitter.com/hashtag/' + hashtag; - case 'facebook' : - return 'https://www.facebook.com/hashtag/' + hashtag; - - default : // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case. - throw new Error( 'Unknown service name to point hashtag to: ', serviceName ); - } - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return '#' + this.hashtag; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Phone - * @extends Autolinker.match.Match - * - * Represents a Phone number match found in an input string which should be - * Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more - * details. - */ -Autolinker.match.Phone = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} number (required) - * - * The phone number that was matched. - */ - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'phone'; - }, - - - /** - * Returns the phone number that was matched. - * - * @return {String} - */ - getNumber: function() { - return this.number; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'tel:' + this.number; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return this.matchedText; - } - -} ); - -/*global Autolinker */ -/** - * @class Autolinker.match.Twitter - * @extends Autolinker.match.Match - * - * Represents a Twitter match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} twitterHandle (required) - * - * The Twitter handle that was matched. - */ - - - /** - * Returns the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'twitter'; - }, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getTwitterHandle : function() { - return this.twitterHandle; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'https://twitter.com/' + this.twitterHandle; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return '@' + this.twitterHandle; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Url - * @extends Autolinker.match.Match - * - * Represents a Url match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} url (required) - * - * The url that was matched. - */ - - /** - * @cfg {Boolean} protocolUrlMatch (required) - * - * `true` if the URL is a match which already has a protocol (i.e. 'http://'), `false` if the match was from a 'www' or - * known TLD match. - */ - - /** - * @cfg {Boolean} protocolRelativeMatch (required) - * - * `true` if the URL is a protocol-relative match. A protocol-relative match is a URL that starts with '//', - * and will be either http:// or https:// based on the protocol that the site is loaded under. - */ - - /** - * @cfg {Boolean} stripPrefix (required) - * @inheritdoc Autolinker#stripPrefix - */ - - - /** - * @private - * @property {RegExp} urlPrefixRegex - * - * A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. - */ - urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, - - /** - * @private - * @property {RegExp} protocolRelativeRegex - * - * The regular expression used to remove the protocol-relative '//' from the {@link #url} string, for purposes - * of {@link #getAnchorText}. A protocol-relative URL is, for example, "//yahoo.com" - */ - protocolRelativeRegex : /^\/\//, - - /** - * @private - * @property {Boolean} protocolPrepended - * - * Will be set to `true` if the 'http://' protocol has been prepended to the {@link #url} (because the - * {@link #url} did not have a protocol) - */ - protocolPrepended : false, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'url'; - }, - - - /** - * Returns the url that was matched, assuming the protocol to be 'http://' if the original - * match was missing a protocol. - * - * @return {String} - */ - getUrl : function() { - var url = this.url; - - // if the url string doesn't begin with a protocol, assume 'http://' - if( !this.protocolRelativeMatch && !this.protocolUrlMatch && !this.protocolPrepended ) { - url = this.url = 'http://' + url; - - this.protocolPrepended = true; - } - - return url; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - var url = this.getUrl(); - - return url.replace( /&/g, '&' ); // any &'s in the URL should be converted back to '&' if they were displayed as & in the source html - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - var anchorText = this.getUrl(); - - if( this.protocolRelativeMatch ) { - // Strip off any protocol-relative '//' from the anchor text - anchorText = this.stripProtocolRelativePrefix( anchorText ); - } - if( this.stripPrefix ) { - anchorText = this.stripUrlPrefix( anchorText ); - } - anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one - - return anchorText; - }, - - - // --------------------------------------- - - // Utility Functionality - - /** - * Strips the URL prefix (such as "http://" or "https://") from the given text. - * - * @private - * @param {String} text The text of the anchor that is being generated, for which to strip off the - * url prefix (such as stripping off "http://") - * @return {String} The `anchorText`, with the prefix stripped. - */ - stripUrlPrefix : function( text ) { - return text.replace( this.urlPrefixRegex, '' ); - }, - - - /** - * Strips any protocol-relative '//' from the anchor text. - * - * @private - * @param {String} text The text of the anchor that is being generated, for which to strip off the - * protocol-relative prefix (such as stripping off "//") - * @return {String} The `anchorText`, with the protocol-relative prefix stripped. - */ - stripProtocolRelativePrefix : function( text ) { - return text.replace( this.protocolRelativeRegex, '' ); - }, - - - /** - * Removes any trailing slash from the given `anchorText`, in preparation for the text to be displayed. - * - * @private - * @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing - * slash ('/') that may exist. - * @return {String} The `anchorText`, with the trailing slash removed. - */ - removeTrailingSlash : function( anchorText ) { - if( anchorText.charAt( anchorText.length - 1 ) === '/' ) { - anchorText = anchorText.slice( 0, -1 ); - } - return anchorText; - } - -} ); -return Autolinker; - -})); diff --git a/app/assets/javascripts/src/JIT.js.erb b/app/assets/javascripts/src/JIT.js similarity index 99% rename from app/assets/javascripts/src/JIT.js.erb rename to app/assets/javascripts/src/JIT.js index 2cb202dc..4754871e 100644 --- a/app/assets/javascripts/src/JIT.js.erb +++ b/app/assets/javascripts/src/JIT.js @@ -3232,7 +3232,7 @@ var Canvas; ctx = base.getCtx(), scale = base.scaleOffsetX; //var pattern = new Image(); - //pattern.src = "<%= asset_path('cubes.png') %>"; + //pattern.src = Metamaps.Erb['cubes.png'] //var ptrn = ctx.createPattern(pattern, 'repeat'); //ctx.fillStyle = ptrn; ctx.fillStyle = Metamaps.Settings.colors.background; diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 0fe5a224..ae9e9293 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,6 @@ -/* global Metamaps */ +/* global Metamaps, $jit */ + +const $jit = $jit || {} import _ from 'lodash' diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index cdcda4e5..a49aaa4d 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,10 +1,11 @@ -/* global Autolinker, $ */ +/* global $ */ import Backbone from 'backbone' +import Autolinker from 'autolinker' // TODO is this line good or bad // Backbone.$ = window.$ -var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); var Private = { messageHTML: "<div class='chat-message'>" + diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index 68946bea..ae482c46 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -1,7 +1,11 @@ /* global describe, it */ import chai from 'chai' -import Import from '../src/Metamaps/Import' + +// JIT needs window.$jit +require('../../app/assets/javascripts/src/JIT.js') + +const Import = require('../src/Metamaps/Import') const { expect } = chai diff --git a/package.json b/package.json index 925323ac..d15d248e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { + "autolinker": "^0.17.1", "babel-cli": "^6.11.4", "babel-loader": "^6.2.4", "babel-plugin-transform-class-properties": "^6.11.5", @@ -27,6 +28,7 @@ "chai": "^3.5.0", "jquery": "1.12.1", "mocha": "^3.0.2", + "mocha-jsdom": "^1.1.0", "node-uuid": "1.2.0", "react": "^15.3.0", "react-dom": "^15.3.0", From 700119cc9efdfd887a4fefe758c2e3eb931a9341 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 23:04:46 -0400 Subject: [PATCH 033/378] opts can be undefined and throw error --- Gemfile.lock | 3 --- frontend/src/Metamaps/TopicCard.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c2fd0d28..79638ae7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,8 +332,5 @@ DEPENDENCIES uglifier uservoice-ruby -RUBY VERSION - ruby 2.3.0p0 - BUNDLED WITH 1.12.5 diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 7320d285..b92d7edd 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -40,7 +40,7 @@ const TopicCard = { */ showCard: function (node, opts) { var self = TopicCard - + if (!opts) opts = {} var topic = node.getData('topic') self.openTopicCard = topic From bda740491c532a907cba06750c4d50a2b3e9756f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 11:47:40 +0800 Subject: [PATCH 034/378] moved JIT to npm. tests pass. whoop whoop --- app/assets/javascripts/application.js | 1 - frontend/src/Metamaps/JIT.js | 14 +++++++++----- frontend/src/Metamaps/Organize.js | 2 ++ frontend/src/Metamaps/Topic.js | 3 +++ frontend/src/Metamaps/Visualize.js | 2 ++ .../src => frontend/src/patched}/JIT.js | 12 ++++++------ frontend/test/Metamaps.Import.spec.js | 5 +---- 7 files changed, 23 insertions(+), 16 deletions(-) rename {app/assets/javascripts/src => frontend/src/patched}/JIT.js (99%) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 68f2179b..df086157 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,7 +14,6 @@ //= require jquery-ui //= require jquery_ujs //= require_directory ./lib -//= require ./src/JIT //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle //= require ./src/check-canvas-support diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index ae9e9293..7145bf9c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,9 +1,9 @@ /* global Metamaps, $jit */ -const $jit = $jit || {} - import _ from 'lodash' +import $jit from '../patched/JIT' + import Active from './Active' import Control from './Control' import Create from './Create' @@ -21,6 +21,7 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' + /* * Metamaps.Erb * Metamaps.Mappings @@ -284,7 +285,8 @@ const JIT = { ForceDirected: { animateSavedLayout: { modes: ['linear'], - transition: $jit.Trans.Quad.easeInOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Quad.easeInOut'), duration: 800, onComplete: function () { Visualize.mGraph.busy = false @@ -293,7 +295,8 @@ const JIT = { }, animateFDLayout: { modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 800, onComplete: function () { Visualize.mGraph.busy = false @@ -554,7 +557,8 @@ const JIT = { ForceDirected3D: { animate: { modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 2500, onComplete: function () { Visualize.mGraph.busy = false diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index c05f870e..ed005d39 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -2,6 +2,8 @@ import _ from 'lodash' +import $jit from '../patched/JIT' + import Visualize from './Visualize' import JIT from './JIT' diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 412f7ef2..3e8743b6 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import $jit from '../patched/JIT' + import Active from './Active' import AutoLayout from './AutoLayout' import Create from './Create' @@ -15,6 +17,7 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' + /* * Metamaps.Topic.js.erb * diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 047cb81d..df5bab99 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -2,6 +2,8 @@ import _ from 'lodash' +import $jit from '../patched/JIT' + import Active from './Active' import JIT from './JIT' import Router from './Router' diff --git a/app/assets/javascripts/src/JIT.js b/frontend/src/patched/JIT.js similarity index 99% rename from app/assets/javascripts/src/JIT.js rename to frontend/src/patched/JIT.js index 4754871e..7814ecbe 100644 --- a/app/assets/javascripts/src/JIT.js +++ b/frontend/src/patched/JIT.js @@ -20,7 +20,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - (function () { /* File: Core.js @@ -34,7 +33,11 @@ THE SOFTWARE. This variable is the *only* global variable defined in the Toolkit. There are also other interesting properties attached to this variable described below. */ -window.$jit = function(w) { +// START METAMAPS CODE +const $jit = function(w) { +// ORIGINAL: +// window.$jit = function(w) { +// END METAMAPS CODE w = w || window; for(var k in $jit) { if($jit[k].$extend) { @@ -11312,7 +11315,4 @@ $jit.ForceDirected3D.$extend = true; })($jit.ForceDirected3D); - - - - })(); +export default $jit diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index ae482c46..c8ee33b1 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -2,10 +2,7 @@ import chai from 'chai' -// JIT needs window.$jit -require('../../app/assets/javascripts/src/JIT.js') - -const Import = require('../src/Metamaps/Import') +import Import from '../src/Metamaps/Import' const { expect } = chai From e65a5e2d1c6dc05f3703b9cf2bd7d6fa6a09d0a6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 12:00:47 +0800 Subject: [PATCH 035/378] whoops, reenable travis npm test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99c917c7..d607a7a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,4 +18,4 @@ before_script: - nvm use stable - npm install script: - - bundle exec rspec && bundle exec brakeman -q -z || npm test + - bundle exec rspec && bundle exec brakeman -q -z && npm test From 6f91ce5ff52fb622a154e9fb6c2cfbab15ee4df4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:12:27 +0800 Subject: [PATCH 036/378] fix a few more errors --- frontend/src/Metamaps/JIT.js | 4 ++-- frontend/src/Metamaps/Map/InfoBox.js | 1 + frontend/src/Metamaps/Topic.js | 2 +- frontend/src/Metamaps/TopicCard.js | 24 ++++++++---------------- frontend/src/patched/JIT.js | 24 +++++++++++++++++++----- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 7145bf9c..5eccbc6c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -99,11 +99,11 @@ const JIT = { synapsesToRemove.push(s) } else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { - existingEdge = _.findWhere(edges, { + existingEdge = _.find(edges, { nodeFrom: edge.nodeFrom, nodeTo: edge.nodeTo }) || - _.findWhere(edges, { + _.find(edges, { nodeFrom: edge.nodeTo, nodeTo: edge.nodeFrom }) diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index a2cc5de2..ec5c1405 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -3,6 +3,7 @@ import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' +import Util from '../Util' /* * Metamaps.Collaborators diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 3e8743b6..c2f3ff29 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -191,7 +191,7 @@ const Topic = { // opts is additional options in a hash // TODO: move createNewInDB and permitCerateSYnapseAfter into opts - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { + renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { var self = Topic var nodeOnViz, tempPos diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index b92d7edd..dad58565 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -187,17 +187,9 @@ const TopicCard = { } var openMetacodeSelect = function (event) { - var windowWidth - var showcardLeft var TOPICCARD_WIDTH = 300 var METACODESELECT_WIDTH = 404 - var distanceFromEdge - var MAX_METACODELIST_HEIGHT = 270 - var windowHeight - var showcardTop - var topicTitleHeight - var distanceFromBottom if (!selectingMetacode) { selectingMetacode = true @@ -206,9 +198,9 @@ const TopicCard = { // select is accessible onscreen, when opened // while topic card is close to the right // edge of the screen - windowWidth = $(window).width() - showcardLeft = parseInt($('.showcard').css('left')) - distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) + var windowWidth = $(window).width() + var showcardLeft = parseInt($('.showcard').css('left')) + var distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) if (distanceFromEdge < METACODESELECT_WIDTH) { $('.metacodeSelect').addClass('onRightEdge') } @@ -217,11 +209,11 @@ const TopicCard = { // select is accessible onscreen, when opened // while topic card is close to the bottom // edge of the screen - windowHeight = $(window).height() - showcardTop = parseInt($('.showcard').css('top')) - topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) - heightOfSetList = $('.showcard .metacodeSelect').height() - distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) + var windowHeight = $(window).height() + var showcardTop = parseInt($('.showcard').css('top')) + var topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) + var heightOfSetList = $('.showcard .metacodeSelect').height() + var distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { $('.metacodeSelect').addClass('onBottomEdge') } diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index 7814ecbe..af7311be 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -3125,9 +3125,15 @@ var Canvas; }; }, translateToCenter: function(ps) { - var size = this.getSize(), - width = ps? (size.width - ps.width - this.translateOffsetX*2) : size.width; - height = ps? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // START METAMAPS CODE + var size = this.getSize(); + var width = ps ? (size.width - ps.width - this.translateOffsetX*2) : size.width; + var height = ps ? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // ORIGINAL CODE + // var size = this.getSize(), + // width = ps? (size.width - ps.width - this.translateOffsetX*2) : size.width; + // height = ps? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // END METAMAPS CODE var ctx = this.getCtx(); ps && ctx.scale(1/this.scaleOffsetX, 1/this.scaleOffsetY); ctx.translate(width/2, height/2); @@ -5637,7 +5643,11 @@ Graph.Op = { break; case 'fade:seq': case 'fade': case 'fade:con': - that = this; + // START METAMAPS CODE + var that = this; + // ORIGINAL CODE: + // that = this; + // END METAMAPS CODE graph = viz.construct(json); //set alpha to 0 for nodes to add. @@ -5773,7 +5783,11 @@ Graph.Op = { break; case 'fade:seq': case 'fade': case 'fade:con': - that = this; + // START METAMAPS CODE + var that = this; + // ORIGINAL CODE: + // that = this; + // END METAMAPS CODE graph = viz.construct(json); //preprocessing for nodes to delete. //get node property modes to interpolate From df84bd9e1d115c64a20d934e03a76d351aa806b4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:39:15 +0800 Subject: [PATCH 037/378] fix @maps serialization bug if @maps is empty, it returns {"maps":[]}, instead of [] like we expect on the frontend. This commit fixes this issue --- app/controllers/maps_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 7c4a74a7..22e75819 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -19,7 +19,7 @@ class MapsController < ApplicationController redirect_to(root_url) && return if authenticated? respond_with(@maps, @user) end - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -33,7 +33,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -51,7 +51,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -69,7 +69,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -88,7 +88,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -101,7 +101,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end From ce1ad3e24bdd3a0c88ad356510280d58e7a046f4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 15:28:31 +0800 Subject: [PATCH 038/378] update gems --- Gemfile.lock | 107 ++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 79638ae7..7e2590c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,50 +1,50 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.0.0) - actionpack (= 5.0.0) + actioncable (5.0.0.1) + actionpack (= 5.0.0.1) nio4r (~> 1.2) websocket-driver (~> 0.6.1) - actionmailer (5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) + actionmailer (5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.0) - actionview (= 5.0.0) - activesupport (= 5.0.0) + actionpack (5.0.0.1) + actionview (= 5.0.0.1) + activesupport (= 5.0.0.1) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.0) - activesupport (= 5.0.0) + actionview (5.0.0.1) + activesupport (= 5.0.0.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - active_model_serializers (0.10.1) + active_model_serializers (0.10.2) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) jsonapi (~> 0.1.1.beta2) railties (>= 4.1, < 6) - activejob (5.0.0) - activesupport (= 5.0.0) + activejob (5.0.0.1) + activesupport (= 5.0.0.1) globalid (>= 0.3.6) - activemodel (5.0.0) - activesupport (= 5.0.0) - activerecord (5.0.0) - activemodel (= 5.0.0) - activesupport (= 5.0.0) + activemodel (5.0.0.1) + activesupport (= 5.0.0.1) + activerecord (5.0.0.1) + activemodel (= 5.0.0.1) + activesupport (= 5.0.0.1) arel (~> 7.0) - activesupport (5.0.0) + activesupport (5.0.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) addressable (2.3.8) - arel (7.1.1) + arel (7.1.2) ast (2.3.0) aws-sdk (1.66.0) aws-sdk-v1 (= 1.66.0) @@ -61,7 +61,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - brakeman (3.3.3) + brakeman (3.4.0) builder (3.2.2) byebug (9.0.5) climate_control (0.0.3) @@ -120,7 +120,7 @@ GEM jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.1.1) + jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -153,15 +153,15 @@ GEM pkg-config (~> 1.1.7) oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.6) + paperclip (4.3.7) activemodel (>= 3.2.0) activesupport (>= 3.2.0) cocaine (~> 0.5.5) mime-types mimemagic (= 0.3.0) - parser (2.3.1.2) + parser (2.3.1.4) ast (~> 2.2) - pg (0.18.4) + pg (0.19.0) pkg-config (1.1.7) powerpack (0.1.1) pry (0.10.4) @@ -175,22 +175,22 @@ GEM pry (>= 0.9.10) pundit (1.1.0) activesupport (>= 3.0.0) - pundit_extra (0.2.0) + pundit_extra (0.3.0) rack (2.0.1) rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) - rails (5.0.0) - actioncable (= 5.0.0) - actionmailer (= 5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) - activemodel (= 5.0.0) - activerecord (= 5.0.0) - activesupport (= 5.0.0) + rails (5.0.0.1) + actioncable (= 5.0.0.1) + actionmailer (= 5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) + activemodel (= 5.0.0.1) + activerecord (= 5.0.0.1) + activesupport (= 5.0.0.1) bundler (>= 1.3.0, < 2.0) - railties (= 5.0.0) + railties (= 5.0.0.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.1) activesupport (>= 4.2.0, < 6.0) @@ -204,18 +204,18 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.0.0) - actionpack (= 5.0.0) - activesupport (= 5.0.0) + railties (5.0.0.1) + actionpack (= 5.0.0.1) + activesupport (= 5.0.0.1) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) - rake (11.2.2) + rake (11.3.0) redis (3.3.1) - responders (2.2.0) + responders (2.3.0) railties (>= 4.2.0, < 5.1) - rspec-core (3.5.2) + rspec-core (3.5.3) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) @@ -223,7 +223,7 @@ GEM rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) - rspec-rails (3.5.1) + rspec-rails (3.5.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -232,7 +232,7 @@ GEM rspec-mocks (~> 3.5.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) - rubocop (0.42.0) + rubocop (0.43.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -240,7 +240,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) sass (3.4.22) - sass-rails (5.0.5) + sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -248,19 +248,19 @@ GEM tilt (>= 1.1, < 3) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) - simplecov (0.11.2) + simplecov (0.12.0) docile (~> 1.1.0) - json (~> 1.8) + json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) slop (3.6.0) snorlax (0.1.6) rails (> 4.1) - sprockets (3.6.2) + sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.1.1) + sprockets-rails (3.2.0) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -270,9 +270,9 @@ GEM tunemygc (1.0.68) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.1) + uglifier (3.0.2) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.0) + unicode-display_width (1.1.1) uservoice-ruby (0.0.11) ezcrypto (>= 0.7.2) json (>= 1.7.5) @@ -332,5 +332,8 @@ DEPENDENCIES uglifier uservoice-ruby +RUBY VERSION + ruby 2.3.0p0 + BUNDLED WITH 1.12.5 From 117b7910bf8465feb444a1399cbe15e51fa5cb74 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:40:30 +0800 Subject: [PATCH 039/378] test --- spec/controllers/maps_controller_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 278ec559..a10c20d1 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -8,6 +8,21 @@ RSpec.describe MapsController, type: :controller do sign_in create(:user) end + describe 'GET #activemaps' do + context 'always returns an array' do + it 'with 0 records' do + Map.delete_all + get :activemaps, format: :json + expect(JSON.parse(response.body)).to eq [] + end + it 'with 1 record' do + map = create(:map) + get :activemaps, format: :json + expect(JSON.parse(response.body).class).to be Array + end + end + end + describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do From a7338f8960d0b9fe7296d38d196cff007b5e263e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:49:26 +0800 Subject: [PATCH 040/378] safer git dating --- config/initializers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 94f54a37..c378cb6f 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,2 @@ METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze -METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad' --date=format:'%b %d, %Y'`.freeze +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1,2,4).join(' ').freeze From bb5ba4861d421dffb21b1aeef44269993cea6a53 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:36:47 +0800 Subject: [PATCH 041/378] [WIP] code climate config file (#654) code climate config file --- .codeclimate.yml | 32 ++++++++++++++++++++++++++++++++ .eslintignore | 3 +++ .eslintrc.js | 9 +++++++++ package.json | 7 +++++++ 4 files changed, 51 insertions(+) create mode 100644 .codeclimate.yml create mode 100644 .eslintignore create mode 100644 .eslintrc.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..9156645c --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,32 @@ +--- +engines: + brakeman: + enabled: true + bundler-audit: + enabled: true + duplication: + enabled: true + config: + languages: + - ruby + - javascript + eslint: + enabled: true + fixme: + enabled: true + rubocop: + enabled: true +ratings: + paths: + - 'Gemfile.lock' + - '**.erb' + - '**.rb' + - '**.js' + - '**.jsx' +exclude_paths: +- app/assets/images/ +- app/assets/javascripts/lib/ +- frontend/src/patched/ +- db/ +- script/ +- spec/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..86a563fd --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +**/*{.,-}min.js +frontend/src/patched/* +app/assets/javascripts/lib/* diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..bc65fe94 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + "sourceType": "module", + "parser": "babel-eslint", + "extends": "standard", + "installedESLint": true, + "plugins": [ + "standard" + ] +}; diff --git a/package.json b/package.json index d15d248e..bb32377d 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,12 @@ "socket.io": "0.9.12", "underscore": "^1.4.4", "webpack": "^1.13.1" + }, + "devDependencies": { + "babel-eslint": "^6.1.2", + "eslint": "^3.5.0", + "eslint-config-standard": "^6.0.1", + "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-standard": "^2.0.0" } } From 04a302736856d15ab3503e1902c7c5d6796fb803 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:43:54 +0800 Subject: [PATCH 042/378] code climate linked to travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d607a7a5..37186702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,3 +19,6 @@ before_script: - npm install script: - bundle exec rspec && bundle exec brakeman -q -z && npm test +addons: + code_climate: + repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e From 8255653d24ac5bcbe5a079d244e44b22e9515075 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:51:34 +0800 Subject: [PATCH 043/378] disable duplication checking in code climate for now --- .codeclimate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 9156645c..d3c19ad6 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,7 +5,7 @@ engines: bundler-audit: enabled: true duplication: - enabled: true + enabled: false config: languages: - ruby From c76657ecb4dd46ee289cfdbafc2d883bdb0a9204 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:54:05 +0800 Subject: [PATCH 044/378] fix restful controller style issuse --- app/controllers/api/v2/restful_controller.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index e73f21b8..de86dafd 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -147,10 +147,9 @@ module Api search_column = -> (column) { table[column].matches(safe_query) } condition = searchable_columns.reduce(nil) do |prev, column| - next search_column.(column) if prev.nil? - search_column.(column).or(prev) + next search_column.call(column) if prev.nil? + search_column.call(column).or(prev) end - puts collection.where(condition).to_sql collection.where(condition) end From b8ae2c4b6a9c7b358cf1e8510aad26054c68a02b Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 15:45:11 -0400 Subject: [PATCH 045/378] Update Router.js --- frontend/src/Metamaps/Router.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index c5f1c9a7..073c1d1b 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -157,8 +157,6 @@ const _Router = Backbone.Router.extend({ maps: function (id) { clearTimeout(this.timeoutId) - document.title = 'Map ' + id + ' | Metamaps' - this.currentSection = 'map' this.currentPage = id From f41ece6f1c5a7e59c560518a1b57d81a31df0398 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 15:47:37 -0400 Subject: [PATCH 046/378] Update index.js --- frontend/src/Metamaps/Map/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 3dd1c531..5de9e061 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -76,6 +76,8 @@ const Map = { var map = Active.Map var mapper = Active.Mapper + document.title = map.attributes.name + ' | Metamaps' + // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { $('.wrapper').addClass('canEditMap') From afa0cc96b9eea6b717ab21e2dd102aa8f400e477 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 16:06:28 -0400 Subject: [PATCH 047/378] Update index.js --- frontend/src/Metamaps/Map/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 5de9e061..944a387b 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -76,7 +76,7 @@ const Map = { var map = Active.Map var mapper = Active.Mapper - document.title = map.attributes.name + ' | Metamaps' + document.title = map.get('name') + ' | Metamaps' // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { From 0ace202ace342b389dba3e81181799bf7fdc2882 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 11:00:46 +0800 Subject: [PATCH 048/378] automatic rubocop updates --- Gemfile | 1 + Rakefile | 1 + Vagrantfile | 3 ++- .../api/v1/deprecated_controller.rb | 3 ++- app/controllers/api/v1/mappings_controller.rb | 1 + app/controllers/api/v1/maps_controller.rb | 1 + app/controllers/api/v1/synapses_controller.rb | 1 + app/controllers/api/v1/tokens_controller.rb | 1 + app/controllers/api/v1/topics_controller.rb | 1 + app/controllers/api/v2/mappings_controller.rb | 1 + app/controllers/api/v2/maps_controller.rb | 1 + app/controllers/api/v2/restful_controller.rb | 9 +++++---- app/controllers/api/v2/sessions_controller.rb | 3 ++- app/controllers/api/v2/synapses_controller.rb | 1 + app/controllers/api/v2/tokens_controller.rb | 1 + app/controllers/api/v2/topics_controller.rb | 1 + app/controllers/application_controller.rb | 8 ++++---- app/controllers/main_controller.rb | 19 ++++++++++--------- app/controllers/mappings_controller.rb | 1 + app/controllers/maps_controller.rb | 13 +++++-------- app/controllers/messages_controller.rb | 1 + app/controllers/metacode_sets_controller.rb | 1 + app/controllers/synapses_controller.rb | 1 + app/controllers/topics_controller.rb | 1 + app/controllers/users/passwords_controller.rb | 3 ++- .../users/registrations_controller.rb | 1 + app/controllers/users_controller.rb | 1 + app/helpers/application_helper.rb | 1 + app/helpers/content_helper.rb | 1 + app/helpers/devise_helper.rb | 1 + app/helpers/in_metacode_sets_helper.rb | 1 + app/helpers/main_helper.rb | 1 + app/helpers/mapping_helper.rb | 1 + app/helpers/maps_helper.rb | 3 ++- app/helpers/metacode_sets_helper.rb | 1 + app/helpers/metacodes_helper.rb | 1 + app/helpers/synapses_helper.rb | 1 + app/helpers/topics_helper.rb | 5 +++-- app/helpers/users_helper.rb | 1 + app/mailers/application_mailer.rb | 1 + app/mailers/map_mailer.rb | 1 + app/models/application_record.rb | 1 + app/models/concerns/routing.rb | 1 + app/models/event.rb | 1 + .../events/conversation_started_on_map.rb | 1 + app/models/events/new_mapping.rb | 1 + app/models/events/user_present_on_map.rb | 1 + app/models/in_metacode_set.rb | 1 + app/models/map.rb | 3 ++- app/models/mapping.rb | 1 + app/models/message.rb | 1 + app/models/metacode.rb | 1 + app/models/metacode_set.rb | 1 + app/models/permitted_params.rb | 1 + app/models/star.rb | 1 + app/models/synapse.rb | 6 ++---- app/models/token.rb | 1 + app/models/topic.rb | 6 ++---- app/models/user.rb | 7 ++++--- app/models/user_map.rb | 1 + app/models/user_preference.rb | 3 ++- app/models/webhook.rb | 1 + app/models/webhooks/slack/base.rb | 1 + .../slack/conversation_started_on_map.rb | 1 + .../webhooks/slack/synapse_added_to_map.rb | 1 + .../webhooks/slack/topic_added_to_map.rb | 1 + .../webhooks/slack/user_present_on_map.rb | 1 + app/policies/application_policy.rb | 3 ++- app/policies/main_policy.rb | 1 + app/policies/map_policy.rb | 1 + app/policies/mapping_policy.rb | 1 + app/policies/message_policy.rb | 1 + app/policies/synapse_policy.rb | 1 + app/policies/token_policy.rb | 1 + app/policies/topic_policy.rb | 1 + .../api/v2/application_serializer.rb | 1 + app/serializers/api/v2/event_serializer.rb | 1 + app/serializers/api/v2/map_serializer.rb | 15 ++++++++------- app/serializers/api/v2/mapping_serializer.rb | 11 ++++++----- app/serializers/api/v2/metacode_serializer.rb | 9 +++++---- app/serializers/api/v2/synapse_serializer.rb | 13 +++++++------ app/serializers/api/v2/token_serializer.rb | 7 ++++--- app/serializers/api/v2/topic_serializer.rb | 15 ++++++++------- app/serializers/api/v2/user_serializer.rb | 9 +++++---- app/serializers/api/v2/webhook_serializer.rb | 1 + app/services/map_export_service.rb | 1 + app/services/perm.rb | 1 + app/services/webhook_service.rb | 1 + config.ru | 1 + config/application.rb | 1 + config/boot.rb | 1 + config/environment.rb | 1 + config/environments/development.rb | 1 + config/environments/production.rb | 1 + config/environments/test.rb | 1 + config/initializers/access_codes.rb | 1 + .../initializers/active_model_serializers.rb | 1 + .../application_controller_renderer.rb | 1 + config/initializers/assets.rb | 3 ++- config/initializers/backtrace_silencers.rb | 1 + config/initializers/cookies_serializer.rb | 1 + config/initializers/cors.rb | 1 + config/initializers/devise.rb | 1 + config/initializers/doorkeeper.rb | 1 + config/initializers/exception_notification.rb | 1 + .../initializers/filter_parameter_logging.rb | 1 + config/initializers/inflections.rb | 1 + config/initializers/kaminari_config.rb | 1 + config/initializers/mime_types.rb | 1 + config/initializers/new_framework_defaults.rb | 1 + config/initializers/paperclip.rb | 1 + config/initializers/secret_token.rb | 1 + config/initializers/session_store.rb | 1 + config/initializers/uservoice.rb | 1 + config/initializers/version.rb | 5 +++-- config/initializers/wrap_parameters.rb | 1 + config/puma.rb | 7 ++++--- config/routes.rb | 1 + config/spring.rb | 1 + lib/tasks/extensions.rake | 5 +++-- lib/tasks/heroku.rake | 3 ++- lib/tasks/perms.rake | 1 + script/rails | 1 + spec/api/v2/mappings_api_spec.rb | 1 + spec/api/v2/maps_api_spec.rb | 1 + spec/api/v2/synapses_api_spec.rb | 1 + spec/api/v2/tokens_api_spec.rb | 1 + spec/api/v2/topics_api_spec.rb | 1 + spec/controllers/mappings_controller_spec.rb | 1 + spec/controllers/maps_controller_spec.rb | 1 + spec/controllers/metacodes_controller_spec.rb | 1 + spec/controllers/synapses_controller_spec.rb | 1 + spec/controllers/topics_controller_spec.rb | 1 + spec/factories/mappings.rb | 1 + spec/factories/maps.rb | 1 + spec/factories/metacodes.rb | 1 + spec/factories/synapses.rb | 3 ++- spec/factories/tokens.rb | 1 + spec/factories/topics.rb | 1 + spec/factories/users.rb | 1 + spec/mailers/previews/map_mailer_preview.rb | 1 + spec/models/map_spec.rb | 1 + spec/models/mapping_spec.rb | 1 + spec/models/metacode_spec.rb | 1 + spec/models/synapse_spec.rb | 1 + spec/models/token_spec.rb | 1 + spec/models/topic_spec.rb | 1 + spec/policies/map_policy_spec.rb | 1 + spec/policies/mapping_policy_spec.rb | 1 + spec/policies/synapse_policy.rb | 1 + spec/policies/topic_policy_spec.rb | 1 + spec/rails_helper.rb | 1 + spec/spec_helper.rb | 1 + spec/support/controller_helpers.rb | 1 + spec/support/factory_girl.rb | 1 + spec/support/pundit.rb | 1 + spec/support/schema_matcher.rb | 1 + spec/support/simplecov.rb | 2 +- 158 files changed, 239 insertions(+), 93 deletions(-) diff --git a/Gemfile b/Gemfile index 4c58772c..b4a0967b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true source 'https://rubygems.org' ruby '2.3.0' diff --git a/Rakefile b/Rakefile index 30cf58f5..74eb42c7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ #!/usr/bin/env rake +# frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/Vagrantfile b/Vagrantfile index 52e040cf..0fa3e8da 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true # -*- mode: ruby -*- # vi: set ft=ruby : @@ -31,7 +32,7 @@ sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '3112';" SCRIPT -VAGRANTFILE_API_VERSION = '2'.freeze +VAGRANTFILE_API_VERSION = '2' Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = 'trusty64' diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index ed68b897..6f6b5f15 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module Api module V1 class DeprecatedController < ApplicationController def method_missing - render json: { error: "/api/v1 is deprecated! Please use /api/v2 instead." } + render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } end end end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb index 35c7d6bd..8ba6e704 100644 --- a/app/controllers/api/v1/mappings_controller.rb +++ b/app/controllers/api/v1/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class MappingsController < DeprecatedController diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb index 056810f1..0ff6f472 100644 --- a/app/controllers/api/v1/maps_controller.rb +++ b/app/controllers/api/v1/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class MapsController < DeprecatedController diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb index e2111e95..32522e52 100644 --- a/app/controllers/api/v1/synapses_controller.rb +++ b/app/controllers/api/v1/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class SynapsesController < DeprecatedController diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb index c96b1065..9df2094a 100644 --- a/app/controllers/api/v1/tokens_controller.rb +++ b/app/controllers/api/v1/tokens_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class TokensController < DeprecatedController diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb index e974fff3..d316bfa8 100644 --- a/app/controllers/api/v1/topics_controller.rb +++ b/app/controllers/api/v1/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class TopicsController < DeprecatedController diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 7f0d9513..86aba865 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class MappingsController < RestfulController diff --git a/app/controllers/api/v2/maps_controller.rb b/app/controllers/api/v2/maps_controller.rb index fd54fa7b..0bcd9bee 100644 --- a/app/controllers/api/v2/maps_controller.rb +++ b/app/controllers/api/v2/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class MapsController < RestfulController diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index de86dafd..d7a1856e 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class RestfulController < ActionController::Base @@ -101,9 +102,9 @@ module Api next_page = current_page < total_pages ? current_page + 1 : 0 base_url = request.base_url + request.path - nxt = request.query_parameters.merge(page: next_page).map{|x| x.join('=')}.join('&') - prev = request.query_parameters.merge(page: prev_page).map{|x| x.join('=')}.join('&') - last = request.query_parameters.merge(page: total_pages).map{|x| x.join('=')}.join('&') + nxt = request.query_parameters.merge(page: next_page).map { |x| x.join('=') }.join('&') + prev = request.query_parameters.merge(page: prev_page).map { |x| x.join('=') }.join('&') + last = request.query_parameters.merge(page: total_pages).map { |x| x.join('=') }.join('&') response.headers['Link'] = [ %(<#{base_url}?#{nxt}>; rel="next"), %(<#{base_url}?#{prev}>; rel="prev"), @@ -163,7 +164,7 @@ module Api builder = builder.order(sort => direction) end end - return builder + builder end def visible_records diff --git a/app/controllers/api/v2/sessions_controller.rb b/app/controllers/api/v2/sessions_controller.rb index 3aefa214..2aa93669 100644 --- a/app/controllers/api/v2/sessions_controller.rb +++ b/app/controllers/api/v2/sessions_controller.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true module Api module V2 class SessionsController < ApplicationController def create @user = User.find_by(email: params[:email]) - if @user && @user.valid_password(params[:password]) + if @user&.valid_password(params[:password]) sign_in(@user) render json: @user else diff --git a/app/controllers/api/v2/synapses_controller.rb b/app/controllers/api/v2/synapses_controller.rb index 6572997d..6484699e 100644 --- a/app/controllers/api/v2/synapses_controller.rb +++ b/app/controllers/api/v2/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class SynapsesController < RestfulController diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 6eeb102b..d1a6b255 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class TokensController < RestfulController diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb index 74fa7105..22e534ce 100644 --- a/app/controllers/api/v2/topics_controller.rb +++ b/app/controllers/api/v2/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class TopicsController < RestfulController diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 58c996c5..7735c681 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationController < ActionController::Base include ApplicationHelper include Pundit @@ -60,10 +61,9 @@ class ApplicationController < ActionController::Base end def require_admin - unless authenticated? && admin? - redirect_to root_url, notice: 'You need to be an admin for that.' - return false - end + return true if authenticated? && admin? + redirect_to root_url, notice: 'You need to be an admin for that.' + false end def user diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 01304328..0d6af64b 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MainController < ApplicationController include TopicsHelper include MapsHelper @@ -12,13 +13,13 @@ class MainController < ApplicationController def home @maps = policy_scope(Map).order('updated_at DESC').page(1).per(20) respond_to do |format| - format.html { - if !authenticated? - render 'main/home' - else - render 'maps/activemaps' - end - } + format.html do + if !authenticated? + render 'main/home' + else + render 'maps/activemaps' + end + end end end @@ -163,8 +164,8 @@ class MainController < ApplicationController @synapses = [] end - #limit to 5 results - @synapses = @synapses.to_a.slice(0,5) + # limit to 5 results + @synapses = @synapses.to_a.slice(0, 5) render json: autocomplete_synapse_array_json(@synapses) end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 3d162c0f..de2c8ea1 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MappingsController < ApplicationController before_action :require_user, only: [:create, :update, :destroy] after_action :verify_authorized, except: :index diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 7c4a74a7..8e7b4136 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] @@ -107,14 +108,14 @@ class MapsController < ApplicationController # GET maps/new def new - @map = Map.new(name: "Untitled Map", permission: "public", arranged: true) + @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) authorize @map respond_to do |format| format.html do @map.user = current_user @map.save - redirect_to(map_path(@map) + '?new') + redirect_to(map_path(@map) + '?new') end end end @@ -305,9 +306,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - if not star - star = Star.create(map_id: @map.id, user_id: current_user.id) - end + star = Star.create(map_id: @map.id, user_id: current_user.id) unless star respond_to do |format| format.json do @@ -321,9 +320,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - if star - star.delete - end + star&.delete respond_to do |format| format.json do diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index ec59a2a4..dd3544e9 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MessagesController < ApplicationController before_action :require_user, except: [:show] after_action :verify_authorized diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index a57c557f..8fec58cc 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodeSetsController < ApplicationController before_action :require_admin diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index ddb3e5ab..8fc31688 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class SynapsesController < ApplicationController include TopicsHelper diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 30ac57fd..1b966ca2 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TopicsController < ApplicationController include TopicsHelper diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index ee7b8667..bffe3ab6 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Users::PasswordsController < Devise::PasswordsController protected @@ -5,7 +6,7 @@ class Users::PasswordsController < Devise::PasswordsController signed_in_root_path(resource) end - def after_sending_reset_password_instructions_path_for(resource_name) + def after_sending_reset_password_instructions_path_for(_resource_name) new_user_session_path if is_navigational_format? end end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 8895cfd2..21cd9666 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :configure_account_update_params, only: [:update] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0ea95211..a9fff9de 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UsersController < ApplicationController before_action :require_user, only: [:edit, :update, :updatemetacodes] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 555a32d2..57b24106 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ApplicationHelper def get_metacodeset @m = current_user.settings.metacodes diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 4a8820f0..4fb2fe84 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ContentHelper def resource_name :user diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index 5081cba0..4b34effc 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module DeviseHelper def devise_error_messages! resource.errors.to_a[0] diff --git a/app/helpers/in_metacode_sets_helper.rb b/app/helpers/in_metacode_sets_helper.rb index 52a47cdc..0c45fd98 100644 --- a/app/helpers/in_metacode_sets_helper.rb +++ b/app/helpers/in_metacode_sets_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module InMetacodeSetsHelper end diff --git a/app/helpers/main_helper.rb b/app/helpers/main_helper.rb index 826effed..33378f43 100644 --- a/app/helpers/main_helper.rb +++ b/app/helpers/main_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MainHelper end diff --git a/app/helpers/mapping_helper.rb b/app/helpers/mapping_helper.rb index 7055739f..9dd903ca 100644 --- a/app/helpers/mapping_helper.rb +++ b/app/helpers/mapping_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MappingHelper end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 3f60fe4d..8ca7b047 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module MapsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_map_array_json(maps) @@ -16,7 +17,7 @@ module MapsHelper contributorTip = '' firstContributorImage = 'https://s3.amazonaws.com/metamaps-assets/site/user.png' - if m.contributors.count > 0 + if m.contributors.count.positive? firstContributorImage = m.contributors[0].image.url(:thirtytwo) m.contributors.each_with_index do |c, _index| userImage = c.image.url(:thirtytwo) diff --git a/app/helpers/metacode_sets_helper.rb b/app/helpers/metacode_sets_helper.rb index 668ceb5c..9a6e09c7 100644 --- a/app/helpers/metacode_sets_helper.rb +++ b/app/helpers/metacode_sets_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MetacodeSetsHelper end diff --git a/app/helpers/metacodes_helper.rb b/app/helpers/metacodes_helper.rb index c896d26d..d00f1ef5 100644 --- a/app/helpers/metacodes_helper.rb +++ b/app/helpers/metacodes_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MetacodesHelper end diff --git a/app/helpers/synapses_helper.rb b/app/helpers/synapses_helper.rb index 470292e5..471f0e05 100644 --- a/app/helpers/synapses_helper.rb +++ b/app/helpers/synapses_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SynapsesHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_generic_json(unique) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index bb589e9a..32697db5 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) @@ -7,7 +8,7 @@ module TopicsHelper topic['id'] = t.id topic['label'] = t.name topic['value'] = t.name - topic['description'] = t.desc ? t.desc.truncate(70) : '' # make this return matched results + topic['description'] = t.desc ? t.desc&.truncate(70) # make this return matched results topic['type'] = t.metacode.name topic['typeImageURL'] = t.metacode.icon topic['permission'] = t.permission @@ -34,7 +35,7 @@ module TopicsHelper # add the node to the array array.push(node) - return array if count == 0 + return array if count.zero? count -= 1 diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 8144dfd8..379cf20a 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module UsersHelper # build custom json autocomplete for typeahead def autocomplete_user_array_json(users) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d934e218..59a2175a 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationMailer < ActionMailer::Base default from: 'team@metamaps.cc' layout 'mailer' diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index 94e8ebd5..e70d0b82 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..767a072b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/concerns/routing.rb b/app/models/concerns/routing.rb index 2f8467bf..ddbfad6f 100644 --- a/app/models/concerns/routing.rb +++ b/app/models/concerns/routing.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Routing extend ActiveSupport::Concern include Rails.application.routes.url_helpers diff --git a/app/models/event.rb b/app/models/event.rb index 90407314..02c6d698 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Event < ApplicationRecord KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze diff --git a/app/models/events/conversation_started_on_map.rb b/app/models/events/conversation_started_on_map.rb index 4ca922be..20fb89c1 100644 --- a/app/models/events/conversation_started_on_map.rb +++ b/app/models/events/conversation_started_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::ConversationStartedOnMap < Event # after_create :notify_users! diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb index d7b91576..889c69bc 100644 --- a/app/models/events/new_mapping.rb +++ b/app/models/events/new_mapping.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::NewMapping < Event # after_create :notify_users! diff --git a/app/models/events/user_present_on_map.rb b/app/models/events/user_present_on_map.rb index 45726002..38d524ef 100644 --- a/app/models/events/user_present_on_map.rb +++ b/app/models/events/user_present_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::UserPresentOnMap < Event # after_create :notify_users! diff --git a/app/models/in_metacode_set.rb b/app/models/in_metacode_set.rb index de1f2514..78dc1c29 100644 --- a/app/models/in_metacode_set.rb +++ b/app/models/in_metacode_set.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class InMetacodeSet < ApplicationRecord belongs_to :metacode, class_name: 'Metacode', foreign_key: 'metacode_id' belongs_to :metacode_set, class_name: 'MetacodeSet', foreign_key: 'metacode_set_id' diff --git a/app/models/map.rb b/app/models/map.rb index f59eb790..9c30479f 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Map < ApplicationRecord belongs_to :user @@ -19,7 +20,7 @@ class Map < ApplicationRecord thumb: ['188x126#', :png] #:full => ['940x630#', :png] }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } diff --git a/app/models/mapping.rb b/app/models/mapping.rb index eba7a6d2..f7219008 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Mapping < ApplicationRecord scope :topicmapping, -> { where(mappable_type: :Topic) } scope :synapsemapping, -> { where(mappable_type: :Synapse) } diff --git a/app/models/message.rb b/app/models/message.rb index 348c5d4e..682b7e51 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Message < ApplicationRecord belongs_to :user belongs_to :resource, polymorphic: true diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 9b05bee5..c97c4fb5 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Metacode < ApplicationRecord has_many :in_metacode_sets has_many :metacode_sets, through: :in_metacode_sets diff --git a/app/models/metacode_set.rb b/app/models/metacode_set.rb index c52811fd..72bcc719 100644 --- a/app/models/metacode_set.rb +++ b/app/models/metacode_set.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodeSet < ApplicationRecord belongs_to :user has_many :in_metacode_sets diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 0ccea1c8..d0696985 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class PermittedParams < Struct.new(:params) %w(map synapse topic mapping token).each do |kind| define_method(kind) do diff --git a/app/models/star.rb b/app/models/star.rb index 52a77044..dcaaa559 100644 --- a/app/models/star.rb +++ b/app/models/star.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Star < ActiveRecord::Base belongs_to :user belongs_to :map diff --git a/app/models/synapse.rb b/app/models/synapse.rb index afd40a25..c7161469 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' @@ -44,10 +45,7 @@ class Synapse < ApplicationRecord # :nocov: def calculated_permission if defer_to_map - defer_to_map.permission - else - permission - end + defer_to_map&.permission end # :nocov: diff --git a/app/models/token.rb b/app/models/token.rb index 9103aebc..9cd93043 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Token < ApplicationRecord belongs_to :user diff --git a/app/models/topic.rb b/app/models/topic.rb index c250338b..09d61897 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Topic < ApplicationRecord include TopicsHelper @@ -74,10 +75,7 @@ class Topic < ApplicationRecord def calculated_permission if defer_to_map - defer_to_map.permission - else - permission - end + defer_to_map&.permission end def as_json(_options = {}) diff --git a/app/models/user.rb b/app/models/user.rb index 876e10cd..4da66e57 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'open-uri' class User < ApplicationRecord @@ -41,7 +42,7 @@ class User < ApplicationRecord default_url: 'https://s3.amazonaws.com/metamaps-assets/site/user.png' # Validate the attached image is image/jpg, image/png, etc - validates_attachment_content_type :image, content_type: %r(\Aimage/.*\Z) + validates_attachment_content_type :image, content_type: %r{\Aimage/.*\Z} # override default as_json def as_json(_options = {}) @@ -79,8 +80,8 @@ class User < ApplicationRecord end end - def starred_map?(map) - return self.stars.where(map_id: map.id).exists? + def starred_map?(map) + stars.where(map_id: map.id).exists? end def settings diff --git a/app/models/user_map.rb b/app/models/user_map.rb index c48cfb96..dc268047 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UserMap < ApplicationRecord belongs_to :map belongs_to :user diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 3aadbdb3..29ca3948 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UserPreference attr_accessor :metacodes @@ -9,7 +10,7 @@ class UserPreference array.push(metacode.id.to_s) if metacode rescue ActiveRecord::StatementInvalid if m == 'Action' - Rails.logger.warn("TODO: remove this travis workaround in user_preference.rb") + Rails.logger.warn('TODO: remove this travis workaround in user_preference.rb') end end end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 6389398e..65057411 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhook < ApplicationRecord belongs_to :hookable, polymorphic: true diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb index 97cf1f04..960775dd 100644 --- a/app/models/webhooks/slack/base.rb +++ b/app/models/webhooks/slack/base.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Webhooks::Slack::Base = Struct.new(:event) do include Routing diff --git a/app/models/webhooks/slack/conversation_started_on_map.rb b/app/models/webhooks/slack/conversation_started_on_map.rb index 5fa325d0..daf2270e 100644 --- a/app/models/webhooks/slack/conversation_started_on_map.rb +++ b/app/models/webhooks/slack/conversation_started_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::ConversationStartedOnMap < Webhooks::Slack::Base def text "There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}" diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb index 5dc636e1..5157afa7 100644 --- a/app/models/webhooks/slack/synapse_added_to_map.rb +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base def text "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps}*" diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index 07a20759..d3a19760 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps}*" diff --git a/app/models/webhooks/slack/user_present_on_map.rb b/app/models/webhooks/slack/user_present_on_map.rb index 666d5121..c3185e48 100644 --- a/app/models/webhooks/slack/user_present_on_map.rb +++ b/app/models/webhooks/slack/user_present_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base def text "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index a9835c98..348ef5f2 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record @@ -39,7 +40,7 @@ class ApplicationPolicy # explicitly say they want to (E.g. seeing/editing/deleting private # maps - they should be able to, but not by accident) def admin_override - user && user.admin + user&.admin end def scope diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index 77ab373c..e0ffc30b 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MainPolicy < ApplicationPolicy def initialize(user, _record) @user = user diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 0a2b33ce..3894520c 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 1cd99783..efcb798b 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MappingPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index 8df6e916..f35a2895 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MessagePolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 310b3947..f9557e70 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb index e150fec9..cd9a5ab7 100644 --- a/app/policies/token_policy.rb +++ b/app/policies/token_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TokenPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 7bca6770..2484ee54 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index f943646c..55a9b8a0 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class ApplicationSerializer < ActiveModel::Serializer diff --git a/app/serializers/api/v2/event_serializer.rb b/app/serializers/api/v2/event_serializer.rb index 644598cf..c875056a 100644 --- a/app/serializers/api/v2/event_serializer.rb +++ b/app/serializers/api/v2/event_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class EventSerializer < ApplicationSerializer diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index 438f97ee..0a0be2c0 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true module Api module V2 class MapSerializer < ApplicationSerializer attributes :id, - :name, - :desc, - :permission, - :screenshot, - :created_at, - :updated_at + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at def self.embeddable { @@ -20,7 +21,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb index dc36421e..19e7318e 100644 --- a/app/serializers/api/v2/mapping_serializer.rb +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class MappingSerializer < ApplicationSerializer attributes :id, - :created_at, - :updated_at, - :mappable_id, - :mappable_type + :created_at, + :updated_at, + :mappable_id, + :mappable_type attribute :xloc, if: -> { object.mappable_type == 'Topic' } attribute :yloc, if: -> { object.mappable_type == 'Topic' } @@ -17,7 +18,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb index 4f4daa35..16013e33 100644 --- a/app/serializers/api/v2/metacode_serializer.rb +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class MetacodeSerializer < ApplicationSerializer attributes :id, - :name, - :manual_icon, - :color, - :aws_icon + :name, + :manual_icon, + :color, + :aws_icon end end end diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb index 9ef86660..f647022c 100644 --- a/app/serializers/api/v2/synapse_serializer.rb +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true module Api module V2 class SynapseSerializer < ApplicationSerializer attributes :id, - :desc, - :category, - :permission, - :created_at, - :updated_at + :desc, + :category, + :permission, + :created_at, + :updated_at def self.embeddable { @@ -16,7 +17,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/token_serializer.rb b/app/serializers/api/v2/token_serializer.rb index 18d15d15..8f86757b 100644 --- a/app/serializers/api/v2/token_serializer.rb +++ b/app/serializers/api/v2/token_serializer.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true module Api module V2 class TokenSerializer < ApplicationSerializer attributes :id, - :token, - :description, - :created_at + :token, + :description, + :created_at end end end diff --git a/app/serializers/api/v2/topic_serializer.rb b/app/serializers/api/v2/topic_serializer.rb index 48d1d6de..8da46a2a 100644 --- a/app/serializers/api/v2/topic_serializer.rb +++ b/app/serializers/api/v2/topic_serializer.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true module Api module V2 class TopicSerializer < ApplicationSerializer attributes :id, - :name, - :desc, - :link, - :permission, - :created_at, - :updated_at + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at def self.embeddable { @@ -16,7 +17,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index fdfffae0..e97bc420 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class UserSerializer < ApplicationSerializer attributes :id, - :name, - :avatar, - :is_admin, - :generation + :name, + :avatar, + :is_admin, + :generation def avatar object.image.url(:sixtyfour) diff --git a/app/serializers/api/v2/webhook_serializer.rb b/app/serializers/api/v2/webhook_serializer.rb index 59d60283..3221e450 100644 --- a/app/serializers/api/v2/webhook_serializer.rb +++ b/app/serializers/api/v2/webhook_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class WebhookSerializer < ApplicationSerializer diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index c52b0802..bd256140 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapExportService < Struct.new(:user, :map) def json # marshal_dump turns OpenStruct into a Hash diff --git a/app/services/perm.rb b/app/services/perm.rb index 99897028..57e0816f 100644 --- a/app/services/perm.rb +++ b/app/services/perm.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Perm # e.g. Perm::ISSIONS ISSIONS = [:commons, :public, :private].freeze diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb index 965f2c91..0efe9392 100644 --- a/app/services/webhook_service.rb +++ b/app/services/webhook_service.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class WebhookService def self.publish!(webhook:, event:) return false unless webhook.event_types.include? event.kind diff --git a/config.ru b/config.ru index 8a0f42cc..ab79c07d 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,4 @@ +# frozen_string_literal: true # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index b80306c5..b629682a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require_relative 'boot' require 'csv' diff --git a/config/boot.rb b/config/boot.rb index e49b6649..f17b883c 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rubygems' require 'rails/commands/server' diff --git a/config/environment.rb b/config/environment.rb index 426333bb..12ea62f8 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Load the Rails application. require_relative 'application' diff --git a/config/environments/development.rb b/config/environments/development.rb index 407a30f3..b1654921 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/environments/production.rb b/config/environments/production.rb index 24ceed21..f9c94af6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/environments/test.rb b/config/environments/test.rb index dac060f1..5f0b1ee2 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 4a220c97..543ce6e9 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true $codes = [] if ActiveRecord::Base.connection.data_source_exists? 'users' $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index aba3586b..929be340 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -1 +1,2 @@ +# frozen_string_literal: true ActiveModelSerializers.config.adapter = :json diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 51639b67..315ac48a 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # ApplicationController.renderer.defaults.merge!( diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 31897cf4..4edab3b6 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. @@ -9,4 +10,4 @@ Rails.application.config.assets.quiet = true # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w( webpacked/metamaps.bundle.js ) +Rails.application.config.assets.precompile += %w(webpacked/metamaps.bundle.js) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf..d0f0d3b5 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index f51a497e..74ea9274 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index a9a8dcff..cb46d3ef 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 8a87b3b9..a086584d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 843fe831..33073f45 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Doorkeeper.configure do # Change the ORM that doorkeeper will use (needs plugins) orm :active_record diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index 5423334e..db508b3c 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'exception_notification/rails' ExceptionNotification.configure do |config| diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..b7fe1231 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9..aa7435fb 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb index b1d87b01..5b9883ab 100644 --- a/config/initializers/kaminari_config.rb +++ b/config/initializers/kaminari_config.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Kaminari.configure do |config| # config.default_per_page = 25 # config.max_per_page = nil diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index c7b0c86d..5e8d015a 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb index 0706cafd..d3c12d7b 100644 --- a/config/initializers/new_framework_defaults.rb +++ b/config/initializers/new_framework_defaults.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains migration options to ease your Rails 5.0 upgrade. diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 4a65d495..6f094b7c 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true Paperclip::Attachment.default_options[:url] = ':s3_domain_url' Paperclip::Attachment.default_options[:path] = '/:class/:attachment/:id_partition/:style/:filename' diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index e7f18911..4da6fb50 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d2dc13b6..57d69156 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_Metamaps_session' diff --git a/config/initializers/uservoice.rb b/config/initializers/uservoice.rb index df04eeaa..3aa65a46 100644 --- a/config/initializers/uservoice.rb +++ b/config/initializers/uservoice.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'uservoice-ruby' def current_sso_token diff --git a/config/initializers/version.rb b/config/initializers/version.rb index c378cb6f..ff08c330 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,3 @@ -METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze -METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1,2,4).join(' ').freeze +# frozen_string_literal: true +METAMAPS_VERSION = '2 build `git log -1 --pretty=%H`' +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 36bb3e27..d65576db 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains settings for ActionController::ParamsWrapper which diff --git a/config/puma.rb b/config/puma.rb index c7f311f8..da41eff9 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum, this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }.to_i threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests, default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV') { 'development' } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 38fe274e..c64c188e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.routes.draw do use_doorkeeper root to: 'main#home', via: :get diff --git a/config/spring.rb b/config/spring.rb index be72de67..b0ca9589 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true %w( .ruby-version .ruby-gemset diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index bf07cec2..776c81e3 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -1,7 +1,8 @@ +# frozen_string_literal: true namespace :assets do task :js_compile do - system "npm install" - system "npm run build" + system 'npm install' + system 'npm run build' end end diff --git a/lib/tasks/heroku.rake b/lib/tasks/heroku.rake index a523a778..d7ce308f 100644 --- a/lib/tasks/heroku.rake +++ b/lib/tasks/heroku.rake @@ -1,9 +1,10 @@ +# frozen_string_literal: true require 'dotenv/tasks' namespace :heroku do desc 'Generate the Heroku gems manifest from gem dependencies' task gems: :dotenv do - RAILS_ENV = 'production'.freeze + RAILS_ENV = 'production' Rake::Task[:environment].invoke list = Rails.configuration.gems.collect do |g| _command, *options = g.send(:install_command) diff --git a/lib/tasks/perms.rake b/lib/tasks/perms.rake index 39cb9b27..bf087bd0 100644 --- a/lib/tasks/perms.rake +++ b/lib/tasks/perms.rake @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'dotenv/tasks' namespace :perms do diff --git a/script/rails b/script/rails index a861c543..1267847e 100644 --- a/script/rails +++ b/script/rails @@ -1,4 +1,5 @@ #!/usr/bin/env ruby.exe +# frozen_string_literal: true # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index 4a1e3298..f3ec6a75 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'mappings API', type: :request do diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index 7356ca72..77cbc24b 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'maps API', type: :request do diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb index f232b879..c422f3bc 100644 --- a/spec/api/v2/synapses_api_spec.rb +++ b/spec/api/v2/synapses_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'synapses API', type: :request do diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb index c2e480a5..cd424ba0 100644 --- a/spec/api/v2/tokens_api_spec.rb +++ b/spec/api/v2/tokens_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'tokens API', type: :request do diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb index 4781348a..9811071d 100644 --- a/spec/api/v2/topics_api_spec.rb +++ b/spec/api/v2/topics_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'topics API', type: :request do diff --git a/spec/controllers/mappings_controller_spec.rb b/spec/controllers/mappings_controller_spec.rb index bcd2b97f..8d1c424d 100644 --- a/spec/controllers/mappings_controller_spec.rb +++ b/spec/controllers/mappings_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MappingsController, type: :controller do diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 278ec559..0f053dd9 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MapsController, type: :controller do diff --git a/spec/controllers/metacodes_controller_spec.rb b/spec/controllers/metacodes_controller_spec.rb index cb4116d4..b25017b9 100644 --- a/spec/controllers/metacodes_controller_spec.rb +++ b/spec/controllers/metacodes_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MetacodesController, type: :controller do diff --git a/spec/controllers/synapses_controller_spec.rb b/spec/controllers/synapses_controller_spec.rb index 15d91250..3a5310e4 100644 --- a/spec/controllers/synapses_controller_spec.rb +++ b/spec/controllers/synapses_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe SynapsesController, type: :controller do diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 315b931f..0d7e3010 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe TopicsController, type: :controller do diff --git a/spec/factories/mappings.rb b/spec/factories/mappings.rb index bed0b754..1bcdf891 100644 --- a/spec/factories/mappings.rb +++ b/spec/factories/mappings.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :mapping do xloc 0 diff --git a/spec/factories/maps.rb b/spec/factories/maps.rb index 14450c00..a95590e4 100644 --- a/spec/factories/maps.rb +++ b/spec/factories/maps.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :map do sequence(:name) { |n| "Cool Map ##{n}" } diff --git a/spec/factories/metacodes.rb b/spec/factories/metacodes.rb index 543e4955..2ed71beb 100644 --- a/spec/factories/metacodes.rb +++ b/spec/factories/metacodes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :metacode do sequence(:name) { |n| "Cool Metacode ##{n}" } diff --git a/spec/factories/synapses.rb b/spec/factories/synapses.rb index db82fc39..6af8ca9e 100644 --- a/spec/factories/synapses.rb +++ b/spec/factories/synapses.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :synapse do sequence(:desc) { |n| "Cool synapse ##{n}" } @@ -6,6 +7,6 @@ FactoryGirl.define do association :topic1, factory: :topic association :topic2, factory: :topic user - weight 1 # todo drop this column + weight 1 # TODO: drop this column end end diff --git a/spec/factories/tokens.rb b/spec/factories/tokens.rb index 3970d76f..6d5f110b 100644 --- a/spec/factories/tokens.rb +++ b/spec/factories/tokens.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :token do user diff --git a/spec/factories/topics.rb b/spec/factories/topics.rb index f4c73f4c..a6048d7c 100644 --- a/spec/factories/topics.rb +++ b/spec/factories/topics.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :topic do user diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 746f52b1..b5b20b9a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # This file supports three factories, because code and joinedwithcode # make things complicated! diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb index 60310bf4..96d07c07 100644 --- a/spec/mailers/previews/map_mailer_preview.rb +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Preview all emails at http://localhost:3000/rails/mailers/map_mailer class MapMailerPreview < ActionMailer::Preview def invite_to_edit_email diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index e70429be..3f3089cf 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Map, type: :model do diff --git a/spec/models/mapping_spec.rb b/spec/models/mapping_spec.rb index 54c72b88..343c19ee 100644 --- a/spec/models/mapping_spec.rb +++ b/spec/models/mapping_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Mapping, type: :model do diff --git a/spec/models/metacode_spec.rb b/spec/models/metacode_spec.rb index c9b34527..49354898 100644 --- a/spec/models/metacode_spec.rb +++ b/spec/models/metacode_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Metacode, type: :model do diff --git a/spec/models/synapse_spec.rb b/spec/models/synapse_spec.rb index 3bdb1ac4..c5b63a41 100644 --- a/spec/models/synapse_spec.rb +++ b/spec/models/synapse_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Synapse, type: :model do diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index 50e89c02..82582218 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Token, type: :model do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index dbaac86d..50e6d74b 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Topic, type: :model do diff --git a/spec/policies/map_policy_spec.rb b/spec/policies/map_policy_spec.rb index 7dd33707..c08432dd 100644 --- a/spec/policies/map_policy_spec.rb +++ b/spec/policies/map_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MapPolicy, type: :policy do diff --git a/spec/policies/mapping_policy_spec.rb b/spec/policies/mapping_policy_spec.rb index 46b9c117..4010e589 100644 --- a/spec/policies/mapping_policy_spec.rb +++ b/spec/policies/mapping_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MappingPolicy, type: :policy do diff --git a/spec/policies/synapse_policy.rb b/spec/policies/synapse_policy.rb index 4c725e37..4d9d422c 100644 --- a/spec/policies/synapse_policy.rb +++ b/spec/policies/synapse_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe SynapsePolicy, type: :policy do diff --git a/spec/policies/topic_policy_spec.rb b/spec/policies/topic_policy_spec.rb index 7078496c..ef80e8dc 100644 --- a/spec/policies/topic_policy_spec.rb +++ b/spec/policies/topic_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe TopicPolicy, type: :policy do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d14d6dbe..ddf0781c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true ENV['RAILS_ENV'] ||= 'test' require 'spec_helper' require File.expand_path('../../config/environment', __FILE__) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a2b164b2..a3477eb7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index 1672479f..1d24b7ca 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # https://github.com/plataformatec/devise/wiki/How-To:-Stub-authentication-in-controller-specs require 'devise' diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb index afae617a..0d10fa34 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_girl.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # lets you type create(:user) instead of FactoryGirl.create(:user) RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods diff --git a/spec/support/pundit.rb b/spec/support/pundit.rb index 1fd8e296..6e8dc0ce 100644 --- a/spec/support/pundit.rb +++ b/spec/support/pundit.rb @@ -1 +1,2 @@ +# frozen_string_literal: true require 'pundit/rspec' diff --git a/spec/support/schema_matcher.rb b/spec/support/schema_matcher.rb index 207c5fa6..998771d9 100644 --- a/spec/support/schema_matcher.rb +++ b/spec/support/schema_matcher.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true RSpec::Matchers.define :match_json_schema do |schema_name| match do |response| schema_directory = Rails.root.join('doc', 'api', 'schemas').to_s diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb index 8017e897..1d48d7c8 100644 --- a/spec/support/simplecov.rb +++ b/spec/support/simplecov.rb @@ -1,2 +1,2 @@ +# frozen_string_literal: true require 'simplecov' - From 5fab6de48abd4e6ee084f0bbb509af026c6b6662 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 11:00:53 +0800 Subject: [PATCH 049/378] fiddle with metacodes controller --- .rubocop.yml | 6 ++++++ app/controllers/metacodes_controller.rb | 18 +++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9fb58aba..897484b7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,3 +12,9 @@ Rails: Metrics/LineLength: Max: 100 + +Metrics/AbcSize: + Max: 16 + +Style/Documentation: + Enabled: false diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 313d9764..00f92878 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true class MetacodesController < ApplicationController before_action :require_admin, except: [:index, :show] + before_action :set_metacode, only: [:edit, :update] # GET /metacodes # GET /metacodes.json @@ -8,10 +10,7 @@ class MetacodesController < ApplicationController respond_to do |format| format.html do - unless authenticated? && user.admin - redirect_to root_url, notice: 'You need to be an admin for that.' - return false - end + return unless require_admin render :index end format.json { render json: @metacodes } @@ -23,7 +22,7 @@ class MetacodesController < ApplicationController # GET /metacodes/action.json def show @metacode = Metacode.where('DOWNCASE(name) = ?', downcase(params[:name])).first if params[:name] - @metacode = Metacode.find(params[:id]) unless @metacode + set_metacode unless @metacode respond_to do |format| format.json { render json: @metacode } @@ -36,14 +35,13 @@ class MetacodesController < ApplicationController @metacode = Metacode.new respond_to do |format| - format.html # new.html.erb + format.html format.json { render json: @metacode } end end # GET /metacodes/1/edit def edit - @metacode = Metacode.find(params[:id]) end # POST /metacodes @@ -65,8 +63,6 @@ class MetacodesController < ApplicationController # PUT /metacodes/1 # PUT /metacodes/1.json def update - @metacode = Metacode.find(params[:id]) - respond_to do |format| if @metacode.update(metacode_params) format.html { redirect_to metacodes_url, notice: 'Metacode was successfully updated.' } @@ -84,4 +80,8 @@ class MetacodesController < ApplicationController def metacode_params params.require(:metacode).permit(:id, :name, :aws_icon, :manual_icon, :color) end + + def set_metacode + @metacode = Metacode.find(params[:id]) + end end From f8c11f234dc5df16a4a392e179ba405ce8fcf5a5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:27:34 +0800 Subject: [PATCH 050/378] more rubocop updates --- .rubocop.yml | 1 + Vagrantfile | 1 - .../api/v1/deprecated_controller.rb | 2 + app/controllers/api/v2/restful_controller.rb | 46 ++++++------- app/controllers/application_controller.rb | 18 +++-- app/helpers/application_helper.rb | 12 ++-- app/helpers/maps_helper.rb | 65 ++++++++++--------- app/helpers/synapses_helper.rb | 35 ++++------ app/helpers/topics_helper.rb | 64 +++++++----------- app/policies/map_policy.rb | 23 ++++--- app/policies/synapse_policy.rb | 12 ++-- app/policies/topic_policy.rb | 18 +++-- .../api/v2/application_serializer.rb | 27 ++++++-- app/serializers/api/v2/user_serializer.rb | 2 + app/services/map_export_service.rb | 8 ++- app/views/layouts/_lightboxes.html.erb | 2 +- app/views/maps/_newtopic.html.erb | 29 +++++---- config/initializers/access_codes.rb | 4 +- config/initializers/backtrace_silencers.rb | 6 +- config/initializers/devise.rb | 6 +- config/initializers/doorkeeper.rb | 15 +++-- config/initializers/uservoice.rb | 9 ++- config/routes.rb | 6 +- script/rails | 1 - spec/api/v2/mappings_api_spec.rb | 8 ++- spec/api/v2/synapses_api_spec.rb | 8 ++- 26 files changed, 234 insertions(+), 194 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 897484b7..6bdcbfc3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - 'bin/**/*' - 'vendor/**/*' - 'app/assets/javascripts/node_modules/**/*' + - 'Vagrantfile' Rails: Enabled: true diff --git a/Vagrantfile b/Vagrantfile index 0fa3e8da..6ee6cb35 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,4 +1,3 @@ -# frozen_string_literal: true # -*- mode: ruby -*- # vi: set ft=ruby : diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index 6f6b5f15..b9e07214 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -2,9 +2,11 @@ module Api module V1 class DeprecatedController < ApplicationController + # rubocop:disable Style/MethodMissing def method_missing render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } end + # rubocop:enable Style/MethodMissing end end end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index d7a1856e..74a8f472 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -52,7 +52,8 @@ module Api "Api::V2::#{resource_name.camelize}Serializer".constantize end - def respond_with_resource(scope: default_scope, serializer: resource_serializer, root: serializer_root) + def respond_with_resource(scope: default_scope, serializer: resource_serializer, + root: serializer_root) if resource.errors.empty? render json: resource, scope: scope, serializer: serializer, root: root else @@ -60,8 +61,11 @@ module Api end end - def respond_with_collection(resources: collection, scope: default_scope, serializer: resource_serializer, root: serializer_root) - render json: resources, scope: scope, each_serializer: serializer, root: root, meta: pagination(resources), meta_key: :page + def respond_with_collection(resources: collection, scope: default_scope, + serializer: resource_serializer, root: serializer_root) + pagination_link_headers!(pagination(resources)) + render json: resources, scope: scope, each_serializer: serializer, root: root, + meta: pagination(resources), meta_key: :page end def default_scope @@ -95,33 +99,31 @@ module Api end def pagination(collection) - per = (params[:per] || 25).to_i - current_page = (params[:page] || 1).to_i - total_pages = (collection.total_count.to_f / per).ceil - prev_page = current_page > 1 ? current_page - 1 : 0 - next_page = current_page < total_pages ? current_page + 1 : 0 + @pagination_data ||= { + current_page: (params[:page] || 1).to_i, + next_page: current_page < total_pages ? current_page + 1 : 0, + prev_page: current_page > 1 ? current_page - 1 : 0, + total_pages: (collection.total_count.to_f / per).ceil, + total_count: collection.total_count, + per: (params[:per] || 25).to_i + } + end + def pagination_link_headers!(data) base_url = request.base_url + request.path - nxt = request.query_parameters.merge(page: next_page).map { |x| x.join('=') }.join('&') - prev = request.query_parameters.merge(page: prev_page).map { |x| x.join('=') }.join('&') - last = request.query_parameters.merge(page: total_pages).map { |x| x.join('=') }.join('&') + old_query = request_query_parameters + nxt = old_query.merge(page: data[:next_page]).map { |x| x.join('=') }.join('&') + prev = old_query.merge(page: data[:prev_page]).map { |x| x.join('=') }.join('&') + last = old_query.merge(page: data[:total_pages]).map { |x| x.join('=') }.join('&') + response.headers['Link'] = [ %(<#{base_url}?#{nxt}>; rel="next"), %(<#{base_url}?#{prev}>; rel="prev"), %(<#{base_url}?#{last}>; rel="last") ].join(',') - response.headers['X-Total-Pages'] = collection.total_pages.to_s - response.headers['X-Total-Count'] = collection.total_count.to_s + response.headers['X-Total-Pages'] = data[:total_pages].to_s + response.headers['X-Total-Count'] = data[:total_count].to_s response.headers['X-Per-Page'] = per.to_s - - { - current_page: current_page, - next_page: next_page, - prev_page: prev_page, - total_pages: total_pages, - total_count: collection.total_count, - per: per - } end def instantiate_collection diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7735c681..83889619 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,7 +6,7 @@ class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery(with: :exception) - before_action :get_invite_link + before_action :invite_link after_action :allow_embedding def default_serializer_options @@ -42,22 +42,20 @@ class ApplicationController < ActionController::Base private - def get_invite_link + def invite_link @invite_link = "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end def require_no_user - if authenticated? - redirect_to edit_user_path(user), notice: 'You must be logged out.' - return false - end + return true unless authenticated? + redirect_to edit_user_path(user), notice: 'You must be logged out.' + return false end def require_user - unless authenticated? - redirect_to new_user_session_path, notice: 'You must be logged in.' - return false - end + return true if authenticated? + redirect_to new_user_session_path, notice: 'You must be logged in.' + return false end def require_admin diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 57b24106..1c9b4da5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true module ApplicationHelper - def get_metacodeset - @m = current_user.settings.metacodes - set = @m[0].include?('metacodeset') ? MetacodeSet.find(@m[0].sub('metacodeset-', '').to_i) : false - set + def metacodeset + metacodes = current_user.settings.metacodes + return false unless metacodes[0].include?('metacodeset') + MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i) end def user_metacodes @m = current_user.settings.metacodes - set = get_metacodeset + set = metacodeset @metacodes = if set set.metacodes.to_a else @@ -17,7 +17,7 @@ module ApplicationHelper @metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end - def determine_invite_link + def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 8ca7b047..0ca99c15 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -1,35 +1,42 @@ # frozen_string_literal: true module MapsHelper - ## this one is for building our custom JSON autocomplete format for typeahead + # JSON autocomplete format for typeahead def autocomplete_map_array_json(maps) - temp = [] - maps.each do |m| - map = {} - map['id'] = m.id - map['label'] = m.name - map['value'] = m.name - map['description'] = m.desc.try(:truncate, 30) - map['permission'] = m.permission - map['topicCount'] = m.topics.count - map['synapseCount'] = m.synapses.count - map['contributorCount'] = m.contributors.count - map['rtype'] = 'map' - - contributorTip = '' - firstContributorImage = 'https://s3.amazonaws.com/metamaps-assets/site/user.png' - if m.contributors.count.positive? - firstContributorImage = m.contributors[0].image.url(:thirtytwo) - m.contributors.each_with_index do |c, _index| - userImage = c.image.url(:thirtytwo) - name = c.name - contributorTip += '<li> <img class="tipUserImage" width="25" height="25" src=' + userImage + ' />' + '<span>' + name + '</span> </li>' - end - end - map['contributorTip'] = contributorTip - map['mapContributorImage'] = firstContributorImage - - temp.push map + maps.map do |m| + { + id: m.id, + label: m.name, + value: m.name, + description: m.desc.try(:truncate, 30), + permission: m.permission, + topicCount: m.topics.count, + synapseCount: m.synapses.count, + contributorCount: m.contributors.count, + rtype: 'map', + contributorTip: contributor_tip(map), + mapContributorImage: first_contributor_image(map) + } end - temp + end + + def first_contributor_image(map) + if map.contributors.count.positive? + return map.contributors[0].image.url(:thirtytwo) + end + 'https://s3.amazonaws.com/metamaps-assets/site/user.png' + end + + def contributor_tip(map) + output = '' + if map.contributors.count.positive? + map.contributors.each_with_index do |contributor, _index| + user_image = contributor.image.url(:thirtytwo) + output += '<li>' + output += %(<img class="tipUserImage" width="25" height="25" src="#{user_image}" />) + output += "<span>#{contributor.name}</span>" + output += '</li>' + end + end + output end end diff --git a/app/helpers/synapses_helper.rb b/app/helpers/synapses_helper.rb index 471f0e05..def3b985 100644 --- a/app/helpers/synapses_helper.rb +++ b/app/helpers/synapses_helper.rb @@ -2,33 +2,24 @@ module SynapsesHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_generic_json(unique) - temp = [] - unique.each do |s| - synapse = {} - synapse['label'] = s.desc - synapse['value'] = s.desc - - temp.push synapse + unique.map do |s| + { label: s.desc, value: s.desc } end - temp end ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_array_json(synapses) - temp = [] - synapses.each do |s| - synapse = {} - synapse['id'] = s.id - synapse['label'] = s.desc.nil? || s.desc == '' ? '(no description)' : s.desc - synapse['value'] = s.desc - synapse['permission'] = s.permission - synapse['mapCount'] = s.maps.count - synapse['originator'] = s.user.name - synapse['originatorImage'] = s.user.image.url(:thirtytwo) - synapse['rtype'] = 'synapse' - - temp.push synapse + synapses.map do |s| + { + id: s.id, + label: s.desc.blank? ? '(no description)' : s.desc, + value: s.desc, + permission: s.permission, + mapCount: s.maps.count, + originator: s.user.name, + originatorImage: s.user.image.url(:thirtytwo), + rtype: 'synapse' + } end - temp end end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 32697db5..e1a1d179 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -2,56 +2,38 @@ module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) - temp = [] - topics.each do |t| - topic = {} - topic['id'] = t.id - topic['label'] = t.name - topic['value'] = t.name - topic['description'] = t.desc ? t.desc&.truncate(70) # make this return matched results - topic['type'] = t.metacode.name - topic['typeImageURL'] = t.metacode.icon - topic['permission'] = t.permission - topic['mapCount'] = t.maps.count - topic['synapseCount'] = t.synapses.count - topic['originator'] = t.user.name - topic['originatorImage'] = t.user.image.url(:thirtytwo) - topic['rtype'] = 'topic' - topic['inmaps'] = t.inmaps - topic['inmapsLinks'] = t.inmapsLinks - - temp.push topic + topics.map do |t| + { + id: t.id, + label: t.name, + value: t.name, + description: t.desc ? t.desc&.truncate(70) : '', # make this return matched results + type: t.metacode.name, + typeImageURL: t.metacode.icon, + permission: t.permission, + mapCount: t.maps.count, + synapseCount: t.synapses.count, + originator: t.user.name, + originatorImage: t.user.image.url(:thirtytwo), + rtype: :topic, + inmaps: t.inmaps, + inmapsLinks: t.inmapsLinks + } end - temp end - # find all nodes in any given nodes network + # recursively find all nodes in any given nodes network def network(node, array, count) - # recurse starting with a node to find all connected nodes and return an array of topics that constitutes the starting nodes network - - # if the array of nodes is empty initialize it array = [] if array.nil? - - # add the node to the array array.push(node) - return array if count.zero? - count -= 1 - # check if each relative is already in the array and if not, call the network function again - if !node.relatives.empty? - if (node.relatives - array).empty? - return array - else - (node.relatives - array).each do |relative| - array = (array | network(relative, array, count)) - end - return array - end - - elsif node.relatives.empty? - return array + remaining_relatives = node.relatives.to_a - array + remaining_relatives.each do |relative| + array = (array | network(relative, array, count - 1)) end + + array end end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 3894520c..b2a04cbe 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -3,13 +3,11 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'maps.permission IN (?)' - if user - shared_maps = user.shared_maps.map(&:id) - scope.where(permission + ' OR maps.id IN (?) OR maps.user_id = ?', visible, shared_maps, user.id) - else - scope.where(permission, visible) - end + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end @@ -18,7 +16,9 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user + record.permission.in?('commons', 'public') || + record.collaborators.include?(user) || + record.user == user end def create? @@ -26,7 +26,10 @@ class MapPolicy < ApplicationPolicy end def update? - user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) + return false unless user.present? + record.permission == 'commons' || + record.collaborators.include?(user) || + record.user == user end def destroy? @@ -34,7 +37,7 @@ class MapPolicy < ApplicationPolicy end def access? - # note that this is to edit access + # note that this is to edit who can access the map user.present? && record.user == user end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index f9557e70..eae820b3 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -3,12 +3,12 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'synapses.permission IN (?)' - if user - scope.where(permission + ' OR synapses.defer_to_map_id IN (?) OR synapses.user_id = ?', visible, user.shared_maps.map(&:id), user.id) - else - scope.where(permission, visible) - end + + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 2484ee54..cbde51d8 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -3,12 +3,11 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'topics.permission IN (?)' - if user - scope.where(permission + ' OR topics.defer_to_map_id IN (?) OR topics.user_id = ?', visible, user.shared_maps.map(&:id), user.id) - else - scope.where(permission, visible) - end + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end @@ -24,14 +23,13 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission == 'commons' || record.permission == 'public' || record.user == user + record.permission.in?('commons', 'public') || record.user == user end end def update? - if !user.present? - false - elsif record.defer_to_map.present? + return false unless user.present? + if record.defer_to_map.present? map_policy.update? else record.permission == 'commons' || record.user == user diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index 55a9b8a0..a5da830a 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -7,21 +7,40 @@ module Api end def embeds + # subclasses can override self.embeddable, and then it will whitelist + # scope[:embeds] based on the contents. That way scope[:embeds] can just pull + # from params and the whitelisting happens here @embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) } end + # self.embeddable might look like this: + # topic1: { attr: :node1, serializer: TopicSerializer } + # topic2: { attr: :node2, serializer: TopicSerializer } + # contributors: { serializer: UserSerializer} + # This method will remove the :attr key if the underlying attribute name + # is different than the name provided in the final json output. All other keys + # in the hash will be passed to the ActiveModel::Serializer `attribute` method + # directly (e.g. serializer in the examples will be passed). + # + # This setup means if you passed this self.embeddable config and sent no + # ?embed= query param with your API request, you would get the regular attributes + # plus topic1_id, topic2_id, and contributor_ids. If you pass + # ?embed=topic1,topic2,contributors, then instead of two ids and an array of ids, + # you would get two serialized topics and an array of serialized users def self.embed_dat embeddable.each_pair do |key, opts| attr = opts.delete(:attr) || key if attr.to_s.pluralize == attr.to_s - attribute "#{attr.to_s.singularize}_ids".to_sym, opts.merge(unless: -> { embeds.include?(key) }) do + attribute("#{attr.to_s.singularize}_ids".to_sym, + opts.merge(unless: -> { embeds.include?(key) })) do object.send(attr).map(&:id) end - has_many attr, opts.merge(if: -> { embeds.include?(key) }) + has_many(attr, opts.merge(if: -> { embeds.include?(key) })) else id_opts = opts.merge(key: "#{key}_id") - attribute "#{attr}_id".to_sym, id_opts.merge(unless: -> { embeds.include?(key) }) - attribute key, opts.merge(if: -> { embeds.include?(key) }) + attribute("#{attr}_id".to_sym, + id_opts.merge(unless: -> { embeds.include?(key) })) + attribute(key, opts.merge(if: -> { embeds.include?(key) })) end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index e97bc420..ec58775d 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -12,9 +12,11 @@ module Api object.image.url(:sixtyfour) end + # rubocop:disable Style/PredicateName def is_admin object.admin end + # rubocop:enable Style/PredicateName end end end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index bd256140..2ded756c 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true -class MapExportService < Struct.new(:user, :map) +class MapExportService + attr_reader :user, :map + def initialize(user, map) + @user = user + @map = map + end + def json # marshal_dump turns OpenStruct into a Hash { diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 68fa9e27..89b5a6b4 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -94,7 +94,7 @@ <p>As a valued beta tester, you have the ability to invite your peers, colleagues and collaborators onto the platform.</p> <p>Below is a personal invite link containing your unique access code, which can be used multiple times.</p> <div id="joinCodesBox"> - <p class="joinCodes"><%= determine_invite_link %> + <p class="joinCodes"><%= invite_link() %> <button class="button" onclick="Metamaps.GlobalUI.shareInvite('<%= @invite_link %>');">COPY INVITE LINK!</button> </div> diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index ba3d1797..8e10c7a7 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -1,29 +1,34 @@ +<% @metacodes = user_metacodes() %> + <%= form_for Topic.new, url: topics_url, remote: true do |form| %> <div class="openMetacodeSwitcher openLightbox" data-open="switchMetacodes"> <div class="tooltipsAbove">Switch Metacodes</div> </div> + <div class="pinCarousel"> <div class="tooltipsAbove helpPin">Pin Open</div> <div class="tooltipsAbove helpUnpin">Unpin</div> </div> + <div id="metacodeImg"> - <% @metacodes = user_metacodes() %> - <% set = get_metacodeset() %> <% @metacodes.each do |metacode| %> <img class="cloudcarousel" width="40" height="40" src="<%= asset_path metacode.icon %>" alt="<%= metacode.name %>" title="<%= metacode.name %>" data-id="<%= metacode.id %>" /> <% end %> </div> + <%= form.text_field :name, :maxlength => 140, :placeholder => "title..." %> + <div id="metacodeImgTitle"></div> <div class="clearfloat"></div> -<script> -<% @metacodes.each do |metacode| %> - <% if !set %> - Metamaps.Create.selectedMetacodes.push("<%= metacode.id %>"); - Metamaps.Create.newSelectedMetacodes.push("<%= metacode.id %>"); - Metamaps.Create.selectedMetacodeNames.push("<%= metacode.name %>"); - Metamaps.Create.newSelectedMetacodeNames.push("<%= metacode.name %>"); - <% end %> -<% end %> -</script> + + <script> + <% @metacodes.each do |metacode| %> + <% if !metacodeset() %> + Metamaps.Create.selectedMetacodes.push("<%= metacode.id %>"); + Metamaps.Create.newSelectedMetacodes.push("<%= metacode.id %>"); + Metamaps.Create.selectedMetacodeNames.push("<%= metacode.name %>"); + Metamaps.Create.newSelectedMetacodeNames.push("<%= metacode.name %>"); + <% end %> + <% end %> + </script> <% end %> diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 543ce6e9..fdf4e4b3 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true $codes = [] if ActiveRecord::Base.connection.data_source_exists? 'users' - $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } + $codes = ActiveRecord::Base.connection + .execute('SELECT code FROM users') + .map { |user| user['code'] } end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index d0f0d3b5..5b98aef4 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true # Be sure to restart your server when you modify this file. -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# You can add backtrace silencers for libraries that you're using but don't +# wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# You can also remove all the silencers if you're trying to debug a problem +# that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a086584d..6740cdc9 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -7,7 +7,8 @@ Devise.setup do |config| # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` # by default. You can change it below and use your own secret key. - # config.secret_key = '4d38a819bcea6314ffccb156a8e84b1b52c51ed446d11877c973791b3cd88449e9dbd7990cbc6e7f37d84702168ec36391467000c842ed5bed4f0b05df2b9507' + # config.secret_key = '4d38a819bcea6314ffccb156a8e84b1b52c51ed446d11877c973791b3cd88' + + # '449e9dbd7990cbc6e7f37d84702168ec36391467000c842ed5bed4f0b05df2b9507' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -92,7 +93,8 @@ Devise.setup do |config| config.stretches = Rails.env.test? ? 1 : 10 # Setup a pepper to generate the encrypted password. - # config.pepper = "640ad415cb5292ac9ddbfa6ad7d9653d1537f1184e4037c2453db3eccb98e1c82facc6d3de7bf9d4c41d9967d41194c6e120f36f430e195ba840cd00e02dea59" + # config.pepper = "640ad415cb5292ac9ddbfa6ad7d9653d1537f1184e4037c2453db3eccb98e1c82" + + # "facc6d3de7bf9d4c41d9967d41194c6e120f36f430e195ba840cd00e02dea59" # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 33073f45..40de1df8 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -8,7 +8,8 @@ Doorkeeper.configure do current_user || redirect_to(new_user_session_url) end - # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # If you want to restrict access to the web interface for adding oauth authorized applications, + # you need to declare the block below. admin_authenticator do current_user || redirect_to(new_user_session_url) end @@ -39,7 +40,9 @@ Doorkeeper.configure do # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter :confirmation => true (default false) if you want to enforce ownership of # a registered application - # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the + # necessary support + # # enable_application_owner :confirmation => false # Define access token scopes for your provider @@ -61,9 +64,11 @@ Doorkeeper.configure do # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param # Change the native redirect uri for client apps - # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider - # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL - # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # When clients register with the following redirect uri, they won't be redirected to any server + # and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients + # must provide a valid URL (Similar behaviour: + # https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) # # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' diff --git a/config/initializers/uservoice.rb b/config/initializers/uservoice.rb index 3aa65a46..9375e3a1 100644 --- a/config/initializers/uservoice.rb +++ b/config/initializers/uservoice.rb @@ -2,7 +2,10 @@ require 'uservoice-ruby' def current_sso_token - @current_sso_token ||= UserVoice.generate_sso_token('metamapscc', ENV['SSO_KEY'], { - email: current_user.email - }, 300) # Default expiry time is 5 minutes = 300 seconds + @current_sso_token ||= UserVoice.generate_sso_token( + 'metamapscc', + ENV['SSO_KEY'], + { email: current_user.email }, + 300 # Default expiry time is 5 minutes = 300 seconds + ) end diff --git a/config/routes.rb b/config/routes.rb index c64c188e..fe48b6ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,7 +64,11 @@ Metamaps::Application.routes.draw do get 'explore/starred', to: 'maps#starredmaps' get 'explore/mapper/:id', to: 'maps#usermaps' - devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, skip: :sessions + devise_for :users, skip: :sessions, controllers: { + registrations: 'users/registrations', + passwords: 'users/passwords', + sessions: 'devise/sessions' + } devise_scope :user do get 'login' => 'devise/sessions#new', :as => :new_user_session diff --git a/script/rails b/script/rails index 1267847e..6f9d9941 100644 --- a/script/rails +++ b/script/rails @@ -1,6 +1,5 @@ #!/usr/bin/env ruby.exe # frozen_string_literal: true -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) require File.expand_path('../../config/boot', __FILE__) diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index f3ec6a75..4d802865 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -24,7 +24,9 @@ RSpec.describe 'mappings API', type: :request do end it 'POST /api/v2/mappings' do - post '/api/v2/mappings', params: { mapping: mapping.attributes, access_token: token } + post '/api/v2/mappings', params: { + mapping: mapping.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) @@ -32,7 +34,9 @@ RSpec.describe 'mappings API', type: :request do end it 'PATCH /api/v2/mappings/:id' do - patch "/api/v2/mappings/#{mapping.id}", params: { mapping: mapping.attributes, access_token: token } + patch "/api/v2/mappings/#{mapping.id}", params: { + mapping: mapping.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb index c422f3bc..093bc41e 100644 --- a/spec/api/v2/synapses_api_spec.rb +++ b/spec/api/v2/synapses_api_spec.rb @@ -24,7 +24,9 @@ RSpec.describe 'synapses API', type: :request do end it 'POST /api/v2/synapses' do - post '/api/v2/synapses', params: { synapse: synapse.attributes, access_token: token } + post '/api/v2/synapses', params: { + synapse: synapse.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:synapse) @@ -32,7 +34,9 @@ RSpec.describe 'synapses API', type: :request do end it 'PATCH /api/v2/synapses/:id' do - patch "/api/v2/synapses/#{synapse.id}", params: { synapse: synapse.attributes, access_token: token } + patch "/api/v2/synapses/#{synapse.id}", params: { + synapse: synapse.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:synapse) From 20bd959c69ac4b4de976887f91534a1ebe6a06b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:59:42 +0800 Subject: [PATCH 051/378] fix models that rubocop broke >:( --- app/models/synapse.rb | 13 +------------ app/models/topic.rb | 3 +-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/models/synapse.rb b/app/models/synapse.rb index c7161469..798f6a54 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -22,17 +22,12 @@ class Synapse < ApplicationRecord where('node1_id = ? OR node2_id = ?', topic_id, topic_id) } - # :nocov: delegate :name, to: :user, prefix: true - # :nocov: - # :nocov: def user_image user.image.url end - # :nocov: - # :nocov: def collaborator_ids if defer_to_map defer_to_map.editors.select { |mapper| mapper != user }.map(&:id) @@ -40,18 +35,12 @@ class Synapse < ApplicationRecord [] end end - # :nocov: - # :nocov: def calculated_permission - if defer_to_map - defer_to_map&.permission + defer_to_map&.permission || permission end - # :nocov: - # :nocov: def as_json(_options = {}) super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) end - # :nocov: end diff --git a/app/models/topic.rb b/app/models/topic.rb index 09d61897..fb635da3 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -74,8 +74,7 @@ class Topic < ApplicationRecord end def calculated_permission - if defer_to_map - defer_to_map&.permission + defer_to_map&.permission || permission end def as_json(_options = {}) From a164dccc946b8ab3ad1f30241dbee7891f81ebdc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 13:55:52 +0800 Subject: [PATCH 052/378] fix errors!! --- app/controllers/api/v2/restful_controller.rb | 17 +++++++++++------ app/policies/map_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index 74a8f472..f837957d 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -99,19 +99,24 @@ module Api end def pagination(collection) - @pagination_data ||= { - current_page: (params[:page] || 1).to_i, + return @pagination_data unless @pagination_data.nil? + + current_page = (params[:page] || 1).to_i + per = (params[:per] || 25).to_i + total_pages = (collection.total_count.to_f / per).ceil + @pagination_data = { + current_page: current_page, next_page: current_page < total_pages ? current_page + 1 : 0, prev_page: current_page > 1 ? current_page - 1 : 0, - total_pages: (collection.total_count.to_f / per).ceil, + total_pages: total_pages, total_count: collection.total_count, - per: (params[:per] || 25).to_i + per: per } end def pagination_link_headers!(data) base_url = request.base_url + request.path - old_query = request_query_parameters + old_query = request.query_parameters nxt = old_query.merge(page: data[:next_page]).map { |x| x.join('=') }.join('&') prev = old_query.merge(page: data[:prev_page]).map { |x| x.join('=') }.join('&') last = old_query.merge(page: data[:total_pages]).map { |x| x.join('=') }.join('&') @@ -123,7 +128,7 @@ module Api ].join(',') response.headers['X-Total-Pages'] = data[:total_pages].to_s response.headers['X-Total-Count'] = data[:total_count].to_s - response.headers['X-Per-Page'] = per.to_s + response.headers['X-Per-Page'] = data[:per].to_s end def instantiate_collection diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index b2a04cbe..4cb2db38 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -16,7 +16,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission.in?('commons', 'public') || + record.permission.in?(['commons', 'public']) || record.collaborators.include?(user) || record.user == user end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index cbde51d8..7bcf585c 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -23,7 +23,7 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission.in?('commons', 'public') || record.user == user + record.permission.in?(['commons', 'public']) || record.user == user end end From 0bb7b1523da3e498b9003a5b52fcb23d64e56437 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:40:40 +0800 Subject: [PATCH 053/378] Metamaps.ReactComponents isn't needed anymore --- frontend/src/Metamaps/ReactComponents.js | 7 ------- frontend/src/Metamaps/Views/ExploreMaps.js | 4 ++-- frontend/src/Metamaps/index.js | 2 -- frontend/src/index.js | 8 +++----- 4 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 frontend/src/Metamaps/ReactComponents.js diff --git a/frontend/src/Metamaps/ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js deleted file mode 100644 index a2495245..00000000 --- a/frontend/src/Metamaps/ReactComponents.js +++ /dev/null @@ -1,7 +0,0 @@ -import Maps from '../components/Maps' - -const ReactComponents = { - Maps -} - -export default ReactComponents diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 155e8453..c9f9b78b 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -4,7 +4,7 @@ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' -import ReactComponents from '../ReactComponents' +import Maps from '../../components/Maps' /* * - Metamaps.Loading @@ -42,7 +42,7 @@ const ExploreMaps = { loadMore: self.loadMore } ReactDOM.render( - React.createElement(ReactComponents.Maps, exploreObj), + React.createElement(Maps, exploreObj), document.getElementById('explore') ) diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 5d15559c..e1aa8b34 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -30,7 +30,6 @@ import TopicCard from './TopicCard' import Util from './Util' import Views from './Views' import Visualize from './Visualize' -import ReactComponents from './ReactComponents' Metamaps.Account = Account Metamaps.Active = Active @@ -55,7 +54,6 @@ Metamaps.Mouse = Mouse Metamaps.Organize = Organize Metamaps.PasteInput = PasteInput Metamaps.Realtime = Realtime -Metamaps.ReactComponents = ReactComponents Metamaps.Router = Router Metamaps.Selected = Selected Metamaps.Settings = Settings diff --git a/frontend/src/index.js b/frontend/src/index.js index 176ac329..67f69141 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,8 +1,6 @@ -// create global references to some utility libraries -import ReactDOM from 'react-dom' +// create global references import _ from 'underscore' -window.ReactDOM = ReactDOM -window._ = _ - import Metamaps from './Metamaps' + +window._ = _ window.Metamaps = Metamaps From 79aa7717ed16f5db1d734a07ebb64b879ec6a36c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:53:32 +0800 Subject: [PATCH 054/378] exact versions in package.json --- package.json | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index bb32377d..08c4f086 100644 --- a/package.json +++ b/package.json @@ -18,26 +18,24 @@ }, "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { - "autolinker": "^0.17.1", - "babel-cli": "^6.11.4", - "babel-loader": "^6.2.4", - "babel-plugin-transform-class-properties": "^6.11.5", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "backbone": "^1.0.0", - "chai": "^3.5.0", - "jquery": "1.12.1", - "mocha": "^3.0.2", - "mocha-jsdom": "^1.1.0", + "autolinker": "0.17.1", + "babel-cli": "6.11.4", + "babel-loader": "6.2.4", + "babel-plugin-transform-class-properties": "6.11.5", + "babel-preset-es2015": "6.9.0", + "babel-preset-react": "6.11.1", + "backbone": "1.0.0", "node-uuid": "1.2.0", - "react": "^15.3.0", - "react-dom": "^15.3.0", - "requirejs": "^2.1.1", + "react": "15.3.0", + "react-dom": "15.3.0", + "requirejs": "2.1.1", "socket.io": "0.9.12", - "underscore": "^1.4.4", - "webpack": "^1.13.1" + "underscore": "1.4.4", + "webpack": "1.13.1" }, "devDependencies": { + "chai": "^3.5.0", + "mocha": "^3.0.2", "babel-eslint": "^6.1.2", "eslint": "^3.5.0", "eslint-config-standard": "^6.0.1", From 045bd3fd73f39cb44c876464cc4a5eafa06b302a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 23:23:12 +0800 Subject: [PATCH 055/378] Metamaps.Filter bug and use _.omit instead of util function --- frontend/src/Metamaps/Filter.js | 2 +- frontend/src/components/Header.js | 4 ++-- frontend/src/utils/index.js | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 frontend/src/utils/index.js diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index f67c6ec8..59aa1bae 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -186,7 +186,7 @@ const Filter = { $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function () { $(this).remove() }) - index = self.visible[filtersToUse].indexOf(identifier) + const index = self.visible[filtersToUse].indexOf(identifier) self.visible[filtersToUse].splice(index, 1) }) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index c8c67619..ee4184d5 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -1,9 +1,9 @@ import React, { Component, PropTypes } from 'react' -import { objectWithoutProperties } from '../utils' +import _ from 'lodash' const MapLink = props => { const { show, text, href, linkClass } = props - const otherProps = objectWithoutProperties(props, ['show', 'text', 'href', 'linkClass']) + const otherProps = _.omit(props, ['show', 'text', 'href', 'linkClass']) if (!show) { return null } diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js deleted file mode 100644 index 1743b9b5..00000000 --- a/frontend/src/utils/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export const objectWithoutProperties = (obj, keys) => { - const target = {} - for (let i in obj) { - if (keys.indexOf(i) !== -1) continue - if (!Object.prototype.hasOwnProperty.call(obj, i)) continue - target[i] = obj[i] - } - return target -} From 0a0ff2fdab38c44a0367598eb1e449b42776449f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 23:28:11 +0800 Subject: [PATCH 056/378] remove fetch api - we don't want no polyfills, and already have jQuery --- frontend/src/Metamaps/Mapper.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 3858101d..70cbd81a 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,17 +1,19 @@ +/* global $ */ + /* - * Metamaps.Backbone + * Dependencies: + * Metamaps.Backbone */ const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve get: function (id, callback) { - return fetch(`/users/${id}.json`, { - }).then(response => { - if (!response.ok) throw response - return response.json() - }).then(payload => { - callback(new Metamaps.Backbone.Mapper(payload)) + $.ajax({ + url: `/users/${id}.json`, + success: data => { + callback(new Metamaps.Backbone.Mapper(data)) + } }) } } From 40f89b1c61bbd80ed31274360464bdc7136ce160 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 15:22:42 +0800 Subject: [PATCH 057/378] enable csv import using csv-parse module --- frontend/src/Metamaps/Import.js | 99 ++++++++++++++++++++--------- frontend/src/Metamaps/PasteInput.js | 16 ++--- package.json | 1 + 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index d5a4b4e1..52a8f21a 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,5 +1,8 @@ /* global Metamaps, $ */ +import parse from 'csv-parse' +import _ from 'lodash' + import Active from './Active' import GlobalUI from './GlobalUI' import Map from './Map' @@ -33,6 +36,40 @@ const Import = { self.handle(results) }, + handleCSV: function (text, parserOpts = {}) { + var self = Import + + const topicsRegex = /("?Topics"?)([\s\S]*)/mi + const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi + let topicsText = text.match(topicsRegex) + if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') + let synapsesText = text.match(synapsesRegex) + if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') + + // merge default options and extra options passed in parserOpts argument + const csv_parser_options = Object.assign({ + columns: true, // get headers + relax_column_count: true, + skip_empty_lines: true + }, parserOpts) + + const topicsPromise = $.Deferred() + parse(topicsText, csv_parser_options, (err, data) => { + if (err) topicsPromise.reject(err) + topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) + }) + + const synapsesPromise = $.Deferred() + parse(synapsesText, csv_parser_options, (err, data) => { + if (err) synapsesPromise.reject(err) + synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) + }) + + $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { + self.handle({ topics, synapses}) + }) + }, + handleJSON: function (text) { var self = Import results = JSON.parse(text) @@ -53,16 +90,6 @@ const Import = { } // if }, - abort: function (message) { - console.error(message) - }, - - simplify: function (string) { - return string - .replace(/(^\s*|\s*$)/g, '') - .toLowerCase() - }, - parseTabbedString: function (text) { var self = Import @@ -235,30 +262,22 @@ const Import = { return true } - var synapse_created = false - topic1.once('sync', function () { - if (topic1.id && topic2.id && !synapse_created) { - synapse_created = true - self.createSynapseWithParameters( - synapse.desc, synapse.category, synapse.permission, - topic1, topic2 - ) - } // if - }) - topic2.once('sync', function () { - if (topic1.id && topic2.id && !synapse_created) { - synapse_created = true - self.createSynapseWithParameters( - synapse.desc, synapse.category, synapse.permission, - topic1, topic2 - ) - } // if + // ensure imported topics have a chance to get a real id attr before creating synapses + const topic1Promise = $.Deferred() + topic1.once('sync', () => topic1Promise.resolve()) + const topic2Promise = $.Deferred() + topic2.once('sync', () => topic2Promise.resolve()) + $.when(topic1Promise, topic2Promise).done(() => { + self.createSynapseWithParameters( + synapse.desc, synapse.category, synapse.permission, + topic1, topic2 + ) }) }) }, createTopicWithParameters: function (name, metacode_name, permission, desc, - link, xloc, yloc, import_id, opts) { + link, xloc, yloc, import_id, opts = {}) { var self = Import $(document).trigger(Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null @@ -326,6 +345,28 @@ const Import = { Metamaps.Mappings.add(mapping) Synapse.renderSynapse(mapping, synapse, node1, node2, true) + }, + + /* + * helper functions + */ + + abort: function (message) { + console.error(message) + }, + + simplify: function (string) { + return string + .replace(/(^\s*|\s*$)/g, '') + .toLowerCase() + }, + + + // thanks to http://stackoverflow.com/a/25290114/5332286 + lowercaseKeys: function(obj) { + return _.transform(obj, (result, val, key) => { + result[key.toLowerCase()] = val + }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index e0620329..d14a0cf4 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -60,11 +60,11 @@ const PasteInput = { if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) } else if (text[0] === '{') { - self.handleJSON(text) + Import.handleJSON(text) } else if (text.match(/\t/)) { - self.handleTSV(text) - } else { - // fail silently + Import.handleTSV(text) + } else if (text.match(/","/)) { + Import.handleCSV(text) } }, @@ -95,14 +95,6 @@ const PasteInput = { } } ) - }, - - handleJSON: function (text) { - Import.handleJSON(text) - }, - - handleTSV: function (text) { - Import.handleTSV(text) } } diff --git a/package.json b/package.json index 08c4f086..aa2caef9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "babel-preset-es2015": "6.9.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", + "csv-parse": "1.1.7", "node-uuid": "1.2.0", "react": "15.3.0", "react-dom": "15.3.0", From 35d6dbd0b43a1882c45af0dc03b57d240b7ddea8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 15:04:14 +0800 Subject: [PATCH 058/378] hide double click to add topic message if can't edit map --- frontend/src/Metamaps/JIT.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 5eccbc6c..1863fa75 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -150,7 +150,9 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() - $('#instructions div.addTopic').show() + if (Metamaps.Active.Map.authorizeToEdit()) { + $('#instructions div.addTopic').show() + } GlobalUI.showDiv('#instructions') Visualize.loadLater = true } From 5819447828caaef4d07ce29e102e8d7f038be2fb Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 18:49:16 +0800 Subject: [PATCH 059/378] fix git versioning --- app/assets/javascripts/src/Metamaps.Erb.js.erb | 2 ++ app/views/layouts/_lightboxes.html.erb | 2 ++ config/initializers/version.rb | 3 ++- frontend/src/Metamaps/index.js | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index 60b64e46..e8f3a25b 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -18,3 +18,5 @@ Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %> Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.VERSION = '<%= METAMAPS_VERSION %>' +Metamaps.BUILD = '<%= METAMAPS_BUILD %>' +Metamaps.LAST_UPDATED = '<%= METAMAPS_LAST_UPDATED %>' diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 89b5a6b4..42c959d6 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -13,12 +13,14 @@ <div id="leftAboutParms"> <p>STATUS: </p> <p>VERSION:</p> + <p>BUILD:</p> <p>LAST UPDATE:</p> </div> <div id="rightAboutParms"> <p>PRIVATE BETA</p> <p><%= METAMAPS_VERSION %></p> + <p><%= METAMAPS_BUILD %></p> <p><%= METAMAPS_LAST_UPDATED %></p> </div> <div class="clearfloat"> diff --git a/config/initializers/version.rb b/config/initializers/version.rb index ff08c330..be0330c3 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true -METAMAPS_VERSION = '2 build `git log -1 --pretty=%H`' +METAMAPS_VERSION = '2.9' +METAMAPS_BUILD = `git log -1 --pretty=%H`.chomp.freeze METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index e1aa8b34..21a3fb8d 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -31,6 +31,7 @@ import Util from './Util' import Views from './Views' import Visualize from './Visualize' +Metamaps = window.Metamaps || {} Metamaps.Account = Account Metamaps.Active = Active Metamaps.Admin = Admin From 77342727370d80a102382f42c5761a8365d6a42a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 20:10:18 +0800 Subject: [PATCH 060/378] hide circles when transitioning from topic view to map view fixes #389 --- frontend/src/Metamaps/Router.js | 9 ++------- frontend/src/Metamaps/Visualize.js | 8 +++++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 073c1d1b..8bcd3590 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -6,7 +6,6 @@ import Backbone from 'backbone' import Active from './Active' import GlobalUI from './GlobalUI' -import JIT from './JIT' import Map from './Map' import Topic from './Topic' import Views from './Views' @@ -170,9 +169,7 @@ const _Router = Backbone.Router.extend({ // clear the visualization, if there was one, before showing its div again if (Visualize.mGraph) { - Visualize.mGraph.graph.empty() - Visualize.mGraph.plot() - JIT.centerMap(Visualize.mGraph.canvas) + Visualize.clearVisualization() } GlobalUI.showDiv('#infovis') Topic.end() @@ -198,9 +195,7 @@ const _Router = Backbone.Router.extend({ // clear the visualization, if there was one, before showing its div again if (Visualize.mGraph) { - Visualize.mGraph.graph.empty() - Visualize.mGraph.plot() - JIT.centerMap(Visualize.mGraph.canvas) + Visualize.clearVisualization() } GlobalUI.showDiv('#infovis') Map.end() diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index df5bab99..3804b6a8 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -220,7 +220,13 @@ const Visualize = { Router.navigate('/topics/' + t.id) } }, 800) - } + }, + clearVisualization: function() { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) + $('#infovis').empty() + }, } export default Visualize From 11d13445fbff30bc450a7e1313f3ab71fb1b7774 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 20:14:45 +0800 Subject: [PATCH 061/378] fix authorizeToEdit call --- frontend/src/Metamaps/JIT.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 1863fa75..2b14d18c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -150,13 +150,14 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit()) { + if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { $('#instructions div.addTopic').show() } GlobalUI.showDiv('#instructions') Visualize.loadLater = true + } else { + GlobalUI.hideDiv('#instructions') } - else GlobalUI.hideDiv('#instructions') Visualize.render() }, // prepareVizData From 2ade375c204301dfbc4121f1e47d1c191d8c89e1 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 21:53:26 +0800 Subject: [PATCH 062/378] babel-plugin-lodash to slim down bundle size by 300 KB --- .babelrc | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index 2ad52bf1..f9151299 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,7 @@ "es2015" ], "plugins": [ + "lodash", "transform-class-properties" ] } diff --git a/package.json b/package.json index aa2caef9..02dc7237 100644 --- a/package.json +++ b/package.json @@ -21,17 +21,18 @@ "autolinker": "0.17.1", "babel-cli": "6.11.4", "babel-loader": "6.2.4", + "babel-plugin-lodash": "^3.2.9", "babel-plugin-transform-class-properties": "6.11.5", "babel-preset-es2015": "6.9.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", "csv-parse": "1.1.7", + "lodash": "4.16.1", "node-uuid": "1.2.0", "react": "15.3.0", "react-dom": "15.3.0", "requirejs": "2.1.1", "socket.io": "0.9.12", - "underscore": "1.4.4", "webpack": "1.13.1" }, "devDependencies": { From 0df17c4aa05dfbe44cc2201e2303449e2dc6580f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 21:53:40 +0800 Subject: [PATCH 063/378] update deps in package.json --- package.json | 15 +++++++-------- webpack.config.js | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 02dc7237..b71e34bf 100644 --- a/package.json +++ b/package.json @@ -19,21 +19,20 @@ "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { "autolinker": "0.17.1", - "babel-cli": "6.11.4", - "babel-loader": "6.2.4", + "babel-cli": "6.14.0", + "babel-loader": "6.2.5", "babel-plugin-lodash": "^3.2.9", "babel-plugin-transform-class-properties": "6.11.5", - "babel-preset-es2015": "6.9.0", + "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", "csv-parse": "1.1.7", "lodash": "4.16.1", - "node-uuid": "1.2.0", - "react": "15.3.0", - "react-dom": "15.3.0", - "requirejs": "2.1.1", + "node-uuid": "1.4.7", + "react": "15.3.2", + "react-dom": "15.3.2", "socket.io": "0.9.12", - "webpack": "1.13.1" + "webpack": "1.13.2" }, "devDependencies": { "chai": "^3.5.0", diff --git a/webpack.config.js b/webpack.config.js index 87881667..fcdcbc04 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const plugins = [ }) ] if (NODE_ENV === 'production') { + plugins.push(new webpack.optimize.DedupePlugin()) plugins.push(new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })) From 8c16c60554d6cd50419c0ca8321b463dceb63052 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:44:07 +0800 Subject: [PATCH 064/378] show link remover for invalid links too --- frontend/src/Metamaps/TopicCard.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index dad58565..68061d96 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -72,9 +72,11 @@ const TopicCard = { } $('.CardOnGraph').addClass('hasAttachment') - if (self.authorizedToEdit) { + }, + showLinkRemover: function() { + if (TopicCard.authorizedToEdit && $('#linkremove').length === 0) { $('.embeds').append('<div id="linkremove"></div>') - $('#linkremove').click(self.removeLink) + $('#linkremove').click(TopicCard.removeLink) } }, removeLink: function () { @@ -151,6 +153,7 @@ const TopicCard = { loader.setRange(0.9); // default is 1.3 loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) + self.showLinkRemover() if (!e) { self.handleInvalidLink() } From cc2e3b9358ef1f30e004bbf6e32759a8fb4cf918 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:29:28 +0800 Subject: [PATCH 065/378] hack to get the <title> tag when importing a url, without CORS issues --- app/controllers/hacks_controller.rb | 35 +++++++++++++++++++++++++++++ app/policies/hack_policy.rb | 5 +++++ config/routes.rb | 4 ++++ frontend/src/Metamaps/PasteInput.js | 12 ++++++++++ 4 files changed, 56 insertions(+) create mode 100644 app/controllers/hacks_controller.rb create mode 100644 app/policies/hack_policy.rb diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb new file mode 100644 index 00000000..42bafd6f --- /dev/null +++ b/app/controllers/hacks_controller.rb @@ -0,0 +1,35 @@ +# bad code that should be seriously checked over before entering one of the +# other prim and proper files in the nice section of this repo +class HacksController < ApplicationController + include ActionView::Helpers::TextHelper # string truncate method + + def load_url_title + authorize :Hack + url = params[:url] # TODO verify!?!?!?! + response, url = get_with_redirects(url) + title = get_encoded_title(response) + render json: { success: true, title: title, url: url } + rescue StandardError => e + render json: { success: false, error_type: e.class.name, error_message: e.message } + end + + private + + def get_with_redirects(url) + uri = URI.parse(url) + response = Net::HTTP.get_response(uri) + while response.code == '301' + uri = URI.parse(response['location']) + response = Net::HTTP.get_response(uri) + end + [response, uri.to_s] + end + + def get_encoded_title(http_response) + title = http_response.body.sub(/.*<title>(.*)<\/title>.*/m, '\1') + charset = http_response['content-type'].sub(/.*charset=(.*);?.*/, '\1') + charset = nil if charset == 'text/html' + title = title.force_encoding(charset) if charset + truncate(title, length: 140) + end +end diff --git a/app/policies/hack_policy.rb b/app/policies/hack_policy.rb new file mode 100644 index 00000000..b6fbf6ce --- /dev/null +++ b/app/policies/hack_policy.rb @@ -0,0 +1,5 @@ +class HackPolicy < ApplicationPolicy + def load_url_title? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index fe48b6ba..84112d23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,4 +80,8 @@ Metamaps::Application.routes.draw do get 'users/:id/details', to: 'users#details', as: :details post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes resources :users, except: [:index, :destroy] + + namespace :hacks do + get 'load_url_title' + end end diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index d14a0cf4..13258857 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -88,6 +88,18 @@ const PasteInput = { import_id, { success: function(topic) { + $.get('/hacks/load_url_title', { + url: text + }, function success(data, textStatus) { + var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' + if ($(selector).find('form').length > 0) { + $(selector).find('textarea, input').val(data.title) + } else { + $(selector).html(data.title) + } + topic.set('name', data.title) + topic.save() + }) TopicCard.showCard(topic.get('node'), function() { $('#showcard #titleActivator').click() .find('textarea, input').focus() From ceb26997606d9ca9f464b2f2647aaf86efc78945 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:54:40 +0800 Subject: [PATCH 066/378] install rack-attack --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index b4a0967b..d5b42d83 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'pg' gem 'pundit' gem 'pundit_extra' gem 'rack-cors' +gem 'rack-attack' gem 'redis' gem 'slack-notifier' gem 'snorlax' diff --git a/Gemfile.lock b/Gemfile.lock index 7e2590c1..23d4c827 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,6 +177,8 @@ GEM activesupport (>= 3.0.0) pundit_extra (0.3.0) rack (2.0.1) + rack-attack (5.0.1) + rack rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) @@ -316,6 +318,7 @@ DEPENDENCIES pry-rails pundit pundit_extra + rack-attack rack-cors rails (~> 5.0.0) rails3-jquery-autocomplete From 7f8110b6be5c486b71084fe6683594f1db2bbb23 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:00:07 +0800 Subject: [PATCH 067/378] configure rack attack to allow 5r/s for the load_url_title route --- config/application.rb | 2 ++ config/initializers/rack-attack.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 config/initializers/rack-attack.rb diff --git a/config/application.rb b/config/application.rb index b629682a..96505b32 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,6 +26,8 @@ module Metamaps Doorkeeper::ApplicationController.helper ApplicationHelper end + config.middleware.use Rack::Attack + # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb new file mode 100644 index 00000000..6c23e151 --- /dev/null +++ b/config/initializers/rack-attack.rb @@ -0,0 +1,15 @@ +class Rack::Attack +end + +Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + +# Throttle requests to 5 requests per second per ip +Rack::Attack.throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + # If the return value is truthy, the cache key for the return value + # is incremented and compared with the limit. In this case: + # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # + # If falsy, the cache key is neither incremented nor checked. + + req.ip if req.path === 'hacks/load_url_title' +end From 959aa693f357888aa77ff17378ab03ac0a082e00 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:06:09 +0800 Subject: [PATCH 068/378] ok, i guess this is ready --- app/controllers/hacks_controller.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb index 42bafd6f..1abe3e60 100644 --- a/app/controllers/hacks_controller.rb +++ b/app/controllers/hacks_controller.rb @@ -1,16 +1,18 @@ -# bad code that should be seriously checked over before entering one of the -# other prim and proper files in the nice section of this repo +# bad code that should be checked over before entering one of the +# nice files from the right side of this repo class HacksController < ApplicationController include ActionView::Helpers::TextHelper # string truncate method + # rate limited by rack-attack - currently 5r/s + # TODO: what else can we do to make get_with_redirects safer? def load_url_title authorize :Hack - url = params[:url] # TODO verify!?!?!?! + url = params[:url] response, url = get_with_redirects(url) title = get_encoded_title(response) render json: { success: true, title: title, url: url } rescue StandardError => e - render json: { success: false, error_type: e.class.name, error_message: e.message } + render json: { success: false } end private From eed5ff76efc021fad81b363874ca86f4c615fa56 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:21:51 +0800 Subject: [PATCH 069/378] add rate limiting headers --- config/initializers/rack-attack.rb | 64 +++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 6c23e151..9dfe3746 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -1,15 +1,59 @@ class Rack::Attack -end + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new -Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new - -# Throttle requests to 5 requests per second per ip -Rack::Attack.throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| - # If the return value is truthy, the cache key for the return value - # is incremented and compared with the limit. In this case: - # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # Throttle all requests by IP (60rpm) # - # If falsy, the cache key is neither incremented nor checked. + # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" + throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| + req.ip # unless req.path.start_with?('/assets') + end - req.ip if req.path === 'hacks/load_url_title' + # Throttle POST requests to /login by IP address + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" + throttle('logins/ip', :limit => 5, :period => 20.seconds) do |req| + if req.path == '/login' && req.post? + req.ip + end + end + + # Throttle POST requests to /login by email param + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}" + # + # Note: This creates a problem where a malicious user could intentionally + # throttle logins for another user and force their login requests to be + # denied, but that's not very common and shouldn't happen to you. (Knock + # on wood!) + throttle("logins/email", :limit => 5, :period => 20.seconds) do |req| + if req.path == '/login' && req.post? + # return the email if present, nil otherwise + req.params['email'].presence + end + end + + throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + # If the return value is truthy, the cache key for the return value + # is incremented and compared with the limit. In this case: + # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # + # If falsy, the cache key is neither incremented nor checked. + + req.ip if req.path == 'hacks/load_url_title' + end + + self.throttled_response = lambda do |env| + now = Time.now + match_data = env['rack.attack.match_data'] + period = match_data[:period] + limit = match_data[:limit] + + headers = { + 'X-RateLimit-Limit' => limit.to_s, + 'X-RateLimit-Remaining' => '0', + 'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s + } + + [429, headers, ['']] + end end From 1ab87030081b54d0725ae2f65fbb5498d2717959 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:41:08 +0800 Subject: [PATCH 070/378] move explore maps methods into their own controller --- app/controllers/explore_controller.rb | 114 +++++++++++++++ app/controllers/maps_controller.rb | 201 ++++++-------------------- app/policies/explore_policy.rb | 25 ++++ 3 files changed, 183 insertions(+), 157 deletions(-) create mode 100644 app/controllers/explore_controller.rb create mode 100644 app/policies/explore_policy.rb diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb new file mode 100644 index 00000000..c21454d1 --- /dev/null +++ b/app/controllers/explore_controller.rb @@ -0,0 +1,114 @@ +class ExploreController < ApplicationController + before_action :authorize_explore + after_action :verify_authorized + after_action :verify_policy_scoped + + respond_to :html, :json, :csv + + # TODO remove? + #autocomplete :map, :name, full: true, extra_data: [:user_id] + + # GET /explore/active + def activemaps + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope(Map).order('updated_at DESC') + .page(page).per(20) + + respond_to do |format| + format.html do + # root url => main/home. main/home renders maps/activemaps view. + redirect_to(root_url) && return if authenticated? + respond_with(@maps, @user) + end + format.json { render json: @maps } + end + end + + # GET /explore/featured + def featuredmaps + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.featured = ? AND maps.permission != ?', + true, 'private') + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mine + def mymaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.user_id = ?', current_user.id) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/shared + def sharedmaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/starred + def starredmaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + stars = current_user.stars.map(&:map_id) + @maps = policy_scope( + Map.where('maps.id IN (?)', stars) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mapper/:id + def usermaps + page = params[:page].present? ? params[:page] : 1 + @user = User.find(params[:id]) + @maps = policy_scope(Map.where(user: @user)) + .order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + private + + def authorize_explore + authorize :Explore + end +end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 4a091e17..0d308f1d 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,111 +1,12 @@ # frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] - after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] + after_action :verify_authorized respond_to :html, :json, :csv autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET /explore/active - def activemaps - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope(Map).order('updated_at DESC') - .page(page).per(20) - - respond_to do |format| - format.html do - # root url => main/home. main/home renders maps/activemaps view. - redirect_to(root_url) && return if authenticated? - respond_with(@maps, @user) - end - format.json { render json: @maps.to_json } - end - end - - # GET /explore/featured - def featuredmaps - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.featured = ? AND maps.permission != ?', - true, 'private') - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/mine - def mymaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.user_id = ?', current_user.id) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/shared - def sharedmaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/starred - def starredmaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - stars = current_user.stars.map(&:map_id) - @maps = policy_scope( - Map.where('maps.id IN (?)', stars) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/mapper/:id - def usermaps - page = params[:page].present? ? params[:page] : 1 - @user = User.find(params[:id]) - @maps = policy_scope(Map.where(user: @user)) - .order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - # GET maps/new def new @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) @@ -182,78 +83,31 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map - @allmappers = @map.contributors - @allcollaborators = @map.editors - @alltopics = @map.topics.to_a.delete_if { |t| !policy(t).show? } - @allsynapses = @map.synapses.to_a.delete_if { |s| !policy(s).show? } - @allmappings = @map.mappings.to_a.delete_if { |m| !policy(m).show? } - - @json = {} - @json['map'] = @map - @json['topics'] = @alltopics - @json['synapses'] = @allsynapses - @json['mappings'] = @allmappings - @json['mappers'] = @allmappers - @json['collaborators'] = @allcollaborators - @json['messages'] = @map.messages.sort_by(&:created_at) - @json['stars'] = @map.stars - respond_to do |format| - format.json { render json: @json } + format.json { render json: @map.contains(current_user) } end end # POST maps def create @user = current_user - @map = Map.new - @map.name = params[:name] - @map.desc = params[:desc] - @map.permission = params[:permission] + @map = Map.new(create_map_params) @map.user = @user @map.arranged = false - if params[:topicsToMap] - @all = params[:topicsToMap] - @all = @all.split(',') - @all.each do |topic| - topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] - authorize mapping, :create? - mapping.save - end - - if params[:synapsesToMap] - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) - authorize mapping, :create? - mapping.save - end - end - + if params[:topicsToMap].present? + create_topics! + create_synapses! if params[:synapsesToMap].present? @map.arranged = true end authorize @map + respond_to do |format| if @map.save - respond_to do |format| - format.json { render json: @map } - end + format.json { render json: @map } else - respond_to do |format| - format.json { render json: 'invalid params' } - end + format.json { render json: 'invalid params' } end end @@ -263,7 +117,7 @@ class MapsController < ApplicationController authorize @map respond_to do |format| - if @map.update_attributes(map_params) + if @map.update_attributes(update_map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } @@ -366,7 +220,40 @@ class MapsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. - def map_params + def create_map_params + params.require(:map).permit(:name, :desc, :permission) + end + + def update_map_params params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end + + def create_topics! + topics = params[:topicsToMap] + topics = topics.split(',') + topics.each do |topic| + topic = topic.split('/') + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Topic.find(topic[0]) + mapping.xloc = topic[1] + mapping.yloc = topic[2] + authorize mapping, :create? + mapping.save + end + end + + def create_synapses! + @synAll = params[:synapsesToMap] + @synAll = @synAll.split(',') + @synAll.each do |synapse_id| + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Synapse.find(synapse_id) + authorize mapping, :create? + mapping.save + end + end end diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb new file mode 100644 index 00000000..6cbdab15 --- /dev/null +++ b/app/policies/explore_policy.rb @@ -0,0 +1,25 @@ +class ExplorePolicy < ApplicationPolicy + def activemaps? + true + end + + def featuredmaps? + true + end + + def mymaps? + true + end + + def sharedmaps? + true + end + + def starredmaps? + true + end + + def usermaps? + true + end +end From 40bd9ed95a30996c6785b33688eda02a6ef24d60 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:41:15 +0800 Subject: [PATCH 071/378] refactor maps controller a bit --- app/models/map.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/models/map.rb b/app/models/map.rb index 9c30479f..7de744a2 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -108,4 +108,23 @@ class Map < ApplicationRecord self.screenshot = data save end + + # user param helps determine what records are visible + def contains(user) + allmappers = contributors + allcollaborators = editors + alltopics = Pundit.policy_scope(user, topics).to_a + allsynapses = Pundit.policy_scope(user, synapses).to_a + allmappings = Pundit.policy_scope(user, mappings).to_a + + json = {} + json['map'] = self + json['topics'] = alltopics + json['synapses'] = allsynapses + json['mappings'] = allmappings + json['mappers'] = allmappers + json['collaborators'] = allcollaborators + json['messages'] = messages.sort_by(&:created_at) + json['stars'] = stars + end end From 7275beb163d585ed733df25d2a41079fbfba40c4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:48:43 +0800 Subject: [PATCH 072/378] put CRUD at top of maps controller, and alphabetize other actions below --- app/controllers/maps_controller.rb | 243 +++++++++++++---------------- 1 file changed, 111 insertions(+), 132 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 0d308f1d..ff7a1686 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,12 +1,33 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] + before_action :require_user, only: [:create, :update, :destroy, :access, :events, :screenshot, :star, :unstar] + before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized respond_to :html, :json, :csv autocomplete :map, :name, full: true, extra_data: [:user_id] + # GET maps/:id + def show + respond_to do |format| + format.html do + @allmappers = @map.contributors + @allcollaborators = @map.editors + @alltopics = policy_scope(@map.topics) + @allsynapses = policy_scope(@map.synapses) + @allmappings = policy_scope(@map.mappings) + @allmessages = @map.messages.sort_by(&:created_at) + @allstars = @map.stars + + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) + end + format.json { render json: @map } + format.csv { redirect_to action: :export, format: :csv } + format.xls { redirect_to action: :export, format: :xls } + end + end + # GET maps/new def new @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) @@ -21,73 +42,6 @@ class MapsController < ApplicationController end end - # GET maps/:id - def show - @map = Map.find(params[:id]) - authorize @map - - respond_to do |format| - format.html do - @allmappers = @map.contributors - @allcollaborators = @map.editors - @alltopics = @map.topics.to_a.delete_if { |t| !policy(t).show? } - @allsynapses = @map.synapses.to_a.delete_if { |s| !policy(s).show? } - @allmappings = @map.mappings.to_a.delete_if { |m| !policy(m).show? } - @allmessages = @map.messages.sort_by(&:created_at) - @allstars = @map.stars - - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) - end - format.json { render json: @map } - format.csv { redirect_to action: :export, format: :csv } - format.xls { redirect_to action: :export, format: :xls } - end - end - - # GET maps/:id/export - def export - map = Map.find(params[:id]) - authorize map - exporter = MapExportService.new(current_user, map) - respond_to do |format| - format.json { render json: exporter.json } - format.csv { send_data exporter.csv } - format.xls { @spreadsheet = exporter.xls } - end - end - - # POST maps/:id/events/:event - def events - map = Map.find(params[:id]) - authorize map - - valid_event = false - if params[:event] == 'conversation' - Events::ConversationStartedOnMap.publish!(map, current_user) - valid_event = true - elsif params[:event] == 'user_presence' - Events::UserPresentOnMap.publish!(map, current_user) - valid_event = true - end - - respond_to do |format| - format.json do - head :ok if valid_event - head :bad_request unless valid_event - end - end - end - - # GET maps/:id/contains - def contains - @map = Map.find(params[:id]) - authorize @map - - respond_to do |format| - format.json { render json: @map.contains(current_user) } - end - end - # POST maps def create @user = current_user @@ -104,18 +58,16 @@ class MapsController < ApplicationController authorize @map respond_to do |format| - if @map.save - format.json { render json: @map } - else - format.json { render json: 'invalid params' } + if @map.save + format.json { render json: @map } + else + format.json { render json: 'invalid params' } + end end end # PUT maps/:id def update - @map = Map.find(params[:id]) - authorize @map - respond_to do |format| if @map.update_attributes(update_map_params) format.json { head :no_content } @@ -125,10 +77,19 @@ class MapsController < ApplicationController end end + # DELETE maps/:id + def destroy + @map.delete + + respond_to do |format| + format.json do + head :no_content + end + end + end + # POST maps/:id/access def access - @map = Map.find(params[:id]) - authorize @map userIds = params[:access] || [] added = userIds.select do |uid| user = User.find(uid) @@ -155,39 +116,44 @@ class MapsController < ApplicationController end end - # POST maps/:id/star - def star - @map = Map.find(params[:id]) - authorize @map - star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - star = Star.create(map_id: @map.id, user_id: current_user.id) unless star - + # GET maps/:id/contains + def contains respond_to do |format| - format.json do - render json: { message: 'Successfully starred map' } - end + format.json { render json: @map.contains(current_user) } end end - # POST maps/:id/unstar - def unstar - @map = Map.find(params[:id]) - authorize @map - star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - star&.delete + # GET maps/:id/export + def export + exporter = MapExportService.new(current_user, @map) + respond_to do |format| + format.json { render json: exporter.json } + format.csv { send_data exporter.csv } + format.xls { @spreadsheet = exporter.xls } + end + end + + # POST maps/:id/events/:event + def events + valid_event = false + if params[:event] == 'conversation' + Events::ConversationStartedOnMap.publish!(@map, current_user) + valid_event = true + elsif params[:event] == 'user_presence' + Events::UserPresentOnMap.publish!(@map, current_user) + valid_event = true + end respond_to do |format| format.json do - render json: { message: 'Successfully unstarred map' } + head :bad_request unless valid_event + head :ok end end end # POST maps/:id/upload_screenshot def screenshot - @map = Map.find(params[:id]) - authorize @map - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length..-1]) StringIO.open(png) do |data| data.class.class_eval { attr_accessor :original_filename, :content_type } @@ -203,23 +169,36 @@ class MapsController < ApplicationController end end - # DELETE maps/:id - def destroy - @map = Map.find(params[:id]) - authorize @map - - @map.delete + # POST maps/:id/star + def star + star = Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) respond_to do |format| format.json do - head :no_content + render json: { message: 'Successfully starred map' } + end + end + end + + # POST maps/:id/unstar + def unstar + star = Star.find_by(map_id: @map.id, user_id: current_user.id) + star&.delete + + respond_to do |format| + format.json do + render json: { message: 'Successfully unstarred map' } end end end private - # Never trust parameters from the scary internet, only allow the white list through. + def set_map + @map = Map.find(params[:id]) + authorize @map + end + def create_map_params params.require(:map).permit(:name, :desc, :permission) end @@ -228,32 +207,32 @@ class MapsController < ApplicationController params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end - def create_topics! - topics = params[:topicsToMap] - topics = topics.split(',') - topics.each do |topic| - topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] - authorize mapping, :create? - mapping.save - end - end + def create_topics! + topics = params[:topicsToMap] + topics = topics.split(',') + topics.each do |topic| + topic = topic.split('/') + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Topic.find(topic[0]) + mapping.xloc = topic[1] + mapping.yloc = topic[2] + authorize mapping, :create? + mapping.save + end + end - def create_synapses! - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) - authorize mapping, :create? - mapping.save - end - end + def create_synapses! + @synAll = params[:synapsesToMap] + @synAll = @synAll.split(',') + @synAll.each do |synapse_id| + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Synapse.find(synapse_id) + authorize mapping, :create? + mapping.save + end + end end From 686d80e27412bbe9a088a300ed86b57f91810467 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:02:52 +0800 Subject: [PATCH 073/378] move more logic into map model --- app/controllers/maps_controller.rb | 31 ++++++--------------------- app/models/map.rb | 34 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index ff7a1686..1eba68e9 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -90,24 +90,13 @@ class MapsController < ApplicationController # POST maps/:id/access def access - userIds = params[:access] || [] - added = userIds.select do |uid| - user = User.find(uid) - if user.nil? || (current_user && user == current_user) - false - else - !@map.collaborators.include?(user) - end - end - removed = @map.collaborators.select { |user| !userIds.include?(user.id.to_s) }.map(&:id) - added.each do |uid| - UserMap.create(user_id: uid.to_i, map_id: @map.id) - user = User.find(uid.to_i) - MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later - end - removed.each do |uid| - @map.user_maps.select { |um| um.user_id == uid }.each(&:destroy) + user_ids = params[:access] || [] + + added = @map.add_new_collaborators(user_ids) + added.each do |user_id| + MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later end + @map.remove_old_collaborators(user_ids) respond_to do |format| format.json do @@ -154,13 +143,7 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length..-1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' - data.content_type = 'image/png' - @map.screenshot = data - end + @map.base64_screenshot(params[:encoded_image]) if @map.save render json: { message: 'Successfully uploaded the map screenshot.' } diff --git a/app/models/map.rb b/app/models/map.rb index 7de744a2..ab3f3eb1 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -127,4 +127,38 @@ class Map < ApplicationRecord json['messages'] = messages.sort_by(&:created_at) json['stars'] = stars end + + def add_new_collaborators(user_ids) + added = [] + users = User.where(id: user_ids) + users.each do |user| + if user && user != current_user && !collaborators.include?(user) + UserMap.create(user_id: uid.to_i, map_id: id) + user = User.find(uid.to_i) + added << user.id + end + end + added + end + + def remove_old_collaborators(user_ids) + removed = [] + collaborators.map(&:id).each do |user_id| + if !user_ids.include?(user_id) + user_maps.select { |um| um.user_id == user_id }.each(&:destroy) + removed << user_id + end + end + removed + end + + def base64_screenshot(encoded_image) + png = Base64.decode64(encoded_image['data:image/png;base64,'.length..-1]) + StringIO.open(png) do |data| + data.class.class_eval { attr_accessor :original_filename, :content_type } + data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' + data.content_type = 'image/png' + @map.screenshot = data + end + end end From 5e180ac10e5d0348c9b5b4a2f30d865df2331ec2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:07:06 +0800 Subject: [PATCH 074/378] set up explore controller routes and rename methods --- app/controllers/explore_controller.rb | 12 ++++++------ config/routes.rb | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index c21454d1..023a6866 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController #autocomplete :map, :name, full: true, extra_data: [:user_id] # GET /explore/active - def activemaps + def active page = params[:page].present? ? params[:page] : 1 @maps = policy_scope(Map).order('updated_at DESC') .page(page).per(20) @@ -25,7 +25,7 @@ class ExploreController < ApplicationController end # GET /explore/featured - def featuredmaps + def featured page = params[:page].present? ? params[:page] : 1 @maps = policy_scope( Map.where('maps.featured = ? AND maps.permission != ?', @@ -39,7 +39,7 @@ class ExploreController < ApplicationController end # GET /explore/mine - def mymaps + def mine unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -57,7 +57,7 @@ class ExploreController < ApplicationController end # GET /explore/shared - def sharedmaps + def shared unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -75,7 +75,7 @@ class ExploreController < ApplicationController end # GET /explore/starred - def starredmaps + def starred unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -94,7 +94,7 @@ class ExploreController < ApplicationController end # GET /explore/mapper/:id - def usermaps + def mapper page = params[:page].present? ? params[:page] : 1 @user = User.find(params[:id]) @maps = policy_scope(Map.where(user: @user)) diff --git a/config/routes.rb b/config/routes.rb index fe48b6ba..f9a1be8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -57,12 +57,14 @@ Metamaps::Application.routes.draw do post 'maps/:id/star', to: 'maps#star', defaults: { format: :json } post 'maps/:id/unstar', to: 'maps#unstar', defaults: { format: :json } - get 'explore/active', to: 'maps#activemaps' - get 'explore/featured', to: 'maps#featuredmaps' - get 'explore/mine', to: 'maps#mymaps' - get 'explore/shared', to: 'maps#sharedmaps' - get 'explore/starred', to: 'maps#starredmaps' - get 'explore/mapper/:id', to: 'maps#usermaps' + namespace :explore do + get 'active' + get 'featured' + get 'mine' + get 'shared' + get 'starred' + get 'mapper/:id', action: 'mapper' + end devise_for :users, skip: :sessions, controllers: { registrations: 'users/registrations', From b722d2d3b07e4a74901b0fd98e723ed55490a047 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:20:06 +0800 Subject: [PATCH 075/378] fix map controller create spec --- app/controllers/maps_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 1eba68e9..a74a35c2 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -183,7 +183,7 @@ class MapsController < ApplicationController end def create_map_params - params.require(:map).permit(:name, :desc, :permission) + params.permit(:name, :desc, :permission) end def update_map_params From 3f9077b3805e77735c9673ea5c81ebb7b47e380f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:35:06 +0800 Subject: [PATCH 076/378] clean up --- app/policies/map_policy.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 4cb2db38..84d24ca4 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -41,10 +41,6 @@ class MapPolicy < ApplicationPolicy user.present? && record.user == user end - def activemaps? - user.blank? # redirect to root url if authenticated for some reason - end - def contains? show? end @@ -57,14 +53,6 @@ class MapPolicy < ApplicationPolicy show? end - def featuredmaps? - true - end - - def mymaps? - user.present? - end - def star? unstar? end @@ -76,8 +64,4 @@ class MapPolicy < ApplicationPolicy def screenshot? update? end - - def usermaps? - true - end end From c76de5b1d5cb18add72499ff41d72ae74bbaf4e7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:54:14 +0800 Subject: [PATCH 077/378] refactor map model a bit and fix bugs --- app/models/map.rb | 52 ++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index ab3f3eb1..0cab398d 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -111,45 +111,37 @@ class Map < ApplicationRecord # user param helps determine what records are visible def contains(user) - allmappers = contributors - allcollaborators = editors - alltopics = Pundit.policy_scope(user, topics).to_a - allsynapses = Pundit.policy_scope(user, synapses).to_a - allmappings = Pundit.policy_scope(user, mappings).to_a - - json = {} - json['map'] = self - json['topics'] = alltopics - json['synapses'] = allsynapses - json['mappings'] = allmappings - json['mappers'] = allmappers - json['collaborators'] = allcollaborators - json['messages'] = messages.sort_by(&:created_at) - json['stars'] = stars + { + map: self, + topics: Pundit.policy_scope(user, topics).to_a, + synapses: Pundit.policy_scope(user, synapses).to_a, + mappings: Pundit.policy_scope(user, mappings).to_a, + mappers: contributors, + collaborators: editors, + messages: messages.sort_by(&:created_at), + stars: stars + } end def add_new_collaborators(user_ids) - added = [] users = User.where(id: user_ids) - users.each do |user| - if user && user != current_user && !collaborators.include?(user) - UserMap.create(user_id: uid.to_i, map_id: id) - user = User.find(uid.to_i) - added << user.id - end + current_collaborators = collaborators + [user] + added = users.map do |new_user| + next nil if current_collaborators.include?(new_user) + UserMap.create(user_id: new_user.id, map_id: id) + new_user.id end - added + added.compact end def remove_old_collaborators(user_ids) - removed = [] - collaborators.map(&:id).each do |user_id| - if !user_ids.include?(user_id) - user_maps.select { |um| um.user_id == user_id }.each(&:destroy) - removed << user_id - end + current_collaborators = collaborators + [user] + removed = current_collaborators.map(&:id).map do |old_user_id| + next nil if user_ids.include?(old_user_id) + user_maps.where(user_id: old_user_id).find_each(&:destroy) + old_user_id end - removed + removed.compact end def base64_screenshot(encoded_image) From dad048eb20341a6acd27bf2e72dd65b5c17e0809 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:58:09 +0800 Subject: [PATCH 078/378] rubocop --- app/controllers/explore_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 023a6866..3c099920 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ExploreController < ApplicationController before_action :authorize_explore after_action :verify_authorized @@ -5,8 +6,8 @@ class ExploreController < ApplicationController respond_to :html, :json, :csv - # TODO remove? - #autocomplete :map, :name, full: true, extra_data: [:user_id] + # TODO: remove? + # autocomplete :map, :name, full: true, extra_data: [:user_id] # GET /explore/active def active From 50f98aebea73fab221a4d0534fb07aef7da6b48b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:35:23 +0800 Subject: [PATCH 079/378] explore controller spec --- app/controllers/explore_controller.rb | 12 +++++------ app/policies/explore_policy.rb | 12 +++++------ spec/controllers/explore_controller_spec.rb | 23 +++++++++++++++++++++ spec/controllers/maps_controller_spec.rb | 15 -------------- 4 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 spec/controllers/explore_controller_spec.rb diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 3c099920..6f24eba5 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -21,7 +21,7 @@ class ExploreController < ApplicationController redirect_to(root_url) && return if authenticated? respond_with(@maps, @user) end - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -35,7 +35,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -53,7 +53,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -71,7 +71,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -90,7 +90,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -103,7 +103,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb index 6cbdab15..b4d52fe5 100644 --- a/app/policies/explore_policy.rb +++ b/app/policies/explore_policy.rb @@ -1,25 +1,25 @@ class ExplorePolicy < ApplicationPolicy - def activemaps? + def active? true end - def featuredmaps? + def featured? true end - def mymaps? + def mine? true end - def sharedmaps? + def shared? true end - def starredmaps? + def starred? true end - def usermaps? + def mapper? true end end diff --git a/spec/controllers/explore_controller_spec.rb b/spec/controllers/explore_controller_spec.rb new file mode 100644 index 00000000..4e298a92 --- /dev/null +++ b/spec/controllers/explore_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe ExploreController, type: :controller do + before :each do + sign_in create(:user) + end + + describe 'GET explore/active' do + context 'always returns an array' do + it 'with 0 records' do + Map.delete_all + get :active, format: :json + expect(JSON.parse(response.body)).to eq [] + end + it 'with 1 record' do + map = create(:map) + get :active, format: :json + expect(JSON.parse(response.body).class).to be Array + end + end + end +end diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index b877dc88..0f053dd9 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -9,21 +9,6 @@ RSpec.describe MapsController, type: :controller do sign_in create(:user) end - describe 'GET #activemaps' do - context 'always returns an array' do - it 'with 0 records' do - Map.delete_all - get :activemaps, format: :json - expect(JSON.parse(response.body)).to eq [] - end - it 'with 1 record' do - map = create(:map) - get :activemaps, format: :json - expect(JSON.parse(response.body).class).to be Array - end - end - end - describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do From 18d8929bf159821d2231a4833c443b3eb8c571d7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:35:26 +0800 Subject: [PATCH 080/378] use .or to fix all sorts of @map.mappings bugs --- app/models/map.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 0cab398d..f9fe6312 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -16,11 +16,12 @@ class Map < ApplicationRecord has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy # This method associates the attribute ":image" with a file attachment - has_attached_file :screenshot, styles: { - thumb: ['188x126#', :png] - #:full => ['940x630#', :png] - }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + has_attached_file :screenshot, + styles: { + thumb: ['188x126#', :png] + #:full => ['940x630#', :png] + }, + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -31,7 +32,7 @@ class Map < ApplicationRecord validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ def mappings - topicmappings + synapsemappings + topicmappings.or(synapsemappings) end def mk_permission From 05495b02246afa85845e0c70b5668283295f2c50 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:35:35 +0800 Subject: [PATCH 081/378] move explore views to their own folder --- app/views/{maps/activemaps.html.erb => explore/active.html.erb} | 0 .../{maps/featuredmaps.html.erb => explore/featured.html.erb} | 0 app/views/{maps/usermaps.html.erb => explore/mapper.html.erb} | 0 app/views/{maps/mymaps.html.erb => explore/mine.html.erb} | 0 app/views/{maps/sharedmaps.html.erb => explore/shared.html.erb} | 0 app/views/{maps/starredmaps.html.erb => explore/starred.html.erb} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename app/views/{maps/activemaps.html.erb => explore/active.html.erb} (100%) rename app/views/{maps/featuredmaps.html.erb => explore/featured.html.erb} (100%) rename app/views/{maps/usermaps.html.erb => explore/mapper.html.erb} (100%) rename app/views/{maps/mymaps.html.erb => explore/mine.html.erb} (100%) rename app/views/{maps/sharedmaps.html.erb => explore/shared.html.erb} (100%) rename app/views/{maps/starredmaps.html.erb => explore/starred.html.erb} (100%) diff --git a/app/views/maps/activemaps.html.erb b/app/views/explore/active.html.erb similarity index 100% rename from app/views/maps/activemaps.html.erb rename to app/views/explore/active.html.erb diff --git a/app/views/maps/featuredmaps.html.erb b/app/views/explore/featured.html.erb similarity index 100% rename from app/views/maps/featuredmaps.html.erb rename to app/views/explore/featured.html.erb diff --git a/app/views/maps/usermaps.html.erb b/app/views/explore/mapper.html.erb similarity index 100% rename from app/views/maps/usermaps.html.erb rename to app/views/explore/mapper.html.erb diff --git a/app/views/maps/mymaps.html.erb b/app/views/explore/mine.html.erb similarity index 100% rename from app/views/maps/mymaps.html.erb rename to app/views/explore/mine.html.erb diff --git a/app/views/maps/sharedmaps.html.erb b/app/views/explore/shared.html.erb similarity index 100% rename from app/views/maps/sharedmaps.html.erb rename to app/views/explore/shared.html.erb diff --git a/app/views/maps/starredmaps.html.erb b/app/views/explore/starred.html.erb similarity index 100% rename from app/views/maps/starredmaps.html.erb rename to app/views/explore/starred.html.erb From 03ba3a89f1cf4c871f691d32fed7721fde605063 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:37:08 +0800 Subject: [PATCH 082/378] main controller renders by name --- app/controllers/main_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0d6af64b..4624c7a6 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -17,7 +17,7 @@ class MainController < ApplicationController if !authenticated? render 'main/home' else - render 'maps/activemaps' + render 'explore/active' end end end From c20e5037854ee05cb3296af8326fba83267c56e8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:27:11 +0800 Subject: [PATCH 083/378] show/hide add a topic instructions more consistently --- frontend/src/Metamaps/Backbone/index.js | 1 - frontend/src/Metamaps/Control.js | 27 ++++++++++++++++--------- frontend/src/Metamaps/Create.js | 6 ++++++ frontend/src/Metamaps/JIT.js | 11 ++++++---- package.json | 1 + 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 2c7ae530..b1ba9e78 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -98,7 +98,6 @@ _Backbone.Map = Backbone.Model.extend({ $.ajax({ url: '/maps/' + this.id + '/contains.json', success: start, - error: errorFunc, async: false }) }, diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 2c14cfca..c6c963ac 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,6 +1,7 @@ /* global Metamaps, $ */ import _ from 'lodash' +import outdent from 'outdent' import Active from './Active' import Filter from './Filter' @@ -52,9 +53,8 @@ const Control = { var n = Selected.Nodes.length var e = Selected.Edges.length - var ntext = n == 1 ? '1 topic' : n + ' topics' - var etext = e == 1 ? '1 synapse' : e + ' synapses' - var text = 'You have ' + ntext + ' and ' + etext + ' selected. ' + var ntext = n === 1 ? '1 topic' : n + ' topics' + var etext = e === 1 ? '1 synapse' : e + ' synapses' var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -63,11 +63,18 @@ const Control = { return } - var r = confirm(text + 'Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.') - if (r == true) { + var r = confirm(outdent` + You have ${ntext} and ${etext} selected. Are you sure you want + to permanently delete them all? This will remove them from all + maps they appear on.`) + if (r) { Control.deleteSelectedEdges() Control.deleteSelectedNodes() } + + if (Metamaps.Topics.length === 0) { + GlobalUI.showDiv('#instructions') + } }, deleteSelectedNodes: function () { // refers to deleting topics permanently if (!Active.Map) return @@ -191,7 +198,7 @@ const Control = { duration: 500 }) setTimeout(function () { - if (nodeid == Visualize.mGraph.root) { // && Visualize.type === "RGraph" + if (nodeid === Visualize.mGraph.root) { // && Visualize.type === "RGraph" var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) graph.root = newroot ? newroot.id : null } @@ -231,7 +238,7 @@ const Control = { color: Settings.colors.synapses.normal }) - if (Mouse.edgeHoveringOver == edge) { + if (Mouse.edgeHoveringOver === edge) { edge.setDataset('current', { showDesc: true, lineWidth: 4 @@ -414,8 +421,8 @@ const Control = { } } - var nString = nCount == 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and ') - var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') + var nString = nCount === 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and ') + var sString = sCount === 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') var message = nString + sString + ' you created updated to ' + permission GlobalUI.notifyUser(message) @@ -444,7 +451,7 @@ const Control = { } } - var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') + var nString = nCount === 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') var message = nString + ' you can edit updated to ' + metacode.get('name') GlobalUI.notifyUser(message) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index c9252aba..1fc18b87 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -13,6 +13,7 @@ import GlobalUI from './GlobalUI' * Dependencies: * - Metamaps.Backbone * - Metamaps.Metacodes + * - Metamaps.Topics */ const Create = { @@ -223,8 +224,10 @@ const Create = { }) Create.newTopic.beingCreated = true Create.newTopic.name = '' + GlobalUI.hideDiv('#instructions') }, hide: function (force) { + if (Create.newTopic.beingCreated === false) return if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') Create.newTopic.beingCreated = false @@ -234,6 +237,9 @@ const Create = { Create.newTopic.pinned = false } $('#topic_name').typeahead('val', '') + if (Metamaps.Topics.length === 0) { + GlobalUI.showDiv('#instructions') + } } }, newSynapse: { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 2b14d18c..fa5c894f 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -148,11 +148,14 @@ const JIT = { if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping) }) + // set up addTopic instructions in case they delete all the topics + // i.e. if there are 0 topics at any time, it should have instructions again + $('#instructions div').hide() + if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { + $('#instructions div.addTopic').show() + } + if (self.vizData.length == 0) { - $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { - $('#instructions div.addTopic').show() - } GlobalUI.showDiv('#instructions') Visualize.loadLater = true } else { diff --git a/package.json b/package.json index b71e34bf..37d953ee 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "csv-parse": "1.1.7", "lodash": "4.16.1", "node-uuid": "1.4.7", + "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", From 0e17ec11ec17ca9d137201669dff33f6aae01180 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:04:09 +0800 Subject: [PATCH 084/378] fix eslint config for code climate this is MOSTLY the same as feross/standard --- .eslintrc.js | 166 +++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 8 +-- 2 files changed, 164 insertions(+), 10 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index bc65fe94..26319254 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,165 @@ module.exports = { - "sourceType": "module", + "sourceType": "module", "parser": "babel-eslint", - "extends": "standard", "installedESLint": true, + "env": { + "es6": true, + "node": true + }, "plugins": [ - "standard" - ] -}; + "react" + ], + "globals": { + "document": false, + "navigator": false, + "window": false + }, + "rules": { + "accessor-pairs": 2, + "arrow-spacing": [2, { "before": true, "after": true }], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": [2, { "properties": "never" }], + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "constructor-super": 2, + "curly": [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + // errors on code climate - disable for now + //"func-call-spacing": [2, "never"], + "handle-callback-err": [2, "^(err|error)$" ], + "indent": [2, 2, { "SwitchCase": 1 }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + // errors on code climate - disable for now + //"keyword-spacing": [2, { "before": true, "after": true }], + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-const-assign": 2, + // errors on code climate - disable for now + //"no-constant-condition": [2, { "checkLoops": false }], + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + // errors on code climate - disable for now + //"no-duplicate-imports": 2, + "no-empty-character-class": 2, + "no-empty-pattern": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + // errors on code climate - disable for now + //"no-global-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + // errors on code climate - disable for now + //"no-labels": [2, { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + // errors on code climate - disable for now + //"no-new-symbol": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": [2, "except-parens"], + // errors on code climate - disable for now + //"no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-sparse-arrays": 2, + // errors on code climate - disable for now + //"no-tabs": 2, + // errors on code climate - disable for now + //"no-template-curly-in-string": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + // errors on code climate - disable for now + //"no-unmodified-loop-condition": 2, + "no-unneeded-ternary": [2, { "defaultAssignment": false }], + "no-unreachable": 2, + // errors on code climate - disable for now + //"no-unsafe-finally": 2, + // errors on code climate - disable for now + //"no-unsafe-negation": 2, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-useless-call": 2, + // errors on code climate - disable for now + //"no-useless-computed-key": 2, + // errors on code climate - disable for now + //"no-useless-constructor": 2, + // errors on code climate - disable for now + //"no-useless-escape": 2, + // errors on code climate - disable for now + //"no-useless-rename": 2, + // errors on code climate - disable for now + //"no-whitespace-before-property": 2, + "no-with": 2, + // errors on code climate - disable for now + //"object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], + "one-var": [2, { "initialized": "never" }], + "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": [2, "never"], + // errors on code climate - disable for now + //"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + // errors on code climate - disable for now + //"rest-spread-spacing": [2, "never"], + "semi": [2, "never"], + "semi-spacing": [2, { "before": false, "after": true }], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + // errors on code climate - disable for now + //"spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], + // errors on code climate - disable for now + //"template-curly-spacing": [2, "never"], + // errors on code climate - disable for now + //"unicode-bom": [2, "never"], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any"], + // errors on code climate - disable for now + //"yield-star-spacing": [2, "both"], + "yoda": [2, "never"], + } +} diff --git a/package.json b/package.json index 37d953ee..c6c74f44 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,10 @@ "webpack": "1.13.2" }, "devDependencies": { - "chai": "^3.5.0", - "mocha": "^3.0.2", "babel-eslint": "^6.1.2", + "chai": "^3.5.0", "eslint": "^3.5.0", - "eslint-config-standard": "^6.0.1", - "eslint-plugin-promise": "^2.0.1", - "eslint-plugin-standard": "^2.0.0" + "eslint-plugin-react": "^6.3.0", + "mocha": "^3.0.2" } } From ebaae084ae27890274c581b6f4fbe4b305ea034c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:37:42 +0800 Subject: [PATCH 085/378] simple eslint fixes --- frontend/src/Metamaps/Create.js | 4 ++-- frontend/src/Metamaps/GlobalUI.js | 2 +- frontend/src/Metamaps/JIT.js | 20 ++++++++------------ frontend/src/Metamaps/Map/InfoBox.js | 6 +++--- frontend/src/Metamaps/Realtime.js | 4 ++-- frontend/src/Metamaps/Views/ChatView.js | 8 +++++++- frontend/src/Metamaps/index.js | 2 +- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1fc18b87..5d290fcd 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, Hogan, Bloodhound */ import Mouse from './Mouse' import Selected from './Selected' @@ -54,7 +54,7 @@ const Create = { }, updateMetacodeSet: function (set, index, custom) { if (custom && Create.newSelectedMetacodes.length == 0) { - alert('Please select at least one metacode to use!') + window.alert('Please select at least one metacode to use!') return false } diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index b24b31c7..7af133de 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,3 +1,4 @@ +/* global Metamaps, $, Hogan, Bloodhound */ import Active from './Active' import Create from './Create' import Filter from './Filter' @@ -585,7 +586,6 @@ GlobalUI.Search = { if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { self.close(0, true); - var win; if (datum.rtype == "topic") { Router.topics(datum.id); } else if (datum.rtype == "map") { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index fa5c894f..ba8bdb8f 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,4 @@ -/* global Metamaps, $jit */ +/* global Metamaps, $, Image, CanvasLoader */ import _ from 'lodash' @@ -79,7 +79,6 @@ const JIT = { var jitReady = [] var synapsesToRemove = [] - var topic var mapping var node var nodes = {} @@ -183,8 +182,6 @@ const JIT = { if (!synapse) return // this means there are no corresponding synapses for // this edge, don't render it - var directionCat = synapse.get('category') - // label placement on edges if (canvas.denySelected) { var color = Settings.colors.synapses.normal @@ -282,8 +279,8 @@ const JIT = { if (synapseNum > 1) { var ctx = canvas.getCtx() - var x = (pos.x + posChild.x) / 2 - var y = (pos.y + posChild.y) / 2 + const x = (pos.x + posChild.x) / 2 + const y = (pos.y + posChild.y) / 2 drawSynapseCount(ctx, x, y, synapseNum) } } @@ -847,8 +844,8 @@ const JIT = { // set the draw synapse start positions var l = Selected.Nodes.length if (l > 0) { - for (var i = l - 1; i >= 0; i -= 1) { - var n = Selected.Nodes[i] + for (let i = l - 1; i >= 0; i -= 1) { + const n = Selected.Nodes[i] Mouse.synapseStartCoordinates.push({ x: n.pos.getc().x, y: n.pos.getc().y @@ -1541,8 +1538,6 @@ const JIT = { if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered - var authorized - e.preventDefault() e.stopPropagation() @@ -1827,10 +1822,11 @@ const JIT = { width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 + let nodes if (!denySelected && Selected.Nodes.length > 0) { - var nodes = Selected.Nodes + nodes = Selected.Nodes } else { - var nodes = _.values(Visualize.mGraph.graph.nodes) + nodes = _.values(Visualize.mGraph.graph.nodes) } if (nodes.length > 1) { diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ec5c1405..c36f70c3 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, Hogan, Bloodhound, Countable */ import Active from '../Active' import GlobalUI from '../GlobalUI' @@ -236,7 +236,7 @@ const InfoBox = { Metamaps.Collaborators.add(mapper) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) - var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') GlobalUI.notifyUser(name + ' will be notified by email') self.updateNumbers() } @@ -349,7 +349,7 @@ const InfoBox = { var confirmString = 'Are you sure you want to delete this map? ' confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' - var doIt = confirm(confirmString) + var doIt = window.confirm(confirmString) var map = Active.Map var mapper = Active.Mapper var authorized = map.authorizePermissionChange(mapper) diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 355e73f8..608523c8 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, SocketIoConnection, SimpleWebRTC */ import _ from 'lodash' @@ -1106,7 +1106,7 @@ const Realtime = { } }, newSynapse: function (data) { - var topic1, topic2, node1, node2, synapse, mapping, cancel + var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper var self = Realtime var socket = self.socket diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index a49aaa4d..473c6f1e 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,7 +1,13 @@ -/* global $ */ +/* global Metamaps, $, Howl */ + +/* + * Dependencies: + * Metamaps.Erb + */ import Backbone from 'backbone' import Autolinker from 'autolinker' +import _ from 'lodash' // TODO is this line good or bad // Backbone.$ = window.$ diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 21a3fb8d..db67409b 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,4 +1,4 @@ -/* global $ */ +/* global Metamaps */ import Account from './Account' import Active from './Active' From c9a79468f477273013ab4456d8f0a9ac8cb35449 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:40:02 +0800 Subject: [PATCH 086/378] switch to eslint-3 --- .codeclimate.yml | 1 + .eslintrc.js | 75 ++++++++++++++++-------------------------------- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index d3c19ad6..53f90d17 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,6 +12,7 @@ engines: - javascript eslint: enabled: true + channel: "eslint-3" fixme: enabled: true rubocop: diff --git a/.eslintrc.js b/.eslintrc.js index 26319254..55c4bec8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,13 +28,11 @@ module.exports = { "dot-location": [2, "property"], "eol-last": 2, "eqeqeq": [2, "allow-null"], - // errors on code climate - disable for now - //"func-call-spacing": [2, "never"], + "func-call-spacing": [2, "never"], "handle-callback-err": [2, "^(err|error)$" ], "indent": [2, 2, { "SwitchCase": 1 }], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - // errors on code climate - disable for now - //"keyword-spacing": [2, { "before": true, "after": true }], + "keyword-spacing": [2, { "before": true, "after": true }], "new-cap": [2, { "newIsCap": true, "capIsNew": false }], "new-parens": 2, "no-array-constructor": 2, @@ -42,8 +40,7 @@ module.exports = { "no-class-assign": 2, "no-cond-assign": 2, "no-const-assign": 2, - // errors on code climate - disable for now - //"no-constant-condition": [2, { "checkLoops": false }], + "no-constant-condition": [2, { "checkLoops": false }], "no-control-regex": 2, "no-debugger": 2, "no-delete-var": 2, @@ -51,8 +48,7 @@ module.exports = { "no-dupe-class-members": 2, "no-dupe-keys": 2, "no-duplicate-case": 2, - // errors on code climate - disable for now - //"no-duplicate-imports": 2, + "no-duplicate-imports": 2, "no-empty-character-class": 2, "no-empty-pattern": 2, "no-eval": 2, @@ -64,16 +60,14 @@ module.exports = { "no-fallthrough": 2, "no-floating-decimal": 2, "no-func-assign": 2, - // errors on code climate - disable for now - //"no-global-assign": 2, + "no-global-assign": 2, "no-implied-eval": 2, "no-inner-declarations": [2, "functions"], "no-invalid-regexp": 2, "no-irregular-whitespace": 2, "no-iterator": 2, "no-label-var": 2, - // errors on code climate - disable for now - //"no-labels": [2, { "allowLoop": false, "allowSwitch": false }], + "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], "no-lone-blocks": 2, "no-mixed-spaces-and-tabs": 2, "no-multi-spaces": 2, @@ -85,8 +79,7 @@ module.exports = { "no-new-func": 2, "no-new-object": 2, "no-new-require": 2, - // errors on code climate - disable for now - //"no-new-symbol": 2, + "no-new-symbol": 2, "no-new-wrappers": 2, "no-obj-calls": 2, "no-octal": 2, @@ -96,52 +89,38 @@ module.exports = { "no-redeclare": 2, "no-regex-spaces": 2, "no-return-assign": [2, "except-parens"], - // errors on code climate - disable for now - //"no-self-assign": 2, + "no-self-assign": 2, "no-self-compare": 2, "no-sequences": 2, "no-shadow-restricted-names": 2, "no-sparse-arrays": 2, - // errors on code climate - disable for now - //"no-tabs": 2, - // errors on code climate - disable for now - //"no-template-curly-in-string": 2, + "no-tabs": 2, + "no-template-curly-in-string": 2, "no-this-before-super": 2, "no-throw-literal": 2, "no-trailing-spaces": 2, "no-undef": 2, "no-undef-init": 2, "no-unexpected-multiline": 2, - // errors on code climate - disable for now - //"no-unmodified-loop-condition": 2, + "no-unmodified-loop-condition": 2, "no-unneeded-ternary": [2, { "defaultAssignment": false }], "no-unreachable": 2, - // errors on code climate - disable for now - //"no-unsafe-finally": 2, - // errors on code climate - disable for now - //"no-unsafe-negation": 2, + "no-unsafe-finally": 2, + "no-unsafe-negation": 2, "no-unused-vars": [2, { "vars": "all", "args": "none" }], "no-useless-call": 2, - // errors on code climate - disable for now - //"no-useless-computed-key": 2, - // errors on code climate - disable for now - //"no-useless-constructor": 2, - // errors on code climate - disable for now - //"no-useless-escape": 2, - // errors on code climate - disable for now - //"no-useless-rename": 2, - // errors on code climate - disable for now - //"no-whitespace-before-property": 2, + "no-useless-computed-key": 2, + "no-useless-constructor": 2, + "no-useless-escape": 2, + "no-useless-rename": 2, + "no-whitespace-before-property": 2, "no-with": 2, - // errors on code climate - disable for now - //"object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], + "object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], "one-var": [2, { "initialized": "never" }], "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], "padded-blocks": [2, "never"], - // errors on code climate - disable for now - //"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - // errors on code climate - disable for now - //"rest-spread-spacing": [2, "never"], + "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "rest-spread-spacing": [2, "never"], "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], "space-before-blocks": [2, "always"], @@ -149,17 +128,13 @@ module.exports = { "space-in-parens": [2, "never"], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], - // errors on code climate - disable for now - //"spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], - // errors on code climate - disable for now - //"template-curly-spacing": [2, "never"], - // errors on code climate - disable for now - //"unicode-bom": [2, "never"], + "spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], + "template-curly-spacing": [2, "never"], + "unicode-bom": [2, "never"], "use-isnan": 2, "valid-typeof": 2, "wrap-iife": [2, "any"], - // errors on code climate - disable for now - //"yield-star-spacing": [2, "both"], + "yield-star-spacing": [2, "both"], "yoda": [2, "never"], } } From 12cb675bb5be164f74f63fdf63e12c93596e14fd Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:44:23 +0800 Subject: [PATCH 087/378] switch to using the eslint-standard plugin again --- .eslintrc.js | 133 ++------------------------------------------------- 1 file changed, 4 insertions(+), 129 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 55c4bec8..11a46fd1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,140 +1,15 @@ module.exports = { "sourceType": "module", "parser": "babel-eslint", + "extends": "standard", "installedESLint": true, "env": { "es6": true, "node": true }, "plugins": [ + "promise", + "standard", "react" - ], - "globals": { - "document": false, - "navigator": false, - "window": false - }, - "rules": { - "accessor-pairs": 2, - "arrow-spacing": [2, { "before": true, "after": true }], - "block-spacing": [2, "always"], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "camelcase": [2, { "properties": "never" }], - "comma-dangle": [2, "never"], - "comma-spacing": [2, { "before": false, "after": true }], - "comma-style": [2, "last"], - "constructor-super": 2, - "curly": [2, "multi-line"], - "dot-location": [2, "property"], - "eol-last": 2, - "eqeqeq": [2, "allow-null"], - "func-call-spacing": [2, "never"], - "handle-callback-err": [2, "^(err|error)$" ], - "indent": [2, 2, { "SwitchCase": 1 }], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "keyword-spacing": [2, { "before": true, "after": true }], - "new-cap": [2, { "newIsCap": true, "capIsNew": false }], - "new-parens": 2, - "no-array-constructor": 2, - "no-caller": 2, - "no-class-assign": 2, - "no-cond-assign": 2, - "no-const-assign": 2, - "no-constant-condition": [2, { "checkLoops": false }], - "no-control-regex": 2, - "no-debugger": 2, - "no-delete-var": 2, - "no-dupe-args": 2, - "no-dupe-class-members": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-duplicate-imports": 2, - "no-empty-character-class": 2, - "no-empty-pattern": 2, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": [2, "functions"], - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-global-assign": 2, - "no-implied-eval": 2, - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], - "no-lone-blocks": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-multiple-empty-lines": [2, { "max": 1 }], - "no-native-reassign": 2, - "no-negated-in-lhs": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-require": 2, - "no-new-symbol": 2, - "no-new-wrappers": 2, - "no-obj-calls": 2, - "no-octal": 2, - "no-octal-escape": 2, - "no-path-concat": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-return-assign": [2, "except-parens"], - "no-self-assign": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow-restricted-names": 2, - "no-sparse-arrays": 2, - "no-tabs": 2, - "no-template-curly-in-string": 2, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-trailing-spaces": 2, - "no-undef": 2, - "no-undef-init": 2, - "no-unexpected-multiline": 2, - "no-unmodified-loop-condition": 2, - "no-unneeded-ternary": [2, { "defaultAssignment": false }], - "no-unreachable": 2, - "no-unsafe-finally": 2, - "no-unsafe-negation": 2, - "no-unused-vars": [2, { "vars": "all", "args": "none" }], - "no-useless-call": 2, - "no-useless-computed-key": 2, - "no-useless-constructor": 2, - "no-useless-escape": 2, - "no-useless-rename": 2, - "no-whitespace-before-property": 2, - "no-with": 2, - "object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], - "one-var": [2, { "initialized": "never" }], - "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], - "padded-blocks": [2, "never"], - "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - "rest-spread-spacing": [2, "never"], - "semi": [2, "never"], - "semi-spacing": [2, { "before": false, "after": true }], - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "always"], - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], - "template-curly-spacing": [2, "never"], - "unicode-bom": [2, "never"], - "use-isnan": 2, - "valid-typeof": 2, - "wrap-iife": [2, "any"], - "yield-star-spacing": [2, "both"], - "yoda": [2, "never"], - } + ] } From bc8ce0fee4e49abbce34a7eee515aebf855742f9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 14:04:31 +0800 Subject: [PATCH 088/378] topic view bug fix --- frontend/src/Metamaps/JIT.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index fa5c894f..0a75a036 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -151,7 +151,7 @@ const JIT = { // set up addTopic instructions in case they delete all the topics // i.e. if there are 0 topics at any time, it should have instructions again $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { + if (Active.Map && Active.Map.authorizeToEdit(Active.Mapper)) { $('#instructions div.addTopic').show() } From c60e103d972310a4a1d8dcb076afdbd3fdc6a3b4 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Mon, 26 Sep 2016 20:28:06 -0400 Subject: [PATCH 089/378] Update _switchmetacodes.html.erb --- app/views/shared/_switchmetacodes.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index 067d4d6b..bd6b8129 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -5,7 +5,7 @@ <% metacodes = current_user.settings.metacodes %> <% selectedSet = metacodes[0].include?("metacodeset") ? metacodes[0].sub("metacodeset-","") : "custom" %> -<% allMetacodeSets = MetacodeSet.order("name").all %> +<% allMetacodeSets = MetacodeSet.order("name").all.to_a %> <% if selectedSet == "custom" index = allMetacodeSets.length else From 8f0b350a2dbddbb306fcf08c6562557be9153cce Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Mon, 26 Sep 2016 20:39:33 -0400 Subject: [PATCH 090/378] Fix underscore bug (#674) * Update package.json * Update ChatView.js --- frontend/src/Metamaps/Views/ChatView.js | 7 ++++--- package.json | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 473c6f1e..9bd8b563 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -8,6 +8,7 @@ import Backbone from 'backbone' import Autolinker from 'autolinker' import _ from 'lodash' +import underscore from 'underscore' // TODO is this line good or bad // Backbone.$ = window.$ @@ -29,12 +30,12 @@ var Private = { "<div class='clearfloat'></div>" + "</div>", templates: function() { - _.templateSettings = { + underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g }; - this.messageTemplate = _.template(Private.messageHTML); + this.messageTemplate = underscore.template(Private.messageHTML); - this.participantTemplate = _.template(Private.participantHTML); + this.participantTemplate = underscore.template(Private.participantHTML); }, createElements: function() { this.$unread = $('<div class="chat-unread"></div>'); diff --git a/package.json b/package.json index c6c74f44..106294fd 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", + "underscore": "1.4.4", "csv-parse": "1.1.7", "lodash": "4.16.1", "node-uuid": "1.4.7", From a86101dda0dd61f16fa7a0e41d682a76dd148d59 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 27 Sep 2016 21:10:14 +0800 Subject: [PATCH 091/378] remove excel export --- app/controllers/maps_controller.rb | 4 ---- app/services/map_export_service.rb | 4 ---- app/views/maps/export.xls.erb | 9 --------- config/initializers/mime_types.rb | 2 -- 4 files changed, 19 deletions(-) delete mode 100644 app/views/maps/export.xls.erb diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index a74a35c2..0a0b11d5 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -4,8 +4,6 @@ class MapsController < ApplicationController before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized - respond_to :html, :json, :csv - autocomplete :map, :name, full: true, extra_data: [:user_id] # GET maps/:id @@ -24,7 +22,6 @@ class MapsController < ApplicationController end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } - format.xls { redirect_to action: :export, format: :xls } end end @@ -118,7 +115,6 @@ class MapsController < ApplicationController respond_to do |format| format.json { render json: exporter.json } format.csv { send_data exporter.csv } - format.xls { @spreadsheet = exporter.xls } end end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 2ded756c..4e1d216e 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -22,10 +22,6 @@ class MapExportService end end - def xls - to_spreadsheet - end - private def topic_headings diff --git a/app/views/maps/export.xls.erb b/app/views/maps/export.xls.erb deleted file mode 100644 index 7030d501..00000000 --- a/app/views/maps/export.xls.erb +++ /dev/null @@ -1,9 +0,0 @@ -<table> - <% @spreadsheet.each do |line| %> - <tr> - <% line.each do |field| %> - <td><%= field %></td> - <% end %> - </tr> - <% end %> -</table> diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 5e8d015a..6e1d16f0 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -3,5 +3,3 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf - -Mime::Type.register 'application/xls', :xls From 743c9b3af91db83353c8fd3011de6ea9ab8e3a41 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 28 Sep 2016 10:32:28 +0800 Subject: [PATCH 092/378] node{1,2}_id => topic{1,2}_id migration and code changes --- app/controllers/main_controller.rb | 4 ++-- app/controllers/synapses_controller.rb | 2 +- app/controllers/topics_controller.rb | 2 +- app/models/permitted_params.rb | 2 +- app/models/synapse.rb | 10 ++++---- app/models/topic.rb | 24 +++++++++---------- .../api/v2/application_serializer.rb | 9 ++++--- app/serializers/api/v2/synapse_serializer.rb | 4 ++-- app/services/map_export_service.rb | 4 ++-- ...ename_node1_id_to_topic1_id_in_synapses.rb | 6 +++++ db/schema.rb | 14 +++++------ frontend/src/Metamaps/Backbone/index.js | 8 +++---- frontend/src/Metamaps/Import.js | 4 ++-- frontend/src/Metamaps/JIT.js | 2 +- frontend/src/Metamaps/Map/index.js | 4 ++-- frontend/src/Metamaps/Synapse.js | 4 ++-- frontend/src/Metamaps/SynapseCard.js | 6 ++--- 17 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 4624c7a6..0a5ccb68 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -156,8 +156,8 @@ class MainController < ApplicationController @synapses = @synapses.uniq(&:desc) elsif topic1id && !topic1id.empty? - @one = policy_scope(Synapse).where('node1_id = ? AND node2_id = ?', topic1id, topic2id) - @two = policy_scope(Synapse).where('node2_id = ? AND node1_id = ?', topic1id, topic2id) + @one = policy_scope(Synapse).where('topic1_id = ? AND topic2_id = ?', topic1id, topic2id) + @two = policy_scope(Synapse).where('topic2_id = ? AND topic1_id = ?', topic1id, topic2id) @synapses = @one + @two @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a else diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 8fc31688..e0b8f727 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -63,6 +63,6 @@ class SynapsesController < ApplicationController private def synapse_params - params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :node1_id, :node2_id, :user_id) + params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id) end end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 1b966ca2..f909626a 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -96,7 +96,7 @@ class TopicsController < ApplicationController # find synapses between topics in alltopics array allsynapses = policy_scope(Synapse.for_topic(@topic.id)).to_a - synapse_ids = (allsynapses.map(&:node1_id) + allsynapses.map(&:node2_id)).uniq + synapse_ids = (allsynapses.map(&:topic1_id) + allsynapses.map(&:topic2_id)).uniq allsynapses.delete_if do |synapse| !synapse_ids.index(synapse.id).nil? end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index d0696985..207854ac 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -19,7 +19,7 @@ class PermittedParams < Struct.new(:params) end def synapse_attributes - [:desc, :category, :weight, :permission, :node1_id, :node2_id] + [:desc, :category, :weight, :permission, :topic1_id, :topic2_id] end def topic_attributes diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 798f6a54..37c9c72d 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -3,8 +3,8 @@ class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' - belongs_to :topic1, class_name: 'Topic', foreign_key: 'node1_id' - belongs_to :topic2, class_name: 'Topic', foreign_key: 'node2_id' + belongs_to :topic1, class_name: 'Topic', foreign_key: 'topic1_id' + belongs_to :topic2, class_name: 'Topic', foreign_key: 'topic2_id' has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, through: :mappings @@ -12,14 +12,14 @@ class Synapse < ApplicationRecord validates :desc, length: { minimum: 0, allow_nil: false } validates :permission, presence: true - validates :node1_id, presence: true - validates :node2_id, presence: true + validates :topic1_id, presence: true + validates :topic2_id, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } scope :for_topic, ->(topic_id = nil) { - where('node1_id = ? OR node2_id = ?', topic_id, topic_id) + where(topic1_id: topic_id).or(where(topic2_id: topic_id)) } delegate :name, to: :user, prefix: true diff --git a/app/models/topic.rb b/app/models/topic.rb index fb635da3..62f81cec 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -5,8 +5,8 @@ class Topic < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' - has_many :synapses1, class_name: 'Synapse', foreign_key: 'node1_id', dependent: :destroy - has_many :synapses2, class_name: 'Synapse', foreign_key: 'node2_id', dependent: :destroy + has_many :synapses1, class_name: 'Synapse', foreign_key: 'topic1_id', dependent: :destroy + has_many :synapses2, class_name: 'Synapse', foreign_key: 'topic2_id', dependent: :destroy has_many :topics1, through: :synapses2, source: :topic1 has_many :topics2, through: :synapses1, source: :topic2 @@ -46,8 +46,8 @@ class Topic < ApplicationRecord scope :relatives, ->(topic_id = nil, user = nil) { # should only see topics through *visible* synapses # e.g. Topic A (commons) -> synapse (private) -> Topic B (commons) must be filtered out - synapses = Pundit.policy_scope(user, Synapse.where(node1_id: topic_id)).pluck(:node2_id) - synapses += Pundit.policy_scope(user, Synapse.where(node2_id: topic_id)).pluck(:node1_id) + synapses = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) + synapses += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) where(id: synapses.uniq) } @@ -94,18 +94,18 @@ class Topic < ApplicationRecord output = [] synapses.each do |synapse| if synapse.category == 'from-to' - if synapse.node1_id == id - output << synapse.node1_id.to_s + '->' + synapse.node2_id.to_s - elsif synapse.node2_id == id - output << synapse.node2_id.to_s + '<-' + synapse.node1_id.to_s + if synapse.topic1_id == id + output << synapse.topic1_id.to_s + '->' + synapse.topic2_id.to_s + elsif synapse.topic2_id == id + output << synapse.topic2_id.to_s + '<-' + synapse.topic1_id.to_s else raise 'invalid synapse on topic in synapse_csv' end elsif synapse.category == 'both' - if synapse.node1_id == id - output << synapse.node1_id.to_s + '<->' + synapse.node2_id.to_s - elsif synapse.node2_id == id - output << synapse.node2_id.to_s + '<->' + synapse.node1_id.to_s + if synapse.topic1_id == id + output << synapse.topic1_id.to_s + '<->' + synapse.topic2_id.to_s + elsif synapse.topic2_id == id + output << synapse.topic2_id.to_s + '<->' + synapse.topic1_id.to_s else raise 'invalid synapse on topic in synapse_csv' end diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index a5da830a..2d7c1b9a 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -14,8 +14,7 @@ module Api end # self.embeddable might look like this: - # topic1: { attr: :node1, serializer: TopicSerializer } - # topic2: { attr: :node2, serializer: TopicSerializer } + # creator: { attr: :first_creator, serializer: UserSerializer } # contributors: { serializer: UserSerializer} # This method will remove the :attr key if the underlying attribute name # is different than the name provided in the final json output. All other keys @@ -24,9 +23,9 @@ module Api # # This setup means if you passed this self.embeddable config and sent no # ?embed= query param with your API request, you would get the regular attributes - # plus topic1_id, topic2_id, and contributor_ids. If you pass - # ?embed=topic1,topic2,contributors, then instead of two ids and an array of ids, - # you would get two serialized topics and an array of serialized users + # plus creator_id and contributor_ids. If you passed ?embed=creator,contributors + # then instead of an id and an array of ids, you would get a serialized user + # (the first_creator) and an array of serialized users (the contributors). def self.embed_dat embeddable.each_pair do |key, opts| attr = opts.delete(:attr) || key diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb index f647022c..3f95af35 100644 --- a/app/serializers/api/v2/synapse_serializer.rb +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -11,8 +11,8 @@ module Api def self.embeddable { - topic1: { attr: :node1, serializer: TopicSerializer }, - topic2: { attr: :node2, serializer: TopicSerializer }, + topic1: { serializer: TopicSerializer }, + topic2: { serializer: TopicSerializer }, user: {} } end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 2ded756c..6a89a15d 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -62,8 +62,8 @@ class MapExportService visible_synapses.map do |synapse| next nil if synapse.nil? OpenStruct.new( - topic1: synapse.node1_id, - topic2: synapse.node2_id, + topic1: synapse.topic1_id, + topic2: synapse.topic2_id, category: synapse.category, description: synapse.desc, user: synapse.user.name, diff --git a/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb b/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb new file mode 100644 index 00000000..fc5b4a1b --- /dev/null +++ b/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb @@ -0,0 +1,6 @@ +class RenameNode1IdToTopic1IdInSynapses < ActiveRecord::Migration[5.0] + def change + rename_column :synapses, :node1_id, :topic1_id + rename_column :synapses, :node2_id, :topic2_id + end +end diff --git a/db/schema.rb b/db/schema.rb index ea06b679..0bfa7f1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160820231717) do +ActiveRecord::Schema.define(version: 20160928022635) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -175,16 +175,16 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.text "category" t.text "weight" t.text "permission" - t.integer "node1_id" - t.integer "node2_id" + t.integer "topic1_id" + t.integer "topic2_id" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "defer_to_map_id" - t.index ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree - t.index ["node1_id"], name: "index_synapses_on_node1_id", using: :btree - t.index ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree - t.index ["node2_id"], name: "index_synapses_on_node2_id", using: :btree + t.index ["topic1_id", "topic1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree + t.index ["topic1_id"], name: "index_synapses_on_topic1_id", using: :btree + t.index ["topic2_id", "topic2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree + t.index ["topic2_id"], name: "index_synapses_on_topic2_id", using: :btree t.index ["user_id"], name: "index_synapses_on_user_id", using: :btree end diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index b1ba9e78..1994c483 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -531,10 +531,10 @@ _Backbone.init = function () { else return false }, getTopic1: function () { - return Metamaps.Topics.get(this.get('node1_id')) + return Metamaps.Topics.get(this.get('topic1_id')) }, getTopic2: function () { - return Metamaps.Topics.get(this.get('node2_id')) + return Metamaps.Topics.get(this.get('topic2_id')) }, getDirection: function () { var t1 = this.getTopic1(), @@ -559,8 +559,8 @@ _Backbone.init = function () { var synapseID = this.isNew() ? this.cid : this.id var edge = { - nodeFrom: this.get('node1_id'), - nodeTo: this.get('node2_id'), + nodeFrom: this.get('topic1_id'), + nodeTo: this.get('topic2_id'), data: { $synapses: [], $synapseIDs: [synapseID], diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 52a8f21a..193cdf5e 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -333,8 +333,8 @@ const Import = { desc: desc || "", category: category, permission: permission, - node1_id: topic1.id, - node2_id: topic2.id + topic1_id: topic1.id, + topic2_id: topic2.id }) Metamaps.Synapses.add(synapse) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 63723dbd..4a665bdc 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -93,7 +93,7 @@ const JIT = { synapses.each(function (s) { edge = s.createEdge() - if (topics.get(s.get('node1_id')) === undefined || topics.get(s.get('node2_id')) === undefined) { + if (topics.get(s.get('topic1_id')) === undefined || topics.get(s.get('topic2_id')) === undefined) { // this means it's an invalid synapse synapsesToRemove.push(s) } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 944a387b..625a549e 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -200,8 +200,8 @@ const Map = { var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 // make sure that both topics are being added, otherwise, it // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 + var topicsNotFiltered = nodes_array.indexOf(synapse.get('topic1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('topic2_id')) > -1 if (descNotFiltered && topicsNotFiltered) { synapses_array.push(synapse.id) } diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index b50e50e6..400cb0b0 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -128,8 +128,8 @@ const Synapse = { topic1 = node1.getData('topic') synapse = new Metamaps.Backbone.Synapse({ desc: Create.newSynapse.description, - node1_id: topic1.isNew() ? topic1.cid : topic1.id, - node2_id: topic2.isNew() ? topic2.cid : topic2.id, + topic1_id: topic1.isNew() ? topic1.cid : topic1.id, + topic2_id: topic2.isNew() ? topic2.cid : topic2.id, }) Metamaps.Synapses.add(synapse) diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 28ff1e32..8203657d 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -238,7 +238,7 @@ const SynapseCard = { var directionCat = synapse.get('category'); // both, none, from-to if (directionCat == 'from-to') { - var from_to = [synapse.get('node1_id'), synapse.get('node2_id')] + var from_to = [synapse.get('topic1_id'), synapse.get('topic2_id')] if (from_to[0] == left.id) { // check left checkbox $('#edit_synapse_left').addClass('checked') @@ -273,8 +273,8 @@ const SynapseCard = { synapse.save({ category: dirCat, - node1_id: dir[0], - node2_id: dir[1] + topic1_id: dir[0], + topic2_id: dir[1] }) Visualize.mGraph.plot() }) From 40b7e95b686c985bae009d14b1a6891971c39ed9 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 04:35:41 -0400 Subject: [PATCH 093/378] Update index.js Prevents the default chrome context menu from appearing overtop the Metamaps context menu --- frontend/src/Metamaps/Map/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 944a387b..29cb940b 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -39,7 +39,7 @@ const Map = { var self = Map // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#center-container').bind('contextmenu', function (e) { + $('#wrapper').on('contextmenu', function (e) { return false }) From bb87c9c2db48a7baff5e24dec4ffd21bd1d1a07a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:16:18 +0800 Subject: [PATCH 094/378] simplify explore controller a bit --- app/controllers/explore_controller.rb | 57 ++++++++------------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 6f24eba5..187e4ba0 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -1,19 +1,16 @@ # frozen_string_literal: true class ExploreController < ApplicationController + before_action :require_authentication, only: [:mine, :shared, :starred] before_action :authorize_explore after_action :verify_authorized after_action :verify_policy_scoped respond_to :html, :json, :csv - # TODO: remove? - # autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET /explore/active def active - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope(Map).order('updated_at DESC') - .page(page).per(20) + @maps = policy_scope(Map).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html do @@ -27,11 +24,8 @@ class ExploreController < ApplicationController # GET /explore/featured def featured - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.featured = ? AND maps.permission != ?', - true, 'private') - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(featured: true).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -41,15 +35,8 @@ class ExploreController < ApplicationController # GET /explore/mine def mine - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.user_id = ?', current_user.id) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(user_id: current_user.id) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -59,15 +46,8 @@ class ExploreController < ApplicationController # GET /explore/shared def shared - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(id: current_user.shared_maps.map(&:id)) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -77,16 +57,9 @@ class ExploreController < ApplicationController # GET /explore/starred def starred - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 stars = current_user.stars.map(&:map_id) - @maps = policy_scope( - Map.where('maps.id IN (?)', stars) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(id: stars).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -96,10 +69,9 @@ class ExploreController < ApplicationController # GET /explore/mapper/:id def mapper - page = params[:page].present? ? params[:page] : 1 @user = User.find(params[:id]) @maps = policy_scope(Map.where(user: @user)) - .order('updated_at DESC').page(page).per(20) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -112,4 +84,9 @@ class ExploreController < ApplicationController def authorize_explore authorize :Explore end + + def require_authentication + # skip_policy_scope + redirect_to explore_active_path unless authenticated? + end end From f75ad41a82c7d1679b2a68c4d5260fbf3d0f61da Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:28:23 +0800 Subject: [PATCH 095/378] factor out map_scope function --- app/controllers/explore_controller.rb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 187e4ba0..59045d5d 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,8 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = policy_scope(Map).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map) respond_to do |format| format.html do @@ -24,8 +23,7 @@ class ExploreController < ApplicationController # GET /explore/featured def featured - @maps = policy_scope(Map).where(featured: true).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map.where(featured: true)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -35,8 +33,7 @@ class ExploreController < ApplicationController # GET /explore/mine def mine - @maps = policy_scope(Map).where(user_id: current_user.id) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(user_id: current_user.id)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -46,8 +43,7 @@ class ExploreController < ApplicationController # GET /explore/shared def shared - @maps = policy_scope(Map).where(id: current_user.shared_maps.map(&:id)) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(id: current_user.shared_maps.map(&:id))) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -57,9 +53,7 @@ class ExploreController < ApplicationController # GET /explore/starred def starred - stars = current_user.stars.map(&:map_id) - @maps = policy_scope(Map).where(id: stars).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map.where(id: current_user.stars.map(&:map_id))) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -70,8 +64,7 @@ class ExploreController < ApplicationController # GET /explore/mapper/:id def mapper @user = User.find(params[:id]) - @maps = policy_scope(Map.where(user: @user)) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(user: @user)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -81,6 +74,10 @@ class ExploreController < ApplicationController private + def map_scope(scope) + policy_scope(scope).order(updated_at: :desc).page(params[:page]).per(20) + end + def authorize_explore authorize :Explore end From 3ee8d41298cafc0ffbc3b8370ca5321a39aac6e7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:36:08 +0800 Subject: [PATCH 096/378] maps controller code climate --- app/controllers/maps_controller.rb | 45 +++++++++++++----------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 0a0b11d5..351b1c0d 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events, :screenshot, :star, :unstar] - before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] + before_action :require_user, only: [:create, :update, :destroy, :access, :events, + :screenshot, :star, :unstar] + before_action :set_map, only: [:show, :update, :destroy, :access, :contains, + :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized autocomplete :map, :name, full: true, extra_data: [:user_id] @@ -18,7 +20,8 @@ class MapsController < ApplicationController @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, + @alltopics, @allmessages, @allstars, @map) end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -41,10 +44,10 @@ class MapsController < ApplicationController # POST maps def create - @user = current_user @map = Map.new(create_map_params) - @map.user = @user + @map.user = current_user @map.arranged = false + authorize @map if params[:topicsToMap].present? create_topics! @@ -52,8 +55,6 @@ class MapsController < ApplicationController @map.arranged = true end - authorize @map - respond_to do |format| if @map.save format.json { render json: @map } @@ -89,8 +90,9 @@ class MapsController < ApplicationController def access user_ids = params[:access] || [] - added = @map.add_new_collaborators(user_ids) - added.each do |user_id| + @map.add_new_collaborators(user_ids).each do |user_id| + # add_new_collaborators returns array of added users, + # who we then send an email to MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later end @map.remove_old_collaborators(user_ids) @@ -150,7 +152,7 @@ class MapsController < ApplicationController # POST maps/:id/star def star - star = Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) + Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) respond_to do |format| format.json do @@ -187,29 +189,20 @@ class MapsController < ApplicationController end def create_topics! - topics = params[:topicsToMap] - topics = topics.split(',') - topics.each do |topic| + params[:topicsToMap].split(',').each do |topic| topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] + mapping = Mapping.new(map: @map, user: current_user, + mappable: Topic.find(topic[0]), + xloc: topic[1], yloc: topic[2]) authorize mapping, :create? mapping.save end end def create_synapses! - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) + params[:synapsesToMap].split(',').each do |synapse_id| + mapping = Mapping.new(map: @map, user: current_user, + mappable: Synapse.find(synapse_id)) authorize mapping, :create? mapping.save end From 50656554369a38f55002fe8c96d8462fb47f2089 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:50:52 +0800 Subject: [PATCH 097/378] factor stars into their own controller --- app/controllers/maps_controller.rb | 27 ++------------------- app/controllers/stars_controller.rb | 37 +++++++++++++++++++++++++++++ config/routes.rb | 19 ++++++++------- 3 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 app/controllers/stars_controller.rb diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 351b1c0d..b24f85fc 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :destroy, :access, :events, - :screenshot, :star, :unstar] + :screenshot] before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export, :screenshot, :star, :unstar] + :events, :export, :screenshot] after_action :verify_authorized autocomplete :map, :name, full: true, extra_data: [:user_id] @@ -150,29 +150,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/star - def star - Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) - - respond_to do |format| - format.json do - render json: { message: 'Successfully starred map' } - end - end - end - - # POST maps/:id/unstar - def unstar - star = Star.find_by(map_id: @map.id, user_id: current_user.id) - star&.delete - - respond_to do |format| - format.json do - render json: { message: 'Successfully unstarred map' } - end - end - end - private def set_map diff --git a/app/controllers/stars_controller.rb b/app/controllers/stars_controller.rb new file mode 100644 index 00000000..ec4d1347 --- /dev/null +++ b/app/controllers/stars_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +class StarsController < ApplicationController + before_action :require_user + before_action :set_map + after_action :verify_authorized + + # POST maps/:id/star + def create + authorize @map, :star? + Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) + + respond_to do |format| + format.json do + render json: { message: 'Successfully starred map' } + end + end + end + + # POST maps/:id/unstar + def destroy + authorize @map, :unstar? + star = Star.find_by(map_id: @map.id, user_id: current_user.id) + star&.delete + + respond_to do |format| + format.json do + render json: { message: 'Successfully unstarred map' } + end + end + end + + private + + def set_map + @map = Map.find(params[:id]) + end +end diff --git a/config/routes.rb b/config/routes.rb index 3ad59cb2..b114d3d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,14 +48,17 @@ Metamaps::Application.routes.draw do get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - resources :maps, except: [:index, :edit] - get 'maps/:id/export', to: 'maps#export' - post 'maps/:id/events/:event', to: 'maps#events' - get 'maps/:id/contains', to: 'maps#contains', as: :contains - post 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot - post 'maps/:id/access', to: 'maps#access', as: :access, defaults: { format: :json } - post 'maps/:id/star', to: 'maps#star', defaults: { format: :json } - post 'maps/:id/unstar', to: 'maps#unstar', defaults: { format: :json } + resources :maps, except: [:index, :edit] do + member do + get :export + post 'events/:event', action: :events + get :contains + post :upload_screenshot, action: :screenshot + post :access, default: { format: :json } + post :star, to: 'stars#create', defaults: { format: :json } + post :unstar, to: 'stars#destroy', defaults: { format: :json } + end + end namespace :explore do get 'active' From 5b9eedc8300740c6dbad18e58f0043e949b25d79 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:00:30 +0800 Subject: [PATCH 098/378] pull search routes into their own controller --- app/controllers/main_controller.rb | 162 +-------------------------- app/controllers/search_controller.rb | 162 +++++++++++++++++++++++++++ app/policies/main_policy.rb | 21 ---- app/policies/search_policy.rb | 18 +++ config/routes.rb | 10 +- 5 files changed, 191 insertions(+), 182 deletions(-) create mode 100644 app/controllers/search_controller.rb create mode 100644 app/policies/search_policy.rb diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0a5ccb68..f89b6b78 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,172 +1,20 @@ # frozen_string_literal: true class MainController < ApplicationController - include TopicsHelper - include MapsHelper - include UsersHelper - include SynapsesHelper + after_action :verify_authorized + after_action :verify_policy_scoped, only: [:home] - after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers] - - respond_to :html, :json - - # home page + # GET / def home - @maps = policy_scope(Map).order('updated_at DESC').page(1).per(20) + authorize :Main respond_to do |format| format.html do if !authenticated? render 'main/home' else + @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) render 'explore/active' end end end end - - ### SEARCHING ### - - # get /search/topics?term=SOMETERM - def searchtopics - term = params[:term] - user = params[:user] ? params[:user] : false - - if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero? - - # remove "topic:" if appended at beginning - term = term[6..-1] if term.downcase[0..5] == 'topic:' - - # if desc: search desc instead - desc = false - if term.downcase[0..4] == 'desc:' - term = term[5..-1] - desc = true - end - - # if link: search link instead - link = false - if term.downcase[0..4] == 'link:' - term = term[5..-1] - link = true - end - - # check whether there's a filter by metacode as part of the query - filterByMetacode = false - Metacode.all.each do |m| - lOne = m.name.length + 1 - lTwo = m.name.length - - if term.downcase[0..lTwo] == m.name.downcase + ':' - term = term[lOne..-1] - filterByMetacode = m - end - end - - search = '%' + term.downcase + '%' - builder = policy_scope(Topic) - - if filterByMetacode - if term == '' - builder = builder.none - else - builder = builder.where('LOWER("name") like ? OR - LOWER("desc") like ? OR - LOWER("link") like ?', search, search, search) - builder = builder.where(metacode_id: filterByMetacode.id) - end - elsif desc - builder = builder.where('LOWER("desc") like ?', search) - elsif link - builder = builder.where('LOWER("link") like ?', search) - else # regular case, just search the name - builder = builder.where('LOWER("name") like ? OR - LOWER("desc") like ? OR - LOWER("link") like ?', search, search, search) - end - - builder = builder.where(user: user) if user - @topics = builder.order(:name) - else - @topics = [] - end - - render json: autocomplete_array_json(@topics) - end - - # get /search/maps?term=SOMETERM - def searchmaps - term = params[:term] - user = params[:user] ? params[:user] : nil - - if term && !term.empty? && term.downcase[0..5] != 'topic:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero? - - # remove "map:" if appended at beginning - term = term[4..-1] if term.downcase[0..3] == 'map:' - - # if desc: search desc instead - desc = false - if term.downcase[0..4] == 'desc:' - term = term[5..-1] - desc = true - end - - search = '%' + term.downcase + '%' - builder = policy_scope(Map) - - builder = if desc - builder.where('LOWER("desc") like ?', search) - else - builder.where('LOWER("name") like ?', search) - end - builder = builder.where(user: user) if user - @maps = builder.order(:name) - else - @maps = [] - end - - render json: autocomplete_map_array_json(@maps) - end - - # get /search/mappers?term=SOMETERM - def searchmappers - term = params[:term] - if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero? - - # remove "mapper:" if appended at beginning - term = term[7..-1] if term.downcase[0..6] == 'mapper:' - search = term.downcase + '%' - - skip_policy_scope # TODO: builder = policy_scope(User) - builder = User.where('LOWER("name") like ?', search) - @mappers = builder.order(:name) - else - @mappers = [] - end - render json: autocomplete_user_array_json(@mappers) - end - - # get /search/synapses?term=SOMETERM OR - # get /search/synapses?topic1id=SOMEID&topic2id=SOMEID - def searchsynapses - term = params[:term] - topic1id = params[:topic1id] - topic2id = params[:topic2id] - - if term && !term.empty? - @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') - - @synapses = @synapses.uniq(&:desc) - elsif topic1id && !topic1id.empty? - @one = policy_scope(Synapse).where('topic1_id = ? AND topic2_id = ?', topic1id, topic2id) - @two = policy_scope(Synapse).where('topic2_id = ? AND topic1_id = ?', topic1id, topic2id) - @synapses = @one + @two - @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a - else - @synapses = [] - end - - # limit to 5 results - @synapses = @synapses.to_a.slice(0, 5) - - render json: autocomplete_synapse_array_json(@synapses) - end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 00000000..91b0b44d --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true +class MainController < ApplicationController + include TopicsHelper + include MapsHelper + include UsersHelper + include SynapsesHelper + + before_action :authorize_search + after_action :verify_authorized + after_action :verify_policy_scoped, only: [:maps, :mappers, :synapses, :topics] + + # get /search/topics?term=SOMETERM + def topics + term = params[:term] + user = params[:user] ? params[:user] : false + + if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero? + + # remove "topic:" if appended at beginning + term = term[6..-1] if term.downcase[0..5] == 'topic:' + + # if desc: search desc instead + desc = false + if term.downcase[0..4] == 'desc:' + term = term[5..-1] + desc = true + end + + # if link: search link instead + link = false + if term.downcase[0..4] == 'link:' + term = term[5..-1] + link = true + end + + # check whether there's a filter by metacode as part of the query + filterByMetacode = false + Metacode.all.each do |m| + lOne = m.name.length + 1 + lTwo = m.name.length + + if term.downcase[0..lTwo] == m.name.downcase + ':' + term = term[lOne..-1] + filterByMetacode = m + end + end + + search = '%' + term.downcase + '%' + builder = policy_scope(Topic) + + if filterByMetacode + if term == '' + builder = builder.none + else + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + builder = builder.where(metacode_id: filterByMetacode.id) + end + elsif desc + builder = builder.where('LOWER("desc") like ?', search) + elsif link + builder = builder.where('LOWER("link") like ?', search) + else # regular case, just search the name + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + end + + builder = builder.where(user: user) if user + @topics = builder.order(:name) + else + @topics = [] + end + + render json: autocomplete_array_json(@topics) + end + + # get /search/maps?term=SOMETERM + def maps + term = params[:term] + user = params[:user] ? params[:user] : nil + + if term && !term.empty? && term.downcase[0..5] != 'topic:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero? + + # remove "map:" if appended at beginning + term = term[4..-1] if term.downcase[0..3] == 'map:' + + # if desc: search desc instead + desc = false + if term.downcase[0..4] == 'desc:' + term = term[5..-1] + desc = true + end + + search = '%' + term.downcase + '%' + builder = policy_scope(Map) + + builder = if desc + builder.where('LOWER("desc") like ?', search) + else + builder.where('LOWER("name") like ?', search) + end + builder = builder.where(user: user) if user + @maps = builder.order(:name) + else + @maps = [] + end + + render json: autocomplete_map_array_json(@maps) + end + + # get /search/mappers?term=SOMETERM + def mappers + term = params[:term] + if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero? + + # remove "mapper:" if appended at beginning + term = term[7..-1] if term.downcase[0..6] == 'mapper:' + search = term.downcase + '%' + + skip_policy_scope # TODO: builder = policy_scope(User) + builder = User.where('LOWER("name") like ?', search) + @mappers = builder.order(:name) + else + @mappers = [] + end + render json: autocomplete_user_array_json(@mappers) + end + + # get /search/synapses?term=SOMETERM OR + # get /search/synapses?topic1id=SOMEID&topic2id=SOMEID + def synapses + term = params[:term] + topic1id = params[:topic1id] + topic2id = params[:topic2id] + + if term && !term.empty? + @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + + @synapses = @synapses.uniq(&:desc) + elsif topic1id && !topic1id.empty? + @one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id) + @two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id) + @synapses = @one + @two + @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a + else + @synapses = [] + end + + # limit to 5 results + @synapses = @synapses.to_a.slice(0, 5) + + render json: autocomplete_synapse_array_json(@synapses) + end + + private + + def authorize_search + authorize :Search + end +end diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index e0ffc30b..1c7a00e5 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -1,27 +1,6 @@ # frozen_string_literal: true class MainPolicy < ApplicationPolicy - def initialize(user, _record) - @user = user - @record = nil - end - def home? true end - - def searchtopics? - true - end - - def searchmaps? - true - end - - def searchmappers? - true - end - - def searchsynapses? - true - end end diff --git a/app/policies/search_policy.rb b/app/policies/search_policy.rb new file mode 100644 index 00000000..5b783457 --- /dev/null +++ b/app/policies/search_policy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +class SearchPolicy < ApplicationPolicy + def topics? + true + end + + def maps? + true + end + + def mappers? + true + end + + def synapses? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index b114d3d6..4800780d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,10 +5,12 @@ Metamaps::Application.routes.draw do get 'request', to: 'main#requestinvite', as: :request - get 'search/topics', to: 'main#searchtopics', as: :searchtopics - get 'search/maps', to: 'main#searchmaps', as: :searchmaps - get 'search/mappers', to: 'main#searchmappers', as: :searchmappers - get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses + namespace :search do + get :topics + get :maps + get :mappers + get :synapses + end namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do From c1acaba941b15950165a120cb615653951c7ce3b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:02:45 +0800 Subject: [PATCH 099/378] re-order config/routes.rb --- config/routes.rb | 83 +++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 4800780d..bdee276b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,49 @@ Metamaps::Application.routes.draw do get :synapses end + namespace :explore do + get 'active' + get 'featured' + get 'mine' + get 'shared' + get 'starred' + get 'mapper/:id', action: 'mapper' + end + + resources :maps, except: [:index, :edit] do + member do + get :export + post 'events/:event', action: :events + get :contains + post :upload_screenshot, action: :screenshot + post :access, default: { format: :json } + post :star, to: 'stars#create', defaults: { format: :json } + post :unstar, to: 'stars#destroy', defaults: { format: :json } + end + end + + resources :mappings, except: [:index, :new, :edit] + + resources :messages, only: [:show, :create, :update, :destroy] + + resources :metacode_sets, except: [:show] + + resources :metacodes, except: [:destroy] + get 'metacodes/:name', to: 'metacodes#show' + + resources :synapses, except: [:index, :new, :edit] + + resources :topics, except: [:index, :new, :edit] do + get :autocomplete_topic, on: :collection + end + get 'topics/:id/network', to: 'topics#network', as: :network + get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers + get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives + + resources :users, except: [:index, :destroy] + get 'users/:id/details', to: 'users#details', as: :details + post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes + namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do resources :maps, only: [:index, :create, :show, :update, :destroy] @@ -35,42 +78,6 @@ Metamaps::Application.routes.draw do end end - resources :messages, only: [:show, :create, :update, :destroy] - resources :mappings, except: [:index, :new, :edit] - resources :metacode_sets, except: [:show] - - resources :metacodes, except: [:destroy] - get 'metacodes/:name', to: 'metacodes#show' - - resources :synapses, except: [:index, :new, :edit] - resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic, on: :collection - end - get 'topics/:id/network', to: 'topics#network', as: :network - get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers - get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - - resources :maps, except: [:index, :edit] do - member do - get :export - post 'events/:event', action: :events - get :contains - post :upload_screenshot, action: :screenshot - post :access, default: { format: :json } - post :star, to: 'stars#create', defaults: { format: :json } - post :unstar, to: 'stars#destroy', defaults: { format: :json } - end - end - - namespace :explore do - get 'active' - get 'featured' - get 'mine' - get 'shared' - get 'starred' - get 'mapper/:id', action: 'mapper' - end - devise_for :users, skip: :sessions, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', @@ -84,10 +91,6 @@ Metamaps::Application.routes.draw do get 'join' => 'devise/registrations#new', :as => :new_user_registration_path end - get 'users/:id/details', to: 'users#details', as: :details - post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes - resources :users, except: [:index, :destroy] - namespace :hacks do get 'load_url_title' end From 9699b4115943ba16ce648a7f8e24541144597ad4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:04:58 +0800 Subject: [PATCH 100/378] make requestinvite controller method explicit --- app/controllers/main_controller.rb | 12 +++++++++++- app/policies/main_policy.rb | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index f89b6b78..d655ea91 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class MainController < ApplicationController + before_action :authorize_main after_action :verify_authorized after_action :verify_policy_scoped, only: [:home] # GET / def home - authorize :Main respond_to do |format| format.html do if !authenticated? @@ -17,4 +17,14 @@ class MainController < ApplicationController end end end + + # GET /request + def requestinvite + end + + private + + def authorize_main + authorize :Main + end end diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index 1c7a00e5..2eb5b3e8 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -3,4 +3,8 @@ class MainPolicy < ApplicationPolicy def home? true end + + def requestinvite? + true + end end From 466b1716a51587eae9c65876aece07f9149391e2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:10:41 +0800 Subject: [PATCH 101/378] more changes to routes.rb --- config/routes.rb | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index bdee276b..d96aa982 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,17 +1,10 @@ # frozen_string_literal: true Metamaps::Application.routes.draw do use_doorkeeper + root to: 'main#home', via: :get - get 'request', to: 'main#requestinvite', as: :request - namespace :search do - get :topics - get :maps - get :mappers - get :synapses - end - namespace :explore do get 'active' get 'featured' @@ -42,17 +35,29 @@ Metamaps::Application.routes.draw do resources :metacodes, except: [:destroy] get 'metacodes/:name', to: 'metacodes#show' + namespace :search do + get :topics + get :maps + get :mappers + get :synapses + end + resources :synapses, except: [:index, :new, :edit] resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic, on: :collection + get :autocomplete_topic + member do + get :network + get :relative_numbers + get :relatives + end end - get 'topics/:id/network', to: 'topics#network', as: :network - get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers - get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - resources :users, except: [:index, :destroy] - get 'users/:id/details', to: 'users#details', as: :details + resources :users, except: [:index, :destroy] do + member do + get :details + end + end post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes namespace :api, path: '/api', default: { format: :json } do From 2c3b387e4231452fb54eb584d7d13d9295933556 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:00:32 -0400 Subject: [PATCH 102/378] Update index.js --- frontend/src/Metamaps/Map/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 24ea08ea..ce277331 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -38,11 +38,6 @@ const Map = { init: function () { var self = Map - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#wrapper').on('contextmenu', function (e) { - return false - }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 10a2782f8553e1fad14f9686b1e7e0d3e3b5bba7 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:03:44 -0400 Subject: [PATCH 103/378] Update JIT.js --- frontend/src/Metamaps/JIT.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 4a665bdc..54ec74b1 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1305,6 +1305,9 @@ const JIT = { // create new menu for clicked on node var rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' + //prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu','return false') + // add the proper options to the menu var menustring = '<ul>' @@ -1550,6 +1553,8 @@ const JIT = { // create new menu for clicked on node var rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' + //prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu','return false') // add the proper options to the menu var menustring = '<ul>' From 67c4912c62bb08b3d317a7b87efc34fa47e49988 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:37:08 -0400 Subject: [PATCH 104/378] Update index.js --- frontend/src/Metamaps/Map/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index ce277331..24ea08ea 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -38,6 +38,11 @@ const Map = { init: function () { var self = Map + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#wrapper').on('contextmenu', function (e) { + return false + }) + $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 4e506ad290a8e13823f81c29a7fbcd19f4996f05 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:18:44 -0400 Subject: [PATCH 105/378] Update JIT.js --- frontend/src/Metamaps/JIT.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 54ec74b1..519bffd4 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -888,6 +888,7 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') + $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element Create.newTopic.x = eventInfo.getPos().x Create.newTopic.y = eventInfo.getPos().y Visualize.mGraph.plot() From a37f60060c2bdac219a025234ffbdefcf1b6a2b5 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:31:08 -0400 Subject: [PATCH 106/378] Update JIT.js --- frontend/src/Metamaps/JIT.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 519bffd4..54ec74b1 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -888,7 +888,6 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') - $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element Create.newTopic.x = eventInfo.getPos().x Create.newTopic.y = eventInfo.getPos().y Visualize.mGraph.plot() From e8746ee7d9523f17cbcb4064a932cc81b5de14b2 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:32:49 -0400 Subject: [PATCH 107/378] Update Create.js --- frontend/src/Metamaps/Create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 5d290fcd..db71fb8c 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -209,6 +209,7 @@ const Create = { bringToFront: true }) $('.new_topic').hide() + $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element }, name: null, newId: 1, From db3cf0490fb391d1a94d62232b1f9534ad53863d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 08:02:25 +0800 Subject: [PATCH 108/378] fix develop branch bugs (#679) * bugfix - rename SearchController so it works * remove unneeded respond_with * fix to_json calls --- app/controllers/maps_controller.rb | 3 --- app/controllers/search_controller.rb | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index b24f85fc..8d4c6e27 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -19,9 +19,6 @@ class MapsController < ApplicationController @allmappings = policy_scope(@map.mappings) @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars - - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, - @alltopics, @allmessages, @allstars, @map) end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 91b0b44d..0fb9c808 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -class MainController < ApplicationController +class SearchController < ApplicationController include TopicsHelper include MapsHelper include UsersHelper @@ -73,7 +73,7 @@ class MainController < ApplicationController @topics = [] end - render json: autocomplete_array_json(@topics) + render json: autocomplete_array_json(@topics).to_json end # get /search/maps?term=SOMETERM @@ -107,7 +107,7 @@ class MainController < ApplicationController @maps = [] end - render json: autocomplete_map_array_json(@maps) + render json: autocomplete_map_array_json(@maps).to_json end # get /search/mappers?term=SOMETERM @@ -125,7 +125,7 @@ class MainController < ApplicationController else @mappers = [] end - render json: autocomplete_user_array_json(@mappers) + render json: autocomplete_user_array_json(@mappers).to_json end # get /search/synapses?term=SOMETERM OR @@ -151,7 +151,7 @@ class MainController < ApplicationController # limit to 5 results @synapses = @synapses.to_a.slice(0, 5) - render json: autocomplete_synapse_array_json(@synapses) + render json: autocomplete_synapse_array_json(@synapses).to_json end private From 93341719a9373bc5f696d27cb2b7457868138732 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 28 Sep 2016 20:22:55 -0400 Subject: [PATCH 109/378] Update main_controller.rb (#682) --- app/controllers/main_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index d655ea91..0ea9ba97 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -9,6 +9,7 @@ class MainController < ApplicationController respond_to do |format| format.html do if !authenticated? + skip_policy_scope render 'main/home' else @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) From 88297b4eaad21b27aeda8de75afe33d1cb0efd68 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 08:38:02 +0800 Subject: [PATCH 110/378] fix routes.rb --- config/routes.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index d96aa982..13b2a5ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,12 +45,14 @@ Metamaps::Application.routes.draw do resources :synapses, except: [:index, :new, :edit] resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic member do get :network get :relative_numbers get :relatives end + collection do + get :autocomplete_topic + end end resources :users, except: [:index, :destroy] do From e858a2a7732e27616484b420edfcf6227a764332 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 09:24:17 +0800 Subject: [PATCH 111/378] update ChatView.js eslint style --- frontend/src/Metamaps/Views/ChatView.js | 538 ++++++++++++------------ 1 file changed, 267 insertions(+), 271 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 9bd8b563..54236bb4 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -12,323 +12,319 @@ import underscore from 'underscore' // TODO is this line good or bad // Backbone.$ = window.$ -const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - "</div>", - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - "</div>", - templates: function() { - underscore.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - this.messageTemplate = underscore.template(Private.messageHTML); - - this.participantTemplate = underscore.template(Private.participantHTML); - }, - createElements: function() { - this.$unread = $('<div class="chat-unread"></div>'); - this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); - this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); - this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); - this.$videoToggle = $('<div class="video-toggle"></div>'); - this.$cursorToggle = $('<div class="cursor-toggle"></div>'); - this.$participants = $('<div class="participants"></div>'); - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); - this.$chatHeader = $('<div class="chat-header">CHAT</div>'); - this.$soundToggle = $('<div class="sound-toggle"></div>'); - this.$messages = $('<div class="chat-messages"></div>'); - this.$container = $('<div class="chat-box"></div>'); - }, - attachElements: function() { - this.$button.append(this.$unread); - - this.$juntoHeader.append(this.$videoToggle); - this.$juntoHeader.append(this.$cursorToggle); - - this.$chatHeader.append(this.$soundToggle); - - this.$participants.append(this.$conversationInProgress); - - this.$container.append(this.$juntoHeader); - this.$container.append(this.$participants); - this.$container.append(this.$chatHeader); - this.$container.append(this.$button); - this.$container.append(this.$messages); - this.$container.append(this.$messageInput); - }, - addEventListeners: function() { - var self = this; - - this.participants.on('add', function (participant) { - Private.addParticipant.call(self, participant); - }); - - this.participants.on('remove', function (participant) { - Private.removeParticipant.call(self, participant); - }); - - this.$button.on('click', function () { - Handlers.buttonClick.call(self); - }); - this.$videoToggle.on('click', function () { - Handlers.videoToggleClick.call(self); - }); - this.$cursorToggle.on('click', function () { - Handlers.cursorToggleClick.call(self); - }); - this.$soundToggle.on('click', function () { - Handlers.soundToggleClick.call(self); - }); - this.$messageInput.on('keyup', function (event) { - Handlers.keyUp.call(self, event); - }); - this.$messageInput.on('focus', function () { - Handlers.inputFocus.call(self); - }); - this.$messageInput.on('blur', function () { - Handlers.inputBlur.call(self); - }); - }, - initializeSounds: function() { - this.sound = new Howl({ - urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], - sprite: { - joinmap: [0, 561], - leavemap: [1000, 592], - receivechat: [2000, 318], - sendchat: [3000, 296], - sessioninvite: [4000, 5393, true] - } - }); - }, - incrementUnread: function() { - this.unreadMessages++; - this.$unread.html(this.unreadMessages); - this.$unread.show(); - }, - addMessage: function(message, isInitial, wasMe) { - - if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); - - function addZero(i) { - if (i < 10) { - i = "0" + i; - } - return i; - } - var m = _.clone(message.attributes); - - var today = new Date(); - m.timestamp = new Date(m.created_at); - - var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); - date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); - m.timestamp = date; - m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove - m.message = linker.link(m.message); - var $html = $(this.messageTemplate(m)); - this.$messages.append($html); - if (!isInitial) this.scrollMessages(200); - - if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); - }, - initialMessages: function() { - var messages = this.messages.models; - for (var i = 0; i < messages.length; i++) { - Private.addMessage.call(this, messages[i], true); - } - }, - handleInputMessage: function() { - var message = { - message: this.$messageInput.val(), - }; - this.$messageInput.val(''); - $(document).trigger(ChatView.events.message + '-' + this.room, [message]); - }, - addParticipant: function(participant) { - var p = _.clone(participant.attributes); - if (p.self) { - p.selfClass = 'is-self'; - p.selfName = '(me)'; - } else { - p.selfClass = ''; - p.selfName = ''; - } - var html = this.participantTemplate(p); - this.$participants.append(html); - }, - removeParticipant: function(participant) { - this.$container.find('.participant-' + participant.get('id')).remove(); + messageHTML: "<div class='chat-message'>" + + "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + + "<div class='chat-message-text'>{{ message }}</div>" + + "<div class='chat-message-time'>{{ timestamp }}</div>" + + "<div class='clearfloat'></div>" + + '</div>', + participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + + "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + + "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + + "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + + "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + + "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + + "<div class='clearfloat'></div>" + + '</div>', + templates: function () { + underscore.templateSettings = { + interpolate: /\{\{(.+?)\}\}/g } -}; + this.messageTemplate = underscore.template(Private.messageHTML) + + this.participantTemplate = underscore.template(Private.participantHTML) + }, + createElements: function () { + this.$unread = $('<div class="chat-unread"></div>') + this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>') + this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>') + this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>') + this.$videoToggle = $('<div class="video-toggle"></div>') + this.$cursorToggle = $('<div class="cursor-toggle"></div>') + this.$participants = $('<div class="participants"></div>') + this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>') + this.$chatHeader = $('<div class="chat-header">CHAT</div>') + this.$soundToggle = $('<div class="sound-toggle"></div>') + this.$messages = $('<div class="chat-messages"></div>') + this.$container = $('<div class="chat-box"></div>') + }, + attachElements: function () { + this.$button.append(this.$unread) + + this.$juntoHeader.append(this.$videoToggle) + this.$juntoHeader.append(this.$cursorToggle) + + this.$chatHeader.append(this.$soundToggle) + + this.$participants.append(this.$conversationInProgress) + + this.$container.append(this.$juntoHeader) + this.$container.append(this.$participants) + this.$container.append(this.$chatHeader) + this.$container.append(this.$button) + this.$container.append(this.$messages) + this.$container.append(this.$messageInput) + }, + addEventListeners: function () { + var self = this + + this.participants.on('add', function (participant) { + Private.addParticipant.call(self, participant) + }) + + this.participants.on('remove', function (participant) { + Private.removeParticipant.call(self, participant) + }) + + this.$button.on('click', function () { + Handlers.buttonClick.call(self) + }) + this.$videoToggle.on('click', function () { + Handlers.videoToggleClick.call(self) + }) + this.$cursorToggle.on('click', function () { + Handlers.cursorToggleClick.call(self) + }) + this.$soundToggle.on('click', function () { + Handlers.soundToggleClick.call(self) + }) + this.$messageInput.on('keyup', function (event) { + Handlers.keyUp.call(self, event) + }) + this.$messageInput.on('focus', function () { + Handlers.inputFocus.call(self) + }) + this.$messageInput.on('blur', function () { + Handlers.inputBlur.call(self) + }) + }, + initializeSounds: function () { + this.sound = new Howl({ + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], + sprite: { + joinmap: [0, 561], + leavemap: [1000, 592], + receivechat: [2000, 318], + sendchat: [3000, 296], + sessioninvite: [4000, 5393, true] + } + }) + }, + incrementUnread: function () { + this.unreadMessages++ + this.$unread.html(this.unreadMessages) + this.$unread.show() + }, + addMessage: function (message, isInitial, wasMe) { + if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) + + function addZero (i) { + if (i < 10) { + i = '0' + i + } + return i + } + var m = _.clone(message.attributes) + + m.timestamp = new Date(m.created_at) + + var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate() + date += ' ' + addZero(m.timestamp.getHours()) + ':' + addZero(m.timestamp.getMinutes()) + m.timestamp = date + m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png' // TODO: remove + m.message = linker.link(m.message) + var $html = $(this.messageTemplate(m)) + this.$messages.append($html) + if (!isInitial) this.scrollMessages(200) + + if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat') + }, + initialMessages: function () { + var messages = this.messages.models + for (var i = 0; i < messages.length; i++) { + Private.addMessage.call(this, messages[i], true) + } + }, + handleInputMessage: function () { + var message = { + message: this.$messageInput.val() + } + this.$messageInput.val('') + $(document).trigger(ChatView.events.message + '-' + this.room, [message]) + }, + addParticipant: function (participant) { + var p = _.clone(participant.attributes) + if (p.self) { + p.selfClass = 'is-self' + p.selfName = '(me)' + } else { + p.selfClass = '' + p.selfName = '' + } + var html = this.participantTemplate(p) + this.$participants.append(html) + }, + removeParticipant: function (participant) { + this.$container.find('.participant-' + participant.get('id')).remove() + } +} var Handlers = { - buttonClick: function() { - if (this.isOpen) this.close(); - else if (!this.isOpen) this.open(); - }, - videoToggleClick: function() { - this.$videoToggle.toggleClass('active'); - this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff); - }, - cursorToggleClick: function() { - this.$cursorToggle.toggleClass('active'); - this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff); - }, - soundToggleClick: function() { - this.alertSound = !this.alertSound; - this.$soundToggle.toggleClass('active'); - }, - keyUp: function(event) { - switch(event.which) { - case 13: // enter - Private.handleInputMessage.call(this); - break; - } - }, - inputFocus: function() { - $(document).trigger(ChatView.events.inputFocus); - }, - inputBlur: function() { - $(document).trigger(ChatView.events.inputBlur); + buttonClick: function () { + if (this.isOpen) this.close() + else if (!this.isOpen) this.open() + }, + videoToggleClick: function () { + this.$videoToggle.toggleClass('active') + this.videosShowing = !this.videosShowing + $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) + }, + cursorToggleClick: function () { + this.$cursorToggle.toggleClass('active') + this.cursorsShowing = !this.cursorsShowing + $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) + }, + soundToggleClick: function () { + this.alertSound = !this.alertSound + this.$soundToggle.toggleClass('active') + }, + keyUp: function (event) { + switch (event.which) { + case 13: // enter + Private.handleInputMessage.call(this) + break } -}; + }, + inputFocus: function () { + $(document).trigger(ChatView.events.inputFocus) + }, + inputBlur: function () { + $(document).trigger(ChatView.events.inputBlur) + } +} -const ChatView = function(messages, mapper, room) { - var self = this; +const ChatView = function (messages, mapper, room) { + this.room = room + this.mapper = mapper + this.messages = messages // backbone collection - this.room = room; - this.mapper = mapper; - this.messages = messages; // backbone collection + this.isOpen = false + this.alertSound = true // whether to play sounds on arrival of new messages or not + this.cursorsShowing = true + this.videosShowing = true + this.unreadMessages = 0 + this.participants = new Backbone.Collection() - this.isOpen = false; - this.alertSound = true; // whether to play sounds on arrival of new messages or not - this.cursorsShowing = true; - this.videosShowing = true; - this.unreadMessages = 0; - this.participants = new Backbone.Collection(); - - Private.templates.call(this); - Private.createElements.call(this); - Private.attachElements.call(this); - Private.addEventListeners.call(this); - Private.initialMessages.call(this); - Private.initializeSounds.call(this); - this.$container.css({ - right: '-300px' - }); -}; + Private.templates.call(this) + Private.createElements.call(this) + Private.attachElements.call(this) + Private.addEventListeners.call(this) + Private.initialMessages.call(this) + Private.initializeSounds.call(this) + this.$container.css({ + right: '-300px' + }) +} ChatView.prototype.conversationInProgress = function (participating) { - this.$conversationInProgress.show(); - this.$participants.addClass('is-live'); - if (participating) this.$participants.addClass('is-participating'); - this.$button.addClass('active'); + this.$conversationInProgress.show() + this.$participants.addClass('is-live') + if (participating) this.$participants.addClass('is-participating') + this.$button.addClass('active') - // hide invite to call buttons +// hide invite to call buttons } ChatView.prototype.conversationEnded = function () { - this.$conversationInProgress.hide(); - this.$participants.removeClass('is-live'); - this.$participants.removeClass('is-participating'); - this.$button.removeClass('active'); - this.$participants.find('.participant').removeClass('active'); - this.$participants.find('.participant').removeClass('pending'); + this.$conversationInProgress.hide() + this.$participants.removeClass('is-live') + this.$participants.removeClass('is-participating') + this.$button.removeClass('active') + this.$participants.find('.participant').removeClass('active') + this.$participants.find('.participant').removeClass('pending') } ChatView.prototype.leaveConversation = function () { - this.$participants.removeClass('is-participating'); + this.$participants.removeClass('is-participating') } ChatView.prototype.mapperJoinedCall = function (id) { - this.$participants.find('.participant-' + id).addClass('active'); + this.$participants.find('.participant-' + id).addClass('active') } ChatView.prototype.mapperLeftCall = function (id) { - this.$participants.find('.participant-' + id).removeClass('active'); + this.$participants.find('.participant-' + id).removeClass('active') } ChatView.prototype.invitationPending = function (id) { - this.$participants.find('.participant-' + id).addClass('pending'); + this.$participants.find('.participant-' + id).addClass('pending') } ChatView.prototype.invitationAnswered = function (id) { - this.$participants.find('.participant-' + id).removeClass('pending'); + this.$participants.find('.participant-' + id).removeClass('pending') } ChatView.prototype.addParticipant = function (participant) { - this.participants.add(participant); + this.participants.add(participant) } ChatView.prototype.removeParticipant = function (username) { - var p = this.participants.find(function (p) { return p.get('username') === username; }); - if (p) { - this.participants.remove(p); - } + var p = this.participants.find(p => p.get('username') === username) + if (p) { + this.participants.remove(p) + } } ChatView.prototype.removeParticipants = function () { - this.participants.remove(this.participants.models); + this.participants.remove(this.participants.models) } ChatView.prototype.open = function () { - this.$container.css({ - right: '0' - }); - this.$messageInput.focus(); - this.isOpen = true; - this.unreadMessages = 0; - this.$unread.hide(); - this.scrollMessages(0); - $(document).trigger(ChatView.events.openTray); + this.$container.css({ + right: '0' + }) + this.$messageInput.focus() + this.isOpen = true + this.unreadMessages = 0 + this.$unread.hide() + this.scrollMessages(0) + $(document).trigger(ChatView.events.openTray) } -ChatView.prototype.addMessage = function(message, isInitial, wasMe) { - this.messages.add(message); - Private.addMessage.call(this, message, isInitial, wasMe); +ChatView.prototype.addMessage = function (message, isInitial, wasMe) { + this.messages.add(message) + Private.addMessage.call(this, message, isInitial, wasMe) } -ChatView.prototype.scrollMessages = function(duration) { - duration = duration || 0; +ChatView.prototype.scrollMessages = function (duration) { + duration = duration || 0 - this.$messages.animate({ - scrollTop: this.$messages[0].scrollHeight - }, duration); + this.$messages.animate({ + scrollTop: this.$messages[0].scrollHeight + }, duration) } ChatView.prototype.clearMessages = function () { - this.unreadMessages = 0; - this.$unread.hide(); - this.$messages.empty(); + this.unreadMessages = 0 + this.$unread.hide() + this.$messages.empty() } ChatView.prototype.close = function () { - this.$container.css({ - right: '-300px' - }); - this.$messageInput.blur(); - this.isOpen = false; - $(document).trigger(ChatView.events.closeTray); + this.$container.css({ + right: '-300px' + }) + this.$messageInput.blur() + this.isOpen = false + $(document).trigger(ChatView.events.closeTray) } ChatView.prototype.remove = function () { - this.$button.off(); - this.$container.remove(); + this.$button.off() + this.$container.remove() } /** @@ -336,15 +332,15 @@ ChatView.prototype.remove = function () { * @static */ ChatView.events = { - message: 'ChatView:message', - openTray: 'ChatView:openTray', - closeTray: 'ChatView:closeTray', - inputFocus: 'ChatView:inputFocus', - inputBlur: 'ChatView:inputBlur', - cursorsOff: 'ChatView:cursorsOff', - cursorsOn: 'ChatView:cursorsOn', - videosOff: 'ChatView:videosOff', - videosOn: 'ChatView:videosOn' -}; + message: 'ChatView:message', + openTray: 'ChatView:openTray', + closeTray: 'ChatView:closeTray', + inputFocus: 'ChatView:inputFocus', + inputBlur: 'ChatView:inputBlur', + cursorsOff: 'ChatView:cursorsOff', + cursorsOn: 'ChatView:cursorsOn', + videosOff: 'ChatView:videosOff', + videosOn: 'ChatView:videosOn' +} export default ChatView From bca85337cc05922665a9d252d53e4da2423a4060 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 09:33:13 +0800 Subject: [PATCH 112/378] add template strings + outdent to chatview --- frontend/src/Metamaps/Views/ChatView.js | 54 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 54236bb4..6b63cd70 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -9,26 +9,41 @@ import Backbone from 'backbone' import Autolinker from 'autolinker' import _ from 'lodash' import underscore from 'underscore' +import outdent from 'outdent' // TODO is this line good or bad // Backbone.$ = window.$ const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - '</div>', - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - '</div>', + messageHTML: outdent` + <div class='chat-message'> + <div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div> + <div class='chat-message-text'>{{ message }}</div> + <div class='chat-message-time'>{{ timestamp }}</div> + <div class='clearfloat'></div> + </div>`, + participantHTML: outdent` + <div class='participant participant-{{ id }} {{ selfClass }}'> + <div class='chat-participant-image'> + <img src='{{ image }}' style='border: 2px solid {{ color }};' /> + </div> + <div class='chat-participant-name'> + {{ username }} {{ selfName }} + </div> + <button type='button' + class='button chat-participant-invite-call' + onclick='Metamaps.Realtime.inviteACall({{ id}});' + ></button> + <button type='button' + class='button chat-participant-invite-join' + onclick='Metamaps.Realtime.inviteToJoin({{ id}});' + ></button> + <span class='chat-participant-participating'> + <div class='green-dot'></div> + </span> + <div class='clearfloat'></div> + </div>`, templates: function () { underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g @@ -45,7 +60,16 @@ var Private = { this.$videoToggle = $('<div class="video-toggle"></div>') this.$cursorToggle = $('<div class="cursor-toggle"></div>') this.$participants = $('<div class="participants"></div>') - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>') + this.$conversationInProgress = $(outdent` + <div class="conversation-live"> + LIVE + <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();"> + LEAVE + </span> + <span class="call-action join" onclick="Metamaps.Realtime.joinCall();"> + JOIN + </span> + </div>`) this.$chatHeader = $('<div class="chat-header">CHAT</div>') this.$soundToggle = $('<div class="sound-toggle"></div>') this.$messages = $('<div class="chat-messages"></div>') From 1bbc72fff09e63017d9d5066fdaa0105313acdc6 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 28 Sep 2016 22:36:53 -0400 Subject: [PATCH 113/378] was destroying and not reinitializing --- frontend/src/Metamaps/Visualize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 3804b6a8..a70ae21c 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -118,7 +118,7 @@ const Visualize = { render: function () { var self = Visualize, RGraphSettings, FDSettings - if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { + if (self.type == 'RGraph') { // clear the previous canvas from #infovis $('#infovis').empty() @@ -133,7 +133,7 @@ const Visualize = { RGraphSettings.levelDistance = JIT.RGraph.levelDistance self.mGraph = new $jit.RGraph(RGraphSettings) - } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { + } else if (self.type == 'ForceDirected') { // clear the previous canvas from #infovis $('#infovis').empty() From 26977d06a881cffa802c3f820aff7c116593e052 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 13:15:14 +0800 Subject: [PATCH 114/378] disable 5 minute request limit on rack attack --- config/initializers/rack-attack.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 9dfe3746..1cb90f0f 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -4,9 +4,9 @@ class Rack::Attack # Throttle all requests by IP (60rpm) # # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" - throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| - req.ip # unless req.path.start_with?('/assets') - end + # throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| + # req.ip # unless req.path.start_with?('/assets') + # end # Throttle POST requests to /login by IP address # @@ -32,7 +32,10 @@ class Rack::Attack end end - throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + throttle('load_url_title/req/5mins/ip', :limit => 300, :period => 5.minutes) do |req| + req.ip if req.path == 'hacks/load_url_title' + end + throttle('load_url_title/req/1s/ip', :limit => 5, :period => 1.second) do |req| # If the return value is truthy, the cache key for the return value # is incremented and compared with the limit. In this case: # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" From 1d4d7f07e29fef30a08035998128b82870947886 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 18:38:54 +0800 Subject: [PATCH 115/378] fix error when searching for synapse with undefined topic1id --- frontend/src/Metamaps/Create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index db71fb8c..e18ed1b3 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -262,7 +262,7 @@ const Create = { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { var self = Create.newSynapse - if (Selected.Nodes.length < 2) { + if (Selected.Nodes.length < 2 && self.topic1id && self.topic2id) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings } else { From 3b8199aac663dd8c98444f4fc791d512e9b2c648 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:05:49 +0800 Subject: [PATCH 116/378] eslint updates for GlobalUI.js --- frontend/src/Metamaps/GlobalUI.js | 612 +++++++++++++++--------------- 1 file changed, 302 insertions(+), 310 deletions(-) diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index 7af133de..6b6ca003 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,4 +1,4 @@ -/* global Metamaps, $, Hogan, Bloodhound */ +/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ import Active from './Active' import Create from './Create' import Filter from './Filter' @@ -14,44 +14,44 @@ const GlobalUI = { notifyTimeout: null, lightbox: null, init: function () { - var self = GlobalUI; + var self = GlobalUI - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); + self.Search.init() + self.CreateMap.init() + self.Account.init() if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - //bind lightbox clicks + // bind lightbox clicks $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); + self.openLightbox($(this).attr('data-open')) + event.preventDefault() + return false + }) - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox) // initialize global backbone models and collections - if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper); + if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper) - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : [] + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : [] + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : [] + var mapperCollection = [] + var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; + mapperCollection = Metamaps.Maps.Mapper.models + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : [] + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : [] + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj) + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) }, showDiv: function (selector) { $(selector).show() @@ -65,261 +65,252 @@ const GlobalUI = { }, 200, 'easeInCubic', function () { $(this).hide() }) }, openLightbox: function (which) { - var self = GlobalUI; + var self = GlobalUI - $('.lightboxContent').hide(); - $('#' + which).show(); + $('.lightboxContent').hide() + $('#' + which).show() - self.lightbox = which; + self.lightbox = which - $('#lightbox_overlay').show(); + $('#lightbox_overlay').show() - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px' // animate the content in from the bottom $('#lightbox_main').animate({ 'top': '50%', 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); + }, 200, 'easeOutCubic') // fade the black overlay in $('#lightbox_screen').animate({ 'opacity': '0.42' - }, 200); + }, 200) - if (which == "switchMetacodes") { - Create.isSwitchingSet = true; + if (which === 'switchMetacodes') { + Create.isSwitchingSet = true } }, closeLightbox: function (event) { - var self = GlobalUI; + var self = GlobalUI - if (event) event.preventDefault(); + if (event) event.preventDefault() // animate the lightbox content offscreen $('#lightbox_main').animate({ 'top': '100%', 'margin-top': '0' - }, 200, 'easeInCubic'); + }, 200, 'easeInCubic') // fade the black overlay out $('#lightbox_screen').animate({ 'opacity': '0.0' }, 200, function () { - $('#lightbox_overlay').hide(); - }); + $('#lightbox_overlay').hide() + }) - if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map') + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map') if (Create && Create.isSwitchingSet) { - Create.cancelMetacodeSetSwitch(); + Create.cancelMetacodeSetSwitch() } - self.lightbox = null; + self.lightbox = null }, notifyUser: function (message, leaveOpen) { - var self = GlobalUI; + var self = GlobalUI $('#toast').html(message) self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); + clearTimeout(self.notifyTimeOut) if (!leaveOpen) { self.notifyTimeOut = setTimeout(function () { self.hideDiv('#toast') - }, 8000); + }, 8000) } }, - clearNotify: function() { - var self = GlobalUI; + clearNotify: function () { + var self = GlobalUI - clearTimeout(self.notifyTimeOut); + clearTimeout(self.notifyTimeOut) self.hideDiv('#toast') }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + shareInvite: function (inviteLink) { + window.prompt('To copy the invite link, press: Ctrl+C, Enter', inviteLink) } } GlobalUI.CreateMap = { newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", + emptyMapForm: '', + emptyForkMapForm: '', topicsToMap: [], synapsesToMap: [], init: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - self.bindFormEvents(); - - self.emptyMapForm = $('#new_map').html(); + self.bindFormEvents() + self.emptyMapForm = $('#new_map').html() }, bindFormEvents: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { if (event.keyCode === 13) self.submit() }) $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); + event.preventDefault() + GlobalUI.closeLightbox() + }) + $('.new_map button.submitMap').unbind().bind('click', self.submit) // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); + $('.permIcon').unbind().bind('click', self.switchPermission) }, closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); + $('#mapCreatedSuccess').fadeOut(300, function () { + $(this).remove() + }) }, generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" + stringStart += id + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " + var page = Active.Map ? 'map' : 'page' + var stringEnd = '</a></div>' + return stringStart + page + stringEnd }, switchPermission: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); + self.newMap.set('permission', $(this).attr('data-permission')) + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') + $(this).find('.mapPermIcon').addClass('selected') - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); + var permText = $(this).find('.tip').html() + $(this).parents('.new_map').find('.permText').html(permText) }, submit: function (event) { - if (event) event.preventDefault(); + if (event) event.preventDefault() - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); + self.newMap.set('topicsToMap', self.topicsToMap) + self.newMap.set('synapsesToMap', self.synapsesToMap) } - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); + self.newMap.set('name', $form.find('#map_name').val()) + self.newMap.set('desc', $form.find('#map_desc').val()) - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; + if (self.newMap.get('name').length === 0) { + self.throwMapNameError() + return } self.newMap.save(null, { success: self.success - // TODO add error message - }); + // TODO add error message + }) - GlobalUI.closeLightbox(); - GlobalUI.notifyUser('Working...'); + GlobalUI.closeLightbox() + GlobalUI.notifyUser('Working...') }, throwMapNameError: function () { - var self = GlobalUI.CreateMap; - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + var message = $("<div class='feedback_message'>Please enter a map name...</div>") - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); + $form.find('#map_name').after(message) + setTimeout(function () { + message.fadeOut('fast', function () { + message.remove() + }) + }, 5000) }, success: function (model) { - var self = GlobalUI.CreateMap; - - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); - - GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); + var self = GlobalUI.CreateMap + // push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model) + GlobalUI.clearNotify() + $('#wrapper').append(self.generateSuccessMessage(model.id)) }, reset: function (id) { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - var form = $('#' + id); + var form = $('#' + id) - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); + if (id === 'fork_map') { + self.topicsToMap = [] + self.synapsesToMap = [] + form.html(self.emptyForkMapForm) + } else { + form.html(self.emptyMapForm) } - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.bindFormEvents() + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - return false; - }, + return false + } } GlobalUI.Account = { isOpen: false, changing: false, init: function () { - var self = GlobalUI.Account; + var self = GlobalUI.Account - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); + $('.sidebarAccountIcon').click(self.toggleBox) + $('.sidebarAccountBox').click(function (event) { + event.stopPropagation() + }) + $('body').click(self.close) }, toggleBox: function (event) { - var self = GlobalUI.Account; + var self = GlobalUI.Account - if (self.isOpen) self.close(); - else self.open(); + if (self.isOpen) self.close() + else self.open() - event.stopPropagation(); + event.stopPropagation() }, open: function () { - var self = GlobalUI.Account; - - Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + var self = GlobalUI.Account + Filter.close() + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') if (!self.isOpen && !self.changing) { - self.changing = true; + self.changing = true $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); + self.changing = false + self.isOpen = true + $('.sidebarAccountBox #user_email').focus() + }) } }, close: function () { - var self = GlobalUI.Account; + var self = GlobalUI.Account - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); + self.changing = true + $('.sidebarAccountBox #user_email').blur() $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); + self.changing = false + self.isOpen = false + }) } } } @@ -333,31 +324,33 @@ GlobalUI.Search = { changing: false, optionsInitialized: false, init: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - var loader = new CanvasLoader('searchLoading'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(24); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default + // TODO does this overlap with Metamaps.Loading? + // devin sez: I'd like to remove Metamaps.Loading from the rails code + var loader = new CanvasLoader('searchLoading') + loader.setColor('#4fb5c0') // default is '#000000' + loader.setDiameter(24) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9) // default is 1.3 + loader.show() // Hidden by default // bind the hover events - $(".sidebarSearch").hover(function () { + $('.sidebarSearch').hover(function () { self.open() }, function () { self.close(800, false) - }); + }) $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus(); - }); + $('.sidebarSearchField').focus() + }) $('.sidebarSearch').click(function (e) { - e.stopPropagation(); - }); + e.stopPropagation() + }) $('body').click(function (e) { - self.close(0, false); - }); + self.close(0, false) + }) // open if the search is closed and user hits ctrl+/ // close if they hit ESC @@ -365,281 +358,280 @@ GlobalUI.Search = { switch (e.which) { case 191: if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true); // true for focus + self.open(true) // true for focus } - break; + break case 27: if (self.isOpen) { - self.close(0, true); + self.close(0, true) } - break; + break default: - break; //console.log(e.which); + break // console.log(e.which) } - }); + }) - self.startTypeahead(); + self.startTypeahead() }, - lock: function() { - var self = GlobalUI.Search; - self.locked = true; + lock: function () { + var self = GlobalUI.Search + self.locked = true }, - unlock: function() { - var self = GlobalUI.Search; - self.locked = false; + unlock: function () { + var self = GlobalUI.Search + self.locked = false }, open: function (focus) { - var self = GlobalUI.Search; + var self = GlobalUI.Search - clearTimeout(self.timeOut); + clearTimeout(self.timeOut) if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true; + self.changing = true $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ width: '400px' }, 300, function () { - if (focus) $('.sidebarSearchField').focus(); + if (focus) $('.sidebarSearchField').focus() $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ padding: '7px 10px 3px 10px', width: '380px' - }); - self.changing = false; - self.isOpen = true; - }); + }) + self.changing = false + self.isOpen = true + }) } }, close: function (closeAfter, bypass) { // for now return - var self = GlobalUI.Search; + var self = GlobalUI.Search self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { - self.changing = true; + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { + self.changing = true $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ padding: '7px 0 3px 0', width: '400px' - }); + }) $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ width: '0' }, 300, function () { - $('.sidebarSearchField').typeahead('val', ''); - $('.sidebarSearchField').blur(); - self.changing = false; - self.isOpen = false; - }); + $('.sidebarSearchField').typeahead('val', '') + $('.sidebarSearchField').blur() + self.changing = false + self.isOpen = false + }) } - }, closeAfter); + }, closeAfter) }, startTypeahead: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' var topics = { name: 'topics', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: "No results", - label: "No results", + value: 'No results', + label: 'No results', typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: "noresult" - }); + rtype: 'noresult' + }) }, header: topicheader, - suggestion: function(s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/topics', - prepare: function(query, settings) { - settings.url += '?term=' + query; + prepare: function (query, settings) { + settings.url += '?term=' + query if (Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Active.Mapper.id.toString(); + settings.url += '&user=' + Active.Mapper.id.toString() } - return settings; - }, - }, - }), - }; + return settings + } + } + }) + } var maps = { name: 'maps', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult" - }); + value: 'No results', + label: 'No results', + rtype: 'noresult' + }) }, header: mapheader, - suggestion: function(s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/maps', - prepare: function(query, settings) { - settings.url += '?term=' + query; + prepare: function (query, settings) { + settings.url += '?term=' + query if (Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Active.Mapper.id.toString(); + settings.url += '&user=' + Active.Mapper.id.toString() } - return settings; - }, - }, - }), - }; + return settings + } + } + }) + } var mappers = { name: 'mappers', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult", + value: 'No results', + label: 'No results', + rtype: 'noresult', profile: Metamaps.Erb['user.png'] - }); + }) }, header: mapperheader, - suggestion: function(s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY', - }, - }), - }; + wildcard: '%QUERY' + } + }) + } // Take all that crazy setup data and put it together into one beautiful typeahead call! $('.sidebarSearchField').typeahead( { - highlight: true, + highlight: true }, [topics, maps, mappers] - ); + ) - //Set max height of the search results box to prevent it from covering bottom left footer + // Set max height of the search results box to prevent it from covering bottom left footer $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions(); - self.hideLoader(); - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); + self.initSearchOptions() + self.hideLoader() + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true); + $('#limitTopicsToMe').prop('checked', true) } if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true); + $('#limitMapsToMe').prop('checked', true) } - }); + }) $(window).resize(function () { - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - }); + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + }) // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) // don't do it, if they clicked on a 'addToMap' button $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation(); - }); + event.stopPropagation() + }) // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function(){ + $('.sidebarSearchField.tt-input').keyup(function () { if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader(); + self.hideLoader() } else { - self.showLoader(); + self.showLoader() } - }); - + }) }, handleResultClick: function (event, datum, dataset) { - var self = GlobalUI.Search; + var self = GlobalUI.Search - self.hideLoader(); + self.hideLoader() - if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { - self.close(0, true); - if (datum.rtype == "topic") { - Router.topics(datum.id); - } else if (datum.rtype == "map") { - Router.maps(datum.id); - } else if (datum.rtype == "mapper") { - Router.explore("mapper", datum.id); + if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { + self.close(0, true) + if (datum.rtype === 'topic') { + Router.topics(datum.id) + } else if (datum.rtype === 'map') { + Router.maps(datum.id) + } else if (datum.rtype === 'mapper') { + Router.explore('mapper', datum.id) } } }, initSearchOptions: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - function toggleResultSet(set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); + function toggleResultSet (set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') if (s.is(':visible')) { - s.hide(); - $(this).removeClass('minimizeResults').addClass('maximizeResults'); + s.hide() + $(this).removeClass('minimizeResults').addClass('maximizeResults') } else { - s.show(); - $(this).removeClass('maximizeResults').addClass('minimizeResults'); + s.show() + $(this).removeClass('maximizeResults').addClass('minimizeResults') } } - $('.limitToMe').unbind().bind("change", function (e) { - if ($(this).attr('id') == 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe; + $('.limitToMe').unbind().bind('change', function (e) { + if ($(this).attr('id') === 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe } - if ($(this).attr('id') == 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe; + if ($(this).attr('id') === 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe } // set the value of the search equal to itself to retrigger the // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val(); - $(".sidebarSearchField").typeahead('val', '') - .typeahead('val', searchQuery); - }); + var searchQuery = $('.sidebarSearchField.tt-input').val() + $('.sidebarSearchField').typeahead('val', '') + .typeahead('val', searchQuery) + }) // when the user clicks minimize section, hide the results for that section $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers'); - }); + toggleResultSet.call(this, 'mappers') + }) $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics'); - }); + toggleResultSet.call(this, 'topics') + }) $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps'); - }); + toggleResultSet.call(this, 'maps') + }) }, hideLoader: function () { - $('#searchLoading').hide(); + $('#searchLoading').hide() }, showLoader: function () { - $('#searchLoading').show(); + $('#searchLoading').show() } } From 24caafba746df0eac42a510e60fd1d883af37c1a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:08:45 +0800 Subject: [PATCH 117/378] move GlobalUI into a folder --- frontend/src/Metamaps/{GlobalUI.js => GlobalUI/index.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/src/Metamaps/{GlobalUI.js => GlobalUI/index.js} (100%) diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI/index.js similarity index 100% rename from frontend/src/Metamaps/GlobalUI.js rename to frontend/src/Metamaps/GlobalUI/index.js From e4e6043ded7439f5ae400a48d123820f2c62a2c3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:20:16 +0800 Subject: [PATCH 118/378] split GlobalUI into files --- frontend/src/Metamaps/GlobalUI/Account.js | 55 +++ frontend/src/Metamaps/GlobalUI/CreateMap.js | 137 ++++++ frontend/src/Metamaps/GlobalUI/Search.js | 331 +++++++++++++ frontend/src/Metamaps/GlobalUI/index.js | 511 +------------------- frontend/src/Metamaps/index.js | 56 +-- 5 files changed, 561 insertions(+), 529 deletions(-) create mode 100644 frontend/src/Metamaps/GlobalUI/Account.js create mode 100644 frontend/src/Metamaps/GlobalUI/CreateMap.js create mode 100644 frontend/src/Metamaps/GlobalUI/Search.js diff --git a/frontend/src/Metamaps/GlobalUI/Account.js b/frontend/src/Metamaps/GlobalUI/Account.js new file mode 100644 index 00000000..210627ff --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/Account.js @@ -0,0 +1,55 @@ +/* global $ */ + +import Filter from '../Filter' + +const Account = { + isOpen: false, + changing: false, + init: function () { + var self = Account + + $('.sidebarAccountIcon').click(self.toggleBox) + $('.sidebarAccountBox').click(function (event) { + event.stopPropagation() + }) + $('body').click(self.close) + }, + toggleBox: function (event) { + var self = Account + + if (self.isOpen) self.close() + else self.open() + + event.stopPropagation() + }, + open: function () { + var self = Account + + Filter.close() + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') + + if (!self.isOpen && !self.changing) { + self.changing = true + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false + self.isOpen = true + $('.sidebarAccountBox #user_email').focus() + }) + } + }, + close: function () { + var self = Account + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') + if (!self.changing) { + self.changing = true + $('.sidebarAccountBox #user_email').blur() + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false + self.isOpen = false + }) + } + } +} + +export default Account diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js new file mode 100644 index 00000000..a24c73c8 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -0,0 +1,137 @@ +/* global Metamaps, $ */ + +import Active from '../Active' +import GlobalUI from './index' + +/* + * Metamaps.Backbone + * Metamaps.Maps + */ + +const CreateMap = { + newMap: null, + emptyMapForm: '', + emptyForkMapForm: '', + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = CreateMap + + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) + + self.bindFormEvents() + + self.emptyMapForm = $('#new_map').html() + }, + bindFormEvents: function () { + var self = CreateMap + + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { + if (event.keyCode === 13) self.submit() + }) + + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault() + GlobalUI.closeLightbox() + }) + $('.new_map button.submitMap').unbind().bind('click', self.submit) + + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission) + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function () { + $(this).remove() + }) + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" + stringStart += id + stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " + var page = Active.Map ? 'map' : 'page' + var stringEnd = '</a></div>' + return stringStart + page + stringEnd + }, + switchPermission: function () { + var self = CreateMap + + self.newMap.set('permission', $(this).attr('data-permission')) + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') + $(this).find('.mapPermIcon').addClass('selected') + + var permText = $(this).find('.tip').html() + $(this).parents('.new_map').find('.permText').html(permText) + }, + submit: function (event) { + if (event) event.preventDefault() + + var self = CreateMap + + if (GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap) + self.newMap.set('synapsesToMap', self.synapsesToMap) + } + + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) + + self.newMap.set('name', $form.find('#map_name').val()) + self.newMap.set('desc', $form.find('#map_desc').val()) + + if (self.newMap.get('name').length === 0) { + self.throwMapNameError() + return + } + + self.newMap.save(null, { + success: self.success + // TODO add error message + }) + + GlobalUI.closeLightbox() + GlobalUI.notifyUser('Working...') + }, + throwMapNameError: function () { + + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) + + var message = $("<div class='feedback_message'>Please enter a map name...</div>") + + $form.find('#map_name').after(message) + setTimeout(function () { + message.fadeOut('fast', function () { + message.remove() + }) + }, 5000) + }, + success: function (model) { + var self = CreateMap + // push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model) + + GlobalUI.clearNotify() + $('#wrapper').append(self.generateSuccessMessage(model.id)) + }, + reset: function (id) { + var self = CreateMap + + var form = $('#' + id) + + if (id === 'fork_map') { + self.topicsToMap = [] + self.synapsesToMap = [] + form.html(self.emptyForkMapForm) + } else { + form.html(self.emptyMapForm) + } + + self.bindFormEvents() + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) + + return false + } +} + +export default CreateMap diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js new file mode 100644 index 00000000..4999e279 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -0,0 +1,331 @@ +/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ + +import Active from '../Active' +import Router from '../Router' + +/* + * Metamaps.Erb + * Metamaps.Maps + */ + +const Search = { + locked: false, + isOpen: false, + limitTopicsToMe: false, + limitMapsToMe: false, + timeOut: null, + changing: false, + optionsInitialized: false, + init: function () { + var self = Search + + // TODO does this overlap with Metamaps.Loading? + // devin sez: I'd like to remove Metamaps.Loading from the rails code + var loader = new CanvasLoader('searchLoading') + loader.setColor('#4fb5c0') // default is '#000000' + loader.setDiameter(24) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9) // default is 1.3 + loader.show() // Hidden by default + + // bind the hover events + $('.sidebarSearch').hover(function () { + self.open() + }, function () { + self.close(800, false) + }) + + $('.sidebarSearchIcon').click(function (e) { + $('.sidebarSearchField').focus() + }) + $('.sidebarSearch').click(function (e) { + e.stopPropagation() + }) + $('body').click(function (e) { + self.close(0, false) + }) + + // open if the search is closed and user hits ctrl+/ + // close if they hit ESC + $('body').bind('keyup', function (e) { + switch (e.which) { + case 191: + if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { + self.open(true) // true for focus + } + break + case 27: + if (self.isOpen) { + self.close(0, true) + } + break + + default: + break // console.log(e.which) + } + }) + + self.startTypeahead() + }, + lock: function () { + var self = Search + self.locked = true + }, + unlock: function () { + var self = Search + self.locked = false + }, + open: function (focus) { + var self = Search + + clearTimeout(self.timeOut) + if (!self.isOpen && !self.changing && !self.locked) { + self.changing = true + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '400px' + }, 300, function () { + if (focus) $('.sidebarSearchField').focus() + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 10px 3px 10px', + width: '380px' + }) + self.changing = false + self.isOpen = true + }) + } + }, + close: function (closeAfter, bypass) { + // for now + return + + var self = Search + + self.timeOut = setTimeout(function () { + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { + self.changing = true + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 0 3px 0', + width: '400px' + }) + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '0' + }, 300, function () { + $('.sidebarSearchField').typeahead('val', '') + $('.sidebarSearchField').blur() + self.changing = false + self.isOpen = false + }) + } + }, closeAfter) + }, + startTypeahead: function () { + var self = Search + + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' + + var topics = { + name: 'topics', + limit: 9999, + + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + typeImageURL: Metamaps.Erb['icons/wildcard.png'], + rtype: 'noresult' + }) + }, + header: topicheader, + suggestion: function (s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/topics', + prepare: function (query, settings) { + settings.url += '?term=' + query + if (Active.Mapper && self.limitTopicsToMe) { + settings.url += '&user=' + Active.Mapper.id.toString() + } + return settings + } + } + }) + } + + var maps = { + name: 'maps', + limit: 9999, + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + rtype: 'noresult' + }) + }, + header: mapheader, + suggestion: function (s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps', + prepare: function (query, settings) { + settings.url += '?term=' + query + if (Active.Mapper && self.limitMapsToMe) { + settings.url += '&user=' + Active.Mapper.id.toString() + } + return settings + } + } + }) + } + + var mappers = { + name: 'mappers', + limit: 9999, + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + rtype: 'noresult', + profile: Metamaps.Erb['user.png'] + }) + }, + header: mapperheader, + suggestion: function (s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY' + } + }) + } + + // Take all that crazy setup data and put it together into one beautiful typeahead call! + $('.sidebarSearchField').typeahead( + { + highlight: true + }, + [topics, maps, mappers] + ) + + // Set max height of the search results box to prevent it from covering bottom left footer + $('.sidebarSearchField').bind('typeahead:render', function (event) { + self.initSearchOptions() + self.hideLoader() + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + if (self.limitTopicsToMe) { + $('#limitTopicsToMe').prop('checked', true) + } + if (self.limitMapsToMe) { + $('#limitMapsToMe').prop('checked', true) + } + }) + $(window).resize(function () { + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + }) + + // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) + + // don't do it, if they clicked on a 'addToMap' button + $('.sidebarSearch button.addToMap').click(function (event) { + event.stopPropagation() + }) + + // make sure that when you click on 'limit to me' or 'toggle section' it works + $('.sidebarSearchField.tt-input').keyup(function () { + if ($('.sidebarSearchField.tt-input').val() === '') { + self.hideLoader() + } else { + self.showLoader() + } + }) + }, + handleResultClick: function (event, datum, dataset) { + var self = Search + + self.hideLoader() + + if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { + self.close(0, true) + if (datum.rtype === 'topic') { + Router.topics(datum.id) + } else if (datum.rtype === 'map') { + Router.maps(datum.id) + } else if (datum.rtype === 'mapper') { + Router.explore('mapper', datum.id) + } + } + }, + initSearchOptions: function () { + var self = Search + + function toggleResultSet (set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') + if (s.is(':visible')) { + s.hide() + $(this).removeClass('minimizeResults').addClass('maximizeResults') + } else { + s.show() + $(this).removeClass('maximizeResults').addClass('minimizeResults') + } + } + + $('.limitToMe').unbind().bind('change', function (e) { + if ($(this).attr('id') === 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe + } + if ($(this).attr('id') === 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe + } + + // set the value of the search equal to itself to retrigger the + // autocomplete event + var searchQuery = $('.sidebarSearchField.tt-input').val() + $('.sidebarSearchField').typeahead('val', '') + .typeahead('val', searchQuery) + }) + + // when the user clicks minimize section, hide the results for that section + $('.minimizeMapperResults').unbind().click(function (e) { + toggleResultSet.call(this, 'mappers') + }) + $('.minimizeTopicResults').unbind().click(function (e) { + toggleResultSet.call(this, 'topics') + }) + $('.minimizeMapResults').unbind().click(function (e) { + toggleResultSet.call(this, 'maps') + }) + }, + hideLoader: function () { + $('#searchLoading').hide() + }, + showLoader: function () { + $('#searchLoading').show() + } +} + +export default Search diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 6b6ca003..3b16375e 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -1,12 +1,14 @@ -/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ -import Active from './Active' -import Create from './Create' -import Filter from './Filter' -import Router from './Router' +/* global Metamaps, $ */ + +import Active from '../Active' +import Create from '../Create' + +import Search from './Search' +import CreateMap from './CreateMap' +import Account from './Account' /* * Metamaps.Backbone - * Metamaps.Erb * Metamaps.Maps */ @@ -139,500 +141,5 @@ const GlobalUI = { } } -GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: '', - emptyForkMapForm: '', - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = GlobalUI.CreateMap - - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - - self.bindFormEvents() - - self.emptyMapForm = $('#new_map').html() - }, - bindFormEvents: function () { - var self = GlobalUI.CreateMap - - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { - if (event.keyCode === 13) self.submit() - }) - - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault() - GlobalUI.closeLightbox() - }) - $('.new_map button.submitMap').unbind().bind('click', self.submit) - - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission) - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function () { - $(this).remove() - }) - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" - stringStart += id - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " - var page = Active.Map ? 'map' : 'page' - var stringEnd = '</a></div>' - return stringStart + page + stringEnd - }, - switchPermission: function () { - var self = GlobalUI.CreateMap - - self.newMap.set('permission', $(this).attr('data-permission')) - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') - $(this).find('.mapPermIcon').addClass('selected') - - var permText = $(this).find('.tip').html() - $(this).parents('.new_map').find('.permText').html(permText) - }, - submit: function (event) { - if (event) event.preventDefault() - - var self = GlobalUI.CreateMap - - if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap) - self.newMap.set('synapsesToMap', self.synapsesToMap) - } - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' - var $form = $(formId) - - self.newMap.set('name', $form.find('#map_name').val()) - self.newMap.set('desc', $form.find('#map_desc').val()) - - if (self.newMap.get('name').length === 0) { - self.throwMapNameError() - return - } - - self.newMap.save(null, { - success: self.success - // TODO add error message - }) - - GlobalUI.closeLightbox() - GlobalUI.notifyUser('Working...') - }, - throwMapNameError: function () { - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' - var $form = $(formId) - - var message = $("<div class='feedback_message'>Please enter a map name...</div>") - - $form.find('#map_name').after(message) - setTimeout(function () { - message.fadeOut('fast', function () { - message.remove() - }) - }, 5000) - }, - success: function (model) { - var self = GlobalUI.CreateMap - // push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model) - - GlobalUI.clearNotify() - $('#wrapper').append(self.generateSuccessMessage(model.id)) - }, - reset: function (id) { - var self = GlobalUI.CreateMap - - var form = $('#' + id) - - if (id === 'fork_map') { - self.topicsToMap = [] - self.synapsesToMap = [] - form.html(self.emptyForkMapForm) - } else { - form.html(self.emptyMapForm) - } - - self.bindFormEvents() - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - - return false - } -} - -GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = GlobalUI.Account - - $('.sidebarAccountIcon').click(self.toggleBox) - $('.sidebarAccountBox').click(function (event) { - event.stopPropagation() - }) - $('body').click(self.close) - }, - toggleBox: function (event) { - var self = GlobalUI.Account - - if (self.isOpen) self.close() - else self.open() - - event.stopPropagation() - }, - open: function () { - var self = GlobalUI.Account - - Filter.close() - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') - - if (!self.isOpen && !self.changing) { - self.changing = true - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false - self.isOpen = true - $('.sidebarAccountBox #user_email').focus() - }) - } - }, - close: function () { - var self = GlobalUI.Account - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') - if (!self.changing) { - self.changing = true - $('.sidebarAccountBox #user_email').blur() - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false - self.isOpen = false - }) - } - } -} - -GlobalUI.Search = { - locked: false, - isOpen: false, - limitTopicsToMe: false, - limitMapsToMe: false, - timeOut: null, - changing: false, - optionsInitialized: false, - init: function () { - var self = GlobalUI.Search - - // TODO does this overlap with Metamaps.Loading? - // devin sez: I'd like to remove Metamaps.Loading from the rails code - var loader = new CanvasLoader('searchLoading') - loader.setColor('#4fb5c0') // default is '#000000' - loader.setDiameter(24) // default is 40 - loader.setDensity(41) // default is 40 - loader.setRange(0.9) // default is 1.3 - loader.show() // Hidden by default - - // bind the hover events - $('.sidebarSearch').hover(function () { - self.open() - }, function () { - self.close(800, false) - }) - - $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus() - }) - $('.sidebarSearch').click(function (e) { - e.stopPropagation() - }) - $('body').click(function (e) { - self.close(0, false) - }) - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true) // true for focus - } - break - case 27: - if (self.isOpen) { - self.close(0, true) - } - break - - default: - break // console.log(e.which) - } - }) - - self.startTypeahead() - }, - lock: function () { - var self = GlobalUI.Search - self.locked = true - }, - unlock: function () { - var self = GlobalUI.Search - self.locked = false - }, - open: function (focus) { - var self = GlobalUI.Search - - clearTimeout(self.timeOut) - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus() - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }) - self.changing = false - self.isOpen = true - }) - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = GlobalUI.Search - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { - self.changing = true - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }) - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', '') - $('.sidebarSearchField').blur() - self.changing = false - self.isOpen = false - }) - } - }, closeAfter) - }, - startTypeahead: function () { - var self = GlobalUI.Search - - var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' - var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' - - var topics = { - name: 'topics', - limit: 9999, - - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: 'noresult' - }) - }, - header: topicheader, - suggestion: function (s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/topics', - prepare: function (query, settings) { - settings.url += '?term=' + query - if (Active.Mapper && self.limitTopicsToMe) { - settings.url += '&user=' + Active.Mapper.id.toString() - } - return settings - } - } - }) - } - - var maps = { - name: 'maps', - limit: 9999, - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - rtype: 'noresult' - }) - }, - header: mapheader, - suggestion: function (s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/maps', - prepare: function (query, settings) { - settings.url += '?term=' + query - if (Active.Mapper && self.limitMapsToMe) { - settings.url += '&user=' + Active.Mapper.id.toString() - } - return settings - } - } - }) - } - - var mappers = { - name: 'mappers', - limit: 9999, - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - rtype: 'noresult', - profile: Metamaps.Erb['user.png'] - }) - }, - header: mapperheader, - suggestion: function (s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY' - } - }) - } - - // Take all that crazy setup data and put it together into one beautiful typeahead call! - $('.sidebarSearchField').typeahead( - { - highlight: true - }, - [topics, maps, mappers] - ) - - // Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions() - self.hideLoader() - var h = $(window).height() - $('.tt-dropdown-menu').css('max-height', h - 100) - if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true) - } - if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true) - } - }) - $(window).resize(function () { - var h = $(window).height() - $('.tt-dropdown-menu').css('max-height', h - 100) - }) - - // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) - - // don't do it, if they clicked on a 'addToMap' button - $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation() - }) - - // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function () { - if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader() - } else { - self.showLoader() - } - }) - }, - handleResultClick: function (event, datum, dataset) { - var self = GlobalUI.Search - - self.hideLoader() - - if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { - self.close(0, true) - if (datum.rtype === 'topic') { - Router.topics(datum.id) - } else if (datum.rtype === 'map') { - Router.maps(datum.id) - } else if (datum.rtype === 'mapper') { - Router.explore('mapper', datum.id) - } - } - }, - initSearchOptions: function () { - var self = GlobalUI.Search - - function toggleResultSet (set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') - if (s.is(':visible')) { - s.hide() - $(this).removeClass('minimizeResults').addClass('maximizeResults') - } else { - s.show() - $(this).removeClass('maximizeResults').addClass('minimizeResults') - } - } - - $('.limitToMe').unbind().bind('change', function (e) { - if ($(this).attr('id') === 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe - } - if ($(this).attr('id') === 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe - } - - // set the value of the search equal to itself to retrigger the - // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val() - $('.sidebarSearchField').typeahead('val', '') - .typeahead('val', searchQuery) - }) - - // when the user clicks minimize section, hide the results for that section - $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers') - }) - $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics') - }) - $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps') - }) - }, - hideLoader: function () { - $('#searchLoading').hide() - }, - showLoader: function () { - $('#searchLoading').show() - } -} - +export { Search, CreateMap, Account } export default GlobalUI diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index db67409b..d179713e 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -9,7 +9,9 @@ import Control from './Control' import Create from './Create' import Debug from './Debug' import Filter from './Filter' -import GlobalUI from './GlobalUI' +import GlobalUI, { + Search, CreateMap, Account as GlobalUI_Account +} from './GlobalUI' import Import from './Import' import JIT from './JIT' import Listeners from './Listeners' @@ -42,6 +44,9 @@ Metamaps.Create = Create Metamaps.Debug = Debug Metamaps.Filter = Filter Metamaps.GlobalUI = GlobalUI +Metamaps.GlobalUI.Search = Search +Metamaps.GlobalUI.CreateMap = CreateMap +Metamaps.GlobalUI.Account = GlobalUI_Account Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners @@ -66,41 +71,38 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize -document.addEventListener("DOMContentLoaded", function() { +document.addEventListener('DOMContentLoaded', function () { // initialize all the modules for (const prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) === 'function' + ) { + Metamaps[prop].init() + } } // load whichever page you are on - if (Metamaps.currentSection === "explore") { - const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + if (Metamaps.currentSection === 'explore') { + const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Metamaps.Views.ExploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Views.ExploreMaps.fetchUserThenRender() - } - else { - Views.ExploreMaps.render() - } - GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Active.Mapper) { - Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) + if (Metamaps.currentPage === 'mapper') { + Views.ExploreMaps.fetchUserThenRender() + } else { Views.ExploreMaps.render() - GlobalUI.showDiv('#explore') - } - else if (Active.Map || Active.Topic) { + } + GlobalUI.showDiv('#explore') + } else if (Metamaps.currentSection === '' && Active.Mapper) { + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.render() + GlobalUI.showDiv('#explore') + } else if (Active.Map || Active.Topic) { Metamaps.Loading.show() JIT.prepareVizData() GlobalUI.showDiv('#infovis') } -}); +}) export default Metamaps From 44a183ed7bd7fdd5fc05b9971f6318e929cd8e63 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 29 Sep 2016 21:32:55 +0000 Subject: [PATCH 119/378] I changed how zoom by mouse-wheel works so that it zooms based on where your mouse pointer is --- frontend/src/patched/JIT.js | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index af7311be..f143fc08 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -2468,16 +2468,36 @@ Extras.Classes.Navigation = new Class({ ans = 1 + scroll * val; // START METAMAPS CODE - if (ans > 1) { - if (5 >= this.canvas.scaleOffsetX) { - this.canvas.scale(ans, ans); - } - } - else if (ans < 1) { - if (this.canvas.scaleOffsetX >= 0.2) { - this.canvas.scale(ans, ans); - } + if (((ans > 1) && (5 >= this.canvas.scaleOffsetX)) || ((ans < 1) && (this.canvas.scaleOffsetX >= 0.2))) { + var s = this.canvas.getSize(), + p = this.canvas.getPos(), + ox = this.canvas.translateOffsetX, + oy = this.canvas.translateOffsetY, + sx = this.canvas.scaleOffsetX, + sy = this.canvas.scaleOffsetY; + + //Basically this is just a duplication of the Util function pixelsToCoords, it finds the canvas coordinate of the mouse pointer + var pointerCoordX = (e.x - p.x - s.width / 2 - ox) * (1 / sx), + pointerCoordY = (e.y - p.y - s.height / 2 - oy) * (1 / sy); + + //This translates the canvas to be centred over the mouse pointer, then the canvas is zoomed as intended. + this.canvas.translate(-pointerCoordX,-pointerCoordY); + this.canvas.scale(ans, ans); + + //Get the canvas attributes again now that is has changed + s = this.canvas.getSize(), + p = this.canvas.getPos(), + ox = this.canvas.translateOffsetX, + oy = this.canvas.translateOffsetY, + sx = this.canvas.scaleOffsetX, + sy = this.canvas.scaleOffsetY; + var newX = (e.x - p.x - s.width / 2 - ox) * (1 / sx), + newY = (e.y - p.y - s.height / 2 - oy) * (1 / sy); + + //Translate the canvas to put the pointer back over top the same coordinate it was over before + this.canvas.translate(newX-pointerCoordX,newY-pointerCoordY); } + // END METAMAPS CODE // ORIGINAL CODE this.canvas.scale(ans, ans); From 816d5adf9490610460b48e10441cced1465187dc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 11:32:58 +0800 Subject: [PATCH 120/378] remove old code from GlobalUI.Search --- frontend/src/Metamaps/GlobalUI/Search.js | 84 +----------------------- 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index 4999e279..7ad1fbc1 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -13,7 +13,6 @@ const Search = { isOpen: false, limitTopicsToMe: false, limitMapsToMe: false, - timeOut: null, changing: false, optionsInitialized: false, init: function () { @@ -28,95 +27,17 @@ const Search = { loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - // bind the hover events - $('.sidebarSearch').hover(function () { - self.open() - }, function () { - self.close(800, false) - }) - $('.sidebarSearchIcon').click(function (e) { $('.sidebarSearchField').focus() }) $('.sidebarSearch').click(function (e) { e.stopPropagation() }) - $('body').click(function (e) { - self.close(0, false) - }) - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true) // true for focus - } - break - case 27: - if (self.isOpen) { - self.close(0, true) - } - break - - default: - break // console.log(e.which) - } - }) self.startTypeahead() }, - lock: function () { - var self = Search - self.locked = true - }, - unlock: function () { - var self = Search - self.locked = false - }, - open: function (focus) { - var self = Search - - clearTimeout(self.timeOut) - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus() - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }) - self.changing = false - self.isOpen = true - }) - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = Search - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { - self.changing = true - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }) - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', '') - $('.sidebarSearchField').blur() - self.changing = false - self.isOpen = false - }) - } - }, closeAfter) + focus: function() { + $('.sidebarSearchField').focus() }, startTypeahead: function () { var self = Search @@ -270,7 +191,6 @@ const Search = { self.hideLoader() if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { - self.close(0, true) if (datum.rtype === 'topic') { Router.topics(datum.id) } else if (datum.rtype === 'map') { From b396b94477307f6b47475ae0adf4851cd4edcb96 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 11:33:39 +0800 Subject: [PATCH 121/378] re-enable Ctrl+/ search box focus shortcut --- frontend/src/Metamaps/Listeners.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 78e881d4..a7f95fdf 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -8,6 +8,7 @@ import Realtime from './Realtime' import Selected from './Selected' import Topic from './Topic' import Visualize from './Visualize' +import { Search } from './GlobalUI' const Listeners = { init: function () { @@ -93,6 +94,11 @@ const Listeners = { }) } break + case 191: // if / is pressed + if (e.ctrlKey) { + Search.focus() + } + break default: // console.log(e.which) break From 7156fab3e29037dc9675be2b1bbc2d2a816203a7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 14:42:07 +0800 Subject: [PATCH 122/378] fix topic controller bugs --- app/controllers/topics_controller.rb | 2 +- app/models/topic.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index f909626a..ce430f43 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -88,7 +88,7 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - alltopics = policy_scope(Topic.relatives(@topic.id)).to_a + alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present? alltopics.delete_if do |topic| !topicsAlreadyHas.index(topic.id.to_s).nil? diff --git a/app/models/topic.rb b/app/models/topic.rb index 62f81cec..7d83ecac 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -36,19 +36,19 @@ class Topic < ApplicationRecord validates_attachment_content_type :audio, content_type: /\Aaudio\/.*\Z/ def synapses - synapses1 + synapses2 + synapses1.or(synapses2) end def relatives - topics1 + topics2 + topics1.or(topics2) end scope :relatives, ->(topic_id = nil, user = nil) { # should only see topics through *visible* synapses # e.g. Topic A (commons) -> synapse (private) -> Topic B (commons) must be filtered out - synapses = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) - synapses += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) - where(id: synapses.uniq) + topic_ids = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) + topic_ids += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) + where(id: topic_ids.uniq) } delegate :name, to: :user, prefix: true From 0e79f2ae4bf602d3728e9effa51fe17271544b7e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 22:31:24 +0800 Subject: [PATCH 123/378] fix tsv --- frontend/src/Metamaps/Import.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 193cdf5e..aa75b382 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -31,13 +31,12 @@ const Import = { cidMappings: {}, // to be filled by import_id => cid mappings handleTSV: function (text) { - var self = Import - results = self.parseTabbedString(text) - self.handle(results) + const results = Import.parseTabbedString(text) + Import.handle(results) }, handleCSV: function (text, parserOpts = {}) { - var self = Import + const self = Import const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi @@ -71,9 +70,8 @@ const Import = { }, handleJSON: function (text) { - var self = Import - results = JSON.parse(text) - self.handle(results) + const results = JSON.parse(text) + Import.handle(results) }, handle: function(results) { From 6ae391265e3911a9e5e6b270a628748b471f18f3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 10:49:03 +0800 Subject: [PATCH 124/378] enable source maps --- webpack.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index fcdcbc04..f94f904a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,3 @@ -const path = require('path') const webpack = require('webpack') const NODE_ENV = process.env.NODE_ENV || 'development' @@ -15,9 +14,12 @@ if (NODE_ENV === 'production') { })) } +const devtool = NODE_ENV === 'production' ? undefined : 'cheap-module-eval-source-map' + const config = module.exports = { context: __dirname, plugins, + devtool, module: { loaders: [ { From 01872e740ea19c4a8b249e9cd73c888d2f66d4f5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:19:38 +0800 Subject: [PATCH 125/378] fix import if there are errors --- frontend/src/Metamaps/Import.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index aa75b382..1307aa45 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -54,13 +54,13 @@ const Import = { const topicsPromise = $.Deferred() parse(topicsText, csv_parser_options, (err, data) => { - if (err) topicsPromise.reject(err) + if (err) return topicsPromise.reject(err) topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) const synapsesPromise = $.Deferred() parse(synapsesText, csv_parser_options, (err, data) => { - if (err) synapsesPromise.reject(err) + if (err) return synapsesPromise.reject(err) synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) From 4328a6205feecf05b37a77281a059ea79a422b2d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:20:31 +0800 Subject: [PATCH 126/378] enable code duplication checks on code climate --- .codeclimate.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 53f90d17..fbd96af2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,11 +5,13 @@ engines: bundler-audit: enabled: true duplication: - enabled: false + enabled: true config: languages: - - ruby - - javascript + ruby: + mass_threshold: 36 # default: 18 + javascript: + mass_threshold: 80 # default: 40 eslint: enabled: true channel: "eslint-3" From e093ca5a30e0587b817911faf575ca8137a29152 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:21:42 +0800 Subject: [PATCH 127/378] more liberally import csv --- frontend/src/Metamaps/PasteInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 13258857..e7029d66 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -63,7 +63,8 @@ const PasteInput = { Import.handleJSON(text) } else if (text.match(/\t/)) { Import.handleTSV(text) - } else if (text.match(/","/)) { + } else { + // just try to see if CSV works Import.handleCSV(text) } }, From 1562d8fcfe63a8076ca0755bfda8c06018ed0678 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:14:38 +0800 Subject: [PATCH 128/378] topics imported with a link get Reference metacode --- frontend/src/Metamaps/Import.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 1307aa45..26a72952 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -40,9 +40,9 @@ const Import = { const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi - let topicsText = text.match(topicsRegex) + let topicsText = text.match(topicsRegex) || "" if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') - let synapsesText = text.match(synapsesRegex) + let synapsesText = text.match(synapsesRegex) || "" if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') // merge default options and extra options passed in parserOpts argument @@ -54,13 +54,19 @@ const Import = { const topicsPromise = $.Deferred() parse(topicsText, csv_parser_options, (err, data) => { - if (err) return topicsPromise.reject(err) + if (err) { + console.warn(err) + return topicsPromise.resolve([]) + } topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) const synapsesPromise = $.Deferred() parse(synapsesText, csv_parser_options, (err, data) => { - if (err) return synapsesPromise.reject(err) + if (err) { + console.warn(err) + return synapsesPromise.resolve([]) + } synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) @@ -240,6 +246,10 @@ const Import = { } } + if (topic.name && topic.link && !topic.metacode) { + topic.metacode = "Reference" + } + self.createTopicWithParameters( topic.name, topic.metacode, topic.permission, topic.desc, topic.link, x, y, topic.id From fdf03ac83af1d4128f59ef5ae40fc51f70969780 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:32:40 +0800 Subject: [PATCH 129/378] source maps! (I think) --- webpack.config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index f94f904a..31adaaa1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ const config = module.exports = { test: /\.(js|jsx)?$/, exclude: /node_modules/, loaders: [ - "babel-loader?cacheDirectory" + "babel-loader?cacheDirectory&retainLines=true" ] } ] @@ -36,6 +36,7 @@ const config = module.exports = { }, output: { path: './app/assets/javascripts/webpacked', - filename: '[name].js' + filename: '[name].js', + devtoolModuleFilenameTemplate: '[absolute-resource-path]' } } From 4949f0dbd63c977b5cb88ac92dbeadce88287f6e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:43:02 +0800 Subject: [PATCH 130/378] eslint and use AutoLayout --- frontend/src/Metamaps/Import.js | 25 +++++++------------------ webpack.config.js | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 26a72952..e5e3e774 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -4,6 +4,7 @@ import parse from 'csv-parse' import _ from 'lodash' import Active from './Active' +import AutoLayout from './AutoLayout' import GlobalUI from './GlobalUI' import Map from './Map' import Synapse from './Synapse' @@ -40,9 +41,9 @@ const Import = { const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi - let topicsText = text.match(topicsRegex) || "" + let topicsText = text.match(topicsRegex) || '' if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') - let synapsesText = text.match(synapsesRegex) || "" + let synapsesText = text.match(synapsesRegex) || '' if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') // merge default options and extra options passed in parserOpts argument @@ -223,31 +224,19 @@ const Import = { importTopics: function (parsedTopics) { var self = Import - // up to 25 topics: scale 100 - // up to 81 topics: scale 200 - // up to 169 topics: scale 300 - var scale = Math.floor((Math.sqrt(parsedTopics.length) - 1) / 4) * 100 - if (scale < 100) scale = 100 - var autoX = -scale - var autoY = -scale - parsedTopics.forEach(function (topic) { var x, y if (topic.x && topic.y) { x = topic.x y = topic.y } else { - x = autoX - y = autoY - autoX += 50 - if (autoX > scale) { - autoY += 50 - autoX = -scale - } + const coords = AutoLayout.getNextCoord() + x = coords.x + y = coords.y } if (topic.name && topic.link && !topic.metacode) { - topic.metacode = "Reference" + topic.metacode = 'Reference' } self.createTopicWithParameters( diff --git a/webpack.config.js b/webpack.config.js index 31adaaa1..91498abd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ const config = module.exports = { test: /\.(js|jsx)?$/, exclude: /node_modules/, loaders: [ - "babel-loader?cacheDirectory&retainLines=true" + 'babel-loader?cacheDirectory&retainLines=true' ] } ] From c5564e02fcceebdfad60c80995d50057aaaedaf6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:47:30 +0800 Subject: [PATCH 131/378] don't needt o open topic card --- frontend/src/Metamaps/PasteInput.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index e7029d66..8166f7c0 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -2,7 +2,6 @@ import AutoLayout from './AutoLayout' import Import from './Import' -import TopicCard from './TopicCard' import Util from './Util' const PasteInput = { @@ -101,10 +100,6 @@ const PasteInput = { topic.set('name', data.title) topic.save() }) - TopicCard.showCard(topic.get('node'), function() { - $('#showcard #titleActivator').click() - .find('textarea, input').focus() - }) } } ) From 20a32afe3b260fa9246c4cf555b2498763443793 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:57:19 +0800 Subject: [PATCH 132/378] integrate handleURL into Import --- frontend/src/Metamaps/Import.js | 78 +++++++++++++++++++++++------ frontend/src/Metamaps/PasteInput.js | 39 +-------------- 2 files changed, 64 insertions(+), 53 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index e5e3e774..2de7ca02 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -59,7 +59,7 @@ const Import = { console.warn(err) return topicsPromise.resolve([]) } - topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) + topicsPromise.resolve(data.map(row => self.normalizeKeys(row))) }) const synapsesPromise = $.Deferred() @@ -68,7 +68,7 @@ const Import = { console.warn(err) return synapsesPromise.resolve([]) } - synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) + synapsesPromise.resolve(data.map(row => self.normalizeKeys(row))) }) $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { @@ -225,23 +225,25 @@ const Import = { var self = Import parsedTopics.forEach(function (topic) { - var x, y - if (topic.x && topic.y) { - x = topic.x - y = topic.y - } else { - const coords = AutoLayout.getNextCoord() - x = coords.x - y = coords.y + let coords = { x: topic.x, y: topic.y } + if (!coords.x || !coords.y) { + coords = AutoLayout.getNextCoord() } - if (topic.name && topic.link && !topic.metacode) { - topic.metacode = 'Reference' + if (!topic.name && topic.link || + topic.name && topic.link && !topic.metacode) { + self.handleURL(topic.link, { + coords, + name: topic.name, + permission: topic.permission, + import_id: topic.id + }) + return // "continue" } self.createTopicWithParameters( topic.name, topic.metacode, topic.permission, - topic.desc, topic.link, x, y, topic.id + topic.desc, topic.link, coords.x, coords.y, topic.id ) }) }, @@ -344,6 +346,47 @@ const Import = { Synapse.renderSynapse(mapping, synapse, node1, node2, true) }, + handleURL: function (url, opts = {}) { + let coords = opts.coords + if (!coords || coords.x === undefined || coords.y === undefined) { + coords = AutoLayout.getNextCoord() + } + + const name = opts.name || 'Link' + const metacode = opts.metacode || 'Reference' + const import_id = opts.import_id || null // don't store a cidMapping + const permission = opts.permission || null // use default + const desc = opts.desc || url + + Import.createTopicWithParameters( + name, + metacode, + permission, + desc, + url, + coords.x, + coords.y, + import_id, + { + success: function(topic) { + if (topic.get('name') !== 'Link') return + $.get('/hacks/load_url_title', { + url + }, function success(data, textStatus) { + var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' + if ($(selector).find('form').length > 0) { + $(selector).find('textarea, input').val(data.title) + } else { + $(selector).html(data.title) + } + topic.set('name', data.title) + topic.save() + }) + } + } + ) + }, + /* * helper functions */ @@ -352,6 +395,7 @@ const Import = { console.error(message) }, + // TODO investigate replacing with es6 (?) trim() simplify: function (string) { return string .replace(/(^\s*|\s*$)/g, '') @@ -360,9 +404,13 @@ const Import = { // thanks to http://stackoverflow.com/a/25290114/5332286 - lowercaseKeys: function(obj) { + normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { - result[key.toLowerCase()] = val + let newKey = key.toLowerCase() + if (newKey === 'url') key = 'link' + if (newKey === 'title') key = 'name' + if (newKey === 'description') key = 'desc' + result[newKey] = val }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 8166f7c0..272ac030 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,6 +1,5 @@ /* global $ */ -import AutoLayout from './AutoLayout' import Import from './Import' import Util from './Util' @@ -57,7 +56,7 @@ const PasteInput = { var self = PasteInput if (text.match(self.URL_REGEX)) { - self.handleURL(text, coords) + Import.handleURL(text, coords) } else if (text[0] === '{') { Import.handleJSON(text) } else if (text.match(/\t/)) { @@ -68,42 +67,6 @@ const PasteInput = { } }, - handleURL: function (text, coords) { - var title = 'Link' - if (!coords || !coords.x || !coords.y) { - coords = AutoLayout.getNextCoord() - } - - var import_id = null // don't store a cidMapping - var permission = null // use default - - Import.createTopicWithParameters( - title, - 'Reference', // metacode - todo fix - permission, - text, // desc - todo load from url? - text, // link - todo fix because this isn't being POSTed - coords.x, - coords.y, - import_id, - { - success: function(topic) { - $.get('/hacks/load_url_title', { - url: text - }, function success(data, textStatus) { - var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' - if ($(selector).find('form').length > 0) { - $(selector).find('textarea, input').val(data.title) - } else { - $(selector).html(data.title) - } - topic.set('name', data.title) - topic.save() - }) - } - } - ) - } } export default PasteInput From bb013787b6eb65298bc466674f0c9dd6df07442e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 13:34:52 +0800 Subject: [PATCH 133/378] make AutoLayout skip over coordinates if there is a mapping at that exact position --- frontend/src/Metamaps/AutoLayout.js | 23 +++++++++++++++++++---- frontend/src/Metamaps/Import.js | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index ee9dc33c..e0835473 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,3 +1,5 @@ +import Active from './Active' + const AutoLayout = { nextX: 0, nextY: 0, @@ -7,7 +9,7 @@ const AutoLayout = { nextYshift: 0, timeToTurn: 0, - getNextCoord: function () { + getNextCoord: function (opts = {}) { var self = AutoLayout var nextX = self.nextX var nextY = self.nextY @@ -49,9 +51,22 @@ const AutoLayout = { } } - return { - x: nextX, - y: nextY + if (opts.map && self.coordsTaken(nextX, nextY, opts.map)) { + // check if the coordinate is already taken on the current map + return self.getNextCoord(opts) + } else { + return { + x: nextX, + y: nextY + } + } + }, + coordsTaken: function(x, y, map) { + const mappings = map.getMappings() + if (mappings.findWhere({ xloc: x, yloc: y })) { + return true + } else { + return false } }, resetSpiral: function () { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 2de7ca02..5d5f91a7 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -227,7 +227,7 @@ const Import = { parsedTopics.forEach(function (topic) { let coords = { x: topic.x, y: topic.y } if (!coords.x || !coords.y) { - coords = AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord({ map: Active.Map }) } if (!topic.name && topic.link || @@ -349,7 +349,7 @@ const Import = { handleURL: function (url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { - coords = AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord({ map: Active.Map }) } const name = opts.name || 'Link' From 8f230736dc11c4ea4158af993fc87216762112d5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 13:47:16 +0800 Subject: [PATCH 134/378] code climate --- frontend/src/Metamaps/AutoLayout.js | 4 +--- frontend/src/Metamaps/PasteInput.js | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index e0835473..f3e91440 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,5 +1,3 @@ -import Active from './Active' - const AutoLayout = { nextX: 0, nextY: 0, @@ -61,7 +59,7 @@ const AutoLayout = { } } }, - coordsTaken: function(x, y, map) { + coordsTaken: function (x, y, map) { const mappings = map.getMappings() if (mappings.findWhere({ xloc: x, yloc: y })) { return true diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 272ac030..bc20ec43 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -65,8 +65,7 @@ const PasteInput = { // just try to see if CSV works Import.handleCSV(text) } - }, - + } } export default PasteInput From ca981898d43213a689e92922b34a6052a138a027 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 00:09:55 +0800 Subject: [PATCH 135/378] arrow key panning - fixes #239 --- frontend/src/Metamaps/Listeners.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index a7f95fdf..2eb092dd 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -24,6 +24,18 @@ const Listeners = { case 27: // if esc key is pressed JIT.escKeyHandler() break + case 37: // if Left arrow key is pressed + Visualize.mGraph.canvas.translate(-20, 0) + break + case 38: // if Up arrow key is pressed + Visualize.mGraph.canvas.translate(0, -20) + break + case 39: // if Right arrow key is pressed + Visualize.mGraph.canvas.translate(20, 0) + break + case 40: // if Down arrow key is pressed + Visualize.mGraph.canvas.translate(0, 20) + break case 65: // if a or A is pressed if (e.ctrlKey) { Control.deselectAllNodes() From bc139608c2f8fe79b6d66dd649e899f810cbb11d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 10:09:13 +0800 Subject: [PATCH 136/378] Search.focus() is the new Search.open() --- app/views/explore/active.html.erb | 3 +-- app/views/explore/featured.html.erb | 3 +-- app/views/explore/mapper.html.erb | 3 +-- app/views/explore/mine.html.erb | 3 +-- app/views/explore/shared.html.erb | 3 +-- app/views/explore/starred.html.erb | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/views/explore/active.html.erb b/app/views/explore/active.html.erb index a70cdcae..0ca442e5 100644 --- a/app/views/explore/active.html.erb +++ b/app/views/explore/active.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Recently Active" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/featured.html.erb b/app/views/explore/featured.html.erb index cfe4a627..806b2fac 100644 --- a/app/views/explore/featured.html.erb +++ b/app/views/explore/featured.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Featured Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/mapper.html.erb b/app/views/explore/mapper.html.erb index 8c5ecf89..68792f47 100644 --- a/app/views/explore/mapper.html.erb +++ b/app/views/explore/mapper.html.erb @@ -14,6 +14,5 @@ <% content_for :mobile_title, @user.name %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/mine.html.erb b/app/views/explore/mine.html.erb index 17f715f9..8e39c296 100644 --- a/app/views/explore/mine.html.erb +++ b/app/views/explore/mine.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "My Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/shared.html.erb b/app/views/explore/shared.html.erb index fd02c810..246498a0 100644 --- a/app/views/explore/shared.html.erb +++ b/app/views/explore/shared.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Shared With Me" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/starred.html.erb b/app/views/explore/starred.html.erb index 4d0e0eb1..825dbf5d 100644 --- a/app/views/explore/starred.html.erb +++ b/app/views/explore/starred.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Starred Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> From b3c7e12d9a8ef5dc29147cc0c27247c53578ecd8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 10:53:35 +0800 Subject: [PATCH 137/378] assets.debug was why assets were loud --- config/environments/development.rb | 5 +++-- config/initializers/assets.rb | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index b1654921..dd6095b2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -Metamaps::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb config.log_level = :info @@ -46,5 +46,6 @@ Metamaps::Application.configure do config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' # Expands the lines which load the assets - config.assets.debug = true + config.assets.debug = false + config.assets.quiet = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 4edab3b6..11f6601e 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# Be sure to restart your server when you modify this file. +Rails.application.configure do + # Version of your assets, change this if you want to expire all your assets. + config.assets.version = '2.0' + config.assets.quiet = true -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '2.0' -Rails.application.config.assets.quiet = true + # Add additional assets to the asset load path + # Rails.application.config.assets.paths << Emoji.images_path -# Add additional assets to the asset load path -# Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w(webpacked/metamaps.bundle.js) + # Precompile additional assets. + # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. + config.assets.precompile += %w(webpacked/metamaps.bundle.js) +end From afa4422608f7e523cad0554a65dc60a30851530c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 18:46:00 +0800 Subject: [PATCH 138/378] Custom formatter for slack exception notifications --- app/controllers/application_controller.rb | 7 +++ config/initializers/exception_notification.rb | 47 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 83889619..5dea17b5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,6 +7,7 @@ class ApplicationController < ActionController::Base protect_from_forgery(with: :exception) before_action :invite_link + before_action :prepare_exception_notifier after_action :allow_embedding def default_serializer_options @@ -82,4 +83,10 @@ class ApplicationController < ActionController::Base # or allow a whitelist # response.headers['X-Frame-Options'] = 'ALLOW-FROM http://blog.metamaps.cc' end + + def prepare_exception_notifier + request.env['exception_notifier.exception_data'] = { + current_user: current_user + } + end end diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index db508b3c..a6c0d2b1 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -1,6 +1,51 @@ # frozen_string_literal: true require 'exception_notification/rails' +module ExceptionNotifier + class MetamapsNotifier < SlackNotifier + def call(exception, options = {}) + # trick original notifier to "ping" self, storing the result + # in @message_opts and then modifying the result + @old_notifier = @notifier + @notifier = self + super + @notifier = @old_notifier + + @message_opts[:attachments][0][:fields] = new_fields(exception, options[:env]) + @message_opts[:attachments][0][:text] = new_text(exception, options[:env]) + + @notifier.ping '', @message_opts + end + + def ping(message, message_opts) + @message = message + @message_opts = message_opts + end + + private + + def new_fields(exception, env) + new_fields = [] + + backtrace = exception.backtrace.reject { |line| line !~ %r{metamaps/(app|config|lib)} } + backtrace = backtrace[0..3] if backtrace.length > 4 + backtrace = "```\n#{backtrace.join("\n")}\n```" + new_fields << { title: 'Backtrace', value: backtrace } + + user = env.dig('exception_notifier.exception_data', :current_user) + new_fields << { title: 'Current User', value: "`#{user.name} <#{user.email}>`" } + + new_fields + end + + def new_text(exception, _env) + text = @message_opts[:attachments][0][:text].chomp + text += ': ' + exception.message + "\n" + text + end + end +end + ExceptionNotification.configure do |config| # Ignore additional exception types. # ActiveRecord::RecordNotFound, AbstractController::ActionNotFound and @@ -20,7 +65,7 @@ ExceptionNotification.configure do |config| # Notifiers ###### if ENV['SLACK_EN_WEBHOOK_URL'] - config.add_notifier :slack, webhook_url: ENV['SLACK_EN_WEBHOOK_URL'] + config.add_notifier :metamaps, webhook_url: ENV['SLACK_EN_WEBHOOK_URL'] end # Email notifier sends notifications by email. From 0f740e751a419c7666972dbeee70e8d9e249b906 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Sun, 2 Oct 2016 17:37:14 -0400 Subject: [PATCH 139/378] topics wasn't in backbone routes --- frontend/src/Metamaps/Create.js | 2 +- frontend/src/Metamaps/Router.js | 5 ++--- frontend/src/Metamaps/Topic.js | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index e18ed1b3..bfb9b94c 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -346,7 +346,7 @@ const Create = { Create.newSynapse.topic1id = 0 Create.newSynapse.topic2id = 0 Mouse.synapseStartCoordinates = [] - Visualize.mGraph.plot() + if (Visualize.mGraph) Visualize.mGraph.plot() }, } } diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 8bcd3590..9ad80187 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -27,7 +27,8 @@ const _Router = Backbone.Router.extend({ '': 'home', // #home 'explore/:section': 'explore', // #explore/active 'explore/:section/:id': 'explore', // #explore/mapper/1234 - 'maps/:id': 'maps' // #maps/7 + 'maps/:id': 'maps', // #maps/7 + 'topics/:id': 'topics' // #topics/7 }, home: function () { let self = this @@ -182,8 +183,6 @@ const _Router = Backbone.Router.extend({ topics: function (id) { clearTimeout(this.timeoutId) - document.title = 'Topic ' + id + ' | Metamaps' - this.currentSection = 'topic' this.currentPage = id diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index c2f3ff29..34e2bb64 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -69,6 +69,8 @@ const Topic = { Metamaps.Synapses = new bb.SynapseCollection(data.synapses) Metamaps.Backbone.attachCollectionEvents() + document.title = Active.Topic.get('name') + ' | Metamaps' + // set filter mapper H3 text $('#filter_by_mapper h3').html('CREATORS') From 87228a9631774be12693e36c6e18bb7a12aba676 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 3 Oct 2016 06:29:35 +0800 Subject: [PATCH 140/378] delete old gems and upgrade aws/paperclip (#676) * remove old gems from gemfile, upgrade aws/paperclip * update paperclip config * upload screenshots as a blob instead of base64 to maps controller --- .example-env | 1 + Gemfile | 26 +++++--------- Gemfile.lock | 54 ++++++++++-------------------- app/controllers/maps_controller.rb | 20 ++--------- app/models/map.rb | 25 -------------- app/policies/map_policy.rb | 4 --- config/application.rb | 12 +++++++ config/environments/development.rb | 11 ------ config/environments/production.rb | 18 ++++------ config/routes.rb | 1 - frontend/src/Metamaps/Map/index.js | 45 ++++++++++++++----------- 11 files changed, 74 insertions(+), 143 deletions(-) diff --git a/.example-env b/.example-env index c2c9a2e9..51d89c5d 100644 --- a/.example-env +++ b/.example-env @@ -14,6 +14,7 @@ export SECRET_KEY_BASE='267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c800 # # you can safely leave these blank, unless you're deploying an instance, in # # which case you'll need to set them up # +# export S3_REGION # export S3_BUCKET_NAME # export AWS_ACCESS_KEY_ID # export AWS_SECRET_ACCESS_KEY diff --git a/Gemfile b/Gemfile index d5b42d83..7f34c12e 100644 --- a/Gemfile +++ b/Gemfile @@ -5,44 +5,34 @@ ruby '2.3.0' gem 'rails', '~> 5.0.0' gem 'active_model_serializers' -gem 'aws-sdk', '< 2.0' +gem 'aws-sdk' gem 'best_in_place' gem 'delayed_job' gem 'delayed_job_active_record' gem 'devise' -gem 'doorkeeper', '~> 4.0.0.rc4' +gem 'doorkeeper' gem 'dotenv-rails' gem 'exception_notification' -gem 'formtastic' -gem 'formula' gem 'httparty' gem 'json' gem 'kaminari' -gem 'paperclip', '~> 4.3.6' +gem 'paperclip' gem 'pg' gem 'pundit' gem 'pundit_extra' -gem 'rack-cors' gem 'rack-attack' +gem 'rack-cors' gem 'redis' gem 'slack-notifier' gem 'snorlax' gem 'uservoice-ruby' +# asset stuff +gem 'coffee-rails' gem 'jquery-rails' gem 'jquery-ui-rails' -gem 'jbuilder' -gem 'rails3-jquery-autocomplete' - -group :assets do - gem 'coffee-rails' - gem 'sass-rails' - gem 'uglifier' -end - -group :production do - gem 'rails_12factor' -end +gem 'sass-rails' +gem 'uglifier' group :test do gem 'factory_girl_rails' diff --git a/Gemfile.lock b/Gemfile.lock index 23d4c827..350585ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,11 +46,12 @@ GEM addressable (2.3.8) arel (7.1.2) ast (2.3.0) - aws-sdk (1.66.0) - aws-sdk-v1 (= 1.66.0) - aws-sdk-v1 (1.66.0) - json (~> 1.4) - nokogiri (>= 1.4.4) + aws-sdk (2.6.3) + aws-sdk-resources (= 2.6.3) + aws-sdk-core (2.6.3) + jmespath (~> 1.0) + aws-sdk-resources (2.6.3) + aws-sdk-core (= 2.6.3) bcrypt (3.1.11) best_in_place (3.1.0) actionpack (>= 3.2) @@ -91,7 +92,7 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - doorkeeper (4.0.0) + doorkeeper (4.2.0) railties (>= 4.2) dotenv (2.1.1) dotenv-rails (2.1.1) @@ -108,18 +109,12 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) - formtastic (3.1.4) - actionpack (>= 3.2.13) - formula (1.1.1) - rails (> 3.0.0) globalid (0.3.7) activesupport (>= 4.1.0) httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.7.0) - jbuilder (2.6.0) - activesupport (>= 3.0.0, < 5.1) - multi_json (~> 1.2) + jmespath (1.3.1) jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -142,10 +137,9 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mimemagic (0.3.0) + mimemagic (0.3.2) mini_portile2 (2.1.0) - minitest (5.9.0) - multi_json (1.12.1) + minitest (5.9.1) multi_xml (0.5.5) nio4r (1.2.1) nokogiri (1.6.8) @@ -153,12 +147,12 @@ GEM pkg-config (~> 1.1.7) oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.7) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) cocaine (~> 0.5.5) mime-types - mimemagic (= 0.3.0) + mimemagic (~> 0.3.0) parser (2.3.1.4) ast (~> 2.2) pg (0.19.0) @@ -199,13 +193,6 @@ GEM nokogiri (~> 1.6.0) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails3-jquery-autocomplete (1.0.15) - rails (>= 3.2) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) railties (5.0.0.1) actionpack (= 5.0.0.1) activesupport (= 5.0.0.1) @@ -290,7 +277,7 @@ PLATFORMS DEPENDENCIES active_model_serializers - aws-sdk (< 2.0) + aws-sdk best_in_place better_errors binding_of_caller @@ -299,20 +286,17 @@ DEPENDENCIES delayed_job delayed_job_active_record devise - doorkeeper (~> 4.0.0.rc4) + doorkeeper dotenv-rails exception_notification factory_girl_rails - formtastic - formula httparty - jbuilder jquery-rails jquery-ui-rails json json-schema kaminari - paperclip (~> 4.3.6) + paperclip pg pry-byebug pry-rails @@ -321,8 +305,6 @@ DEPENDENCIES rack-attack rack-cors rails (~> 5.0.0) - rails3-jquery-autocomplete - rails_12factor redis rspec-rails rubocop @@ -339,4 +321,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.12.5 + 1.13.2 diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 8d4c6e27..cdbbd900 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events, - :screenshot] + before_action :require_user, only: [:create, :update, :destroy, :access, :events] before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export, :screenshot] + :events, :export] after_action :verify_authorized - autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET maps/:id def show respond_to do |format| @@ -136,17 +133,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/upload_screenshot - def screenshot - @map.base64_screenshot(params[:encoded_image]) - - if @map.save - render json: { message: 'Successfully uploaded the map screenshot.' } - else - render json: { message: 'Failed to upload image.' } - end - end - private def set_map @@ -159,7 +145,7 @@ class MapsController < ApplicationController end def update_map_params - params.require(:map).permit(:id, :name, :arranged, :desc, :permission) + params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :screenshot) end def create_topics! diff --git a/app/models/map.rb b/app/models/map.rb index f9fe6312..609b1be4 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -95,21 +95,6 @@ class Map < ApplicationRecord json end - def decode_base64(imgBase64) - decoded_data = Base64.decode64(imgBase64) - - data = StringIO.new(decoded_data) - data.class_eval do - attr_accessor :content_type, :original_filename - end - - data.content_type = 'image/png' - data.original_filename = File.basename('map-' + id.to_s + '-screenshot.png') - - self.screenshot = data - save - end - # user param helps determine what records are visible def contains(user) { @@ -144,14 +129,4 @@ class Map < ApplicationRecord end removed.compact end - - def base64_screenshot(encoded_image) - png = Base64.decode64(encoded_image['data:image/png;base64,'.length..-1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' - data.content_type = 'image/png' - @map.screenshot = data - end - end end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 84d24ca4..9999a055 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -60,8 +60,4 @@ class MapPolicy < ApplicationPolicy def unstar? user.present? end - - def screenshot? - update? - end end diff --git a/config/application.rb b/config/application.rb index 96505b32..9d8870a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -43,5 +43,17 @@ module Metamaps # pundit errors return 403 FORBIDDEN config.action_dispatch.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden + + # S3 file storage + config.paperclip_defaults = { + storage: :s3, + s3_protocol: 'https', + s3_region: ENV['S3_REGION'], + s3_credentials: { + bucket: ENV['S3_BUCKET_NAME'], + access_key_id: ENV['AWS_ACCESS_KEY_ID'], + secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] + } + } end end diff --git a/config/environments/development.rb b/config/environments/development.rb index dd6095b2..5449e5e8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,17 +14,6 @@ Rails.application.configure do config.consider_all_requests_local = true config.action_controller.perform_caching = false - # S3 file storage - config.paperclip_defaults = { - storage: :s3, - s3_credentials: { - bucket: ENV['S3_BUCKET_NAME'], - access_key_id: ENV['AWS_ACCESS_KEY_ID'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] - }, - s3_protocol: 'https' - } - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_SERVER'], diff --git a/config/environments/production.rb b/config/environments/production.rb index f9c94af6..d3f8794e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -5,6 +5,11 @@ Rails.application.configure do config.log_level = :warn config.eager_load = true + # 12 factor: log to stdout + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + # Code is not reloaded between requests config.cache_classes = true @@ -13,24 +18,13 @@ Rails.application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.public_file_server.enabled = false + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false config.assets.js_compressor = :uglifier - # S3 file storage - config.paperclip_defaults = { - storage: :s3, - s3_credentials: { - bucket: ENV['S3_BUCKET_NAME'], - access_key_id: ENV['AWS_ACCESS_KEY_ID'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] - }, - s3_protocol: 'https' - } - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_SERVER'], diff --git a/config/routes.rb b/config/routes.rb index 13b2a5ba..41dd40c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,7 +19,6 @@ Metamaps::Application.routes.draw do get :export post 'events/:event', action: :events get :contains - post :upload_screenshot, action: :screenshot post :access, default: { format: :json } post :star, to: 'stars#create', defaults: { format: :json } post :unstar, to: 'stars#destroy', defaults: { format: :json } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 24ea08ea..387311c2 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import outdent from 'outdent' + import Active from '../Active' import AutoLayout from '../AutoLayout' import Create from '../Create' @@ -323,9 +325,7 @@ const Map = { node.visited = !T }) - var imageData = { - encoded_image: canvas.canvas.toDataURL() - } + var imageData = canvas.canvas.toDataURL() var map = Active.Map @@ -341,24 +341,31 @@ const Map = { } today = mm + '/' + dd + '/' + yyyy - var mapName = map.get('name').split(' ').join([separator = '-']) - var downloadMessage = '' - downloadMessage += 'Captured map screenshot! ' - downloadMessage += "<a href='" + imageData.encoded_image + "' " - downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" + var mapName = map.get('name').split(' ').join(['-']) + const filename = `metamap-${map.id}-${mapName}-${today}.png` + + var downloadMessage = outdent` + Captured map screenshot! + <a href="${imageData.encodedImage}" download="${filename}">DOWNLOAD</a>` GlobalUI.notifyUser(downloadMessage) - $.ajax({ - type: 'POST', - dataType: 'json', - url: '/maps/' + Active.Map.id + '/upload_screenshot', - data: imageData, - success: function (data) { - console.log('successfully uploaded map screenshot') - }, - error: function () { - console.log('failed to save map screenshot') - } + canvas.canvas.toBlob(imageBlob => { + const formData = new window.FormData(); + formData.append('map[screenshot]', imageBlob, filename) + $.ajax({ + type: 'PATCH', + dataType: 'json', + url: `/maps/${map.id}`, + data: formData, + processData: false, + contentType: false, + success: function (data) { + console.log('successfully uploaded map screenshot') + }, + error: function () { + console.log('failed to save map screenshot') + } + }) }) } } From da3795a2c2bee89f68a3edb57e6308ab3561d558 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Sun, 2 Oct 2016 22:49:45 -0400 Subject: [PATCH 141/378] new map improvements (#710) * prehighlight the text for editing when taken to a new map * style --- app/controllers/explore_controller.rb | 2 +- app/controllers/main_controller.rb | 11 +++++------ frontend/src/Metamaps/Map/InfoBox.js | 6 +++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 59045d5d..dc4c2de9 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = map_scope(Map) + @maps = map_scope(Map.where.not(name: 'Untitled Map')) respond_to do |format| format.html do diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0ea9ba97..7df4e366 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -2,18 +2,17 @@ class MainController < ApplicationController before_action :authorize_main after_action :verify_authorized - after_action :verify_policy_scoped, only: [:home] # GET / def home respond_to do |format| format.html do - if !authenticated? - skip_policy_scope - render 'main/home' - else - @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) + if authenticated? + @maps = policy_scope(Map).where.not(name: 'Untitled Map') + .order(updated_at: :desc).page(1).per(20) render 'explore/active' + else + render 'main/home' end end end diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index c36f70c3..ba95df4b 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -38,6 +38,9 @@ const InfoBox = { if (querystring == 'new') { self.open() $('.mapInfoBox').addClass('mapRequestTitle') + $('#mapInfoName').trigger('click') + $('#mapInfoName textarea').focus() + $('#mapInfoName textarea').select() } }, toggleBox: function (event) { @@ -139,7 +142,8 @@ const InfoBox = { // mobile menu $('#header_content').html(name) $('.mapInfoBox').removeClass('mapRequestTitle') - document.title = name + ' | Metamaps' + document.title = `${name} | Metamaps` + window.history.replaceState('', `${name} | Metamaps`, window.location.pathname) }) $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { From a2cde20f8f4ffef04bdf20c39ef8c8156e709b16 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 12:11:58 +0800 Subject: [PATCH 142/378] raml2html with 1.0 syntax working --- doc/api/api.raml | 36 +++++++++++--------------- doc/api/apis/mappings.raml | 4 +-- doc/api/apis/maps.raml | 4 +-- doc/api/apis/synapses.raml | 4 +-- doc/api/apis/tokens.raml | 4 +-- doc/api/apis/topics.raml | 4 +-- doc/api/resourceTypes/collection.raml | 2 +- doc/api/resourceTypes/item.raml | 2 +- doc/api/schemas/_page.json | 1 - doc/api/schemas/error.json | 1 + doc/api/securitySchemes/oauth_2_0.raml | 7 +++++ doc/api/traits/orderable.raml | 2 +- doc/api/traits/pageable.raml | 2 ++ package.json | 3 ++- 14 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 doc/api/schemas/error.json create mode 100644 doc/api/securitySchemes/oauth_2_0.raml diff --git a/doc/api/api.raml b/doc/api/api.raml index d61e66ac..d8a3afc3 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -2,35 +2,29 @@ --- title: Metamaps version: v2 -baseUri: http://metamaps.cc/api/v2 +baseUri: https://metamaps.cc/api/v2 mediaType: application/json securitySchemes: - - oauth_2_0: - description: | - OAuth 2.0 implementation - type: OAuth 2.0 - settings: - authorizationUri: https://metamaps.cc/api/v2/oauth/authorize - accessTokenUri: https://metamaps.cc/api/v2/oauth/token - authorizationGrants: [ authorization_code, password, client_credentials, implicit, refresh_token ] + oauth_2_0: !include securitySchemes/oauth_2_0.raml +securedBy: [ oauth_2_0 ] traits: - - pageable: !include traits/pageable.raml - - orderable: !include traits/orderable.raml - - searchable: !include traits/searchable.raml + pageable: !include traits/pageable.raml + orderable: !include traits/orderable.raml + searchable: !include traits/searchable.raml schemas: - - topic: !include schemas/_topic.json - - synapse: !include schemas/_synapse.json - - map: !include schemas/_map.json - - mapping: !include schemas/_mapping.json - - token: !include schemas/_token.json + topic: !include schemas/_topic.json + synapse: !include schemas/_synapse.json + map: !include schemas/_map.json + mapping: !include schemas/_mapping.json + token: !include schemas/_token.json -resourceTypes: - - base: !include resourceTypes/base.raml - - item: !include resourceTypes/item.raml - - collection: !include resourceTypes/collection.raml +#resourceTypes: +# base: !include resourceTypes/base.raml +# item: !include resourceTypes/item.raml +# collection: !include resourceTypes/collection.raml /topics: !include apis/topics.raml /synapses: !include apis/synapses.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index 8b72b4df..fad67fd2 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -25,7 +25,7 @@ post: application/json: example: !include ../examples/mapping.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index c5499a33..8c2c2825 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -27,7 +27,7 @@ post: application/json: example: !include ../examples/map.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index 3169c712..3fb1eee1 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -27,7 +27,7 @@ post: application/json: example: !include ../examples/synapse.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index 9f471615..b9c3aaff 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection post: body: application/json: @@ -18,7 +18,7 @@ post: application/json: example: !include ../examples/tokens.json /{id}: - type: item + #type: item delete: responses: 204: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 7c214dd2..07eb8886 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -25,7 +25,7 @@ post: application/json: example: !include ../examples/topic.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/resourceTypes/collection.raml b/doc/api/resourceTypes/collection.raml index d54e6c0c..e2710dae 100644 --- a/doc/api/resourceTypes/collection.raml +++ b/doc/api/resourceTypes/collection.raml @@ -16,7 +16,7 @@ get?: post?: description: Create a new <<resourcePathName | !singularize>> responses: - 200: + 201: body: application/json: schema: <<resourcePathName | !singularize>> diff --git a/doc/api/resourceTypes/item.raml b/doc/api/resourceTypes/item.raml index 1abf040e..5c227d61 100644 --- a/doc/api/resourceTypes/item.raml +++ b/doc/api/resourceTypes/item.raml @@ -1,3 +1,4 @@ +type: base get?: description: Get a <<resourcePathName | !singularize>> responses: @@ -26,4 +27,3 @@ delete?: responses: 204: description: Removed -type: base diff --git a/doc/api/schemas/_page.json b/doc/api/schemas/_page.json index 635f0286..47f69d95 100644 --- a/doc/api/schemas/_page.json +++ b/doc/api/schemas/_page.json @@ -35,4 +35,3 @@ "per" ] } - diff --git a/doc/api/schemas/error.json b/doc/api/schemas/error.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/doc/api/schemas/error.json @@ -0,0 +1 @@ +{} diff --git a/doc/api/securitySchemes/oauth_2_0.raml b/doc/api/securitySchemes/oauth_2_0.raml new file mode 100644 index 00000000..b271e03a --- /dev/null +++ b/doc/api/securitySchemes/oauth_2_0.raml @@ -0,0 +1,7 @@ +description: | + OAuth 2.0 implementation +type: OAuth 2.0 +settings: + authorizationUri: https://metamaps.cc/api/v2/oauth/authorize + accessTokenUri: https://metamaps.cc/api/v2/oauth/token + authorizationGrants: [ authorization_code, client_credentials ] diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml index 708736ab..a2b45ce9 100644 --- a/doc/api/traits/orderable.raml +++ b/doc/api/traits/orderable.raml @@ -1,3 +1,3 @@ queryParameters: sort: - description: The name of the field to sort by, prefixed by "-" to sort descending + description: The name of the comma-separated fields to sort by, prefixed by "-" to sort descending diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml index 88165861..31fcb9a8 100644 --- a/doc/api/traits/pageable.raml +++ b/doc/api/traits/pageable.raml @@ -2,6 +2,8 @@ queryParameters: page: description: The page number type: integer + default: 1 per: description: Number of records per page type: integer + default: 20 diff --git a/package.json b/package.json index 106294fd..12f42e6c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "chai": "^3.5.0", "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", - "mocha": "^3.0.2" + "mocha": "^3.0.2", + "raml2html": "^4.0.0-beta5" } } From 3d7a2ef5b1ab678b7dcbcd11060ca9d93297e4a5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 13:51:07 +0800 Subject: [PATCH 143/378] make raml traits work and be accurate/useful --- app/controllers/api/v2/mappings_controller.rb | 3 +++ app/controllers/api/v2/tokens_controller.rb | 4 ++++ app/controllers/api/v2/topics_controller.rb | 3 +++ doc/api/api.raml | 1 + doc/api/apis/mappings.raml | 2 ++ doc/api/apis/maps.raml | 2 ++ doc/api/apis/synapses.raml | 2 ++ doc/api/apis/tokens.raml | 1 + doc/api/apis/topics.raml | 5 ++++- doc/api/examples/topic.json | 6 +++--- doc/api/examples/topics.json | 6 +++--- doc/api/traits/embeddable.raml | 8 ++++++++ doc/api/traits/orderable.raml | 2 ++ doc/api/traits/pageable.raml | 4 +++- doc/api/traits/searchable.raml | 4 +++- 15 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 doc/api/traits/embeddable.raml diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 86aba865..4490e4af 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -2,6 +2,9 @@ module Api module V2 class MappingsController < RestfulController + def searchable_columns + [] + end end end end diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index d1a6b255..291c33d4 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -2,6 +2,10 @@ module Api module V2 class TokensController < RestfulController + def searchable_columns + [:description] + end + def my_tokens authorize resource_class instantiate_collection diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb index 22e534ce..b47dc8a0 100644 --- a/app/controllers/api/v2/topics_controller.rb +++ b/app/controllers/api/v2/topics_controller.rb @@ -2,6 +2,9 @@ module Api module V2 class TopicsController < RestfulController + def searchable_columns + [:name, :desc, :link] + end end end end diff --git a/doc/api/api.raml b/doc/api/api.raml index d8a3afc3..e59ae8d3 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -11,6 +11,7 @@ securedBy: [ oauth_2_0 ] traits: pageable: !include traits/pageable.raml + embeddable: !include traits/embeddable.raml orderable: !include traits/orderable.raml searchable: !include traits/searchable.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index fad67fd2..9d0be18b 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ embeddable: { embedFields: "user,map" }, orderable, pageable ] responses: 200: body: @@ -27,6 +28,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,map" } ] responses: 200: body: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 8c2c2825..3cc7d13c 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ] responses: 200: body: @@ -29,6 +30,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" } ] responses: 200: body: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index 3fb1eee1..cfd2f762 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "desc" }, embeddable: { embedFields: "topic1,topic2,user" }, orderable, pageable ] responses: 200: body: @@ -29,6 +30,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "topic1,topic2,user" } ] responses: 200: body: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index b9c3aaff..70b69765 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -12,6 +12,7 @@ post: example: !include ../examples/token.json /my_tokens: get: + is: [ searchable: { searchFields: description }, pageable, orderable ] responses: 200: body: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 07eb8886..09706754 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "name, desc, link" }, embeddable: { embedFields: "user,metacode" }, orderable, pageable ] responses: 200: body: @@ -14,7 +15,8 @@ post: desc: description: description link: - description: (optional) link to content on the web + description: link to content on the web + required: false permission: description: commons, public, or private metacode_id: @@ -27,6 +29,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,metacode" } ] responses: 200: body: diff --git a/doc/api/examples/topic.json b/doc/api/examples/topic.json index 90e702a2..d65eced1 100644 --- a/doc/api/examples/topic.json +++ b/doc/api/examples/topic.json @@ -1,9 +1,9 @@ { "data": { "id": 670, - "name": "Junto feedback and enhancements map", - "desc": "", - "link": "", + "name": "Metamaps.cc Website", + "desc": "Metamaps is a great website; check it out below!", + "link": "https://metamaps.cc", "permission": "commons", "created_at": "2016-07-02T09:23:30.397Z", "updated_at": "2016-07-02T09:23:30.397Z", diff --git a/doc/api/examples/topics.json b/doc/api/examples/topics.json index d4eba53e..5553c9e5 100644 --- a/doc/api/examples/topics.json +++ b/doc/api/examples/topics.json @@ -2,9 +2,9 @@ "data": [ { "id": 670, - "name": "Junto feedback and enhancements map", - "desc": "", - "link": "", + "name": "Metamaps.cc Website", + "desc": "Metamaps is a great website; check it out below!", + "link": "https://metamaps.cc", "permission": "commons", "created_at": "2016-07-02T09:23:30.397Z", "updated_at": "2016-07-02T09:23:30.397Z", diff --git a/doc/api/traits/embeddable.raml b/doc/api/traits/embeddable.raml new file mode 100644 index 00000000..e9eb61db --- /dev/null +++ b/doc/api/traits/embeddable.raml @@ -0,0 +1,8 @@ +queryParameters: + embed: + description: | + Comma-separated list of columns to embed. Each embedded column will be returned instead of the corresponding <code>field_id</code> or <code>field_ids</code> column. For instance, <code>?embed=user</code> would remove the <code>user_id</code> integer field from a response and replace it with a <code>user</code> object field. + + Possible embeddable fields are: <pre><< embedFields >></pre> + required: false + type: string diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml index a2b45ce9..25baa756 100644 --- a/doc/api/traits/orderable.raml +++ b/doc/api/traits/orderable.raml @@ -1,3 +1,5 @@ queryParameters: sort: description: The name of the comma-separated fields to sort by, prefixed by "-" to sort descending + required: false + type: string diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml index 31fcb9a8..cfb6810d 100644 --- a/doc/api/traits/pageable.raml +++ b/doc/api/traits/pageable.raml @@ -2,8 +2,10 @@ queryParameters: page: description: The page number type: integer + required: false default: 1 per: description: Number of records per page type: integer - default: 20 + required: false + default: 25 diff --git a/doc/api/traits/searchable.raml b/doc/api/traits/searchable.raml index 53ae8525..fb7700a9 100644 --- a/doc/api/traits/searchable.raml +++ b/doc/api/traits/searchable.raml @@ -1,4 +1,6 @@ queryParameters: q: - description: The search string to query by + description: | + Search text columns for this string. A query of <code>"example"</code> will be passed to SQL as <code>LIKE %example%</code>. The searchable columns are: <pre><< searchFields >></pre> + required: false type: string From 2466a0912fb54a534da864c850a0f1bb85dcb0b4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 14:02:07 +0800 Subject: [PATCH 144/378] raml2html build script --- .gitignore | 1 + bin/build-apidocs.sh | 5 +++++ doc/production/first-deploy.md | 9 ++++++++- doc/production/pull-changes.md | 1 + package.json | 4 ++-- 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100755 bin/build-apidocs.sh diff --git a/.gitignore b/.gitignore index 7f17330b..df92a1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ #assety stuff public/assets public/metamaps_mobile +public/api/index.html vendor/ node_modules npm-debug.log diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh new file mode 100755 index 00000000..677bbffd --- /dev/null +++ b/bin/build-apidocs.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Note: you need to run `npm install` before using this script or raml2html won't be installed + +./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md index cc3a1f4a..b118a2b5 100644 --- a/doc/production/first-deploy.md +++ b/doc/production/first-deploy.md @@ -65,7 +65,14 @@ Run this in the metamaps directory, still as metamaps: sudo aptitude install nodejs npm sudo ln -s /usr/bin/nodejs /usr/bin/node npm install - npm run build + +#### Precompile assets + +This step depends on running npm install first; assets:precompile will run `NODE_ENV=production npm run build`, and the build-apidocs.sh script requires the raml2html npm package. + + rake assets:precompile + rake perms:fix + bin/build-apidocs.sh #### Nginx and SSL diff --git a/doc/production/pull-changes.md b/doc/production/pull-changes.md index 30f41cf5..1bd1ebbe 100644 --- a/doc/production/pull-changes.md +++ b/doc/production/pull-changes.md @@ -26,6 +26,7 @@ Now that you have the code, run these commands: npm install rake db:migrate rake assets:precompile # includes `npm run build` + bin/build-apidocs.sh rake perms:fix passenger-config restart-app . diff --git a/package.json b/package.json index 12f42e6c..751ef284 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", + "raml2html": "^4.0.0-beta5", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", @@ -41,7 +42,6 @@ "chai": "^3.5.0", "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", - "mocha": "^3.0.2", - "raml2html": "^4.0.0-beta5" + "mocha": "^3.0.2" } } From 8ac8aad105a91a66f96a9a895150e47175f936e2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 15:30:06 +0800 Subject: [PATCH 145/378] PUT and PATCH parameters are optional --- doc/api/apis/mappings.raml | 25 +++++++++++++++++++++++-- doc/api/apis/maps.raml | 12 ++++++++++++ doc/api/apis/synapses.raml | 28 ++++++++++++++++++++++------ doc/api/apis/tokens.raml | 1 + doc/api/apis/topics.raml | 32 +++++++++++++++++++++++--------- 5 files changed, 81 insertions(+), 17 deletions(-) diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index 9d0be18b..a1643c86 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -17,9 +17,11 @@ post: map_id: description: id of the map xloc: - description: (for Topic mappings only) x location on the canvas + description: (only for Topic mappings) x location on the canvas + required: false yloc: - description: (for Topic mappings only) y location on the canvas + description: (only for Topic mappings) y location on the canvas + required: false responses: 201: body: @@ -40,10 +42,20 @@ post: properties: mappable_id: description: id of the topic/synapse to be mapped + required: false mappable_type: description: Topic or Synapse + required: false map_id: description: id of the map + required: false + xloc: + description: (only for Topic mappings) x location on the canvas + required: false + yloc: + description: (only for Topic mappings) y location on the canvas + required: false + responses: 200: body: @@ -55,10 +67,19 @@ post: properties: mappable_id: description: id of the topic/synapse to be mapped + required: false mappable_type: description: Topic or Synapse + required: false map_id: description: id of the map + required: false + xloc: + description: (only for Topic mappings) x location on the canvas + required: false + yloc: + description: (only for Topic mappings) y location on the canvas + required: false responses: 200: body: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 3cc7d13c..b742adce 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -42,16 +42,22 @@ post: properties: name: description: name + required: false desc: description: description + required: false permission: description: commons, public, or private + required: false screenshot: description: url to a screenshot of the map + required: false contributor_ids: description: the topic being linked from + required: false collaborator_ids: description: the topic being linked to + required: false responses: 200: body: @@ -63,16 +69,22 @@ post: properties: name: description: name + required: false desc: description: description + required: false permission: description: commons, public, or private + required: false screenshot: description: url to a screenshot of the map + required: false contributor_ids: description: the topic being linked from + required: false collaborator_ids: description: the topic being linked to + required: false responses: 200: body: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index cfd2f762..dabcdad7 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -11,9 +11,11 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from to or both + description: | + <code>from-to</code> or <code>both</code> permission: description: commons, public, or private topic1_id: @@ -41,17 +43,24 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from-to or both + description: | + <code>from-to</code> or <code>both</code> + required: false permission: description: commons, public, or private + required: false topic1_id: description: the topic being linked from + required: false topic2_id: description: the topic being linked to + required: false user_id: description: the creator of the topic + required: false responses: 200: body: @@ -62,17 +71,24 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from-to or both + description: | + <code>from-to</code> or <code>both</code> + required: false permission: description: commons, public, or private + required: false topic1_id: description: the topic being linked from + required: false topic2_id: description: the topic being linked to + required: false user_id: description: the creator of the topic + required: false responses: 200: body: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index 70b69765..ef7a8379 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -5,6 +5,7 @@ post: properties: description: description: short string describing this token + required: false responses: 201: body: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 09706754..15b94da4 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -11,11 +11,11 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map desc: - description: description + description: Longer topic description visible when opening a map card link: - description: link to content on the web + description: embed a link to content on the web in the topic card required: false permission: description: commons, public, or private @@ -40,13 +40,20 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map + required: false desc: - description: description + description: Longer topic description visible when opening a map card + required: false link: - description: (optional) link to content on the web + description: embed a link to content on the web in the topic card + required: false permission: description: commons, public, or private + required: false + metacode_id: + description: Topic's metacode + required: false responses: 200: body: @@ -57,13 +64,20 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map + required: false desc: - description: description + description: Longer topic description visible when opening a map card + required: false link: - description: (optional) link to content on the web + description: embed a link to content on the web in the topic card + required: false permission: description: commons, public, or private + required: false + metacode_id: + description: Topic's metacode + required: false responses: 200: body: From 8afef1bc4a66357ec11daa6af5dab94125c40089 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:02:58 +0800 Subject: [PATCH 146/378] make tokens description field optional --- app/controllers/api/v2/tokens_controller.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 291c33d4..1170945f 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -6,6 +6,19 @@ module Api [:description] end + def create + if params[:token].blank? + self.resource = resource_class.new + else + instantiate_resource + end + + resource.user = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + def my_tokens authorize resource_class instantiate_collection From 15b8440fbcaf4d51f8b98011b01fe8dcb030d9e3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:21:09 +0800 Subject: [PATCH 147/378] move raml2html to optional dependencies so it can be installed globally --- .travis.yml | 2 +- bin/build-apidocs.sh | 4 ++++ package.json | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37186702..99b9a655 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_script: - . $HOME/.nvm/nvm.sh - nvm install stable - nvm use stable - - npm install + - npm install --no-optional script: - bundle exec rspec && bundle exec brakeman -q -z && npm test addons: diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh index 677bbffd..be85012c 100755 --- a/bin/build-apidocs.sh +++ b/bin/build-apidocs.sh @@ -2,4 +2,8 @@ # Note: you need to run `npm install` before using this script or raml2html won't be installed +if [[ ! -x ./node_modules/.bin/raml2html ]]; then + npm install +fi + ./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html diff --git a/package.json b/package.json index 751ef284..a976e5f1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", - "raml2html": "^4.0.0-beta5", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", @@ -43,5 +42,8 @@ "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", "mocha": "^3.0.2" + }, + "optionalDependencies": { + "raml2html": "4.0.0-beta5" } } From a9831946d08e97b801852ce9222feda08b65729f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:31:07 +0800 Subject: [PATCH 148/378] ensure public/api directory exists --- public/api/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/api/.keep diff --git a/public/api/.keep b/public/api/.keep new file mode 100644 index 00000000..e69de29b From c90460802e773ef78ea6534c869fb604c7091898 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:55:31 +0800 Subject: [PATCH 149/378] enable heroku to serve apidocs --- lib/tasks/extensions.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index 776c81e3..6efd9026 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -3,6 +3,7 @@ namespace :assets do task :js_compile do system 'npm install' system 'npm run build' + system 'bin/build-apidocs.sh' if ENV['MAILER_DEFAULT_URL'] == 'metamaps.herokuapp.com' end end From 2eae89a6b7c7d46069e839013ac4bb7faa0f4fc9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 22:24:47 +0800 Subject: [PATCH 150/378] users and metacodes api endpoints --- .../api/v2/metacodes_controller.rb | 10 +++++ app/controllers/api/v2/restful_controller.rb | 3 +- app/controllers/api/v2/users_controller.rb | 24 +++++++++++ app/policies/metacode_policy.rb | 27 ++++++++++++ app/policies/user_policy.rb | 41 +++++++++++++++++++ app/serializers/api/v2/metacode_serializer.rb | 3 +- app/serializers/api/v2/user_serializer.rb | 4 +- config/routes.rb | 8 +++- doc/api/api.raml | 12 ++++-- doc/api/apis/metacodes.raml | 16 ++++++++ doc/api/apis/users.raml | 24 +++++++++++ doc/api/examples/metacode.json | 8 ++++ doc/api/examples/metacodes.json | 30 ++++++++++++++ doc/api/examples/user.json | 9 ++++ doc/api/examples/users.json | 24 +++++++++++ doc/api/schemas/_metacode.json | 24 +++++++++++ doc/api/schemas/_user.json | 28 +++++++++++++ doc/api/schemas/metacode.json | 12 ++++++ doc/api/schemas/metacodes.json | 19 +++++++++ doc/api/schemas/user.json | 12 ++++++ doc/api/schemas/users.json | 19 +++++++++ spec/api/v2/metacodes_api_spec.rb | 25 +++++++++++ spec/api/v2/users_api_spec.rb | 33 +++++++++++++++ 23 files changed, 405 insertions(+), 10 deletions(-) create mode 100644 app/controllers/api/v2/metacodes_controller.rb create mode 100644 app/controllers/api/v2/users_controller.rb create mode 100644 app/policies/metacode_policy.rb create mode 100644 app/policies/user_policy.rb create mode 100644 doc/api/apis/metacodes.raml create mode 100644 doc/api/apis/users.raml create mode 100644 doc/api/examples/metacode.json create mode 100644 doc/api/examples/metacodes.json create mode 100644 doc/api/examples/user.json create mode 100644 doc/api/examples/users.json create mode 100644 doc/api/schemas/_metacode.json create mode 100644 doc/api/schemas/_user.json create mode 100644 doc/api/schemas/metacode.json create mode 100644 doc/api/schemas/metacodes.json create mode 100644 doc/api/schemas/user.json create mode 100644 doc/api/schemas/users.json create mode 100644 spec/api/v2/metacodes_api_spec.rb create mode 100644 spec/api/v2/users_api_spec.rb diff --git a/app/controllers/api/v2/metacodes_controller.rb b/app/controllers/api/v2/metacodes_controller.rb new file mode 100644 index 00000000..71cd4a50 --- /dev/null +++ b/app/controllers/api/v2/metacodes_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Api + module V2 + class MetacodesController < RestfulController + def searchable_columns + [:name] + end + end + end +end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index f837957d..5d8f81b3 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -70,7 +70,8 @@ module Api def default_scope { - embeds: embeds + embeds: embeds, + current_user: current_user } end diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb new file mode 100644 index 00000000..9eba232f --- /dev/null +++ b/app/controllers/api/v2/users_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +module Api + module V2 + class UsersController < RestfulController + def current + @user = current_user + authorize @user + return show + end + + private + + def searchable_columns + [:name] + end + + # only ask serializer to return is_admin field if we're on the + # current_user action + def default_scope + super.merge(show_is_admin: action_name == 'current') + end + end + end +end diff --git a/app/policies/metacode_policy.rb b/app/policies/metacode_policy.rb new file mode 100644 index 00000000..e8787f8d --- /dev/null +++ b/app/policies/metacode_policy.rb @@ -0,0 +1,27 @@ +class MetacodePolicy < ApplicationPolicy + def index? + true + end + + def show? + true + end + + def create? + user.is_admin + end + + def update? + user.is_admin + end + + def destroy? + false + end + + class Scope < Scope + def resolve + scope.all + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..fa6158b8 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,41 @@ +class UserPolicy < ApplicationPolicy + def index? + user.present? + end + + def show? + user.present? + end + + def create? + fail 'Create should be handled by Devise' + end + + def update? + user == record + end + + def destroy? + false + end + + def details? + show? + end + + def updatemetacodes? + update? + end + + # API action + def current? + user == record + end + + class Scope < Scope + def resolve + return scope.all if user.present? + scope.none + end + end +end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb index 16013e33..0b1ac553 100644 --- a/app/serializers/api/v2/metacode_serializer.rb +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -4,9 +4,8 @@ module Api class MetacodeSerializer < ApplicationSerializer attributes :id, :name, - :manual_icon, :color, - :aws_icon + :icon end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index ec58775d..3234205e 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -5,9 +5,11 @@ module Api attributes :id, :name, :avatar, - :is_admin, :generation + attribute :is_admin, + if: -> { scope[:show_is_admin] && scope[:current_user] == object } + def avatar object.image.url(:sixtyfour) end diff --git a/config/routes.rb b/config/routes.rb index 41dd40c4..76158105 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,13 +63,17 @@ Metamaps::Application.routes.draw do namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do + resources :metacodes, only: [:index, :show] + resources :mappings, only: [:index, :create, :show, :update, :destroy] resources :maps, only: [:index, :create, :show, :update, :destroy] resources :synapses, only: [:index, :create, :show, :update, :destroy] - resources :topics, only: [:index, :create, :show, :update, :destroy] - resources :mappings, only: [:index, :create, :show, :update, :destroy] resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection end + resources :topics, only: [:index, :create, :show, :update, :destroy] + resources :users, only: [:index, :show] do + get :current, on: :collection + end end namespace :v1, path: '/v1' do # api v1 routes all lead to a deprecation error method diff --git a/doc/api/api.raml b/doc/api/api.raml index e59ae8d3..624e5a46 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -16,19 +16,23 @@ traits: searchable: !include traits/searchable.raml schemas: - topic: !include schemas/_topic.json - synapse: !include schemas/_synapse.json map: !include schemas/_map.json mapping: !include schemas/_mapping.json + metacode: !include schemas/_metacode.json + synapse: !include schemas/_synapse.json token: !include schemas/_token.json + topic: !include schemas/_topic.json + user: !include schemas/_user.json #resourceTypes: # base: !include resourceTypes/base.raml # item: !include resourceTypes/item.raml # collection: !include resourceTypes/collection.raml -/topics: !include apis/topics.raml -/synapses: !include apis/synapses.raml /maps: !include apis/maps.raml /mappings: !include apis/mappings.raml +/metacodes: !include api/metacodes.raml +/synapses: !include apis/synapses.raml /tokens: !include apis/tokens.raml +/topics: !include apis/topics.raml +/users: !include apis/users.raml diff --git a/doc/api/apis/metacodes.raml b/doc/api/apis/metacodes.raml new file mode 100644 index 00000000..37cbd17a --- /dev/null +++ b/doc/api/apis/metacodes.raml @@ -0,0 +1,16 @@ +#type: collection +get: + is: [ searchable: { searchFields: "name" }, orderable, pageable ] + responses: + 200: + body: + application/json: + example: !include ../examples/metacodes.json +/{id}: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/metacode.json diff --git a/doc/api/apis/users.raml b/doc/api/apis/users.raml new file mode 100644 index 00000000..7f421059 --- /dev/null +++ b/doc/api/apis/users.raml @@ -0,0 +1,24 @@ +#type: collection +get: + is: [ searchable: { searchFields: "name" }, orderable, pageable ] + responses: + 200: + body: + application/json: + example: !include ../examples/users.json +/current: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/current_user.json +/{id}: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/user.json diff --git a/doc/api/examples/metacode.json b/doc/api/examples/metacode.json new file mode 100644 index 00000000..506a10c0 --- /dev/null +++ b/doc/api/examples/metacode.json @@ -0,0 +1,8 @@ +{ + "data": { + "id": 1, + "name": "Action", + "color": "#BD6C85", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png" + } +} diff --git a/doc/api/examples/metacodes.json b/doc/api/examples/metacodes.json new file mode 100644 index 00000000..8e06f56c --- /dev/null +++ b/doc/api/examples/metacodes.json @@ -0,0 +1,30 @@ +{ + "data": [ + { + "id": 1, + "name": "Action", + "color": "#BD6C85", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png" + }, + { + "id": 2, + "name": "Activity", + "color": "#6EBF65", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_activity.png" + }, + { + "id": 3, + "name": "Catalyst", + "color": "#EF8964", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_catalyst.png" + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 16, + "total_count": 47, + "per": 3 + } +} diff --git a/doc/api/examples/user.json b/doc/api/examples/user.json new file mode 100644 index 00000000..83476006 --- /dev/null +++ b/doc/api/examples/user.json @@ -0,0 +1,9 @@ +{ + "data": { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0, + "is_admin": false + } +} diff --git a/doc/api/examples/users.json b/doc/api/examples/users.json new file mode 100644 index 00000000..944f631e --- /dev/null +++ b/doc/api/examples/users.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0 + }, + { + "id": 2, + "name": "admin", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0 + } + ], + "page": { + "current_page": 1, + "next_page": 0, + "prev_page": 0, + "total_pages": 1, + "total_count": 2, + "per": 25 + } +} diff --git a/doc/api/schemas/_metacode.json b/doc/api/schemas/_metacode.json new file mode 100644 index 00000000..cc6b4f76 --- /dev/null +++ b/doc/api/schemas/_metacode.json @@ -0,0 +1,24 @@ +{ + "name": "Metacode", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "icon": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "color", + "icon" + ] +} diff --git a/doc/api/schemas/_user.json b/doc/api/schemas/_user.json new file mode 100644 index 00000000..e5805251 --- /dev/null +++ b/doc/api/schemas/_user.json @@ -0,0 +1,28 @@ +{ + "name": "User", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "generation": { + "type": "integer", + "minimum": 0 + }, + "is_admin": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "avatar", + "generation" + ] +} diff --git a/doc/api/schemas/metacode.json b/doc/api/schemas/metacode.json new file mode 100644 index 00000000..c5fa7106 --- /dev/null +++ b/doc/api/schemas/metacode.json @@ -0,0 +1,12 @@ +{ + "name": "Metacode Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_metacode.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/metacodes.json b/doc/api/schemas/metacodes.json new file mode 100644 index 00000000..c3869366 --- /dev/null +++ b/doc/api/schemas/metacodes.json @@ -0,0 +1,19 @@ +{ + "name": "Metacodes", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_metacode.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/user.json b/doc/api/schemas/user.json new file mode 100644 index 00000000..a5c9d490 --- /dev/null +++ b/doc/api/schemas/user.json @@ -0,0 +1,12 @@ +{ + "name": "User Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_user.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/users.json b/doc/api/schemas/users.json new file mode 100644 index 00000000..6cae2b80 --- /dev/null +++ b/doc/api/schemas/users.json @@ -0,0 +1,19 @@ +{ + "name": "Users", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_user.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/spec/api/v2/metacodes_api_spec.rb b/spec/api/v2/metacodes_api_spec.rb new file mode 100644 index 00000000..67d3d543 --- /dev/null +++ b/spec/api/v2/metacodes_api_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'metacodes API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:metacode) { create(:metacode) } + + it 'GET /api/v2/metacodes' do + create_list(:metacode, 5) + get '/api/v2/metacodes', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:metacodes) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/metacodes/:id' do + get "/api/v2/metacodes/#{metacode.id}", params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:metacode) + expect(JSON.parse(response.body)['data']['id']).to eq metacode.id + end +end diff --git a/spec/api/v2/users_api_spec.rb b/spec/api/v2/users_api_spec.rb new file mode 100644 index 00000000..70e22c2f --- /dev/null +++ b/spec/api/v2/users_api_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'users API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:record) { create(:user) } + + it 'GET /api/v2/users' do + create_list(:user, 5) + get '/api/v2/users', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:users) + expect(JSON.parse(response.body)['data'].count).to eq 6 + end + + it 'GET /api/v2/users/:id' do + get "/api/v2/users/#{record.id}", params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:user) + expect(JSON.parse(response.body)['data']['id']).to eq record.id + end + + it 'GET /api/v2/users/current' do + get '/api/v2/users/current', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:user) + expect(JSON.parse(response.body)['data']['id']).to eq user.id + end +end From df29e48d8c7e86a3193b942ff81987458f1d3486 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 22:51:21 +0800 Subject: [PATCH 151/378] rubocop + allow unauthed users to see all users --- app/controllers/api/v2/users_controller.rb | 4 ++-- app/policies/metacode_policy.rb | 1 + app/policies/user_policy.rb | 10 +++++----- app/serializers/api/v2/user_serializer.rb | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 9eba232f..b4b83e3f 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -5,9 +5,9 @@ module Api def current @user = current_user authorize @user - return show + show # delegate to the normal show function end - + private def searchable_columns diff --git a/app/policies/metacode_policy.rb b/app/policies/metacode_policy.rb index e8787f8d..626d23e3 100644 --- a/app/policies/metacode_policy.rb +++ b/app/policies/metacode_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodePolicy < ApplicationPolicy def index? true diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index fa6158b8..943200e8 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true class UserPolicy < ApplicationPolicy def index? - user.present? + true end def show? - user.present? + true end def create? - fail 'Create should be handled by Devise' + raise 'Create should be handled by Devise' end def update? @@ -34,8 +35,7 @@ class UserPolicy < ApplicationPolicy class Scope < Scope def resolve - return scope.all if user.present? - scope.none + scope.all end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index 3234205e..c3b0c3fe 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -8,7 +8,7 @@ module Api :generation attribute :is_admin, - if: -> { scope[:show_is_admin] && scope[:current_user] == object } + if: -> { scope[:show_is_admin] && scope[:current_user] == object } def avatar object.image.url(:sixtyfour) From dbc2ff75df9ca1acdb5bb9b5f074d1df28e82991 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:06:49 +0800 Subject: [PATCH 152/378] make eslint work and update yoda config --- .eslintrc.js | 5 ++++- package.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 11a46fd1..aa594fa7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,5 +11,8 @@ module.exports = { "promise", "standard", "react" - ] + ], + "rules": { + "yoda": [2, "never", { "exceptRange": true }] + } } diff --git a/package.json b/package.json index a976e5f1..c882fdd0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,10 @@ "babel-eslint": "^6.1.2", "chai": "^3.5.0", "eslint": "^3.5.0", + "eslint-config-standard": "^6.2.0", + "eslint-plugin-promise": "^2.0.1", "eslint-plugin-react": "^6.3.0", + "eslint-plugin-standard": "^2.0.1", "mocha": "^3.0.2" }, "optionalDependencies": { From 113a5a253070817f806344bfae98b1d29c8fe574 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:38:32 +0800 Subject: [PATCH 153/378] fix a bunch of bug risk eslint warnings --- frontend/src/Metamaps/Account.js | 6 ++-- frontend/src/Metamaps/Admin.js | 2 +- frontend/src/Metamaps/Control.js | 4 +-- frontend/src/Metamaps/Create.js | 8 ++--- frontend/src/Metamaps/Import.js | 6 ++-- frontend/src/Metamaps/JIT.js | 40 +++++++++++++++++-------- frontend/src/Metamaps/Map/CheatSheet.js | 2 ++ frontend/src/Metamaps/Map/InfoBox.js | 4 +-- frontend/src/Metamaps/Map/index.js | 5 ++-- frontend/src/Metamaps/Organize.js | 7 ++--- frontend/src/Metamaps/PasteInput.js | 8 ++--- frontend/src/Metamaps/Realtime.js | 1 + frontend/src/Metamaps/Selected.js | 2 +- frontend/src/Metamaps/Settings.js | 2 +- frontend/src/Metamaps/TopicCard.js | 2 +- 15 files changed, 59 insertions(+), 40 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 10311cbd..1ac87811 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,3 +1,5 @@ +/* global $, CanvasLoader */ + /* * Metamaps.Erb */ @@ -43,7 +45,7 @@ const Account = { var file = $('#user_image')[0].files[0] - var reader = new FileReader() + var reader = new window.FileReader() reader.onload = function (e) { var $canvas = $('<canvas>').attr({ @@ -51,7 +53,7 @@ const Account = { height: 84 }) var context = $canvas[0].getContext('2d') - var imageObj = new Image() + var imageObj = new window.Image() imageObj.onload = function () { $('.userImageDiv canvas').remove() diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 5d080c2e..d78fcecb 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -41,7 +41,7 @@ const Admin = { var self = Admin if (self.selectMetacodes.length == 0) { - alert('Would you pretty please select at least one metacode for the set?') + window.alert('Would you pretty please select at least one metacode for the set?') return false } } diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index c6c963ac..7662f47d 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -63,7 +63,7 @@ const Control = { return } - var r = confirm(outdent` + var r = window.confirm(outdent` You have ${ntext} and ${etext} selected. Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.`) @@ -456,7 +456,7 @@ const Control = { var message = nString + ' you can edit updated to ' + metacode.get('name') GlobalUI.notifyUser(message) Visualize.mGraph.plot() - }, + } } export default Control diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index bfb9b94c..92271223 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -169,15 +169,15 @@ const Create = { queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/topics/autocomplete_topic?term=%QUERY', - wildcard: '%QUERY', - }, + wildcard: '%QUERY' + } }) // initialize the autocomplete results for the metacode spinner $('#topic_name').typeahead( { highlight: true, - minLength: 2, + minLength: 2 }, [{ name: 'topic_autocomplete', @@ -186,7 +186,7 @@ const Create = { templates: { suggestion: function (s) { return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s) - }, + } }, source: topicBloodhound, }] diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 5d5f91a7..6788335f 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -143,7 +143,7 @@ const Import = { // FALL THROUGH - if we're not sure what to do, pretend // we're on the TOPICS_NEED_HEADERS state and parse some headers - case STATES.TOPICS_NEED_HEADERS: // eslint-disable-line + case STATES.TOPICS_NEED_HEADERS: // eslint-disable-line no-fallthrough if (noblanks.length < 2) { self.abort('Not enough topic headers on line ' + index) state = STATES.ABORT @@ -207,8 +207,8 @@ const Import = { } break case STATES.ABORT: - - default: + // FALL THROUGH + default: // eslint-disable-line no-fallthrough self.abort('Invalid state while parsing import data. Check code.') state = STATES.ABORT } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 54ec74b1..f8ffeb26 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,6 +1,7 @@ /* global Metamaps, $, Image, CanvasLoader */ import _ from 'lodash' +import outdent from 'outdent' import $jit from '../patched/JIT' @@ -1323,16 +1324,26 @@ const JIT = { if (Active.Topic) { menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>' } + menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' + if (Active.Mapper) { - var options = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>' + var options = outdent` + <ul> + <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> + <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> + </ul>` menustring += '<li class="rc-spacer"></li>' - menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + options + '<div class="expandLi"></div></li>' + menustring += outdent` + <li class="rc-permission"> + <div class="rc-icon"></div> + Change permissions + ${options} + <div class="expandLi"></div> + </li>` var metacodeOptions = $('#metacodeOptions').html() @@ -1345,10 +1356,11 @@ const JIT = { // set up the get sibling menu as a "lazy load" // only fill in the submenu when they hover over the get siblings list item - var siblingMenu = '<ul id="fetchSiblingList"> \ - <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> \ - <li id="loadingSiblings"></li> \ - </ul>' + var siblingMenu = outdent` + <ul id="fetchSiblingList"> + <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> + <li id="loadingSiblings"></li> + </ul>` menustring += '<li class="rc-siblings"><div class="rc-icon"></div>Reveal siblings' + siblingMenu + '<div class="expandLi"></div></li>' } @@ -1571,10 +1583,12 @@ const JIT = { if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' if (Active.Mapper) { - var permOptions = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>' + var permOptions = outdent` + <ul> + <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ + <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ + </ul>` menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>' } diff --git a/frontend/src/Metamaps/Map/CheatSheet.js b/frontend/src/Metamaps/Map/CheatSheet.js index 969ee159..be9fbfab 100644 --- a/frontend/src/Metamaps/Map/CheatSheet.js +++ b/frontend/src/Metamaps/Map/CheatSheet.js @@ -1,3 +1,5 @@ +/* global $ */ + const CheatSheet = { init: function () { // tab the cheatsheet diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ba95df4b..0d3a5c5f 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -343,7 +343,7 @@ const InfoBox = { permission: permission }) Active.Map.updateMapWrapper() - shareable = permission === 'private' ? '' : 'shareable' + const shareable = permission === 'private' ? '' : 'shareable' $('.mapPermission').removeClass('commons public private minimize').addClass(permission) $('.mapPermission .permissionSelect').remove() $('.mapInfoBox').removeClass('shareable').addClass(shareable) @@ -369,7 +369,7 @@ const InfoBox = { GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { - alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") + window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") } } } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 387311c2..dc9b4eb8 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -230,7 +230,7 @@ const Map = { canEditNow: function () { var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' - var c = confirm(confirmString) + var c = window.confirm(confirmString) if (c) { Router.maps(Active.Map.id) } @@ -256,10 +256,11 @@ const Map = { canvas.getSize = function () { if (this.size) return this.size var canvas = this.canvas - return this.size = { + this.size = { width: canvas.width, height: canvas.height } + return this.size } canvas.scale = function (x, y) { var px = this.scaleOffsetX * x, diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index ed005d39..c79bd8d7 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,5 +1,3 @@ -/* global $ */ - import _ from 'lodash' import $jit from '../patched/JIT' @@ -44,7 +42,7 @@ const Organize = { var column = floor(width / cellWidth) var totalCells = row * column - if (totalCells) + if (totalCells) { Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 @@ -56,6 +54,7 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) + } Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'radial') { var centerX = centerNode.getPos().x @@ -112,7 +111,7 @@ const Organize = { console.log(lowX, lowY, highX, highY) var newOriginX = (lowX + highX) / 2 var newOriginY = (lowY + highY) / 2 - } else alert('please call function with a valid layout dammit!') + } else window.alert('please call function with a valid layout dammit!') } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index bc20ec43..6f1cc03f 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -13,16 +13,16 @@ const PasteInput = { // intercept dragged files // see http://stackoverflow.com/questions/6756583 window.addEventListener("dragover", function(e) { - e = e || event; + e = e || window.event; e.preventDefault(); }, false); window.addEventListener("drop", function(e) { - e = e || event; + e = e || window.event; e.preventDefault(); var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { - var fileReader = new FileReader() - var text = fileReader.readAsText(e.dataTransfer.files[0]) + var fileReader = new window.FileReader() + fileReader.readAsText(e.dataTransfer.files[0]) fileReader.onload = function(e) { var text = e.currentTarget.result if (text.substring(0,5) === '<?xml') { diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 608523c8..6522d460 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -8,6 +8,7 @@ import GlobalUI from './GlobalUI' import JIT from './JIT' import Map from './Map' import Mapper from './Mapper' +import Synapse from './Synapse' import Topic from './Topic' import Util from './Util' import Views from './Views' diff --git a/frontend/src/Metamaps/Selected.js b/frontend/src/Metamaps/Selected.js index 396270ab..d23517b5 100644 --- a/frontend/src/Metamaps/Selected.js +++ b/frontend/src/Metamaps/Selected.js @@ -1,6 +1,6 @@ const Selected = { reset: function () { - var self = Metamaps.Selected + var self = Selected self.Nodes = [] self.Edges = [] }, diff --git a/frontend/src/Metamaps/Settings.js b/frontend/src/Metamaps/Settings.js index 687a6629..7010e543 100644 --- a/frontend/src/Metamaps/Settings.js +++ b/frontend/src/Metamaps/Settings.js @@ -15,7 +15,7 @@ const Settings = { background: '#18202E', text: '#DDD' } - }, + } } export default Settings diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 68061d96..0b2d1497 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, CanvasLoader, Countable, Hogan, embedly */ import Active from './Active' import GlobalUI from './GlobalUI' From e2c0ce7c22391c0c973734af4ecd814a8704b0d9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:43:42 +0800 Subject: [PATCH 154/378] fix api documentation --- doc/api/api.raml | 2 +- doc/api/examples/current_user.json | 9 +++++++++ doc/api/examples/user.json | 3 +-- lib/tasks/extensions.rake | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 doc/api/examples/current_user.json diff --git a/doc/api/api.raml b/doc/api/api.raml index 624e5a46..50c2c992 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -31,7 +31,7 @@ schemas: /maps: !include apis/maps.raml /mappings: !include apis/mappings.raml -/metacodes: !include api/metacodes.raml +/metacodes: !include apis/metacodes.raml /synapses: !include apis/synapses.raml /tokens: !include apis/tokens.raml /topics: !include apis/topics.raml diff --git a/doc/api/examples/current_user.json b/doc/api/examples/current_user.json new file mode 100644 index 00000000..83476006 --- /dev/null +++ b/doc/api/examples/current_user.json @@ -0,0 +1,9 @@ +{ + "data": { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0, + "is_admin": false + } +} diff --git a/doc/api/examples/user.json b/doc/api/examples/user.json index 83476006..ca54233d 100644 --- a/doc/api/examples/user.json +++ b/doc/api/examples/user.json @@ -3,7 +3,6 @@ "id": 1, "name": "user", "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", - "generation": 0, - "is_admin": false + "generation": 0 } } diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index 6efd9026..fc4a4855 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -3,7 +3,7 @@ namespace :assets do task :js_compile do system 'npm install' system 'npm run build' - system 'bin/build-apidocs.sh' if ENV['MAILER_DEFAULT_URL'] == 'metamaps.herokuapp.com' + system 'bin/build-apidocs.sh' if Rails.env.production? end end From 12417d8cd3b8548f376b8e48bc4153e40b2b871c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 5 Oct 2016 01:45:21 +0800 Subject: [PATCH 155/378] update JIT eslint style --- frontend/src/Metamaps/JIT.js | 705 +++++++++++++++++------------------ 1 file changed, 343 insertions(+), 362 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index f8ffeb26..6c272a50 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -22,7 +22,6 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' - /* * Metamaps.Erb * Metamaps.Mappings @@ -48,19 +47,19 @@ const JIT = { removeSynapse: 'Metamaps:JIT:events:removeSynapse', pan: 'Metamaps:JIT:events:pan', zoom: 'Metamaps:JIT:events:zoom', - animationDone: 'Metamaps:JIT:events:animationDone', + animationDone: 'Metamaps:JIT:events:animationDone' }, vizData: [], // contains the visualization-compatible graph /** * This method will bind the event handlers it is interested and initialize the class. */ init: function () { - var self = JIT + const self = JIT $('.zoomIn').click(self.zoomIn) $('.zoomOut').click(self.zoomOut) - var zoomExtents = function (event) { + const zoomExtents = function (event) { self.zoomExtents(event, Visualize.mGraph.canvas) } $('.zoomExtents').click(zoomExtents) @@ -77,15 +76,15 @@ const JIT = { * convert our topic JSON into something JIT can use */ convertModelsToJIT: function (topics, synapses) { - var jitReady = [] + const jitReady = [] - var synapsesToRemove = [] - var mapping - var node - var nodes = {} - var existingEdge - var edge - var edges = [] + const synapsesToRemove = [] + let mapping + let node + const nodes = {} + let existingEdge + let edge + const edges = [] topics.each(function (t) { node = t.createNode() @@ -97,8 +96,7 @@ const JIT = { if (topics.get(s.get('topic1_id')) === undefined || topics.get(s.get('topic2_id')) === undefined) { // this means it's an invalid synapse synapsesToRemove.push(s) - } - else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { + } else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { existingEdge = _.find(edges, { nodeFrom: edge.nodeFrom, nodeTo: edge.nodeTo @@ -130,14 +128,14 @@ const JIT = { return [jitReady, synapsesToRemove] }, prepareVizData: function () { - var self = JIT - var mapping + const self = JIT + let mapping // reset/empty vizData self.vizData = [] Visualize.loadLater = false - var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) + const results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) self.vizData = results[0] @@ -155,7 +153,7 @@ const JIT = { $('#instructions div.addTopic').show() } - if (self.vizData.length == 0) { + if (self.vizData.length === 0) { GlobalUI.showDiv('#instructions') Visualize.loadLater = true } else { @@ -165,11 +163,11 @@ const JIT = { Visualize.render() }, // prepareVizData edgeRender: function (adj, canvas) { - // get nodes cartesian coordinates - var pos = adj.nodeFrom.pos.getc(true) - var posChild = adj.nodeTo.pos.getc(true) + // get nodes cartesian coordinates + const pos = adj.nodeFrom.pos.getc(true) + const posChild = adj.nodeTo.pos.getc(true) - var synapse + let synapse if (adj.getData('displayIndex')) { synapse = adj.getData('synapses')[adj.getData('displayIndex')] if (!synapse) { @@ -185,17 +183,17 @@ const JIT = { // label placement on edges if (canvas.denySelected) { - var color = Settings.colors.synapses.normal + const color = Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) - // check for edge label in data - var desc = synapse.get('desc') + // check for edge label in data + let desc = synapse.get('desc') - var showDesc = adj.getData('showDesc') + const showDesc = adj.getData('showDesc') - var drawSynapseCount = function (context, x, y, count) { + const drawSynapseCount = function (context, x, y, count) { /* circle size: 16x16px positioning: overlay and center on top right corner of synapse label - 8px left and 8px down @@ -223,28 +221,28 @@ const JIT = { context.fillText(count, x, y + 5) } - if (!canvas.denySelected && desc != '' && showDesc) { + if (!canvas.denySelected && desc !== '' && showDesc) { // '&' to '&' desc = Util.decodeEntities(desc) - // now adjust the label placement - var ctx = canvas.getCtx() + // now adjust the label placement + const ctx = canvas.getCtx() ctx.font = 'bold 14px arial' ctx.fillStyle = '#FFF' ctx.textBaseline = 'alphabetic' - var arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + const arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 16 - var height = (16 * arrayOfLabelLines.length) + 8 + const width = Math.max.apply(null, lineWidths) + 16 + const height = (16 * arrayOfLabelLines.length) + 8 - var x = (pos.x + posChild.x - width) / 2 - var y = ((pos.y + posChild.y) / 2) - height / 2 + const x = (pos.x + posChild.x - width) / 2 + const y = ((pos.y + posChild.y) / 2) - height / 2 - var radius = 5 + const radius = 5 // render background ctx.beginPath() @@ -261,25 +259,24 @@ const JIT = { ctx.fill() // get number of synapses - var synapseNum = adj.getData('synapses').length + const synapseNum = adj.getData('synapses').length // render text ctx.fillStyle = '#424242' ctx.textAlign = 'center' - for (index = 0; index < arrayOfLabelLines.length; ++index) { + for (let index = 0; index < arrayOfLabelLines.length; ++index) { ctx.fillText(arrayOfLabelLines[index], x + (width / 2), y + 18 + (16 * index)) } if (synapseNum > 1) { drawSynapseCount(ctx, x + width, y, synapseNum) } - } - else if (!canvas.denySelected && showDesc) { + } else if (!canvas.denySelected && showDesc) { // get number of synapses - var synapseNum = adj.getData('synapses').length + const synapseNum = adj.getData('synapses').length if (synapseNum > 1) { - var ctx = canvas.getCtx() + const ctx = canvas.getCtx() const x = (pos.x + posChild.x) / 2 const y = (pos.y + posChild.y) / 2 drawSynapseCount(ctx, x, y, synapseNum) @@ -321,13 +318,13 @@ const JIT = { // background: { // type: 'Metamaps' // }, - // NodeStyles: { - // enable: true, - // type: 'Native', - // stylesHover: { - // dim: 30 - // }, - // duration: 300 + // NodeStyles: { + // enable: true, + // type: 'Native', + // stylesHover: { + // dim: 30 + // }, + // duration: 300 // }, // Change node and edge styles such as // color and width. @@ -400,8 +397,8 @@ const JIT = { Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() - var bS = Mouse.boxStartCoordinates - var bE = Mouse.boxEndCoordinates + const bS = Mouse.boxStartCoordinates + const bE = Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { JIT.zoomToBox(e) return @@ -421,7 +418,7 @@ const JIT = { } } - if (e.target.id != 'infovis-canvas') return false + if (e.target.id !== 'infovis-canvas') return false // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { @@ -447,7 +444,7 @@ const JIT = { return } - if (e.target.id != 'infovis-canvas') return false + if (e.target.id !== 'infovis-canvas') return false // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { @@ -462,16 +459,16 @@ const JIT = { // Number of iterations for the FD algorithm iterations: 200, // Edge length - levelDistance: 200, + levelDistance: 200 }, nodeSettings: { 'customNode': { 'render': function (node, canvas) { - var pos = node.pos.getc(true), - dim = node.getData('dim'), - topic = node.getData('topic'), - metacode = topic ? topic.getMetacode() : false, - ctx = canvas.getCtx() + const pos = node.pos.getc(true) + const dim = node.getData('dim') + const topic = node.getData('topic') + const metacode = topic ? topic.getMetacode() : false + const ctx = canvas.getCtx() // if the topic is selected draw a circle around it if (!canvas.denySelected && node.selected) { @@ -496,9 +493,9 @@ const JIT = { } // if the topic has a link, draw a small image to indicate that - var hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null - var linkImage = JIT.topicLinkImage - var linkImageLoaded = linkImage.complete || + const hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null + const linkImage = JIT.topicLinkImage + const linkImageLoaded = linkImage.complete || (typeof linkImage.naturalWidth !== 'undefined' && linkImage.naturalWidth !== 0) if (hasLink && linkImageLoaded) { @@ -506,9 +503,9 @@ const JIT = { } // if the topic has a desc, draw a small image to indicate that - var hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null - var descImage = JIT.topicDescImage - var descImageLoaded = descImage.complete || + const hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null + const descImage = JIT.topicDescImage + const descImageLoaded = descImage.complete || (typeof descImage.naturalWidth !== 'undefined' && descImage.naturalWidth !== 0) if (hasDesc && descImageLoaded) { @@ -516,21 +513,21 @@ const JIT = { } }, 'contains': function (node, pos) { - var npos = node.pos.getc(true), - dim = node.getData('dim'), - arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n'), - ctx = Visualize.mGraph.canvas.getCtx() + const npos = node.pos.getc(true) + const dim = node.getData('dim') + const arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n') + const ctx = Visualize.mGraph.canvas.getCtx() - var height = 25 * arrayOfLabelLines.length + const height = 25 * arrayOfLabelLines.length - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 8 - var labely = npos.y + node.getData('height') + 5 + height / 2 + const width = Math.max.apply(null, lineWidths) + 8 + const labely = npos.y + node.getData('height') + 5 + height / 2 - var overLabel = this.nodeHelper.rectangle.contains({ + const overLabel = this.nodeHelper.rectangle.contains({ x: npos.x, y: labely }, pos, width, height) @@ -545,8 +542,8 @@ const JIT = { JIT.edgeRender(adj, canvas) }, 'contains': function (adj, pos) { - var from = adj.nodeFrom.pos.getc(), - to = adj.nodeTo.pos.getc() + const from = adj.nodeFrom.pos.getc() + const to = adj.nodeTo.pos.getc() // this fixes an issue where when edges are perfectly horizontal or perfectly vertical // it becomes incredibly difficult to hover over them @@ -625,7 +622,7 @@ const JIT = { i: 0, onMouseMove: function (node, eventInfo, e) { // if(this.i++ % 3) return - var pos = eventInfo.getPos() + const pos = eventInfo.getPos() Visualize.cameraPosition.x += (pos.x - Visualize.cameraPosition.x) * 0.5 Visualize.cameraPosition.y += (-pos.y - Visualize.cameraPosition.y) * 0.5 Visualize.mGraph.plot() @@ -669,16 +666,16 @@ const JIT = { levelDistance: 200 }, onMouseEnter: function (edge) { - var filtered = edge.getData('alpha') === 0 + const filtered = edge.getData('alpha') === 0 // don't do anything if the edge is filtered - // or if the canvas is animating + // or if the canvas is animating if (filtered || Visualize.mGraph.busy) return $('canvas').css('cursor', 'pointer') - var edgeIsSelected = Selected.Edges.indexOf(edge) + const edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { + if (edgeIsSelected === -1) { edge.setData('showDesc', true, 'current') } @@ -692,11 +689,11 @@ const JIT = { Visualize.mGraph.plot() }, // onMouseEnter onMouseLeave: function (edge) { - if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered + if (edge.getData('alpha') === 0) return // don't do anything if the edge is filtered $('canvas').css('cursor', 'default') - var edgeIsSelected = Selected.Edges.indexOf(edge) + const edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { + if (edgeIsSelected === -1) { edge.setData('showDesc', false, 'current') } @@ -709,16 +706,16 @@ const JIT = { }) Visualize.mGraph.plot() }, // onMouseLeave - onMouseMoveHandler: function (node, eventInfo, e) { - var self = JIT + onMouseMoveHandler: function (_node, eventInfo, e) { + const self = JIT if (Visualize.mGraph.busy) return - var node = eventInfo.getNode() - var edge = eventInfo.getEdge() + const node = eventInfo.getNode() + const edge = eventInfo.getEdge() // if we're on top of a node object, act like there aren't edges under it - if (node != false) { + if (node !== false) { if (Mouse.edgeHoveringOver) { self.onMouseLeave(Mouse.edgeHoveringOver) } @@ -726,13 +723,13 @@ const JIT = { return } - if (edge == false && Mouse.edgeHoveringOver != false) { + if (edge === false && Mouse.edgeHoveringOver !== false) { // mouse not on an edge, but we were on an edge previously self.onMouseLeave(Mouse.edgeHoveringOver) - } else if (edge != false && Mouse.edgeHoveringOver == false) { + } else if (edge !== false && Mouse.edgeHoveringOver === false) { // mouse is on an edge, but there isn't a stored edge self.onMouseEnter(edge) - } else if (edge != false && Mouse.edgeHoveringOver != edge) { + } else if (edge !== false && Mouse.edgeHoveringOver !== edge) { // mouse is on an edge, but a different edge is stored self.onMouseLeave(Mouse.edgeHoveringOver) self.onMouseEnter(edge) @@ -746,16 +743,12 @@ const JIT = { } }, // onMouseMoveHandler enterKeyHandler: function () { - var creatingMap = GlobalUI.lightbox + const creatingMap = GlobalUI.lightbox if (creatingMap === 'newmap' || creatingMap === 'forkmap') { GlobalUI.CreateMap.submit() - } - // this is to submit new topic creation - else if (Create.newTopic.beingCreated) { + } else if (Create.newTopic.beingCreated) { Topic.createTopicLocally() - } - // to submit new synapse creation - else if (Create.newSynapse.beingCreated) { + } else if (Create.newSynapse.beingCreated) { Synapse.createSynapseLocally() } }, // enterKeyHandler @@ -764,27 +757,27 @@ const JIT = { Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { - var self = JIT + const self = JIT - // this is used to send nodes that are moving to + // this is used to send nodes that are moving to // other realtime collaborators on the same map - var positionsToSend = {} - var topic + const positionsToSend = {} + let topic - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (node && !node.nodeFrom) { - var pos = eventInfo.getPos() + const pos = eventInfo.getPos() // if it's a left click, or a touch, move the node - if (e.touches || (e.button == 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons == 0 || e.buttons == 1 || e.buttons == undefined))) { + if (e.touches || (e.button === 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons === 0 || e.buttons === 1 || e.buttons === undefined))) { // if the node dragged isn't already selected, select it - var whatToDo = self.handleSelectionBeforeDragging(node, e) + const whatToDo = self.handleSelectionBeforeDragging(node, e) if (node.pos.rho || node.pos.rho === 0) { // this means we're in topic view - var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) - var theta = Math.atan2(pos.y, pos.x) + const rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) + const theta = Math.atan2(pos.y, pos.x) node.pos.setp(theta, rho) - } else if (whatToDo == 'only-drag-this-one') { + } else if (whatToDo === 'only-drag-this-one') { node.pos.setc(pos.x, pos.y) if (Active.Map) { @@ -797,21 +790,21 @@ const JIT = { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { - var len = Selected.Nodes.length + const len = Selected.Nodes.length // first define offset for each node - var xOffset = [] - var yOffset = [] - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] + const xOffset = [] + const yOffset = [] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] xOffset[i] = n.pos.x - node.pos.x yOffset[i] = n.pos.y - node.pos.y } // for - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] - var x = pos.x + xOffset[i] - var y = pos.y + yOffset[i] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] + const x = pos.x + xOffset[i] + const y = pos.y + yOffset[i] n.pos.setc(x, y) if (Active.Map) { @@ -829,21 +822,20 @@ const JIT = { } } // if - if (whatToDo == 'deselect') { + if (whatToDo === 'deselect') { Control.deselectNode(node) } Visualize.mGraph.plot() - } - // if it's a right click or holding down alt, start synapse creation ->third option is for firefox - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { - if (JIT.tempInit == false) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { + // if it's a right click or holding down alt, start synapse creation ->third option is for firefox + if (JIT.tempInit === false) { JIT.tempNode = node JIT.tempInit = true Create.newTopic.hide() Create.newSynapse.hide() // set the draw synapse start positions - var l = Selected.Nodes.length + const l = Selected.Nodes.length if (l > 0) { for (let i = l - 1; i >= 0; i -= 1) { const n = Selected.Nodes[i] @@ -865,7 +857,7 @@ const JIT = { } // let temp = eventInfo.getNode() - if (temp != false && temp.id != node.id && Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned + if (temp !== false && temp.id !== node.id && Selected.Nodes.indexOf(temp) === -1) { // this means a Node has been returned JIT.tempNode2 = temp Mouse.synapseEndCoordinates = { @@ -885,8 +877,8 @@ const JIT = { n.setData('dim', 25, 'current') }) // pop up node creation :) - var myX = e.clientX - 110 - var myY = e.clientY - 30 + const myX = e.clientX - 110 + const myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') Create.newTopic.x = eventInfo.getPos().x @@ -898,11 +890,9 @@ const JIT = { y: pos.y } } - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Active.Topic) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { GlobalUI.notifyUser('Cannot edit Public map.') } } @@ -918,7 +908,9 @@ const JIT = { Visualize.mGraph.plot() }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { - var midpoint = {}, pixelPos, mapping + let midpoint = {} + let pixelPos + let mapping if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse @@ -944,20 +936,20 @@ const JIT = { // this means you dragged an existing node, autosave that to the database // check whether to save mappings - var checkWhetherToSave = function () { - var map = Active.Map + const checkWhetherToSave = function () { + const map = Active.Map if (!map) return false - var mapper = Active.Mapper + const mapper = Active.Mapper // this case // covers when it is a public map owned by you // and also when it's a private map - var activeMappersMap = map.authorizePermissionChange(mapper) - var commonsMap = map.get('permission') === 'commons' - var realtimeOn = Realtime.status + const activeMappersMap = map.authorizePermissionChange(mapper) + const commonsMap = map.get('permission') === 'commons' + const realtimeOn = Realtime.status - // don't save if commons map, and you have realtime off, + // don't save if commons map, and you have realtime off, // even if you're map creator return map && mapper && ((commonsMap && realtimeOn) || (activeMappersMap && !commonsMap)) } @@ -969,9 +961,9 @@ const JIT = { yloc: node.getPos().y }) // also save any other selected nodes that also got dragged along - var l = Selected.Nodes.length + const l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var n = Selected.Nodes[i] + const n = Selected.Nodes[i] if (n !== node) { mapping = n.getData('mapping') mapping.save({ @@ -984,24 +976,23 @@ const JIT = { } }, // onDragEndTopicHandler canvasClickHandler: function (canvasLoc, e) { - // grab the location and timestamp of the click - var storedTime = Mouse.lastCanvasClick - var now = Date.now() // not compatible with IE8 FYI + // grab the location and timestamp of the click + const storedTime = Mouse.lastCanvasClick + const now = Date.now() // not compatible with IE8 FYI Mouse.lastCanvasClick = now - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE && !Mouse.didPan) { if (Active.Map && !authorized) { GlobalUI.notifyUser('Cannot edit Public map.') return - } - else if (Active.Topic) { + } else if (Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') return } // DOUBLE CLICK - // pop up node creation :) + // pop up node creation :) Create.newTopic.addSynapse = false Create.newTopic.x = canvasLoc.x Create.newTopic.y = canvasLoc.y @@ -1026,7 +1017,7 @@ const JIT = { Control.deselectAllNodes() } } - }, // canvasClickHandler + }, // canvasClickHandler nodeDoubleClickHandler: function (node, e) { TopicCard.showCard(node) }, // nodeDoubleClickHandler @@ -1034,9 +1025,9 @@ const JIT = { SynapseCard.showCard(adj, e) }, // nodeDoubleClickHandler nodeWasDoubleClicked: function () { - // grab the timestamp of the click - var storedTime = Mouse.lastNodeClick - var now = Date.now() // not compatible with IE8 FYI + // grab the timestamp of the click + const storedTime = Mouse.lastNodeClick + const now = Date.now() // not compatible with IE8 FYI Mouse.lastNodeClick = now if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE) { @@ -1052,10 +1043,10 @@ const JIT = { // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) // return value: deselect node again after? - if (Selected.Nodes.length == 0) { + if (Selected.Nodes.length === 0) { return 'only-drag-this-one' } - if (Selected.Nodes.indexOf(node) == -1) { + if (Selected.Nodes.indexOf(node) === -1) { if (e.shiftKey) { Control.selectNode(node, e) return 'nothing' @@ -1063,12 +1054,12 @@ const JIT = { return 'only-drag-this-one' } } - return 'nothing'; // case 4? + return 'nothing' // case 4? }, // handleSelectionBeforeDragging - getNodeXY: function(node) { - if (typeof node.pos.x === "number" && typeof node.pos.y === "number") { + getNodeXY: function (node) { + if (typeof node.pos.x === 'number' && typeof node.pos.y === 'number') { return node.pos - } else if (typeof node.pos.theta === "number" && typeof node.pos.rho === "number") { + } else if (typeof node.pos.theta === 'number' && typeof node.pos.rho === 'number') { return new $jit.Polar(node.pos.theta, node.pos.rho).getc(true) } else { console.error('getNodeXY: unrecognized node pos format') @@ -1076,11 +1067,11 @@ const JIT = { } }, selectWithBox: function (e) { - var self = this - var sX = Mouse.boxStartCoordinates.x, - sY = Mouse.boxStartCoordinates.y, - eX = Mouse.boxEndCoordinates.x, - eY = Mouse.boxEndCoordinates.y + const self = this + let sX = Mouse.boxStartCoordinates.x + let sY = Mouse.boxStartCoordinates.y + let eX = Mouse.boxEndCoordinates.x + let eY = Mouse.boxEndCoordinates.y if (!e.shiftKey) { Control.deselectAllNodes() @@ -1088,17 +1079,17 @@ const JIT = { } // select all nodes that are within the box - Visualize.mGraph.graph.eachNode(function(n) { - var pos = self.getNodeXY(n) - var x = pos.x, - y = pos.y + Visualize.mGraph.graph.eachNode(function (n) { + const pos = self.getNodeXY(n) + const x = pos.x + const y = pos.y // depending on which way the person dragged the box, check that // x and y are between the start and end values of the box if ((sX < x && x < eX && sY < y && y < eY) || - (sX > x && x > eX && sY > y && y > eY) || - (sX > x && x > eX && sY < y && y < eY) || - (sX < x && x < eX && sY > y && y > eY)) { + (sX > x && x > eX && sY > y && y > eY) || + (sX > x && x > eX && sY < y && y < eY) || + (sX < x && x < eX && sY > y && y > eY)) { if (e.shiftKey) { if (n.selected) { Control.deselectNode(n) @@ -1115,62 +1106,62 @@ const JIT = { sY = -1 * sY eY = -1 * eY - var edgesToToggle = [] + const edgesToToggle = [] Metamaps.Synapses.each(function (synapse) { - var e = synapse.get('edge') + const e = synapse.get('edge') if (edgesToToggle.indexOf(e) === -1) { edgesToToggle.push(e) } }) edgesToToggle.forEach(function (edge) { - var fromNodePos = self.getNodeXY(edge.nodeFrom) - var fromNodeX = fromNodePos.x - var fromNodeY = -1 * fromNodePos.y - var toNodePos = self.getNodeXY(edge.nodeTo) - var toNodeX = toNodePos.x - var toNodeY = -1 * toNodePos.y + const fromNodePos = self.getNodeXY(edge.nodeFrom) + const fromNodeX = fromNodePos.x + const fromNodeY = -1 * fromNodePos.y + const toNodePos = self.getNodeXY(edge.nodeTo) + const toNodeX = toNodePos.x + const toNodeY = -1 * toNodePos.y - var maxX = fromNodeX - var maxY = fromNodeY - var minX = fromNodeX - var minY = fromNodeY + let maxX = fromNodeX + let maxY = fromNodeY + let minX = fromNodeX + let minY = fromNodeY // Correct maxX, MaxY values ;(toNodeX > maxX) ? (maxX = toNodeX) : (minX = toNodeX) ;(toNodeY > maxY) ? (maxY = toNodeY) : (minY = toNodeY) - var maxBoxX = sX - var maxBoxY = sY - var minBoxX = sX - var minBoxY = sY + let maxBoxX = sX + let maxBoxY = sY + let minBoxX = sX + let minBoxY = sY // Correct maxBoxX, maxBoxY values ;(eX > maxBoxX) ? (maxBoxX = eX) : (minBoxX = eX) ;(eY > maxBoxY) ? (maxBoxY = eY) : (minBoxY = eY) // Find the slopes from the synapse fromNode to the 4 corners of the selection box - var slopes = [] + const slopes = [] slopes.push((sY - fromNodeY) / (sX - fromNodeX)) slopes.push((sY - fromNodeY) / (eX - fromNodeX)) slopes.push((eY - fromNodeY) / (eX - fromNodeX)) slopes.push((eY - fromNodeY) / (sX - fromNodeX)) - var minSlope = slopes[0] - var maxSlope = slopes[0] + let minSlope = slopes[0] + let maxSlope = slopes[0] slopes.forEach(function (entry) { if (entry > maxSlope) maxSlope = entry if (entry < minSlope) minSlope = entry }) // Find synapse-in-question's slope - var synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX) - var b = fromNodeY - synSlope * fromNodeX + const synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX) + const b = fromNodeY - synSlope * fromNodeX // Use the selection box edges as test cases for synapse intersection - var testX = sX - var testY = synSlope * testX + b + let testX = sX + let testY = synSlope * testX + b - var selectTest + let selectTest if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY) { selectTest = true @@ -1205,9 +1196,9 @@ const JIT = { // The test synapse was selected! if (selectTest) { - // shiftKey = toggleSelect, otherwise + // shiftKey = toggleSelect, otherwise if (e.shiftKey) { - if (Selected.Edges.indexOf(edge) != -1) { + if (Selected.Edges.indexOf(edge) !== -1) { Control.deselectEdge(edge) } else { Control.selectEdge(edge) @@ -1222,12 +1213,12 @@ const JIT = { Visualize.mGraph.plot() }, // selectWithBox drawSelectBox: function (eventInfo, e) { - var ctx = Visualize.mGraph.canvas.getCtx() + const ctx = Visualize.mGraph.canvas.getCtx() - var startX = Mouse.boxStartCoordinates.x, - startY = Mouse.boxStartCoordinates.y, - currX = eventInfo.getPos().x, - currY = eventInfo.getPos().y + const startX = Mouse.boxStartCoordinates.x + const startY = Mouse.boxStartCoordinates.y + const currX = eventInfo.getPos().x + const currY = eventInfo.getPos().y Visualize.mGraph.canvas.clear() Visualize.mGraph.plot() @@ -1244,10 +1235,10 @@ const JIT = { selectNodeOnClickHandler: function (node, e) { if (Visualize.mGraph.busy) return - var self = JIT + const self = JIT // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { self.selectNodeOnRightClickHandler(node, e) return } @@ -1258,7 +1249,7 @@ const JIT = { return } - var check = self.nodeWasDoubleClicked() + const check = self.nodeWasDoubleClicked() if (check) { self.nodeDoubleClickHandler(node, e) return @@ -1266,7 +1257,7 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var nodeAlreadySelected = node.selected + const nodeAlreadySelected = node.selected if (!e.shiftKey) { Control.deselectAllNodes() @@ -1304,17 +1295,17 @@ const JIT = { // delete old right click menu $('.rightclickmenu').remove() // create new menu for clicked on node - var rightclickmenu = document.createElement('div') + const rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' - //prevent the custom context menu from immediately opening the default context menu as well - rightclickmenu.setAttribute('oncontextmenu','return false') - + // prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu', 'return false') + // add the proper options to the menu - var menustring = '<ul>' + let menustring = '<ul>' - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - var disabled = authorized ? '' : 'disabled' + const disabled = authorized ? '' : 'disabled' if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' @@ -1328,7 +1319,7 @@ const JIT = { menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' if (Active.Mapper) { - var options = outdent` + const options = outdent` <ul> <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> @@ -1345,7 +1336,7 @@ const JIT = { <div class="expandLi"></div> </li>` - var metacodeOptions = $('#metacodeOptions').html() + const metacodeOptions = $('#metacodeOptions').html() menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>' } @@ -1356,7 +1347,7 @@ const JIT = { // set up the get sibling menu as a "lazy load" // only fill in the submenu when they hover over the get siblings list item - var siblingMenu = outdent` + const siblingMenu = outdent` <ul id="fetchSiblingList"> <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> <li id="loadingSiblings"></li> @@ -1368,36 +1359,35 @@ const JIT = { rightclickmenu.innerHTML = menustring // position the menu where the click happened - var position = {} - var RIGHTCLICK_WIDTH = 300 - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256 - var MAX_SUBMENU_HEIGHT = 270 - var windowWidth = $(window).width() - var windowHeight = $(window).height() + const position = {} + const RIGHTCLICK_WIDTH = 300 + const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static + const SUBMENUS_WIDTH = 256 + const MAX_SUBMENU_HEIGHT = 270 + const windowWidth = $(window).width() + const windowHeight = $(window).height() if (windowWidth - e.clientX < SUBMENUS_WIDTH) { position.right = windowWidth - e.clientX $(rightclickmenu).addClass('moveMenusToLeft') - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { position.right = windowWidth - e.clientX - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { position.left = e.clientX $(rightclickmenu).addClass('moveMenusToLeft') + } else { + position.left = e.clientX } - else position.left = e.clientX if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { position.bottom = windowHeight - e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + } else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { position.top = e.clientY $(rightclickmenu).addClass('moveMenusUp') + } else { + position.top = e.clientY } - else position.top = e.clientY $(rightclickmenu).css(position) // add the menu to the page @@ -1438,7 +1428,7 @@ const JIT = { // open the entity in a new tab $('.rc-popout').click(function () { $('.rightclickmenu').remove() - var win = window.open('/topics/' + node.id, '_blank') + const win = window.open('/topics/' + node.id, '_blank') win.focus() }) @@ -1457,11 +1447,11 @@ const JIT = { }) // fetch relatives - var fetch_sent = false + let fetchSent = false $('.rc-siblings').hover(function () { - if (!fetch_sent) { + if (!fetchSent) { JIT.populateRightClickSiblings(node) - fetch_sent = true + fetchSent = true } }) $('.rc-siblings .fetchAll').click(function () { @@ -1471,28 +1461,25 @@ const JIT = { }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { - var self = JIT - // depending on how many topics are selected, do different things - - var topic = node.getData('topic') + const topic = node.getData('topic') // add a loading icon for now - var loader = new CanvasLoader('loadingSiblings') - loader.setColor('#4FC059'); // default is '#000000' + const loader = new CanvasLoader('loadingSiblings') + loader.setColor('#4FC059') // default is '#000000' loader.setDiameter(15) // default is 40 loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 + loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - var topics = Metamaps.Topics.map(function (t) { return t.id }) - var topics_string = topics.join() + const topics = Metamaps.Topics.map(function (t) { return t.id }) + const topicsString = topics.join() - var successCallback = function (data) { + const successCallback = function (data) { $('#loadingSiblings').remove() for (var key in data) { - var string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' + const string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' $('#fetchSiblingList').append('<li class="getSiblings" data-id="' + key + '">' + string + '</li>') } @@ -1505,7 +1492,7 @@ const JIT = { $.ajax({ type: 'GET', - url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topics_string, + url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topicsString, success: successCallback, error: function () {} }) @@ -1513,15 +1500,15 @@ const JIT = { selectEdgeOnClickHandler: function (adj, e) { if (Visualize.mGraph.busy) return - var self = JIT + const self = JIT // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { self.selectEdgeOnRightClickHandler(adj, e) return } - var check = self.nodeWasDoubleClicked() + const check = self.nodeWasDoubleClicked() if (check) { self.edgeDoubleClickHandler(adj, e) return @@ -1529,7 +1516,7 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 + const edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { Control.deselectAllNodes() @@ -1551,7 +1538,7 @@ const JIT = { // the 'node' variable is a JIT node, the one that was clicked on // the 'e' variable is the click event - if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered + if (adj.getData('alpha') === 0) return // don't do anything if the edge is filtered e.preventDefault() e.stopPropagation() @@ -1563,17 +1550,17 @@ const JIT = { // delete old right click menu $('.rightclickmenu').remove() // create new menu for clicked on node - var rightclickmenu = document.createElement('div') + const rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' - //prevent the custom context menu from immediately opening the default context menu as well - rightclickmenu.setAttribute('oncontextmenu','return false') + // prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu', 'return false') // add the proper options to the menu - var menustring = '<ul>' + let menustring = '<ul>' - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - var disabled = authorized ? '' : 'disabled' + const disabled = authorized ? '' : 'disabled' if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' @@ -1583,12 +1570,10 @@ const JIT = { if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' if (Active.Mapper) { - var permOptions = outdent` + const permOptions = outdent` <ul> <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>` + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> </ul>` menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>' } @@ -1597,32 +1582,28 @@ const JIT = { rightclickmenu.innerHTML = menustring // position the menu where the click happened - var position = {} - var RIGHTCLICK_WIDTH = 300 - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256 - var MAX_SUBMENU_HEIGHT = 270 - var windowWidth = $(window).width() - var windowHeight = $(window).height() + const position = {} + const RIGHTCLICK_WIDTH = 300 + const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static + const SUBMENUS_WIDTH = 256 + const MAX_SUBMENU_HEIGHT = 270 + const windowWidth = $(window).width() + const windowHeight = $(window).height() if (windowWidth - e.clientX < SUBMENUS_WIDTH) { position.right = windowWidth - e.clientX $(rightclickmenu).addClass('moveMenusToLeft') - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { position.right = windowWidth - e.clientX - } - else position.left = e.clientX + } else position.left = e.clientX if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { position.bottom = windowHeight - e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + } else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { position.top = e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else position.top = e.clientY + } else position.top = e.clientY $(rightclickmenu).css(position) @@ -1663,20 +1644,19 @@ const JIT = { }) }, // selectEdgeOnRightClickHandler SmoothPanning: function () { - var sx = Visualize.mGraph.canvas.scaleOffsetX, - sy = Visualize.mGraph.canvas.scaleOffsetY, - y_velocity = Mouse.changeInY, // initial y velocity - x_velocity = Mouse.changeInX, // initial x velocity - easing = 1 // frictional value + const sx = Visualize.mGraph.canvas.scaleOffsetX + const sy = Visualize.mGraph.canvas.scaleOffsetY + const yVelocity = Mouse.changeInY // initial y velocity + const xVelocity = Mouse.changeInX // initial x velocity + let easing = 1 // frictional value - easing = 1 window.clearInterval(panningInt) panningInt = setInterval(function () { myTimer() }, 1) function myTimer () { - Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) + Visualize.mGraph.canvas.translate(xVelocity * easing * 1 / sx, yVelocity * easing * 1 / sy) $(document).trigger(JIT.events.pan) easing = easing * 0.75 @@ -1684,30 +1664,30 @@ const JIT = { } }, // SmoothPanning renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { - var ctx = canvas.getCtx() - // invert edge direction + const ctx = canvas.getCtx() + // invert edge direction if (swap) { - var tmp = from + const tmp = from from = to to = tmp } - // vect represents a line from tip to tail of the arrow - var vect = new $jit.Complex(to.x - from.x, to.y - from.y) - // scale it + // vect represents a line from tip to tail of the arrow + const vect = new $jit.Complex(to.x - from.x, to.y - from.y) + // scale it vect.$scale(dim / vect.norm()) - // compute the midpoint of the edge line - var newX = (to.x - from.x) * placement + from.x - var newY = (to.y - from.y) * placement + from.y - var midPoint = new $jit.Complex(newX, newY) + // compute the midpoint of the edge line + const newX = (to.x - from.x) * placement + from.x + const newY = (to.y - from.y) * placement + from.y + const midPoint = new $jit.Complex(newX, newY) - // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint - var arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y) - // compute the tail intersection point with the edge line - var intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y) - // vector perpendicular to vect - var normal = new $jit.Complex(-vect.y / 2, vect.x / 2) - var v1 = intermediatePoint.add(normal) - var v2 = intermediatePoint.$add(normal.$scale(-1)) + // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint + const arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y) + // compute the tail intersection point with the edge line + const intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y) + // vector perpendicular to vect + const normal = new $jit.Complex(-vect.y / 2, vect.x / 2) + const v1 = intermediatePoint.add(normal) + const v2 = intermediatePoint.$add(normal.$scale(-1)) if (newSynapse) { ctx.strokeStyle = '#4fc059' @@ -1725,18 +1705,18 @@ const JIT = { ctx.stroke() }, // renderMidArrow renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { - var self = JIT + const self = JIT - var directionCat = synapse.get('category') - var direction = synapse.getDirection() + const directionCat = synapse.get('category') + const direction = synapse.getDirection() - var pos = adj.nodeFrom.pos.getc(true) - var posChild = adj.nodeTo.pos.getc(true) + const pos = adj.nodeFrom.pos.getc(true) + const posChild = adj.nodeTo.pos.getc(true) - // plot arrow edge + // plot arrow edge if (!direction) { // render nothing for this arrow if the direction couldn't be retrieved - } else if (directionCat == 'none') { + } else if (directionCat === 'none') { edgeHelper.line.render({ x: pos.x, y: pos.y @@ -1744,7 +1724,7 @@ const JIT = { x: posChild.x, y: posChild.y }, canvas) - } else if (directionCat == 'both') { + } else if (directionCat === 'both') { self.renderMidArrow({ x: pos.x, y: pos.y @@ -1759,8 +1739,8 @@ const JIT = { x: posChild.x, y: posChild.y }, 13, false, canvas, 0.7) - } else if (directionCat == 'from-to') { - var inv = (direction[0] != adj.nodeFrom.id) + } else if (directionCat === 'from-to') { + const inv = (direction[0] !== adj.nodeFrom.id) self.renderMidArrow({ x: pos.x, y: pos.y @@ -1786,38 +1766,37 @@ const JIT = { $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { - var offsetScale = canvas.scaleOffsetX + const offsetScale = canvas.scaleOffsetX canvas.scale(1 / offsetScale, 1 / offsetScale) - var offsetX = canvas.translateOffsetX - var offsetY = canvas.translateOffsetY + const offsetX = canvas.translateOffsetX + const offsetY = canvas.translateOffsetY canvas.translate(-1 * offsetX, -1 * offsetY) }, zoomToBox: function (event) { - var sX = Mouse.boxStartCoordinates.x, - sY = Mouse.boxStartCoordinates.y, - eX = Mouse.boxEndCoordinates.x, - eY = Mouse.boxEndCoordinates.y + const sX = Mouse.boxStartCoordinates.x + const sY = Mouse.boxStartCoordinates.y + const eX = Mouse.boxEndCoordinates.x + const eY = Mouse.boxEndCoordinates.y - var canvas = Visualize.mGraph.canvas + let canvas = Visualize.mGraph.canvas JIT.centerMap(canvas) - var height = $(document).height(), - width = $(document).width() + let height = $(document).height() + let width = $(document).width() - var spanX = Math.abs(sX - eX) - var spanY = Math.abs(sY - eY) - var ratioX = width / spanX - var ratioY = height / spanY + let spanX = Math.abs(sX - eX) + let spanY = Math.abs(sY - eY) + let ratioX = width / spanX + let ratioY = height / spanY - var newRatio = Math.min(ratioX, ratioY) + let newRatio = Math.min(ratioX, ratioY) if (canvas.scaleOffsetX * newRatio <= 5 && canvas.scaleOffsetX * newRatio >= 0.2) { canvas.scale(newRatio, newRatio) - } - else if (canvas.scaleOffsetX * newRatio > 5) { + } else if (canvas.scaleOffsetX * newRatio > 5) { newRatio = 5 / canvas.scaleOffsetX canvas.scale(newRatio, newRatio) } else { @@ -1825,8 +1804,8 @@ const JIT = { canvas.scale(newRatio, newRatio) } - var cogX = (sX + eX) / 2 - var cogY = (sY + eY) / 2 + const cogX = (sX + eX) / 2 + const cogY = (sY + eY) / 2 canvas.translate(-1 * cogX, -1 * cogY) $(document).trigger(JIT.events.zoom, [event]) @@ -1837,9 +1816,13 @@ const JIT = { }, zoomExtents: function (event, canvas, denySelected) { JIT.centerMap(canvas) - var height = canvas.getSize().height, - width = canvas.getSize().width, - maxX, minX, maxY, minY, counter = 0 + let height = canvas.getSize().height + let width = canvas.getSize().width + let maxX + let maxY + let minX + let minY + let counter = 0 let nodes if (!denySelected && Selected.Nodes.length > 0) { @@ -1850,30 +1833,30 @@ const JIT = { if (nodes.length > 1) { nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y + let x = n.pos.x + let y = n.pos.y - if (counter == 0 && n.getData('alpha') == 1) { + if (counter === 0 && n.getData('alpha') === 1) { maxX = x minX = x maxY = y minY = y } - var arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n'), - dim = n.getData('dim'), - ctx = canvas.getCtx() + let arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n') + let dim = n.getData('dim') + let ctx = canvas.getCtx() - var height = 25 * arrayOfLabelLines.length + let height = 25 * arrayOfLabelLines.length - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 8 + let width = Math.max.apply(null, lineWidths) + 8 // only adjust these values if the node is not filtered - if (n.getData('alpha') == 1) { + if (n.getData('alpha') === 1) { maxX = Math.max(x + width / 2, maxX) maxY = Math.max(y + n.getData('height') + 5 + height, maxY) minX = Math.min(x - width / 2, minX) @@ -1883,23 +1866,22 @@ const JIT = { } }) - var spanX = maxX - minX - var spanY = maxY - minY - var ratioX = spanX / width - var ratioY = spanY / height + let spanX = maxX - minX + let spanY = maxY - minY + let ratioX = spanX / width + let ratioY = spanY / height - var cogX = (maxX + minX) / 2 - var cogY = (maxY + minY) / 2 + let cogX = (maxX + minX) / 2 + let cogY = (maxY + minY) / 2 canvas.translate(-1 * cogX, -1 * cogY) - var newRatio = Math.max(ratioX, ratioY) - var scaleMultiplier = 1 / newRatio * 0.9 + let newRatio = Math.max(ratioX, ratioY) + let scaleMultiplier = 1 / newRatio * 0.9 if (canvas.scaleOffsetX * scaleMultiplier <= 3 && canvas.scaleOffsetX * scaleMultiplier >= 0.2) { canvas.scale(scaleMultiplier, scaleMultiplier) - } - else if (canvas.scaleOffsetX * scaleMultiplier > 3) { + } else if (canvas.scaleOffsetX * scaleMultiplier > 3) { scaleMultiplier = 3 / canvas.scaleOffsetX canvas.scale(scaleMultiplier, scaleMultiplier) } else { @@ -1908,11 +1890,10 @@ const JIT = { } $(document).trigger(JIT.events.zoom, [event]) - } - else if (nodes.length == 1) { + } else if (nodes.length === 1) { nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y + const x = n.pos.x + const y = n.pos.y canvas.translate(-1 * x, -1 * y) $(document).trigger(JIT.events.zoom, [event]) From d193c9a53cf905a611bb646c42994aa1510856d8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 5 Oct 2016 22:36:03 +0800 Subject: [PATCH 156/378] add starred to maps API (#719) * add starred to maps API and endpoint to create/delete * add token to requests without token param * add minor version number to api version * metacode/user use uri in schema * make code climate happier --- app/controllers/api/v2/stars_controller.rb | 29 ++++++++++++++++++++++ app/models/map.rb | 13 ++++------ app/models/star.rb | 1 + app/serializers/api/v2/map_serializer.rb | 5 ++++ config/routes.rb | 5 +++- doc/api/api.raml | 2 +- doc/api/apis/maps.raml | 12 +++++++++ doc/api/examples/map.json | 1 + doc/api/examples/maps.json | 1 + doc/api/schemas/_map.json | 4 +++ doc/api/schemas/_metacode.json | 1 + doc/api/schemas/_user.json | 1 + spec/api/v2/mappings_api_spec.rb | 3 ++- spec/api/v2/maps_api_spec.rb | 19 +++++++++++++- spec/api/v2/topics_api_spec.rb | 3 ++- spec/factories/stars.rb | 5 ++++ 16 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 app/controllers/api/v2/stars_controller.rb create mode 100644 spec/factories/stars.rb diff --git a/app/controllers/api/v2/stars_controller.rb b/app/controllers/api/v2/stars_controller.rb new file mode 100644 index 00000000..8b62ee36 --- /dev/null +++ b/app/controllers/api/v2/stars_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +module Api + module V2 + class StarsController < RestfulController + skip_before_action :load_resource + + def create + @map = Map.find(params[:id]) + @star = Star.new(user: current_user, map: @map) + authorize @map, :star? + create_action + + if @star.errors.empty? + render json: @map, scope: default_scope, serializer: MapSerializer, root: serializer_root + else + respond_with_errors + end + end + + def destroy + @map = Map.find(params[:id]) + authorize @map, :unstar? + @star = @map.stars.find_by(user: current_user) + @star.destroy if @star.present? + head :no_content + end + end + end +end diff --git a/app/models/map.rb b/app/models/map.rb index 609b1be4..a8e9c866 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -39,15 +39,8 @@ class Map < ApplicationRecord Perm.short(permission) end - # return an array of the contributors to the map def contributors - contributors = [] - - mappings.each do |m| - contributors.push(m.user) unless contributors.include?(m.user) - end - - contributors + mappings.map(&:user).uniq end def editors @@ -88,6 +81,10 @@ class Map < ApplicationRecord updated_at.strftime('%m/%d/%Y') end + def starred_by_user?(user) + user.stars.where(map: self).exists? + end + def as_json(_options = {}) json = super(methods: [:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str diff --git a/app/models/star.rb b/app/models/star.rb index dcaaa559..a49ae2b1 100644 --- a/app/models/star.rb +++ b/app/models/star.rb @@ -2,4 +2,5 @@ class Star < ActiveRecord::Base belongs_to :user belongs_to :map + validates :map, uniqueness: { scope: :user, message: 'You have already starred this map' } end diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index 0a0be2c0..ff641c69 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -7,9 +7,14 @@ module Api :desc, :permission, :screenshot, + :starred, :created_at, :updated_at + def starred + object.starred_by_user?(scope[:current_user]) + end + def self.embeddable { user: {}, diff --git a/config/routes.rb b/config/routes.rb index 76158105..05fe5845 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,7 +65,10 @@ Metamaps::Application.routes.draw do namespace :v2, path: '/v2' do resources :metacodes, only: [:index, :show] resources :mappings, only: [:index, :create, :show, :update, :destroy] - resources :maps, only: [:index, :create, :show, :update, :destroy] + resources :maps, only: [:index, :create, :show, :update, :destroy] do + post :stars, to: 'stars#create', on: :member + delete :stars, to: 'stars#destroy', on: :member + end resources :synapses, only: [:index, :create, :show, :update, :destroy] resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection diff --git a/doc/api/api.raml b/doc/api/api.raml index 50c2c992..6ffa29f1 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -1,7 +1,7 @@ #%RAML 1.0 --- title: Metamaps -version: v2 +version: v2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index b742adce..434dcc67 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -94,3 +94,15 @@ post: responses: 204: description: No content + /stars: + post: + responses: + 201: + description: Created + body: + application/json: + example: !include ../examples/map.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/examples/map.json b/doc/api/examples/map.json index fe3796ca..20e63204 100644 --- a/doc/api/examples/map.json +++ b/doc/api/examples/map.json @@ -5,6 +5,7 @@ "desc": "Example map for the API", "permission": "commons", "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "starred": false, "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "topic_ids": [ diff --git a/doc/api/examples/maps.json b/doc/api/examples/maps.json index 8b963990..501c0325 100644 --- a/doc/api/examples/maps.json +++ b/doc/api/examples/maps.json @@ -6,6 +6,7 @@ "desc": "Example map for the API", "permission": "commons", "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "starred": false, "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "topic_ids": [ diff --git a/doc/api/schemas/_map.json b/doc/api/schemas/_map.json index 469b4dbe..1234122d 100644 --- a/doc/api/schemas/_map.json +++ b/doc/api/schemas/_map.json @@ -18,6 +18,9 @@ "format": "uri", "type": "string" }, + "starred": { + "type": "boolean" + }, "created_at": { "$ref": "_datetimestamp.json" }, @@ -61,6 +64,7 @@ "desc", "permission", "screenshot", + "starred", "created_at", "updated_at" ] diff --git a/doc/api/schemas/_metacode.json b/doc/api/schemas/_metacode.json index cc6b4f76..2001be8e 100644 --- a/doc/api/schemas/_metacode.json +++ b/doc/api/schemas/_metacode.json @@ -12,6 +12,7 @@ "type": "string" }, "icon": { + "format": "uri", "type": "string" } }, diff --git a/doc/api/schemas/_user.json b/doc/api/schemas/_user.json index e5805251..ee2ef14f 100644 --- a/doc/api/schemas/_user.json +++ b/doc/api/schemas/_user.json @@ -9,6 +9,7 @@ "type": "string" }, "avatar": { + "format": "uri", "type": "string" }, "generation": { diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index 4d802865..6f225c6a 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -16,7 +16,8 @@ RSpec.describe 'mappings API', type: :request do end it 'GET /api/v2/mappings/:id' do - get "/api/v2/mappings/#{mapping.id}" + get "/api/v2/mappings/#{mapping.id}", params: { access_token: token } + expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index 77cbc24b..abed255d 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'maps API', type: :request do end it 'GET /api/v2/maps/:id' do - get "/api/v2/maps/#{map.id}" + get "/api/v2/maps/#{map.id}", params: { access_token: token } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:map) @@ -45,6 +45,23 @@ RSpec.describe 'maps API', type: :request do expect(Map.count).to eq 0 end + it 'POST /api/v2/maps/:id/stars' do + post "/api/v2/maps/#{map.id}/stars", params: { access_token: token } + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(user.stars.count).to eq 1 + expect(map.stars.count).to eq 1 + end + + it 'DELETE /api/v2/maps/:id/stars' do + create(:star, map: map, user: user) + delete "/api/v2/maps/#{map.id}/stars", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(user.stars.count).to eq 0 + expect(map.stars.count).to eq 0 + end + context 'RAML example' do let(:resource) { get_json_example(:map) } let(:collection) { get_json_example(:maps) } diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb index 9811071d..3f781df9 100644 --- a/spec/api/v2/topics_api_spec.rb +++ b/spec/api/v2/topics_api_spec.rb @@ -16,7 +16,8 @@ RSpec.describe 'topics API', type: :request do end it 'GET /api/v2/topics/:id' do - get "/api/v2/topics/#{topic.id}" + get "/api/v2/topics/#{topic.id}", params: { access_token: token } + expect(response).to have_http_status(:success) expect(response).to match_json_schema(:topic) diff --git a/spec/factories/stars.rb b/spec/factories/stars.rb new file mode 100644 index 00000000..60b10cf1 --- /dev/null +++ b/spec/factories/stars.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :star do + end +end From 8d613eab33cbdb1a0a037a8a77ee445cb1a724d6 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 10:38:16 -0400 Subject: [PATCH 157/378] improve descriptors --- doc/api/apis/maps.raml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 434dcc67..3ae05d2e 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -19,9 +19,9 @@ post: screenshot: description: url to a screenshot of the map contributor_ids: - description: the topic being linked from + description: the ids of people who have contributed to the map collaborator_ids: - description: the topic being linked to + description: the ids of people who have edit access to the map responses: 201: body: @@ -52,12 +52,6 @@ post: screenshot: description: url to a screenshot of the map required: false - contributor_ids: - description: the topic being linked from - required: false - collaborator_ids: - description: the topic being linked to - required: false responses: 200: body: @@ -79,12 +73,6 @@ post: screenshot: description: url to a screenshot of the map required: false - contributor_ids: - description: the topic being linked from - required: false - collaborator_ids: - description: the topic being linked to - required: false responses: 200: body: From 6d6a5099e96e76b1f790a0726d9728abdf951ec1 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 10:45:39 -0400 Subject: [PATCH 158/378] Enable access to Most Used and Recently Used metacodes in lists and carousel (#708) * used and recent * enable most used and recent in all metacode select situations * selected changed to active at some point * switch recent and most used positions * remove index doc page --- app/helpers/application_helper.rb | 19 +++++++- app/models/user.rb | 22 +++++++++ app/views/shared/_metacodeoptions.html.erb | 26 +++++++++++ app/views/shared/_switchmetacodes.html.erb | 54 ++++++++++++++++++++-- frontend/src/Metamaps/Create.js | 2 +- 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1c9b4da5..03cbfbf4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,13 +3,22 @@ module ApplicationHelper def metacodeset metacodes = current_user.settings.metacodes return false unless metacodes[0].include?('metacodeset') + if metacodes[0].sub('metacodeset-', '') == 'Most' + return 'Most' + elsif metacodes[0].sub('metacodeset-', '') == 'Recent' + return 'Recent' + end MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i) end def user_metacodes @m = current_user.settings.metacodes set = metacodeset - @metacodes = if set + @metacodes = if set && set == 'Most' + Metacode.where(id: current_user.mostUsedMetacodes).to_a + elsif set && set == 'Recent' + Metacode.where(id: current_user.recentMetacodes).to_a + elsif set set.metacodes.to_a else Metacode.where(id: @m).to_a @@ -17,6 +26,14 @@ module ApplicationHelper @metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end + def user_most_used_metacodes + @metacodes = current_user.mostUsedMetacodes.map { |id| Metacode.find(id) } + end + + def user_recent_metacodes + @metacodes = current_user.recentMetacodes.map { |id| Metacode.find(id) } + end + def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end diff --git a/app/models/user.rb b/app/models/user.rb index 4da66e57..4f679c1b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -64,6 +64,28 @@ class User < ApplicationRecord json['rtype'] = 'mapper' json end + + def recentMetacodes + array = [] + self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| + if array.length < 5 and array.index(t.metacode_id) == nil + array.push(t.metacode_id) + end + end + array + end + + def mostUsedMetacodes + self.topics.to_a.reduce({}) { |memo, topic| + if memo[topic.metacode_id] == nil + memo[topic.metacode_id] = 1 + else + memo[topic.metacode_id] = memo[topic.metacode_id] + 1 + end + + memo + }.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5) + end # generate a random 8 letter/digit code that they can use to invite people def generate_code diff --git a/app/views/shared/_metacodeoptions.html.erb b/app/views/shared/_metacodeoptions.html.erb index a6092c3e..54fb9e48 100644 --- a/app/views/shared/_metacodeoptions.html.erb +++ b/app/views/shared/_metacodeoptions.html.erb @@ -5,6 +5,32 @@ <div id="metacodeOptions"> <ul> + <li> + <span>Recently Used</span> + <div class="expandMetacodeSet"></div> + <ul> + <% user_recent_metacodes().each do |m| %> + <li data-id="<%= m.id.to_s %>"> + <img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" /> + <div class="mSelectName"><%= m.name %></div> + <div class="clearfloat"></div> + </li> + <% end %> + </ul> + </li> + <li> + <span>Most Used</span> + <div class="expandMetacodeSet"></div> + <ul> + <% user_most_used_metacodes().each do |m| %> + <li data-id="<%= m.id.to_s %>"> + <img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" /> + <div class="mSelectName"><%= m.name %></div> + <div class="clearfloat"></div> + </li> + <% end %> + </ul> + </li> <% MetacodeSet.order("name").all.each do |set| %> <li> <span><%= set.name %></span> diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index bd6b8129..24739716 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -7,10 +7,14 @@ <% selectedSet = metacodes[0].include?("metacodeset") ? metacodes[0].sub("metacodeset-","") : "custom" %> <% allMetacodeSets = MetacodeSet.order("name").all.to_a %> <% if selectedSet == "custom" - index = allMetacodeSets.length + index = allMetacodeSets.length + 2 + elsif selectedSet == 'Recent' + index = 0 + elsif selectedSet == 'Most' + index = 1 else set = MetacodeSet.find(selectedSet.to_i) - index = allMetacodeSets.index(set) + index = allMetacodeSets.index(set) + 2 end %> <h3>Switch Metacode Set</h3> @@ -18,11 +22,53 @@ <div id="metacodeSwitchTabs"> <ul> + <li><a href="#metacodeSwitchTabsRecent" data-set-id="recent" id="metacodeSetRecent">RECENTLY USED</a></li> + <li><a href="#metacodeSwitchTabsMost" data-set-id="most" id="metacodeSetMost">MOST USED</a></li> <% allMetacodeSets.each do |m| %> <li><a href="#metacodeSwitchTabs<%= m.id %>" data-set-id="<%= m.id %>"><%= m.name %></a></li> <% end %> <li><a href="#metacodeSwitchTabsCustom" data-set-id="custom" id="metacodeSetCustom">CUSTOM SELECTION</a></li> </ul> + <% recent = user_recent_metacodes() %> + <div id="metacodeSwitchTabsRecent" + data-metacodes="<%= recent.map(&:id).join(',') %>"> + <% @list = '' %> + <% recent.each_with_index do |m, index| %> + <% @list += '<li><img src="' + asset_path(m.icon) + '" alt="' + m.name + '" /><p>' + m.name.downcase + '</p><div class="clearfloat"></div></li>' %> + <% end %> + <div class="metacodeSwitchTab"> + <p class="setDesc">The 5 Metacodes you've used most recently.</p> + <div class="metacodeSetList"> + <ul> + <%= @list.html_safe %> + </ul> + <div class="clearfloat"></div> + </div> + </div> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('Recent', 0, false);"> + Switch Set + </button> + </div> + <% most_used = user_most_used_metacodes() %> + <div id="metacodeSwitchTabsMost" + data-metacodes="<%= most_used.map(&:id).join(',') %>"> + <% @list = '' %> + <% most_used.each_with_index do |m, index| %> + <% @list += '<li><img src="' + asset_path(m.icon) + '" alt="' + m.name + '" /><p>' + m.name.downcase + '</p><div class="clearfloat"></div></li>' %> + <% end %> + <div class="metacodeSwitchTab"> + <p class="setDesc">The 5 Metacodes you've used the most.</p> + <div class="metacodeSetList"> + <ul> + <%= @list.html_safe %> + </ul> + <div class="clearfloat"></div> + </div> + </div> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('Most', 1, false);"> + Switch Set + </button> + </div> <% allMetacodeSets.each_with_index do |m, localindex| %> <div id="metacodeSwitchTabs<%= m.id %>" data-metacodes="<%= m.metacodes.map(&:id).join(',') %>"> @@ -39,7 +85,7 @@ <div class="clearfloat"></div> </div> </div> - <button class="button" onclick="Metamaps.Create.updateMetacodeSet(<%= m.id %>, <%= localindex %>, false);"> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet(<%= m.id %>, <%= localindex + 2 %>, false);"> Switch Set </button> </div> @@ -62,7 +108,7 @@ </ul> <div class="clearfloat"></div> </div> - <button class="button" onclick="Metamaps.Create.updateMetacodeSet('custom', <%= allMetacodeSets.length %>, true);"> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('custom', <%= allMetacodeSets.length + 2 %>, true);"> Switch to Custom Set </button> </div> diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 92271223..87b91540 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -32,7 +32,7 @@ const Create = { // // SWITCHING METACODE SETS $('#metacodeSwitchTabs').tabs({ - selected: self.selectedMetacodeSetIndex + active: self.selectedMetacodeSetIndex }).addClass('ui-tabs-vertical ui-helper-clearfix') $('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab From c256d0891b78f13dfc050a7c9a4a64c635bce44c Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:17:04 -0400 Subject: [PATCH 159/378] dont conflict message sending with topic creation --- frontend/src/Metamaps/Listeners.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 2eb092dd..f78a030b 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -18,7 +18,10 @@ const Listeners = { switch (e.which) { case 13: // if enter key is pressed - JIT.enterKeyHandler() + // prevent topic creation if sending a message + if (e.target.className !== 'chat-input') { + JIT.enterKeyHandler() + } e.preventDefault() break case 27: // if esc key is pressed From 0cfbe41d95acc52b7ec8604166d5712004fedd91 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:22:38 -0400 Subject: [PATCH 160/378] don't prevent all right clicking --- frontend/src/Metamaps/Map/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index dc9b4eb8..4c2f78bb 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -40,11 +40,6 @@ const Map = { init: function () { var self = Map - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#wrapper').on('contextmenu', function (e) { - return false - }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 98fae4b7213fa4e4a8beca9b295c53bc4e607604 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:28:37 -0400 Subject: [PATCH 161/378] fixes #711 toast button styling --- app/assets/stylesheets/application.css.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index d6d80201..97276b9f 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -142,6 +142,7 @@ button.button.btn-no:hover { .toast .toast-button { margin-top: -10px; margin-left: 10px; + margin-bottom: -10px; } /* * Utility From eb4073c22818567f1a11e06a695f887f5141cbb9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 11:18:55 +0800 Subject: [PATCH 162/378] word wrap on chat message text. Fixes #726 --- app/assets/stylesheets/junto.css.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb index 22471b19..91b610fc 100644 --- a/app/assets/stylesheets/junto.css.erb +++ b/app/assets/stylesheets/junto.css.erb @@ -339,6 +339,7 @@ margin-top: 12px; padding: 2px 8px 0; text-align: left; + word-wrap: break-word; } .chat-box .chat-messages .chat-message .chat-message-time { float: right; From c0a220abc9921a41b35f5746f0b03f712fc68154 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 11:52:05 +0800 Subject: [PATCH 163/378] allow synapses to be imported by topic name as well as id --- frontend/src/Metamaps/Import.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 6788335f..91da38cf 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -253,12 +253,16 @@ const Import = { parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted + // if there isn't a cidMapping, check by topic name instead var topic1 = Metamaps.Topics.get(self.cidMappings[synapse.topic1]) + if (!topic1) topic1 = Metamaps.Topics.findWhere({ name: synapse.topic1 }) var topic2 = Metamaps.Topics.get(self.cidMappings[synapse.topic2]) + if (!topic1) topic1 = Metamaps.Topics.findWhere({ name: synapse.topic1 }) + if (!topic1 || !topic2) { console.error("One of the two topics doesn't exist!") console.error(synapse) - return true + return // next } // ensure imported topics have a chance to get a real id attr before creating synapses @@ -407,6 +411,7 @@ const Import = { normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { let newKey = key.toLowerCase() + newKey = newKey.replace(/\s/g, '') // remove whitespace if (newKey === 'url') key = 'link' if (newKey === 'title') key = 'name' if (newKey === 'description') key = 'desc' From b4d12509595e04aa3cb76cd557f74b2b15eb075b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 12:02:14 +0800 Subject: [PATCH 164/378] share normalizeKey between TSV, CSV, and JSON --- frontend/src/Metamaps/Import.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 91da38cf..f70a1290 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -27,7 +27,7 @@ const Import = { 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission' ], synapseWhitelist: [ - 'topic1', 'topic2', 'category', 'desc', 'description', 'permission' + 'topic1', 'topic2', 'category', 'direction', 'desc', 'description', 'permission' ], cidMappings: {}, // to be filled by import_id => cid mappings @@ -59,7 +59,7 @@ const Import = { console.warn(err) return topicsPromise.resolve([]) } - topicsPromise.resolve(data.map(row => self.normalizeKeys(row))) + topicsPromise.resolve(data) }) const synapsesPromise = $.Deferred() @@ -68,7 +68,7 @@ const Import = { console.warn(err) return synapsesPromise.resolve([]) } - synapsesPromise.resolve(data.map(row => self.normalizeKeys(row))) + synapsesPromise.resolve(data) }) $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { @@ -83,8 +83,8 @@ const Import = { handle: function(results) { var self = Import - var topics = results.topics - var synapses = results.synapses + var topics = results.topics.map(topic => self.normalizeKeys(topic)) + var synapses = results.synapses.map(synapse => self.normalizeKeys(synapse)) if (topics.length > 0 || synapses.length > 0) { if (window.confirm('Are you sure you want to create ' + topics.length + @@ -149,7 +149,7 @@ const Import = { state = STATES.ABORT } topicHeaders = line.map(function (header, index) { - return header.toLowerCase().replace('description', 'desc') + return self.normalizeKey(header) }) state = STATES.TOPICS break @@ -160,7 +160,7 @@ const Import = { state = STATES.ABORT } synapseHeaders = line.map(function (header, index) { - return header.toLowerCase().replace('description', 'desc') + return self.normalizeKey(header) }) state = STATES.SYNAPSES break @@ -406,15 +406,20 @@ const Import = { .toLowerCase() }, + normalizeKey: function(key) { + let newKey = key.toLowerCase() + newKey = newKey.replace(/\s/g, '') // remove whitespace + if (newKey === 'url') newKey = 'link' + if (newKey === 'title') newKey = 'name' + if (newKey === 'description') newKey = 'desc' + if (newKey === 'direction') newKey = 'category' + return newKey + }, // thanks to http://stackoverflow.com/a/25290114/5332286 normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { - let newKey = key.toLowerCase() - newKey = newKey.replace(/\s/g, '') // remove whitespace - if (newKey === 'url') key = 'link' - if (newKey === 'title') key = 'name' - if (newKey === 'description') key = 'desc' + const newKey = Import.normalizeKey(key) result[newKey] = val }) } From 33bcfc150542b5226af215ead0a96bf49b142b97 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 00:02:42 +0800 Subject: [PATCH 165/378] move Maps into a folder --- frontend/src/components/{ => Maps}/Header.js | 0 frontend/src/components/{ => Maps}/MapCard.js | 0 frontend/src/components/{ => Maps}/MapListItem.js | 0 frontend/src/components/{ => Maps}/MapperCard.js | 0 frontend/src/components/{Maps.js => Maps/index.js} | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/components/{ => Maps}/Header.js (100%) rename frontend/src/components/{ => Maps}/MapCard.js (100%) rename frontend/src/components/{ => Maps}/MapListItem.js (100%) rename frontend/src/components/{ => Maps}/MapperCard.js (100%) rename frontend/src/components/{Maps.js => Maps/index.js} (91%) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Maps/Header.js similarity index 100% rename from frontend/src/components/Header.js rename to frontend/src/components/Maps/Header.js diff --git a/frontend/src/components/MapCard.js b/frontend/src/components/Maps/MapCard.js similarity index 100% rename from frontend/src/components/MapCard.js rename to frontend/src/components/Maps/MapCard.js diff --git a/frontend/src/components/MapListItem.js b/frontend/src/components/Maps/MapListItem.js similarity index 100% rename from frontend/src/components/MapListItem.js rename to frontend/src/components/Maps/MapListItem.js diff --git a/frontend/src/components/MapperCard.js b/frontend/src/components/Maps/MapperCard.js similarity index 100% rename from frontend/src/components/MapperCard.js rename to frontend/src/components/Maps/MapperCard.js diff --git a/frontend/src/components/Maps.js b/frontend/src/components/Maps/index.js similarity index 91% rename from frontend/src/components/Maps.js rename to frontend/src/components/Maps/index.js index 7931da5d..2c3e8ba1 100644 --- a/frontend/src/components/Maps.js +++ b/frontend/src/components/Maps/index.js @@ -1,8 +1,8 @@ import React, { Component, PropTypes } from 'react' -import Header from './Header.js' -import MapperCard from './MapperCard.js' -import MapCard from './MapCard.js' -import MapListItem from './MapListItem.js' +import Header from './Header' +import MapperCard from './MapperCard' +import MapCard from './MapCard' +import MapListItem from './MapListItem' class Maps extends Component { render = () => { From 518773d6e1c98008a37e3a204efebd798f10c9e6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 00:27:04 +0800 Subject: [PATCH 166/378] pop up a lightbox using React to help you export --- app/assets/images/import-example.png | Bin 0 -> 57176 bytes app/assets/images/import.png | Bin 0 -> 320 bytes .../javascripts/src/Metamaps.Erb.js.erb | 1 + ...plication.css.erb => application.scss.erb} | 31 ++++++-- app/assets/stylesheets/clean.css.erb | 36 +++++---- app/assets/stylesheets/mobile.scss.erb | 2 +- app/views/layouts/_lowermapelements.html.erb | 1 + .../src/Metamaps/GlobalUI/ImportDialog.js | 36 +++++++++ frontend/src/Metamaps/GlobalUI/index.js | 4 +- frontend/src/Metamaps/Import.js | 1 + frontend/src/Metamaps/Map/index.js | 14 +++- frontend/src/Metamaps/PasteInput.js | 27 ++++--- frontend/src/Metamaps/index.js | 3 +- frontend/src/components/ImportDialogBox.js | 75 ++++++++++++++++++ package.json | 1 + 15 files changed, 195 insertions(+), 37 deletions(-) create mode 100644 app/assets/images/import-example.png create mode 100644 app/assets/images/import.png rename app/assets/stylesheets/{application.css.erb => application.scss.erb} (99%) create mode 100644 frontend/src/Metamaps/GlobalUI/ImportDialog.js create mode 100644 frontend/src/components/ImportDialogBox.js diff --git a/app/assets/images/import-example.png b/app/assets/images/import-example.png new file mode 100644 index 0000000000000000000000000000000000000000..3f013d58ce972f7236cd3d49f9aab47458cc299f GIT binary patch literal 57176 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/import.png b/app/assets/images/import.png new file mode 100644 index 0000000000000000000000000000000000000000..5c66e984a88e7f7f8408c44bf4eb999428cee9c8 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsi;YD~aiT-hSD?^YPZ!6Kh{JEM8G0Rd6gl!RKgmIu z+mXdHQSk{!n2^W(;u)8`KJZnE@=v>T%BvvvnwwCEV!c@6@>`D%{(ie>`{89%7A<v? zTForOGvUb=Nry>>&phKM=|ou+^jWi9_e^8A{y9B;hRTkkQu8eiv&mOzFWtlc_5h3a zoGZ@^4$GWYnS7Ap)!Y3An-@Qk3oo^2D@^EGZuLF3>PYIOivkS%J8mq_>&+~Ev%~vG zw#s_@=Rv26`ycUr$Y;D%@3NNh*l(7z8@K3g_{Cx6aK4f60^>f$*Y8xGBo%vZ0(y+W M)78&qol`;+0AW~si2wiq literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index e8f3a25b..916d175a 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -14,6 +14,7 @@ Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Erb['import-example.png'] = '<%= asset_path('import-example.png') %>' Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.scss.erb similarity index 99% rename from app/assets/stylesheets/application.css.erb rename to app/assets/stylesheets/application.scss.erb index 97276b9f..e4e7762e 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -1526,9 +1526,8 @@ h3.filterBox { background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } /* map info box */ -/* map info box */ -.wrapper div.mapInfoBox { +.wrapper .mapInfoBox { display: none; position: absolute; bottom: 40px; @@ -1536,12 +1535,34 @@ h3.filterBox { background-color: #424242; color: #F5F5F5; border-radius: 2px; + box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16); + text-align: center; + font-style: normal; +} +.import-dialog{ + button { + margin: 1em 0.5em; + } + .import-blue-button { + display: inline-block; + box-sizing: border-box; + margin: 0.75em; + padding: 0.75em; + height: 3em; + background-color: #AAB0FB; + border-radius: 0.3em; + color: white; + cursor: pointer; + } + .fileupload { + width: 75%; + text-align: center; + } +} +.wrapper .mapInfoBox { width: 360px; min-height: 300px; padding: 0; - font-style: normal; - text-align: center; - box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16); } .requestTitle { display: none; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index b41f14ca..deb2719f 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -188,7 +188,7 @@ .upperRightIcon { width: 32px; height: 32px; - background-image: url(<%= asset_data_uri('topright_sprite.png') %>); + background-image: url(<%= asset_path('topright_sprite.png') %>); background-repeat: no-repeat; cursor: pointer; } @@ -325,7 +325,7 @@ } .fullWidthWrapper.withPartners { - background: url(<%= asset_data_uri('homepage_bg_fade.png') %>) no-repeat center -300px; + background: url(<%= asset_path('homepage_bg_fade.png') %>) no-repeat center -300px; } .homeWrapper.homePartners { padding: 64px 0 280px; @@ -364,7 +364,7 @@ cursor: pointer; } .openCheatsheet { - background-image: url(<%= asset_data_uri('help_sprite.png') %>); + background-image: url(<%= asset_path('help_sprite.png') %>); background-repeat:no-repeat; } .openCheatsheet:hover { @@ -373,7 +373,7 @@ .mapInfoIcon { position: relative; top: 56px; /* puts it just offscreen */ - background-image: url(<%= asset_data_uri('mapinfo_sprite.png') %>); + background-image: url(<%= asset_path('mapinfo_sprite.png') %>); background-repeat:no-repeat; } .mapInfoIcon:hover { @@ -382,8 +382,14 @@ .mapPage .mapInfoIcon { top: 0; } +.importDialog { + background-image: url(<%= asset_path('import.png') %>); + background-position: 0 0; + background-repeat: no-repeat; + width: 32px; +} .starMap { - background-image: url(<%= asset_data_uri('starmap_sprite.png') %>); + background-image: url(<%= asset_path('starmap_sprite.png') %>); background-position: 0 0; background-repeat: no-repeat; width: 32px; @@ -437,7 +443,7 @@ .takeScreenshot { margin-bottom: 5px; border-radius: 2px; - background-image: url(<%= asset_data_uri 'screenshot_sprite.png' %>); + background-image: url(<%= asset_path 'screenshot_sprite.png' %>); display: none; } .takeScreenshot:hover { @@ -450,7 +456,7 @@ .zoomExtents { margin-bottom:5px; border-radius: 2px; - background-image: url(<%= asset_data_uri('extents_sprite.png') %>); + background-image: url(<%= asset_path('extents_sprite.png') %>); } .zoomExtents:hover { @@ -458,7 +464,7 @@ } .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, - .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { + .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, importDialog:hover .tooltipsAbove, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -623,7 +629,7 @@ } .zoomIn { - background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); + background-image: url(<%= asset_path('zoom_sprite.png') %>); background-position: 0 /…0; border-top-left-radius: 2px; border-top-right-radius: 2px; @@ -632,7 +638,7 @@ background-position: -32px 0; } .zoomOut { - background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); + background-image: url(<%= asset_path('zoom_sprite.png') %>); background-position:0 -32px; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; @@ -740,23 +746,23 @@ left:5px; } .exploreMapsCenter .myMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -32px 0; } .exploreMapsCenter .sharedMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -128px 0; } .exploreMapsCenter .activeMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: 0 0; } .exploreMapsCenter .featuredMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } .exploreMapsCenter .starredMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } .myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon { diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index cf416e37..a646fea4 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -56,7 +56,7 @@ width: 100%; } - .wrapper div.mapInfoBox { + .wrapper .mapInfoBox { position: fixed; top: 50px; right: 0px; diff --git a/app/views/layouts/_lowermapelements.html.erb b/app/views/layouts/_lowermapelements.html.erb index fe120219..b8b7f868 100644 --- a/app/views/layouts/_lowermapelements.html.erb +++ b/app/views/layouts/_lowermapelements.html.erb @@ -8,6 +8,7 @@ <div class="infoAndHelp"> <%= render :partial => 'maps/mapinfobox' %> + <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"><div class="tooltipsAbove">Import data</div></div> <% starred = current_user && @map && current_user.starred_map?(@map) starClass = starred ? 'starred' : '' tooltip = starred ? 'Star' : 'Unstar' %> diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js new file mode 100644 index 00000000..3671cd90 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -0,0 +1,36 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import outdent from 'outdent' + +import ImportDialogBox from '../../components/ImportDialogBox' + +import PasteInput from '../PasteInput' + +const ImportDialog = { + openLightbox: null, + closeLightbox: null, + + init: function(serverData, openLightbox, closeLightbox) { + const self = ImportDialog + self.openLightbox = openLightbox + self.closeLightbox = closeLightbox + + $('#lightbox_content').append($(outdent` + <div class="lightboxContent" id="import-dialog-lightbox"> + <div class="importDialogWrapper" /> + </div> + `)) + ReactDOM.render(React.createElement(ImportDialogBox, { + onFileAdded: PasteInput.handleFile, + exampleImageUrl: serverData['import-example.png'], + }), $('.importDialogWrapper').get(0)) + }, + show: function() { + self.openLightbox('import-dialog') + }, + hide: function() { + self.closeLightbox('import-dialog') + } +} + +export default ImportDialog diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 3b16375e..4eb29bb7 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -6,6 +6,7 @@ import Create from '../Create' import Search from './Search' import CreateMap from './CreateMap' import Account from './Account' +import ImportDialog from './ImportDialog' /* * Metamaps.Backbone @@ -21,6 +22,7 @@ const GlobalUI = { self.Search.init() self.CreateMap.init() self.Account.init() + self.ImportDialog.init(Metamaps.Erb, self.openLightbox, self.closeLightbox) if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) @@ -141,5 +143,5 @@ const GlobalUI = { } } -export { Search, CreateMap, Account } +export { Search, CreateMap, Account, ImportDialog } export default GlobalUI diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index f70a1290..05d762e5 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -411,6 +411,7 @@ const Import = { newKey = newKey.replace(/\s/g, '') // remove whitespace if (newKey === 'url') newKey = 'link' if (newKey === 'title') newKey = 'name' + if (newKey === 'label') newKey = 'desc' if (newKey === 'description') newKey = 'desc' if (newKey === 'direction') newKey = 'category' return newKey diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 4c2f78bb..7d7322fc 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,6 +1,8 @@ /* global Metamaps, $ */ import outdent from 'outdent' +import React from 'react' +import ReactDOM from 'react-dom' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -40,6 +42,12 @@ const Map = { init: function () { var self = Map + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#wrapper').on('contextmenu', function (e) { + return false + }) + + $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() @@ -52,7 +60,7 @@ const Map = { GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() self.updateStar() - self.InfoBox.init() + InfoBox.init() CheatSheet.init() $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) @@ -102,7 +110,7 @@ const Map = { Selected.reset() // set the proper mapinfobox content - Map.InfoBox.load() + InfoBox.load() // these three update the actual filter box with the right list items Filter.checkMetacodes() @@ -132,7 +140,7 @@ const Map = { Create.newTopic.hide(true) // true means force (and override pinned) Create.newSynapse.hide() Filter.close() - Map.InfoBox.close() + InfoBox.close() Realtime.endActiveMap() } }, diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 6f1cc03f..51d4a933 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -21,16 +21,7 @@ const PasteInput = { e.preventDefault(); var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { - var fileReader = new window.FileReader() - fileReader.readAsText(e.dataTransfer.files[0]) - fileReader.onload = function(e) { - var text = e.currentTarget.result - if (text.substring(0,5) === '<?xml') { - // assume this is a macOS .webloc link - text = text.replace(/[\s\S]*<string>(.*)<\/string>[\s\S]*/m, '$1') - } - self.handle(text, coords) - } + self.handleFile(e.dataTransfer.files[0], coords) } // OMG import bookmarks 😍 if (e.dataTransfer.items.length > 0) { @@ -52,7 +43,21 @@ const PasteInput = { }) }, - handle: function(text, coords) { + handleFile: (file, coords = null) => { + var self = PasteInput + var fileReader = new FileReader() + fileReader.readAsText(file) + fileReader.onload = function(e) { + var text = e.currentTarget.result + if (text.substring(0,5) === '<?xml') { + // assume this is a macOS .webloc link + text = text.replace(/[\s\S]*<string>(.*)<\/string>[\s\S]*/m, '$1') + } + self.handle(text, coords) + } + }, + + handle: function(text, coords = null) { var self = PasteInput if (text.match(self.URL_REGEX)) { diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index d179713e..44bbfdb6 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -10,7 +10,7 @@ import Create from './Create' import Debug from './Debug' import Filter from './Filter' import GlobalUI, { - Search, CreateMap, Account as GlobalUI_Account + Search, CreateMap, ImportDialog, Account as GlobalUI_Account } from './GlobalUI' import Import from './Import' import JIT from './JIT' @@ -47,6 +47,7 @@ Metamaps.GlobalUI = GlobalUI Metamaps.GlobalUI.Search = Search Metamaps.GlobalUI.CreateMap = CreateMap Metamaps.GlobalUI.Account = GlobalUI_Account +Metamaps.GlobalUI.ImportDialog = ImportDialog Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js new file mode 100644 index 00000000..4d113ccc --- /dev/null +++ b/frontend/src/components/ImportDialogBox.js @@ -0,0 +1,75 @@ +import React, { PropTypes, Component } from 'react' +import Dropzone from 'react-dropzone' + +class ImportDialogBox extends Component { + constructor(props) { + super(props) + + this.state = { + showImportInstructions: false + } + } + + handleExport = format => () => { + window.open(`${window.location.pathname}/export.${format}`, '_blank') + } + + handleFile = (files, e) => { + // for some reason it uploads twice, so we need this debouncer + this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) + if (!this.debouncer) { + this.props.onFileAdded(files[0]) + } + } + + toggleShowInstructions = e => { + this.setState({ + showImportInstructions: !this.state.showImportInstructions + }) + } + + render = () => { + return ( + <div className="import-dialog"> + <h3>EXPORT</h3> + <div className="import-blue-button" onClick={this.handleExport('csv')}> + Export as CSV + </div> + <div className="import-blue-button" onClick={this.handleExport('json')}> + Export as JSON + </div> + <h3>IMPORT</h3> + <p>To upload a file, drop it here:</p> + <Dropzone onDropAccepted={this.handleFile} + className="import-blue-button fileupload" + > + Drop files here! + </Dropzone> + <p> + <a onClick={this.toggleShowInstructions} style={{ textDecoration: 'underline', cursor: 'pointer' }}> + Show/hide import instructions + </a> + </p> + {!this.state.showImportInstructions ? null : (<div> + <p> + You can import topics and synapses by uploading a spreadsheet here. + The file should be in comma-separated format (when you save, change the + filetype from .xls to .csv). + </p> + <img src={this.props.exampleImageUrl} style={{ maxWidth: '75%', float: 'right', margin: '1em' }}/> + <p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p> + <p> </p> + <p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p> + <p> * If you are importing a list of links, you can use a Link column in place of the Name column.</p> + </div>)} + </div> + ) + } +} + +ImportDialogBox.propTypes = { + onFileAdded: PropTypes.func, + exampleImageUrl: PropTypes.string +} + +export default ImportDialogBox diff --git a/package.json b/package.json index c882fdd0..82cd120d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", + "react-dropzone": "3.6.0", "socket.io": "0.9.12", "webpack": "1.13.2" }, From 38c323a18a719c31eee879389aedd72195ddf7ab Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 16:22:09 +0800 Subject: [PATCH 167/378] global lightbox css changes --- app/assets/stylesheets/application.scss.erb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index e4e7762e..2797cbeb 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -2049,17 +2049,17 @@ and it won't be important on password protected instances */ left: 0; width: 100%; height: 100%; - position: fixed; + position: absolute; z-index: 1000000; display: none; } #lightbox_main { width: 800px; - height: auto; margin: 0 auto; z-index: 2; position: relative; - top: 50%; + top: 5vh; + height: 90vh; background-color: transparent; color: black; } @@ -2098,8 +2098,11 @@ and it won't be important on password protected instances */ background-position: center center; } #lightbox_content { - width: 552px; - height: 434px; + width: 800px; + height: 500px; + max-height: 90vh; + box-sizing: border-box; + overflow-y: auto; background-color: #e0e0e0; padding: 64px 124px 64px 124px; box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19); From a79d6a824cc4b6e5d376419bdc746861003fcf36 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:07:46 -0400 Subject: [PATCH 168/378] dont do async: false (#731) * dont do async: false * account for case where callback isn't provided --- frontend/src/Metamaps/AutoLayout.js | 5 +- frontend/src/Metamaps/Backbone/index.js | 40 ----------- frontend/src/Metamaps/Import.js | 4 +- frontend/src/Metamaps/Realtime.js | 1 - frontend/src/Metamaps/Synapse.js | 60 +++++++---------- frontend/src/Metamaps/Topic.js | 88 ++++++++++--------------- 6 files changed, 61 insertions(+), 137 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index f3e91440..1408ba62 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -49,7 +49,7 @@ const AutoLayout = { } } - if (opts.map && self.coordsTaken(nextX, nextY, opts.map)) { + if (opts.mappings && self.coordsTaken(nextX, nextY, opts.mappings)) { // check if the coordinate is already taken on the current map return self.getNextCoord(opts) } else { @@ -59,8 +59,7 @@ const AutoLayout = { } } }, - coordsTaken: function (x, y, map) { - const mappings = map.getMappings() + coordsTaken: function (x, y, mappings) { if (mappings.findWhere({ xloc: x, yloc: y })) { return true } else { diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 1994c483..389d7dcf 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -85,46 +85,6 @@ _Backbone.Map = Backbone.Model.extend({ getUser: function () { return Mapper.get(this.get('user_id')) }, - fetchContained: function () { - var bb = _Backbone - var that = this - var start = function (data) { - that.set('mappers', new bb.MapperCollection(data.mappers)) - that.set('topics', new bb.TopicCollection(data.topics)) - that.set('synapses', new bb.SynapseCollection(data.synapses)) - that.set('mappings', new bb.MappingCollection(data.mappings)) - } - - $.ajax({ - url: '/maps/' + this.id + '/contains.json', - success: start, - async: false - }) - }, - getTopics: function () { - if (!this.get('topics')) { - this.fetchContained() - } - return this.get('topics') - }, - getSynapses: function () { - if (!this.get('synapses')) { - this.fetchContained() - } - return this.get('synapses') - }, - getMappings: function () { - if (!this.get('mappings')) { - this.fetchContained() - } - return this.get('mappings') - }, - getMappers: function () { - if (!this.get('mappers')) { - this.fetchContained() - } - return this.get('mappers') - }, updateView: function () { var map = Active.Map var isActiveMap = this.id === map.id diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index f70a1290..f3e872ec 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -227,7 +227,7 @@ const Import = { parsedTopics.forEach(function (topic) { let coords = { x: topic.x, y: topic.y } if (!coords.x || !coords.y) { - coords = AutoLayout.getNextCoord({ map: Active.Map }) + coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } if (!topic.name && topic.link || @@ -353,7 +353,7 @@ const Import = { handleURL: function (url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { - coords = AutoLayout.getNextCoord({ map: Active.Map }) + coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } const name = opts.name || 'Link' diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 6522d460..f905bb84 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -981,7 +981,6 @@ const Realtime = { else if (!couldEditBefore && canEditNow) { Map.canEditNow() } else { - model.fetchContained() model.trigger('changeByOther') } } diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 400cb0b0..8253d6ba 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -19,35 +19,21 @@ import Visualize from './Visualize' * - Metamaps.Topics */ +const noOp = () => {} const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve - get: function (id, callback) { + get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it if (Metamaps.Synapses.get(id) == undefined) { - if (!callback) { - var e = $.ajax({ - url: '/synapses/' + id + '.json', - async: false - }) - Metamaps.Synapses.add($.parseJSON(e.responseText)) - return Metamaps.Synapses.get(id) - } else { - return $.ajax({ - url: '/synapses/' + id + '.json', - success: function (data) { - Metamaps.Synapses.add(data) - callback(Metamaps.Synapses.get(id)) - } - }) - } - } else { - if (!callback) { - return Metamaps.Synapses.get(id) - } else { - return callback(Metamaps.Synapses.get(id)) - } - } + $.ajax({ + url: '/synapses/' + id + '.json', + success: function (data) { + Metamaps.Synapses.add(data) + callback(Metamaps.Synapses.get(id)) + } + }) + } else callback(Metamaps.Synapses.get(id)) }, /* * @@ -152,21 +138,19 @@ const Synapse = { node1, node2 - var synapse = self.get(id) - - var mapping = new Metamaps.Backbone.Mapping({ - mappable_type: 'Synapse', - mappable_id: synapse.id, + self.get(id, synapse => { + var mapping = new Metamaps.Backbone.Mapping({ + mappable_type: 'Synapse', + mappable_id: synapse.id, + }) + Metamaps.Mappings.add(mapping) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) + node1 = topic1.get('node') + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) + node2 = topic2.get('node') + Create.newSynapse.hide() + self.renderSynapse(mapping, synapse, node1, node2, true) }) - Metamaps.Mappings.add(mapping) - - topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) - node1 = topic1.get('node') - topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) - node2 = topic2.get('node') - Create.newSynapse.hide() - - self.renderSynapse(mapping, synapse, node1, node2, true) } } diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 34e2bb64..5be6cc57 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -28,37 +28,21 @@ import Visualize from './Visualize' * - Metamaps.Synapses * - Metamaps.Topics */ - +const noOp = () => {} const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve - get: function (id, callback) { + get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it if (Metamaps.Topics.get(id) == undefined) { - // console.log("Ajax call!") - if (!callback) { - var e = $.ajax({ - url: '/topics/' + id + '.json', - async: false - }) - Metamaps.Topics.add($.parseJSON(e.responseText)) - return Metamaps.Topics.get(id) - } else { - return $.ajax({ - url: '/topics/' + id + '.json', - success: function (data) { - Metamaps.Topics.add(data) - callback(Metamaps.Topics.get(id)) - } - }) - } - } else { - if (!callback) { - return Metamaps.Topics.get(id) - } else { - return callback(Metamaps.Topics.get(id)) - } - } + $.ajax({ + url: '/topics/' + id + '.json', + success: function (data) { + Metamaps.Topics.add(data) + callback(Metamaps.Topics.get(id)) + } + }) + } else callback(Metamaps.Topics.get(id)) }, launch: function (id) { var bb = Metamaps.Backbone @@ -192,7 +176,7 @@ const Topic = { }, // opts is additional options in a hash - // TODO: move createNewInDB and permitCerateSYnapseAfter into opts + // TODO: move createNewInDB and permitCreateSynapseAfter into opts renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { var self = Topic @@ -335,7 +319,7 @@ const Topic = { Metamaps.Topics.add(topic) if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord() + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Create.newTopic.x, @@ -357,40 +341,38 @@ const Topic = { Create.newTopic.hide() - var topic = self.get(id) + self.get(id, (topic) => { + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + } + var mapping = new Metamaps.Backbone.Mapping({ + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) - if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord() - } - var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Create.newTopic.y, - mappable_type: 'Topic', - mappable_id: topic.id, + self.renderTopic(mapping, topic, true, true) }) - Metamaps.Mappings.add(mapping) - - self.renderTopic(mapping, topic, true, true) }, getTopicFromSearch: function (event, id) { var self = Topic $(document).trigger(Map.events.editedByActiveMapper) - var topic = self.get(id) - - var nextCoords = AutoLayout.getNextCoord() - var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords.x, - yloc: nextCoords.y, - mappable_type: 'Topic', - mappable_id: topic.id, + self.get(id, (topic) => { + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + var mapping = new Metamaps.Backbone.Mapping({ + xloc: nextCoords.x, + yloc: nextCoords.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) + self.renderTopic(mapping, topic, true, true) + GlobalUI.notifyUser('Topic was added to your map!') }) - Metamaps.Mappings.add(mapping) - - self.renderTopic(mapping, topic, true, true) - - GlobalUI.notifyUser('Topic was added to your map!') event.stopPropagation() event.preventDefault() From 85dcad928f4aa03ef12c004d43216488cf8ab3d5 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:12:01 -0400 Subject: [PATCH 169/378] enable pulling in of references to maps through typeahead (#636) --- .../javascripts/src/Metamaps.Erb.js.erb | 1 + app/controllers/topics_controller.rb | 19 ++++++++---- app/helpers/topics_helper.rb | 18 ++++++----- app/models/topic.rb | 10 ++++++ frontend/src/Metamaps/Create.js | 9 +++++- frontend/src/Metamaps/Topic.js | 31 +++++++++++++++++++ frontend/src/Metamaps/TopicCard.js | 11 +++++-- 7 files changed, 82 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index e8f3a25b..75535f82 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -8,6 +8,7 @@ window.Metamaps = window.Metamaps || {} Metamaps.Erb = {} Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['RAILS_ENV'] = '<%= ENV['RAILS_ENV'] %>' Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ce430f43..986266be 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -10,12 +10,19 @@ class TopicsController < ApplicationController # GET /topics/autocomplete_topic def autocomplete_topic term = params[:term] - @topics = if term && !term.empty? - policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') - else - [] - end - render json: autocomplete_array_json(@topics) + if term && !term.empty? + @topics = policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') + @mapTopics = @topics.select { |t| t.metacode.name == 'Metamap' } + # prioritize topics which point to maps, over maps + @exclude = @mapTopics.length > 0 ? @mapTopics.map(&:name) : [''] + @maps = policy_scope(Map.where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude)).order('"name"') + else + @topics = [] + @maps = [] + end + @all= @topics.concat(@maps).sort { |a, b| a.name <=> b.name } + + render json: autocomplete_array_json(@all) end # GET topics/:id diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index e1a1d179..926e51b1 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -3,21 +3,23 @@ module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) topics.map do |t| + is_map = t.is_a?(Map) { id: t.id, label: t.name, value: t.name, description: t.desc ? t.desc&.truncate(70) : '', # make this return matched results - type: t.metacode.name, - typeImageURL: t.metacode.icon, - permission: t.permission, - mapCount: t.maps.count, - synapseCount: t.synapses.count, originator: t.user.name, originatorImage: t.user.image.url(:thirtytwo), - rtype: :topic, - inmaps: t.inmaps, - inmapsLinks: t.inmapsLinks + permission: t.permission, + + rtype: is_map ? 'map' : 'topic', + inmaps: is_map ? [] : t.inmaps, + inmapsLinks: is_map ? [] : t.inmapsLinks + type: is_map ? metamapsMetacode.name : t.metacode.name, + typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, + mapCount: is_map ? 0 : t.maps.count, + synapseCount: is_map ? 0 : t.synapses.count, } end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 7d83ecac..c3028cc4 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -15,6 +15,8 @@ class Topic < ApplicationRecord belongs_to :metacode + before_create :create_metamap? + validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -128,4 +130,12 @@ class Topic < ApplicationRecord def mk_permission Perm.short(permission) end + + protected + def create_metamap? + if link == '' and metacode.name == 'Metamap' + @map = Map.create({ name: name, permission: permission, desc: '', arranged: true, user_id: user_id }) + self.link = Rails.application.routes.url_helpers.map_url(:host => ENV['MAILER_DEFAULT_URL'], :id => @map.id) + end + end end diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 87b91540..e52024be 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -194,7 +194,14 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Topic.getTopicFromAutocomplete(datum.id) + if (datum.rtype === 'topic') { + Topic.getTopicFromAutocomplete(datum.id) + } else if (datum.rtype === 'map') { + Topic.getMapFromAutocomplete({ + id: datum.id, + name: datum.label + }) + } }) // initialize metacode spinner and then hide it diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 5be6cc57..0de5526a 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -356,6 +356,37 @@ const Topic = { self.renderTopic(mapping, topic, true, true) }) }, + getMapFromAutocomplete: function (data) { + var self = Metamaps.Topic + + // hide the 'double-click to add a topic' message + Metamaps.GlobalUI.hideDiv('#instructions') + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + var metacode = Metamaps.Metacodes.findWhere({ name: 'Metamap' }) + + var topic = new Metamaps.Backbone.Topic({ + name: data.name, + metacode_id: metacode.id, + defer_to_map_id: Metamaps.Active.Map.id, + link: window.location.origin + '/maps/' + data.id + }) + Metamaps.Topics.add(topic) + + var mapping = new Metamaps.Backbone.Mapping({ + xloc: Metamaps.Create.newTopic.x, + yloc: Metamaps.Create.newTopic.y, + mappable_id: topic.cid, + mappable_type: 'Topic', + }) + Metamaps.Mappings.add(mapping) + + // these can't happen until the value is retrieved, which happens in the line above + Metamaps.Create.newTopic.hide() + + self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database + }, getTopicFromSearch: function (event, id) { var self = Topic diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 0b2d1497..40c51fbd 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -135,9 +135,13 @@ const TopicCard = { loader.setRange(0.9); // default is 1.3 loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) - if (!e) { + if (!e && Metamaps.Erb.RAILS_ENV != 'development') { self.handleInvalidLink() } + else if (!e) { + $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() + $('#embedlyLinkLoader').hide() + } } }, 100) } @@ -154,8 +158,11 @@ const TopicCard = { loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) self.showLinkRemover() - if (!e) { + if (!e && Metamaps.Erb.RAILS_ENV != 'development') { self.handleInvalidLink() + } else if (!e) { + $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() + $('#embedlyLinkLoader').hide() } } From a56c4eb110e16891f1c4b9abf98f5d355b6a7919 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:27:18 -0400 Subject: [PATCH 170/378] missing comma --- app/helpers/topics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 926e51b1..8ef9647a 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -15,7 +15,7 @@ module TopicsHelper rtype: is_map ? 'map' : 'topic', inmaps: is_map ? [] : t.inmaps, - inmapsLinks: is_map ? [] : t.inmapsLinks + inmapsLinks: is_map ? [] : t.inmapsLinks, type: is_map ? metamapsMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, From e72ae5df94dd785bb19165b50e240345949b64d0 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:33:10 -0400 Subject: [PATCH 171/378] another issue from the maps in maps branch --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 986266be..9f280d0b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -20,7 +20,7 @@ class TopicsController < ApplicationController @topics = [] @maps = [] end - @all= @topics.concat(@maps).sort { |a, b| a.name <=> b.name } + @all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name } render json: autocomplete_array_json(@all) end From b52523e7be054ce6339a8893e6f6d826bfaffd93 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:32:06 -0400 Subject: [PATCH 172/378] one more maps in maps error --- app/helpers/topics_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 8ef9647a..fa16c358 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -4,6 +4,7 @@ module TopicsHelper def autocomplete_array_json(topics) topics.map do |t| is_map = t.is_a?(Map) + metamapMetacode = Metacode.find_by_name('Metamap') { id: t.id, label: t.name, @@ -16,7 +17,7 @@ module TopicsHelper rtype: is_map ? 'map' : 'topic', inmaps: is_map ? [] : t.inmaps, inmapsLinks: is_map ? [] : t.inmapsLinks, - type: is_map ? metamapsMetacode.name : t.metacode.name, + type: is_map ? metamapMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, synapseCount: is_map ? 0 : t.synapses.count, From 658f102a4eb73149c1cb6fa3d53bf4333c212b02 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:37:01 -0400 Subject: [PATCH 173/378] fixes #720 double topic create when pinned (#732) --- frontend/src/Metamaps/Create.js | 8 +++++--- frontend/src/Metamaps/Topic.js | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index e52024be..088622e3 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -194,6 +194,7 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { + Create.newTopic.beingCreated = false if (datum.rtype === 'topic') { Topic.getTopicFromAutocomplete(datum.id) } else if (datum.rtype === 'map') { @@ -235,19 +236,20 @@ const Create = { GlobalUI.hideDiv('#instructions') }, hide: function (force) { - if (Create.newTopic.beingCreated === false) return if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') - Create.newTopic.beingCreated = false } if (force) { $('.pinCarousel').removeClass('isPinned') Create.newTopic.pinned = false } - $('#topic_name').typeahead('val', '') if (Metamaps.Topics.length === 0) { GlobalUI.showDiv('#instructions') } + Create.newTopic.beingCreated = false + }, + reset: function () { + $('#topic_name').typeahead('val', '') } }, newSynapse: { diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 0de5526a..ddf20840 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -330,16 +330,21 @@ const Topic = { Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database }, getTopicFromAutocomplete: function (id) { var self = Topic + // hide the 'double-click to add a topic' message + GlobalUI.hideDiv('#instructions') + $(document).trigger(Map.events.editedByActiveMapper) - Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.get(id, (topic) => { if (Create.newTopic.pinned) { @@ -354,18 +359,16 @@ const Topic = { Metamaps.Mappings.add(mapping) self.renderTopic(mapping, topic, true, true) + // this blocked the enterKeyHandler from creating a new topic as well + if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }) }, getMapFromAutocomplete: function (data) { var self = Metamaps.Topic - // hide the 'double-click to add a topic' message - Metamaps.GlobalUI.hideDiv('#instructions') - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.findWhere({ name: 'Metamap' }) - var topic = new Metamaps.Backbone.Topic({ name: data.name, metacode_id: metacode.id, @@ -383,9 +386,12 @@ const Topic = { Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Metamaps.Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database + // this blocked the enterKeyHandler from creating a new topic as well + if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }, getTopicFromSearch: function (event, id) { var self = Topic From 97d2868fadaae6819c4b432322cf09c31ae5ebbf Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:49:49 -0400 Subject: [PATCH 174/378] dont pan while using arrow keys during creation fixes #721 (#733) --- frontend/src/Metamaps/Listeners.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index f78a030b..5861e15e 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -16,6 +16,7 @@ const Listeners = { $(document).on('keydown', function (e) { if (!(Active.Map || Active.Topic)) return + const creatingTopic = e.target.id === 'topic_name' switch (e.which) { case 13: // if enter key is pressed // prevent topic creation if sending a message @@ -28,16 +29,16 @@ const Listeners = { JIT.escKeyHandler() break case 37: // if Left arrow key is pressed - Visualize.mGraph.canvas.translate(-20, 0) + if (!creatingTopic) Visualize.mGraph.canvas.translate(-20, 0) break case 38: // if Up arrow key is pressed - Visualize.mGraph.canvas.translate(0, -20) + if (!creatingTopic) Visualize.mGraph.canvas.translate(0, -20) break case 39: // if Right arrow key is pressed - Visualize.mGraph.canvas.translate(20, 0) + if (!creatingTopic) Visualize.mGraph.canvas.translate(20, 0) break case 40: // if Down arrow key is pressed - Visualize.mGraph.canvas.translate(0, 20) + if (!creatingTopic) Visualize.mGraph.canvas.translate(0, 20) break case 65: // if a or A is pressed if (e.ctrlKey) { From 0aeb6caadb20312ac906e9b585df8ed726b8d35c Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 00:33:16 +0000 Subject: [PATCH 175/378] Makes it so that resizing the browser window doesn't change the user's location on the map --- frontend/src/Metamaps/Listeners.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 5861e15e..08aa9cee 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -7,6 +7,7 @@ import Mobile from './Mobile' import Realtime from './Realtime' import Selected from './Selected' import Topic from './Topic' +import Util from './Util' import Visualize from './Visualize' import { Search } from './GlobalUI' @@ -122,7 +123,22 @@ const Listeners = { }) $(window).resize(function () { + var canvas = Visualize.mGraph.canvas, + scaleX = canvas.scaleOffsetX, + scaleY = canvas.scaleOffsetY, + centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); + if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + + canvas.scale(scaleX,scaleY); + var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() }) From b978247785fd4e918a9c93548eba042b23033891 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 00:51:52 +0000 Subject: [PATCH 176/378] Put all the code within the if statement --- frontend/src/Metamaps/Listeners.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 08aa9cee..bd73e59b 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -123,21 +123,23 @@ const Listeners = { }) $(window).resize(function () { - var canvas = Visualize.mGraph.canvas, + if (Visualize && Visualize.mGraph){ + var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - - if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) - - canvas.scale(scaleX,scaleY); - var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, - newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); - - canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + + Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + + canvas.scale(scaleX,scaleY); + var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + } if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() From 86a6e92bc3ea74a6fb45e3d3a46fecf5598d5426 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 23:45:17 -0400 Subject: [PATCH 177/378] dont show private maps in global collection (#734) * dont show private maps in global collection * Update explore_controller.rb * Update main_controller.rb --- app/controllers/explore_controller.rb | 2 +- app/controllers/main_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index dc4c2de9..2e713213 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = map_scope(Map.where.not(name: 'Untitled Map')) + @maps = map_scope(Map.where.not(name: 'Untitled Map').where.not(permission: 'private')) respond_to do |format| format.html do diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 7df4e366..38d9458c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -8,7 +8,7 @@ class MainController < ApplicationController respond_to do |format| format.html do if authenticated? - @maps = policy_scope(Map).where.not(name: 'Untitled Map') + @maps = policy_scope(Map).where.not(name: 'Untitled Map').where.not(permission: 'private') .order(updated_at: :desc).page(1).per(20) render 'explore/active' else From 08f89ee630b35e44625ff4da929664df8c4a2933 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 6 Oct 2016 23:56:39 -0400 Subject: [PATCH 178/378] Update Listeners.js --- frontend/src/Metamaps/Listeners.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index bd73e59b..48e50a69 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -124,6 +124,7 @@ const Listeners = { $(window).resize(function () { if (Visualize && Visualize.mGraph){ + //Find the current canvas scale and map-coordinate at the centre of the user's screen var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, @@ -131,8 +132,10 @@ const Listeners = { centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); + //Resize the canvas to fill the new indow size, based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen canvas.scale(scaleX,scaleY); var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, From 3e4ff59a82fd089707c6fa826d49a05fcb447654 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 6 Oct 2016 23:58:57 -0400 Subject: [PATCH 179/378] Update Listeners.js --- frontend/src/Metamaps/Listeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 48e50a69..cc5621cb 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -132,7 +132,7 @@ const Listeners = { centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - //Resize the canvas to fill the new indow size, based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 + //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen From 2b036bfb4eb5cb771403fca8250089fb07321b6e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 14:03:48 +0800 Subject: [PATCH 180/378] all Ctrl shortcuts now also work with Meta (Cmd on OSX) --- frontend/src/Metamaps/Listeners.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 5861e15e..cf3365f3 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -41,7 +41,7 @@ const Listeners = { if (!creatingTopic) Visualize.mGraph.canvas.translate(0, 20) break case 65: // if a or A is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { Control.deselectAllNodes() Control.deselectAllEdges() @@ -55,13 +55,13 @@ const Listeners = { break case 68: // if d or D is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.deleteSelected() } break case 69: // if e or E is pressed - if (e.ctrlKey && Active.Map) { + if ((e.ctrlKey || e.metaKey) && Active.Map) { e.preventDefault() JIT.zoomExtents(null, Visualize.mGraph.canvas) break @@ -79,14 +79,14 @@ const Listeners = { } break case 72: // if h or H is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.hideSelectedNodes() Control.hideSelectedEdges() } break case 77: // if m or M is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.removeSelectedNodes() Control.removeSelectedEdges() @@ -111,7 +111,7 @@ const Listeners = { } break case 191: // if / is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { Search.focus() } break From b6da38e29e143bd05a85db910a0284f184b0361b Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 02:36:41 -0400 Subject: [PATCH 181/378] Update Listeners.js Simplified based on Connor's suggestion about usage of variables. --- frontend/src/Metamaps/Listeners.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index cc5621cb..ce14dc9f 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -128,17 +128,17 @@ const Listeners = { var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, - centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + centrePixX = canvas.canvases[0].size.width / 2, + centrePixY = canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 - Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + canvas.resize($(window).width(), $(window).height()) //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen canvas.scale(scaleX,scaleY); - var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + var newCentrePixX = canvas.canvases[0].size.width / 2, + newCentrePixY = canvas.canvases[0].size.height / 2, newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); From 42bb2cd86a60c463e8f32e56fbc4744f44f276c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:16:37 +0800 Subject: [PATCH 182/378] look and feel updates --- app/assets/images/import-example.png | Bin 57176 -> 45339 bytes app/assets/stylesheets/application.scss.erb | 6 ++++++ app/views/layouts/_lowermapelements.html.erb | 15 +++++++-------- app/views/layouts/_upperelements.html.erb | 4 ++++ frontend/src/components/ImportDialogBox.js | 4 ++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/assets/images/import-example.png b/app/assets/images/import-example.png index 3f013d58ce972f7236cd3d49f9aab47458cc299f..02d59dcd6024aef449b485ea0e3485013e435328 100644 GIT binary patch literal 45339 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%0AIjRdYjq~X9 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 Xgkc+nVc6;eAfK<|@?w=Dh5`Qv9sg9Y literal 57176 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 diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 2797cbeb..5961c633 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -1555,8 +1555,14 @@ h3.filterBox { cursor: pointer; } .fileupload { + box-sizing: border-box; + margin: 0.75em; + padding: 0.75em; + height: 3em; + border: 3px dashed #AAB0FB; width: 75%; text-align: center; + cursor: pointer; } } .wrapper .mapInfoBox { diff --git a/app/views/layouts/_lowermapelements.html.erb b/app/views/layouts/_lowermapelements.html.erb index b8b7f868..82ec71f2 100644 --- a/app/views/layouts/_lowermapelements.html.erb +++ b/app/views/layouts/_lowermapelements.html.erb @@ -8,12 +8,11 @@ <div class="infoAndHelp"> <%= render :partial => 'maps/mapinfobox' %> - <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"><div class="tooltipsAbove">Import data</div></div> - <% starred = current_user && @map && current_user.starred_map?(@map) - starClass = starred ? 'starred' : '' - tooltip = starred ? 'Star' : 'Unstar' %> - <div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div> - <div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div> - <div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div> - <div class="clearfloat"></div> + <% starred = current_user && @map && current_user.starred_map?(@map) + starClass = starred ? 'starred' : '' + tooltip = starred ? 'Star' : 'Unstar' %> + <div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div> + <div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div> + <div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div> + <div class="clearfloat"></div> </div> diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index e393f767..c2344643 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -19,6 +19,10 @@ <div class="upperRightUI"> <div class="mapElement upperRightEl upperRightMapButtons"> + <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"> + <div class="tooltipsAbove">Import data</div> + </div> + <!-- filtering --> <div class="sidebarFilter upperRightEl"> <div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div> diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index 4d113ccc..9851fd07 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -41,7 +41,7 @@ class ImportDialogBox extends Component { <h3>IMPORT</h3> <p>To upload a file, drop it here:</p> <Dropzone onDropAccepted={this.handleFile} - className="import-blue-button fileupload" + className="fileupload" > Drop files here! </Dropzone> @@ -56,7 +56,7 @@ class ImportDialogBox extends Component { The file should be in comma-separated format (when you save, change the filetype from .xls to .csv). </p> - <img src={this.props.exampleImageUrl} style={{ maxWidth: '75%', float: 'right', margin: '1em' }}/> + <img src={this.props.exampleImageUrl} style={{ width: '100%' }} /> <p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p> <p> </p> <p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p> From 20da1ef39f9480cccdd38d43f3aeb36cff639b50 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:21:04 +0800 Subject: [PATCH 183/378] fiddle with import icon --- app/assets/images/import.png | Bin 320 -> 325 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/import.png b/app/assets/images/import.png index 5c66e984a88e7f7f8408c44bf4eb999428cee9c8..29b5b89653aba363907399e286a688f8e6e4a6b6 100644 GIT binary patch delta 292 zcmV+<0o(q-0>uK5Dt`a~0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jB_^5EDAKGOB6-007cSL_t(o!|m2FN<&c)h2ifc4>Yk5M358~LY819g1zf7rIp|w z%vM}jN-HbDYx4pr648*yMZD8p7?^+N%p7h?DQd<@<!AtV1Aiou<fsZj66i2b@{;7X zh6E?LMsSg2wpTO1Im5p5;0g<T;20~s<F%CXHuMB0xW~9J4l~Sgmt-;sz()6<xZII; zgYzWaP!pKqsR!8=k2p&*8Uo+}r$5MB^h<i!CEOy(a{Vr)JU1(D0Dlr7?ML+Q0lIIT qZh7cp+uX8A&>eiBNzeeQ1AGCiFC^s10uZDC0000<MNUMnLSTXtPj)B( delta 287 zcmV+)0pR|{0>A>0Du4d~{{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jB_@1S%nsK!WuE007NNL_t(o!|m2FO2a`E#NlsZKobi=1W92b<Oo(0NS||<(n|0S zdKC|r(#lG3ZPrE-h#`L#VW-;S!TasJx5K8CqNPR}s{<SikbmT~89<UT=1E?XELu2l zjynX`NoEH-`Il#yB)G*IpE$z?A9yRJybtZb1S_1><}kw?%OsP*0P5|(aI>T29#?fg z2614Dr#kmtdc<Xt(GUO+xY(m^jcIl4xJ&qJB(Fc`Qp$6W;tud{0n&d&tpUdS1kJIu lEwK6sIzZ2X4j9k@z5(m*B;;aqNwNR{002ovPDHLkV1jroc^&`& From fc044294f168ea4243ac56f6e4db06c749606e8d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 17:23:11 +0800 Subject: [PATCH 184/378] add markdown to topic cards --- app/views/layouts/_templates.html.erb | 2 +- frontend/src/Metamaps/TopicCard.js | 17 +++++++++++------ frontend/src/Metamaps/Util.js | 5 +++++ package.json | 4 +++- webpack.config.js | 3 +++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index ff41c7dc..59a5ceb8 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -220,7 +220,7 @@ </div> <div class="scroll"> <div class="desc"> - <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea">{{desc}}</span> + <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea" data-original-content="{{desc_markdown}}">{{{desc_html}}}</span> <div class="clearfloat"></div> </div> </div> diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 40c51fbd..84892450 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -273,10 +273,14 @@ const TopicCard = { topic.trigger('saved') }) + // this is for all subsequent renders after in-place editing the desc field $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { - this.innerHTML = this.innerHTML.replace(/\r/g, '') - var desc = $(this).html() === $(this).data('nil') ? '' : $(this).html() + var desc = $(this).html() === $(this).data('nil') + ? '' + : $(this).text() topic.set('desc', desc) + $(this).data('bestInPlaceEditor').original_content = desc + this.innerHTML = Util.mdToHTML(desc) topic.trigger('saved') }) } @@ -397,8 +401,6 @@ const TopicCard = { } else { } - var desc_nil = 'Click to add description...' - nodeValues.attachmentsHidden = '' if (topic.get('link') && topic.get('link') !== '') { nodeValues.embeds = '<a href="' + topic.get('link') + '" id="embedlyLink" target="_blank" data-card-description="0">' @@ -454,8 +456,11 @@ const TopicCard = { nodeValues.date = topic.getDate() // the code for this is stored in /views/main/_metacodeOptions.html.erb nodeValues.metacode_select = $('#metacodeOptions').html() - nodeValues.desc_nil = desc_nil - nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') + nodeValues.desc_nil = 'Click to add description...' + nodeValues.desc_markdown = (topic.get('desc') === '' && authorized) + ? desc_nil + : topic.get('desc') + nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown) return nodeValues } } diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 9eb715de..f1f8b39c 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,3 +1,5 @@ +import { Parser, HtmlRenderer } from 'commonmark' + import Visualize from './Visualize' const Util = { @@ -119,6 +121,9 @@ const Util = { }, checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) + }, + mdToHTML: text => { + return new HtmlRenderer().render(new Parser().parse(text)) } } diff --git a/package.json b/package.json index c882fdd0..29e37af3 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,16 @@ "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", - "underscore": "1.4.4", + "commonmark": "0.26.0", "csv-parse": "1.1.7", + "json-loader": "0.5.4", "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", + "underscore": "1.4.4", "webpack": "1.13.2" }, "devDependencies": { diff --git a/webpack.config.js b/webpack.config.js index 91498abd..644ff002 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,9 @@ const config = module.exports = { plugins, devtool, module: { + preLoaders: [ + { test: /\.json$/, loader: 'json' } + ], loaders: [ { test: /\.(js|jsx)?$/, From 0085ce71e6e7d43861fc0f00341e5c3d9093ee5d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 17:26:20 +0800 Subject: [PATCH 185/378] upgrade to best in place 3.0.0 alpha --- app/assets/javascripts/lib/best_in_place.js | 685 +++++++++++++++++ app/assets/javascripts/lib/bip.js | 780 -------------------- app/views/layouts/_templates.html.erb | 13 +- app/views/maps/_mapinfobox.html.erb | 4 +- frontend/src/Metamaps/Listeners.js | 1 - frontend/src/Metamaps/Map/InfoBox.js | 25 +- frontend/src/Metamaps/SynapseCard.js | 11 +- frontend/src/Metamaps/TopicCard.js | 9 +- 8 files changed, 728 insertions(+), 800 deletions(-) create mode 100644 app/assets/javascripts/lib/best_in_place.js delete mode 100644 app/assets/javascripts/lib/bip.js diff --git a/app/assets/javascripts/lib/best_in_place.js b/app/assets/javascripts/lib/best_in_place.js new file mode 100644 index 00000000..5000103f --- /dev/null +++ b/app/assets/javascripts/lib/best_in_place.js @@ -0,0 +1,685 @@ +/* + * BestInPlace (for jQuery) + * version: 3.0.0.alpha (2014) + * + * By Bernat Farrero based on the work of Jan Varwig. + * Examples at http://bernatfarrero.com + * + * Licensed under the MIT: + * http://www.opensource.org/licenses/mit-license.php + * + * @requires jQuery + * + * Usage: + * + * Attention. + * The format of the JSON object given to the select inputs is the following: + * [["key", "value"],["key", "value"]] + * The format of the JSON object given to the checkbox inputs is the following: + * ["falseValue", "trueValue"] + + */ +//= require jquery.autosize + +function BestInPlaceEditor(e) { + 'use strict'; + this.element = e; + this.initOptions(); + this.bindForm(); + this.initPlaceHolder(); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); +} + +BestInPlaceEditor.prototype = { + // Public Interface Functions ////////////////////////////////////////////// + + activate: function () { + 'use strict'; + var to_display; + if (this.isPlaceHolder()) { + to_display = ""; + } else if (this.original_content) { + to_display = this.original_content; + } else { + switch (this.formType) { + case 'input': + case 'textarea': + if (this.display_raw) { + to_display = this.element.html().replace(/&/gi, '&'); + } + else { + var value = this.element.data('bipValue'); + if (typeof value === 'undefined') { + to_display = ''; + } else if (typeof value === 'string') { + to_display = this.element.data('bipValue').replace(/&/gi, '&'); + } else { + to_display = this.element.data('bipValue'); + } + } + break; + case 'select': + to_display = this.element.html(); + + } + } + + this.oldValue = this.isPlaceHolder() ? "" : this.element.html(); + this.display_value = to_display; + jQuery(this.activator).unbind("click", this.clickHandler); + this.activateForm(); + this.element.trigger(jQuery.Event("best_in_place:activate")); + }, + + abort: function () { + 'use strict'; + this.activateText(this.oldValue); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:abort")); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + abortIfConfirm: function () { + 'use strict'; + if (!this.useConfirm) { + this.abort(); + return; + } + + if (confirm(BestInPlaceEditor.defaults.locales[''].confirmMessage)) { + this.abort(); + } + }, + + update: function () { + 'use strict'; + var editor = this, + value = this.getValue(); + + // Avoid request if no change is made + if (this.formType in {"input": 1, "textarea": 1} && value === this.oldValue) { + this.abort(); + return true; + } + + editor.ajax({ + "type": this.requestMethod(), + "dataType": BestInPlaceEditor.defaults.ajaxDataType, + "data": editor.requestData(), + "success": function (data, status, xhr) { + editor.loadSuccessCallback(data, status, xhr); + }, + "error": function (request, error) { + editor.loadErrorCallback(request, error); + } + }); + + + switch (this.formType) { + case "select": + this.previousCollectionValue = value; + + // search for the text for the span + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + case "checkbox": + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + default: + if (value !== "") { + if (this.display_raw) { + editor.element.html(value); + } else { + editor.element.text(value); + } + } else { + editor.element.html(this.placeHolder); + } + } + + editor.element.data('bipValue', value); + editor.element.attr('data-bip-value', value); + + editor.element.trigger(jQuery.Event("best_in_place:update")); + + + }, + + activateForm: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + activateText: function (value) { + 'use strict'; + this.element.html(value); + if (this.isPlaceHolder()) { + this.element.html(this.placeHolder); + } + }, + + // Helper Functions //////////////////////////////////////////////////////// + + initOptions: function () { + // Try parent supplied info + 'use strict'; + var self = this; + self.element.parents().each(function () { + var $parent = jQuery(this); + self.url = self.url || $parent.data("bipUrl"); + self.activator = self.activator || $parent.data("bipActivator"); + self.okButton = self.okButton || $parent.data("bipOkButton"); + self.okButtonClass = self.okButtonClass || $parent.data("bipOkButtonClass"); + self.cancelButton = self.cancelButton || $parent.data("bipCancelButton"); + self.cancelButtonClass = self.cancelButtonClass || $parent.data("bipCancelButtonClass"); + self.skipBlur = self.skipBlur || $parent.data("bipSkipBlur"); + }); + + // Load own attributes (overrides all others) + self.url = self.element.data("bipUrl") || self.url || document.location.pathname; + self.collection = self.element.data("bipCollection") || self.collection; + self.formType = self.element.data("bipType") || "input"; + self.objectName = self.element.data("bipObject") || self.objectName; + self.attributeName = self.element.data("bipAttribute") || self.attributeName; + self.activator = self.element.data("bipActivator") || self.element; + self.okButton = self.element.data("bipOkButton") || self.okButton; + self.okButtonClass = self.element.data("bipOkButtonClass") || self.okButtonClass || BestInPlaceEditor.defaults.okButtonClass; + self.cancelButton = self.element.data("bipCancelButton") || self.cancelButton; + self.cancelButtonClass = self.element.data("bipCancelButtonClass") || self.cancelButtonClass || BestInPlaceEditor.defaults.cancelButtonClass; + self.skipBlur = self.element.data("bipSkipBlur") || self.skipBlur || BestInPlaceEditor.defaults.skipBlur; + self.isNewObject = self.element.data("bipNewObject"); + self.dataExtraPayload = self.element.data("bipExtraPayload"); + + // Fix for default values of 0 + if (self.element.data("bipPlaceholder") == null) { + self.placeHolder = BestInPlaceEditor.defaults.locales[''].placeHolder; + } else { + self.placeHolder = self.element.data("bipPlaceholder"); + } + + self.inner_class = self.element.data("bipInnerClass"); + self.html_attrs = self.element.data("bipHtmlAttrs"); + self.original_content = self.element.data("bipOriginalContent") || self.original_content; + + // if set the input won't be satinized + self.display_raw = self.element.data("bip-raw"); + + self.useConfirm = self.element.data("bip-confirm"); + + if (self.formType === "select" || self.formType === "checkbox") { + self.values = self.collection; + self.collectionValue = self.element.data("bipValue") || self.collectionValue; + } + }, + + bindForm: function () { + 'use strict'; + this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; + this.getValue = BestInPlaceEditor.forms[this.formType].getValue; + }, + + + initPlaceHolder: function () { + 'use strict'; + // TODO add placeholder for select and checkbox + if (this.element.html() === "") { + this.element.addClass('bip-placeholder'); + this.element.html(this.placeHolder); + } + }, + + isPlaceHolder: function () { + 'use strict'; + // TODO: It only work when form is deactivated. + // Condition will fail when form is activated + return this.element.html() === "" || this.element.html() === this.placeHolder; + }, + + getValue: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + // Trim and Strips HTML from text + sanitizeValue: function (s) { + 'use strict'; + return jQuery.trim(s); + }, + + requestMethod: function() { + 'use strict'; + return this.isNewObject ? 'post' : BestInPlaceEditor.defaults.ajaxMethod; + }, + + /* Generate the data sent in the POST request */ + requestData: function () { + 'use strict'; + // To prevent xss attacks, a csrf token must be defined as a meta attribute + var csrf_token = jQuery('meta[name=csrf-token]').attr('content'), + csrf_param = jQuery('meta[name=csrf-param]').attr('content'); + + var data = {} + data['_method'] = this.requestMethod() + + data[this.objectName] = this.dataExtraPayload || {} + + data[this.objectName][this.attributeName] = this.getValue() + + if (csrf_param !== undefined && csrf_token !== undefined) { + data[csrf_param] = csrf_token + } + return jQuery.param(data); + }, + + ajax: function (options) { + 'use strict'; + options.url = this.url; + options.beforeSend = function (xhr) { + xhr.setRequestHeader("Accept", "application/json"); + }; + return jQuery.ajax(options); + }, + + // Handlers //////////////////////////////////////////////////////////////// + + loadSuccessCallback: function (data, status, xhr) { + 'use strict'; + data = jQuery.trim(data); + //Update original content with current text. + if (this.display_raw) { + this.original_content = this.element.html(); + } else { + this.original_content = this.element.text(); + } + + if (data && data !== "") { + var response = jQuery.parseJSON(data); + if (response !== null && response.hasOwnProperty("display_as")) { + this.element.data('bip-original-content', this.element.text()); + this.element.html(response.display_as); + } + if (this.isNewObject && response && response[this.objectName]) { + if (response[this.objectName]["id"]) { + this.isNewObject = false + this.url += "/" + response[this.objectName]["id"] // in REST a POST /thing url should become PUT /thing/123 + } + } + } + this.element.toggleClass('bip-placeholder', this.isPlaceHolder()); + + this.element.trigger(jQuery.Event("best_in_place:success"), [data, status, xhr]); + this.element.trigger(jQuery.Event("ajax:success"), [data, status, xhr]); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + + if (this.collectionValue !== null && this.formType === "select") { + this.collectionValue = this.previousCollectionValue; + this.previousCollectionValue = null; + } + }, + + loadErrorCallback: function (request, error) { + 'use strict'; + this.activateText(this.oldValue); + + this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); + this.element.trigger(jQuery.Event("ajax:error"), request, error); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + clickHandler: function (event) { + 'use strict'; + event.preventDefault(); + event.data.editor.activate(); + }, + + setHtmlAttributes: function () { + 'use strict'; + var formField = this.element.find(this.formType); + + if (this.html_attrs) { + var attrs = this.html_attrs; + $.each(attrs, function (key, val) { + formField.attr(key, val); + }); + } + }, + + placeButtons: function (output, field) { + 'use strict'; + if (field.okButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'submit') + .attr('class', field.okButtonClass) + .attr('value', field.okButton) + ); + } + if (field.cancelButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'button') + .attr('class', field.cancelButtonClass) + .attr('value', field.cancelButton) + ); + } + } +}; + + +// Button cases: +// If no buttons, then blur saves, ESC cancels +// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) +// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels +// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels +BestInPlaceEditor.forms = { + "input": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var input_elt = jQuery(document.createElement('input')) + .attr('type', 'text') + .attr('name', this.attributeName) + .val(this.display_value); + + // Add class to form input + if (this.inner_class) { + input_elt.addClass(this.inner_class); + } + + output.append(input_elt); + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + this.element.find("input[type='text']")[0].select(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); + } + if (!this.okButton) { + this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); + } + this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("input").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + inputBlurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abort(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abort(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + event.stopImmediatePropagation(); + } + } + }, + + "select": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .attr('action', 'javascript:void(0)') + .attr('style', 'display:inline'), + selected = '', + select_elt = jQuery(document.createElement('select')) + .attr('class', this.inner_class !== null ? this.inner_class : ''), + currentCollectionValue = this.collectionValue, + key, value, + a = this.values; + + $.each(a, function(index, arr){ + key = arr[0]; + value = arr[1]; + var option_elt = jQuery(document.createElement('option')) + .val(key) + .html(value); + + if (currentCollectionValue) { + if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); + } + select_elt.append(option_elt); + }); + output.append(select_elt); + + this.element.html(output); + this.setHtmlAttributes(); + this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); + this.element.find("select")[0].focus(); + + // automatically click on the select so you + // don't have to click twice + try { + var e = document.createEvent("MouseEvents"); + e.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + this.element.find("select")[0].dispatchEvent(e); + } + catch(e) { + // browser doesn't support this, e.g. IE8 + } + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("select").val()); + }, + + blurHandler: function (event) { + 'use strict'; + event.data.editor.update(); + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + } + } + }, + + "checkbox": { + activateForm: function () { + 'use strict'; + this.collectionValue = !this.getValue(); + this.setHtmlAttributes(); + this.update(); + }, + + getValue: function () { + 'use strict'; + return this.collectionValue; + } + }, + + "textarea": { + activateForm: function () { + 'use strict'; + // grab width and height of text + var width = this.element.css('width'); + var height = this.element.css('height'); + + // construct form + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var textarea_elt = jQuery(document.createElement('textarea')) + .attr('name', this.attributeName) + .val(this.sanitizeValue(this.display_value)); + + if (this.inner_class !== null) { + textarea_elt.addClass(this.inner_class); + } + + output.append(textarea_elt); + + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + // set width and height of textarea + jQuery(this.element.find("textarea")[0]).css({'min-width': width, 'min-height': height}); + jQuery(this.element.find("textarea")[0]).autosize(); + + this.element.find("textarea")[0].focus(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); + + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); + } + + if (!this.skipBlur) { + this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); + } + this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("textarea").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + blurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abortIfConfirm(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abortIfConfirm(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abortIfConfirm(); + } + } + } +}; + +BestInPlaceEditor.defaults = { + locales: {}, + ajaxMethod: "put", //TODO Change to patch when support to 3.2 is dropped + ajaxDataType: 'text', + okButtonClass: '', + cancelButtonClass: '', + skipBlur: false +}; + +// Default locale +BestInPlaceEditor.defaults.locales[''] = { + confirmMessage: "Are you sure you want to discard your changes?", + uninitializedForm: "The form was not properly initialized. getValue is unbound", + placeHolder: '-' +}; + +jQuery.fn.best_in_place = function () { + 'use strict'; + function setBestInPlace(element) { + if (!element.data('bestInPlaceEditor')) { + element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); + return true; + } + } + + jQuery(this.context).delegate(this.selector, 'click', function () { + var el = jQuery(this); + if (setBestInPlace(el)) { + el.click(); + } + }); + + this.each(function () { + setBestInPlace(jQuery(this)); + }); + + return this; +}; + + + diff --git a/app/assets/javascripts/lib/bip.js b/app/assets/javascripts/lib/bip.js deleted file mode 100644 index 1d575fef..00000000 --- a/app/assets/javascripts/lib/bip.js +++ /dev/null @@ -1,780 +0,0 @@ -/* - BestInPlace (for jQuery) - version: 0.1.0 (01/01/2011) - @requires jQuery >= v1.4 - @requires jQuery.purr to display pop-up windows - - By Bernat Farrero based on the work of Jan Varwig. - Examples at http://bernatfarrero.com - - Licensed under the MIT: - http://www.opensource.org/licenses/mit-license.php - - Usage: - - Attention. - The format of the JSON object given to the select inputs is the following: - [["key", "value"],["key", "value"]] - The format of the JSON object given to the checkbox inputs is the following: - ["falseValue", "trueValue"] -*/ - - -function BestInPlaceEditor(e) { - this.element = e; - this.initOptions(); - this.bindForm(); - this.initNil(); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); -} - -BestInPlaceEditor.prototype = { - // Public Interface Functions ////////////////////////////////////////////// - - activate : function() { - var to_display = ""; - if (this.isNil()) { - to_display = ""; - } - else if (this.original_content) { - to_display = this.original_content; - } - else { - if (this.sanitize) { - to_display = this.element.text(); - } else { - to_display = this.element.html(); - } - } - - this.oldValue = this.isNil() ? "" : this.element.html(); - this.display_value = to_display; - jQuery(this.activator).unbind("click", this.clickHandler); - this.activateForm(); - this.element.trigger(jQuery.Event("best_in_place:activate")); - }, - - abort : function() { - this.activateText(this.oldValue); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:abort")); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - abortIfConfirm : function () { - if (!this.useConfirm) { - this.abort(); - return; - } - - if (confirm("Are you sure you want to discard your changes?")) { - this.abort(); - } - }, - - update : function() { - var editor = this; - if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue) - { // Avoid request if no change is made - this.abort(); - return true; - } - editor.ajax({ - "type" : "post", - "dataType" : "text", - "data" : editor.requestData(), - "success" : function(data){ editor.loadSuccessCallback(data); }, - "error" : function(request, error){ editor.loadErrorCallback(request, error); } - }); - if (this.formType == "select") { - var value = this.getValue(); - this.previousCollectionValue = value; - - jQuery.each(this.values, function(i, v) { - if (value == v[0]) { - editor.element.html(v[1]); - } - } - ); - } else if (this.formType == "checkbox") { - editor.element.html(this.getValue() ? this.values[1] : this.values[0]); - } else { - if (this.getValue() !== "") { - editor.element.text(this.getValue()); - } else { - editor.element.html(this.nil); - } - } - editor.element.trigger(jQuery.Event("best_in_place:update")); - }, - - activateForm : function() { - alert("The form was not properly initialized. activateForm is unbound"); - }, - - activateText : function(value){ - this.element.html(value); - if(this.isNil()) this.element.html(this.nil); - }, - - // Helper Functions //////////////////////////////////////////////////////// - - initOptions : function() { - // Try parent supplied info - var self = this; - self.element.parents().each(function(){ - $parent = jQuery(this); - self.url = self.url || $parent.attr("data-url"); - self.collection = self.collection || $parent.attr("data-collection"); - self.formType = self.formType || $parent.attr("data-type"); - self.objectName = self.objectName || $parent.attr("data-object"); - self.attributeName = self.attributeName || $parent.attr("data-attribute"); - self.activator = self.activator || $parent.attr("data-activator"); - self.okButton = self.okButton || $parent.attr("data-ok-button"); - self.okButtonClass = self.okButtonClass || $parent.attr("data-ok-button-class"); - self.cancelButton = self.cancelButton || $parent.attr("data-cancel-button"); - self.cancelButtonClass = self.cancelButtonClass || $parent.attr("data-cancel-button-class"); - self.nil = self.nil || $parent.attr("data-nil"); - self.inner_class = self.inner_class || $parent.attr("data-inner-class"); - self.html_attrs = self.html_attrs || $parent.attr("data-html-attrs"); - self.original_content = self.original_content || $parent.attr("data-original-content"); - self.collectionValue = self.collectionValue || $parent.attr("data-value"); - }); - - // Try Rails-id based if parents did not explicitly supply something - self.element.parents().each(function(){ - var res = this.id.match(/^(\w+)_(\d+)$/i); - if (res) { - self.objectName = self.objectName || res[1]; - } - }); - - // Load own attributes (overrides all others) - self.url = self.element.attr("data-url") || self.url || document.location.pathname; - self.collection = self.element.attr("data-collection") || self.collection; - self.formType = self.element.attr("data-type") || self.formtype || "input"; - self.objectName = self.element.attr("data-object") || self.objectName; - self.attributeName = self.element.attr("data-attribute") || self.attributeName; - self.activator = self.element.attr("data-activator") || self.element; - self.okButton = self.element.attr("data-ok-button") || self.okButton; - self.okButtonClass = self.element.attr("data-ok-button-class") || self.okButtonClass || ""; - self.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton; - self.cancelButtonClass = self.element.attr("data-cancel-button-class") || self.cancelButtonClass || ""; - self.nil = self.element.attr("data-nil") || self.nil || "—"; - self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null; - self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs; - self.original_content = self.element.attr("data-original-content") || self.original_content; - self.collectionValue = self.element.attr("data-value") || self.collectionValue; - - if (!self.element.attr("data-sanitize")) { - self.sanitize = true; - } - else { - self.sanitize = (self.element.attr("data-sanitize") == "true"); - } - - if (!self.element.attr("data-use-confirm")) { - self.useConfirm = true; - } else { - self.useConfirm = (self.element.attr("data-use-confirm") != "false"); - } - - if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null) - { - self.values = jQuery.parseJSON(self.collection); - } - - }, - - bindForm : function() { - this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; - this.getValue = BestInPlaceEditor.forms[this.formType].getValue; - }, - - initNil: function() { - if (this.element.html() === "") - { - this.element.html(this.nil); - } - }, - - isNil: function() { - // TODO: It only work when form is deactivated. - // Condition will fail when form is activated - return this.element.html() === "" || this.element.html() === this.nil; - }, - - getValue : function() { - alert("The form was not properly initialized. getValue is unbound"); - }, - - // Trim and Strips HTML from text - sanitizeValue : function(s) { - return jQuery.trim(s); - }, - - /* Generate the data sent in the POST request */ - requestData : function() { - // To prevent xss attacks, a csrf token must be defined as a meta attribute - csrf_token = jQuery('meta[name=csrf-token]').attr('content'); - csrf_param = jQuery('meta[name=csrf-param]').attr('content'); - - var data = "_method=put"; - data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue()); - - if (csrf_param !== undefined && csrf_token !== undefined) { - data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token); - } - return data; - }, - - ajax : function(options) { - options.url = this.url; - options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); }; - return jQuery.ajax(options); - }, - - // Handlers //////////////////////////////////////////////////////////////// - - loadSuccessCallback : function(data) { - data = jQuery.trim(data); - - if(data && data!=""){ - var response = jQuery.parseJSON(jQuery.trim(data)); - if (response !== null && response.hasOwnProperty("display_as")) { - this.element.attr("data-original-content", this.element.text()); - this.original_content = this.element.text(); - this.element.html(response["display_as"]); - } - - this.element.trigger(jQuery.Event("best_in_place:success"), data); - this.element.trigger(jQuery.Event("ajax:success"), data); - } else { - this.element.trigger(jQuery.Event("best_in_place:success")); - this.element.trigger(jQuery.Event("ajax:success")); - } - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - - if (this.collectionValue !== null && this.formType == "select") { - this.collectionValue = this.previousCollectionValue; - this.previousCollectionValue = null; - } - }, - - loadErrorCallback : function(request, error) { - this.activateText(this.oldValue); - - this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); - this.element.trigger(jQuery.Event("ajax:error"), request, error); - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - clickHandler : function(event) { - event.preventDefault(); - event.data.editor.activate(); - }, - - setHtmlAttributes : function() { - var formField = this.element.find(this.formType); - - if(this.html_attrs){ - var attrs = jQuery.parseJSON(this.html_attrs); - for(var key in attrs){ - formField.attr(key, attrs[key]); - } - } - } -}; - - -// Button cases: -// If no buttons, then blur saves, ESC cancels -// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) -// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels -// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels -BestInPlaceEditor.forms = { - "input" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'); - var input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .val(this.display_value); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('class', this.okButtonClass) - .attr('value', this.okButton) - ) - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('class', this.cancelButtonClass) - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - // START METAMAPS CODE - //this.element.find("input[type='text']")[0].select(); - this.element.find("input[type='text']")[0].focus(); - // END METAMAPS CODE - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); - } - this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); - // START METAMAPS CODE - this.element.find("input[type='text']").bind('keydown', {editor: this}, BestInPlaceEditor.forms.input.keydownHandler); - // END METAMAPS CODE - this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - inputBlurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abort(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abort(); - event.stopPropagation(); // Without this, click isn't handled - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - }, - - "date" : { - activateForm : function() { - var that = this, - output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'), - input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .attr('value', this.sanitizeValue(this.display_value)); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt) - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find('input')[0].select(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - - this.element.find('input') - .datepicker({ - onClose: function() { - that.update(); - } - }) - .datepicker('show'); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - submitHandler : function(event) { - event.data.editor.update(); - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - } - }, - - "select" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline'); - selected = '', - oldValue = this.oldValue, - select_elt = jQuery(document.createElement('select')) - .attr('class', this.inned_class !== null ? this.inner_class : '' ), - currentCollectionValue = this.collectionValue; - - jQuery.each(this.values, function (index, value) { - var option_elt = jQuery(document.createElement('option')) - // .attr('value', value[0]) - .val(value[0]) - .html(value[1]); - if(value[0] == currentCollectionValue) { - option_elt.attr('selected', 'selected'); - } - select_elt.append(option_elt); - }); - output.append(select_elt); - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); - this.element.find("select")[0].focus(); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("select").val()); - // return this.element.find("select").val(); - }, - - blurHandler : function(event) { - event.data.editor.update(); - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) event.data.editor.abort(); - } - }, - - "checkbox" : { - activateForm : function() { - this.collectionValue = !this.getValue(); - this.setHtmlAttributes(); - this.update(); - }, - - getValue : function() { - return this.collectionValue; - } - }, - - "textarea" : { - activateForm : function() { - // grab width and height of text - width = this.element.css('width'); - height = this.element.css('height'); - - // construct form - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline') - .append(jQuery(document.createElement('textarea')) - .val(this.sanitizeValue(this.display_value))); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('value', this.okButton) - ); - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - - // set width and height of textarea - jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height }); - jQuery(this.element.find("textarea")[0]).elastic(); - - this.element.find("textarea")[0].focus(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); - } - this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); - // START METAMAPS CODE - this.element.find("textarea").bind('keydown', {editor: this}, BestInPlaceEditor.forms.textarea.keydownHandler); - // END METAMAPS CODE - this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("textarea").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - blurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abortIfConfirm(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abortIfConfirm(); - event.stopPropagation(); // Without this, click isn't handled - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abortIfConfirm(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - } -}; - -jQuery.fn.best_in_place = function() { - - function setBestInPlace(element) { - if (!element.data('bestInPlaceEditor')) { - element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); - return true; - } - } - - jQuery(this.context).delegate(this.selector, 'click', function () { - var el = jQuery(this); - if (setBestInPlace(el)) - el.click(); - }); - - this.each(function () { - setBestInPlace(jQuery(this)); - }); - - return this; -}; - - - -/** -* @name Elastic -* @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy -* @version 1.6.5 -* @requires Jquery 1.2.6+ -* -* @author Jan Jarfalk -* @author-email jan.jarfalk@unwrongest.com -* @author-website http://www.unwrongest.com -* -* @licens MIT License - http://www.opensource.org/licenses/mit-license.php -*/ - -(function(jQuery){ - if (typeof jQuery.fn.elastic !== 'undefined') return; - - jQuery.fn.extend({ - elastic: function() { - // We will create a div clone of the textarea - // by copying these attributes from the textarea to the div. - var mimics = [ - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - 'fontSize', - 'lineHeight', - 'fontFamily', - 'width', - 'fontWeight']; - - return this.each( function() { - - // Elastic only works on textareas - if ( this.type != 'textarea' ) { - return false; - } - - var $textarea = jQuery(this), - $twin = jQuery('<div />').css({'position': 'absolute','display':'none','word-wrap':'break-word'}), - lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), - minheight = parseInt($textarea.css('height'),10) || lineHeight*3, - maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, - goalheight = 0, - i = 0; - - // Opera returns max-height of -1 if not set - if (maxheight < 0) { maxheight = Number.MAX_VALUE; } - - // Append the twin to the DOM - // We are going to meassure the height of this, not the textarea. - $twin.appendTo($textarea.parent()); - - // Copy the essential styles (mimics) from the textarea to the twin - i = mimics.length; - while(i--){ - $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); - } - - - // Sets a given height and overflow state on the textarea - function setHeightAndOverflow(height, overflow){ - curratedHeight = Math.floor(parseInt(height,10)); - if($textarea.height() != curratedHeight){ - $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); - - } - } - - - // This function will update the height of the textarea if necessary - function update() { - - // Get curated content from the textarea. - var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '<br />'); - - // Compare curated content with curated twin. - var twinContent = $twin.html().replace(/<br>/ig,'<br />'); - - if(textareaContent+' ' != twinContent){ - - // Add an extra white space so new rows are added when you are at the end of a row. - $twin.html(textareaContent+' '); - - // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height - if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ - - var goalheight = $twin.height()+lineHeight; - if(goalheight >= maxheight) { - setHeightAndOverflow(maxheight,'auto'); - } else if(goalheight <= minheight) { - setHeightAndOverflow(minheight,'hidden'); - } else { - setHeightAndOverflow(goalheight,'hidden'); - } - - } - - } - - } - - // Hide scrollbars - $textarea.css({'overflow':'hidden'}); - - // Update textarea size on keyup, change, cut and paste - $textarea.bind('keyup change cut paste', function(){ - update(); - }); - - // Compact textarea on blur - // Lets animate this.... - $textarea.bind('blur',function(){ - if($twin.height() < maxheight){ - if($twin.height() > minheight) { - $textarea.height($twin.height()); - } else { - $textarea.height(minheight); - } - } - }); - - // And this line is to catch the browser paste event - $textarea.on("input paste", function(e){ setTimeout( update, 250); }); - - // Run update once when elastic is initialized - update(); - - }); - - } - }); -})(jQuery); diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index 59a5ceb8..921a7d88 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -183,11 +183,12 @@ <span class="title"> <div class="titleWrapper" id="titleActivator"> <span class="best_in_place best_in_place_name" - data-url="/topics/{{id}}" - data-object="topic" - data-attribute="name" - data-activator="#titleActivator" - data-type="textarea">{{name}}</span> + data-bip-url="/topics/{{id}}" + data-bip-object="topic" + data-bip-attribute="name" + data-bip-activator="#titleActivator" + data-bip-value="{{name}}" + data-bip-type="textarea">{{name}}</span> </div> </span> <div class="links"> @@ -220,7 +221,7 @@ </div> <div class="scroll"> <div class="desc"> - <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea" data-original-content="{{desc_markdown}}">{{{desc_html}}}</span> + <span class="best_in_place best_in_place_desc" data-bip-url="/topics/{{id}}" data-bip-object="topic" data-bip-nil="{{desc_nil}}" data-bip-attribute="desc" data-bip-type="textarea" data-bip-value="{{desc_markdown}}">{{{desc_html}}}</span> <div class="clearfloat"></div> </div> </div> diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 5a158d2d..8e6b2dba 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -16,7 +16,7 @@ <% if @map %> <div class="mapInfoName" id="mapInfoName"> <% if policy(@map).update? %> - <span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName"><%= @map.name %></span> + <span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="name" data-bip-type="textarea" data-bip-activator="#mapInfoName" data-bip-value="<%= @map.name %>"><%= @map.name %></span> <% else %> <%= @map.name %> <% end %> @@ -67,7 +67,7 @@ <div class="mapInfoDesc" id="mapInfoDesc"> <% if policy(@map).update? %> - <span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc"><%= @map.desc %></span> + <span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="desc" data-bip-nil="Click to add description..." data-bip-type="textarea" data-bip-activator="#mapInfoDesc" data-bip-value="<%= @map.desc %>"><%= @map.desc %></span> <% else %> <%= @map.desc %> <% end %> diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index cf3365f3..db78323d 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -23,7 +23,6 @@ const Listeners = { if (e.target.className !== 'chat-input') { JIT.enterKeyHandler() } - e.preventDefault() break case 27: // if esc key is pressed JIT.escKeyHandler() diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 0d3a5c5f..ddfd72c3 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,5 +1,7 @@ /* global Metamaps, $, Hogan, Bloodhound, Countable */ +import outdent from 'outdent' + import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' @@ -19,8 +21,27 @@ const InfoBox = { changing: false, selectingPermission: false, changePermissionText: "<div class='tooltips'>As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.</div>", - nameHTML: '<span class="best_in_place best_in_place_name" id="best_in_place_map_{{id}}_name" data-url="/maps/{{id}}" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName">{{name}}</span>', - descHTML: '<span class="best_in_place best_in_place_desc" id="best_in_place_map_{{id}}_desc" data-url="/maps/{{id}}" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc">{{desc}}</span>', + nameHTML: outdent` + <span class="best_in_place best_in_place_name" + id="best_in_place_map_{{id}}_name" + data-bip-url="/maps/{{id}}" + data-bip-object="map" + data-bip-attribute="name" + data-bip-type="textarea" + data-bip-activator="#mapInfoName" + data-bip-value="{{name}}" + >{{name}}</span>`, + descHTML: outdent` + <span class="best_in_place best_in_place_desc" + id="best_in_place_map_{{id}}_desc" + data-bip-url="/maps/{{id}}" + data-bip-object="map" + data-bip-attribute="desc" + data-bip-nil="Click to add description..." + data-bip-type="textarea" + data-bip-activator="#mapInfoDesc" + data-bip-value="{{desc}}" + >{{desc}}</span>`, init: function () { var self = InfoBox diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 8203657d..b1810dac 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -80,11 +80,12 @@ const SynapseCard = { // desc editing form $('#editSynUpperBar').append('<div id="edit_synapse_desc"></div>') $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc') - $('#edit_synapse_desc').attr('data-object', 'synapse') - $('#edit_synapse_desc').attr('data-attribute', 'desc') - $('#edit_synapse_desc').attr('data-type', 'textarea') - $('#edit_synapse_desc').attr('data-nil', data_nil) - $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').attr('data-bip-object', 'synapse') + $('#edit_synapse_desc').attr('data-bip-attribute', 'desc') + $('#edit_synapse_desc').attr('data-bip-type', 'textarea') + $('#edit_synapse_desc').attr('data-bip-nil', data_nil) + $('#edit_synapse_desc').attr('data-bip-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').attr('data-bip-value', synapse.get('desc')) $('#edit_synapse_desc').html(synapse.get('desc')) // if edge data is blank or just whitespace, populate it with data_nil diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 84892450..f74cf18f 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -274,12 +274,13 @@ const TopicCard = { }) // this is for all subsequent renders after in-place editing the desc field - $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { - var desc = $(this).html() === $(this).data('nil') + const bipDesc = $(showCard).find('.best_in_place_desc') + bipDesc.bind('ajax:success', function () { + var desc = $(this).html() === $(this).data('bip-nil') ? '' : $(this).text() topic.set('desc', desc) - $(this).data('bestInPlaceEditor').original_content = desc + $(this).data('bip-value', desc) this.innerHTML = Util.mdToHTML(desc) topic.trigger('saved') }) @@ -458,7 +459,7 @@ const TopicCard = { nodeValues.metacode_select = $('#metacodeOptions').html() nodeValues.desc_nil = 'Click to add description...' nodeValues.desc_markdown = (topic.get('desc') === '' && authorized) - ? desc_nil + ? nodeValues.desc_nil : topic.get('desc') nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown) return nodeValues From f77562937177e3b9daa92139f226db1a2e32801d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:03:50 +0800 Subject: [PATCH 186/378] showCard .desc css for ul and a tags --- app/assets/stylesheets/base.css.erb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 5b0fcf84..1e5d68e8 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -143,6 +143,15 @@ margin-top:5px; } +.CardOnGraph .desc ul { + margin-left: 1em; + +} +.CardOnGraph .desc a:hover { + text-decoration: underline; + opacity: 0.9; +} + .CardOnGraph .best_in_place_desc { display:block; margin-top:2px; From 7eacda2ae7c175ba1113ad2de01ae9e9be9f8118 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:31:32 +0800 Subject: [PATCH 187/378] code style --- .eslintrc.js | 7 ++++++ .../src/Metamaps/GlobalUI/ImportDialog.js | 14 ++++++----- frontend/src/Metamaps/Map/index.js | 3 --- frontend/src/Metamaps/PasteInput.js | 2 +- frontend/src/components/ImportDialogBox.js | 3 ++- frontend/src/components/Maps/Header.js | 24 +++++++++---------- frontend/src/components/Maps/MapCard.js | 10 ++++---- frontend/src/components/Maps/MapperCard.js | 16 ++++++------- frontend/src/components/Maps/index.js | 12 +++++----- 9 files changed, 49 insertions(+), 42 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index aa594fa7..1222f4a1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,11 @@ module.exports = { "sourceType": "module", "parser": "babel-eslint", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, "extends": "standard", "installedESLint": true, "env": { @@ -13,6 +18,8 @@ module.exports = { "react" ], "rules": { + "react/jsx-uses-react": [2], + "react/jsx-uses-vars": [2], "yoda": [2, "never", { "exceptRange": true }] } } diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js index 3671cd90..96f9524f 100644 --- a/frontend/src/Metamaps/GlobalUI/ImportDialog.js +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -1,3 +1,5 @@ +/* global $ */ + import React from 'react' import ReactDOM from 'react-dom' import outdent from 'outdent' @@ -10,7 +12,7 @@ const ImportDialog = { openLightbox: null, closeLightbox: null, - init: function(serverData, openLightbox, closeLightbox) { + init: function (serverData, openLightbox, closeLightbox) { const self = ImportDialog self.openLightbox = openLightbox self.closeLightbox = closeLightbox @@ -22,14 +24,14 @@ const ImportDialog = { `)) ReactDOM.render(React.createElement(ImportDialogBox, { onFileAdded: PasteInput.handleFile, - exampleImageUrl: serverData['import-example.png'], + exampleImageUrl: serverData['import-example.png'] }), $('.importDialogWrapper').get(0)) }, - show: function() { - self.openLightbox('import-dialog') + show: function () { + ImportDialog.openLightbox('import-dialog') }, - hide: function() { - self.closeLightbox('import-dialog') + hide: function () { + ImportDialog.closeLightbox('import-dialog') } } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 7d7322fc..43f04a30 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,8 +1,6 @@ /* global Metamaps, $ */ import outdent from 'outdent' -import React from 'react' -import ReactDOM from 'react-dom' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -47,7 +45,6 @@ const Map = { return false }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 51d4a933..f0425032 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -45,7 +45,7 @@ const PasteInput = { handleFile: (file, coords = null) => { var self = PasteInput - var fileReader = new FileReader() + var fileReader = new window.FileReader() fileReader.readAsText(file) fileReader.onload = function(e) { var text = e.currentTarget.result diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index 9851fd07..bfb60235 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -2,7 +2,7 @@ import React, { PropTypes, Component } from 'react' import Dropzone from 'react-dropzone' class ImportDialogBox extends Component { - constructor(props) { + constructor (props) { super(props) this.state = { @@ -16,6 +16,7 @@ class ImportDialogBox extends Component { handleFile = (files, e) => { // for some reason it uploads twice, so we need this debouncer + // eslint-disable-next-line no-return-assign this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) if (!this.debouncer) { this.props.onFileAdded(files[0]) diff --git a/frontend/src/components/Maps/Header.js b/frontend/src/components/Maps/Header.js index ee4184d5..d323c0d5 100644 --- a/frontend/src/components/Maps/Header.js +++ b/frontend/src/components/Maps/Header.js @@ -21,15 +21,15 @@ class Header extends Component { const { signedIn, section } = this.props const activeClass = (title) => { - let forClass = "exploreMapsButton" - forClass += " " + title + "Maps" - if (title == "my" && section == "mine" || - title == section) forClass += " active" + let forClass = 'exploreMapsButton' + forClass += ' ' + title + 'Maps' + if (title === 'my' && section === 'mine' || + title === section) forClass += ' active' return forClass } - const explore = section == "mine" || section == "active" || section == "starred" || section == "shared" || section == "featured" - const mapper = section == "mapper" + const explore = section === 'mine' || section === 'active' || section === 'starred' || section === 'shared' || section === 'featured' + const mapper = section === 'mapper' return ( <div id="exploreMapsHeader"> @@ -38,31 +38,31 @@ class Header extends Component { <div className="exploreMapsCenter"> <MapLink show={signedIn && explore} href="/explore/mine" - linkClass={activeClass("my")} + linkClass={activeClass('my')} data-router="true" text="My Maps" /> <MapLink show={signedIn && explore} href="/explore/shared" - linkClass={activeClass("shared")} + linkClass={activeClass('shared')} data-router="true" text="Shared With Me" /> <MapLink show={signedIn && explore} href="/explore/starred" - linkClass={activeClass("starred")} + linkClass={activeClass('starred')} data-router="true" text="Starred By Me" /> <MapLink show={explore} - href={signedIn ? "/" : "/explore/active"} - linkClass={activeClass("active")} + href={signedIn ? '/' : '/explore/active'} + linkClass={activeClass('active')} data-router="true" text="Global" /> <MapLink show={!signedIn && explore} href="/explore/featured" - linkClass={activeClass("featured")} + linkClass={activeClass('featured')} data-router="true" text="Featured Maps" /> diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index bf1416fc..e31ede18 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -3,7 +3,7 @@ import React, { Component, PropTypes } from 'react' class MapCard extends Component { render = () => { const { map, currentUser } = this.props - + function capitalize (string) { return string.charAt(0).toUpperCase() + string.slice(1) } @@ -16,12 +16,12 @@ class MapCard extends Component { const truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' const truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' const editPermission = map.authorizeToEdit(currentUser) ? 'canEdit' : 'cannotEdit' - + return ( <div className="map" id={ map.id }> <a href={ '/maps/' + map.id } data-router="true"> <div className={ 'permission ' + editPermission }> - <div className="mapCard"> + <div className="mapCard"> <span className="title" title={ map.get('name') }> { truncatedName } </span> @@ -46,7 +46,7 @@ class MapCard extends Component { { map.get('topic_count') } </span> { map.get('topic_count') === 1 ? ' topic' : ' topics' } - </div> + </div> <div className="metadataSection mapPermission"> { map.get('permission') ? capitalize(map.get('permission')) : 'Commons' } </div> @@ -57,7 +57,7 @@ class MapCard extends Component { { map.get('synapse_count') === 1 ? ' synapse' : ' synapses' } </div> <div className="clearfloat"></div> - </div> + </div> </div> </div> </a> diff --git a/frontend/src/components/Maps/MapperCard.js b/frontend/src/components/Maps/MapperCard.js index e2f4cb33..dbb06cbc 100644 --- a/frontend/src/components/Maps/MapperCard.js +++ b/frontend/src/components/Maps/MapperCard.js @@ -3,14 +3,14 @@ import React, { Component, PropTypes } from 'react' class MapperCard extends Component { render = () => { const { user } = this.props - + return ( <div className="mapper"> - <div className="mapperCard"> + <div className="mapperCard"> <div className="mapperImage"> <img src={ user.image } width="96" height="96" /> - </div> - <div className="mapperName" title={ user.name }> + </div> + <div className="mapperName" title={ user.name }> { user.name } </div> <div className="mapperInfo"> @@ -19,10 +19,10 @@ class MapperCard extends Component { </div> <div className="mapperMetadata"> <div className="metadataSection metadataMaps"><div>{ user.numMaps }</div>maps</div> - <div className="metadataSection metadataTopics"><div>{ user.numTopics }</div>topics</div> - <div className="metadataSection metadataSynapses"><div>{ user.numSynapses }</div>synapses</div> - <div className="clearfloat"></div> - </div> + <div className="metadataSection metadataTopics"><div>{ user.numTopics }</div>topics</div> + <div className="metadataSection metadataSynapses"><div>{ user.numSynapses }</div>synapses</div> + <div className="clearfloat"></div> + </div> </div> </div> ) diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 2c3e8ba1..22a41d3e 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -9,12 +9,11 @@ class Maps extends Component { const { maps, currentUser, section, displayStyle, user, moreToLoad, loadMore } = this.props let mapElements - if (displayStyle == 'grid') { + if (displayStyle === 'grid') { mapElements = maps.models.map(function (map) { return <MapCard key={ map.id } map={ map } currentUser={ currentUser } /> }) - } - else if (displayStyle == 'list') { + } else if (displayStyle === 'list') { mapElements = maps.models.map(function (map) { return <MapListItem key={ map.id } map={ map } /> }) @@ -28,9 +27,10 @@ class Maps extends Component { { currentUser && !user ? <div className="map newMap"><a href="/maps/new"><div className="newMapImage"></div><span>Create new map...</span></a></div> : null } { mapElements } <div className='clearfloat'></div> - { moreToLoad ? - [<button className="button loadMore" onClick={ loadMore }>load more</button>, <div className='clearfloat'></div>] - : null } + {!moreToLoad ? null : [ + <button className="button loadMore" onClick={ loadMore }>load more</button>, + <div className='clearfloat'></div> + ]} </div> </div> <Header signedIn={ !!currentUser } From 129e3db9465cbb46182c38acbc3f5af97a5a0a30 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 12:26:08 +0800 Subject: [PATCH 188/378] redirect to root_path if you get a 403 --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5dea17b5..eddf510d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base def handle_unauthorized if authenticated? - head :forbidden # TODO: make this better + redirect_to root_path, notice: "You don't have permission to see that page." else redirect_to new_user_session_path, notice: 'Try signing in to do that.' end From be6a2401b657fcccf4bf962c781df0456bed4a89 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 13:42:25 +0800 Subject: [PATCH 189/378] fix spec. not sure how this should work --- spec/controllers/maps_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 0f053dd9..800ab4dc 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -79,8 +79,8 @@ RSpec.describe MapsController, type: :controller do id: unowned_map.to_param } end.to change(Map, :count).by(0) - expect(response.body).to eq '' - expect(response.status).to eq 403 + expect(response.headers['Location']).to eq(request.base_url + root_path) + expect(response.status).to eq 302 end it 'deletes owned map' do From 2c64b67abdb86ae23cfc775a02cd70019bc8ed10 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 12:42:17 +0800 Subject: [PATCH 190/378] return 404s for all unmatched api routes --- app/controllers/api/v1/deprecated_controller.rb | 2 +- app/controllers/api/v2/restful_controller.rb | 5 +++++ config/routes.rb | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index b9e07214..aa67d6b1 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -4,7 +4,7 @@ module Api class DeprecatedController < ApplicationController # rubocop:disable Style/MethodMissing def method_missing - render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } + render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' }, status: :gone end # rubocop:enable Style/MethodMissing end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index 5d8f81b3..b64682f3 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -29,6 +29,11 @@ module Api head :no_content end + def catch_404 + skip_authorization + render json: { error: '404 Not found' }, status: :not_found + end + private def accessible_records diff --git a/config/routes.rb b/config/routes.rb index 05fe5845..62728fe7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,7 @@ Metamaps::Application.routes.draw do resources :users, only: [:index, :show] do get :current, on: :collection end + match '*path', to: 'restful#catch_404', via: :all end namespace :v1, path: '/v1' do # api v1 routes all lead to a deprecation error method @@ -88,7 +89,9 @@ Metamaps::Application.routes.draw do resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection end + match '*path', to: 'deprecated#method_missing', via: :all end + match '*path', to: 'v2/restful#catch_404', via: :all end devise_for :users, skip: :sessions, controllers: { From 9513087bbddcefdf86c46e8d3284781ff6674228 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 14:00:29 +0800 Subject: [PATCH 191/378] remove unnecessary api v1 code --- app/controllers/api/v1/deprecated_controller.rb | 8 ++++---- app/controllers/api/v1/mappings_controller.rb | 7 ------- app/controllers/api/v1/maps_controller.rb | 7 ------- app/controllers/api/v1/synapses_controller.rb | 7 ------- app/controllers/api/v1/tokens_controller.rb | 7 ------- app/controllers/api/v1/topics_controller.rb | 7 ------- config/routes.rb | 12 ++---------- 7 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 app/controllers/api/v1/mappings_controller.rb delete mode 100644 app/controllers/api/v1/maps_controller.rb delete mode 100644 app/controllers/api/v1/synapses_controller.rb delete mode 100644 app/controllers/api/v1/tokens_controller.rb delete mode 100644 app/controllers/api/v1/topics_controller.rb diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index aa67d6b1..3269a1a8 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -2,11 +2,11 @@ module Api module V1 class DeprecatedController < ApplicationController - # rubocop:disable Style/MethodMissing - def method_missing - render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' }, status: :gone + def deprecated + render json: { + error: '/api/v1 has been deprecated! Please use /api/v2 instead.' + }, status: :gone end - # rubocop:enable Style/MethodMissing end end end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb deleted file mode 100644 index 8ba6e704..00000000 --- a/app/controllers/api/v1/mappings_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class MappingsController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb deleted file mode 100644 index 0ff6f472..00000000 --- a/app/controllers/api/v1/maps_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class MapsController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb deleted file mode 100644 index 32522e52..00000000 --- a/app/controllers/api/v1/synapses_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class SynapsesController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb deleted file mode 100644 index 9df2094a..00000000 --- a/app/controllers/api/v1/tokens_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class TokensController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb deleted file mode 100644 index d316bfa8..00000000 --- a/app/controllers/api/v1/topics_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class TopicsController < DeprecatedController - end - end -end diff --git a/config/routes.rb b/config/routes.rb index 62728fe7..b5078d86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,16 +80,8 @@ Metamaps::Application.routes.draw do match '*path', to: 'restful#catch_404', via: :all end namespace :v1, path: '/v1' do - # api v1 routes all lead to a deprecation error method - # see app/controllers/api/v1/deprecated_controller.rb - resources :maps, only: [:create, :show, :update, :destroy] - resources :synapses, only: [:create, :show, :update, :destroy] - resources :topics, only: [:create, :show, :update, :destroy] - resources :mappings, only: [:create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection - end - match '*path', to: 'deprecated#method_missing', via: :all + root to: 'deprecated#deprecated', via: :all + match '*path', to: 'deprecated#deprecated', via: :all end match '*path', to: 'v2/restful#catch_404', via: :all end From fe1c57b458ce6fb04b1821e098f7f304b1f47a6b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 16:42:34 +0800 Subject: [PATCH 192/378] further updates - make Enter update bip fields whaaat --- app/assets/javascripts/lib/jquery.purr.js | 180 ---------------------- app/assets/stylesheets/base.css.erb | 1 + frontend/src/Metamaps/Map/InfoBox.js | 7 + frontend/src/Metamaps/SynapseCard.js | 6 + frontend/src/Metamaps/TopicCard.js | 13 ++ 5 files changed, 27 insertions(+), 180 deletions(-) delete mode 100644 app/assets/javascripts/lib/jquery.purr.js diff --git a/app/assets/javascripts/lib/jquery.purr.js b/app/assets/javascripts/lib/jquery.purr.js deleted file mode 100644 index 1972165b..00000000 --- a/app/assets/javascripts/lib/jquery.purr.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * jquery.purr.js - * Copyright (c) 2008 Net Perspective (net-perspective.com) - * Licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @author R.A. Ray - * @projectDescription jQuery plugin for dynamically displaying unobtrusive messages in the browser. Mimics the behavior of the MacOS program "Growl." - * @version 0.1.0 - * - * @requires jquery.js (tested with 1.2.6) - * - * @param fadeInSpeed int - Duration of fade in animation in miliseconds - * default: 500 - * @param fadeOutSpeed int - Duration of fade out animationin miliseconds - default: 500 - * @param removeTimer int - Timeout, in miliseconds, before notice is removed once it is the top non-sticky notice in the list - default: 4000 - * @param isSticky bool - Whether the notice should fade out on its own or wait to be manually closed - default: false - * @param usingTransparentPNG bool - Whether or not the notice is using transparent .png images in its styling - default: false - */ - -( function( $ ) { - - $.purr = function ( notice, options ) - { - // Convert notice to a jQuery object - notice = $( notice ); - - // Add a class to denote the notice as not sticky - if ( !options.isSticky ) - { - notice.addClass( 'not-sticky' ); - }; - - // Get the container element from the page - var cont = document.getElementById( 'purr-container' ); - - // If the container doesn't yet exist, we need to create it - if ( !cont ) - { - cont = '<div id="purr-container"></div>'; - } - - // Convert cont to a jQuery object - cont = $( cont ); - - // Add the container to the page - $( 'body' ).append( cont ); - - notify(); - - function notify () - { - // Set up the close button - var close = document.createElement( 'a' ); - $( close ).attr( - { - className: 'close', - href: '#close', - innerHTML: 'Close' - } - ) - .appendTo( notice ) - .click( function () - { - removeNotice(); - - return false; - } - ); - - // Add the notice to the page and keep it hidden initially - notice.appendTo( cont ) - .hide(); - - if ( jQuery.browser.msie && options.usingTransparentPNG ) - { - // IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our - // notice style, we'll just skip the fading in. - notice.show(); - } - else - { - //Fade in the notice we just added - notice.fadeIn( options.fadeInSpeed ); - } - - // Set up the removal interval for the added notice if that notice is not a sticky - if ( !options.isSticky ) - { - var topSpotInt = setInterval( function () - { - // Check to see if our notice is the first non-sticky notice in the list - if ( notice.prevAll( '.not-sticky' ).length == 0 ) - { - // Stop checking once the condition is met - clearInterval( topSpotInt ); - - // Call the close action after the timeout set in options - setTimeout( function () - { - removeNotice(); - }, options.removeTimer - ); - } - }, 200 ); - } - } - - function removeNotice () - { - // IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our - // notice style, we'll just skip the fading out. - if ( jQuery.browser.msie && options.usingTransparentPNG ) - { - notice.css( { opacity: 0 } ) - .animate( - { - height: '0px' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.remove(); - } - } - ); - } - else - { - // Fade the object out before reducing its height to produce the sliding effect - notice.animate( - { - opacity: '0' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.animate( - { - height: '0px' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.remove(); - } - } - ); - } - } - ); - } - }; - }; - - $.fn.purr = function ( options ) - { - options = options || {}; - options.fadeInSpeed = options.fadeInSpeed || 500; - options.fadeOutSpeed = options.fadeOutSpeed || 500; - options.removeTimer = options.removeTimer || 4000; - options.isSticky = options.isSticky || false; - options.usingTransparentPNG = options.usingTransparentPNG || false; - - this.each( function() - { - new $.purr( this, options ); - } - ); - - return this; - }; -})( jQuery ); - diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 1e5d68e8..6cfb6b57 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -143,6 +143,7 @@ margin-top:5px; } +.CardOnGraph .desc ol, .CardOnGraph .desc ul { margin-left: 1em; diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ddfd72c3..79fa6c4d 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -173,6 +173,13 @@ const InfoBox = { Active.Map.trigger('saved') }) + $('.mapInfoDesc .best_in_place_desc, .mapInfoName .best_in_place_name').unbind('keypress').keypress(function(e) { + const ENTER = 13 + if (e.which === ENTER) { + $(this).data('bestInPlaceEditor').update() + } + }) + $('.yourMap .mapPermission').unbind().click(self.onPermissionClick) // .yourMap in the unbind/bind is just a namespace for the events // not a reference to the class .yourMap on the .mapInfoBox diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index b1810dac..303b98cf 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -97,6 +97,12 @@ const SynapseCard = { } } + $('#edit_synapse_desc').keypress(function (e) { + const ENTER = 13 + if (e.which === ENTER) { + $(this).data('bestInPlaceEditor').update() + } + }) $('#edit_synapse_desc').bind('ajax:success', function () { var desc = $(this).html() if (desc == data_nil) { diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index f74cf18f..0956c07b 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -265,6 +265,12 @@ const TopicCard = { bipName.bind('best_in_place:deactivate', function () { $('.nameCounter.forTopic').remove() }) + bipName.keypress(function(e) { + const ENTER = 13 + if (e.which === ENTER) { // enter + $(this).data('bestInPlaceEditor').update() + } + }) // bind best_in_place ajax callbacks bipName.bind('ajax:success', function () { @@ -284,6 +290,13 @@ const TopicCard = { this.innerHTML = Util.mdToHTML(desc) topic.trigger('saved') }) + bipDesc.keypress(function(e) { + // allow typing Enter with Shift+Enter + const ENTER = 13 + if (e.shiftKey === false && e.which === ENTER) { + $(this).data('bestInPlaceEditor').update() + } + }) } var permissionLiClick = function (event) { From ba9e26bc05384a03e711dc5d443621138070a916 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 9 Oct 2016 10:20:17 +0800 Subject: [PATCH 193/378] enable xss filtering and smart quote replacement in markdown --- frontend/src/Metamaps/Util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index f1f8b39c..2e21f4e5 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -123,7 +123,9 @@ const Util = { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) }, mdToHTML: text => { - return new HtmlRenderer().render(new Parser().parse(text)) + // use safe: true to filter xss + return new HtmlRenderer({ safe: true, smart: true }) + .render(new Parser().parse(text)) } } From 8b1d85c3ca3b964109cf83b0b94d742cbb3e85b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 9 Oct 2016 10:24:13 +0800 Subject: [PATCH 194/378] actually the smart option is dumb --- frontend/src/Metamaps/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 2e21f4e5..32730a6f 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -124,7 +124,7 @@ const Util = { }, mdToHTML: text => { // use safe: true to filter xss - return new HtmlRenderer({ safe: true, smart: true }) + return new HtmlRenderer({ safe: true }) .render(new Parser().parse(text)) } } From 6e6d33abbe4cfbd305269ccd3b8c812a8b963a3f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 10 Oct 2016 12:12:42 +0800 Subject: [PATCH 195/378] fix screenshot no file error --- frontend/src/Metamaps/Map/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 43f04a30..48be9def 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -326,8 +326,6 @@ const Map = { node.visited = !T }) - var imageData = canvas.canvas.toDataURL() - var map = Active.Map var today = new Date() @@ -347,7 +345,7 @@ const Map = { var downloadMessage = outdent` Captured map screenshot! - <a href="${imageData.encodedImage}" download="${filename}">DOWNLOAD</a>` + <a href="${canvas.canvas.toDataURL()}" download="${filename}">DOWNLOAD</a>` GlobalUI.notifyUser(downloadMessage) canvas.canvas.toBlob(imageBlob => { From 858ca66d69ed9e43f4d3a4ce6be288508cf4efa1 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 10 Oct 2016 17:20:43 +0800 Subject: [PATCH 196/378] eslint updates --- frontend/src/Metamaps/Listeners.js | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index ce14dc9f..00238e90 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -123,36 +123,36 @@ const Listeners = { }) $(window).resize(function () { - if (Visualize && Visualize.mGraph){ - //Find the current canvas scale and map-coordinate at the centre of the user's screen - var canvas = Visualize.mGraph.canvas, - scaleX = canvas.scaleOffsetX, - scaleY = canvas.scaleOffsetY, - centrePixX = canvas.canvases[0].size.width / 2, - centrePixY = canvas.canvases[0].size.height / 2, - centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - - //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 + if (Visualize && Visualize.mGraph) { + // Find the current canvas scale and map-coordinate at the centre of the user's screen + let canvas = Visualize.mGraph.canvas + const scaleX = canvas.scaleOffsetX + const scaleY = canvas.scaleOffsetY + const centrePixX = canvas.canvases[0].size.width / 2 + const centrePixY = canvas.canvases[0].size.height / 2 + const centreCoords = Util.pixelsToCoords({ x: centrePixX, y: centrePixY }) + + // Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 canvas.resize($(window).width(), $(window).height()) - - //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen - canvas.scale(scaleX,scaleY); - var newCentrePixX = canvas.canvases[0].size.width / 2, - newCentrePixY = canvas.canvases[0].size.height / 2, - newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); - - canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + + // Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen + canvas.scale(scaleX, scaleY) + const newCentrePixX = canvas.canvases[0].size.width / 2 + const newCentrePixY = canvas.canvases[0].size.height / 2 + const newCentreCoords = Util.pixelsToCoords({ x: newCentrePixX, y: newCentrePixY }) + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y) } - + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() }) }, - centerAndReveal: function(nodes, opts) { + centerAndReveal: function (nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Topic.centerOn(node.id, function() { + Topic.centerOn(node.id, function () { Topic.fetchRelatives(nodes) }) } else if (opts.center) { From 3051723bcf27ad0adcfbe7864c1e166da8d6efa9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 12 Oct 2016 00:08:31 +0800 Subject: [PATCH 197/378] [WIP] add markdown getting started page to api docs (#752) * add markdown getting started page to api docs. TODO section 3 * Update getting-started.md --- doc/api/api.raml | 6 +++ doc/api/pages/getting-started.md | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 doc/api/pages/getting-started.md diff --git a/doc/api/api.raml b/doc/api/api.raml index 6ffa29f1..b45fe0c5 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -4,6 +4,12 @@ title: Metamaps version: v2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json +protocols: [ HTTPS ] +documentation: + - title: Getting Started + content: !include pages/getting-started.md + - title: Endpoints + content: "" securitySchemes: oauth_2_0: !include securitySchemes/oauth_2_0.raml diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md new file mode 100644 index 00000000..889168a4 --- /dev/null +++ b/doc/api/pages/getting-started.md @@ -0,0 +1,81 @@ +[Skip ahead to the endpoints.](#endpoints) + +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. + +### 1. Cookie-based authentication + +One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. + +To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. + +### 2. Token-based authentication + +If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. + +To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. + +``` +fetch('/api/v2/tokens', { + method: 'GET', + credentials: 'same-origin' // needed to use the cookie-based auth +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: + +``` +fetch('/api/v2/tokens', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. + +### 3. OAuth 2 Authentication + +We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) + +The first step is to register your client app. + +#### Registering the client + +Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). + +#### Requesting authorization + +To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: + +``` +http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code +``` + +Once you are there, you should sign in and click on `Authorize`. +You will then see a response that contains your "authorization code", which you need to exchange for an access token. + +#### Requesting the access token + +To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` + +```javascript +fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) + +# The response will be like +{ + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" +} +``` + +You can now make requests to the API with the access token returned. From 62c489cba7ad6c1003b33d6c8eb67cfd36d7065f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 13 Oct 2016 00:22:38 +0800 Subject: [PATCH 198/378] suggesting api doc updates (#756) --- doc/api/api.raml | 2 +- doc/api/pages/getting-started.md | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/api/api.raml b/doc/api/api.raml index b45fe0c5..4473a6dd 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -1,7 +1,7 @@ #%RAML 1.0 --- title: Metamaps -version: v2.0 +version: 2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json protocols: [ HTTPS ] diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md index 889168a4..c620e888 100644 --- a/doc/api/pages/getting-started.md +++ b/doc/api/pages/getting-started.md @@ -1,6 +1,6 @@ [Skip ahead to the endpoints.](#endpoints) -There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. ### 1. Cookie-based authentication @@ -14,7 +14,7 @@ If you are logged into the API via another means, you can create a token. Once y To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. -``` +```javascript fetch('/api/v2/tokens', { method: 'GET', credentials: 'same-origin' // needed to use the cookie-based auth @@ -25,13 +25,15 @@ fetch('/api/v2/tokens', { If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: -``` +```javascript fetch('/api/v2/tokens', { method: 'POST', credentials: 'same-origin' }).then(response => { return response.json() -}).then(console.log).catch(console.error) +}).then(payload => { + console.log(payload) +}).catch(console.error) ``` `payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. @@ -68,8 +70,11 @@ fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET }).then(response => { return response.json() }).then(console.log).catch(console.error) +``` -# The response will be like +The response will look like + +```json { "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", "token_type": "bearer", From 7eae8deacbaed64394dd3071274f46cd1632da20 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 13 Oct 2016 01:54:43 +0800 Subject: [PATCH 199/378] revamp HTML template a bit for api docs (#757) * my_tokens endpoint moved to normal index * remove secured_by from metacodes/users * ch ch ch changes * mess with template * fix securedBy * convenience open * gross authentication notes at the top of every endpoint * better ordering * move login tutorials into security tab * oauth tutorial * getting closer * remove unneeded Endpoints header * ok looks OK --- .env.swp | Bin 0 -> 12288 bytes app/controllers/api/v2/tokens_controller.rb | 6 - bin/build-apidocs.sh | 10 +- config/routes.rb | 4 +- doc/api/api.raml | 6 +- doc/api/apis/metacodes.raml | 2 + doc/api/apis/tokens.raml | 17 +- doc/api/apis/users.raml | 18 +- doc/api/pages/cookie_tutorial.md | 3 + doc/api/pages/getting-started.md | 86 +----- doc/api/pages/oauth_2_0_tutorial.md | 41 +++ doc/api/pages/token_tutorial.md | 25 ++ doc/api/securitySchemes/cookie.raml | 3 + doc/api/securitySchemes/oauth_2_0.raml | 3 +- doc/api/securitySchemes/token.raml | 3 + doc/api/templates/item.nunjucks | 61 ++++ doc/api/templates/resource.nunjucks | 314 ++++++++++++++++++++ doc/api/templates/template.nunjucks | 232 +++++++++++++++ 18 files changed, 718 insertions(+), 116 deletions(-) create mode 100644 .env.swp create mode 100644 doc/api/pages/cookie_tutorial.md create mode 100644 doc/api/pages/oauth_2_0_tutorial.md create mode 100644 doc/api/pages/token_tutorial.md create mode 100644 doc/api/securitySchemes/cookie.raml create mode 100644 doc/api/securitySchemes/token.raml create mode 100644 doc/api/templates/item.nunjucks create mode 100644 doc/api/templates/resource.nunjucks create mode 100644 doc/api/templates/template.nunjucks diff --git a/.env.swp b/.env.swp new file mode 100644 index 0000000000000000000000000000000000000000..e737a79b5a0d3866f6b3955a4495bd58f83cdc9c GIT binary patch literal 12288 zcmeI2&yU+g6vthJprw>naIc2ukX_Ma$G`0r2C|9U4V%QPIN7CiD>ELCH_>|SU^{IW zDv;VDapTwvN5qXA5>opQ@CP6+2!VP++&FXLIh&;WLo6WmK&7$tjpH}(y?OKTjI*1J zk2jiaq*oOIjtheDrTfF?#Z!Xt?el^#_Q!EPE*efgXcEUsnv9Yt8E2JUmXlpF%7P@P zv?7^`s)+I=!6*9(mu@BLPBtQxEmc{3^W@yTl@s6uo)&>~f-H%+$X2@aI(qHZ{?kg# z^Kb&304Kl+Z~~kFC%_4C0-V4zKp-ET5<Z1GpPSY?`5pk?!&d;4V|Jci@)IY(32*|O z04Kl+Z~~kFC%_4C0-OLRzzIBq1V|tV4_*|6d#9mz`2BzO|NrCjg77`)8_)yL$Dofu z70~am2*M-KL(oOg{c{ixQbA`yXFz|vEC@e>z5{&?`U>;~=r)Lf)<Gu70R4GZ5Pk;z z1bPH|2>Kk<1ziTc0Q%)6*pmSI4D<m=2b~7}dIs#Ed!W0ZHs}KAJm@zV(|u4ew!6jn z_`wNq0-OLRzzJ{y{~H3O5<+M@Wg}Ecn^`hIm34G!;q5mqyR)&luvHCNP6t~HcebuC zR<GA8?|V1is4i?R-r2gbcnRH^MkWw5Z7hsglIEx~MsJ~<?T(O8LSd33g2wUiLzJWe zOId)TVV0vLM6e4H-6UzY4Ev%E*YclADT2dc9H4x_5Y&$7I89lcM|)@(qjAQHQ_)67 zBe)#K+h`fVpF_7tMX}xf$~Et5-E&%vcCFQN>RzkaZuY#ZU1zhm>RqYTdrsF9VW;Ll z1hi}KS(+hfrU_fUPWH8ANyVmOh)4ZStUxnPbf5HZVpSDY`6Nf^+%RMXYe$_{?b>>Q z!C86NuJ@)G{(k%u_bDal+jCn^&$V<#)sGGTpzAy#K33(#?Mm0Sr@2(sl=NeRo_IAo zaEtbP6b((0jt!>gtl1q)2^HDEAvK9)sCpoV7#kR?L}tFBnbNV`y?)2GFe+s%M|pz8 z;)TV;j~KdIhbf)qFjY}H_V>^>Nqw@-5KW?p(R`T1D9guTC=`=^di&j0t-j{j9dFZK zS#_K>uitG!YYAlxrYpNm*=Q0NC;2cOQc_%GMV|y^5urOUacQz!OxRn@A0)|6(fcfS zySMK3UAx<<we2Hjux{AhBgS>7+dFEix$dUZZ5%N-oOZ3*5lRQluH&uQ*Pa+^o37{D z^{(CXYW2G9y2qQJ*uCb#9%OH&Utfd3{lXz<3EdhD=>SmzYjrOfm!k;93?39fVsa-2 zY!{736HjOcKVVHl1`9l5<R&W?H$?goiFYo;qb6cmHp!DR6tGd0>=g(hXc%WXi7C4b z@8%DH=`dh;-b&52t+J%))F6fuYO<-pVv#~c^{I?S%t#P~Skj3f5??h!s;g$8F-bOQ zF|9&FWy}OifJI*<KEx|D023v8Xi!rUWrAsFih&#$fh?;y#HJ=nGLtBw)Tb1ik`k6@ z3$tNwG+kH>`<$Ubs~qYgRWzNck`_vGpoxkv``9ovjgmmo4TGv!@>u}0n--O0jp;ID z8Z{wG^))GEVyFeG6cXJhhG=TOEMplKMj(Z<51A#M5v*&bOjU_d21WT;)G>zq&|7(q zZCV`6l$PCv8aoZUYn5|GcHfz8MXypiAIsFQVPC2BTfJf-Sml}D{+9Ja+u5tsga>z; zRfowuMc1yidd+qLq?q1Td64I$Y<X$v-{o0WF%}QhzcQJC^C~cw<~=o5ykfOjWmS<6 mSyvqwtWiQqG)S`iP{4F8S!G!i4+YH@sZ}0941C~bW#KPuW{ao* literal 0 HcmV?d00001 diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 1170945f..e0474e25 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -18,12 +18,6 @@ module Api create_action respond_with_resource end - - def my_tokens - authorize resource_class - instantiate_collection - respond_with_collection - end end end end diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh index be85012c..28931b2f 100755 --- a/bin/build-apidocs.sh +++ b/bin/build-apidocs.sh @@ -2,8 +2,16 @@ # Note: you need to run `npm install` before using this script or raml2html won't be installed +OLD_DIR=$(pwd) +cd $(dirname $0)/.. + if [[ ! -x ./node_modules/.bin/raml2html ]]; then npm install fi -./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html +./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html -t doc/api/templates/template.nunjucks +if [[ -x $(which open) ]]; then + open public/api/index.html +fi + +cd $OLD_DIR diff --git a/config/routes.rb b/config/routes.rb index b5078d86..79db8599 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,9 +70,7 @@ Metamaps::Application.routes.draw do delete :stars, to: 'stars#destroy', on: :member end resources :synapses, only: [:index, :create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection - end + resources :tokens, only: [:index, :create, :destroy] resources :topics, only: [:index, :create, :show, :update, :destroy] resources :users, only: [:index, :show] do get :current, on: :collection diff --git a/doc/api/api.raml b/doc/api/api.raml index 4473a6dd..8703aae9 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -8,12 +8,12 @@ protocols: [ HTTPS ] documentation: - title: Getting Started content: !include pages/getting-started.md - - title: Endpoints - content: "" securitySchemes: + cookie: !include securitySchemes/cookie.raml + token: !include securitySchemes/token.raml oauth_2_0: !include securitySchemes/oauth_2_0.raml -securedBy: [ oauth_2_0 ] +securedBy: [ cookie, token, oauth_2_0 ] traits: pageable: !include traits/pageable.raml diff --git a/doc/api/apis/metacodes.raml b/doc/api/apis/metacodes.raml index 37cbd17a..877e1835 100644 --- a/doc/api/apis/metacodes.raml +++ b/doc/api/apis/metacodes.raml @@ -1,4 +1,5 @@ #type: collection +securedBy: [ null, cookie, token, oauth_2_0 ] get: is: [ searchable: { searchFields: "name" }, orderable, pageable ] responses: @@ -7,6 +8,7 @@ get: application/json: example: !include ../examples/metacodes.json /{id}: + securedBy: [ null, cookie, token, oauth_2_0 ] #type: item get: responses: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index ef7a8379..5d4eb191 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -1,4 +1,13 @@ #type: collection +get: + description: | + A list of the current user's tokens. + is: [ searchable: { searchFields: description }, pageable, orderable ] + responses: + 200: + body: + application/json: + example: !include ../examples/tokens.json post: body: application/json: @@ -11,14 +20,6 @@ post: body: application/json: example: !include ../examples/token.json -/my_tokens: - get: - is: [ searchable: { searchFields: description }, pageable, orderable ] - responses: - 200: - body: - application/json: - example: !include ../examples/tokens.json /{id}: #type: item delete: diff --git a/doc/api/apis/users.raml b/doc/api/apis/users.raml index 7f421059..1d37bc0d 100644 --- a/doc/api/apis/users.raml +++ b/doc/api/apis/users.raml @@ -1,4 +1,5 @@ #type: collection +securedBy: [ null, cookie, token, oauth_2_0 ] get: is: [ searchable: { searchFields: "name" }, orderable, pageable ] responses: @@ -6,6 +7,15 @@ get: body: application/json: example: !include ../examples/users.json +/{id}: + #type: item + securedBy: [ null, cookie, token, oauth_2_0 ] + get: + responses: + 200: + body: + application/json: + example: !include ../examples/user.json /current: #type: item get: @@ -14,11 +24,3 @@ get: body: application/json: example: !include ../examples/current_user.json -/{id}: - #type: item - get: - responses: - 200: - body: - application/json: - example: !include ../examples/user.json diff --git a/doc/api/pages/cookie_tutorial.md b/doc/api/pages/cookie_tutorial.md new file mode 100644 index 00000000..9481b919 --- /dev/null +++ b/doc/api/pages/cookie_tutorial.md @@ -0,0 +1,3 @@ +One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. + +To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md index c620e888..429e6971 100644 --- a/doc/api/pages/getting-started.md +++ b/doc/api/pages/getting-started.md @@ -1,86 +1,2 @@ -[Skip ahead to the endpoints.](#endpoints) +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. Check the security tab of any of the endpoints above for instructions on logging in. -There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. - -### 1. Cookie-based authentication - -One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. - -To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. - -### 2. Token-based authentication - -If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. - -To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. - -```javascript -fetch('/api/v2/tokens', { - method: 'GET', - credentials: 'same-origin' // needed to use the cookie-based auth -}).then(response => { - return response.json() -}).then(console.log).catch(console.error) -``` - -If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: - -```javascript -fetch('/api/v2/tokens', { - method: 'POST', - credentials: 'same-origin' -}).then(response => { - return response.json() -}).then(payload => { - console.log(payload) -}).catch(console.error) -``` - -`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. - -### 3. OAuth 2 Authentication - -We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) - -The first step is to register your client app. - -#### Registering the client - -Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). - -#### Requesting authorization - -To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: - -``` -http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code -``` - -Once you are there, you should sign in and click on `Authorize`. -You will then see a response that contains your "authorization code", which you need to exchange for an access token. - -#### Requesting the access token - -To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` - -```javascript -fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { - method: 'POST', - credentials: 'same-origin' -}).then(response => { - return response.json() -}).then(console.log).catch(console.error) -``` - -The response will look like - -```json -{ - "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", - "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" -} -``` - -You can now make requests to the API with the access token returned. diff --git a/doc/api/pages/oauth_2_0_tutorial.md b/doc/api/pages/oauth_2_0_tutorial.md new file mode 100644 index 00000000..e419a621 --- /dev/null +++ b/doc/api/pages/oauth_2_0_tutorial.md @@ -0,0 +1,41 @@ +We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) + +The first step is to register your client app. + +#### Registering the client + +Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). + +#### Requesting authorization + +To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: + +``` +http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code +``` + +Once you are there, you should sign in and click on `Authorize`. +You will then see a response that contains your "authorization code", which you need to exchange for an access token. + +#### Requesting the access token + +To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` + +```javascript +fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) + +# The response will be like +{ + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" +} +``` + +You can now make requests to the API with the access token returned. diff --git a/doc/api/pages/token_tutorial.md b/doc/api/pages/token_tutorial.md new file mode 100644 index 00000000..3a46582a --- /dev/null +++ b/doc/api/pages/token_tutorial.md @@ -0,0 +1,25 @@ +If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. + +To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. + +``` +fetch('/api/v2/tokens', { + method: 'GET', + credentials: 'same-origin' // needed to use the cookie-based auth +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: + +``` +fetch('/api/v2/tokens', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. diff --git a/doc/api/securitySchemes/cookie.raml b/doc/api/securitySchemes/cookie.raml new file mode 100644 index 00000000..fb1041b1 --- /dev/null +++ b/doc/api/securitySchemes/cookie.raml @@ -0,0 +1,3 @@ +description: !include ../pages/cookie_tutorial.md +type: x-cookie +displayName: Secured by cookie-based authentication diff --git a/doc/api/securitySchemes/oauth_2_0.raml b/doc/api/securitySchemes/oauth_2_0.raml index b271e03a..3a84e293 100644 --- a/doc/api/securitySchemes/oauth_2_0.raml +++ b/doc/api/securitySchemes/oauth_2_0.raml @@ -1,5 +1,4 @@ -description: | - OAuth 2.0 implementation +description: !include ../pages/oauth_2_0_tutorial.md type: OAuth 2.0 settings: authorizationUri: https://metamaps.cc/api/v2/oauth/authorize diff --git a/doc/api/securitySchemes/token.raml b/doc/api/securitySchemes/token.raml new file mode 100644 index 00000000..f83e1177 --- /dev/null +++ b/doc/api/securitySchemes/token.raml @@ -0,0 +1,3 @@ +description: !include ../pages/token_tutorial.md +type: x-token +displayName: Secured by token-based authentication diff --git a/doc/api/templates/item.nunjucks b/doc/api/templates/item.nunjucks new file mode 100644 index 00000000..044c24d9 --- /dev/null +++ b/doc/api/templates/item.nunjucks @@ -0,0 +1,61 @@ +<li> + {% if item.displayName %} + <strong>{{ item.displayName }}</strong>: + {% else %} + <strong>{{ item.key }}</strong>: + {% endif %} + + {% if not item.structuredValue %} + <em> + {%- if item.required -%}required {% endif -%} + ( + {%- if item.enum -%} + {%- if item.enum.length === 1 -%} + {{ item.enum.join(', ') }} + {%- else -%} + one of {{ item.enum.join(', ') }} + {%- endif -%} + {%- else -%} + {{ item.type }} + {%- endif -%} + + {%- if item.default or item.default == 0 or item.default == false %} - default: {{ item.default }}{%- endif -%} + {%- if item.repeat %} - repeat: {{ item.repeat }}{%- endif -%} + {%- if item.type == 'string' -%} + {%- if item.minLength or item.minLength == 0 %} - minLength: {{ item.minLength }}{%- endif -%} + {%- if item.maxLength or item.maxLength == 0 %} - maxLength: {{ item.maxLength }}{%- endif -%} + {%- else -%} + {%- if item.minimum or item.minimum == 0 %} - minimum: {{ item.minimum }}{%- endif -%} + {%- if item.maximum or item.maximum == 0 %} - maximum: {{ item.maximum }}{%- endif -%} + {%- endif -%} + {%- if item.pattern %} - pattern: {{ item.pattern }}{%- endif -%} + ) + </em> + {% endif %} + +{% markdown %} +{{ item.description }} +{% endmarkdown %} + +{# + {% if item.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ item.type | escape }}</code></pre> + {% endif %} +#} + + {% if item.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in item.examples %} + {% if item.type == 'string' %} + <pre>{{ example | escape }}</pre> + {% else %} + <pre><code>{{ example | escape }}</code></pre> + {% endif %} + {% endfor %} + {% endif %} + + {% if item.structuredValue %} + <pre><code>{{ item.structuredValue | dump }}</code></pre> + {% endif %} +</li> diff --git a/doc/api/templates/resource.nunjucks b/doc/api/templates/resource.nunjucks new file mode 100644 index 00000000..6bdaf2a6 --- /dev/null +++ b/doc/api/templates/resource.nunjucks @@ -0,0 +1,314 @@ +{% if (resource.methods or (resource.description and resource.parentUrl)) %} + <div class="panel panel-white"> + <div class="panel-heading"> + <h4 class="panel-title"> + <a class="collapsed" data-toggle="collapse" href="#panel_{{ resource.uniqueId }}"> + <span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }} + </a> + + <span class="methods"> + {% for method in resource.methods %} + <a href="#{{ resource.uniqueId }}_{{ method.method }}"><!-- modal shown by hashchange event --> + <span class="badge badge_{{ method.method }}">{{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + </a> + {% endfor %} + </span> + </h4> + </div> + + <div id="panel_{{ resource.uniqueId }}" class="panel-collapse collapse"> + <div class="panel-body"> + {% if resource.parentUrl %} + {% if resource.description %} + <div class="resource-description"> +{% markdown %} +{{ resource.description }} +{% endmarkdown %} + </div> + {% endif %} + {% endif %} + + <div class="list-group"> + {% for method in resource.methods %} + <div onclick="window.location.href = '#{{ resource.uniqueId }}_{{ method.method }}'" class="list-group-item"> + <span class="badge badge_{{ method.method }}"> + {{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + <div class="method_description"> +{% markdown %} +{{ method.description}} +{% endmarkdown %} + </div> + <div class="clearfix"></div> + </div> + {% endfor %} + </div> + </div> + </div> + + {% for method in resource.methods %} + <div class="modal fade" tabindex="0" id="{{ resource.uniqueId }}_{{ method.method }}"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title" id="myModalLabel"> + <span class="badge badge_{{ method.method }}"> + {{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + <span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }} + </h4> + </div> + + <div class="modal-body"> + {% if method.description %} + <div class="alert alert-info"> +{% markdown %} +{{ method.description}} +{% endmarkdown %} + </div> + {% endif %} + + <!-- Nav tabs --> + <ul class="nav nav-tabs"> + {% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %} + <li class="active"> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_request" data-toggle="tab">Request</a> + </li> + {% endif %} + + {% if method.responses %} + <li{% + if not method.allUriParameters.length and not method.queryParameters + and not method.queryString + and not method.headers and not method.body + %} class="active"{% + endif + %}> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_response" data-toggle="tab">Response</a> + </li> + {% endif %} + + {% if method.securedBy.length %} + <li> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_securedby" data-toggle="tab">Security</a> + </li> + {% endif %} + </ul> + + <!-- Tab panes --> + <div class="tab-content"> + {% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %} + <div class="tab-pane active" id="{{ resource.uniqueId }}_{{ method.method }}_request"> + {% if resource.allUriParameters.length %} + <h3>URI Parameters</h3> + <ul> + {% for item in resource.allUriParameters %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.annotations.length %} + <h3>Annotations</h3> + <ul> + {% for item in method.annotations %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in method.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.queryString and method.queryString.properties.length %} + <h3>Query String</h3> + <ul> + {% for item in method.queryString.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.queryParameters.length %} + <h3>Query Parameters</h3> + <ul> + {% for item in method.queryParameters %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.body %} + <h3>Body</h3> + {% for b in method.body %} + <p><strong>Type: {{ b.key }}</strong></p> + +{# + {% if b.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ b.type | escape }}</code></pre> + {% endif %} +#} + + {% if b.properties.length %} + <strong>Properties</strong> + <ul> + {% for item in b.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if b.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in b.examples %} + <pre><code>{{ example | escape }}</code></pre> + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + </div> + {% endif %} + + {% if method.responses %} + <div class="tab-pane{% + if not method.allUriParameters.length and not method.queryParameters.length + and not method.queryString + and not method.headers.length and not method.body.length + %} active{% + endif + %}" id="{{ resource.uniqueId }}_{{ method.method }}_response"> + {% for response in method.responses %} + <h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2> +{% markdown %} +{{ response.description}} +{% endmarkdown %} + + {% if response.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in response.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if response.body.length %} + <h3>Body</h3> + {% for b in response.body %} + <p><strong>Type: {{ b.key }}</strong></p> + +{# + {% if b.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ b.type | escape }}</code></pre> + {% endif %} +#} + + {% if b.properties.length %} + <strong>Properties</strong> + <ul> + {% for item in b.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if b.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in b.examples %} + <pre><code>{{ example | escape }}</code></pre> + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + </div> + {% endif %} + + {% if method.securedBy.length %} + <div class="tab-pane" id="{{ resource.uniqueId }}_{{ method.method }}_securedby"> + {% for securedBy in method.securedBy %} + {% if securedBy == null %} + <div class="alert alert-info"> + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + This route can be accessed anonymously.</h1> + </div> + {% else %} + {% set securityScheme = securitySchemes[securedBy] %} + <div class="alert alert-warning"> + {% set securedByScopes = renderSecuredBy(securedBy) %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> Secured by {{ securedByScopes }} + {% set securityScheme = securitySchemes[securedBy] %} + {% if securityScheme.description %} +{% markdown %} +{{ securityScheme.description }} +{% endmarkdown %} + {% endif %} + + {% if securityScheme.describedBy.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in securityScheme.describedBy.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% for response in securityScheme.describedBy.responses.length %} + <h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2> +{% markdown %} +{{ response.description}} +{% endmarkdown %} + {% if response.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in response.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + {% endfor %} + </div> + {% endif %} + {% endfor %} + </div> + {% endif %} + </div> + </div> + </div> + </div> + </div> + {% endfor %} + </div> +{% endif %} + +{% for resource in resource.resources %} + {% include "./resource.nunjucks" %} +{% endfor %} diff --git a/doc/api/templates/template.nunjucks b/doc/api/templates/template.nunjucks new file mode 100644 index 00000000..6911f44e --- /dev/null +++ b/doc/api/templates/template.nunjucks @@ -0,0 +1,232 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>{{ title }} API documentation + + + + + + + + + + + + + + + + +
+
+
+ + + {% for resource in resources %} +
+
+

{% if resource.displayName %}{{ resource.displayName}}{% else %}{{ resource.relativeUri }}{% endif %}

+
+ +
+ {% if resource.description %} +
+{% markdown %} +{{ resource.description }} +{% endmarkdown %} +
+ {% endif %} + +
+ {% include "./resource.nunjucks" %} +
+
+
+ {% endfor %} + + {% for chapter in documentation %} +

{{ chapter.title }}

+{% markdown %} +{{ chapter.content }} +{% endmarkdown %} + {% endfor %} +
+ + +
+
+ + From b2a4acc99d7b5524e88bae6731946754546bdf40 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 14:23:55 +0800 Subject: [PATCH 200/378] make default category explicit in import.js --- frontend/src/Metamaps/Import.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 868843bf..6f25d21b 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -333,8 +333,8 @@ const Import = { } // if var synapse = new Metamaps.Backbone.Synapse({ - desc: desc || "", - category: category, + desc: desc || '' + category: category || 'from-to', permission: permission, topic1_id: topic1.id, topic2_id: topic2.id From 6e03132f1b16d6b0109d7f19c65b916778f4015f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 14:51:58 +0800 Subject: [PATCH 201/378] fix spec --- spec/api/v2/tokens_api_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb index cd424ba0..c9ec77ed 100644 --- a/spec/api/v2/tokens_api_spec.rb +++ b/spec/api/v2/tokens_api_spec.rb @@ -6,13 +6,14 @@ RSpec.describe 'tokens API', type: :request do let(:auth_token) { create(:token, user: user).token } let(:token) { create(:token, user: user) } - it 'GET /api/v2/tokens/my_tokens' do + it 'GET /api/v2/tokens' do create_list(:token, 5, user: user) - get '/api/v2/tokens/my_tokens', params: { access_token: auth_token } + get '/api/v2/tokens', params: { access_token: auth_token } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:tokens) - expect(Token.count).to eq 6 # 5 + the extra auth token; let(:token) wasn't used + # 5 + the auth_token; let(:token) wasn't used + expect(Token.count).to eq 6 end it 'POST /api/v2/tokens' do From 0e7e649f56975666637bb22ce3e90655dd86aec1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 12 Oct 2016 22:01:42 +0800 Subject: [PATCH 202/378] don't need coffeescript, tunemygc fails on Windows --- Gemfile | 1 - Gemfile.lock | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 7f34c12e..8fae0328 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,6 @@ gem 'snorlax' gem 'uservoice-ruby' # asset stuff -gem 'coffee-rails' gem 'jquery-rails' gem 'jquery-ui-rails' gem 'sass-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 350585ae..5af227db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,13 +70,6 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) - coffee-rails (4.2.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.2.x) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.10.0) concurrent-ruby (1.0.2) debug_inspector (0.0.2) delayed_job (4.1.2) @@ -282,7 +275,6 @@ DEPENDENCIES better_errors binding_of_caller brakeman - coffee-rails delayed_job delayed_job_active_record devise @@ -321,4 +313,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.13.2 + 1.13.3 From 6f3c74b7f1866ff1eb97f5400afe39e89847c7e3 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:21:27 +0800 Subject: [PATCH 203/378] token policy fix --- app/policies/token_policy.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb index cd9a5ab7..e96e4db8 100644 --- a/app/policies/token_policy.rb +++ b/app/policies/token_policy.rb @@ -10,11 +10,11 @@ class TokenPolicy < ApplicationPolicy end end - def create? + def index? user.present? end - def my_tokens? + def create? user.present? end From fc2849824f192ba75f59604ccdcfcafc3b9ded33 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 16:48:46 +0800 Subject: [PATCH 204/378] fix js syntax error --- frontend/src/Metamaps/Import.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 6f25d21b..f1539274 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -296,8 +296,8 @@ const Import = { metacode_id: metacode.id, permission: topic_permission, defer_to_map_id: defer_to_map_id, - desc: desc || "", - link: link || "", + desc: desc || '', + link: link || '', calculated_permission: Active.Map.get('permission') }) Metamaps.Topics.add(topic) @@ -333,7 +333,7 @@ const Import = { } // if var synapse = new Metamaps.Backbone.Synapse({ - desc: desc || '' + desc: desc || '', category: category || 'from-to', permission: permission, topic1_id: topic1.id, From 407ac1f29ce56b08094b090e0527d826dc5716c0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:31:45 +0800 Subject: [PATCH 205/378] more simplecov groups --- .simplecov | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.simplecov b/.simplecov index b81ebfeb..efee8860 100644 --- a/.simplecov +++ b/.simplecov @@ -1,3 +1,7 @@ if ENV['COVERAGE'] == 'on' - SimpleCov.start 'rails' + SimpleCov.start 'rails' do + add_group 'Policies', 'app/policies' + add_group 'Services', 'app/services' + add_group 'Serializers', 'app/serializers' + end end From 26a8cddd1494969a784c88c93908267132361bf7 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:39:13 +0800 Subject: [PATCH 206/378] mailer spec --- config/environments/test.rb | 1 + spec/mailers/map_mailer_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 spec/mailers/map_mailer_spec.rb diff --git a/config/environments/test.rb b/config/environments/test.rb index 5f0b1ee2..d85899fd 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,6 +30,7 @@ Metamaps::Application.configure do # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'localhost:3000' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb new file mode 100644 index 00000000..d4da119e --- /dev/null +++ b/spec/mailers/map_mailer_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe MapMailer, type: :mailer do + let(:map) { create(:map) } + let(:inviter) { create(:user) } + let(:invitee) { create(:user) } + describe 'invite_to_edit_email' do + let(:mail) { described_class.invite_to_edit_email(map, inviter, invitee) } + + it { expect(mail.from).to eq ['team@metamaps.cc'] } + it { expect(mail.to).to eq [invitee.email] } + it { expect(mail.subject).to match map.name } + it { expect(mail.body.encoded).to match inviter.name } + it { expect(mail.body.encoded).to match map.name } + it { expect(mail.body.encoded).to match map_url(map) } + end +end From 8180a8cc7149b9a57644a6b0748eed330c2d5c84 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 14 Oct 2016 14:29:16 +0800 Subject: [PATCH 207/378] fix file upload box --- frontend/src/components/ImportDialogBox.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index bfb60235..43825421 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -15,12 +15,13 @@ class ImportDialogBox extends Component { } handleFile = (files, e) => { - // for some reason it uploads twice, so we need this debouncer - // eslint-disable-next-line no-return-assign - this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) - if (!this.debouncer) { - this.props.onFileAdded(files[0]) - } + // // for some reason it uploads twice, so we need this debouncer + // // eslint-disable-next-line no-return-assign + // this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) + // if (!this.debouncer) { + // this.props.onFileAdded(files[0]) + // } + this.props.onFileAdded(files[0]) } toggleShowInstructions = e => { From 4602ded8a414e6db1adbd857e19ceb4211020ffc Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 16 Oct 2016 20:22:00 -0400 Subject: [PATCH 208/378] access requests (#762) * start on access requests * set up access requests further * set default values for approved and answered --- app/assets/images/view-only.png | Bin 0 -> 421 bytes .../stylesheets/request_access.scss.erb | 98 ++++++++++++++++++ app/controllers/access_controller.rb | 98 ++++++++++++++++++ app/controllers/application_controller.rb | 17 +-- app/controllers/maps_controller.rb | 24 +---- .../users/registrations_controller.rb | 11 +- app/mailers/map_mailer.rb | 9 +- app/models/access_request.rb | 18 ++++ app/models/map.rb | 5 +- app/policies/map_policy.rb | 28 ++++- app/views/layouts/_upperelements.html.erb | 17 +++ .../map_mailer/access_request_email.html.erb | 23 ++++ .../map_mailer/access_request_email.text.erb | 10 ++ .../map_mailer/invite_to_edit_email.html.erb | 2 +- .../map_mailer/invite_to_edit_email.text.erb | 2 +- app/views/maps/request_access.html.erb | 37 +++++++ config/environments/development.rb | 2 +- config/routes.rb | 40 ++++--- .../20161013162214_create_access_requests.rb | 12 +++ db/schema.rb | 15 ++- frontend/src/Metamaps/Map/index.js | 24 +++++ spec/mailers/previews/map_mailer_preview.rb | 5 + spec/models/access_request_spec.rb | 5 + 23 files changed, 448 insertions(+), 54 deletions(-) create mode 100644 app/assets/images/view-only.png create mode 100644 app/assets/stylesheets/request_access.scss.erb create mode 100644 app/controllers/access_controller.rb create mode 100644 app/models/access_request.rb create mode 100644 app/views/map_mailer/access_request_email.html.erb create mode 100644 app/views/map_mailer/access_request_email.text.erb create mode 100644 app/views/maps/request_access.html.erb create mode 100644 db/migrate/20161013162214_create_access_requests.rb create mode 100644 spec/models/access_request_spec.rb diff --git a/app/assets/images/view-only.png b/app/assets/images/view-only.png new file mode 100644 index 0000000000000000000000000000000000000000..a4cc262f325e05c2d81f4ff16bd7d8b47a1d50e7 GIT binary patch literal 421 zcmV;W0b2fvP)yO$#(=1Q>kd7oxxfWY!yv!(zxpkj&fWs zW`malWe0}2-P_&w_U*p6nT&^rhsR%pwKfY*fYac-lIC*Uc*FTg7*+w8~p z*0^?j0N%jJE7aI{u>XbQz*-^^!6M5rdJOG??=tcR*1;lJkk-|2G>+q&VEH#^9Domz zUDC+D P00000NkvXXu0mjf2`9d; literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/request_access.scss.erb b/app/assets/stylesheets/request_access.scss.erb new file mode 100644 index 00000000..19ae2792 --- /dev/null +++ b/app/assets/stylesheets/request_access.scss.erb @@ -0,0 +1,98 @@ +.viewOnly { + float: left; + margin-left: 16px; + display: none; + height: 32px; + border: 1px solid #BDBDBD; + border-radius: 2px; + background-color: #424242; + color: #FFF; + font-size: 14px; + line-height: 32px; + + &.isViewOnly { + display: block; + } + + .eyeball { + background: url('<%= asset_path('view-only.png') %>') no-repeat 4px 0; + padding-left: 40px; + border-right: #747474; + padding-right: 10px; + display: inline-block; + } + + .requestNotice { + display: none; + padding: 0 8px; + } + + .requestAccess { + background-color: #a354cd; + &:hover { + background-color: #9150bc; + } + cursor: pointer; + } + + .requestPending { + background-color: #4fc059; + } + + .requestNotAccepted { + background-color: #c04f4f; + } + + &.sendRequest .requestAccess { + display: inline-block; + } + &.sentRequest .requestPending { + display: inline-block; + } + &.requestDenied .requestNotAccepted { + display: inline-block; + } +} + +.request_access { + position: absolute; + width: 90%; + margin: 0 5%; + + .monkey { + width: 250px; + height: 250px; + border: 6px solid #424242; + border-radius: 125px; + background: url(https://s3.amazonaws.com/metamaps-assets/site/monkeyselfie.jpg) no-repeat; + background-position: 50% 20%; + background-size: 100%; + margin: 80px auto 20px auto; + } + + .explainer_text { + padding: 0 20% 0 20%; + font-size: 24px; + line-height: 30px; + margin-bottom: 20px; + text-align: center; + } + + .make_request { + background-color: #a354cd; + display: block; + width: 220px; + height: 14px; + padding: 16px 0; + margin-bottom: 16px; + text-align: center; + border-radius: 2px; + font-size: 14px; + box-shadow: 0px 1px 1.5px rgba(0,0,0,0.12), 0 1px 1px rgba(0,0,0,0.24); + margin: 0 auto 20px auto; + text-decoration: none; + color: #FFFFFF !important; + cursor: pointer; + } + +} diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb new file mode 100644 index 00000000..302e9385 --- /dev/null +++ b/app/controllers/access_controller.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +class AccessController < ApplicationController + before_action :require_user, only: [:access, :access_request, :approve_access, :approve_access_post, + :deny_access, :deny_access_post, :request_access] + before_action :set_map, only: [:access, :access_request, :approve_access, :approve_access_post, + :deny_access, :deny_access_post, :request_access] + after_action :verify_authorized + + + # GET maps/:id/request_access + def request_access + @map = nil + respond_to do |format| + format.html do + render 'maps/request_access' + end + end + end + + # POST maps/:id/access_request + def access_request + request = AccessRequest.create(user: current_user, map: @map) + # what about push notification to map owner? + MapMailer.access_request_email(request, @map).deliver_later + + respond_to do |format| + format.json do + head :ok + end + end + end + + # POST maps/:id/access + def access + user_ids = params[:access] || [] + + @map.add_new_collaborators(user_ids).each do |user_id| + # add_new_collaborators returns array of added users, + # who we then send an email to + MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later + end + @map.remove_old_collaborators(user_ids) + + respond_to do |format| + format.json do + head :ok + end + end + end + + # GET maps/:id/approve_access/:request_id + def approve_access + request = AccessRequest.find(params[:request_id]) + request.approve() + respond_to do |format| + format.html { redirect_to map_path(@map), notice: 'Request was approved' } + end + end + + # GET maps/:id/deny_access/:request_id + def deny_access + request = AccessRequest.find(params[:request_id]) + request.deny() + respond_to do |format| + format.html { redirect_to map_path(@map), notice: 'Request was turned down' } + end + end + + # POST maps/:id/approve_access/:request_id + def approve_access_post + request = AccessRequest.find(params[:request_id]) + request.approve() + respond_to do |format| + format.json do + head :ok + end + end + end + + # POST maps/:id/deny_access/:request_id + def deny_access_post + request = AccessRequest.find(params[:request_id]) + request.deny() + respond_to do |format| + format.json do + head :ok + end + end + end + + private + + def set_map + @map = Map.find(params[:id]) + authorize @map + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eddf510d..6138fa31 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,21 +22,26 @@ class ApplicationController < ActionController::Base helper_method :admin? def after_sign_in_path_for(resource) - sign_in_url = url_for(action: 'new', controller: 'sessions', only_path: false) + sign_in_url = new_user_session_url + sign_up_url = new_user_registration_url + stored = stored_location_for(User) - if request.referer == sign_in_url + if stored + stored + elsif request.referer.include?(sign_in_url) || request.referer.include?(sign_up_url) super - elsif params[:uv_login] == '1' - 'http://support.metamaps.cc/login_success?sso=' + current_sso_token else - stored_location_for(resource) || request.referer || root_path + request.referer || root_path end end def handle_unauthorized - if authenticated? + if authenticated? and params[:controller] == 'maps' and params[:action] == 'show' + redirect_to request_access_map_path(params[:id]) + elsif authenticated? redirect_to root_path, notice: "You don't have permission to see that page." else + store_location_for(resource, request.fullpath) redirect_to new_user_session_path, notice: 'Try signing in to do that.' end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index cdbbd900..7044d424 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events] - before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export] + before_action :require_user, only: [:create, :update, :destroy, :events] + before_action :set_map, only: [:show, :update, :destroy, :contains, :events, :export] after_action :verify_authorized # GET maps/:id @@ -16,6 +15,7 @@ class MapsController < ApplicationController @allmappings = policy_scope(@map.mappings) @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars + @allrequests = @map.access_requests end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -80,24 +80,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/access - def access - user_ids = params[:access] || [] - - @map.add_new_collaborators(user_ids).each do |user_id| - # add_new_collaborators returns array of added users, - # who we then send an email to - MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later - end - @map.remove_old_collaborators(user_ids) - - respond_to do |format| - format.json do - render json: { message: 'Successfully altered edit permissions' } - end - end - end - # GET maps/:id/contains def contains respond_to do |format| diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 21cd9666..e472152e 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -2,19 +2,22 @@ class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :configure_account_update_params, only: [:update] + after_action :store_location, only: [:new] protected - def after_sign_up_path_for(resource) - signed_in_root_path(resource) - end - def after_update_path_for(resource) signed_in_root_path(resource) end private + def store_location + if params[:redirect_to] + store_location_for(User, params[:redirect_to]) + end + end + def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode]) end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index e70d0b82..f6865ecd 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -2,10 +2,17 @@ class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' + def access_request_email(request, map) + @request = request + @map = map + subject = @map.name + ' - request to edit' + mail(to: @map.user.email, subject: subject) + end + def invite_to_edit_email(map, inviter, invitee) @inviter = inviter @map = map - subject = @map.name + ' - Invitation to edit' + subject = @map.name + ' - invitation to edit' mail(to: invitee.email, subject: subject) end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb new file mode 100644 index 00000000..185a04f0 --- /dev/null +++ b/app/models/access_request.rb @@ -0,0 +1,18 @@ +class AccessRequest < ApplicationRecord + belongs_to :user + belongs_to :map + + def approve + self.approved = true + self.answered = true + self.save + UserMap.create(user: self.user, map: self.map) + MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later + end + + def deny + self.approved = false + self.answered = true + self.save + end +end diff --git a/app/models/map.rb b/app/models/map.rb index a8e9c866..cdd6b333 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -9,6 +9,7 @@ class Map < ApplicationRecord has_many :messages, as: :resource, dependent: :destroy has_many :stars + has_many :access_requests, dependent: :destroy has_many :user_maps, dependent: :destroy has_many :collaborators, through: :user_maps, source: :user @@ -102,7 +103,8 @@ class Map < ApplicationRecord mappers: contributors, collaborators: editors, messages: messages.sort_by(&:created_at), - stars: stars + stars: stars, + requests: access_requests } end @@ -122,6 +124,7 @@ class Map < ApplicationRecord removed = current_collaborators.map(&:id).map do |old_user_id| next nil if user_ids.include?(old_user_id) user_maps.where(user_id: old_user_id).find_each(&:destroy) + access_requests.where(user_id: old_user_id).find_each(&:destroy) old_user_id end removed.compact diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 9999a055..f670f59e 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -37,10 +37,36 @@ class MapPolicy < ApplicationPolicy end def access? - # note that this is to edit who can access the map + # this is for the map creator to bulk change who can access the map user.present? && record.user == user end + def request_access? + # this is to access the page where you can request access to a map + user.present? + end + + def access_request? + # this is to actually request access + user.present? + end + + def approve_access? + record.user == user + end + + def deny_access? + approve_access? + end + + def approve_access_post? + approve_access? + end + + def deny_access_post? + approve_access? + end + def contains? show? end diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index c2344643..1d26ced9 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -14,6 +14,23 @@
+ + <% request = current_user && @map && @allrequests.find{|a| a.user == current_user} + className = (@map and not policy(@map).update?) ? 'isViewOnly ' : '' + if @map + className += 'sendRequest' if not request + className += 'sentRequest' if request and not request.answered + className += 'requestDenied' if request and request.answered and not request.approved + end %> + +
+
View Only
+ <% if current_user %> +
Request Access
+
Request Pending
+
Request Not Accepted
+ <% end %> +
diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb new file mode 100644 index 00000000..0445c6da --- /dev/null +++ b/app/views/map_mailer/access_request_email.html.erb @@ -0,0 +1,23 @@ + + + + + + + +
+ <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> + +

<%= @request.user.name %> is requesting access to collaboratively edit the following metamap:

+ +

<%= @map.name %>

+ +

<%= link_to "Grant", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

<%= link_to "Deny", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

+ + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> + +

Make sense with Metamaps

+
+ + diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb new file mode 100644 index 00000000..0c5b07dd --- /dev/null +++ b/app/views/map_mailer/access_request_email.text.erb @@ -0,0 +1,10 @@ +<%= @request.user.name %> has requested to collaboratively edit the following metamap: + +<%= @map.name %> [<%= map_url(@map) %>] + +Approve Request [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>] +Deny Request [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] + +Make sense with Metamaps + + diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index 1a8b80c2..73067c48 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -8,7 +8,7 @@
<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -

<%= @inviter.name %> has invited you to collaboratively edit the following metamap:

+

<%= @inviter.name %> has invited you to collaboratively edit the following map:

<%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

<% if @map.desc %>

<%= @map.desc %>

diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 62bd2c90..80eecfed 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -1,4 +1,4 @@ -<%= @inviter.name %> has invited you to collaboratively edit the following metamap: +<%= @inviter.name %> has invited you to collaboratively edit the following map: <%= @map.name %> [<%= map_url(@map) %>] diff --git a/app/views/maps/request_access.html.erb b/app/views/maps/request_access.html.erb new file mode 100644 index 00000000..cf8aadb4 --- /dev/null +++ b/app/views/maps/request_access.html.erb @@ -0,0 +1,37 @@ +<%# +# @file +# Code to request access to a map +# /maps/:id/request_access +#%> + +<% content_for :title, 'Request Access | Metamaps' %> +<% content_for :mobile_title, 'Request Access' %> + +
+
+
+
+ Hmmm. This map is private, but you can request to edit it from the map creator. +
+
REQUEST ACCESS
+
+
+ + diff --git a/config/environments/development.rb b/config/environments/development.rb index 5449e5e8..38741a18 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -32,7 +32,7 @@ Rails.application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log - config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' + config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews" # Expands the lines which load the assets config.assets.debug = false diff --git a/config/routes.rb b/config/routes.rb index 79db8599..e20f600f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,9 +19,17 @@ Metamaps::Application.routes.draw do get :export post 'events/:event', action: :events get :contains - post :access, default: { format: :json } - post :star, to: 'stars#create', defaults: { format: :json } - post :unstar, to: 'stars#destroy', defaults: { format: :json } + + get :request_access, to: 'access#request_access' + get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access + get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access + post :access_request, to: 'access#access_request', default: { format: :json } + post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json } + post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json } + post :access, to: 'access#access', default: { format: :json } + + post :star, to: 'stars#create', default: { format: :json } + post :unstar, to: 'stars#destroy', default: { format: :json } end end @@ -54,6 +62,19 @@ Metamaps::Application.routes.draw do end end + devise_for :users, skip: :sessions, controllers: { + registrations: 'users/registrations', + passwords: 'users/passwords', + sessions: 'devise/sessions' + } + + devise_scope :user do + get 'login' => 'devise/sessions#new', :as => :new_user_session + post 'login' => 'devise/sessions#create', :as => :user_session + get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session + get 'join' => 'devise/registrations#new', :as => :new_user_registration_path + end + resources :users, except: [:index, :destroy] do member do get :details @@ -84,19 +105,6 @@ Metamaps::Application.routes.draw do match '*path', to: 'v2/restful#catch_404', via: :all end - devise_for :users, skip: :sessions, controllers: { - registrations: 'users/registrations', - passwords: 'users/passwords', - sessions: 'devise/sessions' - } - - devise_scope :user do - get 'login' => 'devise/sessions#new', :as => :new_user_session - post 'login' => 'devise/sessions#create', :as => :user_session - get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session - get 'join' => 'devise/registrations#new', :as => :new_user_registration_path - end - namespace :hacks do get 'load_url_title' end diff --git a/db/migrate/20161013162214_create_access_requests.rb b/db/migrate/20161013162214_create_access_requests.rb new file mode 100644 index 00000000..248ad005 --- /dev/null +++ b/db/migrate/20161013162214_create_access_requests.rb @@ -0,0 +1,12 @@ +class CreateAccessRequests < ActiveRecord::Migration[5.0] + def change + create_table :access_requests do |t| + t.references :user, foreign_key: true + t.boolean :approved, default: false + t.boolean :answered, default: false + t.references :map, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0bfa7f1a..9807c3e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160928022635) do +ActiveRecord::Schema.define(version: 20161013162214) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "access_requests", force: :cascade do |t| + t.integer "user_id" + t.boolean "approved" + t.boolean "answered" + t.integer "map_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["map_id"], name: "index_access_requests_on_map_id", using: :btree + t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -268,5 +279,7 @@ ActiveRecord::Schema.define(version: 20160928022635) do t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree end + add_foreign_key "access_requests", "maps" + add_foreign_key "access_requests", "users" add_foreign_key "tokens", "users" end diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 48be9def..e5f50633 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -60,8 +60,28 @@ const Map = { InfoBox.init() CheatSheet.init() + $('.viewOnly .requestAccess').click(self.requestAccess) + $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) }, + requestAccess: function () { + $('.viewOnly').removeClass('sendRequest').addClass('sentRequest') + const mapId = Active.Map.id + $.post({ + url: `/maps/${mapId}/access_request` + }) + GlobalUI.notifyUser('Map creator will be notified of your request') + }, + setAccessRequest: function (requests, activeMapper) { + let className = 'isViewOnly ' + if (activeMapper) { + const request = _.find(requests, r => r.user_id === activeMapper.id) + if (!request) className += 'sendRequest' + else if (request && !request.answered) className += 'sentRequest' + else if (request && request.answered && !request.approved) className += 'requestDenied' + } + $('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className) + }, launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { @@ -84,6 +104,9 @@ const Map = { if (map.authorizeToEdit(mapper)) { $('.wrapper').addClass('canEditMap') } + else { + Map.setAccessRequest(data.requests, mapper) + } // add class to .wrapper for specifying if the map can // be collaborated on @@ -139,6 +162,7 @@ const Map = { Filter.close() InfoBox.close() Realtime.endActiveMap() + $('.viewOnly').removeClass('isViewOnly') } }, updateStar: function () { diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb index 96d07c07..17ea7671 100644 --- a/spec/mailers/previews/map_mailer_preview.rb +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -4,4 +4,9 @@ class MapMailerPreview < ActionMailer::Preview def invite_to_edit_email MapMailer.invite_to_edit_email(Map.first, User.first, User.second) end + + def access_request_email + request = AccessRequest.first + MapMailer.access_request_email(request, request.map) + end end diff --git a/spec/models/access_request_spec.rb b/spec/models/access_request_spec.rb new file mode 100644 index 00000000..4119eaa6 --- /dev/null +++ b/spec/models/access_request_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AccessRequest, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From e46aa54ba35c8f9623252b88b229bfb43ffd7e0b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 10:39:46 +0800 Subject: [PATCH 209/378] schema update --- db/schema.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 9807c3e0..850f59a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -17,11 +17,11 @@ ActiveRecord::Schema.define(version: 20161013162214) do create_table "access_requests", force: :cascade do |t| t.integer "user_id" - t.boolean "approved" - t.boolean "answered" + t.boolean "approved", default: false + t.boolean "answered", default: false t.integer "map_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["map_id"], name: "index_access_requests_on_map_id", using: :btree t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree end From c113253fc56ced7fb8693624d680e1fab4f840b0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 10:41:56 +0800 Subject: [PATCH 210/378] add scripts from default rails install --- bin/bundle | 3 +++ bin/rails | 4 ++++ bin/rake | 4 ++++ 3 files changed, 11 insertions(+) create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..66e9889e --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..07396602 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..17240489 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run From 179849b639bfdfdb4bf0f9a4bf175a1b05b99771 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 11:42:11 +0800 Subject: [PATCH 211/378] remove check-canvas-support.js --- .../javascripts/src/check-canvas-support.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 app/assets/javascripts/src/check-canvas-support.js diff --git a/app/assets/javascripts/src/check-canvas-support.js b/app/assets/javascripts/src/check-canvas-support.js deleted file mode 100644 index 90afdde1..00000000 --- a/app/assets/javascripts/src/check-canvas-support.js +++ /dev/null @@ -1,15 +0,0 @@ -// TODO document this user agent function -var labelType, useGradients, nativeTextSupport, animate -;(function () { - var ua = navigator.userAgent, - iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), - typeOfCanvas = typeof HTMLCanvasElement, - nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), - textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function') - // I'm setting this based on the fact that ExCanvas provides text support for IE - // and that as of today iPhone/iPad current text support is lame - labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML' - nativeTextSupport = labelType == 'Native' - useGradients = nativeCanvasSupport - animate = !(iStuff || !nativeCanvasSupport) -})() From 332bb2ec0898e16269817c5b6c9700d3bf8cc2a5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 16 Oct 2016 23:46:55 -0400 Subject: [PATCH 212/378] Map Card changes (#769) * map card rewrite underway * star count * css fix --- app/assets/stylesheets/base.css.erb | 152 +----------------------- app/assets/stylesheets/mapcard.scss.erb | 144 ++++++++++++++++++++++ app/models/map.rb | 12 +- frontend/src/components/Maps/MapCard.js | 61 +++++----- 4 files changed, 185 insertions(+), 184 deletions(-) create mode 100644 app/assets/stylesheets/mapcard.scss.erb diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 6cfb6b57..aa2c96db 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -17,7 +17,6 @@ } - #center-container { position:relative; height:100%; @@ -592,10 +591,10 @@ background-color: #E0E0E0; position: relative; } -.CardOnGraph .hoverForTip:hover .tip, .mapCard .hoverForTip:hover .tip, #mapContribs:hover .tip { +.CardOnGraph .hoverForTip:hover .tip, #mapContribs:hover .tip { display:block; } -.CardOnGraph .tip, .mapCard .tip { +.CardOnGraph .tip { display:none; position: absolute; background: black; @@ -952,154 +951,7 @@ font-family: 'din-regular', helvetica, sans-serif; background-position: 0 -24px; } -/* Map Cards */ -.map { - display:inline-block; - width:220px; - height:340px; - font-size: 12px; - text-align: left; - overflow: visible; - background: #e8e8e8; - border-radius:2px; - margin:16px 16px 16px 19px; - box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); -} -.map:hover { - background: #dcdcdc; -} -.map.newMap { - float: left; - position: relative; -} -.map.newMap a { - height: 340px; - display: block; - position: relative; -} -.newMap .newMapImage { - display: block; - width: 72px; - height: 72px; - background-image: url("<%= asset_data_uri('newmap_sprite.png') %>"); - background-repeat: no-repeat; - background-position: 0 0; - position: absolute; - left: 50%; - margin-left: -36px; - top: 50%; - margin-top: -36px; -} -.map:hover .newMapImage { - background-position: 0 -72px; -} -.newMap span { - font-family: 'din-regular', sans-serif; - font-size: 18px; - line-height: 22px; - text-align: center; - display: block; - padding-top: 220px; -} - -.mapCard { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - -webkit-box-orient: vertical; - -moz-box-orient: vertical; - -webkit-box-direction: normal; - -moz-box-direction: normal; - -ms-flex-direction: column; - -webkit-flex-direction: column; - flex-direction: column; - position:relative; - width:100%; - height:308px; - padding: 16px 0; - color: #424242; -} - -.mapCard .title { - word-wrap: break-word; - font-size:18px; - line-height:22px; - height: 44px; - display:block; - padding: 0 16px; - text-align: center; - -webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: none; /* OLD - Firefox 19- */ - -webkit-flex: none; /* Chrome */ - -ms-flex: none; /* IE 10 */ - flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - font-family: 'din-regular', sans-serif; -} - -.mapCard .mapScreenshot { - width: 188px; - height: 126px; - padding: 8px 16px; -} -.mapCard .mapScreenshot img { - width: 188px; - height: 126px; - border-radius: 2px; -} - -.mapCard .scroll { - display:block; - -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: 1; /* OLD - Firefox 19- */ - -webkit-flex: 1; /* Chrome */ - -ms-flex: 1; /* IE 10 */ - flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - padding:0 16px 8px; - font-family: helvetica, sans-serif; - font-style: italic; - font-size: 12px; - word-wrap: break-word; -} -.mCS_no_scrollbar { - padding-right: 5px; -} - -.mapCard .mapMetadata { - font-family: 'din-regular', sans-serif; - font-size: 12px; - position:relative; - border-top: 1px solid #BDBDBD; - -webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: none; /* OLD - Firefox 19- */ - -webkit-flex: none; /* Chrome */ - -ms-flex: none; /* IE 10 */ - flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */ -} - -.mapCard .metadataSection { - padding: 8px 16px 0 16px; - width: 78px; - float: left; -} - -.mapPermission { - font-family: 'din-medium', sans-serif; -} -.cCountColor { - font-family: 'din-medium', sans-serif; - color: #DB5D5D; -} -.tCountColor { - font-family: 'din-medium', sans-serif; - color: #4FC059; -} -.sCountColor { - font-family: 'din-medium', sans-serif; - color: #DAB539; -} /* mapper card */ diff --git a/app/assets/stylesheets/mapcard.scss.erb b/app/assets/stylesheets/mapcard.scss.erb new file mode 100644 index 00000000..529257ed --- /dev/null +++ b/app/assets/stylesheets/mapcard.scss.erb @@ -0,0 +1,144 @@ +/* Map Cards */ + +.map { + display:inline-block; + width:220px; + height:340px; + font-size: 12px; + text-align: left; + overflow: visible; + background: #e8e8e8; + border-radius:2px; + margin:16px 16px 16px 19px; + box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); +} +.map.newMap { + float: left; + position: relative; +} +.map.newMap:hover { + background: #dcdcdc; +} +.map.newMap a { + height: 340px; + display: block; + position: relative; +} +.newMap .newMapImage { + display: block; + width: 72px; + height: 72px; + background-image: url("<%= asset_data_uri('newmap_sprite.png') %>"); + background-repeat: no-repeat; + background-position: 0 0; + position: absolute; + left: 50%; + margin-left: -36px; + top: 50%; + margin-top: -36px; +} +.map:hover .newMapImage { + background-position: 0 -72px; +} +.newMap span { + font-family: 'din-regular', sans-serif; + font-size: 18px; + line-height: 22px; + text-align: center; + display: block; + padding-top: 220px; +} + +.mapCard { + position:relative; + width:100%; + height:308px; + padding: 0 0 16px 0; + color: #424242; + +.mapScreenshot { + width: 100%; + height: 220px; +} + +.mapScreenshot img { + width: 100%; +} + +.title { + word-wrap: break-word; + font-size:18px; + line-height:22px; + height: 71px; + display:table; + padding: 0 16px; + font-family: 'din-regular', sans-serif; + margin: 0 auto; + + .innerTitle { + display: table-cell; + vertical-align: middle; + text-align: center; + } +} + +.creatorAndPerm { + padding: 8px; +} + +.creatorImage { + display: inline-block; + border-radius: 16px; + vertical-align: middle; + width: 32px; + height: 32px; +} + +span.creatorName { + margin-left: 8px; +} + + +.scroll { + display:block; + font-family: helvetica, sans-serif; + font-size: 12px; + word-wrap: break-word; + text-align: center; + margin-top: 16px; +} + +&:hover .mainContent { + filter: blur(2px); +} + +&:hover .mapMetadata { + display: block; +} + +.mapMetadata { + display: none; + position: absolute; + top: 0; + left: 0; + padding: 40px 20px 0; + height: 300px; + font-family: 'din-regular', sans-serif; + font-size: 12px; + color: #FFF; + background: -moz-linear-gradient(top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.43) 81%, rgba(0,0,0,0) 100%); + background: -webkit-linear-gradient(top, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a6000000', endColorstr='#00000000',GradientType=0 ); +} + +.metadataSection { + padding: 16px 0; + width: 90px; + float: left; + font-family: 'din-medium', sans-serif; + text-align: center; +} + +} + diff --git a/app/models/map.rb b/app/models/map.rb index cdd6b333..3f5a0a16 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -19,10 +19,10 @@ class Map < ApplicationRecord # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, styles: { - thumb: ['188x126#', :png] + thumb: ['220x220#', :png] #:full => ['940x630#', :png] }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -59,13 +59,17 @@ class Map < ApplicationRecord delegate :name, to: :user, prefix: true def user_image - user.image.url + user.image.url(:thirtytwo) end def contributor_count contributors.length end + def star_count + stars.length + end + def collaborator_ids collaborators.map(&:id) end @@ -87,7 +91,7 @@ class Map < ApplicationRecord end def as_json(_options = {}) - json = super(methods: [:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) + json = super(methods: [:user_name, :user_image, :star_count, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str json[:updated_at_clean] = updated_at_str json diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index e31ede18..3a1557ee 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -12,7 +12,7 @@ class MapCard extends Component { const d = map.get('desc') const maxNameLength = 32 - const maxDescLength = 118 + const maxDescLength = 236 const truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' const truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' const editPermission = map.authorizeToEdit(currentUser) ? 'canEdit' : 'cannotEdit' @@ -21,42 +21,43 @@ class MapCard extends Component {
-
- - { truncatedName } - -
- -
-
-
- { truncatedDesc } -
+
+
+
+ +
+
+
{ truncatedName }
+
+
+ + { map.get('user_name') }
-
- - { map.get('contributor_count') } - - { map.get('contributor_count') === 1 ? ' contributor' : ' contributors' } +
+ { map.get('contributor_count') }
+ { map.get('contributor_count') === 1 ? 'contributor' : 'contributors' }
-
- - { map.get('topic_count') } - - { map.get('topic_count') === 1 ? ' topic' : ' topics' } -
-
- { map.get('permission') ? capitalize(map.get('permission')) : 'Commons' } +
+ { map.get('topic_count') }
+ { map.get('topic_count') === 1 ? 'topic' : 'topics' }
-
- - { map.get('synapse_count') } - - { map.get('synapse_count') === 1 ? ' synapse' : ' synapses' } +
+ { map.get('star_count') }
+ { map.get('star_count') === 1 ? 'star' : 'stars' } +
+
+ { map.get('synapse_count') }
+ { map.get('synapse_count') === 1 ? 'synapse' : 'synapses' }
+
+
+ { truncatedDesc } +
+
+
From c0955d7c5ea9592ecfac6b624bc6ca6a518617ba Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 17 Oct 2016 01:20:48 -0400 Subject: [PATCH 213/378] multiple policy issues (#771) * multiple policy errors * make some things more explicit --- app/models/user.rb | 5 +++++ app/policies/mapping_policy.rb | 12 +++++++----- app/policies/message_policy.rb | 12 +++++++----- app/policies/synapse_policy.rb | 3 +-- app/policies/topic_policy.rb | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 4f679c1b..52d6ef09 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,11 @@ class User < ApplicationRecord json end + def all_accessible_maps + #TODO: is there a way to keep this an ActiveRecord relation? + maps + shared_maps + end + def recentMetacodes array = [] self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index efcb798b..6cdb7e9b 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -8,11 +8,13 @@ class MappingPolicy < ApplicationPolicy # a private topic, since you can't see the private topic anyways visible = %w(public commons) permission = 'maps.permission IN (?)' - if user - scope.joins(:map).where(permission, visible).or(scope.joins(:map).where(user_id: user.id)) - else - scope.joins(:map).where(permission, visible) - end + return scope.joins(:map).where(permission, visible) unless user + + # if this is getting changed, the policy_scope for messages should also be changed + # as it is based entirely on the map to which it belongs + scope.joins(:map).where(permission, visible) + .or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id))) + .or(scope.joins(:map).where('maps.user_id = ?', user.id)) end end diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index f35a2895..c32e29ed 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -4,11 +4,13 @@ class MessagePolicy < ApplicationPolicy def resolve visible = %w(public commons) permission = 'maps.permission IN (?)' - if user - scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) - else - scope.where(permission, visible) - end + return scope.joins(:map).where(permission, visible) unless user + + # if this is getting changed, the policy_scope for mappings should also be changed + # as it is based entirely on the map to which it belongs + scope.joins(:map).where(permission, visible) + .or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id))) + .or(scope.joins(:map).where('maps.user_id = ?', user.id)) end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index eae820b3..f3d2c997 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -3,11 +3,10 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - return scope.where(permission: visible) unless user scope.where(permission: visible) - .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 7bcf585c..cf091662 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -6,7 +6,7 @@ class TopicPolicy < ApplicationPolicy return scope.where(permission: visible) unless user scope.where(permission: visible) - .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end From 0ee1b3284a97487b5a113778078ddc3323cb80ed Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 13:47:42 +0800 Subject: [PATCH 214/378] fix check-canvas-support require --- app/assets/javascripts/application.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index df086157..051edc8b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,4 +16,3 @@ //= require_directory ./lib //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle -//= require ./src/check-canvas-support From 517cfcb9132c300ba749557a40543d18aa8c2c8f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 17 Oct 2016 10:39:08 -0400 Subject: [PATCH 215/378] remove static lib files in favor of npm ones (#773) * remove static lib files in favor of npm ones * update howler to work correctly * patch npm modules to not use window --- .../javascripts/lib/attachMediaStream.js | 39 - app/assets/javascripts/lib/howler.js | 1353 --- .../javascripts/lib/simplewebrtc.bundle.js | 9808 ----------------- frontend/src/Metamaps/Realtime.js | 18 +- frontend/src/Metamaps/Views/ChatView.js | 5 +- frontend/src/Metamaps/Views/Room.js | 3 +- package.json | 5 + 7 files changed, 20 insertions(+), 11211 deletions(-) delete mode 100644 app/assets/javascripts/lib/attachMediaStream.js delete mode 100644 app/assets/javascripts/lib/howler.js delete mode 100644 app/assets/javascripts/lib/simplewebrtc.bundle.js diff --git a/app/assets/javascripts/lib/attachMediaStream.js b/app/assets/javascripts/lib/attachMediaStream.js deleted file mode 100644 index de3feef5..00000000 --- a/app/assets/javascripts/lib/attachMediaStream.js +++ /dev/null @@ -1,39 +0,0 @@ -var attachMediaStream = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; - }; \ No newline at end of file diff --git a/app/assets/javascripts/lib/howler.js b/app/assets/javascripts/lib/howler.js deleted file mode 100644 index f393b3b1..00000000 --- a/app/assets/javascripts/lib/howler.js +++ /dev/null @@ -1,1353 +0,0 @@ -/*! - * howler.js v1.1.26 - * howlerjs.com - * - * (c) 2013-2015, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - -(function() { - // setup - var cache = {}; - - // setup the audio context - var ctx = null, - usingWebAudio = true, - noAudio = false; - try { - if (typeof AudioContext !== 'undefined') { - ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - ctx = new webkitAudioContext(); - } else { - usingWebAudio = false; - } - } catch(e) { - usingWebAudio = false; - } - - if (!usingWebAudio) { - if (typeof Audio !== 'undefined') { - try { - new Audio(); - } catch(e) { - noAudio = true; - } - } else { - noAudio = true; - } - } - - // create a master gain node - if (usingWebAudio) { - var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); - masterGain.gain.value = 1; - masterGain.connect(ctx.destination); - } - - // create global controller - var HowlerGlobal = function(codecs) { - this._volume = 1; - this._muted = false; - this.usingWebAudio = usingWebAudio; - this.ctx = ctx; - this.noAudio = noAudio; - this._howls = []; - this._codecs = codecs; - this.iOSAutoEnable = true; - }; - HowlerGlobal.prototype = { - /** - * Get/set the global volume for all sounds. - * @param {Float} vol Volume from 0.0 to 1.0. - * @return {Howler/Float} Returns self or current volume. - */ - volume: function(vol) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - if (usingWebAudio) { - masterGain.gain.value = vol; - } - - // loop through cache and change volume of all nodes that are using HTML5 Audio - for (var key in self._howls) { - if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { - // loop through the audio nodes - for (var i=0; i 0) ? node._pos : self._sprite[sprite][0] / 1000; - - // determine how long to play for - var duration = 0; - if (self._webAudio) { - duration = self._sprite[sprite][1] / 1000 - node._pos; - if (node._pos > 0) { - pos = self._sprite[sprite][0] / 1000 + pos; - } - } else { - duration = self._sprite[sprite][1] / 1000 - (pos - self._sprite[sprite][0] / 1000); - } - - // determine if this sound should be looped - var loop = !!(self._loop || self._sprite[sprite][2]); - - // set timer to fire the 'onend' event - var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', - timerId; - (function() { - var data = { - id: soundId, - sprite: sprite, - loop: loop - }; - timerId = setTimeout(function() { - // if looping, restart the track - if (!self._webAudio && loop) { - self.stop(data.id).play(sprite, data.id); - } - - // set web audio node to paused at end - if (self._webAudio && !loop) { - self._nodeById(data.id).paused = true; - self._nodeById(data.id)._pos = 0; - - // clear the end timer - self._clearEndTimer(data.id); - } - - // end the track if it is HTML audio and a sprite - if (!self._webAudio && !loop) { - self.stop(data.id); - } - - // fire ended event - self.on('end', soundId); - }, duration * 1000); - - // store the reference to the timer - self._onendTimer.push({timer: timerId, id: data.id}); - })(); - - if (self._webAudio) { - var loopStart = self._sprite[sprite][0] / 1000, - loopEnd = self._sprite[sprite][1] / 1000; - - // set the play id to this node and load into context - node.id = soundId; - node.paused = false; - refreshBuffer(self, [loop, loopStart, loopEnd], soundId); - self._playStart = ctx.currentTime; - node.gain.value = self._volume; - - if (typeof node.bufferSource.start === 'undefined') { - loop ? node.bufferSource.noteGrainOn(0, pos, 86400) : node.bufferSource.noteGrainOn(0, pos, duration); - } else { - loop ? node.bufferSource.start(0, pos, 86400) : node.bufferSource.start(0, pos, duration); - } - } else { - if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { - node.readyState = 4; - node.id = soundId; - node.currentTime = pos; - node.muted = Howler._muted || node.muted; - node.volume = self._volume * Howler.volume(); - setTimeout(function() { node.play(); }, 0); - } else { - self._clearEndTimer(soundId); - - (function(){ - var sound = self, - playSprite = sprite, - fn = callback, - newNode = node; - var listener = function() { - sound.play(playSprite, fn); - - // clear the event listener - newNode.removeEventListener('canplaythrough', listener, false); - }; - newNode.addEventListener('canplaythrough', listener, false); - })(); - - return self; - } - } - - // fire the play event and send the soundId back in the callback - self.on('play'); - if (typeof callback === 'function') callback(soundId); - - return self; - }); - - return self; - }, - - /** - * Pause playback and save the current position. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - pause: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.pause(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = self.pos(null, id); - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else { - activeNode.pause(); - } - } - - self.on('pause'); - - return self; - }, - - /** - * Stop playback and reset to start. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - stop: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.stop(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = 0; - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else if (!isNaN(activeNode.duration)) { - activeNode.pause(); - activeNode.currentTime = 0; - } - } - - return self; - }, - - /** - * Mute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - mute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.mute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = 0; - } else { - activeNode.muted = true; - } - } - - return self; - }, - - /** - * Unmute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - unmute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.unmute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = self._volume; - } else { - activeNode.muted = false; - } - } - - return self; - }, - - /** - * Get/set volume of this sound. - * @param {Float} vol Volume from 0.0 to 1.0. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current volume. - */ - volume: function(vol, id) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.volume(vol, id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = vol; - } else { - activeNode.volume = vol * Howler.volume(); - } - } - - return self; - } else { - return self._volume; - } - }, - - /** - * Get/set whether to loop the sound. - * @param {Boolean} loop To loop or not to loop, that is the question. - * @return {Howl/Boolean} Returns self or current looping value. - */ - loop: function(loop) { - var self = this; - - if (typeof loop === 'boolean') { - self._loop = loop; - - return self; - } else { - return self._loop; - } - }, - - /** - * Get/set sound sprite definition. - * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} - * @param {Integer} offset Where to begin playback in milliseconds - * @param {Integer} duration How long to play in milliseconds - * @param {Boolean} loop (optional) Set true to loop this sprite - * @return {Howl} Returns current sprite sheet or self. - */ - sprite: function(sprite) { - var self = this; - - if (typeof sprite === 'object') { - self._sprite = sprite; - - return self; - } else { - return self._sprite; - } - }, - - /** - * Get/set the position of playback. - * @param {Float} pos The position to move current playback to. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current playback position. - */ - pos: function(pos, id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.pos(pos); - }); - - return typeof pos === 'number' ? self : self._pos || 0; - } - - // make sure we are dealing with a number for pos - pos = parseFloat(pos); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (pos >= 0) { - self.pause(id); - activeNode._pos = pos; - self.play(activeNode._sprite, id); - - return self; - } else { - return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; - } - } else if (pos >= 0) { - return self; - } else { - // find the first inactive node to return the pos for - for (var i=0; i= 0 || x < 0) { - if (self._webAudio) { - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - self._pos3d = [x, y, z]; - activeNode.panner.setPosition(x, y, z); - activeNode.panner.panningModel = self._model || 'HRTF'; - } - } - } else { - return self._pos3d; - } - - return self; - }, - - /** - * Fade a currently playing sound between two volumes. - * @param {Number} from The volume to fade from (0.0 to 1.0). - * @param {Number} to The volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback (optional) Fired when the fade is complete. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fade: function(from, to, len, callback, id) { - var self = this, - diff = Math.abs(from - to), - dir = from > to ? 'down' : 'up', - steps = diff / 0.01, - stepTime = len / steps; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.fade(from, to, len, callback, id); - }); - - return self; - } - - // set the volume to the start position - self.volume(from, id); - - for (var i=1; i<=steps; i++) { - (function() { - var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, - vol = Math.round(1000 * change) / 1000, - toVol = to; - - setTimeout(function() { - self.volume(vol, id); - - if (vol === toVol) { - if (callback) callback(); - } - }, stepTime * i); - })(); - } - }, - - /** - * [DEPRECATED] Fade in the current sound. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @return {Howl} - */ - fadeIn: function(to, len, callback) { - return this.volume(0).play().fade(0, to, len, callback); - }, - - /** - * [DEPRECATED] Fade out the current sound and pause when finished. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fadeOut: function(to, len, callback, id) { - var self = this; - - return self.fade(self._volume, to, len, function() { - if (callback) callback(); - self.pause(id); - - // fire ended event - self.on('end'); - }, id); - }, - - /** - * Get an audio node by ID. - * @return {Howl} Audio node. - */ - _nodeById: function(id) { - var self = this, - node = self._audioNode[0]; - - // find the node with this ID - for (var i=0; i=0; i--) { - if (inactive <= 5) { - break; - } - - if (self._audioNode[i].paused) { - // disconnect the audio source if using Web Audio - if (self._webAudio) { - self._audioNode[i].disconnect(0); - } - - inactive--; - self._audioNode.splice(i, 1); - } - } - }, - - /** - * Clear 'onend' timeout before it ends. - * @param {String} soundId The play instance ID. - */ - _clearEndTimer: function(soundId) { - var self = this, - index = 0; - - // loop through the timers to find the one associated with this sound - for (var i=0; i= 0) { - Howler._howls.splice(index, 1); - } - - // delete this sound from the cache - delete cache[self._src]; - self = null; - } - - }; - - // only define these functions when using WebAudio - if (usingWebAudio) { - - /** - * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). - * @param {Object} obj The Howl object for the sound to load. - * @param {String} url The path to the sound file. - */ - var loadBuffer = function(obj, url) { - // check if the buffer has already been cached - if (url in cache) { - // set the duration from the cache - obj._duration = cache[url].duration; - - // load the sound into this object - loadSound(obj); - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - // Decode base64 data-URIs because some browsers cannot load data-URIs with XMLHttpRequest. - var data = atob(url.split(',')[1]); - var dataView = new Uint8Array(data.length); - for (var i=0; i= 26) || - (window.navigator.userAgent.match('Firefox') && parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10) >= 33)); -var AudioContext = window.AudioContext || window.webkitAudioContext; -var videoEl = document.createElement('video'); -var supportVp8 = videoEl && videoEl.canPlayType && videoEl.canPlayType('video/webm; codecs="vp8", vorbis') === "probably"; -var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; - -// export support flags and constructors.prototype && PC -module.exports = { - prefix: prefix, - support: !!PC && supportVp8 && !!getUserMedia, - // new support style - supportRTCPeerConnection: !!PC, - supportVp8: supportVp8, - supportGetUserMedia: !!getUserMedia, - supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), - supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - supportScreenSharing: !!screenSharing, - // old deprecated style. Dont use this anymore - dataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), - webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - screenSharing: !!screenSharing, - // constructors - AudioContext: AudioContext, - PeerConnection: PC, - SessionDescription: SessionDescription, - IceCandidate: IceCandidate, - MediaStream: MediaStream, - getUserMedia: getUserMedia -}; - -},{}],6:[function(require,module,exports){ -module.exports = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; -}; - -},{}],7:[function(require,module,exports){ -var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); -var l = methods.length; -var fn = function () {}; -var mockconsole = {}; - -while (l--) { - mockconsole[methods[l]] = fn; -} - -module.exports = mockconsole; - -},{}],2:[function(require,module,exports){ -var io = require('socket.io-client'); - -function SocketIoConnection(config) { - this.connection = io.connect(config.url, config.socketio); -} - -SocketIoConnection.prototype.on = function (ev, fn) { - this.connection.on(ev, fn); -}; - -SocketIoConnection.prototype.emit = function () { - this.connection.emit.apply(this.connection, arguments); -}; - -SocketIoConnection.prototype.getSessionid = function () { - return this.connection.socket.sessionid; -}; - -SocketIoConnection.prototype.disconnect = function () { - return this.connection.disconnect(); -}; - -module.exports = SocketIoConnection; - -},{"socket.io-client":8}],8:[function(require,module,exports){ -/*! Socket.IO.js build:0.9.16, development. Copyright(c) 2011 LearnBoost MIT Licensed */ - -var io = ('undefined' === typeof module ? {} : module.exports); -(function() { - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, global) { - - /** - * IO namespace. - * - * @namespace - */ - - var io = exports; - - /** - * Socket.IO version - * - * @api public - */ - - io.version = '0.9.16'; - - /** - * Protocol implemented. - * - * @api public - */ - - io.protocol = 1; - - /** - * Available transports, these will be populated with the available transports - * - * @api public - */ - - io.transports = []; - - /** - * Keep track of jsonp callbacks. - * - * @api private - */ - - io.j = []; - - /** - * Keep track of our io.Sockets - * - * @api private - */ - io.sockets = {}; - - - /** - * Manages connections to hosts. - * - * @param {String} uri - * @Param {Boolean} force creation of new socket (defaults to false) - * @api public - */ - - io.connect = function (host, details) { - var uri = io.util.parseUri(host) - , uuri - , socket; - - if (global && global.location) { - uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); - uri.host = uri.host || (global.document - ? global.document.domain : global.location.hostname); - uri.port = uri.port || global.location.port; - } - - uuri = io.util.uniqueUri(uri); - - var options = { - host: uri.host - , secure: 'https' == uri.protocol - , port: uri.port || ('https' == uri.protocol ? 443 : 80) - , query: uri.query || '' - }; - - io.util.merge(options, details); - - if (options['force new connection'] || !io.sockets[uuri]) { - socket = new io.Socket(options); - } - - if (!options['force new connection'] && socket) { - io.sockets[uuri] = socket; - } - - socket = socket || io.sockets[uuri]; - - // if path is different from '' or / - return socket.of(uri.path.length > 1 ? uri.path : ''); - }; - -})('object' === typeof module ? module.exports : (this.io = {}), this); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, global) { - - /** - * Utilities namespace. - * - * @namespace - */ - - var util = exports.util = {}; - - /** - * Parses an URI - * - * @author Steven Levithan (MIT license) - * @api public - */ - - var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; - - var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', - 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', - 'anchor']; - - util.parseUri = function (str) { - var m = re.exec(str || '') - , uri = {} - , i = 14; - - while (i--) { - uri[parts[i]] = m[i] || ''; - } - - return uri; - }; - - /** - * Produces a unique url that identifies a Socket.IO connection. - * - * @param {Object} uri - * @api public - */ - - util.uniqueUri = function (uri) { - var protocol = uri.protocol - , host = uri.host - , port = uri.port; - - if ('document' in global) { - host = host || document.domain; - port = port || (protocol == 'https' - && document.location.protocol !== 'https:' ? 443 : document.location.port); - } else { - host = host || 'localhost'; - - if (!port && protocol == 'https') { - port = 443; - } - } - - return (protocol || 'http') + '://' + host + ':' + (port || 80); - }; - - /** - * Mergest 2 query strings in to once unique query string - * - * @param {String} base - * @param {String} addition - * @api public - */ - - util.query = function (base, addition) { - var query = util.chunkQuery(base || '') - , components = []; - - util.merge(query, util.chunkQuery(addition || '')); - for (var part in query) { - if (query.hasOwnProperty(part)) { - components.push(part + '=' + query[part]); - } - } - - return components.length ? '?' + components.join('&') : ''; - }; - - /** - * Transforms a querystring in to an object - * - * @param {String} qs - * @api public - */ - - util.chunkQuery = function (qs) { - var query = {} - , params = qs.split('&') - , i = 0 - , l = params.length - , kv; - - for (; i < l; ++i) { - kv = params[i].split('='); - if (kv[0]) { - query[kv[0]] = kv[1]; - } - } - - return query; - }; - - /** - * Executes the given function when the page is loaded. - * - * io.util.load(function () { console.log('page loaded'); }); - * - * @param {Function} fn - * @api public - */ - - var pageLoaded = false; - - util.load = function (fn) { - if ('document' in global && document.readyState === 'complete' || pageLoaded) { - return fn(); - } - - util.on(global, 'load', fn, false); - }; - - /** - * Adds an event. - * - * @api private - */ - - util.on = function (element, event, fn, capture) { - if (element.attachEvent) { - element.attachEvent('on' + event, fn); - } else if (element.addEventListener) { - element.addEventListener(event, fn, capture); - } - }; - - /** - * Generates the correct `XMLHttpRequest` for regular and cross domain requests. - * - * @param {Boolean} [xdomain] Create a request that can be used cross domain. - * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. - * @api private - */ - - util.request = function (xdomain) { - - if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { - return new XDomainRequest(); - } - - if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { - return new XMLHttpRequest(); - } - - if (!xdomain) { - try { - return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); - } catch(e) { } - } - - return null; - }; - - /** - * XHR based transport constructor. - * - * @constructor - * @api public - */ - - /** - * Change the internal pageLoaded value. - */ - - if ('undefined' != typeof window) { - util.load(function () { - pageLoaded = true; - }); - } - - /** - * Defers a function to ensure a spinner is not displayed by the browser - * - * @param {Function} fn - * @api public - */ - - util.defer = function (fn) { - if (!util.ua.webkit || 'undefined' != typeof importScripts) { - return fn(); - } - - util.load(function () { - setTimeout(fn, 100); - }); - }; - - /** - * Merges two objects. - * - * @api public - */ - - util.merge = function merge (target, additional, deep, lastseen) { - var seen = lastseen || [] - , depth = typeof deep == 'undefined' ? 2 : deep - , prop; - - for (prop in additional) { - if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { - if (typeof target[prop] !== 'object' || !depth) { - target[prop] = additional[prop]; - seen.push(additional[prop]); - } else { - util.merge(target[prop], additional[prop], depth - 1, seen); - } - } - } - - return target; - }; - - /** - * Merges prototypes from objects - * - * @api public - */ - - util.mixin = function (ctor, ctor2) { - util.merge(ctor.prototype, ctor2.prototype); - }; - - /** - * Shortcut for prototypical and static inheritance. - * - * @api private - */ - - util.inherit = function (ctor, ctor2) { - function f() {}; - f.prototype = ctor2.prototype; - ctor.prototype = new f; - }; - - /** - * Checks if the given object is an Array. - * - * io.util.isArray([]); // true - * io.util.isArray({}); // false - * - * @param Object obj - * @api public - */ - - util.isArray = Array.isArray || function (obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; - }; - - /** - * Intersects values of two arrays into a third - * - * @api public - */ - - util.intersect = function (arr, arr2) { - var ret = [] - , longest = arr.length > arr2.length ? arr : arr2 - , shortest = arr.length > arr2.length ? arr2 : arr; - - for (var i = 0, l = shortest.length; i < l; i++) { - if (~util.indexOf(longest, shortest[i])) - ret.push(shortest[i]); - } - - return ret; - }; - - /** - * Array indexOf compatibility. - * - * @see bit.ly/a5Dxa2 - * @api public - */ - - util.indexOf = function (arr, o, i) { - - for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; - i < j && arr[i] !== o; i++) {} - - return j <= i ? -1 : i; - }; - - /** - * Converts enumerables to array. - * - * @api public - */ - - util.toArray = function (enu) { - var arr = []; - - for (var i = 0, l = enu.length; i < l; i++) - arr.push(enu[i]); - - return arr; - }; - - /** - * UA / engines detection namespace. - * - * @namespace - */ - - util.ua = {}; - - /** - * Whether the UA supports CORS for XHR. - * - * @api public - */ - - util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { - try { - var a = new XMLHttpRequest(); - } catch (e) { - return false; - } - - return a.withCredentials != undefined; - })(); - - /** - * Detect webkit. - * - * @api public - */ - - util.ua.webkit = 'undefined' != typeof navigator - && /webkit/i.test(navigator.userAgent); - - /** - * Detect iPad/iPhone/iPod. - * - * @api public - */ - - util.ua.iDevice = 'undefined' != typeof navigator - && /iPad|iPhone|iPod/i.test(navigator.userAgent); - -})('undefined' != typeof io ? io : module.exports, this); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.EventEmitter = EventEmitter; - - /** - * Event emitter constructor. - * - * @api public. - */ - - function EventEmitter () {}; - - /** - * Adds a listener - * - * @api public - */ - - EventEmitter.prototype.on = function (name, fn) { - if (!this.$events) { - this.$events = {}; - } - - if (!this.$events[name]) { - this.$events[name] = fn; - } else if (io.util.isArray(this.$events[name])) { - this.$events[name].push(fn); - } else { - this.$events[name] = [this.$events[name], fn]; - } - - return this; - }; - - EventEmitter.prototype.addListener = EventEmitter.prototype.on; - - /** - * Adds a volatile listener. - * - * @api public - */ - - EventEmitter.prototype.once = function (name, fn) { - var self = this; - - function on () { - self.removeListener(name, on); - fn.apply(this, arguments); - }; - - on.listener = fn; - this.on(name, on); - - return this; - }; - - /** - * Removes a listener. - * - * @api public - */ - - EventEmitter.prototype.removeListener = function (name, fn) { - if (this.$events && this.$events[name]) { - var list = this.$events[name]; - - if (io.util.isArray(list)) { - var pos = -1; - - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { - pos = i; - break; - } - } - - if (pos < 0) { - return this; - } - - list.splice(pos, 1); - - if (!list.length) { - delete this.$events[name]; - } - } else if (list === fn || (list.listener && list.listener === fn)) { - delete this.$events[name]; - } - } - - return this; - }; - - /** - * Removes all listeners for an event. - * - * @api public - */ - - EventEmitter.prototype.removeAllListeners = function (name) { - if (name === undefined) { - this.$events = {}; - return this; - } - - if (this.$events && this.$events[name]) { - this.$events[name] = null; - } - - return this; - }; - - /** - * Gets all listeners for a certain event. - * - * @api publci - */ - - EventEmitter.prototype.listeners = function (name) { - if (!this.$events) { - this.$events = {}; - } - - if (!this.$events[name]) { - this.$events[name] = []; - } - - if (!io.util.isArray(this.$events[name])) { - this.$events[name] = [this.$events[name]]; - } - - return this.$events[name]; - }; - - /** - * Emits an event. - * - * @api public - */ - - EventEmitter.prototype.emit = function (name) { - if (!this.$events) { - return false; - } - - var handler = this.$events[name]; - - if (!handler) { - return false; - } - - var args = Array.prototype.slice.call(arguments, 1); - - if ('function' == typeof handler) { - handler.apply(this, args); - } else if (io.util.isArray(handler)) { - var listeners = handler.slice(); - - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - } else { - return false; - } - - return true; - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -/** - * Based on JSON2 (http://www.JSON.org/js.html). - */ - -(function (exports, nativeJSON) { - "use strict"; - - // use native JSON if it's available - if (nativeJSON && nativeJSON.parse){ - return exports.JSON = { - parse: nativeJSON.parse - , stringify: nativeJSON.stringify - }; - } - - var JSON = exports.JSON = {}; - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - function date(d, key) { - return isFinite(d.valueOf()) ? - d.getUTCFullYear() + '-' + - f(d.getUTCMonth() + 1) + '-' + - f(d.getUTCDate()) + 'T' + - f(d.getUTCHours()) + ':' + - f(d.getUTCMinutes()) + ':' + - f(d.getUTCSeconds()) + 'Z' : null; - }; - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : '"' + string + '"'; - } - - - function str(key, holder) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value instanceof Date) { - value = date(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 ? '[]' : gap ? - '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 ? '{}' : gap ? - '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : - '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - -// If the JSON object does not yet have a parse method, give it one. - - JSON.parse = function (text, reviver) { - // The parse method takes a text and an optional reviver function, and returns - // a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - - // The walk method is used to recursively walk the resulting structure so - // that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - - // Parsing happens in four stages. In the first stage, we replace certain - // Unicode characters with escape sequences. JavaScript handles many characters - // incorrectly, either silently deleting them, or treating them as line endings. - - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - - // In the second stage, we run the text against regular expressions that look - // for non-JSON patterns. We are especially concerned with '()' and 'new' - // because they can cause invocation, and '=' because it can cause mutation. - // But just to be safe, we want to reject all unexpected forms. - - // We split the second stage into 4 regexp operations in order to work around - // crippling inefficiencies in IE's and Safari's regexp engines. First we - // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we - // replace all simple value tokens with ']' characters. Third, we delete all - // open brackets that follow a colon or comma or that begin the text. Finally, - // we look to see that the remaining characters are only whitespace or ']' or - // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/ - .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - - // In the third stage we use the eval function to compile the text into a - // JavaScript structure. The '{' operator is subject to a syntactic ambiguity - // in JavaScript: it can begin a block or an object literal. We wrap the text - // in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - - // In the optional fourth stage, we recursively walk the new structure, passing - // each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } - - // If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , typeof JSON !== 'undefined' ? JSON : undefined -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Parser namespace. - * - * @namespace - */ - - var parser = exports.parser = {}; - - /** - * Packet types. - */ - - var packets = parser.packets = [ - 'disconnect' - , 'connect' - , 'heartbeat' - , 'message' - , 'json' - , 'event' - , 'ack' - , 'error' - , 'noop' - ]; - - /** - * Errors reasons. - */ - - var reasons = parser.reasons = [ - 'transport not supported' - , 'client not handshaken' - , 'unauthorized' - ]; - - /** - * Errors advice. - */ - - var advice = parser.advice = [ - 'reconnect' - ]; - - /** - * Shortcuts. - */ - - var JSON = io.JSON - , indexOf = io.util.indexOf; - - /** - * Encodes a packet. - * - * @api private - */ - - parser.encodePacket = function (packet) { - var type = indexOf(packets, packet.type) - , id = packet.id || '' - , endpoint = packet.endpoint || '' - , ack = packet.ack - , data = null; - - switch (packet.type) { - case 'error': - var reason = packet.reason ? indexOf(reasons, packet.reason) : '' - , adv = packet.advice ? indexOf(advice, packet.advice) : ''; - - if (reason !== '' || adv !== '') - data = reason + (adv !== '' ? ('+' + adv) : ''); - - break; - - case 'message': - if (packet.data !== '') - data = packet.data; - break; - - case 'event': - var ev = { name: packet.name }; - - if (packet.args && packet.args.length) { - ev.args = packet.args; - } - - data = JSON.stringify(ev); - break; - - case 'json': - data = JSON.stringify(packet.data); - break; - - case 'connect': - if (packet.qs) - data = packet.qs; - break; - - case 'ack': - data = packet.ackId - + (packet.args && packet.args.length - ? '+' + JSON.stringify(packet.args) : ''); - break; - } - - // construct packet with required fragments - var encoded = [ - type - , id + (ack == 'data' ? '+' : '') - , endpoint - ]; - - // data fragment is optional - if (data !== null && data !== undefined) - encoded.push(data); - - return encoded.join(':'); - }; - - /** - * Encodes multiple messages (payload). - * - * @param {Array} messages - * @api private - */ - - parser.encodePayload = function (packets) { - var decoded = ''; - - if (packets.length == 1) - return packets[0]; - - for (var i = 0, l = packets.length; i < l; i++) { - var packet = packets[i]; - decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; - } - - return decoded; - }; - - /** - * Decodes a packet - * - * @api private - */ - - var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; - - parser.decodePacket = function (data) { - var pieces = data.match(regexp); - - if (!pieces) return {}; - - var id = pieces[2] || '' - , data = pieces[5] || '' - , packet = { - type: packets[pieces[1]] - , endpoint: pieces[4] || '' - }; - - // whether we need to acknowledge the packet - if (id) { - packet.id = id; - if (pieces[3]) - packet.ack = 'data'; - else - packet.ack = true; - } - - // handle different packet types - switch (packet.type) { - case 'error': - var pieces = data.split('+'); - packet.reason = reasons[pieces[0]] || ''; - packet.advice = advice[pieces[1]] || ''; - break; - - case 'message': - packet.data = data || ''; - break; - - case 'event': - try { - var opts = JSON.parse(data); - packet.name = opts.name; - packet.args = opts.args; - } catch (e) { } - - packet.args = packet.args || []; - break; - - case 'json': - try { - packet.data = JSON.parse(data); - } catch (e) { } - break; - - case 'connect': - packet.qs = data || ''; - break; - - case 'ack': - var pieces = data.match(/^([0-9]+)(\+)?(.*)/); - if (pieces) { - packet.ackId = pieces[1]; - packet.args = []; - - if (pieces[3]) { - try { - packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; - } catch (e) { } - } - } - break; - - case 'disconnect': - case 'heartbeat': - break; - }; - - return packet; - }; - - /** - * Decodes data payload. Detects multiple messages - * - * @return {Array} messages - * @api public - */ - - parser.decodePayload = function (data) { - // IE doesn't like data[i] for unicode chars, charAt works fine - if (data.charAt(0) == '\ufffd') { - var ret = []; - - for (var i = 1, length = ''; i < data.length; i++) { - if (data.charAt(i) == '\ufffd') { - ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); - i += Number(length) + 1; - length = ''; - } else { - length += data.charAt(i); - } - } - - return ret; - } else { - return [parser.decodePacket(data)]; - } - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.Transport = Transport; - - /** - * This is the transport template for all supported transport methods. - * - * @constructor - * @api public - */ - - function Transport (socket, sessid) { - this.socket = socket; - this.sessid = sessid; - }; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(Transport, io.EventEmitter); - - - /** - * Indicates whether heartbeats is enabled for this transport - * - * @api private - */ - - Transport.prototype.heartbeats = function () { - return true; - }; - - /** - * Handles the response from the server. When a new response is received - * it will automatically update the timeout, decode the message and - * forwards the response to the onMessage function for further processing. - * - * @param {String} data Response from the server. - * @api private - */ - - Transport.prototype.onData = function (data) { - this.clearCloseTimeout(); - - // If the connection in currently open (or in a reopening state) reset the close - // timeout since we have just received data. This check is necessary so - // that we don't reset the timeout on an explicitly disconnected connection. - if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { - this.setCloseTimeout(); - } - - if (data !== '') { - // todo: we should only do decodePayload for xhr transports - var msgs = io.parser.decodePayload(data); - - if (msgs && msgs.length) { - for (var i = 0, l = msgs.length; i < l; i++) { - this.onPacket(msgs[i]); - } - } - } - - return this; - }; - - /** - * Handles packets. - * - * @api private - */ - - Transport.prototype.onPacket = function (packet) { - this.socket.setHeartbeatTimeout(); - - if (packet.type == 'heartbeat') { - return this.onHeartbeat(); - } - - if (packet.type == 'connect' && packet.endpoint == '') { - this.onConnect(); - } - - if (packet.type == 'error' && packet.advice == 'reconnect') { - this.isOpen = false; - } - - this.socket.onPacket(packet); - - return this; - }; - - /** - * Sets close timeout - * - * @api private - */ - - Transport.prototype.setCloseTimeout = function () { - if (!this.closeTimeout) { - var self = this; - - this.closeTimeout = setTimeout(function () { - self.onDisconnect(); - }, this.socket.closeTimeout); - } - }; - - /** - * Called when transport disconnects. - * - * @api private - */ - - Transport.prototype.onDisconnect = function () { - if (this.isOpen) this.close(); - this.clearTimeouts(); - this.socket.onDisconnect(); - return this; - }; - - /** - * Called when transport connects - * - * @api private - */ - - Transport.prototype.onConnect = function () { - this.socket.onConnect(); - return this; - }; - - /** - * Clears close timeout - * - * @api private - */ - - Transport.prototype.clearCloseTimeout = function () { - if (this.closeTimeout) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; - } - }; - - /** - * Clear timeouts - * - * @api private - */ - - Transport.prototype.clearTimeouts = function () { - this.clearCloseTimeout(); - - if (this.reopenTimeout) { - clearTimeout(this.reopenTimeout); - } - }; - - /** - * Sends a packet - * - * @param {Object} packet object. - * @api private - */ - - Transport.prototype.packet = function (packet) { - this.send(io.parser.encodePacket(packet)); - }; - - /** - * Send the received heartbeat message back to server. So the server - * knows we are still connected. - * - * @param {String} heartbeat Heartbeat response from the server. - * @api private - */ - - Transport.prototype.onHeartbeat = function (heartbeat) { - this.packet({ type: 'heartbeat' }); - }; - - /** - * Called when the transport opens. - * - * @api private - */ - - Transport.prototype.onOpen = function () { - this.isOpen = true; - this.clearCloseTimeout(); - this.socket.onOpen(); - }; - - /** - * Notifies the base when the connection with the Socket.IO server - * has been disconnected. - * - * @api private - */ - - Transport.prototype.onClose = function () { - var self = this; - - /* FIXME: reopen delay causing a infinit loop - this.reopenTimeout = setTimeout(function () { - self.open(); - }, this.socket.options['reopen delay']);*/ - - this.isOpen = false; - this.socket.onClose(); - this.onDisconnect(); - }; - - /** - * Generates a connection url based on the Socket.IO URL Protocol. - * See for more details. - * - * @returns {String} Connection url - * @api private - */ - - Transport.prototype.prepareUrl = function () { - var options = this.socket.options; - - return this.scheme() + '://' - + options.host + ':' + options.port + '/' - + options.resource + '/' + io.protocol - + '/' + this.name + '/' + this.sessid; - }; - - /** - * Checks if the transport is ready to start a connection. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - Transport.prototype.ready = function (socket, fn) { - fn.call(this); - }; -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports.Socket = Socket; - - /** - * Create a new `Socket.IO client` which can establish a persistent - * connection with a Socket.IO enabled server. - * - * @api public - */ - - function Socket (options) { - this.options = { - port: 80 - , secure: false - , document: 'document' in global ? document : false - , resource: 'socket.io' - , transports: io.transports - , 'connect timeout': 10000 - , 'try multiple transports': true - , 'reconnect': true - , 'reconnection delay': 500 - , 'reconnection limit': Infinity - , 'reopen delay': 3000 - , 'max reconnection attempts': 10 - , 'sync disconnect on unload': false - , 'auto connect': true - , 'flash policy port': 10843 - , 'manualFlush': false - }; - - io.util.merge(this.options, options); - - this.connected = false; - this.open = false; - this.connecting = false; - this.reconnecting = false; - this.namespaces = {}; - this.buffer = []; - this.doBuffer = false; - - if (this.options['sync disconnect on unload'] && - (!this.isXDomain() || io.util.ua.hasCORS)) { - var self = this; - io.util.on(global, 'beforeunload', function () { - self.disconnectSync(); - }, false); - } - - if (this.options['auto connect']) { - this.connect(); - } -}; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(Socket, io.EventEmitter); - - /** - * Returns a namespace listener/emitter for this socket - * - * @api public - */ - - Socket.prototype.of = function (name) { - if (!this.namespaces[name]) { - this.namespaces[name] = new io.SocketNamespace(this, name); - - if (name !== '') { - this.namespaces[name].packet({ type: 'connect' }); - } - } - - return this.namespaces[name]; - }; - - /** - * Emits the given event to the Socket and all namespaces - * - * @api private - */ - - Socket.prototype.publish = function () { - this.emit.apply(this, arguments); - - var nsp; - - for (var i in this.namespaces) { - if (this.namespaces.hasOwnProperty(i)) { - nsp = this.of(i); - nsp.$emit.apply(nsp, arguments); - } - } - }; - - /** - * Performs the handshake - * - * @api private - */ - - function empty () { }; - - Socket.prototype.handshake = function (fn) { - var self = this - , options = this.options; - - function complete (data) { - if (data instanceof Error) { - self.connecting = false; - self.onError(data.message); - } else { - fn.apply(null, data.split(':')); - } - }; - - var url = [ - 'http' + (options.secure ? 's' : '') + ':/' - , options.host + ':' + options.port - , options.resource - , io.protocol - , io.util.query(this.options.query, 't=' + +new Date) - ].join('/'); - - if (this.isXDomain() && !io.util.ua.hasCORS) { - var insertAt = document.getElementsByTagName('script')[0] - , script = document.createElement('script'); - - script.src = url + '&jsonp=' + io.j.length; - insertAt.parentNode.insertBefore(script, insertAt); - - io.j.push(function (data) { - complete(data); - script.parentNode.removeChild(script); - }); - } else { - var xhr = io.util.request(); - - xhr.open('GET', url, true); - if (this.isXDomain()) { - xhr.withCredentials = true; - } - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - xhr.onreadystatechange = empty; - - if (xhr.status == 200) { - complete(xhr.responseText); - } else if (xhr.status == 403) { - self.onError(xhr.responseText); - } else { - self.connecting = false; - !self.reconnecting && self.onError(xhr.responseText); - } - } - }; - xhr.send(null); - } - }; - - /** - * Find an available transport based on the options supplied in the constructor. - * - * @api private - */ - - Socket.prototype.getTransport = function (override) { - var transports = override || this.transports, match; - - for (var i = 0, transport; transport = transports[i]; i++) { - if (io.Transport[transport] - && io.Transport[transport].check(this) - && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { - return new io.Transport[transport](this, this.sessionid); - } - } - - return null; - }; - - /** - * Connects to the server. - * - * @param {Function} [fn] Callback. - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.connect = function (fn) { - if (this.connecting) { - return this; - } - - var self = this; - self.connecting = true; - - this.handshake(function (sid, heartbeat, close, transports) { - self.sessionid = sid; - self.closeTimeout = close * 1000; - self.heartbeatTimeout = heartbeat * 1000; - if(!self.transports) - self.transports = self.origTransports = (transports ? io.util.intersect( - transports.split(',') - , self.options.transports - ) : self.options.transports); - - self.setHeartbeatTimeout(); - - function connect (transports){ - if (self.transport) self.transport.clearTimeouts(); - - self.transport = self.getTransport(transports); - if (!self.transport) return self.publish('connect_failed'); - - // once the transport is ready - self.transport.ready(self, function () { - self.connecting = true; - self.publish('connecting', self.transport.name); - self.transport.open(); - - if (self.options['connect timeout']) { - self.connectTimeoutTimer = setTimeout(function () { - if (!self.connected) { - self.connecting = false; - - if (self.options['try multiple transports']) { - var remaining = self.transports; - - while (remaining.length > 0 && remaining.splice(0,1)[0] != - self.transport.name) {} - - if (remaining.length){ - connect(remaining); - } else { - self.publish('connect_failed'); - } - } - } - }, self.options['connect timeout']); - } - }); - } - - connect(self.transports); - - self.once('connect', function (){ - clearTimeout(self.connectTimeoutTimer); - - fn && typeof fn == 'function' && fn(); - }); - }); - - return this; - }; - - /** - * Clears and sets a new heartbeat timeout using the value given by the - * server during the handshake. - * - * @api private - */ - - Socket.prototype.setHeartbeatTimeout = function () { - clearTimeout(this.heartbeatTimeoutTimer); - if(this.transport && !this.transport.heartbeats()) return; - - var self = this; - this.heartbeatTimeoutTimer = setTimeout(function () { - self.transport.onClose(); - }, this.heartbeatTimeout); - }; - - /** - * Sends a message. - * - * @param {Object} data packet. - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.packet = function (data) { - if (this.connected && !this.doBuffer) { - this.transport.packet(data); - } else { - this.buffer.push(data); - } - - return this; - }; - - /** - * Sets buffer state - * - * @api private - */ - - Socket.prototype.setBuffer = function (v) { - this.doBuffer = v; - - if (!v && this.connected && this.buffer.length) { - if (!this.options['manualFlush']) { - this.flushBuffer(); - } - } - }; - - /** - * Flushes the buffer data over the wire. - * To be invoked manually when 'manualFlush' is set to true. - * - * @api public - */ - - Socket.prototype.flushBuffer = function() { - this.transport.payload(this.buffer); - this.buffer = []; - }; - - - /** - * Disconnect the established connect. - * - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.disconnect = function () { - if (this.connected || this.connecting) { - if (this.open) { - this.of('').packet({ type: 'disconnect' }); - } - - // handle disconnection immediately - this.onDisconnect('booted'); - } - - return this; - }; - - /** - * Disconnects the socket with a sync XHR. - * - * @api private - */ - - Socket.prototype.disconnectSync = function () { - // ensure disconnection - var xhr = io.util.request(); - var uri = [ - 'http' + (this.options.secure ? 's' : '') + ':/' - , this.options.host + ':' + this.options.port - , this.options.resource - , io.protocol - , '' - , this.sessionid - ].join('/') + '/?disconnect=1'; - - xhr.open('GET', uri, false); - xhr.send(null); - - // handle disconnection immediately - this.onDisconnect('booted'); - }; - - /** - * Check if we need to use cross domain enabled transports. Cross domain would - * be a different port or different domain name. - * - * @returns {Boolean} - * @api private - */ - - Socket.prototype.isXDomain = function () { - - var port = global.location.port || - ('https:' == global.location.protocol ? 443 : 80); - - return this.options.host !== global.location.hostname - || this.options.port != port; - }; - - /** - * Called upon handshake. - * - * @api private - */ - - Socket.prototype.onConnect = function () { - if (!this.connected) { - this.connected = true; - this.connecting = false; - if (!this.doBuffer) { - // make sure to flush the buffer - this.setBuffer(false); - } - this.emit('connect'); - } - }; - - /** - * Called when the transport opens - * - * @api private - */ - - Socket.prototype.onOpen = function () { - this.open = true; - }; - - /** - * Called when the transport closes. - * - * @api private - */ - - Socket.prototype.onClose = function () { - this.open = false; - clearTimeout(this.heartbeatTimeoutTimer); - }; - - /** - * Called when the transport first opens a connection - * - * @param text - */ - - Socket.prototype.onPacket = function (packet) { - this.of(packet.endpoint).onPacket(packet); - }; - - /** - * Handles an error. - * - * @api private - */ - - Socket.prototype.onError = function (err) { - if (err && err.advice) { - if (err.advice === 'reconnect' && (this.connected || this.connecting)) { - this.disconnect(); - if (this.options.reconnect) { - this.reconnect(); - } - } - } - - this.publish('error', err && err.reason ? err.reason : err); - }; - - /** - * Called when the transport disconnects. - * - * @api private - */ - - Socket.prototype.onDisconnect = function (reason) { - var wasConnected = this.connected - , wasConnecting = this.connecting; - - this.connected = false; - this.connecting = false; - this.open = false; - - if (wasConnected || wasConnecting) { - this.transport.close(); - this.transport.clearTimeouts(); - if (wasConnected) { - this.publish('disconnect', reason); - - if ('booted' != reason && this.options.reconnect && !this.reconnecting) { - this.reconnect(); - } - } - } - }; - - /** - * Called upon reconnection. - * - * @api private - */ - - Socket.prototype.reconnect = function () { - this.reconnecting = true; - this.reconnectionAttempts = 0; - this.reconnectionDelay = this.options['reconnection delay']; - - var self = this - , maxAttempts = this.options['max reconnection attempts'] - , tryMultiple = this.options['try multiple transports'] - , limit = this.options['reconnection limit']; - - function reset () { - if (self.connected) { - for (var i in self.namespaces) { - if (self.namespaces.hasOwnProperty(i) && '' !== i) { - self.namespaces[i].packet({ type: 'connect' }); - } - } - self.publish('reconnect', self.transport.name, self.reconnectionAttempts); - } - - clearTimeout(self.reconnectionTimer); - - self.removeListener('connect_failed', maybeReconnect); - self.removeListener('connect', maybeReconnect); - - self.reconnecting = false; - - delete self.reconnectionAttempts; - delete self.reconnectionDelay; - delete self.reconnectionTimer; - delete self.redoTransports; - - self.options['try multiple transports'] = tryMultiple; - }; - - function maybeReconnect () { - if (!self.reconnecting) { - return; - } - - if (self.connected) { - return reset(); - }; - - if (self.connecting && self.reconnecting) { - return self.reconnectionTimer = setTimeout(maybeReconnect, 1000); - } - - if (self.reconnectionAttempts++ >= maxAttempts) { - if (!self.redoTransports) { - self.on('connect_failed', maybeReconnect); - self.options['try multiple transports'] = true; - self.transports = self.origTransports; - self.transport = self.getTransport(); - self.redoTransports = true; - self.connect(); - } else { - self.publish('reconnect_failed'); - reset(); - } - } else { - if (self.reconnectionDelay < limit) { - self.reconnectionDelay *= 2; // exponential back off - } - - self.connect(); - self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); - self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); - } - }; - - this.options['try multiple transports'] = false; - this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); - - this.on('connect', maybeReconnect); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.SocketNamespace = SocketNamespace; - - /** - * Socket namespace constructor. - * - * @constructor - * @api public - */ - - function SocketNamespace (socket, name) { - this.socket = socket; - this.name = name || ''; - this.flags = {}; - this.json = new Flag(this, 'json'); - this.ackPackets = 0; - this.acks = {}; - }; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(SocketNamespace, io.EventEmitter); - - /** - * Copies emit since we override it - * - * @api private - */ - - SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; - - /** - * Creates a new namespace, by proxying the request to the socket. This - * allows us to use the synax as we do on the server. - * - * @api public - */ - - SocketNamespace.prototype.of = function () { - return this.socket.of.apply(this.socket, arguments); - }; - - /** - * Sends a packet. - * - * @api private - */ - - SocketNamespace.prototype.packet = function (packet) { - packet.endpoint = this.name; - this.socket.packet(packet); - this.flags = {}; - return this; - }; - - /** - * Sends a message - * - * @api public - */ - - SocketNamespace.prototype.send = function (data, fn) { - var packet = { - type: this.flags.json ? 'json' : 'message' - , data: data - }; - - if ('function' == typeof fn) { - packet.id = ++this.ackPackets; - packet.ack = true; - this.acks[packet.id] = fn; - } - - return this.packet(packet); - }; - - /** - * Emits an event - * - * @api public - */ - - SocketNamespace.prototype.emit = function (name) { - var args = Array.prototype.slice.call(arguments, 1) - , lastArg = args[args.length - 1] - , packet = { - type: 'event' - , name: name - }; - - if ('function' == typeof lastArg) { - packet.id = ++this.ackPackets; - packet.ack = 'data'; - this.acks[packet.id] = lastArg; - args = args.slice(0, args.length - 1); - } - - packet.args = args; - - return this.packet(packet); - }; - - /** - * Disconnects the namespace - * - * @api private - */ - - SocketNamespace.prototype.disconnect = function () { - if (this.name === '') { - this.socket.disconnect(); - } else { - this.packet({ type: 'disconnect' }); - this.$emit('disconnect'); - } - - return this; - }; - - /** - * Handles a packet - * - * @api private - */ - - SocketNamespace.prototype.onPacket = function (packet) { - var self = this; - - function ack () { - self.packet({ - type: 'ack' - , args: io.util.toArray(arguments) - , ackId: packet.id - }); - }; - - switch (packet.type) { - case 'connect': - this.$emit('connect'); - break; - - case 'disconnect': - if (this.name === '') { - this.socket.onDisconnect(packet.reason || 'booted'); - } else { - this.$emit('disconnect', packet.reason); - } - break; - - case 'message': - case 'json': - var params = ['message', packet.data]; - - if (packet.ack == 'data') { - params.push(ack); - } else if (packet.ack) { - this.packet({ type: 'ack', ackId: packet.id }); - } - - this.$emit.apply(this, params); - break; - - case 'event': - var params = [packet.name].concat(packet.args); - - if (packet.ack == 'data') - params.push(ack); - - this.$emit.apply(this, params); - break; - - case 'ack': - if (this.acks[packet.ackId]) { - this.acks[packet.ackId].apply(this, packet.args); - delete this.acks[packet.ackId]; - } - break; - - case 'error': - if (packet.advice){ - this.socket.onError(packet); - } else { - if (packet.reason == 'unauthorized') { - this.$emit('connect_failed', packet.reason); - } else { - this.$emit('error', packet.reason); - } - } - break; - } - }; - - /** - * Flag interface. - * - * @api private - */ - - function Flag (nsp, name) { - this.namespace = nsp; - this.name = name; - }; - - /** - * Send a message - * - * @api public - */ - - Flag.prototype.send = function () { - this.namespace.flags[this.name] = true; - this.namespace.send.apply(this.namespace, arguments); - }; - - /** - * Emit an event - * - * @api public - */ - - Flag.prototype.emit = function () { - this.namespace.flags[this.name] = true; - this.namespace.emit.apply(this.namespace, arguments); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports.websocket = WS; - - /** - * The WebSocket transport uses the HTML5 WebSocket API to establish an - * persistent connection with the Socket.IO server. This transport will also - * be inherited by the FlashSocket fallback as it provides a API compatible - * polyfill for the WebSockets. - * - * @constructor - * @extends {io.Transport} - * @api public - */ - - function WS (socket) { - io.Transport.apply(this, arguments); - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(WS, io.Transport); - - /** - * Transport name - * - * @api public - */ - - WS.prototype.name = 'websocket'; - - /** - * Initializes a new `WebSocket` connection with the Socket.IO server. We attach - * all the appropriate listeners to handle the responses from the server. - * - * @returns {Transport} - * @api public - */ - - WS.prototype.open = function () { - var query = io.util.query(this.socket.options.query) - , self = this - , Socket - - - if (!Socket) { - Socket = global.MozWebSocket || global.WebSocket; - } - - this.websocket = new Socket(this.prepareUrl() + query); - - this.websocket.onopen = function () { - self.onOpen(); - self.socket.setBuffer(false); - }; - this.websocket.onmessage = function (ev) { - self.onData(ev.data); - }; - this.websocket.onclose = function () { - self.onClose(); - self.socket.setBuffer(true); - }; - this.websocket.onerror = function (e) { - self.onError(e); - }; - - return this; - }; - - /** - * Send a message to the Socket.IO server. The message will automatically be - * encoded in the correct message format. - * - * @returns {Transport} - * @api public - */ - - // Do to a bug in the current IDevices browser, we need to wrap the send in a - // setTimeout, when they resume from sleeping the browser will crash if - // we don't allow the browser time to detect the socket has been closed - if (io.util.ua.iDevice) { - WS.prototype.send = function (data) { - var self = this; - setTimeout(function() { - self.websocket.send(data); - },0); - return this; - }; - } else { - WS.prototype.send = function (data) { - this.websocket.send(data); - return this; - }; - } - - /** - * Payload - * - * @api private - */ - - WS.prototype.payload = function (arr) { - for (var i = 0, l = arr.length; i < l; i++) { - this.packet(arr[i]); - } - return this; - }; - - /** - * Disconnect the established `WebSocket` connection. - * - * @returns {Transport} - * @api public - */ - - WS.prototype.close = function () { - this.websocket.close(); - return this; - }; - - /** - * Handle the errors that `WebSocket` might be giving when we - * are attempting to connect or send messages. - * - * @param {Error} e The error. - * @api private - */ - - WS.prototype.onError = function (e) { - this.socket.onError(e); - }; - - /** - * Returns the appropriate scheme for the URI generation. - * - * @api private - */ - WS.prototype.scheme = function () { - return this.socket.options.secure ? 'wss' : 'ws'; - }; - - /** - * Checks if the browser has support for native `WebSockets` and that - * it's not the polyfill created for the FlashSocket transport. - * - * @return {Boolean} - * @api public - */ - - WS.check = function () { - return ('WebSocket' in global && !('__addTask' in WebSocket)) - || 'MozWebSocket' in global; - }; - - /** - * Check if the `WebSocket` transport support cross domain communications. - * - * @returns {Boolean} - * @api public - */ - - WS.xdomainCheck = function () { - return true; - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('websocket'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.flashsocket = Flashsocket; - - /** - * The FlashSocket transport. This is a API wrapper for the HTML5 WebSocket - * specification. It uses a .swf file to communicate with the server. If you want - * to serve the .swf file from a other server than where the Socket.IO script is - * coming from you need to use the insecure version of the .swf. More information - * about this can be found on the github page. - * - * @constructor - * @extends {io.Transport.websocket} - * @api public - */ - - function Flashsocket () { - io.Transport.websocket.apply(this, arguments); - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(Flashsocket, io.Transport.websocket); - - /** - * Transport name - * - * @api public - */ - - Flashsocket.prototype.name = 'flashsocket'; - - /** - * Disconnect the established `FlashSocket` connection. This is done by adding a - * new task to the FlashSocket. The rest will be handled off by the `WebSocket` - * transport. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.open = function () { - var self = this - , args = arguments; - - WebSocket.__addTask(function () { - io.Transport.websocket.prototype.open.apply(self, args); - }); - return this; - }; - - /** - * Sends a message to the Socket.IO server. This is done by adding a new - * task to the FlashSocket. The rest will be handled off by the `WebSocket` - * transport. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.send = function () { - var self = this, args = arguments; - WebSocket.__addTask(function () { - io.Transport.websocket.prototype.send.apply(self, args); - }); - return this; - }; - - /** - * Disconnects the established `FlashSocket` connection. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.close = function () { - WebSocket.__tasks.length = 0; - io.Transport.websocket.prototype.close.call(this); - return this; - }; - - /** - * The WebSocket fall back needs to append the flash container to the body - * element, so we need to make sure we have access to it. Or defer the call - * until we are sure there is a body element. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - Flashsocket.prototype.ready = function (socket, fn) { - function init () { - var options = socket.options - , port = options['flash policy port'] - , path = [ - 'http' + (options.secure ? 's' : '') + ':/' - , options.host + ':' + options.port - , options.resource - , 'static/flashsocket' - , 'WebSocketMain' + (socket.isXDomain() ? 'Insecure' : '') + '.swf' - ]; - - // Only start downloading the swf file when the checked that this browser - // actually supports it - if (!Flashsocket.loaded) { - if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') { - // Set the correct file based on the XDomain settings - WEB_SOCKET_SWF_LOCATION = path.join('/'); - } - - if (port !== 843) { - WebSocket.loadFlashPolicyFile('xmlsocket://' + options.host + ':' + port); - } - - WebSocket.__initialize(); - Flashsocket.loaded = true; - } - - fn.call(self); - } - - var self = this; - if (document.body) return init(); - - io.util.load(init); - }; - - /** - * Check if the FlashSocket transport is supported as it requires that the Adobe - * Flash Player plug-in version `10.0.0` or greater is installed. And also check if - * the polyfill is correctly loaded. - * - * @returns {Boolean} - * @api public - */ - - Flashsocket.check = function () { - if ( - typeof WebSocket == 'undefined' - || !('__initialize' in WebSocket) || !swfobject - ) return false; - - return swfobject.getFlashPlayerVersion().major >= 10; - }; - - /** - * Check if the FlashSocket transport can be used as cross domain / cross origin - * transport. Because we can't see which type (secure or insecure) of .swf is used - * we will just return true. - * - * @returns {Boolean} - * @api public - */ - - Flashsocket.xdomainCheck = function () { - return true; - }; - - /** - * Disable AUTO_INITIALIZATION - */ - - if (typeof window != 'undefined') { - WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; - } - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('flashsocket'); -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/* SWFObject v2.2 - is released under the MIT License -*/ -if ('undefined' != typeof window) { -var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O[(['Active'].concat('Object').join('X'))]!=D){try{var ad=new window[(['Active'].concat('Object').join('X'))](W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab -// License: New BSD License -// Reference: http://dev.w3.org/html5/websockets/ -// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol - -(function() { - - if ('undefined' == typeof window || window.WebSocket) return; - - var console = window.console; - if (!console || !console.log || !console.error) { - console = {log: function(){ }, error: function(){ }}; - } - - if (!swfobject.hasFlashPlayerVersion("10.0.0")) { - console.error("Flash Player >= 10.0.0 is required."); - return; - } - if (location.protocol == "file:") { - console.error( - "WARNING: web-socket-js doesn't work in file:///... URL " + - "unless you set Flash Security Settings properly. " + - "Open the page via Web server i.e. http://..."); - } - - /** - * This class represents a faux web socket. - * @param {string} url - * @param {array or string} protocols - * @param {string} proxyHost - * @param {int} proxyPort - * @param {string} headers - */ - WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { - var self = this; - self.__id = WebSocket.__nextId++; - WebSocket.__instances[self.__id] = self; - self.readyState = WebSocket.CONNECTING; - self.bufferedAmount = 0; - self.__events = {}; - if (!protocols) { - protocols = []; - } else if (typeof protocols == "string") { - protocols = [protocols]; - } - // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. - // Otherwise, when onopen fires immediately, onopen is called before it is set. - setTimeout(function() { - WebSocket.__addTask(function() { - WebSocket.__flash.create( - self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); - }); - }, 0); - }; - - /** - * Send data to the web socket. - * @param {string} data The data to send to the socket. - * @return {boolean} True for success, false for failure. - */ - WebSocket.prototype.send = function(data) { - if (this.readyState == WebSocket.CONNECTING) { - throw "INVALID_STATE_ERR: Web Socket connection has not been established"; - } - // We use encodeURIComponent() here, because FABridge doesn't work if - // the argument includes some characters. We don't use escape() here - // because of this: - // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions - // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't - // preserve all Unicode characters either e.g. "\uffff" in Firefox. - // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require - // additional testing. - var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); - if (result < 0) { // success - return true; - } else { - this.bufferedAmount += result; - return false; - } - }; - - /** - * Close this web socket gracefully. - */ - WebSocket.prototype.close = function() { - if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { - return; - } - this.readyState = WebSocket.CLOSING; - WebSocket.__flash.close(this.__id); - }; - - /** - * Implementation of {@link
DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.addEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) { - this.__events[type] = []; - } - this.__events[type].push(listener); - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) return; - var events = this.__events[type]; - for (var i = events.length - 1; i >= 0; --i) { - if (events[i] === listener) { - events.splice(i, 1); - break; - } - } - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {Event} event - * @return void - */ - WebSocket.prototype.dispatchEvent = function(event) { - var events = this.__events[event.type] || []; - for (var i = 0; i < events.length; ++i) { - events[i](event); - } - var handler = this["on" + event.type]; - if (handler) handler(event); - }; - - /** - * Handles an event from Flash. - * @param {Object} flashEvent - */ - WebSocket.prototype.__handleEvent = function(flashEvent) { - if ("readyState" in flashEvent) { - this.readyState = flashEvent.readyState; - } - if ("protocol" in flashEvent) { - this.protocol = flashEvent.protocol; - } - - var jsEvent; - if (flashEvent.type == "open" || flashEvent.type == "error") { - jsEvent = this.__createSimpleEvent(flashEvent.type); - } else if (flashEvent.type == "close") { - // TODO implement jsEvent.wasClean - jsEvent = this.__createSimpleEvent("close"); - } else if (flashEvent.type == "message") { - var data = decodeURIComponent(flashEvent.message); - jsEvent = this.__createMessageEvent("message", data); - } else { - throw "unknown event type: " + flashEvent.type; - } - - this.dispatchEvent(jsEvent); - }; - - WebSocket.prototype.__createSimpleEvent = function(type) { - if (document.createEvent && window.Event) { - var event = document.createEvent("Event"); - event.initEvent(type, false, false); - return event; - } else { - return {type: type, bubbles: false, cancelable: false}; - } - }; - - WebSocket.prototype.__createMessageEvent = function(type, data) { - if (document.createEvent && window.MessageEvent && !window.opera) { - var event = document.createEvent("MessageEvent"); - event.initMessageEvent("message", false, false, data, null, null, window, null); - return event; - } else { - // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. - return {type: type, data: data, bubbles: false, cancelable: false}; - } - }; - - /** - * Define the WebSocket readyState enumeration. - */ - WebSocket.CONNECTING = 0; - WebSocket.OPEN = 1; - WebSocket.CLOSING = 2; - WebSocket.CLOSED = 3; - - WebSocket.__flash = null; - WebSocket.__instances = {}; - WebSocket.__tasks = []; - WebSocket.__nextId = 0; - - /** - * Load a new flash security policy file. - * @param {string} url - */ - WebSocket.loadFlashPolicyFile = function(url){ - WebSocket.__addTask(function() { - WebSocket.__flash.loadManualPolicyFile(url); - }); - }; - - /** - * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. - */ - WebSocket.__initialize = function() { - if (WebSocket.__flash) return; - - if (WebSocket.__swfLocation) { - // For backword compatibility. - window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; - } - if (!window.WEB_SOCKET_SWF_LOCATION) { - console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); - return; - } - var container = document.createElement("div"); - container.id = "webSocketContainer"; - // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents - // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). - // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash - // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is - // the best we can do as far as we know now. - container.style.position = "absolute"; - if (WebSocket.__isFlashLite()) { - container.style.left = "0px"; - container.style.top = "0px"; - } else { - container.style.left = "-100px"; - container.style.top = "-100px"; - } - var holder = document.createElement("div"); - holder.id = "webSocketFlash"; - container.appendChild(holder); - document.body.appendChild(container); - // See this article for hasPriority: - // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html - swfobject.embedSWF( - WEB_SOCKET_SWF_LOCATION, - "webSocketFlash", - "1" /* width */, - "1" /* height */, - "10.0.0" /* SWF version */, - null, - null, - {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, - null, - function(e) { - if (!e.success) { - console.error("[WebSocket] swfobject.embedSWF failed"); - } - }); - }; - - /** - * Called by Flash to notify JS that it's fully loaded and ready - * for communication. - */ - WebSocket.__onFlashInitialized = function() { - // We need to set a timeout here to avoid round-trip calls - // to flash during the initialization process. - setTimeout(function() { - WebSocket.__flash = document.getElementById("webSocketFlash"); - WebSocket.__flash.setCallerUrl(location.href); - WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); - for (var i = 0; i < WebSocket.__tasks.length; ++i) { - WebSocket.__tasks[i](); - } - WebSocket.__tasks = []; - }, 0); - }; - - /** - * Called by Flash to notify WebSockets events are fired. - */ - WebSocket.__onFlashEvent = function() { - setTimeout(function() { - try { - // Gets events using receiveEvents() instead of getting it from event object - // of Flash event. This is to make sure to keep message order. - // It seems sometimes Flash events don't arrive in the same order as they are sent. - var events = WebSocket.__flash.receiveEvents(); - for (var i = 0; i < events.length; ++i) { - WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); - } - } catch (e) { - console.error(e); - } - }, 0); - return true; - }; - - // Called by Flash. - WebSocket.__log = function(message) { - console.log(decodeURIComponent(message)); - }; - - // Called by Flash. - WebSocket.__error = function(message) { - console.error(decodeURIComponent(message)); - }; - - WebSocket.__addTask = function(task) { - if (WebSocket.__flash) { - task(); - } else { - WebSocket.__tasks.push(task); - } - }; - - /** - * Test if the browser is running flash lite. - * @return {boolean} True if flash lite is running, false otherwise. - */ - WebSocket.__isFlashLite = function() { - if (!window.navigator || !window.navigator.mimeTypes) { - return false; - } - var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; - if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { - return false; - } - return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; - }; - - if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { - if (window.addEventListener) { - window.addEventListener("load", function(){ - WebSocket.__initialize(); - }, false); - } else { - window.attachEvent("onload", function(){ - WebSocket.__initialize(); - }); - } - } - -})(); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - * - * @api public - */ - - exports.XHR = XHR; - - /** - * XHR constructor - * - * @costructor - * @api public - */ - - function XHR (socket) { - if (!socket) return; - - io.Transport.apply(this, arguments); - this.sendBuffer = []; - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(XHR, io.Transport); - - /** - * Establish a connection - * - * @returns {Transport} - * @api public - */ - - XHR.prototype.open = function () { - this.socket.setBuffer(false); - this.onOpen(); - this.get(); - - // we need to make sure the request succeeds since we have no indication - // whether the request opened or not until it succeeded. - this.setCloseTimeout(); - - return this; - }; - - /** - * Check if we need to send data to the Socket.IO server, if we have data in our - * buffer we encode it and forward it to the `post` method. - * - * @api private - */ - - XHR.prototype.payload = function (payload) { - var msgs = []; - - for (var i = 0, l = payload.length; i < l; i++) { - msgs.push(io.parser.encodePacket(payload[i])); - } - - this.send(io.parser.encodePayload(msgs)); - }; - - /** - * Send data to the Socket.IO server. - * - * @param data The message - * @returns {Transport} - * @api public - */ - - XHR.prototype.send = function (data) { - this.post(data); - return this; - }; - - /** - * Posts a encoded message to the Socket.IO server. - * - * @param {String} data A encoded message. - * @api private - */ - - function empty () { }; - - XHR.prototype.post = function (data) { - var self = this; - this.socket.setBuffer(true); - - function stateChange () { - if (this.readyState == 4) { - this.onreadystatechange = empty; - self.posting = false; - - if (this.status == 200){ - self.socket.setBuffer(false); - } else { - self.onClose(); - } - } - } - - function onload () { - this.onload = empty; - self.socket.setBuffer(false); - }; - - this.sendXHR = this.request('POST'); - - if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) { - this.sendXHR.onload = this.sendXHR.onerror = onload; - } else { - this.sendXHR.onreadystatechange = stateChange; - } - - this.sendXHR.send(data); - }; - - /** - * Disconnects the established `XHR` connection. - * - * @returns {Transport} - * @api public - */ - - XHR.prototype.close = function () { - this.onClose(); - return this; - }; - - /** - * Generates a configured XHR request - * - * @param {String} url The url that needs to be requested. - * @param {String} method The method the request should use. - * @returns {XMLHttpRequest} - * @api private - */ - - XHR.prototype.request = function (method) { - var req = io.util.request(this.socket.isXDomain()) - , query = io.util.query(this.socket.options.query, 't=' + +new Date); - - req.open(method || 'GET', this.prepareUrl() + query, true); - - if (method == 'POST') { - try { - if (req.setRequestHeader) { - req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); - } else { - // XDomainRequest - req.contentType = 'text/plain'; - } - } catch (e) {} - } - - return req; - }; - - /** - * Returns the scheme to use for the transport URLs. - * - * @api private - */ - - XHR.prototype.scheme = function () { - return this.socket.options.secure ? 'https' : 'http'; - }; - - /** - * Check if the XHR transports are supported - * - * @param {Boolean} xdomain Check if we support cross domain requests. - * @returns {Boolean} - * @api public - */ - - XHR.check = function (socket, xdomain) { - try { - var request = io.util.request(xdomain), - usesXDomReq = (global.XDomainRequest && request instanceof XDomainRequest), - socketProtocol = (socket && socket.options && socket.options.secure ? 'https:' : 'http:'), - isXProtocol = (global.location && socketProtocol != global.location.protocol); - if (request && !(usesXDomReq && isXProtocol)) { - return true; - } - } catch(e) {} - - return false; - }; - - /** - * Check if the XHR transport supports cross domain requests. - * - * @returns {Boolean} - * @api public - */ - - XHR.xdomainCheck = function (socket) { - return XHR.check(socket, true); - }; - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.htmlfile = HTMLFile; - - /** - * The HTMLFile transport creates a `forever iframe` based transport - * for Internet Explorer. Regular forever iframe implementations will - * continuously trigger the browsers buzy indicators. If the forever iframe - * is created inside a `htmlfile` these indicators will not be trigged. - * - * @constructor - * @extends {io.Transport.XHR} - * @api public - */ - - function HTMLFile (socket) { - io.Transport.XHR.apply(this, arguments); - }; - - /** - * Inherits from XHR transport. - */ - - io.util.inherit(HTMLFile, io.Transport.XHR); - - /** - * Transport name - * - * @api public - */ - - HTMLFile.prototype.name = 'htmlfile'; - - /** - * Creates a new Ac...eX `htmlfile` with a forever loading iframe - * that can be used to listen to messages. Inside the generated - * `htmlfile` a reference will be made to the HTMLFile transport. - * - * @api private - */ - - HTMLFile.prototype.get = function () { - this.doc = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); - this.doc.open(); - this.doc.write(''); - this.doc.close(); - this.doc.parentWindow.s = this; - - var iframeC = this.doc.createElement('div'); - iframeC.className = 'socketio'; - - this.doc.body.appendChild(iframeC); - this.iframe = this.doc.createElement('iframe'); - - iframeC.appendChild(this.iframe); - - var self = this - , query = io.util.query(this.socket.options.query, 't='+ +new Date); - - this.iframe.src = this.prepareUrl() + query; - - io.util.on(window, 'unload', function () { - self.destroy(); - }); - }; - - /** - * The Socket.IO server will write script tags inside the forever - * iframe, this function will be used as callback for the incoming - * information. - * - * @param {String} data The message - * @param {document} doc Reference to the context - * @api private - */ - - HTMLFile.prototype._ = function (data, doc) { - // unescape all forward slashes. see GH-1251 - data = data.replace(/\\\//g, '/'); - this.onData(data); - try { - var script = doc.getElementsByTagName('script')[0]; - script.parentNode.removeChild(script); - } catch (e) { } - }; - - /** - * Destroy the established connection, iframe and `htmlfile`. - * And calls the `CollectGarbage` function of Internet Explorer - * to release the memory. - * - * @api private - */ - - HTMLFile.prototype.destroy = function () { - if (this.iframe){ - try { - this.iframe.src = 'about:blank'; - } catch(e){} - - this.doc = null; - this.iframe.parentNode.removeChild(this.iframe); - this.iframe = null; - - CollectGarbage(); - } - }; - - /** - * Disconnects the established connection. - * - * @returns {Transport} Chaining. - * @api public - */ - - HTMLFile.prototype.close = function () { - this.destroy(); - return io.Transport.XHR.prototype.close.call(this); - }; - - /** - * Checks if the browser supports this transport. The browser - * must have an `Ac...eXObject` implementation. - * - * @return {Boolean} - * @api public - */ - - HTMLFile.check = function (socket) { - if (typeof window != "undefined" && (['Active'].concat('Object').join('X')) in window){ - try { - var a = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); - return a && io.Transport.XHR.check(socket); - } catch(e){} - } - return false; - }; - - /** - * Check if cross domain requests are supported. - * - * @returns {Boolean} - * @api public - */ - - HTMLFile.xdomainCheck = function () { - // we can probably do handling for sub-domains, we should - // test that it's cross domain but a subdomain here - return false; - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('htmlfile'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports['xhr-polling'] = XHRPolling; - - /** - * The XHR-polling transport uses long polling XHR requests to create a - * "persistent" connection with the server. - * - * @constructor - * @api public - */ - - function XHRPolling () { - io.Transport.XHR.apply(this, arguments); - }; - - /** - * Inherits from XHR transport. - */ - - io.util.inherit(XHRPolling, io.Transport.XHR); - - /** - * Merge the properties from XHR transport - */ - - io.util.merge(XHRPolling, io.Transport.XHR); - - /** - * Transport name - * - * @api public - */ - - XHRPolling.prototype.name = 'xhr-polling'; - - /** - * Indicates whether heartbeats is enabled for this transport - * - * @api private - */ - - XHRPolling.prototype.heartbeats = function () { - return false; - }; - - /** - * Establish a connection, for iPhone and Android this will be done once the page - * is loaded. - * - * @returns {Transport} Chaining. - * @api public - */ - - XHRPolling.prototype.open = function () { - var self = this; - - io.Transport.XHR.prototype.open.call(self); - return false; - }; - - /** - * Starts a XHR request to wait for incoming messages. - * - * @api private - */ - - function empty () {}; - - XHRPolling.prototype.get = function () { - if (!this.isOpen) return; - - var self = this; - - function stateChange () { - if (this.readyState == 4) { - this.onreadystatechange = empty; - - if (this.status == 200) { - self.onData(this.responseText); - self.get(); - } else { - self.onClose(); - } - } - }; - - function onload () { - this.onload = empty; - this.onerror = empty; - self.retryCounter = 1; - self.onData(this.responseText); - self.get(); - }; - - function onerror () { - self.retryCounter ++; - if(!self.retryCounter || self.retryCounter > 3) { - self.onClose(); - } else { - self.get(); - } - }; - - this.xhr = this.request(); - - if (global.XDomainRequest && this.xhr instanceof XDomainRequest) { - this.xhr.onload = onload; - this.xhr.onerror = onerror; - } else { - this.xhr.onreadystatechange = stateChange; - } - - this.xhr.send(null); - }; - - /** - * Handle the unclean close behavior. - * - * @api private - */ - - XHRPolling.prototype.onClose = function () { - io.Transport.XHR.prototype.onClose.call(this); - - if (this.xhr) { - this.xhr.onreadystatechange = this.xhr.onload = this.xhr.onerror = empty; - try { - this.xhr.abort(); - } catch(e){} - this.xhr = null; - } - }; - - /** - * Webkit based browsers show a infinit spinner when you start a XHR request - * before the browsers onload event is called so we need to defer opening of - * the transport until the onload event is called. Wrapping the cb in our - * defer method solve this. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - XHRPolling.prototype.ready = function (socket, fn) { - var self = this; - - io.util.defer(function () { - fn.call(self); - }); - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('xhr-polling'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - /** - * There is a way to hide the loading indicator in Firefox. If you create and - * remove a iframe it will stop showing the current loading indicator. - * Unfortunately we can't feature detect that and UA sniffing is evil. - * - * @api private - */ - - var indicator = global.document && "MozAppearance" in - global.document.documentElement.style; - - /** - * Expose constructor. - */ - - exports['jsonp-polling'] = JSONPPolling; - - /** - * The JSONP transport creates an persistent connection by dynamically - * inserting a script tag in the page. This script tag will receive the - * information of the Socket.IO server. When new information is received - * it creates a new script tag for the new data stream. - * - * @constructor - * @extends {io.Transport.xhr-polling} - * @api public - */ - - function JSONPPolling (socket) { - io.Transport['xhr-polling'].apply(this, arguments); - - this.index = io.j.length; - - var self = this; - - io.j.push(function (msg) { - self._(msg); - }); - }; - - /** - * Inherits from XHR polling transport. - */ - - io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); - - /** - * Transport name - * - * @api public - */ - - JSONPPolling.prototype.name = 'jsonp-polling'; - - /** - * Posts a encoded message to the Socket.IO server using an iframe. - * The iframe is used because script tags can create POST based requests. - * The iframe is positioned outside of the view so the user does not - * notice it's existence. - * - * @param {String} data A encoded message. - * @api private - */ - - JSONPPolling.prototype.post = function (data) { - var self = this - , query = io.util.query( - this.socket.options.query - , 't='+ (+new Date) + '&i=' + this.index - ); - - if (!this.form) { - var form = document.createElement('form') - , area = document.createElement('textarea') - , id = this.iframeId = 'socketio_iframe_' + this.index - , iframe; - - form.className = 'socketio'; - form.style.position = 'absolute'; - form.style.top = '0px'; - form.style.left = '0px'; - form.style.display = 'none'; - form.target = id; - form.method = 'POST'; - form.setAttribute('accept-charset', 'utf-8'); - area.name = 'd'; - form.appendChild(area); - document.body.appendChild(form); - - this.form = form; - this.area = area; - } - - this.form.action = this.prepareUrl() + query; - - function complete () { - initIframe(); - self.socket.setBuffer(false); - }; - - function initIframe () { - if (self.iframe) { - self.form.removeChild(self.iframe); - } - - try { - // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) - iframe = document.createElement(' -
1. GETTING STARTED
-
2. UP YOUR SKILLZ
-
3. ADVANCED MAPPING
From c0b35280f6e47db9103c5b70c4bd47933ff9db0c Mon Sep 17 00:00:00 2001 From: Robert Best Date: Sat, 22 Oct 2016 02:58:13 -0400 Subject: [PATCH 249/378] Middle.mouse.click features (Open contained link & copy text to clipboard) (#792) * changed the code to be based off of the current dev branch * Update JIT.js * Update Util.js * Update JIT.js A few logical operators were replaced with their stricter counterpart. * Update JIT.js * Update index.js * Update Util.js --- frontend/src/Metamaps/JIT.js | 67 ++++++++++++++++++++---------- frontend/src/Metamaps/Map/index.js | 4 ++ frontend/src/Metamaps/Util.js | 12 ++++++ 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 593cf52e..73c1b600 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -21,6 +21,7 @@ import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' +import clipboard from 'clipboard-js' /* * Metamaps.Erb @@ -410,14 +411,13 @@ const JIT = { Mouse.boxStartCoordinates = null Mouse.boxEndCoordinates = null } - // console.log('called zoom to box') } if (e.shiftKey) { Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) - // console.log('called select with box') + return } } @@ -427,13 +427,10 @@ const JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { JIT.selectEdgeOnClickHandler(node, e) - // console.log('called selectEdgeOnClickHandler') } else if (node && !node.nodeFrom) { JIT.selectNodeOnClickHandler(node, e) - // console.log('called selectNodeOnClickHandler') } else { JIT.canvasClickHandler(eventInfo.getPos(), e) - // console.log('called canvasClickHandler') } // if }, // Add also a click handler to nodes @@ -1333,6 +1330,9 @@ const JIT = { if (Visualize.mGraph.busy) return const self = JIT + + //Copy topic title to clipboard + if(e.button===1 && e.ctrlKey) clipboard.copy(node.name); // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { @@ -1354,25 +1354,45 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - const nodeAlreadySelected = node.selected - - if (!e.shiftKey) { - Control.deselectAllNodes() - Control.deselectAllEdges() - } - - if (nodeAlreadySelected) { - Control.deselectNode(node) + var nodeAlreadySelected = node.selected + + if(e.button!==1){ + if (!e.shiftKey) { + Control.deselectAllNodes() + Control.deselectAllEdges() + } + + if (nodeAlreadySelected) { + Control.deselectNode(node) + } else { + Control.selectNode(node, e) + } + + // trigger animation to final styles + Visualize.mGraph.fx.animate({ + modes: ['edge-property:lineWidth:color:alpha'], + duration: 500 + }) + Visualize.mGraph.plot() + } else { - Control.selectNode(node, e) + if(!e.ctrlKey){ + var len = Selected.Nodes.length; + + for (let i = 0; i < len; i += 1) { + let n = Selected.Nodes[i]; + let result = Metamaps.Util.openLink(Metamaps.Topics.get(n.id).attributes.link); + + if (!result) { //if link failed to open + break; + } + } + + if(!node.selected){ + Metamaps.Util.openLink(Metamaps.Topics.get(node.id).attributes.link); + } + } } - - // trigger animation to final styles - Visualize.mGraph.fx.animate({ - modes: ['edge-property:lineWidth:color:alpha'], - duration: 500 - }) - Visualize.mGraph.plot() } }, Mouse.DOUBLE_CLICK_TOLERANCE) } @@ -1598,6 +1618,9 @@ const JIT = { if (Visualize.mGraph.busy) return const self = JIT + var synapseText = adj.data.$synapses[0].attributes.desc; + //Copy synapse label to clipboard + if(e.button===1 && e.ctrlKey && synapseText !== "") clipboard.copy(synapseText); // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index e5f50633..7aa98cc3 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -44,6 +44,10 @@ const Map = { $('#wrapper').on('contextmenu', function (e) { return false }) + + $('#wrapper').mousedown(function (e){ + if(e.button === 1)return false; + }); $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 445a898c..49797ab4 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -124,6 +124,18 @@ const Util = { checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) }, + openLink: function(url){ + var win = (url !== "") ? window.open(url, '_blank') : "empty"; + + if (win) { + //Browser has allowed it to be opened + return true; + } else { + //Browser has blocked it + alert('Please allow popups in order to open the link'); + return false; + } + }, mdToHTML: text => { // use safe: true to filter xss return new HtmlRenderer({ safe: true }) From bc8660c83e79a0e847de7c50123f4cc267538469 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 22 Oct 2016 03:10:09 -0400 Subject: [PATCH 250/378] remove about lightbox in prep for homepage redo and about page --- app/views/layouts/_lightboxes.html.erb | 61 -------------------------- 1 file changed, 61 deletions(-) diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 2e092151..70d62600 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -7,67 +7,6 @@ + diff --git a/app/views/maps/show.html.erb b/app/views/maps/show.html.erb index ae391d6d..b25c7610 100644 --- a/app/views/maps/show.html.erb +++ b/app/views/maps/show.html.erb @@ -18,5 +18,5 @@ Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>; Metamaps.Messages = <%= @allmessages.to_json.html_safe %>; Metamaps.Stars = <%= @allstars.to_json.html_safe %>; - Metamaps.Visualize.type = "ForceDirected"; + Metamaps.ServerData.VisualizeType = "ForceDirected"; diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index 24739716..4c56dd41 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -117,6 +117,6 @@
diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 7662f47d..8305e097 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,9 +1,10 @@ -/* global Metamaps, $ */ +/* global $ */ import _ from 'lodash' import outdent from 'outdent' import Active from './Active' +import DataModel from './DataModel' import Filter from './Filter' import GlobalUI from './GlobalUI' import JIT from './JIT' @@ -12,16 +13,6 @@ import Selected from './Selected' import Settings from './Settings' import Visualize from './Visualize' -/* - * Metamaps.Control.js - * - * Dependencies: - * - Metamaps.Mappings - * - Metamaps.Metacodes - * - Metamaps.Synapses - * - Metamaps.Topics - */ - const Control = { init: function () {}, selectNode: function (node, e) { @@ -72,7 +63,7 @@ const Control = { Control.deleteSelectedNodes() } - if (Metamaps.Topics.length === 0) { + if (DataModel.Topics.length === 0) { GlobalUI.showDiv('#instructions') } }, @@ -110,7 +101,7 @@ const Control = { var mappableid = topic.id var mapping = node.getData('mapping') topic.destroy() - Metamaps.Mappings.remove(mapping) + DataModel.Mappings.remove(mapping) $(document).trigger(JIT.events.deleteTopic, [{ mappableid: mappableid }]) @@ -127,7 +118,7 @@ const Control = { }) _.each(nodeids, function(nodeid) { if (Active.Topic.id !== nodeid) { - Metamaps.Topics.remove(nodeid) + DataModel.Topics.remove(nodeid) Control.hideNode(nodeid) } }) @@ -165,7 +156,7 @@ const Control = { var mappableid = topic.id var mapping = node.getData('mapping') mapping.destroy() - Metamaps.Topics.remove(topic) + DataModel.Topics.remove(topic) $(document).trigger(JIT.events.removeTopic, [{ mappableid: mappableid }]) @@ -293,7 +284,7 @@ const Control = { synapse.destroy() // the server will destroy the mapping, we just need to remove it here - Metamaps.Mappings.remove(mapping) + DataModel.Mappings.remove(mapping) edge.getData('mappings').splice(index, 1) edge.getData('synapses').splice(index, 1) if (edge.getData('displayIndex')) { @@ -348,7 +339,7 @@ const Control = { var mappableid = synapse.id mapping.destroy() - Metamaps.Synapses.remove(synapse) + DataModel.Synapses.remove(synapse) edge.getData('mappings').splice(index, 1) edge.getData('synapses').splice(index, 1) @@ -432,7 +423,7 @@ const Control = { GlobalUI.notifyUser('Working...') - var metacode = Metamaps.Metacodes.get(metacode_id) + var metacode = DataModel.Metacodes.get(metacode_id) // variables to keep track of how many nodes and synapses you had the ability to change the permission of var nCount = 0 diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1327821f..d12a5de5 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,5 +1,6 @@ -/* global Metamaps, $, Hogan, Bloodhound */ +/* global $, Hogan, Bloodhound */ +import DataModel from './DataModel' import Mouse from './Mouse' import Selected from './Selected' import Synapse from './Synapse' @@ -7,15 +8,6 @@ import Topic from './Topic' import Visualize from './Visualize' import GlobalUI from './GlobalUI' -/* - * Metamaps.Create.js - * - * Dependencies: - * - Metamaps.DataModel - * - Metamaps.Metacodes - * - Metamaps.Topics - */ - const Create = { isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open selectedMetacodeSet: null, @@ -59,7 +51,7 @@ const Create = { } var codesToSwitchToIds - var metacodeModels = new Metamaps.DataModel.MetacodeCollection() + var metacodeModels = new DataModel.MetacodeCollection() Create.selectedMetacodeSetIndex = index Create.selectedMetacodeSet = 'metacodeset-' + set @@ -80,7 +72,7 @@ const Create = { // sort by name for (var i = 0; i < codesToSwitchToIds.length; i++) { - metacodeModels.add(Metamaps.Metacodes.get(codesToSwitchToIds[i])) + metacodeModels.add(DataModel.Metacodes.get(codesToSwitchToIds[i])) } metacodeModels.sort() @@ -243,7 +235,7 @@ const Create = { $('.pinCarousel').removeClass('isPinned') Create.newTopic.pinned = false } - if (Metamaps.Topics.length === 0) { + if (DataModel.Topics.length === 0) { GlobalUI.showDiv('#instructions') } Create.newTopic.beingCreated = false diff --git a/frontend/src/Metamaps/DataModel/MapCollection.js b/frontend/src/Metamaps/DataModel/MapCollection.js index a08309df..69b2f5a8 100644 --- a/frontend/src/Metamaps/DataModel/MapCollection.js +++ b/frontend/src/Metamaps/DataModel/MapCollection.js @@ -1,5 +1,3 @@ -/* global Metamaps */ - import Backbone from 'backbone' Backbone.$ = window.$ diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index b9a3153b..0be1cfda 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -11,10 +11,10 @@ import Realtime from '../Realtime' import SynapseCard from '../SynapseCard' import Visualize from '../Visualize' +import DataModel from './index' + /* - * Dependencies: - * - Metamaps.Mappings - * - Metamaps.Topics + * Metamaps.Erb */ const Synapse = Backbone.Model.extend({ @@ -96,10 +96,10 @@ const Synapse = Backbone.Model.extend({ else return false }, getTopic1: function () { - return Metamaps.Topics.get(this.get('topic1_id')) + return DataModel.Topics.get(this.get('topic1_id')) }, getTopic2: function () { - return Metamaps.Topics.get(this.get('topic2_id')) + return DataModel.Topics.get(this.get('topic2_id')) }, getDirection: function () { var t1 = this.getTopic1() @@ -113,7 +113,7 @@ const Synapse = Backbone.Model.extend({ getMapping: function () { if (!Active.Map) return false - return Metamaps.Mappings.findWhere({ + return DataModel.Mappings.findWhere({ map_id: Active.Map.id, mappable_type: 'Synapse', mappable_id: this.isNew() ? this.cid : this.id diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js index 3d27814e..cd709a24 100644 --- a/frontend/src/Metamaps/DataModel/Topic.js +++ b/frontend/src/Metamaps/DataModel/Topic.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import _ from 'lodash' import Backbone from 'backbone' @@ -11,11 +11,7 @@ import Realtime from '../Realtime' import TopicCard from '../TopicCard' import Visualize from '../Visualize' -/* - * Dependencies: - * - Metamaps.Mappings - * - Metamaps.Metacodes - */ +import DataModel from './index' const Topic = Backbone.Model.extend({ urlRoot: '/topics', @@ -99,12 +95,12 @@ const Topic = Backbone.Model.extend({ }, getDate: function () {}, getMetacode: function () { - return Metamaps.Metacodes.get(this.get('metacode_id')) + return DataModel.Metacodes.get(this.get('metacode_id')) }, getMapping: function () { if (!Active.Map) return false - return Metamaps.Mappings.findWhere({ + return DataModel.Mappings.findWhere({ map_id: Active.Map.id, mappable_type: 'Topic', mappable_id: this.isNew() ? this.cid : this.id diff --git a/frontend/src/Metamaps/DataModel/index.js b/frontend/src/Metamaps/DataModel/index.js index bd9cc342..c973cb3b 100644 --- a/frontend/src/Metamaps/DataModel/index.js +++ b/frontend/src/Metamaps/DataModel/index.js @@ -1,5 +1,3 @@ -/* global Metamaps */ - import Active from '../Active' import Filter from '../Filter' import { InfoBox } from '../Map' @@ -19,19 +17,6 @@ import SynapseCollection from './SynapseCollection' import Mapping from './Mapping' import MappingCollection from './MappingCollection' -/* - * DataModel.js - * - * Dependencies: - * - Metamaps.Collaborators - * - Metamaps.Creators - * - Metamaps.Mappers - * - Metamaps.Mappings - * - Metamaps.Metacodes - * - Metamaps.Synapses - * - Metamaps.Topics - */ - const DataModel = { Map: Map, MapCollection: MapCollection, @@ -48,45 +33,50 @@ const DataModel = { Mapping: Mapping, MappingCollection: MappingCollection, - Metacodes: new MetacodeCollection(), - Topics: new TopicCollection(), - Synapses: new SynapseCollection(), - Mappings: new MappingCollection(), - Mappers: new MapperCollection(), Collaborators: new MapperCollection(), Creators: new MapperCollection(), + Mappers: new MapperCollection(), + Mappings: new MappingCollection(), + Messages: [], + Metacodes: new MetacodeCollection(), + Stars: [], + Synapses: new SynapseCollection(), + Topics: new TopicCollection(), init: function (serverData) { var self = DataModel + if (serverData.Collaborators) self.Collaborators = new MapperCollection(serverData.Collaborators) + if (serverData.Creators) self.Creators = new MapperCollection(serverData.Creators) + if (serverData.Mappers) self.Mappers = new MapperCollection(serverData.Mappers) + if (serverData.Mappings) self.Mappings = new MappingCollection(serverData.Mappings) + if (serverData.Messages) self.Messages = serverData.Messages if (serverData.Metacodes) self.Metacodes = new MetacodeCollection(serverData.Metacodes) - - // attach collection event listeners + if (serverData.Stars) self.Stars = serverData.Stars + if (serverData.Synapses) self.Synapses = new SynapseCollection(serverData.Synapses) if (serverData.Topics) self.Topics = new TopicCollection(serverData.Topics) + + self.attachCollectionEvents() + }, + + attachCollectionEvents: function () { + var self = DataModel self.Topics.on('add remove', function (topic) { InfoBox.updateNumbers() Filter.checkMetacodes() Filter.checkMappers() }) - - if (serverData.Synapses) self.Synapses = new SynapseCollection(serverData.Synapses) self.Synapses.on('add remove', function (synapse) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMappers() }) - - if (serverData.Mappings) self.Mappings = new MappingCollection(serverData.Mappings) self.Mappings.on('add remove', function (mapping) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMetacodes() Filter.checkMappers() }) - - if (serverData.Mappers) self.Mappers = new MapperCollection(serverData.Mappers) - if (serverData.Collaborators) self.Collaborators = new MapperCollection(serverData.Collaborators) - if (serverData.Creators) self.Creators = new MapperCollection(serverData.Creators) } } diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 59aa1bae..271e5758 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,23 +1,14 @@ -/* global Metamaps, $ */ +/* global $ */ import _ from 'lodash' import Active from './Active' import Control from './Control' +import DataModel from './DataModel' import GlobalUI from './GlobalUI' import Settings from './Settings' import Visualize from './Visualize' -/* - * Metamaps.Filter.js.erb - * - * Dependencies: - * - Metamaps.Creators - * - Metamaps.Mappers - * - Metamaps.Metacodes - * - Metamaps.Synapses - * - Metamaps.Topics - */ const Filter = { filters: { name: '', @@ -148,7 +139,7 @@ const Filter = { // the first option enables us to accept // ['Topics', 'Synapses'] as 'collection' if (typeof collection === 'object') { - Metamaps[collection[0]].each(function (model) { + DataModel[collection[0]].each(function (model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -157,7 +148,7 @@ const Filter = { } } }) - Metamaps[collection[1]].each(function (model) { + DataModel[collection[1]].each(function (model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -167,7 +158,7 @@ const Filter = { } }) } else if (typeof collection === 'string') { - Metamaps[collection].each(function (model) { + DataModel[collection].each(function (model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -196,8 +187,8 @@ const Filter = { } // for each new filter to be added, create a list item for it and fade it in _.each(added, function (identifier) { - model = Metamaps[correlatedModel].get(identifier) || - Metamaps[correlatedModel].find(function (model) { + model = DataModel[correlatedModel].get(identifier) || + DataModel[correlatedModel].find(function (model) { return model.get(propertyToCheck) === identifier }) li = model.prepareLiForFilter() @@ -359,7 +350,7 @@ const Filter = { var opacityForFilter = onMap ? 0 : 0.4 - Metamaps.Topics.each(function (topic) { + DataModel.Topics.each(function (topic) { var n = topic.get('node') var metacode_id = topic.get('metacode_id').toString() @@ -400,11 +391,11 @@ const Filter = { }) // flag all the edges back to 'untouched' - Metamaps.Synapses.each(function (synapse) { + DataModel.Synapses.each(function (synapse) { var e = synapse.get('edge') e.setData('touched', false) }) - Metamaps.Synapses.each(function (synapse) { + DataModel.Synapses.each(function (synapse) { var e = synapse.get('edge') var desc var user_id = synapse.get('user_id').toString() diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js index d53b4106..97110c91 100644 --- a/frontend/src/Metamaps/GlobalUI/CreateMap.js +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -1,13 +1,9 @@ -/* global Metamaps, $ */ +/* global $ */ import Active from '../Active' +import DataModel from '../DataModel' import GlobalUI from './index' -/* - * Metamaps.DataModel - * Metamaps.Maps - */ - const CreateMap = { newMap: null, emptyMapForm: '', @@ -17,7 +13,7 @@ const CreateMap = { init: function () { var self = CreateMap - self.newMap = new Metamaps.DataModel.Map({ permission: 'commons' }) + self.newMap = new DataModel.Map({ permission: 'commons' }) self.bindFormEvents() @@ -109,7 +105,7 @@ const CreateMap = { success: function (model) { var self = CreateMap // push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model) + DataModel.Maps.Mine.add(model) GlobalUI.clearNotify() $('#wrapper').append(self.generateSuccessMessage(model.id)) @@ -128,7 +124,7 @@ const CreateMap = { } self.bindFormEvents() - self.newMap = new Metamaps.DataModel.Map({ permission: 'commons' }) + self.newMap = new DataModel.Map({ permission: 'commons' }) return false } diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index 82293ac3..eb5fe0a7 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -5,7 +5,6 @@ import Router from '../Router' /* * Metamaps.Erb - * Metamaps.Maps */ const Search = { diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index e82516c2..2444925a 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -1,20 +1,16 @@ -/* global Metamaps, $ */ +/* global $ */ import clipboard from 'clipboard-js' import Active from '../Active' import Create from '../Create' +import DataModel from '../DataModel' import Search from './Search' import CreateMap from './CreateMap' import Account from './Account' import ImportDialog from './ImportDialog' -/* - * Metamaps.DataModel - * Metamaps.Maps - */ - const GlobalUI = { notifyTimeout: null, lightbox: null, @@ -38,26 +34,26 @@ const GlobalUI = { $('#lightbox_screen, #lightbox_close').click(self.closeLightbox) // initialize global backbone models and collections - if (Active.Mapper) Active.Mapper = new Metamaps.DataModel.Mapper(Active.Mapper) + if (Active.Mapper) Active.Mapper = new DataModel.Mapper(Active.Mapper) - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : [] - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : [] - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : [] + var myCollection = DataModel.Maps.Mine ? DataModel.Maps.Mine : [] + var sharedCollection = DataModel.Maps.Shared ? DataModel.Maps.Shared : [] + var starredCollection = DataModel.Maps.Starred ? DataModel.Maps.Starred : [] var mapperCollection = [] var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } - if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id + if (DataModel.Maps.Mapper) { + mapperCollection = DataModel.Maps.Mapper.models + mapperOptionsObj.mapperId = DataModel.Maps.Mapper.id } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : [] - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : [] - Metamaps.Maps.Mine = new Metamaps.DataModel.MapCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) - Metamaps.Maps.Shared = new Metamaps.DataModel.MapCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) - Metamaps.Maps.Starred = new Metamaps.DataModel.MapCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) + var featuredCollection = DataModel.Maps.Featured ? DataModel.Maps.Featured : [] + var activeCollection = DataModel.Maps.Active ? DataModel.Maps.Active : [] + DataModel.Maps.Mine = new DataModel.MapCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) + DataModel.Maps.Shared = new DataModel.MapCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) + DataModel.Maps.Starred = new DataModel.MapCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.DataModel.MapCollection(mapperCollection, mapperOptionsObj) - Metamaps.Maps.Featured = new Metamaps.DataModel.MapCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) - Metamaps.Maps.Active = new Metamaps.DataModel.MapCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) + DataModel.Maps.Mapper = new DataModel.MapCollection(mapperCollection, mapperOptionsObj) + DataModel.Maps.Featured = new DataModel.MapCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) + DataModel.Maps.Active = new DataModel.MapCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) }, showDiv: function (selector) { $(selector).show() diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index b829c0b1..d273edbb 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,26 +1,16 @@ -/* global Metamaps, $ */ +/* global $ */ import parse from 'csv-parse' import _ from 'lodash' import Active from './Active' import AutoLayout from './AutoLayout' +import DataModel from './DataModel' import GlobalUI from './GlobalUI' import Map from './Map' import Synapse from './Synapse' import Topic from './Topic' -/* - * Metamaps.Import.js.erb - * - * Dependencies: - * - Metamaps.DataModel - * - Metamaps.Mappings - * - Metamaps.Metacodes - * - Metamaps.Synapses - * - Metamaps.Topics - */ - const Import = { // note that user is not imported topicWhitelist: [ @@ -255,10 +245,10 @@ const Import = { parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted // if there isn't a cidMapping, check by topic name instead - var topic1 = Metamaps.Topics.get(self.cidMappings[synapse.topic1]) - if (!topic1) topic1 = Metamaps.Topics.findWhere({ name: synapse.topic1 }) - var topic2 = Metamaps.Topics.get(self.cidMappings[synapse.topic2]) - if (!topic2) topic2 = Metamaps.Topics.findWhere({ name: synapse.topic2 }) + var topic1 = DataModel.Topics.get(self.cidMappings[synapse.topic1]) + if (!topic1) topic1 = DataModel.Topics.findWhere({ name: synapse.topic1 }) + var topic2 = DataModel.Topics.get(self.cidMappings[synapse.topic2]) + if (!topic2) topic2 = DataModel.Topics.findWhere({ name: synapse.topic2 }) if (!topic1 || !topic2) { console.error("One of the two topics doesn't exist!") @@ -291,15 +281,15 @@ const Import = { link, xloc, yloc, import_id, opts = {}) { var self = Import $(document).trigger(Map.events.editedByActiveMapper) - var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null + var metacode = DataModel.Metacodes.where({name: metacode_name})[0] || null if (metacode === null) { - metacode = Metamaps.Metacodes.where({ name: 'Wildcard' })[0] + metacode = DataModel.Metacodes.where({ name: 'Wildcard' })[0] console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') } var topic_permission = permission || Active.Map.get('permission') var defer_to_map_id = permission === topic_permission ? Active.Map.get('id') : null - var topic = new Metamaps.DataModel.Topic({ + var topic = new DataModel.Topic({ name: name, metacode_id: metacode.id, permission: topic_permission, @@ -308,19 +298,19 @@ const Import = { link: link || '', calculated_permission: Active.Map.get('permission') }) - Metamaps.Topics.add(topic) + DataModel.Topics.add(topic) if (import_id !== null && import_id !== undefined) { self.cidMappings[import_id] = topic.cid } - var mapping = new Metamaps.DataModel.Mapping({ + var mapping = new DataModel.Mapping({ xloc: xloc, yloc: yloc, mappable_id: topic.cid, mappable_type: 'Topic' }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) // this function also includes the creation of the topic in the database Topic.renderTopic(mapping, topic, true, true, { @@ -340,20 +330,20 @@ const Import = { return } // if - var synapse = new Metamaps.DataModel.Synapse({ + var synapse = new DataModel.Synapse({ desc: desc || '', category: category || 'from-to', permission: permission, topic1_id: topic1.id, topic2_id: topic2.id }) - Metamaps.Synapses.add(synapse) + DataModel.Synapses.add(synapse) - var mapping = new Metamaps.DataModel.Mapping({ + var mapping = new DataModel.Mapping({ mappable_type: 'Synapse', mappable_id: synapse.cid }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) Synapse.renderSynapse(mapping, synapse, node1, node2, true) }, diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index f8bc51e8..e57244c0 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -8,6 +8,7 @@ import $jit from '../patched/JIT' import Active from './Active' import Control from './Control' import Create from './Create' +import DataModel from './DataModel' import Filter from './Filter' import GlobalUI from './GlobalUI' import Map from './Map' @@ -25,10 +26,6 @@ import clipboard from 'clipboard-js' /* * Metamaps.Erb - * Metamaps.Mappings - * Metamaps.Metacodes - * Metamaps.Synapses - * Metamaps.Topics */ let panningInt @@ -140,15 +137,15 @@ const JIT = { self.vizData = [] Visualize.loadLater = false - const results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) + const results = self.convertModelsToJIT(DataModel.Topics, DataModel.Synapses) self.vizData = results[0] // clean up the synapses array in case of any faulty data _.each(results[1], function (synapse) { mapping = synapse.getMapping() - Metamaps.Synapses.remove(synapse) - if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping) + DataModel.Synapses.remove(synapse) + if (DataModel.Mappings) DataModel.Mappings.remove(mapping) }) // set up addTopic instructions in case they delete all the topics @@ -1191,7 +1188,7 @@ const JIT = { eY = -1 * eY const edgesToToggle = [] - Metamaps.Synapses.each(function (synapse) { + DataModel.Synapses.each(function (synapse) { const e = synapse.get('edge') if (edgesToToggle.indexOf(e) === -1) { edgesToToggle.push(e) @@ -1579,15 +1576,15 @@ const JIT = { loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - const topics = Metamaps.Topics.map(function (t) { return t.id }) - const topicsString = topics.join() + const topics = DataModel.Topics.map(function (t) { return t.id }) + const topics_string = topics.join() const successCallback = function (data) { $('#loadingSiblings').remove() for (var key in data) { - const string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' - $('#fetchSiblingList').append('
  • ' + string + '
  • ') + const string = `${DataModel.Metacodes.get(key).get('name')} (${data[key]})` + $('#fetchSiblingList').append(`
  • ${string}
  • `) } $('.rc-siblings .getSiblings').click(function () { diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 79fa6c4d..95455fd4 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -3,17 +3,13 @@ import outdent from 'outdent' import Active from '../Active' +import DataModel from '../DataModel' import GlobalUI from '../GlobalUI' import Router from '../Router' import Util from '../Util' /* - * Metamaps.Collaborators * Metamaps.Erb - * Metamaps.Mappers - * Metamaps.Maps - * Metamaps.Synapses - * Metamaps.Topics */ const InfoBox = { @@ -106,7 +102,7 @@ const InfoBox = { var isCreator = map.authorizePermissionChange(Active.Mapper) var canEdit = map.authorizeToEdit(Active.Mapper) - var relevantPeople = map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var relevantPeople = map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators var shareable = map.get('permission') !== 'private' obj['name'] = canEdit ? Hogan.compile(self.nameHTML).render({id: map.id, name: map.get('name')}) : map.get('name') @@ -251,24 +247,24 @@ const InfoBox = { }, removeCollaborator: function (collaboratorId) { var self = InfoBox - Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) - var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + DataModel.Collaborators.remove(DataModel.Collaborators.get(collaboratorId)) + var mapperIds = DataModel.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, addCollaborator: function (newCollaboratorId) { var self = InfoBox - if (Metamaps.Collaborators.get(newCollaboratorId)) { + if (DataModel.Collaborators.get(newCollaboratorId)) { GlobalUI.notifyUser('That user already has access') return } function callback(mapper) { - Metamaps.Collaborators.add(mapper) - var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) + DataModel.Collaborators.add(mapper) + var mapperIds = DataModel.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) - var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + var name = DataModel.Collaborators.get(newCollaboratorId).get('name') GlobalUI.notifyUser(name + ' will be notified by email') self.updateNumbers() } @@ -289,7 +285,7 @@ const InfoBox = { }, createContributorList: function () { var self = InfoBox - var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' string += '
      ' @@ -315,7 +311,7 @@ const InfoBox = { var self = InfoBox var mapper = Active.Mapper - var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators var contributors_class = '' if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' @@ -333,8 +329,8 @@ const InfoBox = { $('.mapContributors .tip').unbind().click(function (event) { event.stopPropagation() }) - $('.mapTopics').text(Metamaps.Topics.length) - $('.mapSynapses').text(Metamaps.Synapses.length) + $('.mapTopics').text(DataModel.Topics.length) + $('.mapSynapses').text(DataModel.Synapses.length) $('.mapEditedAt').html('Last edited: ' + Util.nowDateFormatted()) }, @@ -388,10 +384,10 @@ const InfoBox = { if (doIt && authorized) { InfoBox.close() - Metamaps.Maps.Active.remove(map) - Metamaps.Maps.Featured.remove(map) - Metamaps.Maps.Mine.remove(map) - Metamaps.Maps.Shared.remove(map) + DataModel.Maps.Active.remove(map) + DataModel.Maps.Featured.remove(map) + DataModel.Maps.Mine.remove(map) + DataModel.Maps.Shared.remove(map) map.destroy() Router.home() GlobalUI.notifyUser('Map eliminated!') diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 6bd4f00a..7ff806a5 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,10 +1,12 @@ -/* global Metamaps, $ */ +/* global $ */ import outdent from 'outdent' import Active from '../Active' import AutoLayout from '../AutoLayout' import Create from '../Create' +import DataModel from '../DataModel' +import DataModelMap from '../DataModel/Map' import Filter from '../Filter' import GlobalUI from '../GlobalUI' import JIT from '../JIT' @@ -23,14 +25,7 @@ import InfoBox from './InfoBox' * Metamaps.Map.js.erb * * Dependencies: - * - Metamaps.DataModel * - Metamaps.Erb - * - Metamaps.Mappers - * - Metamaps.Mappings - * - Metamaps.Maps - * - Metamaps.Messages - * - Metamaps.Synapses - * - Metamaps.Topics */ const Map = { @@ -83,15 +78,15 @@ const Map = { }, launch: function (id) { var start = function (data) { - Active.Map = new Metamaps.DataModel.Map(data.map) - Metamaps.Mappers = new Metamaps.DataModel.MapperCollection(data.mappers) - Metamaps.Collaborators = new Metamaps.DataModel.MapperCollection(data.collaborators) - Metamaps.Topics = new Metamaps.DataModel.TopicCollection(data.topics) - Metamaps.Synapses = new Metamaps.DataModel.SynapseCollection(data.synapses) - Metamaps.Mappings = new Metamaps.DataModel.MappingCollection(data.mappings) - Metamaps.Messages = data.messages - Metamaps.Stars = data.stars - Metamaps.DataModel.attachCollectionEvents() + Active.Map = new DataModelMap(data.map) + DataModel.Mappers = new DataModel.MapperCollection(data.mappers) + DataModel.Collaborators = new DataModel.MapperCollection(data.collaborators) + DataModel.Topics = new DataModel.TopicCollection(data.topics) + DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) + DataModel.Mappings = new DataModel.MappingCollection(data.mappings) + DataModel.Messages = data.messages + DataModel.Stars = data.stars + DataModel.attachCollectionEvents() var map = Active.Map var mapper = Active.Mapper @@ -164,9 +159,9 @@ const Map = { } }, updateStar: function () { - if (!Active.Mapper || !Metamaps.Stars) return + if (!Active.Mapper || !DataModel.Stars) return // update the star/unstar icon - if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { + if (DataModel.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { $('.starMap').addClass('starred') $('.starMap .tooltipsAbove').html('Unstar') } else { @@ -179,8 +174,8 @@ const Map = { if (!Active.Map) return $.post('/maps/' + Active.Map.id + '/star') - Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) - Metamaps.Maps.Starred.add(Active.Map) + DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) + DataModel.Maps.Starred.add(Active.Map) GlobalUI.notifyUser('Map is now starred') self.updateStar() }, @@ -189,8 +184,8 @@ const Map = { if (!Active.Map) return $.post('/maps/' + Active.Map.id + '/unstar') - Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) - Metamaps.Maps.Starred.remove(Active.Map) + DataModel.Stars = DataModel.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + DataModel.Maps.Starred.remove(Active.Map) self.updateStar() }, fork: function () { @@ -218,7 +213,7 @@ const Map = { } }) // collect the unfiltered synapses - Metamaps.Synapses.each(function (synapse) { + DataModel.Synapses.each(function (synapse) { var desc = synapse.get('desc') var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 @@ -239,8 +234,8 @@ const Map = { }, leavePrivateMap: function () { var map = Active.Map - Metamaps.Maps.Active.remove(map) - Metamaps.Maps.Featured.remove(map) + DataModel.Maps.Active.remove(map) + DataModel.Maps.Featured.remove(map) Router.home() GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, @@ -259,7 +254,7 @@ const Map = { }, editedByActiveMapper: function () { if (Active.Mapper) { - Metamaps.Mappers.add(Active.Mapper) + DataModel.Mappers.add(Active.Mapper) } }, exportImage: function () { diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 4beff8bf..03919378 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,9 +1,6 @@ /* global $ */ -/* - * Dependencies: - * Metamaps.DataModel - */ +import DataModel from './DataModel' const Mapper = { // this function is to retrieve a mapper JSON object from the database @@ -12,7 +9,7 @@ const Mapper = { $.ajax({ url: `/users/${id}.json`, success: data => { - callback(new Metamaps.DataModel.Mapper(data)) + callback(new DataModel.Mapper(data)) } }) } diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js index c1432333..4ae838ab 100644 --- a/frontend/src/Metamaps/Realtime/receivable.js +++ b/frontend/src/Metamaps/Realtime/receivable.js @@ -302,9 +302,10 @@ export const newMapper = self => data => { var notifyMessage = data.username + ' just joined the map' if (firstOtherPerson) { - notifyMessage += ' ' + notifyMessage += ' ' } GlobalUI.notifyUser(notifyMessage) + $('#toast button').click(e => self.inviteACall(data.userid)) self.sendMapperInfo(data.userid) } } @@ -335,9 +336,11 @@ export const invitedToCall = self => inviter => { var username = self.mappersOnMap[inviter].name var notifyText = '' notifyText += username + ' is inviting you to a conversation. Join live?' - notifyText += ' ' - notifyText += ' ' + notifyText += ' ' + notifyText += ' ' GlobalUI.notifyUser(notifyText, true) + $('#toast button.yes').click(e => self.acceptCall(inviter)) + $('#toast button.no').click(e => self.denyCall(inviter)) } export const invitedToJoin = self => inviter => { @@ -346,9 +349,11 @@ export const invitedToJoin = self => inviter => { var username = self.mappersOnMap[inviter].name var notifyText = username + ' is inviting you to the conversation. Join?' - notifyText += ' ' - notifyText += ' ' + notifyText += ' ' + notifyText += ' ' GlobalUI.notifyUser(notifyText, true) + $('#toast button.yes').click(e => self.joinCall()) + $('#toast button.no').click(e => self.denyInvite(inviter)) } export const mapperJoinedCall = self => id => { @@ -385,18 +390,24 @@ export const mapperLeftCall = self => id => { export const callInProgress = self => () => { var notifyText = "There's a conversation happening, want to join?" - notifyText += ' ' - notifyText += ' ' + notifyText += ' ' + notifyText += ' ' GlobalUI.notifyUser(notifyText, true) + $('#toast button.yes').click(e => self.joinCall()) + $('#toast button.no').click(e => GlobalUI.clearNotify()) + self.room.conversationInProgress() } export const callStarted = self => () => { if (self.inConversation) return var notifyText = "There's a conversation starting, want to join?" - notifyText += ' ' - notifyText += ' ' + notifyText += ' ' + notifyText += ' ' GlobalUI.notifyUser(notifyText, true) + $('#toast button.yes').click(e => self.joinCall()) + $('#toast button.no').click(e => GlobalUI.clearNotify()) + self.room.conversationInProgress() } diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index f55fab48..9046c112 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,8 +1,7 @@ -/* global Metamaps, $ */ +/* global $ */ import Backbone from 'backbone' -//TODO is this line good or bad? -//Backbone.$ = window.$ +Backbone.$ = window.$ import Active from './Active' import GlobalUI from './GlobalUI' @@ -12,13 +11,6 @@ import Topic from './Topic' import Views from './Views' import Visualize from './Visualize' -/* - * Metamaps.Router.js.erb - * - * Dependencies: - * - Metamaps.Maps - */ - const _Router = Backbone.Router.extend({ currentPage: '', currentSection: '', @@ -57,10 +49,10 @@ const _Router = Backbone.Router.extend({ GlobalUI.showDiv('#explore') - Views.ExploreMaps.setCollection(Metamaps.Maps.Active) - if (Metamaps.Maps.Active.length === 0) { + Views.ExploreMaps.setCollection(DataModel.Maps.Active) + if (DataModel.Maps.Active.length === 0) { Views.ExploreMaps.pending = true - Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render + DataModel.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { Views.ExploreMaps.render(navigate) } @@ -109,23 +101,23 @@ const _Router = Backbone.Router.extend({ // this will mean it's a mapper page being loaded if (id) { - if (Metamaps.Maps.Mapper.mapperId !== id) { + if (DataModel.Maps.Mapper.mapperId !== id) { // empty the collection if we are trying to load the maps // collection of a different mapper than we had previously - Metamaps.Maps.Mapper.reset() - Metamaps.Maps.Mapper.page = 1 + DataModel.Maps.Mapper.reset() + DataModel.Maps.Mapper.page = 1 } - Metamaps.Maps.Mapper.mapperId = id + DataModel.Maps.Mapper.mapperId = id } - Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) var navigate = function () { var path = '/explore/' + self.currentPage // alter url if for mapper profile page if (self.currentPage === 'mapper') { - path += '/' + Metamaps.Maps.Mapper.mapperId + path += '/' + DataModel.Maps.Mapper.mapperId } self.navigate(path) @@ -133,11 +125,11 @@ const _Router = Backbone.Router.extend({ var navigateTimeout = function () { self.timeoutId = setTimeout(navigate, 300) } - if (Metamaps.Maps[capitalize].length === 0) { + if (DataModel.Maps[capitalize].length === 0) { Loading.show() Views.ExploreMaps.pending = true setTimeout(function () { - Metamaps.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render + DataModel.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { if (id) { diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 4b80c05a..824aad4b 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import Active from './Active' import Control from './Control' @@ -9,36 +9,24 @@ import Selected from './Selected' import Settings from './Settings' import Visualize from './Visualize' -/* - * Metamaps.Synapse.js.erb - * - * Dependencies: - * - Metamaps.DataModel - * - Metamaps.Mappings - * - Metamaps.Synapses - * - Metamaps.Topics - */ - const noOp = () => {} + const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it - if (Metamaps.Synapses.get(id) == undefined) { + if (DataModel.Synapses.get(id) == undefined) { $.ajax({ url: '/synapses/' + id + '.json', success: function (data) { - Metamaps.Synapses.add(data) - callback(Metamaps.Synapses.get(id)) + DataModel.Synapses.add(data) + callback(DataModel.Synapses.get(id)) } }) - } else callback(Metamaps.Synapses.get(id)) + } else callback(DataModel.Synapses.get(id)) }, - /* - * - * - */ + renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { var self = Synapse @@ -98,12 +86,12 @@ const Synapse = { // for each node in this array we will create a synapse going to the position2 node. var synapsesToCreate = [] - topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) + topic2 = DataModel.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') var len = Selected.Nodes.length if (len == 0) { - topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) + topic1 = DataModel.Topics.get(Create.newSynapse.topic1id) synapsesToCreate[0] = topic1.get('node') } else if (len > 0) { synapsesToCreate = Selected.Nodes @@ -112,18 +100,18 @@ const Synapse = { for (var i = 0; i < synapsesToCreate.length; i++) { node1 = synapsesToCreate[i] topic1 = node1.getData('topic') - synapse = new Metamaps.DataModel.Synapse({ + synapse = new DataModel.Synapse({ desc: Create.newSynapse.description, topic1_id: topic1.isNew() ? topic1.cid : topic1.id, topic2_id: topic2.isNew() ? topic2.cid : topic2.id, }) - Metamaps.Synapses.add(synapse) + DataModel.Synapses.add(synapse) - mapping = new Metamaps.DataModel.Mapping({ + mapping = new DataModel.Mapping({ mappable_type: 'Synapse', mappable_id: synapse.cid, }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) // this function also includes the creation of the synapse in the database self.renderSynapse(mapping, synapse, node1, node2, true) @@ -139,14 +127,14 @@ const Synapse = { node2 self.get(id, synapse => { - var mapping = new Metamaps.DataModel.Mapping({ + var mapping = new DataModel.Mapping({ mappable_type: 'Synapse', mappable_id: synapse.id, }) - Metamaps.Mappings.add(mapping) - topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) + DataModel.Mappings.add(mapping) + topic1 = DataModel.Topics.get(Create.newSynapse.topic1id) node1 = topic1.get('node') - topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) + topic2 = DataModel.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') Create.newSynapse.hide() self.renderSynapse(mapping, synapse, node1, node2, true) diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 38596b9e..05cd54f1 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,10 +1,11 @@ -/* global Metamaps, $ */ +/* global $ */ import $jit from '../patched/JIT' import Active from './Active' import AutoLayout from './AutoLayout' import Create from './Create' +import DataModel from './DataModel' import Filter from './Filter' import GlobalUI from './GlobalUI' import JIT from './JIT' @@ -17,41 +18,30 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' - -/* - * Metamaps.Topic.js.erb - * - * Dependencies: - * - Metamaps.DataModel - * - Metamaps.Creators - * - Metamaps.Mappings - * - Metamaps.Synapses - * - Metamaps.Topics - */ const noOp = () => {} + const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it - if (Metamaps.Topics.get(id) == undefined) { + if (DataModel.Topics.get(id) == undefined) { $.ajax({ url: '/topics/' + id + '.json', success: function (data) { - Metamaps.Topics.add(data) - callback(Metamaps.Topics.get(id)) + DataModel.Topics.add(data) + callback(DataModel.Topics.get(id)) } }) - } else callback(Metamaps.Topics.get(id)) + } else callback(DataModel.Topics.get(id)) }, launch: function (id) { - var bb = Metamaps.DataModel var start = function (data) { - Active.Topic = new bb.Topic(data.topic) - Metamaps.Creators = new bb.MapperCollection(data.creators) - Metamaps.Topics = new bb.TopicCollection([data.topic].concat(data.relatives)) - Metamaps.Synapses = new bb.SynapseCollection(data.synapses) - Metamaps.DataModel.attachCollectionEvents() + Active.Topic = new DataModel.Topic(data.topic) + DataModel.Creators = new DataModel.MapperCollection(data.creators) + DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives)) + DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) + DataModel.attachCollectionEvents() document.title = Active.Topic.get('name') + ' | Metamaps' @@ -101,7 +91,7 @@ const Topic = { } }) Router.navigate('/topics/' + nodeid) - Active.Topic = Metamaps.Topics.get(nodeid) + Active.Topic = DataModel.Topics.get(nodeid) } }, fetchRelatives: function (nodes, metacode_id) { @@ -109,10 +99,10 @@ const Topic = { var node = $.isArray(nodes) ? nodes[0] : nodes - var topics = Metamaps.Topics.map(function (t) { return t.id }) + var topics = DataModel.Topics.map(function (t) { return t.id }) var topics_string = topics.join() - var creators = Metamaps.Creators.map(function (t) { return t.id }) + var creators = DataModel.Creators.map(function (t) { return t.id }) var creators_string = creators.join() var topic = node.getData('topic') @@ -124,13 +114,13 @@ const Topic = { window.setTimeout(function() { successCallback(data) }, 100) return } - if (data.creators.length > 0) Metamaps.Creators.add(data.creators) - if (data.topics.length > 0) Metamaps.Topics.add(data.topics) - if (data.synapses.length > 0) Metamaps.Synapses.add(data.synapses) + if (data.creators.length > 0) DataModel.Creators.add(data.creators) + if (data.topics.length > 0) DataModel.Topics.add(data.topics) + if (data.synapses.length > 0) DataModel.Synapses.add(data.synapses) - var topicColl = new Metamaps.DataModel.TopicCollection(data.topics) + var topicColl = new DataModel.TopicCollection(data.topics) topicColl.add(topic) - var synapseColl = new Metamaps.DataModel.SynapseCollection(data.synapses) + var synapseColl = new DataModel.SynapseCollection(data.synapses) var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] Visualize.mGraph.op.sum(graph, { @@ -142,7 +132,7 @@ const Topic = { var i, l, t, s Visualize.mGraph.graph.eachNode(function (n) { - t = Metamaps.Topics.get(n.id) + t = DataModel.Topics.get(n.id) t.set({ node: n }, { silent: true }) t.updateNode() @@ -152,7 +142,7 @@ const Topic = { l = edge.getData('synapseIDs').length for (i = 0; i < l; i++) { - s = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + s = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) s.set({ edge: edge }, { silent: true }) s.updateEdge() } @@ -309,25 +299,25 @@ const Topic = { $(document).trigger(Map.events.editedByActiveMapper) - var metacode = Metamaps.Metacodes.get(Create.newTopic.metacode) + var metacode = DataModel.Metacodes.get(Create.newTopic.metacode) - var topic = new Metamaps.DataModel.Topic({ + var topic = new DataModel.Topic({ name: Create.newTopic.name, metacode_id: metacode.id, defer_to_map_id: Active.Map.id }) - Metamaps.Topics.add(topic) + DataModel.Topics.add(topic) if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + var nextCoords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) } - var mapping = new Metamaps.DataModel.Mapping({ + var mapping = new DataModel.Mapping({ xloc: nextCoords ? nextCoords.x : Create.newTopic.x, yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_id: topic.cid, mappable_type: 'Topic', }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above if (!Create.newTopic.pinned) Create.newTopic.hide() @@ -348,15 +338,15 @@ const Topic = { self.get(id, (topic) => { if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + var nextCoords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) } - var mapping = new Metamaps.Backbone.Mapping({ + var mapping = new DataModel.Mapping({ xloc: nextCoords ? nextCoords.x : Create.newTopic.x, yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_type: 'Topic', mappable_id: topic.id, }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) self.renderTopic(mapping, topic, true, true) // this blocked the enterKeyHandler from creating a new topic as well @@ -364,11 +354,11 @@ const Topic = { }) }, getMapFromAutocomplete: function (data) { - var self = Metamaps.Topic + var self = Topic - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) - var metacode = Metamaps.Metacodes.findWhere({ name: 'Metamap' }) + var metacode = DataModel.Metacodes.findWhere({ name: 'Metamap' }) var topic = new Metamaps.Backbone.Topic({ name: data.name, metacode_id: metacode.id, @@ -377,13 +367,13 @@ const Topic = { }) Metamaps.Topics.add(topic) - var mapping = new Metamaps.DataModel.Mapping({ - xloc: Metamaps.Create.newTopic.x, - yloc: Metamaps.Create.newTopic.y, + var mapping = new DataModel.Mapping({ + xloc: Create.newTopic.x, + yloc: Create.newTopic.y, mappable_id: topic.cid, mappable_type: 'Topic', }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above if (!Create.newTopic.pinned) Create.newTopic.hide() @@ -399,14 +389,14 @@ const Topic = { $(document).trigger(Map.events.editedByActiveMapper) self.get(id, (topic) => { - var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) - var mapping = new Metamaps.DataModel.Mapping({ + var nextCoords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) + var mapping = new DataModel.Mapping({ xloc: nextCoords.x, yloc: nextCoords.y, mappable_type: 'Topic', mappable_id: topic.id, }) - Metamaps.Mappings.add(mapping) + DataModel.Mappings.add(mapping) self.renderTopic(mapping, topic, true, true) GlobalUI.notifyUser('Topic was added to your map!') }) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 936e1d2d..dd172971 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,18 +1,13 @@ -/* global Metamaps, $, CanvasLoader, Countable, Hogan, embedly */ +/* global $, CanvasLoader, Countable, Hogan, embedly */ import Active from './Active' +import DataModel from './DataModel' import GlobalUI from './GlobalUI' import Mapper from './Mapper' import Router from './Router' import Util from './Util' import Visualize from './Visualize' -/* - * Metamaps.TopicCard.js - * - * Dependencies: - * - Metamaps.Metacodes - */ const TopicCard = { openTopicCard: null, // stores the topic that's currently open authorizedToEdit: false, // stores boolean for edit permission for open topic card @@ -182,7 +177,7 @@ const TopicCard = { var metacodeLiClick = function () { selectingMetacode = false var metacodeId = parseInt($(this).attr('data-id')) - var metacode = Metamaps.Metacodes.get(metacodeId) + var metacode = DataModel.Metacodes.get(metacodeId) $('.CardOnGraph').find('.metacodeTitle').html(metacode.get('name')) .append('
      ') .attr('class', 'metacodeTitle mbg' + metacode.id) diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 52d47ff3..3f69fe8c 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index 9bbd5e31..ff8975d2 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import Backbone from 'backbone' import attachMediaStream from 'attachmediastream' @@ -7,16 +7,12 @@ import attachMediaStream from 'attachmediastream' // Backbone.$ = window.$ import Active from '../Active' +import DataModel from '../DataModel' import Realtime from '../Realtime' import ChatView from './ChatView' import VideoView from './VideoView' -/* - * Dependencies: - * Metamaps.DataModel - */ - const Room = function(opts) { var self = this @@ -170,14 +166,14 @@ Room.prototype.init = function () { var self = this //this.roomRef.child('messages').push(data) if (self.chat.alertSound) self.chat.sound.play('sendchat') - var m = new Metamaps.DataModel.Message({ + var m = new DataModel.Message({ message: data.message, resource_id: Active.Map.id, resource_type: "Map" }) m.save(null, { success: function (model, response) { - self.addMessages(new Metamaps.DataModel.MessageCollection(model), false, true) + self.addMessages(new DataModel.MessageCollection(model), false, true) $(document).trigger(Room.events.newMessage, [model]) }, error: function (model, response) { diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index c5323972..720ef598 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,24 +1,16 @@ -/* global Metamaps, $ */ +/* global $ */ import _ from 'lodash' import $jit from '../patched/JIT' import Active from './Active' +import DataModel from './DataModel' import JIT from './JIT' import Loading from './Loading' import Router from './Router' import TopicCard from './TopicCard' -/* - * Metamaps.Visualize - * - * Dependencies: - * - Metamaps.Metacodes - * - Metamaps.Synapses - * - Metamaps.Topics - */ - const Visualize = { mGraph: null, // a reference to the graph object. cameraPosition: null, // stores the camera position when using a 3D visualization @@ -27,6 +19,9 @@ const Visualize = { touchDragNode: null, init: function () { var self = Visualize + + if (serverData.VisualizeType) self.type = serverData.VisualizeType + // disable awkward dragging of the canvas element that would sometimes happen $('#infovis-canvas').on('dragstart', function (event) { event.preventDefault() @@ -59,7 +54,7 @@ const Visualize = { var i, l, startPos, endPos, topic, synapse self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id) + topic = DataModel.Topics.get(n.id) topic.set({ node: n }, { silent: true }) topic.updateNode() @@ -69,7 +64,7 @@ const Visualize = { l = edge.getData('synapseIDs').length for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) synapse.set({ edge: edge }, { silent: true }) synapse.updateEdge() } @@ -84,7 +79,7 @@ const Visualize = { var i, l, startPos, endPos, topic, synapse self.mGraph.graph.eachNode(function (n) { - topic = Metamaps.Topics.get(n.id) + topic = DataModel.Topics.get(n.id) topic.set({ node: n }, { silent: true }) topic.updateNode() mapping = topic.getMapping() @@ -95,7 +90,7 @@ const Visualize = { l = edge.getData('synapseIDs').length for (i = 0; i < l; i++) { - synapse = Metamaps.Synapses.get(edge.getData('synapseIDs')[i]) + synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) synapse.set({ edge: edge }, { silent: true }) synapse.updateEdge() } @@ -189,12 +184,12 @@ const Visualize = { // hold for a maximum of 80 passes, or 4 seconds of waiting time var tries = 0 function hold () { - var unique = _.uniq(Metamaps.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), + var unique = _.uniq(DataModel.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), loadedCount = 0 _.each(requiredMetacodes, function (metacode_id) { - var metacode = Metamaps.Metacodes.get(metacode_id), + var metacode = DataModel.Metacodes.get(metacode_id), img = metacode ? metacode.get('image') : false if (img && (img.complete || (typeof img.naturalWidth !== 'undefined' && img.naturalWidth !== 0))) { From 518782e1c7c4dd304b9d0092d9d0c8fe649a6b23 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 2 Oct 2016 22:37:38 +0800 Subject: [PATCH 319/378] remove Metamaps.Erb --- .../javascripts/Metamaps.ServerData.js.erb | 18 ++++++++++++++ app/assets/javascripts/application.js | 2 +- .../javascripts/src/Metamaps.Erb.js.erb | 24 ------------------- frontend/src/Metamaps/Account.js | 20 ++++++---------- frontend/src/Metamaps/DataModel/Synapse.js | 8 ++----- frontend/src/Metamaps/GlobalUI/Search.js | 10 +++----- frontend/src/Metamaps/JIT.js | 10 +++----- frontend/src/Metamaps/Map/InfoBox.js | 12 ++++------ frontend/src/Metamaps/Map/index.js | 7 ------ frontend/src/Metamaps/Views/ChatView.js | 9 ++----- frontend/src/patched/JIT.js | 2 +- 11 files changed, 41 insertions(+), 81 deletions(-) create mode 100644 app/assets/javascripts/Metamaps.ServerData.js.erb delete mode 100644 app/assets/javascripts/src/Metamaps.Erb.js.erb diff --git a/app/assets/javascripts/Metamaps.ServerData.js.erb b/app/assets/javascripts/Metamaps.ServerData.js.erb new file mode 100644 index 00000000..86c8d2b9 --- /dev/null +++ b/app/assets/javascripts/Metamaps.ServerData.js.erb @@ -0,0 +1,18 @@ +/* global Metamaps */ + +/* erb variables from rails */ +Metamaps.ServerData = Metamaps.ServerData || {} +Metamaps.ServerData['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' +Metamaps.ServerData['user.png'] = '<%= asset_path('user.png') %>' +Metamaps.ServerData['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' +Metamaps.ServerData['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' +Metamaps.ServerData['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' +Metamaps.ServerData['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.ServerData['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' +Metamaps.ServerData['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' +Metamaps.ServerData.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> +Metamaps.ServerData.REALTIME_SERVER = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.ServerData.RAILS_ENV = '<%= ENV['RAILS_ENV'] %>' +Metamaps.ServerData.VERSION = '<%= METAMAPS_VERSION %>' +Metamaps.ServerData.BUILD = '<%= METAMAPS_BUILD %>' +Metamaps.ServerData.LAST_UPDATED = '<%= METAMAPS_LAST_UPDATED %>' diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 051edc8b..d669012a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,5 +14,5 @@ //= require jquery-ui //= require jquery_ujs //= require_directory ./lib -//= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle +//= require ./Metamaps.ServerData diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb deleted file mode 100644 index 57f26c98..00000000 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ /dev/null @@ -1,24 +0,0 @@ -/* global Metamaps */ - -/* - * Metamaps.Erb.js.erb - */ - -/* erb variables from rails */ -window.Metamaps = window.Metamaps || {} -Metamaps.Erb = {} -Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' -Metamaps.Erb['RAILS_ENV'] = '<%= ENV['RAILS_ENV'] %>' -Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' -Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' -Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' -Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' -Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' -Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' -Metamaps.Erb['import-example.png'] = '<%= asset_path('import-example.png') %>' -Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' -Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' -Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> -Metamaps.VERSION = '<%= METAMAPS_VERSION %>' -Metamaps.BUILD = '<%= METAMAPS_BUILD %>' -Metamaps.LAST_UPDATED = '<%= METAMAPS_LAST_UPDATED %>' diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 1ac87811..15b6f30b 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,17 +1,17 @@ /* global $, CanvasLoader */ -/* - * Metamaps.Erb - */ - const Account = { listenersInitialized: false, + userIconUrl: null, initListeners: function () { var self = Account $('#user_image').change(self.showImagePreview) self.listenersInitialized = true }, + init: function (serverData) { + Account.userIconUrl = serverData['user.png'] + }, toggleChangePicture: function () { var self = Account @@ -25,24 +25,18 @@ const Account = { if (!self.listenersInitialized) self.initListeners() }, closeChangePicture: function () { - var self = Account - $('.userImageMenu').hide() }, showLoading: function () { - var self = Account - var loader = new CanvasLoader('accountPageLoading') - loader.setColor('#4FC059'); // default is '#000000' + loader.setColor('#4FC059') // default is '#000000' loader.setDiameter(28) // default is 40 loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 + loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default $('#accountPageLoading').show() }, showImagePreview: function () { - var self = Account - var file = $('#user_image')[0].files[0] var reader = new window.FileReader() @@ -90,7 +84,7 @@ const Account = { var self = Account $('.userImageDiv canvas').remove() - $('.userImageDiv img').attr('src', window.Metamaps.Erb['user.png']).show() + $('.userImageDiv img').attr('src', self.userIconUrl).show() $('.userImageMenu').hide() var input = $('#user_image') diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index 0be1cfda..09f75743 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import _ from 'lodash' import Backbone from 'backbone' @@ -13,10 +13,6 @@ import Visualize from '../Visualize' import DataModel from './index' -/* - * Metamaps.Erb - */ - const Synapse = Backbone.Model.extend({ urlRoot: '/synapses', blacklist: ['edge', 'created_at', 'updated_at'], @@ -82,7 +78,7 @@ const Synapse = Backbone.Model.extend({ prepareLiForFilter: function () { var li = '' li += '
    • ' - li += '
    • ' return li diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index eb5fe0a7..1d4b82e4 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -1,12 +1,8 @@ -/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ +/* global $, Hogan, Bloodhound, CanvasLoader */ import Active from '../Active' import Router from '../Router' -/* - * Metamaps.Erb - */ - const Search = { locked: false, isOpen: false, @@ -54,7 +50,7 @@ const Search = { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ value: 'No results', label: 'No results', - typeImageURL: Metamaps.Erb['icons/wildcard.png'], + typeImageURL: Metamaps.ServerData['icons/wildcard.png'], rtype: 'noresult' }) }, @@ -122,7 +118,7 @@ const Search = { value: 'No results', label: 'No results', rtype: 'noresult', - profile: Metamaps.Erb['user.png'] + profile: Metamaps.ServerData['user.png'] }) }, header: mapperheader, diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index e57244c0..cbcd131b 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,4 @@ -/* global Metamaps, $, Image, CanvasLoader */ +/* global $, Image, CanvasLoader */ import _ from 'lodash' import outdent from 'outdent' @@ -24,10 +24,6 @@ import Util from './Util' import Visualize from './Visualize' import clipboard from 'clipboard-js' -/* - * Metamaps.Erb - */ - let panningInt const JIT = { @@ -69,10 +65,10 @@ const JIT = { $('.takeScreenshot').click(Map.exportImage) self.topicDescImage = new Image() - self.topicDescImage.src = Metamaps.Erb['topic_description_signifier.png'] + self.topicDescImage.src = Metamaps.ServerData['topic_description_signifier.png'] self.topicLinkImage = new Image() - self.topicLinkImage.src = Metamaps.Erb['topic_link_signifier.png'] + self.topicLinkImage.src = Metamaps.ServerData['topic_link_signifier.png'] }, /** * convert our topic JSON into something JIT can use diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 95455fd4..a3e31f19 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,4 +1,4 @@ -/* global Metamaps, $, Hogan, Bloodhound, Countable */ +/* global $, Hogan, Bloodhound, Countable */ import outdent from 'outdent' @@ -8,10 +8,6 @@ import GlobalUI from '../GlobalUI' import Router from '../Router' import Util from '../Util' -/* - * Metamaps.Erb - */ - const InfoBox = { isOpen: false, changing: false, @@ -112,7 +108,7 @@ const InfoBox = { obj['contributor_count'] = relevantPeople.length obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : '' obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : '' - obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : Metamaps.Erb['user.png'] + obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : Metamaps.ServerData['user.png'] obj['contributor_list'] = self.createContributorList() obj['user_name'] = isCreator ? 'You' : map.get('user_name') @@ -214,7 +210,7 @@ const InfoBox = { value: "No results", label: "No results", rtype: "noresult", - profile: Metamaps.Erb['user.png'], + profile: Metamaps.ServerData['user.png'], }); }, suggestion: function(s) { @@ -317,7 +313,7 @@ const InfoBox = { if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' else if (relevantPeople.length > 2) contributors_class = 'multiple' - var contributors_image = Metamaps.Erb['user.png'] + var contributors_image = Metamaps.ServerData['user.png'] if (relevantPeople.length > 0) { // get the first contributor and use their image contributors_image = relevantPeople.models[0].get('image') diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 7ff806a5..578c1f9f 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -21,13 +21,6 @@ import Visualize from '../Visualize' import CheatSheet from './CheatSheet' import InfoBox from './InfoBox' -/* - * Metamaps.Map.js.erb - * - * Dependencies: - * - Metamaps.Erb - */ - const Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 8febe9e1..fe33ac68 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,9 +1,4 @@ -/* global Metamaps, $ */ - -/* - * Dependencies: - * Metamaps.Erb - */ +/* global $ */ import Backbone from 'backbone' import { Howl } from 'howler' @@ -128,7 +123,7 @@ var Private = { }, initializeSounds: function () { this.sound = new Howl({ - src: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], + src: [Metamaps.ServerData['sounds/MM_sounds.mp3'], Metamaps.ServerData['sounds/MM_sounds.ogg']], sprite: { joinmap: [0, 561], leavemap: [1000, 592], diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index d3cac135..a41f1c65 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -3261,7 +3261,7 @@ var Canvas; ctx = base.getCtx(), scale = base.scaleOffsetX; //var pattern = new Image(); - //pattern.src = Metamaps.Erb['cubes.png'] + //pattern.src = Metamaps.ServerData['cubes.png'] //var ptrn = ctx.createPattern(pattern, 'repeat'); //ctx.fillStyle = ptrn; ctx.fillStyle = Metamaps.Settings.colors.background; From 2f4fe525ce584ef2d472d5159c210e2369509e1f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 3 Oct 2016 08:32:37 +0800 Subject: [PATCH 320/378] Fix a bunch of errors and style issues --- app/views/explore/active.html.erb | 2 +- app/views/explore/featured.html.erb | 2 +- app/views/explore/mapper.html.erb | 2 +- app/views/explore/mine.html.erb | 2 +- app/views/explore/shared.html.erb | 2 +- app/views/explore/starred.html.erb | 2 +- app/views/maps/show.html.erb | 4 +- frontend/src/Metamaps/Account.js | 6 +- frontend/src/Metamaps/Active.js | 11 +--- frontend/src/Metamaps/DataModel/Map.js | 4 +- .../src/Metamaps/DataModel/MapCollection.js | 2 +- frontend/src/Metamaps/DataModel/Mapper.js | 2 +- .../Metamaps/DataModel/MapperCollection.js | 2 +- frontend/src/Metamaps/DataModel/Mapping.js | 2 +- .../Metamaps/DataModel/MappingCollection.js | 2 +- frontend/src/Metamaps/DataModel/Message.js | 2 +- .../Metamaps/DataModel/MessageCollection.js | 2 +- frontend/src/Metamaps/DataModel/Metacode.js | 2 +- .../Metamaps/DataModel/MetacodeCollection.js | 2 +- frontend/src/Metamaps/DataModel/Synapse.js | 14 ++--- .../Metamaps/DataModel/SynapseCollection.js | 2 +- frontend/src/Metamaps/DataModel/Topic.js | 2 +- .../src/Metamaps/DataModel/TopicCollection.js | 2 +- frontend/src/Metamaps/DataModel/index.js | 51 ++++++++++++++-- frontend/src/Metamaps/GlobalUI/CreateMap.js | 29 ++++++---- frontend/src/Metamaps/GlobalUI/Search.js | 9 ++- frontend/src/Metamaps/GlobalUI/index.js | 34 ++--------- frontend/src/Metamaps/Import.js | 6 +- frontend/src/Metamaps/JIT.js | 20 ++----- frontend/src/Metamaps/Loading.js | 17 +++--- frontend/src/Metamaps/Map/InfoBox.js | 13 +++-- frontend/src/Metamaps/Map/index.js | 6 +- frontend/src/Metamaps/Router.js | 3 +- frontend/src/Metamaps/Synapse.js | 1 + frontend/src/Metamaps/Topic.js | 6 +- frontend/src/Metamaps/TopicCard.js | 58 ++++++++++--------- frontend/src/Metamaps/Views/ChatView.js | 6 +- frontend/src/Metamaps/Views/Room.js | 3 +- frontend/src/Metamaps/Views/index.js | 2 + frontend/src/Metamaps/Visualize.js | 2 +- frontend/src/Metamaps/index.js | 20 ++++--- frontend/src/patched/JIT.js | 2 + package.json | 1 + webpack.config.js | 11 +++- 44 files changed, 201 insertions(+), 174 deletions(-) diff --git a/app/views/explore/active.html.erb b/app/views/explore/active.html.erb index fd9ebe68..e6e2a3a6 100644 --- a/app/views/explore/active.html.erb +++ b/app/views/explore/active.html.erb @@ -10,6 +10,6 @@ Metamaps.currentSection = "explore"; Metamaps.currentPage = "active"; - Metamaps.Maps.Active = <%= @maps.to_json.html_safe %>; + Metamaps.ServerData.Active = <%= @maps.to_json.html_safe %>; Metamaps.GlobalUI.Search.focus(); diff --git a/app/views/explore/featured.html.erb b/app/views/explore/featured.html.erb index 15ab97fc..9396ebdc 100644 --- a/app/views/explore/featured.html.erb +++ b/app/views/explore/featured.html.erb @@ -10,6 +10,6 @@ Metamaps.currentSection = "explore"; Metamaps.currentPage = "featured"; - Metamaps.Maps.Featured = <%= @maps.to_json.html_safe %>; + Metamaps.ServerData.Featured = <%= @maps.to_json.html_safe %>; Metamaps.GlobalUI.Search.focus(); diff --git a/app/views/explore/mapper.html.erb b/app/views/explore/mapper.html.erb index 7669b808..bf3e73c0 100644 --- a/app/views/explore/mapper.html.erb +++ b/app/views/explore/mapper.html.erb @@ -10,7 +10,7 @@ Metamaps.currentSection = "explore"; Metamaps.currentPage = "mapper"; - Metamaps.Maps.Mapper = { + Metamaps.ServerData.Mapper = { models: <%= @maps.to_json.html_safe %>, id: <%= params[:id] %> }; diff --git a/app/views/explore/mine.html.erb b/app/views/explore/mine.html.erb index 8bf2da6f..21f034c3 100644 --- a/app/views/explore/mine.html.erb +++ b/app/views/explore/mine.html.erb @@ -10,6 +10,6 @@ Metamaps.currentPage = "mine"; Metamaps.currentSection = "explore"; - Metamaps.Maps.Mine = <%= @maps.to_json.html_safe %>; + Metamaps.ServerData.Mine = <%= @maps.to_json.html_safe %>; Metamaps.GlobalUI.Search.focus(); diff --git a/app/views/explore/shared.html.erb b/app/views/explore/shared.html.erb index a47757ec..0f01fcf0 100644 --- a/app/views/explore/shared.html.erb +++ b/app/views/explore/shared.html.erb @@ -10,6 +10,6 @@ Metamaps.currentPage = "shared"; Metamaps.currentSection = "explore"; - Metamaps.Maps.Shared = <%= @maps.to_json.html_safe %>; + Metamaps.ServerData.Shared = <%= @maps.to_json.html_safe %>; Metamaps.GlobalUI.Search.focus(); diff --git a/app/views/explore/starred.html.erb b/app/views/explore/starred.html.erb index 83c8de13..125c3e76 100644 --- a/app/views/explore/starred.html.erb +++ b/app/views/explore/starred.html.erb @@ -10,6 +10,6 @@ Metamaps.currentPage = "starred"; Metamaps.currentSection = "explore"; - Metamaps.Maps.Starred = <%= @maps.to_json.html_safe %>; + Metamaps.ServerData.Starred = <%= @maps.to_json.html_safe %>; Metamaps.GlobalUI.Search.focus(); diff --git a/app/views/maps/show.html.erb b/app/views/maps/show.html.erb index b25c7610..4ecbd274 100644 --- a/app/views/maps/show.html.erb +++ b/app/views/maps/show.html.erb @@ -16,7 +16,7 @@ Metamaps.ServerData.Topics = <%= @alltopics.to_json(user: current_user).html_safe %>; Metamaps.ServerData.Synapses = <%= @allsynapses.to_json.html_safe %>; Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>; - Metamaps.Messages = <%= @allmessages.to_json.html_safe %>; - Metamaps.Stars = <%= @allstars.to_json.html_safe %>; + Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>; + Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>; Metamaps.ServerData.VisualizeType = "ForceDirected"; diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 15b6f30b..b7e1fe79 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,6 +1,9 @@ /* global $, CanvasLoader */ const Account = { + init: function (serverData) { + Account.userIconUrl = serverData['user.png'] + }, listenersInitialized: false, userIconUrl: null, initListeners: function () { @@ -9,9 +12,6 @@ const Account = { $('#user_image').change(self.showImagePreview) self.listenersInitialized = true }, - init: function (serverData) { - Account.userIconUrl = serverData['user.png'] - }, toggleChangePicture: function () { var self = Account diff --git a/frontend/src/Metamaps/Active.js b/frontend/src/Metamaps/Active.js index fe8bda6d..ddd44152 100644 --- a/frontend/src/Metamaps/Active.js +++ b/frontend/src/Metamaps/Active.js @@ -1,16 +1,7 @@ -import DataModelMap from './DataModel/Map' -import DataModelMapper from './DataModel/Mapper' -import DataModelTopic from './DataModel/Topic' - const Active = { Map: null, Mapper: null, - Topic: null, - init: function(serverData) { - if (serverData.Map) Active.Map = new DataModelMap(severData.ActiveMap) - if (serverData.Mapper) Active.Mapper = new DataModelMapper(serverData.ActiveMapper) - if (serverData.Topic) Active.Topic = new DataModelTopic(serverData.ActiveTopic) - } + Topic: null } export default Active diff --git a/frontend/src/Metamaps/DataModel/Map.js b/frontend/src/Metamaps/DataModel/Map.js index 3ae47d1a..2ab98575 100644 --- a/frontend/src/Metamaps/DataModel/Map.js +++ b/frontend/src/Metamaps/DataModel/Map.js @@ -2,10 +2,10 @@ import _ from 'lodash' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Active from '../Active' -import { InfoBox } from '../Map' +import InfoBox from '../Map/InfoBox' import Mapper from '../Mapper' import Realtime from '../Realtime' diff --git a/frontend/src/Metamaps/DataModel/MapCollection.js b/frontend/src/Metamaps/DataModel/MapCollection.js index 69b2f5a8..22bbcf7b 100644 --- a/frontend/src/Metamaps/DataModel/MapCollection.js +++ b/frontend/src/Metamaps/DataModel/MapCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Loading from '../Loading' diff --git a/frontend/src/Metamaps/DataModel/Mapper.js b/frontend/src/Metamaps/DataModel/Mapper.js index 3627fbd6..39e83503 100644 --- a/frontend/src/Metamaps/DataModel/Mapper.js +++ b/frontend/src/Metamaps/DataModel/Mapper.js @@ -1,6 +1,6 @@ import _ from 'lodash' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import outdent from 'outdent' const Mapper = Backbone.Model.extend({ diff --git a/frontend/src/Metamaps/DataModel/MapperCollection.js b/frontend/src/Metamaps/DataModel/MapperCollection.js index e0ce2bed..836ee48f 100644 --- a/frontend/src/Metamaps/DataModel/MapperCollection.js +++ b/frontend/src/Metamaps/DataModel/MapperCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Mapper from './Mapper' diff --git a/frontend/src/Metamaps/DataModel/Mapping.js b/frontend/src/Metamaps/DataModel/Mapping.js index 8bf92ed2..282c419f 100644 --- a/frontend/src/Metamaps/DataModel/Mapping.js +++ b/frontend/src/Metamaps/DataModel/Mapping.js @@ -1,6 +1,6 @@ import _ from 'lodash' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Active from '../Active' import Map from '../Map' diff --git a/frontend/src/Metamaps/DataModel/MappingCollection.js b/frontend/src/Metamaps/DataModel/MappingCollection.js index e475e098..9a69f56b 100644 --- a/frontend/src/Metamaps/DataModel/MappingCollection.js +++ b/frontend/src/Metamaps/DataModel/MappingCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Mapping from './Mapping' diff --git a/frontend/src/Metamaps/DataModel/Message.js b/frontend/src/Metamaps/DataModel/Message.js index f7dc9bee..00f1cf4b 100644 --- a/frontend/src/Metamaps/DataModel/Message.js +++ b/frontend/src/Metamaps/DataModel/Message.js @@ -1,6 +1,6 @@ import _ from 'lodash' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} const Message = Backbone.Model.extend({ urlRoot: '/messages', diff --git a/frontend/src/Metamaps/DataModel/MessageCollection.js b/frontend/src/Metamaps/DataModel/MessageCollection.js index a572c212..3ce440eb 100644 --- a/frontend/src/Metamaps/DataModel/MessageCollection.js +++ b/frontend/src/Metamaps/DataModel/MessageCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Message from './Message' diff --git a/frontend/src/Metamaps/DataModel/Metacode.js b/frontend/src/Metamaps/DataModel/Metacode.js index f9bd06cf..fbd13755 100644 --- a/frontend/src/Metamaps/DataModel/Metacode.js +++ b/frontend/src/Metamaps/DataModel/Metacode.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import outdent from 'outdent' const Metacode = Backbone.Model.extend({ diff --git a/frontend/src/Metamaps/DataModel/MetacodeCollection.js b/frontend/src/Metamaps/DataModel/MetacodeCollection.js index ff4626d1..03c41613 100644 --- a/frontend/src/Metamaps/DataModel/MetacodeCollection.js +++ b/frontend/src/Metamaps/DataModel/MetacodeCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Metacode from './Metacode' diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index 09f75743..a60611a3 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -1,8 +1,9 @@ /* global $ */ import _ from 'lodash' +import outdent from 'outdent' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Active from '../Active' import Filter from '../Filter' @@ -76,12 +77,11 @@ const Synapse = Backbone.Model.extend({ this.on('change:desc', Filter.checkSynapses, this) }, prepareLiForFilter: function () { - var li = '' - li += '
    • ' - li += '
    • ' - return li + return outdent` +
    • + synapse icon +

      ${this.get('desc')}

      +
    • ` }, authorizeToEdit: function (mapper) { if (mapper && (this.get('calculated_permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true diff --git a/frontend/src/Metamaps/DataModel/SynapseCollection.js b/frontend/src/Metamaps/DataModel/SynapseCollection.js index 86bf8c47..8d315353 100644 --- a/frontend/src/Metamaps/DataModel/SynapseCollection.js +++ b/frontend/src/Metamaps/DataModel/SynapseCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Synapse from './Synapse' diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js index cd709a24..d8426c92 100644 --- a/frontend/src/Metamaps/DataModel/Topic.js +++ b/frontend/src/Metamaps/DataModel/Topic.js @@ -2,7 +2,7 @@ import _ from 'lodash' import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Active from '../Active' import Filter from '../Filter' diff --git a/frontend/src/Metamaps/DataModel/TopicCollection.js b/frontend/src/Metamaps/DataModel/TopicCollection.js index 4bcaf622..a1de51d6 100644 --- a/frontend/src/Metamaps/DataModel/TopicCollection.js +++ b/frontend/src/Metamaps/DataModel/TopicCollection.js @@ -1,5 +1,5 @@ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Topic from './Topic' diff --git a/frontend/src/Metamaps/DataModel/index.js b/frontend/src/Metamaps/DataModel/index.js index c973cb3b..4e62113d 100644 --- a/frontend/src/Metamaps/DataModel/index.js +++ b/frontend/src/Metamaps/DataModel/index.js @@ -37,6 +37,17 @@ const DataModel = { Creators: new MapperCollection(), Mappers: new MapperCollection(), Mappings: new MappingCollection(), + Maps: { + Mine: [], + Shared: [], + Starred: [], + Mapper: { + models: [], + mapperId: null + }, + Featured: [], + Active: [] + }, Messages: [], Metacodes: new MetacodeCollection(), Stars: [], @@ -46,6 +57,15 @@ const DataModel = { init: function (serverData) { var self = DataModel + // workaround circular import problem + if (!self.MapCollection.model) self.MapCollection.model = Map + + self.synapseIconUrl = serverData['synapse16.png'] + + if (serverData.ActiveMap) Active.Map = new Map(serverData.ActiveMap) + if (serverData.ActiveMapper) Active.Mapper = new Mapper(serverData.ActiveMapper) + if (serverData.ActiveTopic) Active.Topic = new Topic(serverData.ActiveTopic) + if (serverData.Collaborators) self.Collaborators = new MapperCollection(serverData.Collaborators) if (serverData.Creators) self.Creators = new MapperCollection(serverData.Creators) if (serverData.Mappers) self.Mappers = new MapperCollection(serverData.Mappers) @@ -56,22 +76,43 @@ const DataModel = { if (serverData.Synapses) self.Synapses = new SynapseCollection(serverData.Synapses) if (serverData.Topics) self.Topics = new TopicCollection(serverData.Topics) + // initialize global backbone models and collections + if (Active.Mapper) Active.Mapper = new self.Mapper(Active.Mapper) + + var myCollection = serverData.Mine ? serverData.Mine : [] + var sharedCollection = serverData.Shared ? serverData.Shared : [] + var starredCollection = serverData.Starred ? serverData.Starred : [] + var mapperCollection = [] + var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } + if (self.Maps.Mapper.mapperId) { + mapperCollection = serverData.Mapper.models + mapperOptionsObj.mapperId = serverData.Mapper.id + } + var featuredCollection = serverData.Featured ? serverData.Featured : [] + var activeCollection = serverData.Active ? serverData.Active : [] + + self.Maps.Mine = new MapCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) + self.Maps.Shared = new MapCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) + self.Maps.Starred = new MapCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) + // 'Mapper' refers to another mapper + self.Maps.Mapper = new MapCollection(mapperCollection, mapperOptionsObj) + self.Maps.Featured = new MapCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) + self.Maps.Active = new MapCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) + self.attachCollectionEvents() }, - attachCollectionEvents: function () { - var self = DataModel - self.Topics.on('add remove', function (topic) { + DataModel.Topics.on('add remove', function (topic) { InfoBox.updateNumbers() Filter.checkMetacodes() Filter.checkMappers() }) - self.Synapses.on('add remove', function (synapse) { + DataModel.Synapses.on('add remove', function (synapse) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMappers() }) - self.Mappings.on('add remove', function (mapping) { + DataModel.Mappings.on('add remove', function (mapping) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMetacodes() diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js index 97110c91..11c8deb9 100644 --- a/frontend/src/Metamaps/GlobalUI/CreateMap.js +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -1,7 +1,10 @@ /* global $ */ +import outdent from 'outdent' + import Active from '../Active' import DataModel from '../DataModel' +import DataModelMap from '../DataModel/Map' import GlobalUI from './index' const CreateMap = { @@ -13,7 +16,7 @@ const CreateMap = { init: function () { var self = CreateMap - self.newMap = new DataModel.Map({ permission: 'commons' }) + self.newMap = new DataModelMap({ permission: 'commons' }) self.bindFormEvents() @@ -40,15 +43,6 @@ const CreateMap = { $(this).remove() }) }, - generateSuccessMessage: function (id) { - var stringStart = "
      SUCCESS!
      Your map has been created. Do you want to: Go to your new map" - stringStart += "ORStay on this " - var page = Active.Map ? 'map' : 'page' - var stringEnd = '
      ' - return stringStart + page + stringEnd - }, switchPermission: function () { var self = CreateMap @@ -108,7 +102,20 @@ const CreateMap = { DataModel.Maps.Mine.add(model) GlobalUI.clearNotify() - $('#wrapper').append(self.generateSuccessMessage(model.id)) + $('#wrapper').append(outdent` +
      +
      SUCCESS!
      + Your map has been created. Do you want to: + Go to your new map + OR + Stay on this ${Active.Map ? 'map' : 'page'} +
      + `) + $('#mapGo').click(e => GlobalUI.CreateMap.closeSuccess()) + $('#mapStay').click(e => { + GlobalUI.CreateMap.closeSuccess() + return false + }) }, reset: function (id) { var self = CreateMap diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index 1d4b82e4..0859b941 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -10,9 +10,12 @@ const Search = { limitMapsToMe: false, changing: false, optionsInitialized: false, - init: function () { + init: function (serverData) { var self = Search + self.wildcardIconUrl = serverData['icons/wildcard.png'] + self.userIconUrl = serverData['user.png'] + // this is similar to Metamaps.Loading, but it's for the search element var loader = new CanvasLoader('searchLoading') loader.setColor('#4fb5c0') // default is '#000000' @@ -50,7 +53,7 @@ const Search = { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ value: 'No results', label: 'No results', - typeImageURL: Metamaps.ServerData['icons/wildcard.png'], + typeImageURL: self.wildcardIconUrl, rtype: 'noresult' }) }, @@ -118,7 +121,7 @@ const Search = { value: 'No results', label: 'No results', rtype: 'noresult', - profile: Metamaps.ServerData['user.png'] + profile: self.userIconUrl }) }, header: mapperheader, diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 2444925a..d1a0b6ba 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -2,9 +2,7 @@ import clipboard from 'clipboard-js' -import Active from '../Active' import Create from '../Create' -import DataModel from '../DataModel' import Search from './Search' import CreateMap from './CreateMap' @@ -14,13 +12,13 @@ import ImportDialog from './ImportDialog' const GlobalUI = { notifyTimeout: null, lightbox: null, - init: function () { + init: function (serverData) { var self = GlobalUI - self.Search.init() - self.CreateMap.init() - self.Account.init() - self.ImportDialog.init(Metamaps.Erb, self.openLightbox, self.closeLightbox) + self.Search.init(serverData) + self.CreateMap.init(serverData) + self.Account.init(serverData) + self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox) if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) @@ -32,28 +30,6 @@ const GlobalUI = { }) $('#lightbox_screen, #lightbox_close').click(self.closeLightbox) - - // initialize global backbone models and collections - if (Active.Mapper) Active.Mapper = new DataModel.Mapper(Active.Mapper) - - var myCollection = DataModel.Maps.Mine ? DataModel.Maps.Mine : [] - var sharedCollection = DataModel.Maps.Shared ? DataModel.Maps.Shared : [] - var starredCollection = DataModel.Maps.Starred ? DataModel.Maps.Starred : [] - var mapperCollection = [] - var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } - if (DataModel.Maps.Mapper) { - mapperCollection = DataModel.Maps.Mapper.models - mapperOptionsObj.mapperId = DataModel.Maps.Mapper.id - } - var featuredCollection = DataModel.Maps.Featured ? DataModel.Maps.Featured : [] - var activeCollection = DataModel.Maps.Active ? DataModel.Maps.Active : [] - DataModel.Maps.Mine = new DataModel.MapCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) - DataModel.Maps.Shared = new DataModel.MapCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) - DataModel.Maps.Starred = new DataModel.MapCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) - // 'Mapper' refers to another mapper - DataModel.Maps.Mapper = new DataModel.MapCollection(mapperCollection, mapperOptionsObj) - DataModel.Maps.Featured = new DataModel.MapCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) - DataModel.Maps.Active = new DataModel.MapCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) }, showDiv: function (selector) { $(selector).show() diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index d273edbb..8436c148 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -5,7 +5,7 @@ import _ from 'lodash' import Active from './Active' import AutoLayout from './AutoLayout' -import DataModel from './DataModel' +import DataModel from './DataModel' import GlobalUI from './GlobalUI' import Map from './Map' import Synapse from './Synapse' @@ -218,7 +218,7 @@ const Import = { parsedTopics.forEach(topic => { let coords = { x: topic.x, y: topic.y } if (!coords.x || !coords.y) { - coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + coords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) } if (!topic.name && topic.link || @@ -351,7 +351,7 @@ const Import = { handleURL: function (url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { - coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + coords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) } const name = opts.name || 'Link' diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index cbcd131b..7cbce927 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -51,7 +51,7 @@ const JIT = { /** * This method will bind the event handlers it is interested and initialize the class. */ - init: function () { + init: function (serverData) { const self = JIT $('.zoomIn').click(self.zoomIn) @@ -65,10 +65,10 @@ const JIT = { $('.takeScreenshot').click(Map.exportImage) self.topicDescImage = new Image() - self.topicDescImage.src = Metamaps.ServerData['topic_description_signifier.png'] + self.topicDescImage.src = serverData['topic_description_signifier.png'] self.topicLinkImage = new Image() - self.topicLinkImage.src = Metamaps.ServerData['topic_link_signifier.png'] + self.topicLinkImage.src = serverData['topic_link_signifier.png'] }, /** * convert our topic JSON into something JIT can use @@ -313,17 +313,6 @@ const JIT = { panning: 'avoid nodes', zooming: 28 // zoom speed. higher is more sensible }, - // background: { - // type: 'Metamaps' - // }, - // NodeStyles: { - // enable: true, - // type: 'Native', - // stylesHover: { - // dim: 30 - // }, - // duration: 300 - // }, // Change node and edge styles such as // color and width. // These properties are also set per node @@ -649,7 +638,6 @@ const JIT = { }, // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ background: { - // type: 'Metamaps', levelDistance: 200, numberOfCircles: 4, CanvasStyles: { @@ -1573,7 +1561,7 @@ const JIT = { loader.show() // Hidden by default const topics = DataModel.Topics.map(function (t) { return t.id }) - const topics_string = topics.join() + const topicsString = topics.join() const successCallback = function (data) { $('#loadingSiblings').remove() diff --git a/frontend/src/Metamaps/Loading.js b/frontend/src/Metamaps/Loading.js index 97275547..b1fc2abb 100644 --- a/frontend/src/Metamaps/Loading.js +++ b/frontend/src/Metamaps/Loading.js @@ -1,19 +1,20 @@ /* global CanvasLoader, $ */ const Loading = { - loader: new CanvasLoader('loading'), + loader: null, // needs CanvasLoader to be defined hide: function () { - $('#loading').hide(); + $('#loading').hide() }, show: function () { - $('#loading').show(); + $('#loading').show() }, setup: function () { - Loading.loader.setColor('#4fb5c0'); // default is '#000000' - Loading.loader.setDiameter(28); // default is 40 - Loading.loader.setDensity(41); // default is 40 - Loading.loader.setRange(0.9); // default is 1.3 - Loading.loader.show(); // Hidden by default + if (!Loading.loader) Loading.loader = new CanvasLoader('loading') + Loading.loader.setColor('#4fb5c0') // default is '#000000' + Loading.loader.setDiameter(28) // default is 40 + Loading.loader.setDensity(41) // default is 40 + Loading.loader.setRange(0.9) // default is 1.3 + Loading.loader.show() // Hidden by default } } diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index a3e31f19..bf56dc90 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -34,7 +34,8 @@ const InfoBox = { data-bip-activator="#mapInfoDesc" data-bip-value="{{desc}}" >{{desc}}`, - init: function () { + userImageUrl: '', + init: function (serverData) { var self = InfoBox $('.mapInfoIcon').click(self.toggleBox) @@ -46,7 +47,9 @@ const InfoBox = { self.attachEventListeners() self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) - + + self.userImageUrl = serverData['user.png'] + var querystring = window.location.search.replace(/^\?/, '') if (querystring == 'new') { self.open() @@ -108,7 +111,7 @@ const InfoBox = { obj['contributor_count'] = relevantPeople.length obj['contributors_class'] = relevantPeople.length > 1 ? 'multiple' : '' obj['contributors_class'] += relevantPeople.length === 2 ? ' mTwo' : '' - obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : Metamaps.ServerData['user.png'] + obj['contributor_image'] = relevantPeople.length > 0 ? relevantPeople.models[0].get('image') : self.userImageUrl obj['contributor_list'] = self.createContributorList() obj['user_name'] = isCreator ? 'You' : map.get('user_name') @@ -210,7 +213,7 @@ const InfoBox = { value: "No results", label: "No results", rtype: "noresult", - profile: Metamaps.ServerData['user.png'], + profile: self.userImageUrl }); }, suggestion: function(s) { @@ -313,7 +316,7 @@ const InfoBox = { if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' else if (relevantPeople.length > 2) contributors_class = 'multiple' - var contributors_image = Metamaps.ServerData['user.png'] + var contributors_image = self.userImageUrl if (relevantPeople.length > 0) { // get the first contributor and use their image contributors_image = relevantPeople.models[0].get('image') diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 578c1f9f..b616e95f 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -25,7 +25,7 @@ const Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' }, - init: function () { + init: function (serverData) { var self = Map $('#wrapper').mousedown(function (e){ @@ -44,8 +44,8 @@ const Map = { GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() self.updateStar() - InfoBox.init() - CheatSheet.init() + InfoBox.init(serverData) + CheatSheet.init(serverData) $('.viewOnly .requestAccess').click(self.requestAccess) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 9046c112..74648ac1 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,9 +1,10 @@ /* global $ */ import Backbone from 'backbone' -Backbone.$ = window.$ +try { Backbone.$ = window.$ } catch (err) {} import Active from './Active' +import DataModel from './DataModel' import GlobalUI from './GlobalUI' import Loading from './Loading' import Map from './Map' diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 824aad4b..0345690d 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -3,6 +3,7 @@ import Active from './Active' import Control from './Control' import Create from './Create' +import DataModel from './DataModel' import JIT from './JIT' import Map from './Map' import Selected from './Selected' diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 05cd54f1..08ac0661 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -359,13 +359,13 @@ const Topic = { $(document).trigger(Map.events.editedByActiveMapper) var metacode = DataModel.Metacodes.findWhere({ name: 'Metamap' }) - var topic = new Metamaps.Backbone.Topic({ + var topic = new DataModel.Topic({ name: data.name, metacode_id: metacode.id, - defer_to_map_id: Metamaps.Active.Map.id, + defer_to_map_id: Active.Map.id, link: window.location.origin + '/maps/' + data.id }) - Metamaps.Topics.add(topic) + DataModel.Topics.add(topic) var mapping = new DataModel.Mapping({ xloc: Create.newTopic.x, diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index dd172971..6e798ee6 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -11,9 +11,16 @@ import Visualize from './Visualize' const TopicCard = { openTopicCard: null, // stores the topic that's currently open authorizedToEdit: false, // stores boolean for edit permission for open topic card - init: function () { + RAILS_ENV: undefined, + init: function (serverData) { var self = TopicCard + if (serverData.RAILS_ENV) { + self.RAILS_ENV = serverData.RAILS_ENV + } else { + console.error('RAILS_ENV is not defined! See TopicCard.js init function.') + } + // initialize best_in_place editing $('.authenticated div.permission.canEdit .best_in_place').best_in_place() @@ -84,6 +91,23 @@ const TopicCard = { $('.attachments').removeClass('hidden') $('.CardOnGraph').removeClass('hasAttachment') }, + showLinkLoader: function() { + var loader = new CanvasLoader('embedlyLinkLoader') + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(28) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show() // Hidden by default + }, + showLink: function(topic) { + var e = embedly('card', document.getElementById('embedlyLink')) + if (!e && TopicCard.RAILS_ENV != 'development') { + TopicCard.handleInvalidLink() + } else if (!e) { + $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() + $('#embedlyLinkLoader').hide() + } + }, bindShowCardListeners: function (topic) { var self = TopicCard var showCard = document.getElementById('showcard') @@ -123,20 +147,9 @@ const TopicCard = { $('.attachments').addClass('hidden') $('.embeds').append(embedlyEl) $('.embeds').append('
      ') - var loader = new CanvasLoader('embedlyLinkLoader') - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28) // default is 40 - loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show() // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')) - if (!e && Metamaps.Erb.RAILS_ENV != 'development') { - self.handleInvalidLink() - } - else if (!e) { - $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() - $('#embedlyLinkLoader').hide() - } + + self.showLinkLoader() + self.showLink(topic) } }, 100) } @@ -145,20 +158,9 @@ const TopicCard = { // initialize the link card, if there is a link if (topic.get('link') && topic.get('link') !== '') { - var loader = new CanvasLoader('embedlyLinkLoader') - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(28) // default is 40 - loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show() // Hidden by default - var e = embedly('card', document.getElementById('embedlyLink')) + self.showLinkLoader() + self.showLink(topic) self.showLinkRemover() - if (!e && Metamaps.Erb.RAILS_ENV != 'development') { - self.handleInvalidLink() - } else if (!e) { - $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() - $('#embedlyLinkLoader').hide() - } } var selectingMetacode = false diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index fe33ac68..7175949f 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -121,9 +121,9 @@ var Private = { Handlers.inputBlur.call(self) }) }, - initializeSounds: function () { + initializeSounds: function (soundUrls) { this.sound = new Howl({ - src: [Metamaps.ServerData['sounds/MM_sounds.mp3'], Metamaps.ServerData['sounds/MM_sounds.ogg']], + src: soundUrls, sprite: { joinmap: [0, 561], leavemap: [1000, 592], @@ -243,7 +243,7 @@ const ChatView = function (messages, mapper, room) { Private.attachElements.call(this) Private.addEventListeners.call(this) Private.initialMessages.call(this) - Private.initializeSounds.call(this) + Private.initializeSounds.call(this, room.soundUrls) this.$container.css({ right: '-300px' }) diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index ff8975d2..f0821a10 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -13,7 +13,7 @@ import Realtime from '../Realtime' import ChatView from './ChatView' import VideoView from './VideoView' -const Room = function(opts) { +const Room = function(opts = {}) { var self = this this.isActiveRoom = false @@ -31,6 +31,7 @@ const Room = function(opts) { this.chat = new ChatView(this.messages, this.currentMapper, this.room) this.videos = {} + this.soundUrls = opts.soundUrls this.init() } diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index 39104b18..17b592d6 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -15,4 +15,6 @@ const Views = { VideoView, Room } + +export { ExploreMaps, ChatView, VideoView, Room } export default Views diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 720ef598..3760a33a 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -17,7 +17,7 @@ const Visualize = { type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created touchDragNode: null, - init: function () { + init: function (serverData) { var self = Visualize if (serverData.VisualizeType) self.type = serverData.VisualizeType diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 012af50b..696d1539 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,5 +1,3 @@ -/* global Metamaps */ - import Account from './Account' import Active from './Active' import Admin from './Admin' @@ -34,7 +32,7 @@ import Util from './Util' import Views from './Views' import Visualize from './Visualize' -Metamaps = window.Metamaps || {} +const Metamaps = window.Metamaps || {} Metamaps.Account = Account Metamaps.Active = Active Metamaps.Admin = Admin @@ -72,6 +70,10 @@ Metamaps.Topic = Topic Metamaps.TopicCard = TopicCard Metamaps.Util = Util Metamaps.Views = Views +Metamaps.Views.ExploreMaps = ExploreMaps +Metamaps.Views.ChatView = ChatView +Metamaps.Views.VideoView = VideoView +Metamaps.Views.Room = Room Metamaps.Visualize = Visualize document.addEventListener('DOMContentLoaded', function () { @@ -90,19 +92,19 @@ document.addEventListener('DOMContentLoaded', function () { if (Metamaps.currentSection === 'explore') { const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) if (Metamaps.currentPage === 'mapper') { - Views.ExploreMaps.fetchUserThenRender() + ExploreMaps.fetchUserThenRender() } else { - Views.ExploreMaps.render() + ExploreMaps.render() } GlobalUI.showDiv('#explore') } else if (Metamaps.currentSection === '' && Active.Mapper) { - Views.ExploreMaps.setCollection(Metamaps.Maps.Active) - Views.ExploreMaps.render() + ExploreMaps.setCollection(DataModel.Maps.Active) + ExploreMaps.render() GlobalUI.showDiv('#explore') } else if (Active.Map || Active.Topic) { - Metamaps.Loading.show() + Loading.show() JIT.prepareVizData() GlobalUI.showDiv('#infovis') } diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index a41f1c65..d4c3154a 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -11349,4 +11349,6 @@ $jit.ForceDirected3D.$extend = true; })($jit.ForceDirected3D); +// START METAMAPS CODE export default $jit +// END METAMAPS CODE diff --git a/package.json b/package.json index 940f7c58..e33a6e0a 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "devDependencies": { "babel-eslint": "^6.1.2", "chai": "^3.5.0", + "circular-dependency-plugin": "^2.0.0", "eslint": "^3.5.0", "eslint-config-standard": "^6.2.0", "eslint-plugin-promise": "^2.0.1", diff --git a/webpack.config.js b/webpack.config.js index 644ff002..423616f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,13 @@ if (NODE_ENV === 'production') { plugins.push(new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })) +} else { + // enable this to test for circular dependencies + // const CircularDependencyPlugin = require('circular-dependency-plugin') + // plugins.push(new CircularDependencyPlugin({ + // exclude: /^node_modules\//, + // failOnError: true + // })) } const devtool = NODE_ENV === 'production' ? undefined : 'cheap-module-eval-source-map' @@ -28,9 +35,7 @@ const config = module.exports = { { test: /\.(js|jsx)?$/, exclude: /node_modules/, - loaders: [ - 'babel-loader?cacheDirectory&retainLines=true' - ] + loader: 'babel-loader?cacheDirectory&retainLines=true' } ] }, From 6a2646c9cdf7f31b5c72932d7e5ead1fc154f34a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 7 Nov 2016 14:56:35 -0500 Subject: [PATCH 321/378] fix mailer specs (#921) --- spec/mailers/map_mailer_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb index d72163f7..5fed48f5 100644 --- a/spec/mailers/map_mailer_spec.rb +++ b/spec/mailers/map_mailer_spec.rb @@ -11,8 +11,8 @@ RSpec.describe MapMailer, type: :mailer do it { expect(mail.subject).to match map.name } it { expect(mail.body.encoded).to match map.name } it { expect(mail.body.encoded).to match request.user.name } - it { expect(mail.body.encoded).to match 'Approve Request' } - it { expect(mail.body.encoded).to match 'Deny Request' } + it { expect(mail.body.encoded).to match 'Allow' } + it { expect(mail.body.encoded).to match 'Decline' } end describe 'invite_to_edit_email' do From 9df974a0372c4cf26d31e4ccbed4712572d23384 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 7 Nov 2016 14:58:53 -0500 Subject: [PATCH 322/378] strip whitespace from search terms (#919) --- app/controllers/search_controller.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 0fb9c808..ae686d8f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -45,7 +45,7 @@ class SearchController < ApplicationController end end - search = '%' + term.downcase + '%' + search = '%' + term.downcase.strip + '%' builder = policy_scope(Topic) if filterByMetacode @@ -93,7 +93,7 @@ class SearchController < ApplicationController desc = true end - search = '%' + term.downcase + '%' + search = '%' + term.downcase.strip + '%' builder = policy_scope(Map) builder = if desc @@ -117,7 +117,7 @@ class SearchController < ApplicationController # remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == 'mapper:' - search = term.downcase + '%' + search = term.downcase.strip + '%' skip_policy_scope # TODO: builder = policy_scope(User) builder = User.where('LOWER("name") like ?', search) @@ -136,7 +136,7 @@ class SearchController < ApplicationController topic2id = params[:topic2id] if term && !term.empty? - @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase.strip + '%').order('"desc"') @synapses = @synapses.uniq(&:desc) elsif topic1id && !topic1id.empty? From a176cdf2312156fd3fd512bb80a2cc832039dc38 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 7 Nov 2016 15:25:08 -0500 Subject: [PATCH 323/378] eslint frontend folder (#923) --- .eslintrc.js | 1 + frontend/src/Metamaps/Account.js | 26 +- frontend/src/Metamaps/Admin.js | 21 +- frontend/src/Metamaps/AutoLayout.js | 23 +- frontend/src/Metamaps/Control.js | 117 ++--- frontend/src/Metamaps/Create.js | 108 ++-- frontend/src/Metamaps/DataModel/Map.js | 25 +- .../src/Metamaps/DataModel/MapCollection.js | 12 +- frontend/src/Metamaps/DataModel/Mapper.js | 4 +- frontend/src/Metamaps/DataModel/Mapping.js | 10 +- frontend/src/Metamaps/DataModel/Message.js | 2 +- frontend/src/Metamaps/DataModel/Metacode.js | 4 +- .../Metamaps/DataModel/MetacodeCollection.js | 2 +- frontend/src/Metamaps/DataModel/Synapse.js | 38 +- frontend/src/Metamaps/DataModel/Topic.js | 34 +- frontend/src/Metamaps/DataModel/index.js | 10 +- frontend/src/Metamaps/Filter.js | 143 +++--- frontend/src/Metamaps/GlobalUI/Account.js | 14 +- frontend/src/Metamaps/GlobalUI/CreateMap.js | 28 +- .../src/Metamaps/GlobalUI/ImportDialog.js | 6 +- frontend/src/Metamaps/GlobalUI/Search.js | 50 +- frontend/src/Metamaps/GlobalUI/index.js | 24 +- frontend/src/Metamaps/Import.js | 74 +-- frontend/src/Metamaps/JIT.js | 482 +++++++++--------- frontend/src/Metamaps/Listeners.js | 12 +- frontend/src/Metamaps/Loading.js | 6 +- frontend/src/Metamaps/Map/CheatSheet.js | 32 +- frontend/src/Metamaps/Map/InfoBox.js | 151 +++--- frontend/src/Metamaps/Map/index.js | 144 +++--- frontend/src/Metamaps/Mapper.js | 2 +- frontend/src/Metamaps/Mobile.js | 12 +- frontend/src/Metamaps/Organize.js | 82 ++- frontend/src/Metamaps/PasteInput.js | 22 +- frontend/src/Metamaps/Realtime/index.js | 203 ++++---- frontend/src/Metamaps/Realtime/receivable.js | 73 ++- frontend/src/Metamaps/Realtime/sendable.js | 6 +- frontend/src/Metamaps/Router.js | 30 +- frontend/src/Metamaps/Selected.js | 2 +- frontend/src/Metamaps/Synapse.js | 58 +-- frontend/src/Metamaps/SynapseCard.js | 88 ++-- frontend/src/Metamaps/Topic.js | 83 ++- frontend/src/Metamaps/TopicCard.js | 91 ++-- frontend/src/Metamaps/Util.js | 103 ++-- frontend/src/Metamaps/Views/ChatView.js | 90 ++-- frontend/src/Metamaps/Views/ExploreMaps.js | 39 +- frontend/src/Metamaps/Views/Room.js | 208 ++++---- frontend/src/Metamaps/Views/VideoView.js | 327 ++++++------ frontend/src/Metamaps/Views/index.js | 4 +- frontend/src/Metamaps/Visualize.js | 114 ++--- frontend/src/Metamaps/index.js | 14 +- frontend/src/components/ImportDialogBox.js | 2 +- frontend/src/components/Maps/MapCard.js | 4 - frontend/src/components/Maps/index.js | 10 +- frontend/test/Metamaps.Import.spec.js | 4 +- frontend/test/Metamaps.Util.spec.js | 72 +-- realtime/global.js | 15 +- realtime/junto.js | 23 +- realtime/map.js | 25 +- realtime/realtime-server.js | 13 +- realtime/reducer.js | 80 +-- realtime/signal.js | 30 +- 61 files changed, 1723 insertions(+), 1809 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 1222f4a1..949de95b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,7 @@ module.exports = { "rules": { "react/jsx-uses-react": [2], "react/jsx-uses-vars": [2], + "space-before-function-paren": [2, "never"], "yoda": [2, "never", { "exceptRange": true }] } } diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index b7e1fe79..19785797 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,33 +1,33 @@ /* global $, CanvasLoader */ const Account = { - init: function (serverData) { + init: function(serverData) { Account.userIconUrl = serverData['user.png'] }, listenersInitialized: false, userIconUrl: null, - initListeners: function () { + initListeners: function() { var self = Account $('#user_image').change(self.showImagePreview) self.listenersInitialized = true }, - toggleChangePicture: function () { + toggleChangePicture: function() { var self = Account $('.userImageMenu').toggle() if (!self.listenersInitialized) self.initListeners() }, - openChangePicture: function () { + openChangePicture: function() { var self = Account $('.userImageMenu').show() if (!self.listenersInitialized) self.initListeners() }, - closeChangePicture: function () { + closeChangePicture: function() { $('.userImageMenu').hide() }, - showLoading: function () { + showLoading: function() { var loader = new CanvasLoader('accountPageLoading') loader.setColor('#4FC059') // default is '#000000' loader.setDiameter(28) // default is 40 @@ -36,12 +36,12 @@ const Account = { loader.show() // Hidden by default $('#accountPageLoading').show() }, - showImagePreview: function () { + showImagePreview: function() { var file = $('#user_image')[0].files[0] var reader = new window.FileReader() - reader.onload = function (e) { + reader.onload = function(e) { var $canvas = $('').attr({ width: 84, height: 84 @@ -49,7 +49,7 @@ const Account = { var context = $canvas[0].getContext('2d') var imageObj = new window.Image() - imageObj.onload = function () { + imageObj.onload = function() { $('.userImageDiv canvas').remove() $('.userImageDiv img').hide() @@ -80,7 +80,7 @@ const Account = { $('#remove_image').val('0') } }, - removePicture: function () { + removePicture: function() { var self = Account $('.userImageDiv canvas').remove() @@ -91,15 +91,15 @@ const Account = { input.replaceWith(input.val('').clone(true)) $('#remove_image').val('1') }, - changeName: function () { + changeName: function() { $('.accountName').hide() $('.changeName').show() }, - showPass: function () { + showPass: function() { $('.toHide').show() $('.changePass').hide() }, - hidePass: function () { + hidePass: function() { $('.toHide').hide() $('.changePass').show() diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index d78fcecb..9cb80750 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -3,44 +3,43 @@ const Admin = { selectMetacodes: [], allMetacodes: [], - init: function () { + init: function() { var self = Admin $('#metacodes_value').val(self.selectMetacodes.toString()) }, - selectAll: function () { + selectAll: function() { var self = Admin $('.editMetacodes li').removeClass('toggledOff') self.selectMetacodes = self.allMetacodes.slice(0) $('#metacodes_value').val(self.selectMetacodes.toString()) }, - deselectAll: function () { + deselectAll: function() { var self = Admin $('.editMetacodes li').addClass('toggledOff') self.selectMetacodes = [] $('#metacodes_value').val(0) }, - liClickHandler: function () { + liClickHandler: function() { var self = Admin - if ($(this).attr('class') != 'toggledOff') { + if ($(this).attr('class') !== 'toggledOff') { $(this).addClass('toggledOff') - var value_to_remove = $(this).attr('id') - self.selectMetacodes.splice(self.selectMetacodes.indexOf(value_to_remove), 1) + const valueToRemove = $(this).attr('id') + self.selectMetacodes.splice(self.selectMetacodes.indexOf(valueToRemove), 1) $('#metacodes_value').val(self.selectMetacodes.toString()) - } - else if ($(this).attr('class') == 'toggledOff') { + } else if ($(this).attr('class') === 'toggledOff') { $(this).removeClass('toggledOff') self.selectMetacodes.push($(this).attr('id')) $('#metacodes_value').val(self.selectMetacodes.toString()) } }, - validate: function () { + validate: function() { var self = Admin - if (self.selectMetacodes.length == 0) { + if (self.selectMetacodes.length === 0) { window.alert('Would you pretty please select at least one metacode for the set?') return false } diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index 1408ba62..acbca6ff 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -7,7 +7,7 @@ const AutoLayout = { nextYshift: 0, timeToTurn: 0, - getNextCoord: function (opts = {}) { + getNextCoord: function(opts = {}) { var self = AutoLayout var nextX = self.nextX var nextY = self.nextY @@ -28,22 +28,19 @@ const AutoLayout = { self.timeToTurn = 0 // going right? turn down - if (self.nextXshift == 1 && self.nextYshift == 0) { + if (self.nextXshift === 1 && self.nextYshift === 0) { self.nextXshift = 0 self.nextYshift = 1 - } - // going down? turn left - else if (self.nextXshift == 0 && self.nextYshift == 1) { + } else if (self.nextXshift === 0 && self.nextYshift === 1) { + // going down? turn left self.nextXshift = -1 self.nextYshift = 0 - } - // going left? turn up - else if (self.nextXshift == -1 && self.nextYshift == 0) { + } else if (self.nextXshift === -1 && self.nextYshift === 0) { + // going left? turn up self.nextXshift = 0 self.nextYshift = -1 - } - // going up? turn right - else if (self.nextXshift == 0 && self.nextYshift == -1) { + } else if (self.nextXshift === 0 && self.nextYshift === -1) { + // going up? turn right self.nextXshift = 1 self.nextYshift = 0 } @@ -59,14 +56,14 @@ const AutoLayout = { } } }, - coordsTaken: function (x, y, mappings) { + coordsTaken: function(x, y, mappings) { if (mappings.findWhere({ xloc: x, yloc: y })) { return true } else { return false } }, - resetSpiral: function () { + resetSpiral: function() { var self = AutoLayout self.nextX = 0 self.nextY = 0 diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 8305e097..8f613b16 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -14,16 +14,16 @@ import Settings from './Settings' import Visualize from './Visualize' const Control = { - init: function () {}, - selectNode: function (node, e) { + init: function() {}, + selectNode: function(node, e) { var filtered = node.getData('alpha') === 0 - if (filtered || Selected.Nodes.indexOf(node) != -1) return + if (filtered || Selected.Nodes.indexOf(node) !== -1) return node.selected = true node.setData('dim', 30, 'current') Selected.Nodes.push(node) }, - deselectAllNodes: function () { + deselectAllNodes: function() { var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { var node = Selected.Nodes[i] @@ -31,7 +31,7 @@ const Control = { } Visualize.mGraph.plot() }, - deselectNode: function (node) { + deselectNode: function(node) { delete node.selected node.setData('dim', 25, 'current') @@ -39,7 +39,7 @@ const Control = { Selected.Nodes.splice( Selected.Nodes.indexOf(node), 1) }, - deleteSelected: function () { + deleteSelected: function() { if (!Active.Map) return var n = Selected.Nodes.length @@ -67,7 +67,7 @@ const Control = { GlobalUI.showDiv('#instructions') } }, - deleteSelectedNodes: function () { // refers to deleting topics permanently + deleteSelectedNodes: function() { // refers to deleting topics permanently if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -83,7 +83,7 @@ const Control = { Control.deleteNode(node.id) } }, - deleteNode: function (nodeid) { // refers to deleting topics permanently + deleteNode: function(nodeid) { // refers to deleting topics permanently if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -110,7 +110,7 @@ const Control = { GlobalUI.notifyUser('Only topics you created can be deleted') } }, - removeSelectedNodes: function () { // refers to removing topics permanently from a map + removeSelectedNodes: function() { // refers to removing topics permanently from a map if (Active.Topic) { // hideNode will handle synapses as well var nodeids = _.map(Selected.Nodes, function(node) { @@ -126,22 +126,20 @@ const Control = { } if (!Active.Map) return - var l = Selected.Nodes.length, - i, - node, - authorized = Active.Map.authorizeToEdit(Active.Mapper) + const l = Selected.Nodes.length + const authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { GlobalUI.notifyUser('Cannot edit Public map.') return } - for (i = l - 1; i >= 0; i -= 1) { - node = Selected.Nodes[i] + for (let i = l - 1; i >= 0; i -= 1) { + const node = Selected.Nodes[i] Control.removeNode(node.id) } }, - removeNode: function (nodeid) { // refers to removing topics permanently from a map + removeNode: function(nodeid) { // refers to removing topics permanently from a map if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -162,24 +160,21 @@ const Control = { }]) Control.hideNode(nodeid) }, - hideSelectedNodes: function () { - var l = Selected.Nodes.length, - i, - node - - for (i = l - 1; i >= 0; i -= 1) { - node = Selected.Nodes[i] + hideSelectedNodes: function() { + const l = Selected.Nodes.length + for (let i = l - 1; i >= 0; i -= 1) { + const node = Selected.Nodes[i] Control.hideNode(node.id) } }, - hideNode: function (nodeid) { + hideNode: function(nodeid) { var node = Visualize.mGraph.graph.getNode(nodeid) var graph = Visualize.mGraph Control.deselectNode(node) node.setData('alpha', 0, 'end') - node.eachAdjacency(function (adj) { + node.eachAdjacency(function(adj) { adj.setData('alpha', 0, 'end') }) Visualize.mGraph.fx.animate({ @@ -188,9 +183,9 @@ const Control = { ], duration: 500 }) - setTimeout(function () { + setTimeout(function() { if (nodeid === Visualize.mGraph.root) { // && Visualize.type === "RGraph" - var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) + var newroot = _.find(graph.graph.nodes, function(n) { return n.id !== nodeid }) graph.root = newroot ? newroot.id : null } Visualize.mGraph.graph.removeNode(nodeid) @@ -198,10 +193,10 @@ const Control = { Filter.checkMetacodes() Filter.checkMappers() }, - selectEdge: function (edge) { - var filtered = edge.getData('alpha') === 0; // don't select if the edge is filtered + selectEdge: function(edge) { + var filtered = edge.getData('alpha') === 0 // don't select if the edge is filtered - if (filtered || Selected.Edges.indexOf(edge) != -1) return + if (filtered || Selected.Edges.indexOf(edge) !== -1) return var width = Mouse.edgeHoveringOver === edge ? 4 : 2 edge.setDataset('current', { @@ -213,7 +208,7 @@ const Control = { Selected.Edges.push(edge) }, - deselectAllEdges: function () { + deselectAllEdges: function() { var l = Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { var edge = Selected.Edges[i] @@ -221,7 +216,7 @@ const Control = { } Visualize.mGraph.plot() }, - deselectEdge: function (edge) { + deselectEdge: function(edge) { edge.setData('showDesc', false, 'current') edge.setDataset('current', { @@ -242,10 +237,7 @@ const Control = { Selected.Edges.splice( Selected.Edges.indexOf(edge), 1) }, - deleteSelectedEdges: function () { // refers to deleting topics permanently - var edge, - l = Selected.Edges.length - + deleteSelectedEdges: function() { // refers to deleting topics permanently if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -255,12 +247,13 @@ const Control = { return } - for (var i = l - 1; i >= 0; i -= 1) { - edge = Selected.Edges[i] + const l = Selected.Edges.length + for (let i = l - 1; i >= 0; i -= 1) { + const edge = Selected.Edges[i] Control.deleteEdge(edge) } }, - deleteEdge: function (edge) { + deleteEdge: function(edge) { if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -297,13 +290,11 @@ const Control = { GlobalUI.notifyUser('Only synapses you created can be deleted') } }, - removeSelectedEdges: function () { + removeSelectedEdges: function() { // Topic view is handled by removeSelectedNodes if (!Active.Map) return - var l = Selected.Edges.length, - i, - edge + const l = Selected.Edges.length var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -312,13 +303,13 @@ const Control = { return } - for (i = l - 1; i >= 0; i -= 1) { - edge = Selected.Edges[i] + for (let i = l - 1; i >= 0; i -= 1) { + const edge = Selected.Edges[i] Control.removeEdge(edge) } Selected.Edges = [ ] }, - removeEdge: function (edge) { + removeEdge: function(edge) { if (!Active.Map) return var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -350,17 +341,15 @@ const Control = { mappableid: mappableid }]) }, - hideSelectedEdges: function () { - var edge, - l = Selected.Edges.length, - i - for (i = l - 1; i >= 0; i -= 1) { - edge = Selected.Edges[i] + hideSelectedEdges: function() { + const l = Selected.Edges.length + for (let i = l - 1; i >= 0; i -= 1) { + const edge = Selected.Edges[i] Control.hideEdge(edge) } Selected.Edges = [ ] }, - hideEdge: function (edge) { + hideEdge: function(edge) { var from = edge.nodeFrom.id var to = edge.nodeTo.id edge.setData('alpha', 0, 'end') @@ -369,24 +358,24 @@ const Control = { modes: ['edge-property:alpha'], duration: 500 }) - setTimeout(function () { + setTimeout(function() { Visualize.mGraph.graph.removeAdjacence(from, to) }, 500) Filter.checkSynapses() Filter.checkMappers() }, - updateSelectedPermissions: function (permission) { + updateSelectedPermissions: function(permission) { var edge, synapse, node, topic GlobalUI.notifyUser('Working...') // variables to keep track of how many nodes and synapses you had the ability to change the permission of - var nCount = 0, - sCount = 0 + var nCount = 0 + var sCount = 0 // change the permission of the selected synapses, if logged in user is the original creator - var l = Selected.Edges.length - for (var i = l - 1; i >= 0; i -= 1) { + const edgesLength = Selected.Edges.length + for (let i = edgesLength - 1; i >= 0; i -= 1) { edge = Selected.Edges[i] synapse = edge.getData('synapses')[0] @@ -399,8 +388,8 @@ const Control = { } // change the permission of the selected topics, if logged in user is the original creator - var l = Selected.Nodes.length - for (var i = l - 1; i >= 0; i -= 1) { + const nodesLength = Selected.Nodes.length + for (let i = nodesLength - 1; i >= 0; i -= 1) { node = Selected.Nodes[i] topic = node.getData('topic') @@ -418,12 +407,12 @@ const Control = { var message = nString + sString + ' you created updated to ' + permission GlobalUI.notifyUser(message) }, - updateSelectedMetacodes: function (metacode_id) { + updateSelectedMetacodes: function(metacodeId) { var node, topic GlobalUI.notifyUser('Working...') - var metacode = DataModel.Metacodes.get(metacode_id) + var metacode = DataModel.Metacodes.get(metacodeId) // variables to keep track of how many nodes and synapses you had the ability to change the permission of var nCount = 0 @@ -436,7 +425,7 @@ const Control = { if (topic.authorizeToEdit(Active.Mapper)) { topic.save({ - 'metacode_id': metacode_id + 'metacode_id': metacodeId }) nCount++ } diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index d12a5de5..7c3c4ff3 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -16,7 +16,7 @@ const Create = { newSelectedMetacodeNames: [], selectedMetacodes: [], newSelectedMetacodes: [], - init: function () { + init: function() { var self = Create self.newTopic.init() self.newSynapse.init() @@ -29,23 +29,23 @@ const Create = { $('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab }, - toggleMetacodeSelected: function () { + toggleMetacodeSelected: function() { var self = Create - if ($(this).attr('class') != 'toggledOff') { + if ($(this).attr('class') !== 'toggledOff') { $(this).addClass('toggledOff') - var value_to_remove = $(this).attr('id') - var name_to_remove = $(this).attr('data-name') - self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(value_to_remove), 1) - self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(name_to_remove), 1) - } else if ($(this).attr('class') == 'toggledOff') { + var valueToRemove = $(this).attr('id') + var nameToRemove = $(this).attr('data-name') + self.newSelectedMetacodes.splice(self.newSelectedMetacodes.indexOf(valueToRemove), 1) + self.newSelectedMetacodeNames.splice(self.newSelectedMetacodeNames.indexOf(nameToRemove), 1) + } else if ($(this).attr('class') === 'toggledOff') { $(this).removeClass('toggledOff') self.newSelectedMetacodes.push($(this).attr('id')) self.newSelectedMetacodeNames.push($(this).attr('data-name')) } }, - updateMetacodeSet: function (set, index, custom) { - if (custom && Create.newSelectedMetacodes.length == 0) { + updateMetacodeSet: function(set, index, custom) { + if (custom && Create.newSelectedMetacodes.length === 0) { window.alert('Please select at least one metacode to use!') return false } @@ -62,8 +62,7 @@ const Create = { Create.selectedMetacodeNames = [] Create.newSelectedMetacodes = [] Create.newSelectedMetacodeNames = [] - } - else if (custom) { + } else if (custom) { // uses .slice to avoid setting the two arrays to the same actual array Create.selectedMetacodes = Create.newSelectedMetacodes.slice(0) Create.selectedMetacodeNames = Create.newSelectedMetacodeNames.slice(0) @@ -79,7 +78,7 @@ const Create = { $('#metacodeImg, #metacodeImgTitle').empty() $('#metacodeImg').removeData('cloudcarousel') var newMetacodes = '' - metacodeModels.each(function (metacode) { + metacodeModels.each(function(metacode) { newMetacodes += '' + metacode.get('name') + '' }) @@ -107,20 +106,20 @@ const Create = { dataType: 'json', url: '/user/updatemetacodes', data: mdata, - success: function (data) { + success: function(data) { console.log('selected metacodes saved') }, - error: function () { + error: function() { console.log('failed to save selected metacodes') } }) }, - cancelMetacodeSetSwitch: function () { + cancelMetacodeSetSwitch: function() { var self = Create self.isSwitchingSet = false - if (self.selectedMetacodeSet != 'metacodeset-custom') { + if (self.selectedMetacodeSet !== 'metacodeset-custom') { $('.customMetacodeList li').addClass('toggledOff') self.selectedMetacodes = [] self.selectedMetacodeNames = [] @@ -140,17 +139,16 @@ const Create = { $('#topic_name').focus() }, newTopic: { - init: function () { - $('#topic_name').keyup(function () { + init: function() { + $('#topic_name').keyup(function() { Create.newTopic.name = $(this).val() }) - + $('.pinCarousel').click(function() { if (Create.newTopic.pinned) { $('.pinCarousel').removeClass('isPinned') Create.newTopic.pinned = false - } - else { + } else { $('.pinCarousel').addClass('isPinned') Create.newTopic.pinned = true } @@ -174,18 +172,18 @@ const Create = { [{ name: 'topic_autocomplete', limit: 8, - display: function (s) { return s.label; }, + display: function(s) { return s.label }, templates: { - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s) } }, - source: topicBloodhound, + source: topicBloodhound }] ) // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete - $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { + $('#topic_name').bind('typeahead:select', function(event, datum, dataset) { Create.newTopic.beingCreated = false if (datum.rtype === 'topic') { Topic.getTopicFromAutocomplete(datum.id) @@ -209,7 +207,7 @@ const Create = { bringToFront: true }) $('.new_topic').hide() - $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element + $('#new_topic').attr('oncontextmenu', 'return false') // prevents the mouse up event from opening the default context menu on this element }, name: null, newId: 1, @@ -219,15 +217,15 @@ const Create = { y: null, addSynapse: false, pinned: false, - open: function () { - $('#new_topic').fadeIn('fast', function () { + open: function() { + $('#new_topic').fadeIn('fast', function() { $('#topic_name').focus() }) Create.newTopic.beingCreated = true Create.newTopic.name = '' GlobalUI.hideDiv('#instructions') }, - hide: function (force) { + hide: function(force) { if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') } @@ -240,28 +238,26 @@ const Create = { } Create.newTopic.beingCreated = false }, - reset: function () { + reset: function() { $('#topic_name').typeahead('val', '') } }, newSynapse: { - init: function () { - var self = Create.newSynapse - + init: function() { var synapseBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/synapses?term=%QUERY', - wildcard: '%QUERY', - }, + wildcard: '%QUERY' + } }) var existingSynapseBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', - prepare: function (query, settings) { + prepare: function(query, settings) { var self = Create.newSynapse if (Selected.Nodes.length < 2 && self.topic1id && self.topic2id) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) @@ -269,42 +265,44 @@ const Create = { } else { return null } - }, - }, + } + } }) // initialize the autocomplete results for synapse creation $('#synapse_desc').typeahead( { highlight: true, - minLength: 2, + minLength: 2 }, [{ name: 'synapse_autocomplete', - display: function (s) { return s.label; }, + display: function(s) { return s.label }, templates: { - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile("
      {{label}}
      ").render(s) - }, + } }, - source: synapseBloodhound, + source: synapseBloodhound }, { name: 'existing_synapses', limit: 50, - display: function (s) { return s.label; }, + display: function(s) { return s.label }, templates: { - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s) }, header: '

      Existing synapses

      ' }, - source: existingSynapseBloodhound, + source: existingSynapseBloodhound }] ) - $('#synapse_desc').keyup(function (e) { - var ESC = 27, BACKSPACE = 8, DELETE = 46 + $('#synapse_desc').keyup(function(e) { + const ESC = 27 + const BACKSPACE = 8 + const DELETE = 46 if (e.keyCode === BACKSPACE && $(this).val() === '' || e.keyCode === DELETE && $(this).val() === '' || e.keyCode === ESC) { @@ -313,13 +311,13 @@ const Create = { Create.newSynapse.description = $(this).val() }) - $('#synapse_desc').focusout(function () { + $('#synapse_desc').focusout(function() { if (Create.newSynapse.beingCreated) { Synapse.createSynapseLocally() } }) - $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { + $('#synapse_desc').bind('typeahead:select', function(event, datum, dataset) { if (datum.id) { // if they clicked on an existing synapse get it Synapse.getSynapseFromAutocomplete(datum.id) } else { @@ -333,13 +331,13 @@ const Create = { topic1id: null, topic2id: null, newSynapseId: null, - open: function () { - $('#new_synapse').fadeIn(100, function () { + open: function() { + $('#new_synapse').fadeIn(100, function() { $('#synapse_desc').focus() }) Create.newSynapse.beingCreated = true }, - hide: function () { + hide: function() { $('#new_synapse').fadeOut('fast') $('#synapse_desc').typeahead('val', '') Create.newSynapse.beingCreated = false @@ -348,7 +346,7 @@ const Create = { Create.newSynapse.topic2id = 0 Mouse.synapseStartCoordinates = [] if (Visualize.mGraph) Visualize.mGraph.plot() - }, + } } } diff --git a/frontend/src/Metamaps/DataModel/Map.js b/frontend/src/Metamaps/DataModel/Map.js index 2ab98575..17e9ad18 100644 --- a/frontend/src/Metamaps/DataModel/Map.js +++ b/frontend/src/Metamaps/DataModel/Map.js @@ -9,18 +9,13 @@ import InfoBox from '../Map/InfoBox' import Mapper from '../Mapper' import Realtime from '../Realtime' -import MapperCollection from './MapperCollection' -import TopicCollection from './TopicCollection' -import SynapseCollection from './SynapseCollection' -import MappingCollection from './MappingCollection' - const Map = Backbone.Model.extend({ urlRoot: '/maps', blacklist: ['created_at', 'updated_at', 'created_at_clean', 'updated_at_clean', 'user_name', 'contributor_count', 'topic_count', 'synapse_count', 'topics', 'synapses', 'mappings', 'mappers'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - save: function (key, val, options) { + save: function(key, val, options) { var attrs // Handle both `"key", value` and `{key: value}` -style arguments. @@ -34,20 +29,20 @@ const Map = Backbone.Model.extend({ var newOptions = options || {} var s = newOptions.success - newOptions.success = function (model, response, opt) { + newOptions.success = function(model, response, opt) { if (s) s(model, response, opt) model.trigger('saved') } return Backbone.Model.prototype.save.call(this, attrs, newOptions) }, - initialize: function () { + initialize: function() { this.on('changeByOther', this.updateView) this.on('saved', this.savedEvent) }, - savedEvent: function () { + savedEvent: function() { Realtime.updateMap(this) }, - authorizeToEdit: function (mapper) { + authorizeToEdit: function(mapper) { if (mapper && ( this.get('permission') === 'commons' || (this.get('collaborator_ids') || []).includes(mapper.get('id')) || @@ -57,17 +52,17 @@ const Map = Backbone.Model.extend({ return false } }, - authorizePermissionChange: function (mapper) { + authorizePermissionChange: function(mapper) { if (mapper && this.get('user_id') === mapper.get('id')) { return true } else { return false } }, - getUser: function () { + getUser: function() { return Mapper.get(this.get('user_id')) }, - updateView: function () { + updateView: function() { var map = Active.Map var isActiveMap = this.id === map.id if (isActiveMap) { @@ -78,7 +73,7 @@ const Map = Backbone.Model.extend({ document.title = this.get('name') + ' | Metamaps' } }, - updateMapWrapper: function () { + updateMapWrapper: function() { var map = Active.Map var isActiveMap = this.id === map.id var authorized = map && map.authorizeToEdit(Active.Mapper) ? 'canEditMap' : '' diff --git a/frontend/src/Metamaps/DataModel/MapCollection.js b/frontend/src/Metamaps/DataModel/MapCollection.js index 22bbcf7b..b15a945f 100644 --- a/frontend/src/Metamaps/DataModel/MapCollection.js +++ b/frontend/src/Metamaps/DataModel/MapCollection.js @@ -7,7 +7,7 @@ import Map from './Map' const MapCollection = Backbone.Collection.extend({ model: Map, - initialize: function (models, options) { + initialize: function(models, options) { this.id = options.id this.sortBy = options.sortBy @@ -18,14 +18,14 @@ const MapCollection = Backbone.Collection.extend({ // this.page represents the NEXT page to fetch this.page = models.length > 0 ? (models.length < 20 ? 'loadedAll' : 2) : 1 }, - url: function () { + url: function() { if (!this.mapperId) { return '/explore/' + this.id + '.json' } else { return '/explore/mapper/' + this.mapperId + '.json' } }, - comparator: function (a, b) { + comparator: function(a, b) { a = a.get(this.sortBy) b = b.get(this.sortBy) var temp @@ -42,7 +42,7 @@ const MapCollection = Backbone.Collection.extend({ } return a > b ? 1 : a < b ? -1 : 0 }, - getMaps: function (cb) { + getMaps: function(cb) { var self = this Loading.show() @@ -53,7 +53,7 @@ const MapCollection = Backbone.Collection.extend({ remove: false, silent: true, data: { page: this.page }, - success: function (collection, response, options) { + success: function(collection, response, options) { // you can pass additional options to the event you trigger here as well if (collection.length - numBefore < 20) { self.page = 'loadedAll' @@ -62,7 +62,7 @@ const MapCollection = Backbone.Collection.extend({ } self.trigger('successOnFetch', cb) }, - error: function (collection, response, options) { + error: function(collection, response, options) { // you can pass additional options to the event you trigger here as well self.trigger('errorOnFetch') } diff --git a/frontend/src/Metamaps/DataModel/Mapper.js b/frontend/src/Metamaps/DataModel/Mapper.js index 39e83503..f772c288 100644 --- a/frontend/src/Metamaps/DataModel/Mapper.js +++ b/frontend/src/Metamaps/DataModel/Mapper.js @@ -6,10 +6,10 @@ import outdent from 'outdent' const Mapper = Backbone.Model.extend({ urlRoot: '/users', blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - prepareLiForFilter: function () { + prepareLiForFilter: function() { return outdent`
    • ${this.get('name')} diff --git a/frontend/src/Metamaps/DataModel/Mapping.js b/frontend/src/Metamaps/DataModel/Mapping.js index 282c419f..2cd2b0b8 100644 --- a/frontend/src/Metamaps/DataModel/Mapping.js +++ b/frontend/src/Metamaps/DataModel/Mapping.js @@ -10,10 +10,10 @@ import Topic from '../Topic' const Mapping = Backbone.Model.extend({ urlRoot: '/mappings', blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - initialize: function () { + initialize: function() { if (this.isNew()) { this.set({ 'user_id': Active.Mapper.id, @@ -21,14 +21,14 @@ const Mapping = Backbone.Model.extend({ }) } }, - getMap: function () { + getMap: function() { return Map.get(this.get('map_id')) }, - getTopic: function () { + getTopic: function() { if (this.get('mappable_type') !== 'Topic') return false return Topic.get(this.get('mappable_id')) }, - getSynapse: function () { + getSynapse: function() { if (this.get('mappable_type') !== 'Synapse') return false return Synapse.get(this.get('mappable_id')) } diff --git a/frontend/src/Metamaps/DataModel/Message.js b/frontend/src/Metamaps/DataModel/Message.js index 00f1cf4b..5def166f 100644 --- a/frontend/src/Metamaps/DataModel/Message.js +++ b/frontend/src/Metamaps/DataModel/Message.js @@ -5,7 +5,7 @@ try { Backbone.$ = window.$ } catch (err) {} const Message = Backbone.Model.extend({ urlRoot: '/messages', blacklist: ['created_at', 'updated_at'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) } }) diff --git a/frontend/src/Metamaps/DataModel/Metacode.js b/frontend/src/Metamaps/DataModel/Metacode.js index fbd13755..e7ee5a31 100644 --- a/frontend/src/Metamaps/DataModel/Metacode.js +++ b/frontend/src/Metamaps/DataModel/Metacode.js @@ -3,13 +3,13 @@ try { Backbone.$ = window.$ } catch (err) {} import outdent from 'outdent' const Metacode = Backbone.Model.extend({ - initialize: function () { + initialize: function() { var image = new window.Image() image.crossOrigin = 'Anonymous' image.src = this.get('icon') this.set('image', image) }, - prepareLiForFilter: function () { + prepareLiForFilter: function() { return outdent`
    • ${this.get('name')} diff --git a/frontend/src/Metamaps/DataModel/MetacodeCollection.js b/frontend/src/Metamaps/DataModel/MetacodeCollection.js index 03c41613..80022780 100644 --- a/frontend/src/Metamaps/DataModel/MetacodeCollection.js +++ b/frontend/src/Metamaps/DataModel/MetacodeCollection.js @@ -6,7 +6,7 @@ import Metacode from './Metacode' const MetacodeCollection = Backbone.Collection.extend({ model: Metacode, url: '/metacodes', - comparator: function (a, b) { + comparator: function(a, b) { a = a.get('name').toLowerCase() b = b.get('name').toLowerCase() return a > b ? 1 : a < b ? -1 : 0 diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index a60611a3..e5002824 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -17,10 +17,10 @@ import DataModel from './index' const Synapse = Backbone.Model.extend({ urlRoot: '/synapses', blacklist: ['edge', 'created_at', 'updated_at'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - save: function (key, val, options) { + save: function(key, val, options) { var attrs // Handle both `"key", value` and `{key: value}` -style arguments. @@ -36,7 +36,7 @@ const Synapse = Backbone.Model.extend({ var permBefore = this.get('permission') - newOptions.success = function (model, response, opt) { + newOptions.success = function(model, response, opt) { if (s) s(model, response, opt) model.trigger('saved') @@ -48,7 +48,7 @@ const Synapse = Backbone.Model.extend({ } return Backbone.Model.prototype.save.call(this, attrs, newOptions) }, - initialize: function () { + initialize: function() { if (this.isNew()) { this.set({ 'user_id': Active.Mapper.id, @@ -60,7 +60,7 @@ const Synapse = Backbone.Model.extend({ this.on('changeByOther', this.updateCardView) this.on('change', this.updateEdgeView) this.on('saved', this.savedEvent) - this.on('noLongerPrivate', function () { + this.on('noLongerPrivate', function() { var newSynapseData = { mappingid: this.getMapping().id, mappableid: this.id @@ -68,7 +68,7 @@ const Synapse = Backbone.Model.extend({ $(document).trigger(JIT.events.newSynapse, [newSynapseData]) }) - this.on('nowPrivate', function () { + this.on('nowPrivate', function() { $(document).trigger(JIT.events.removeSynapse, [{ mappableid: this.id }]) @@ -76,28 +76,28 @@ const Synapse = Backbone.Model.extend({ this.on('change:desc', Filter.checkSynapses, this) }, - prepareLiForFilter: function () { + prepareLiForFilter: function() { return outdent`
    • synapse icon

      ${this.get('desc')}

    • ` }, - authorizeToEdit: function (mapper) { + authorizeToEdit: function(mapper) { if (mapper && (this.get('calculated_permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true else return false }, - authorizePermissionChange: function (mapper) { + authorizePermissionChange: function(mapper) { if (mapper && this.get('user_id') === mapper.get('id')) return true else return false }, - getTopic1: function () { + getTopic1: function() { return DataModel.Topics.get(this.get('topic1_id')) }, - getTopic2: function () { + getTopic2: function() { return DataModel.Topics.get(this.get('topic2_id')) }, - getDirection: function () { + getDirection: function() { var t1 = this.getTopic1() var t2 = this.getTopic2() @@ -106,7 +106,7 @@ const Synapse = Backbone.Model.extend({ t2.get('node').id ] : false }, - getMapping: function () { + getMapping: function() { if (!Active.Map) return false return DataModel.Mappings.findWhere({ @@ -115,7 +115,7 @@ const Synapse = Backbone.Model.extend({ mappable_id: this.isNew() ? this.cid : this.id }) }, - createEdge: function (providedMapping) { + createEdge: function(providedMapping) { var mapping, mappingID var synapseID = this.isNew() ? this.cid : this.id @@ -137,7 +137,7 @@ const Synapse = Backbone.Model.extend({ return edge }, - updateEdge: function () { + updateEdge: function() { var mapping var edge = this.get('edge') edge.getData('synapses').push(this) @@ -149,14 +149,14 @@ const Synapse = Backbone.Model.extend({ return edge }, - savedEvent: function () { + savedEvent: function() { Realtime.updateSynapse(this) }, - updateViews: function () { + updateViews: function() { this.updateCardView() this.updateEdgeView() }, - updateCardView: function () { + updateCardView: function() { var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') @@ -165,7 +165,7 @@ const Synapse = Backbone.Model.extend({ SynapseCard.showCard(edge) } }, - updateEdgeView: function () { + updateEdgeView: function() { var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js index d8426c92..1e27d138 100644 --- a/frontend/src/Metamaps/DataModel/Topic.js +++ b/frontend/src/Metamaps/DataModel/Topic.js @@ -16,10 +16,10 @@ import DataModel from './index' const Topic = Backbone.Model.extend({ urlRoot: '/topics', blacklist: ['node', 'created_at', 'updated_at', 'user_name', 'user_image', 'map_count', 'synapse_count'], - toJSON: function (options) { + toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - save: function (key, val, options) { + save: function(key, val, options) { var attrs // Handle both `"key", value` and `{key: value}` -style arguments. @@ -35,7 +35,7 @@ const Topic = Backbone.Model.extend({ var permBefore = this.get('permission') - newOptions.success = function (model, response, opt) { + newOptions.success = function(model, response, opt) { if (s) s(model, response, opt) model.trigger('saved') model.set('calculated_permission', model.get('permission')) @@ -48,7 +48,7 @@ const Topic = Backbone.Model.extend({ } return Backbone.Model.prototype.save.call(this, attrs, newOptions) }, - initialize: function () { + initialize: function() { if (this.isNew()) { this.set({ 'user_id': Active.Mapper.id, @@ -61,14 +61,14 @@ const Topic = Backbone.Model.extend({ this.on('changeByOther', this.updateCardView) this.on('change', this.updateNodeView) this.on('saved', this.savedEvent) - this.on('nowPrivate', function () { + this.on('nowPrivate', function() { var removeTopicData = { mappableid: this.id } $(document).trigger(JIT.events.removeTopic, [removeTopicData]) }) - this.on('noLongerPrivate', function () { + this.on('noLongerPrivate', function() { var newTopicData = { mappingid: this.getMapping().id, mappableid: this.id @@ -79,7 +79,7 @@ const Topic = Backbone.Model.extend({ this.on('change:metacode_id', Filter.checkMetacodes, this) }, - authorizeToEdit: function (mapper) { + authorizeToEdit: function(mapper) { if (mapper && (this.get('user_id') === mapper.get('id') || this.get('calculated_permission') === 'commons' || @@ -89,15 +89,15 @@ const Topic = Backbone.Model.extend({ return false } }, - authorizePermissionChange: function (mapper) { + authorizePermissionChange: function(mapper) { if (mapper && this.get('user_id') === mapper.get('id')) return true else return false }, - getDate: function () {}, - getMetacode: function () { + getDate: function() {}, + getMetacode: function() { return DataModel.Metacodes.get(this.get('metacode_id')) }, - getMapping: function () { + getMapping: function() { if (!Active.Map) return false return DataModel.Mappings.findWhere({ @@ -106,7 +106,7 @@ const Topic = Backbone.Model.extend({ mappable_id: this.isNew() ? this.cid : this.id }) }, - createNode: function () { + createNode: function() { var mapping var node = { adjacencies: [], @@ -124,7 +124,7 @@ const Topic = Backbone.Model.extend({ return node }, - updateNode: function () { + updateNode: function() { var mapping var node = this.get('node') node.setData('topic', this) @@ -136,10 +136,10 @@ const Topic = Backbone.Model.extend({ return node }, - savedEvent: function () { + savedEvent: function() { Realtime.updateTopic(this) }, - updateViews: function () { + updateViews: function() { var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there @@ -153,7 +153,7 @@ const Topic = Backbone.Model.extend({ Visualize.mGraph.plot() } }, - updateCardView: function () { + updateCardView: function() { var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there @@ -161,7 +161,7 @@ const Topic = Backbone.Model.extend({ TopicCard.showCard(node) } }, - updateNodeView: function () { + updateNodeView: function() { var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') diff --git a/frontend/src/Metamaps/DataModel/index.js b/frontend/src/Metamaps/DataModel/index.js index 4e62113d..6235b879 100644 --- a/frontend/src/Metamaps/DataModel/index.js +++ b/frontend/src/Metamaps/DataModel/index.js @@ -54,7 +54,7 @@ const DataModel = { Synapses: new SynapseCollection(), Topics: new TopicCollection(), - init: function (serverData) { + init: function(serverData) { var self = DataModel // workaround circular import problem @@ -101,18 +101,18 @@ const DataModel = { self.attachCollectionEvents() }, - attachCollectionEvents: function () { - DataModel.Topics.on('add remove', function (topic) { + attachCollectionEvents: function() { + DataModel.Topics.on('add remove', function(topic) { InfoBox.updateNumbers() Filter.checkMetacodes() Filter.checkMappers() }) - DataModel.Synapses.on('add remove', function (synapse) { + DataModel.Synapses.on('add remove', function(synapse) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMappers() }) - DataModel.Mappings.on('add remove', function (mapping) { + DataModel.Mappings.on('add remove', function(mapping) { InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMetacodes() diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 271e5758..8f45423e 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -23,7 +23,7 @@ const Filter = { }, isOpen: false, changing: false, - init: function () { + init: function() { var self = Filter $('.sidebarFilterIcon').click(self.toggleBox) @@ -38,7 +38,7 @@ const Filter = { self.bindLiClicks() self.getFilterData() }, - toggleBox: function (event) { + toggleBox: function(event) { var self = Filter if (self.isOpen) self.close() @@ -46,7 +46,7 @@ const Filter = { event.stopPropagation() }, - open: function () { + open: function() { var self = Filter GlobalUI.Account.close() @@ -56,26 +56,26 @@ const Filter = { self.changing = true var height = $(document).height() - 108 - $('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function () { + $('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function() { self.changing = false self.isOpen = true }) } }, - close: function () { + close: function() { var self = Filter $('.sidebarFilterIcon div').removeClass('hide') if (!self.changing) { self.changing = true - $('.sidebarFilterBox').fadeOut(200, function () { + $('.sidebarFilterBox').fadeOut(200, function() { self.changing = false self.isOpen = false }) } }, - reset: function () { + reset: function() { var self = Filter self.filters.metacodes = [] @@ -95,30 +95,30 @@ const Filter = { Most of this data essentially depends on the ruby function which are happening for filter inside view filterBox But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses */ - getFilterData: function () { + getFilterData: function() { var self = Filter var metacode, mapper, synapse - $('#filter_by_metacode li').each(function () { + $('#filter_by_metacode li').each(function() { metacode = $(this).attr('data-id') self.filters.metacodes.push(metacode) self.visible.metacodes.push(metacode) }) - $('#filter_by_mapper li').each(function () { + $('#filter_by_mapper li').each(function() { mapper = ($(this).attr('data-id')) self.filters.mappers.push(mapper) self.visible.mappers.push(mapper) }) - $('#filter_by_synapse li').each(function () { + $('#filter_by_synapse li').each(function() { synapse = ($(this).attr('data-id')) self.filters.synapses.push(synapse) self.visible.synapses.push(synapse) }) }, - bindLiClicks: function () { + bindLiClicks: function() { var self = Filter $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode) $('#filter_by_mapper ul li').unbind().click(self.toggleMapper) @@ -129,7 +129,7 @@ const Filter = { /* @param */ - updateFilters: function (collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { + updateFilters: function(collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { var self = Filter var newList = [] @@ -139,7 +139,7 @@ const Filter = { // the first option enables us to accept // ['Topics', 'Synapses'] as 'collection' if (typeof collection === 'object') { - DataModel[collection[0]].each(function (model) { + DataModel[collection[0]].each(function(model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -148,7 +148,7 @@ const Filter = { } } }) - DataModel[collection[1]].each(function (model) { + DataModel[collection[1]].each(function(model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -158,7 +158,7 @@ const Filter = { } }) } else if (typeof collection === 'string') { - DataModel[collection].each(function (model) { + DataModel[collection].each(function(model) { var prop = model.get(propertyToCheck) if (prop !== null) { prop = prop.toString() @@ -173,8 +173,8 @@ const Filter = { added = _.difference(newList, self.filters[filtersToUse]) // remove the list items for things no longer present on the map - _.each(removed, function (identifier) { - $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function () { + _.each(removed, function(identifier) { + $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function() { $(this).remove() }) const index = self.visible[filtersToUse].indexOf(identifier) @@ -182,13 +182,13 @@ const Filter = { }) var model, li, jQueryLi - function sortAlpha (a, b) { + function sortAlpha(a, b) { return a.childNodes[1].innerHTML.toLowerCase() > b.childNodes[1].innerHTML.toLowerCase() ? 1 : -1 } // for each new filter to be added, create a list item for it and fade it in - _.each(added, function (identifier) { + _.each(added, function(identifier) { model = DataModel[correlatedModel].get(identifier) || - DataModel[correlatedModel].find(function (model) { + DataModel[correlatedModel].find(function(model) { return model.get(propertyToCheck) === identifier }) li = model.prepareLiForFilter() @@ -204,25 +204,24 @@ const Filter = { // make sure clicks on list items still trigger the right events self.bindLiClicks() }, - checkMetacodes: function () { + checkMetacodes: function() { var self = Filter self.updateFilters('Topics', 'metacode_id', 'Metacodes', 'metacodes', 'metacode') }, - checkMappers: function () { + checkMappers: function() { var self = Filter - var onMap = Active.Map ? true : false - if (onMap) { + if (Active.Map) { self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') } else { // on topic view self.updateFilters(['Topics', 'Synapses'], 'user_id', 'Creators', 'mappers', 'mapper') } }, - checkSynapses: function () { + checkSynapses: function() { var self = Filter self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') }, - filterAllMetacodes: function (e) { + filterAllMetacodes: function(e) { var self = Filter $('#filter_by_metacode ul li').addClass('toggledOff') $('.showAllMetacodes').removeClass('active') @@ -230,7 +229,7 @@ const Filter = { self.visible.metacodes = [] self.passFilters() }, - filterNoMetacodes: function (e) { + filterNoMetacodes: function(e) { var self = Filter $('#filter_by_metacode ul li').removeClass('toggledOff') $('.showAllMetacodes').addClass('active') @@ -238,7 +237,7 @@ const Filter = { self.visible.metacodes = self.filters.metacodes.slice() self.passFilters() }, - filterAllMappers: function (e) { + filterAllMappers: function(e) { var self = Filter $('#filter_by_mapper ul li').addClass('toggledOff') $('.showAllMappers').removeClass('active') @@ -246,7 +245,7 @@ const Filter = { self.visible.mappers = [] self.passFilters() }, - filterNoMappers: function (e) { + filterNoMappers: function(e) { var self = Filter $('#filter_by_mapper ul li').removeClass('toggledOff') $('.showAllMappers').addClass('active') @@ -254,7 +253,7 @@ const Filter = { self.visible.mappers = self.filters.mappers.slice() self.passFilters() }, - filterAllSynapses: function (e) { + filterAllSynapses: function(e) { var self = Filter $('#filter_by_synapse ul li').addClass('toggledOff') $('.showAllSynapses').removeClass('active') @@ -262,7 +261,7 @@ const Filter = { self.visible.synapses = [] self.passFilters() }, - filterNoSynapses: function (e) { + filterNoSynapses: function(e) { var self = Filter $('#filter_by_synapse ul li').removeClass('toggledOff') $('.showAllSynapses').addClass('active') @@ -273,28 +272,27 @@ const Filter = { // an abstraction function for toggleMetacode, toggleMapper, toggleSynapse // to reduce code redundancy // gets called in the context of a list item in a filter box - toggleLi: function (whichToFilter) { - var self = Filter, index + toggleLi: function(whichToFilter) { + var self = Filter var id = $(this).attr('data-id') - if (self.visible[whichToFilter].indexOf(id) == -1) { + if (self.visible[whichToFilter].indexOf(id) === -1) { self.visible[whichToFilter].push(id) $(this).removeClass('toggledOff') } else { - index = self.visible[whichToFilter].indexOf(id) + const index = self.visible[whichToFilter].indexOf(id) self.visible[whichToFilter].splice(index, 1) $(this).addClass('toggledOff') } self.passFilters() }, - toggleMetacode: function () { + toggleMetacode: function() { var self = Filter self.toggleLi.call(this, 'metacodes') if (self.visible.metacodes.length === self.filters.metacodes.length) { $('.showAllMetacodes').addClass('active') $('.hideAllMetacodes').removeClass('active') - } - else if (self.visible.metacodes.length === 0) { + } else if (self.visible.metacodes.length === 0) { $('.showAllMetacodes').removeClass('active') $('.hideAllMetacodes').addClass('active') } else { @@ -302,15 +300,14 @@ const Filter = { $('.hideAllMetacodes').removeClass('active') } }, - toggleMapper: function () { + toggleMapper: function() { var self = Filter self.toggleLi.call(this, 'mappers') if (self.visible.mappers.length === self.filters.mappers.length) { $('.showAllMappers').addClass('active') $('.hideAllMappers').removeClass('active') - } - else if (self.visible.mappers.length === 0) { + } else if (self.visible.mappers.length === 0) { $('.showAllMappers').removeClass('active') $('.hideAllMappers').addClass('active') } else { @@ -318,15 +315,14 @@ const Filter = { $('.hideAllMappers').removeClass('active') } }, - toggleSynapse: function () { + toggleSynapse: function() { var self = Filter self.toggleLi.call(this, 'synapses') if (self.visible.synapses.length === self.filters.synapses.length) { $('.showAllSynapses').addClass('active') $('.hideAllSynapses').removeClass('active') - } - else if (self.visible.synapses.length === 0) { + } else if (self.visible.synapses.length === 0) { $('.showAllSynapses').removeClass('active') $('.hideAllSynapses').addClass('active') } else { @@ -334,71 +330,65 @@ const Filter = { $('.hideAllSynapses').removeClass('active') } }, - passFilters: function () { + passFilters: function() { var self = Filter var visible = self.visible var passesMetacode, passesMapper, passesSynapse - var onMap - if (Active.Map) { - onMap = true - } - else if (Active.Topic) { - onMap = false - } + var opacityForFilter = Active.Map ? 0 : 0.4 - var opacityForFilter = onMap ? 0 : 0.4 - - DataModel.Topics.each(function (topic) { + DataModel.Topics.each(function(topic) { var n = topic.get('node') - var metacode_id = topic.get('metacode_id').toString() + var metacodeId = topic.get('metacode_id').toString() - if (visible.metacodes.indexOf(metacode_id) == -1) passesMetacode = false + if (visible.metacodes.indexOf(metacodeId) === -1) passesMetacode = false else passesMetacode = true - if (onMap) { + if (Active.Map) { // when on a map, // we filter by mapper according to the person who added the // topic or synapse to the map - var user_id = topic.getMapping().get('user_id').toString() - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + let userId = topic.getMapping().get('user_id').toString() + if (visible.mappers.indexOf(userId) === -1) passesMapper = false else passesMapper = true } else { // when on a topic view, // we filter by mapper according to the person who created the // topic or synapse - var user_id = topic.get('user_id').toString() - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + let userId = topic.get('user_id').toString() + if (visible.mappers.indexOf(userId) === -1) passesMapper = false else passesMapper = true } if (passesMetacode && passesMapper) { if (n) { n.setData('alpha', 1, 'end') + } else { + console.log(topic) } - else console.log(topic) } else { if (n) { Control.deselectNode(n, true) n.setData('alpha', opacityForFilter, 'end') - n.eachAdjacency(function (e) { + n.eachAdjacency(function(e) { Control.deselectEdge(e, true) }) + } else { + console.log(topic) } - else console.log(topic) } }) // flag all the edges back to 'untouched' - DataModel.Synapses.each(function (synapse) { + DataModel.Synapses.each(function(synapse) { var e = synapse.get('edge') e.setData('touched', false) }) - DataModel.Synapses.each(function (synapse) { + DataModel.Synapses.each(function(synapse) { var e = synapse.get('edge') var desc - var user_id = synapse.get('user_id').toString() + var userId = synapse.get('user_id').toString() if (e && !e.getData('touched')) { var synapses = e.getData('synapses') @@ -406,7 +396,7 @@ const Filter = { // if any of the synapses represent by the edge are still unfiltered // leave the edge visible passesSynapse = false - for (var i = 0; i < synapses.length; i++) { + for (let i = 0; i < synapses.length; i++) { desc = synapses[i].get('desc') if (visible.synapses.indexOf(desc) > -1) passesSynapse = true } @@ -416,9 +406,9 @@ const Filter = { var displayIndex = e.getData('displayIndex') ? e.getData('displayIndex') : 0 var displayedSynapse = synapses[displayIndex] desc = displayedSynapse.get('desc') - if (passesSynapse && visible.synapses.indexOf(desc) == -1) { + if (passesSynapse && visible.synapses.indexOf(desc) === -1) { // iterate and find an unfiltered one - for (var i = 0; i < synapses.length; i++) { + for (let i = 0; i < synapses.length; i++) { desc = synapses[i].get('desc') if (visible.synapses.indexOf(desc) > -1) { e.setData('displayIndex', i) @@ -427,13 +417,13 @@ const Filter = { } } - if (onMap) { + if (Active.Map) { // when on a map, // we filter by mapper according to the person who added the // topic or synapse to the map - user_id = synapse.getMapping().get('user_id').toString() + userId = synapse.getMapping().get('user_id').toString() } - if (visible.mappers.indexOf(user_id) == -1) passesMapper = false + if (visible.mappers.indexOf(userId) === -1) passesMapper = false else passesMapper = true var color = Settings.colors.synapses.normal @@ -446,8 +436,9 @@ const Filter = { } e.setData('touched', true) + } else if (!e) { + console.log(synapse) } - else if (!e) console.log(synapse) }) // run the animation diff --git a/frontend/src/Metamaps/GlobalUI/Account.js b/frontend/src/Metamaps/GlobalUI/Account.js index 210627ff..a2823ef5 100644 --- a/frontend/src/Metamaps/GlobalUI/Account.js +++ b/frontend/src/Metamaps/GlobalUI/Account.js @@ -5,16 +5,16 @@ import Filter from '../Filter' const Account = { isOpen: false, changing: false, - init: function () { + init: function() { var self = Account $('.sidebarAccountIcon').click(self.toggleBox) - $('.sidebarAccountBox').click(function (event) { + $('.sidebarAccountBox').click(function(event) { event.stopPropagation() }) $('body').click(self.close) }, - toggleBox: function (event) { + toggleBox: function(event) { var self = Account if (self.isOpen) self.close() @@ -22,7 +22,7 @@ const Account = { event.stopPropagation() }, - open: function () { + open: function() { var self = Account Filter.close() @@ -30,21 +30,21 @@ const Account = { if (!self.isOpen && !self.changing) { self.changing = true - $('.sidebarAccountBox').fadeIn(200, function () { + $('.sidebarAccountBox').fadeIn(200, function() { self.changing = false self.isOpen = true $('.sidebarAccountBox #user_email').focus() }) } }, - close: function () { + close: function() { var self = Account $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') if (!self.changing) { self.changing = true $('.sidebarAccountBox #user_email').blur() - $('.sidebarAccountBox').fadeOut(200, function () { + $('.sidebarAccountBox').fadeOut(200, function() { self.changing = false self.isOpen = false }) diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js index 11c8deb9..9a4d8770 100644 --- a/frontend/src/Metamaps/GlobalUI/CreateMap.js +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -13,7 +13,7 @@ const CreateMap = { emptyForkMapForm: '', topicsToMap: [], synapsesToMap: [], - init: function () { + init: function() { var self = CreateMap self.newMap = new DataModelMap({ permission: 'commons' }) @@ -22,14 +22,14 @@ const CreateMap = { self.emptyMapForm = $('#new_map').html() }, - bindFormEvents: function () { + bindFormEvents: function() { var self = CreateMap - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { if (event.keyCode === 13) self.submit() }) - $('.new_map button.cancel').unbind().bind('click', function (event) { + $('.new_map button.cancel').unbind().bind('click', function(event) { event.preventDefault() GlobalUI.closeLightbox() }) @@ -38,12 +38,12 @@ const CreateMap = { // bind permission changer events on the createMap form $('.permIcon').unbind().bind('click', self.switchPermission) }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function () { + closeSuccess: function() { + $('#mapCreatedSuccess').fadeOut(300, function() { $(this).remove() }) }, - switchPermission: function () { + switchPermission: function() { var self = CreateMap self.newMap.set('permission', $(this).attr('data-permission')) @@ -53,7 +53,7 @@ const CreateMap = { var permText = $(this).find('.tip').html() $(this).parents('.new_map').find('.permText').html(permText) }, - submit: function (event) { + submit: function(event) { if (event) event.preventDefault() var self = CreateMap @@ -82,22 +82,20 @@ const CreateMap = { GlobalUI.closeLightbox() GlobalUI.notifyUser('Working...') }, - throwMapNameError: function () { - + throwMapNameError: function() { var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' var $form = $(formId) var message = $("") $form.find('#map_name').after(message) - setTimeout(function () { - message.fadeOut('fast', function () { + setTimeout(function() { + message.fadeOut('fast', function() { message.remove() }) }, 5000) }, - success: function (model) { - var self = CreateMap + success: function(model) { // push the new map onto the collection of 'my maps' DataModel.Maps.Mine.add(model) @@ -117,7 +115,7 @@ const CreateMap = { return false }) }, - reset: function (id) { + reset: function(id) { var self = CreateMap var form = $('#' + id) diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js index 96f9524f..dfc319b7 100644 --- a/frontend/src/Metamaps/GlobalUI/ImportDialog.js +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -12,7 +12,7 @@ const ImportDialog = { openLightbox: null, closeLightbox: null, - init: function (serverData, openLightbox, closeLightbox) { + init: function(serverData, openLightbox, closeLightbox) { const self = ImportDialog self.openLightbox = openLightbox self.closeLightbox = closeLightbox @@ -27,10 +27,10 @@ const ImportDialog = { exampleImageUrl: serverData['import-example.png'] }), $('.importDialogWrapper').get(0)) }, - show: function () { + show: function() { ImportDialog.openLightbox('import-dialog') }, - hide: function () { + hide: function() { ImportDialog.closeLightbox('import-dialog') } } diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index 0859b941..a37b62a8 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -10,7 +10,7 @@ const Search = { limitMapsToMe: false, changing: false, optionsInitialized: false, - init: function (serverData) { + init: function(serverData) { var self = Search self.wildcardIconUrl = serverData['icons/wildcard.png'] @@ -24,10 +24,10 @@ const Search = { loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - $('.sidebarSearchIcon').click(function (e) { + $('.sidebarSearchIcon').click(function(e) { $('.sidebarSearchField').focus() }) - $('.sidebarSearch').click(function (e) { + $('.sidebarSearch').click(function(e) { e.stopPropagation() }) @@ -36,7 +36,7 @@ const Search = { focus: function() { $('.sidebarSearchField').focus() }, - startTypeahead: function () { + startTypeahead: function() { var self = Search var mapheader = Active.Mapper ? '

      Maps

      ' : '

      Maps

      ' @@ -49,7 +49,7 @@ const Search = { display: s => s.label, templates: { - notFound: function (s) { + notFound: function(s) { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ value: 'No results', label: 'No results', @@ -58,7 +58,7 @@ const Search = { }) }, header: topicheader, - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile($('#topicSearchTemplate').html()).render(s) } }, @@ -67,7 +67,7 @@ const Search = { queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/topics', - prepare: function (query, settings) { + prepare: function(query, settings) { settings.url += '?term=' + query if (Active.Mapper && self.limitTopicsToMe) { settings.url += '&user=' + Active.Mapper.id.toString() @@ -83,7 +83,7 @@ const Search = { limit: 9999, display: s => s.label, templates: { - notFound: function (s) { + notFound: function(s) { return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ value: 'No results', label: 'No results', @@ -91,7 +91,7 @@ const Search = { }) }, header: mapheader, - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile($('#mapSearchTemplate').html()).render(s) } }, @@ -100,7 +100,7 @@ const Search = { queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/maps', - prepare: function (query, settings) { + prepare: function(query, settings) { settings.url += '?term=' + query if (Active.Mapper && self.limitMapsToMe) { settings.url += '&user=' + Active.Mapper.id.toString() @@ -116,7 +116,7 @@ const Search = { limit: 9999, display: s => s.label, templates: { - notFound: function (s) { + notFound: function(s) { return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ value: 'No results', label: 'No results', @@ -125,7 +125,7 @@ const Search = { }) }, header: mapperheader, - suggestion: function (s) { + suggestion: function(s) { return Hogan.compile($('#mapperSearchTemplate').html()).render(s) } }, @@ -148,7 +148,7 @@ const Search = { ) // Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:render', function (event) { + $('.sidebarSearchField').bind('typeahead:render', function(event) { self.initSearchOptions() self.hideLoader() var h = $(window).height() @@ -160,7 +160,7 @@ const Search = { $('#limitMapsToMe').prop('checked', true) } }) - $(window).resize(function () { + $(window).resize(function() { var h = $(window).height() $('.tt-dropdown-menu').css('max-height', h - 100) }) @@ -169,12 +169,12 @@ const Search = { $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) // don't do it, if they clicked on a 'addToMap' button - $('.sidebarSearch button.addToMap').click(function (event) { + $('.sidebarSearch button.addToMap').click(function(event) { event.stopPropagation() }) // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function () { + $('.sidebarSearchField.tt-input').keyup(function() { if ($('.sidebarSearchField.tt-input').val() === '') { self.hideLoader() } else { @@ -182,7 +182,7 @@ const Search = { } }) }, - handleResultClick: function (event, datum, dataset) { + handleResultClick: function(event, datum, dataset) { var self = Search self.hideLoader() @@ -197,10 +197,10 @@ const Search = { } } }, - initSearchOptions: function () { + initSearchOptions: function() { var self = Search - function toggleResultSet (set) { + function toggleResultSet(set) { var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') if (s.is(':visible')) { s.hide() @@ -211,7 +211,7 @@ const Search = { } } - $('.limitToMe').unbind().bind('change', function (e) { + $('.limitToMe').unbind().bind('change', function(e) { if ($(this).attr('id') === 'limitTopicsToMe') { self.limitTopicsToMe = !self.limitTopicsToMe } @@ -227,20 +227,20 @@ const Search = { }) // when the user clicks minimize section, hide the results for that section - $('.minimizeMapperResults').unbind().click(function (e) { + $('.minimizeMapperResults').unbind().click(function(e) { toggleResultSet.call(this, 'mappers') }) - $('.minimizeTopicResults').unbind().click(function (e) { + $('.minimizeTopicResults').unbind().click(function(e) { toggleResultSet.call(this, 'topics') }) - $('.minimizeMapResults').unbind().click(function (e) { + $('.minimizeMapResults').unbind().click(function(e) { toggleResultSet.call(this, 'maps') }) }, - hideLoader: function () { + hideLoader: function() { $('#searchLoading').hide() }, - showLoader: function () { + showLoader: function() { $('#searchLoading').show() } } diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index d1a0b6ba..95e484f8 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -12,7 +12,7 @@ import ImportDialog from './ImportDialog' const GlobalUI = { notifyTimeout: null, lightbox: null, - init: function (serverData) { + init: function(serverData) { var self = GlobalUI self.Search.init(serverData) @@ -23,7 +23,7 @@ const GlobalUI = { if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) // bind lightbox clicks - $('.openLightbox').click(function (event) { + $('.openLightbox').click(function(event) { self.openLightbox($(this).attr('data-open')) event.preventDefault() return false @@ -31,18 +31,18 @@ const GlobalUI = { $('#lightbox_screen, #lightbox_close').click(self.closeLightbox) }, - showDiv: function (selector) { + showDiv: function(selector) { $(selector).show() $(selector).animate({ opacity: 1 }, 200, 'easeOutCubic') }, - hideDiv: function (selector) { + hideDiv: function(selector) { $(selector).animate({ opacity: 0 - }, 200, 'easeInCubic', function () { $(this).hide() }) + }, 200, 'easeInCubic', function() { $(this).hide() }) }, - openLightbox: function (which) { + openLightbox: function(which) { var self = GlobalUI $('.lightboxContent').hide() @@ -69,7 +69,7 @@ const GlobalUI = { } }, - closeLightbox: function (event) { + closeLightbox: function(event) { var self = GlobalUI if (event) event.preventDefault() @@ -83,7 +83,7 @@ const GlobalUI = { // fade the black overlay out $('#lightbox_screen').animate({ 'opacity': '0.0' - }, 200, function () { + }, 200, function() { $('#lightbox_overlay').hide() }) @@ -94,25 +94,25 @@ const GlobalUI = { } self.lightbox = null }, - notifyUser: function (message, leaveOpen) { + notifyUser: function(message, leaveOpen) { var self = GlobalUI $('#toast').html(message) self.showDiv('#toast') clearTimeout(self.notifyTimeOut) if (!leaveOpen) { - self.notifyTimeOut = setTimeout(function () { + self.notifyTimeOut = setTimeout(function() { self.hideDiv('#toast') }, 8000) } }, - clearNotify: function () { + clearNotify: function() { var self = GlobalUI clearTimeout(self.notifyTimeOut) self.hideDiv('#toast') }, - shareInvite: function (inviteLink) { + shareInvite: function(inviteLink) { clipboard.copy({ 'text/plain': inviteLink }).then(() => { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 8436c148..cde37a73 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -19,14 +19,14 @@ const Import = { synapseWhitelist: [ 'topic1', 'topic2', 'category', 'direction', 'desc', 'description', 'permission' ], - cidMappings: {}, // to be filled by import_id => cid mappings + cidMappings: {}, // to be filled by importId => cid mappings - handleTSV: function (text) { + handleTSV: function(text) { const results = Import.parseTabbedString(text) Import.handle(results) }, - handleCSV: function (text, parserOpts = {}) { + handleCSV: function(text, parserOpts = {}) { const self = Import const topicsRegex = /("?Topics"?)([\s\S]*)/mi @@ -37,14 +37,14 @@ const Import = { if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') // merge default options and extra options passed in parserOpts argument - const csv_parser_options = Object.assign({ + const csvParserOptions = Object.assign({ columns: true, // get headers relax_column_count: true, skip_empty_lines: true }, parserOpts) const topicsPromise = $.Deferred() - parse(topicsText, csv_parser_options, (err, data) => { + parse(topicsText, csvParserOptions, (err, data) => { if (err) { console.warn(err) return topicsPromise.resolve([]) @@ -53,7 +53,7 @@ const Import = { }) const synapsesPromise = $.Deferred() - parse(synapsesText, csv_parser_options, (err, data) => { + parse(synapsesText, csvParserOptions, (err, data) => { if (err) { console.warn(err) return synapsesPromise.resolve([]) @@ -62,11 +62,11 @@ const Import = { }) $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { - self.handle({ topics, synapses}) + self.handle({ topics, synapses }) }) }, - handleJSON: function (text) { + handleJSON: function(text) { const results = JSON.parse(text) Import.handle(results) }, @@ -80,13 +80,13 @@ const Import = { if (window.confirm('Are you sure you want to create ' + topics.length + ' new topics and ' + synapses.length + ' new synapses?')) { self.importTopics(topics) - //window.setTimeout(() => self.importSynapses(synapses), 5000) + // window.setTimeout(() => self.importSynapses(synapses), 5000) self.importSynapses(synapses) } // if } // if }, - parseTabbedString: function (text) { + parseTabbedString: function(text) { var self = Import // determine line ending and split lines @@ -113,9 +113,9 @@ const Import = { var topicHeaders = [] var synapseHeaders = [] - lines.forEach(function (line_raw, index) { - var line = line_raw.split('\t') - var noblanks = line.filter(function (elt) { + lines.forEach(function(lineRaw, index) { + const line = lineRaw.split('\t') + var noblanks = line.filter(function(elt) { return elt !== '' }) switch (state) { @@ -139,7 +139,7 @@ const Import = { self.abort('Not enough topic headers on line ' + index) state = STATES.ABORT } - topicHeaders = line.map(function (header, index) { + topicHeaders = line.map(function(header, index) { return self.normalizeKey(header) }) state = STATES.TOPICS @@ -150,7 +150,7 @@ const Import = { self.abort('Not enough synapse headers on line ' + index) state = STATES.ABORT } - synapseHeaders = line.map(function (header, index) { + synapseHeaders = line.map(function(header, index) { return self.normalizeKey(header) }) state = STATES.SYNAPSES @@ -165,7 +165,7 @@ const Import = { state = STATES.SYNAPSES_NEED_HEADERS } else { var topic = {} - line.forEach(function (field, index) { + line.forEach(function(field, index) { var header = topicHeaders[index] if (self.topicWhitelist.indexOf(header) === -1) return topic[header] = field @@ -186,7 +186,7 @@ const Import = { state = STATES.SYNAPSES_NEED_HEADERS } else { var synapse = {} - line.forEach(function (field, index) { + line.forEach(function(field, index) { var header = synapseHeaders[index] if (self.synapseWhitelist.indexOf(header) === -1) return synapse[header] = field @@ -212,7 +212,7 @@ const Import = { } }, - importTopics: function (parsedTopics) { + importTopics: function(parsedTopics) { var self = Import parsedTopics.forEach(topic => { @@ -227,7 +227,7 @@ const Import = { coords, name: topic.name, permission: topic.permission, - import_id: topic.id + importId: topic.id }) return // "continue" } @@ -239,10 +239,10 @@ const Import = { }) }, - importSynapses: function (parsedSynapses) { + importSynapses: function(parsedSynapses) { var self = Import - parsedSynapses.forEach(function (synapse) { + parsedSynapses.forEach(function(synapse) { // only createSynapseWithParameters once both topics are persisted // if there isn't a cidMapping, check by topic name instead var topic1 = DataModel.Topics.get(self.cidMappings[synapse.topic1]) @@ -277,31 +277,31 @@ const Import = { }) }, - createTopicWithParameters: function (name, metacode_name, permission, desc, - link, xloc, yloc, import_id, opts = {}) { + createTopicWithParameters: function(name, metacodeName, permission, desc, + link, xloc, yloc, importId, opts = {}) { var self = Import $(document).trigger(Map.events.editedByActiveMapper) - var metacode = DataModel.Metacodes.where({name: metacode_name})[0] || null + var metacode = DataModel.Metacodes.where({name: metacodeName})[0] || null if (metacode === null) { metacode = DataModel.Metacodes.where({ name: 'Wildcard' })[0] - console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') + console.warn("Couldn't find metacode " + metacodeName + ' so used Wildcard instead.') } - var topic_permission = permission || Active.Map.get('permission') - var defer_to_map_id = permission === topic_permission ? Active.Map.get('id') : null + const topicPermision = permission || Active.Map.get('permission') + var deferToMapId = permission === topicPermision ? Active.Map.get('id') : null var topic = new DataModel.Topic({ name: name, metacode_id: metacode.id, - permission: topic_permission, - defer_to_map_id: defer_to_map_id, + permission: topicPermision, + defer_to_map_id: deferToMapId, desc: desc || '', link: link || '', calculated_permission: Active.Map.get('permission') }) DataModel.Topics.add(topic) - if (import_id !== null && import_id !== undefined) { - self.cidMappings[import_id] = topic.cid + if (importId !== null && importId !== undefined) { + self.cidMappings[importId] = topic.cid } var mapping = new DataModel.Mapping({ @@ -320,7 +320,7 @@ const Import = { GlobalUI.hideDiv('#instructions') }, - createSynapseWithParameters: function (desc, category, permission, + createSynapseWithParameters: function(desc, category, permission, topic1, topic2) { var node1 = topic1.get('node') var node2 = topic2.get('node') @@ -348,7 +348,7 @@ const Import = { Synapse.renderSynapse(mapping, synapse, node1, node2, true) }, - handleURL: function (url, opts = {}) { + handleURL: function(url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { coords = AutoLayout.getNextCoord({ mappings: DataModel.Mappings }) @@ -356,7 +356,7 @@ const Import = { const name = opts.name || 'Link' const metacode = opts.metacode || 'Reference' - const import_id = opts.import_id || null // don't store a cidMapping + const importId = opts.importId || null // don't store a cidMapping const permission = opts.permission || null // use default const desc = opts.desc || url @@ -368,7 +368,7 @@ const Import = { url, coords.x, coords.y, - import_id, + importId, { success: function(topic) { if (topic.get('name') !== 'Link') return @@ -393,12 +393,12 @@ const Import = { * helper functions */ - abort: function (message) { + abort: function(message) { console.error(message) }, // TODO investigate replacing with es6 (?) trim() - simplify: function (string) { + simplify: function(string) { return string .replace(/(^\s*|\s*$)/g, '') .toLowerCase() diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 7cbce927..0f21f70b 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -13,7 +13,6 @@ import Filter from './Filter' import GlobalUI from './GlobalUI' import Map from './Map' import Mouse from './Mouse' -import Realtime from './Realtime' import Selected from './Selected' import Settings from './Settings' import Synapse from './Synapse' @@ -31,7 +30,7 @@ const JIT = { tempNode: null, tempNode2: null, mouseDownPix: {}, - dragFlag : 0, + dragFlag: 0, dragTolerance: 0, virtualPointer: {}, @@ -51,13 +50,13 @@ const JIT = { /** * This method will bind the event handlers it is interested and initialize the class. */ - init: function (serverData) { + init: function(serverData) { const self = JIT $('.zoomIn').click(self.zoomIn) $('.zoomOut').click(self.zoomOut) - const zoomExtents = function (event) { + const zoomExtents = function(event) { self.zoomExtents(event, Visualize.mGraph.canvas) } $('.zoomExtents').click(zoomExtents) @@ -73,7 +72,7 @@ const JIT = { /** * convert our topic JSON into something JIT can use */ - convertModelsToJIT: function (topics, synapses) { + convertModelsToJIT: function(topics, synapses) { const jitReady = [] const synapsesToRemove = [] @@ -84,11 +83,11 @@ const JIT = { let edge const edges = [] - topics.each(function (t) { + topics.each(function(t) { node = t.createNode() nodes[node.id] = node }) - synapses.each(function (s) { + synapses.each(function(s) { edge = s.createEdge() if (topics.get(s.get('topic1_id')) === undefined || topics.get(s.get('topic2_id')) === undefined) { @@ -119,13 +118,13 @@ const JIT = { } }) - _.each(nodes, function (node) { + _.each(nodes, function(node) { jitReady.push(node) }) return [jitReady, synapsesToRemove] }, - prepareVizData: function () { + prepareVizData: function() { const self = JIT let mapping @@ -138,7 +137,7 @@ const JIT = { self.vizData = results[0] // clean up the synapses array in case of any faulty data - _.each(results[1], function (synapse) { + _.each(results[1], function(synapse) { mapping = synapse.getMapping() DataModel.Synapses.remove(synapse) if (DataModel.Mappings) DataModel.Mappings.remove(mapping) @@ -160,7 +159,7 @@ const JIT = { Visualize.render() }, // prepareVizData - edgeRender: function (adj, canvas) { + edgeRender: function(adj, canvas) { // get nodes cartesian coordinates const pos = adj.nodeFrom.pos.getc(true) const posChild = adj.nodeTo.pos.getc(true) @@ -191,7 +190,7 @@ const JIT = { const showDesc = adj.getData('showDesc') - const drawSynapseCount = function (context, x, y, count) { + const drawSynapseCount = function(context, x, y, count) { /* circle size: 16x16px positioning: overlay and center on top right corner of synapse label - 8px left and 8px down @@ -287,7 +286,7 @@ const JIT = { // TODO fix tests so we don't need _.get transition: _.get($jit, 'Trans.Quad.easeInOut'), duration: 800, - onComplete: function () { + onComplete: function() { Visualize.mGraph.busy = false $(document).trigger(JIT.events.animationDone) } @@ -297,7 +296,7 @@ const JIT = { // TODO fix tests so we don't need _.get transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 800, - onComplete: function () { + onComplete: function() { Visualize.mGraph.busy = false } }, @@ -342,40 +341,40 @@ const JIT = { // Add Tips Tips: { enable: false, - onShow: function (tip, node) {} + onShow: function(tip, node) {} }, // Add node events Events: { enable: true, enableForEdges: true, - onMouseMove: function (node, eventInfo, e) { + onMouseMove: function(node, eventInfo, e) { JIT.onMouseMoveHandler(node, eventInfo, e) // console.log('called mouse move handler') }, // Update node positions when dragged - onDragMove: function (node, eventInfo, e) { + onDragMove: function(node, eventInfo, e) { JIT.onDragMoveTopicHandler(node, eventInfo, e) // console.log('called drag move handler') }, - onDragEnd: function (node, eventInfo, e) { + onDragEnd: function(node, eventInfo, e) { JIT.onDragEndTopicHandler(node, eventInfo, e, false) // console.log('called drag end handler') }, - onDragCancel: function (node, eventInfo, e) { + onDragCancel: function(node, eventInfo, e) { JIT.onDragCancelHandler(node, eventInfo, e, false) }, // Implement the same handler for touchscreens - onTouchStart: function (node, eventInfo, e) {}, + onTouchStart: function(node, eventInfo, e) {}, // Implement the same handler for touchscreens - onTouchMove: function (node, eventInfo, e) { + onTouchMove: function(node, eventInfo, e) { JIT.onDragMoveTopicHandler(node, eventInfo, e) }, // Implement the same handler for touchscreens - onTouchEnd: function (node, eventInfo, e) {}, + onTouchEnd: function(node, eventInfo, e) {}, // Implement the same handler for touchscreens - onTouchCancel: function (node, eventInfo, e) {}, + onTouchCancel: function(node, eventInfo, e) {}, // Add also a click handler to nodes - onClick: function (node, eventInfo, e) { + onClick: function(node, eventInfo, e) { // remove the rightclickmenu $('.rightclickmenu').remove() @@ -399,7 +398,7 @@ const JIT = { Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) - + return } } @@ -416,7 +415,7 @@ const JIT = { } // if }, // Add also a click handler to nodes - onRightClick: function (node, eventInfo, e) { + onRightClick: function(node, eventInfo, e) { // remove the rightclickmenu $('.rightclickmenu').remove() @@ -446,7 +445,7 @@ const JIT = { }, nodeSettings: { 'customNode': { - 'render': function (node, canvas) { + 'render': function(node, canvas) { const pos = node.pos.getc(true) const dim = node.getData('dim') const topic = node.getData('topic') @@ -495,7 +494,7 @@ const JIT = { ctx.drawImage(descImage, pos.x + dim - 8, pos.y - dim - 8, 16, 16) } }, - 'contains': function (node, pos) { + 'contains': function(node, pos) { const npos = node.pos.getc(true) const dim = node.getData('dim') const arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n') @@ -521,10 +520,10 @@ const JIT = { }, edgeSettings: { 'customEdge': { - 'render': function (adj, canvas) { + 'render': function(adj, canvas) { JIT.edgeRender(adj, canvas) }, - 'contains': function (adj, pos) { + 'contains': function(adj, pos) { const from = adj.nodeFrom.pos.getc() const to = adj.nodeTo.pos.getc() @@ -544,7 +543,7 @@ const JIT = { // TODO fix tests so we don't need _.get transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 2500, - onComplete: function () { + onComplete: function() { Visualize.mGraph.busy = false } }, @@ -603,18 +602,18 @@ const JIT = { enable: true, type: 'Native', i: 0, - onMouseMove: function (node, eventInfo, e) { + onMouseMove: function(node, eventInfo, e) { // if(this.i++ % 3) return const pos = eventInfo.getPos() Visualize.cameraPosition.x += (pos.x - Visualize.cameraPosition.x) * 0.5 Visualize.cameraPosition.y += (-pos.y - Visualize.cameraPosition.y) * 0.5 Visualize.mGraph.plot() }, - onMouseWheel: function (delta) { + onMouseWheel: function(delta) { Visualize.cameraPosition.z += -delta * 20 Visualize.mGraph.plot() }, - onClick: function () {} + onClick: function() {} }, // Number of iterations for the FD algorithm iterations: 200, @@ -632,7 +631,7 @@ const JIT = { animate: { modes: ['polar'], duration: 800, - onComplete: function () { + onComplete: function() { Visualize.mGraph.busy = false } }, @@ -647,7 +646,7 @@ const JIT = { }, levelDistance: 200 }, - onMouseEnter: function (edge) { + onMouseEnter: function(edge) { const filtered = edge.getData('alpha') === 0 // don't do anything if the edge is filtered @@ -670,7 +669,7 @@ const JIT = { }) Visualize.mGraph.plot() }, // onMouseEnter - onMouseLeave: function (edge) { + onMouseLeave: function(edge) { if (edge.getData('alpha') === 0) return // don't do anything if the edge is filtered $('canvas').css('cursor', 'default') const edgeIsSelected = Selected.Edges.indexOf(edge) @@ -688,7 +687,7 @@ const JIT = { }) Visualize.mGraph.plot() }, // onMouseLeave - onMouseMoveHandler: function (_node, eventInfo, e) { + onMouseMoveHandler: function(_node, eventInfo, e) { const self = JIT if (Visualize.mGraph.busy) return @@ -724,7 +723,7 @@ const JIT = { $('canvas').css('cursor', 'default') } }, // onMouseMoveHandler - enterKeyHandler: function () { + enterKeyHandler: function() { const creatingMap = GlobalUI.lightbox if (creatingMap === 'newmap' || creatingMap === 'forkmap') { GlobalUI.CreateMap.submit() @@ -734,11 +733,11 @@ const JIT = { Synapse.createSynapseLocally() } }, // enterKeyHandler - escKeyHandler: function () { + escKeyHandler: function() { Control.deselectAllEdges() Control.deselectAllNodes() }, // escKeyHandler - onDragMoveTopicHandler: function (node, eventInfo, e) { + onDragMoveTopicHandler: function(node, eventInfo, e) { var self = JIT var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) @@ -746,111 +745,109 @@ const JIT = { if (node && !node.nodeFrom) { self.handleSelectionBeforeDragging(node, e) - var pos = eventInfo.getPos(), - EDGE_THICKNESS = 30 /** Metamaps.Visualize.mGraph.canvas.scaleOffsetX*/, - SHIFT = 2 / Metamaps.Visualize.mGraph.canvas.scaleOffsetX, - PERIOD = 5; - - //self.virtualPointer = pos; + const pos = eventInfo.getPos() + const EDGE_THICKNESS = 30 + const SHIFT = 2 / Visualize.mGraph.canvas.scaleOffsetX + const PERIOD = 5 + + // self.virtualPointer = pos; + // if it's a left click, or a touch, move the node if (e.touches || (e.button === 0 && !e.altKey && !e.ctrlKey && (e.buttons === 0 || e.buttons === 1 || e.buttons === undefined))) { + const width = Visualize.mGraph.canvas.getSize().width + const height = Visualize.mGraph.canvas.getSize().height + const xPix = Util.coordsToPixels(Visualize.mGraph, pos).x + const yPix = Util.coordsToPixels(Visualize.mGraph, pos).y - var width = Visualize.mGraph.canvas.getSize().width, - height = Visualize.mGraph.canvas.getSize().height, - xPix = Util.coordsToPixels(Visualize.mGraph, pos).x, - yPix = Util.coordsToPixels(Visualize.mGraph, pos).y; - - if(self.dragFlag === 0){ - self.mouseDownPix = Util.coordsToPixels(Visualize.mGraph, eventInfo.getPos()); - self.dragFlag = 1; - } - - if(Util.getDistance(Util.coordsToPixels(Visualize.mGraph, pos),self.mouseDownPix) > 2 && !self.dragTolerance){ - self.dragTolerance = 1; + if (self.dragFlag === 0) { + self.mouseDownPix = Util.coordsToPixels(Visualize.mGraph, eventInfo.getPos()) + self.dragFlag = 1 } - if(xPix < EDGE_THICKNESS && self.dragTolerance ){ - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); + if (Util.getDistance(Util.coordsToPixels(Visualize.mGraph, pos), self.mouseDownPix) > 2 && !self.dragTolerance) { + self.dragTolerance = 1 + } + + if (xPix < EDGE_THICKNESS && self.dragTolerance) { + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: EDGE_THICKNESS, y: yPix }).x - SHIFT, y: pos.y } - Visualize.mGraph.canvas.translate(SHIFT, 0); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - - self.dragLeftEdge = setInterval( function(){ + Visualize.mGraph.canvas.translate(SHIFT, 0) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + + self.dragLeftEdge = setInterval(function() { self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: EDGE_THICKNESS, y: yPix }).x - SHIFT, y: pos.y } - Visualize.mGraph.canvas.translate(SHIFT, 0); - self.updateTopicPositions(node,self.virtualPointer); - Visualize.mGraph.plot(); - } , PERIOD); - - } - if(width - xPix < EDGE_THICKNESS && self.dragTolerance){ - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); - self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: width - EDGE_THICKNESS, y: yPix }).x + SHIFT, y: pos.y } - Visualize.mGraph.canvas.translate(-SHIFT, 0); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - - self.dragRightEdge = setInterval( function(){ - self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: width - EDGE_THICKNESS, y: yPix }).x + SHIFT, y: pos.y } - Visualize.mGraph.canvas.translate(-SHIFT, 0); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - } , PERIOD); - } - if(yPix < EDGE_THICKNESS && self.dragTolerance){ - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); - self.virtualPointer = { x:pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: EDGE_THICKNESS }).y - SHIFT } - Visualize.mGraph.canvas.translate(0, SHIFT); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - - self.dragTopEdge = setInterval( function(){ - self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: EDGE_THICKNESS }).y - SHIFT } - Visualize.mGraph.canvas.translate(0, SHIFT); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - } , PERIOD); - } - if(height - yPix < EDGE_THICKNESS && self.dragTolerance){ - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); - self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: height - EDGE_THICKNESS }).y + SHIFT } - Visualize.mGraph.canvas.translate(0, -SHIFT); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); - - self.dragBottomEdge = setInterval(function () { - self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: height - EDGE_THICKNESS }).y + SHIFT } - Visualize.mGraph.canvas.translate(0, -SHIFT); - self.updateTopicPositions(node, self.virtualPointer); - Visualize.mGraph.plot(); + Visualize.mGraph.canvas.translate(SHIFT, 0) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() }, PERIOD) } - - if(xPix >= EDGE_THICKNESS && width - xPix >= EDGE_THICKNESS && yPix >= EDGE_THICKNESS && height - yPix >= EDGE_THICKNESS) { - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); + if (width - xPix < EDGE_THICKNESS && self.dragTolerance) { + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) + self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: width - EDGE_THICKNESS, y: yPix }).x + SHIFT, y: pos.y } + Visualize.mGraph.canvas.translate(-SHIFT, 0) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() - self.updateTopicPositions(node,pos); + self.dragRightEdge = setInterval(function() { + self.virtualPointer = { x: Util.pixelsToCoords(Visualize.mGraph, { x: width - EDGE_THICKNESS, y: yPix }).x + SHIFT, y: pos.y } + Visualize.mGraph.canvas.translate(-SHIFT, 0) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + }, PERIOD) + } + if (yPix < EDGE_THICKNESS && self.dragTolerance) { + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) + self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: EDGE_THICKNESS }).y - SHIFT } + Visualize.mGraph.canvas.translate(0, SHIFT) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + + self.dragTopEdge = setInterval(function() { + self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: EDGE_THICKNESS }).y - SHIFT } + Visualize.mGraph.canvas.translate(0, SHIFT) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + }, PERIOD) + } + if (height - yPix < EDGE_THICKNESS && self.dragTolerance) { + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) + self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: height - EDGE_THICKNESS }).y + SHIFT } + Visualize.mGraph.canvas.translate(0, -SHIFT) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + + self.dragBottomEdge = setInterval(function() { + self.virtualPointer = { x: pos.x, y: Util.pixelsToCoords(Visualize.mGraph, { x: xPix, y: height - EDGE_THICKNESS }).y + SHIFT } + Visualize.mGraph.canvas.translate(0, -SHIFT) + self.updateTopicPositions(node, self.virtualPointer) + Visualize.mGraph.plot() + }, PERIOD) + } + + if (xPix >= EDGE_THICKNESS && width - xPix >= EDGE_THICKNESS && yPix >= EDGE_THICKNESS && height - yPix >= EDGE_THICKNESS) { + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) + + self.updateTopicPositions(node, pos) Visualize.mGraph.plot() } - } - // if it's a right click or holding down alt, start synapse creation ->third option is for firefox - else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { + // if it's a right click or holding down alt, start synapse creation ->third option is for firefox if (JIT.tempInit === false) { JIT.tempNode = node JIT.tempInit = true @@ -889,14 +886,14 @@ const JIT = { } // before making the highlighted one bigger, make sure all the others are regular size - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { n.setData('dim', 25, 'current') }) temp.setData('dim', 35, 'current') Visualize.mGraph.plot() } else if (!temp) { JIT.tempNode2 = null - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { n.setData('dim', 25, 'current') }) // pop up node creation :) @@ -913,16 +910,14 @@ const JIT = { y: pos.y } } - } - else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') - } - else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { GlobalUI.notifyUser('Cannot edit this map.') } } }, // onDragMoveTopicHandler - onDragCancelHandler: function (node, eventInfo, e) { + onDragCancelHandler: function(node, eventInfo, e) { JIT.tempNode = null if (JIT.tempNode2) JIT.tempNode2.setData('dim', 25, 'current') JIT.tempNode2 = null @@ -932,22 +927,24 @@ const JIT = { Mouse.synapseEndCoordinates = null Visualize.mGraph.plot() }, // onDragCancelHandler - onDragEndTopicHandler: function (node, eventInfo, e) { - var self = JIT; - var midpoint = {}, pixelPos, mapping - - clearInterval(self.dragLeftEdge); - clearInterval(self.dragRightEdge); - clearInterval(self.dragTopEdge); - clearInterval(self.dragBottomEdge); - - delete self.dragLeftEdge; - delete self.dragRightEdge; - delete self.dragTopEdge; - delete self.dragBottomEdge; + onDragEndTopicHandler: function(node, eventInfo, e) { + const self = JIT + const midpoint = {} + let pixelPos + let mapping - self.dragFlag = 0; - self.dragTolerance = 0; + clearInterval(self.dragLeftEdge) + clearInterval(self.dragRightEdge) + clearInterval(self.dragTopEdge) + clearInterval(self.dragBottomEdge) + + delete self.dragLeftEdge + delete self.dragRightEdge + delete self.dragTopEdge + delete self.dragBottomEdge + + self.dragFlag = 0 + self.dragTolerance = 0 if (JIT.tempInit && JIT.tempNode2 === null) { // this means you want to add a new topic, and then a synapse @@ -973,7 +970,7 @@ const JIT = { // this means you dragged an existing node, autosave that to the database // check whether to save mappings - const checkWhetherToSave = function () { + const checkWhetherToSave = function() { const map = Active.Map if (!map) return false return map.authorizeToEdit(Active.Mapper) @@ -1000,7 +997,7 @@ const JIT = { } } }, // onDragEndTopicHandler - canvasClickHandler: function (canvasLoc, e) { + canvasClickHandler: function(canvasLoc, e) { // grab the location and timestamp of the click const storedTime = Mouse.lastCanvasClick const now = Date.now() // not compatible with IE8 FYI @@ -1045,37 +1042,37 @@ const JIT = { // SINGLE CLICK, resulting from pan Create.newTopic.hide() } - }, // canvasClickHandler - updateTopicPositions: function (node, pos){ - var len = Selected.Nodes.length; - var topic; - // this is used to send nodes that are moving to + }, // canvasClickHandler + updateTopicPositions: function(node, pos) { + const len = Selected.Nodes.length + // this is used to send nodes that are moving to // other realtime collaborators on the same map - var positionsToSend = {}; + const positionsToSend = {} // first define offset for each node var xOffset = [] var yOffset = [] - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] xOffset[i] = n.pos.getc().x - node.pos.getc().x yOffset[i] = n.pos.getc().y - node.pos.getc().y } // for - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] - var x = pos.x + xOffset[i] - var y = pos.y + yOffset[i] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] + const x = pos.x + xOffset[i] + const y = pos.y + yOffset[i] if (n.pos.rho || n.pos.rho === 0) { // this means we're in topic view - var rho = Math.sqrt(x * x + y * y) - var theta = Math.atan2(y, x) + const rho = Math.sqrt(x * x + y * y) + const theta = Math.atan2(y, x) n.pos.setp(theta, rho) + } else { + n.pos.setc(x, y) } - else n.pos.setc(x, y) if (Active.Map) { - topic = n.getData('topic') + const topic = n.getData('topic') // we use the topic ID not the node id // because we can't depend on the node id // to be the same as on other collaborators @@ -1089,13 +1086,13 @@ const JIT = { } }, - nodeDoubleClickHandler: function (node, e) { + nodeDoubleClickHandler: function(node, e) { TopicCard.showCard(node) }, // nodeDoubleClickHandler - edgeDoubleClickHandler: function (adj, e) { + edgeDoubleClickHandler: function(adj, e) { SynapseCard.showCard(adj, e) }, // nodeDoubleClickHandler - nodeWasDoubleClicked: function () { + nodeWasDoubleClicked: function() { // grab the timestamp of the click const storedTime = Mouse.lastNodeClick const now = Date.now() // not compatible with IE8 FYI @@ -1107,7 +1104,7 @@ const JIT = { return false } }, // nodeWasDoubleClicked - handleSelectionBeforeDragging: function (node, e) { + handleSelectionBeforeDragging: function(node, e) { if (Selected.Nodes.length === 0) { Control.selectNode(node, e) } @@ -1121,7 +1118,7 @@ const JIT = { } } }, // handleSelectionBeforeDragging - getNodeXY: function (node) { + getNodeXY: function(node) { if (typeof node.pos.x === 'number' && typeof node.pos.y === 'number') { return node.pos } else if (typeof node.pos.theta === 'number' && typeof node.pos.rho === 'number') { @@ -1131,7 +1128,7 @@ const JIT = { return {} } }, - selectWithBox: function (e) { + selectWithBox: function(e) { const self = this let sX = Mouse.boxStartCoordinates.x let sY = Mouse.boxStartCoordinates.y @@ -1144,7 +1141,7 @@ const JIT = { } // select all nodes that are within the box - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { const pos = self.getNodeXY(n) const x = pos.x const y = pos.y @@ -1172,13 +1169,13 @@ const JIT = { eY = -1 * eY const edgesToToggle = [] - DataModel.Synapses.each(function (synapse) { + DataModel.Synapses.each(function(synapse) { const e = synapse.get('edge') if (edgesToToggle.indexOf(e) === -1) { edgesToToggle.push(e) } }) - edgesToToggle.forEach(function (edge) { + edgesToToggle.forEach(function(edge) { const fromNodePos = self.getNodeXY(edge.nodeFrom) const fromNodeX = fromNodePos.x const fromNodeY = -1 * fromNodePos.y @@ -1213,7 +1210,7 @@ const JIT = { let minSlope = slopes[0] let maxSlope = slopes[0] - slopes.forEach(function (entry) { + slopes.forEach(function(entry) { if (entry > maxSlope) maxSlope = entry if (entry < minSlope) minSlope = entry }) @@ -1277,7 +1274,7 @@ const JIT = { Mouse.boxEndCoordinates = false Visualize.mGraph.plot() }, // selectWithBox - drawSelectBox: function (eventInfo, e) { + drawSelectBox: function(eventInfo, e) { const ctx = Visualize.mGraph.canvas.getCtx() const startX = Mouse.boxStartCoordinates.x @@ -1297,13 +1294,13 @@ const JIT = { ctx.strokeStyle = 'black' ctx.stroke() }, // drawSelectBox - selectNodeOnClickHandler: function (node, e) { + selectNodeOnClickHandler: function(node, e) { if (Visualize.mGraph.busy) return const self = JIT - - //Copy topic title to clipboard - if(e.button===1 && e.ctrlKey) clipboard.copy(node.name); + + // Copy topic title to clipboard + if (e.button === 1 && e.ctrlKey) clipboard.copy(node.name) // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { @@ -1323,44 +1320,43 @@ const JIT = { return } else { // wait a certain length of time, then check again, then run this code - setTimeout(function () { + setTimeout(function() { if (!JIT.nodeWasDoubleClicked()) { var nodeAlreadySelected = node.selected - - if(e.button!==1){ + + if (e.button !== 1) { if (!e.shiftKey) { Control.deselectAllNodes() Control.deselectAllEdges() } - + if (nodeAlreadySelected) { Control.deselectNode(node) } else { Control.selectNode(node, e) } - + // trigger animation to final styles Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth:color:alpha'], duration: 500 }) Visualize.mGraph.plot() - } else { - if(!e.ctrlKey){ - var len = Selected.Nodes.length; - + if (!e.ctrlKey) { + var len = Selected.Nodes.length + for (let i = 0; i < len; i += 1) { - let n = Selected.Nodes[i]; - let result = Metamaps.Util.openLink(Metamaps.Topics.get(n.id).attributes.link); - - if (!result) { //if link failed to open - break; + let n = Selected.Nodes[i] + let result = Util.openLink(DataModel.Topics.get(n.id).attributes.link) + + if (!result) { // if link failed to open + break } } - - if(!node.selected){ - Metamaps.Util.openLink(Metamaps.Topics.get(node.id).attributes.link); + + if (!node.selected) { + Util.openLink(DataModel.Topics.get(node.id).attributes.link) } } } @@ -1368,7 +1364,7 @@ const JIT = { }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectNodeOnClickHandler - selectNodeOnRightClickHandler: function (node, e) { + selectNodeOnRightClickHandler: function(node, e) { // the 'node' variable is a JIT node, the one that was clicked on // the 'e' variable is the click event @@ -1485,7 +1481,7 @@ const JIT = { // delete the selected things from the database if (authorized) { - $('.rc-delete').click(function () { + $('.rc-delete').click(function() { $('.rightclickmenu').remove() Control.deleteSelected() }) @@ -1493,7 +1489,7 @@ const JIT = { // remove the selected things from the map if (Active.Topic || authorized) { - $('.rc-remove').click(function () { + $('.rc-remove').click(function() { $('.rightclickmenu').remove() Control.removeSelectedEdges() Control.removeSelectedNodes() @@ -1501,34 +1497,34 @@ const JIT = { } // hide selected nodes and synapses until refresh - $('.rc-hide').click(function () { + $('.rc-hide').click(function() { $('.rightclickmenu').remove() Control.hideSelectedEdges() Control.hideSelectedNodes() }) // when in radial, center on the topic you picked - $('.rc-center').click(function () { + $('.rc-center').click(function() { $('.rightclickmenu').remove() Topic.centerOn(node.id) }) // open the entity in a new tab - $('.rc-popout').click(function () { + $('.rc-popout').click(function() { $('.rightclickmenu').remove() const win = window.open('/topics/' + node.id, '_blank') win.focus() }) // change the permission of all the selected nodes and synapses that you were the originator of - $('.rc-permission li').click(function () { + $('.rc-permission li').click(function() { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' Control.updateSelectedPermissions($(this).text()) }) // change the metacode of all the selected nodes that you have edit permission for - $('.rc-metacode li li').click(function () { + $('.rc-metacode li li').click(function() { $('.rightclickmenu').remove() // Control.updateSelectedMetacodes($(this).attr('data-id')) @@ -1536,19 +1532,19 @@ const JIT = { // fetch relatives let fetchSent = false - $('.rc-siblings').hover(function () { + $('.rc-siblings').hover(function() { if (!fetchSent) { JIT.populateRightClickSiblings(node) fetchSent = true } }) - $('.rc-siblings .fetchAll').click(function () { + $('.rc-siblings .fetchAll').click(function() { $('.rightclickmenu').remove() // data-id is a metacode id Topic.fetchRelatives(node) }) }, // selectNodeOnRightClickHandler, - populateRightClickSiblings: function (node) { + populateRightClickSiblings: function(node) { // depending on how many topics are selected, do different things const topic = node.getData('topic') @@ -1560,10 +1556,10 @@ const JIT = { loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - const topics = DataModel.Topics.map(function (t) { return t.id }) + const topics = DataModel.Topics.map(function(t) { return t.id }) const topicsString = topics.join() - const successCallback = function (data) { + const successCallback = function(data) { $('#loadingSiblings').remove() for (var key in data) { @@ -1571,7 +1567,7 @@ const JIT = { $('#fetchSiblingList').append(`
    • ${string}
    • `) } - $('.rc-siblings .getSiblings').click(function () { + $('.rc-siblings .getSiblings').click(function() { $('.rightclickmenu').remove() // data-id is a metacode id Topic.fetchRelatives(node, $(this).attr('data-id')) @@ -1582,16 +1578,16 @@ const JIT = { type: 'GET', url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topicsString, success: successCallback, - error: function () {} + error: function() {} }) }, - selectEdgeOnClickHandler: function (adj, e) { + selectEdgeOnClickHandler: function(adj, e) { if (Visualize.mGraph.busy) return const self = JIT - var synapseText = adj.data.$synapses[0].attributes.desc; - //Copy synapse label to clipboard - if(e.button===1 && e.ctrlKey && synapseText !== "") clipboard.copy(synapseText); + var synapseText = adj.data.$synapses[0].attributes.desc + // Copy synapse label to clipboard + if (e.button === 1 && e.ctrlKey && synapseText !== '') clipboard.copy(synapseText) // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { @@ -1605,7 +1601,7 @@ const JIT = { return } else { // wait a certain length of time, then check again, then run this code - setTimeout(function () { + setTimeout(function() { if (!JIT.nodeWasDoubleClicked()) { const edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 @@ -1625,7 +1621,7 @@ const JIT = { }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectEdgeOnClickHandler - selectEdgeOnRightClickHandler: function (adj, e) { + selectEdgeOnRightClickHandler: function(adj, e) { // the 'node' variable is a JIT node, the one that was clicked on // the 'e' variable is the click event @@ -1705,7 +1701,7 @@ const JIT = { // delete the selected things from the database if (authorized) { - $('.rc-delete').click(function () { + $('.rc-delete').click(function() { $('.rightclickmenu').remove() Control.deleteSelected() }) @@ -1713,7 +1709,7 @@ const JIT = { // remove the selected things from the map if (authorized) { - $('.rc-remove').click(function () { + $('.rc-remove').click(function() { $('.rightclickmenu').remove() Control.removeSelectedEdges() Control.removeSelectedNodes() @@ -1721,20 +1717,20 @@ const JIT = { } // hide selected nodes and synapses until refresh - $('.rc-hide').click(function () { + $('.rc-hide').click(function() { $('.rightclickmenu').remove() Control.hideSelectedEdges() Control.hideSelectedNodes() }) // change the permission of all the selected nodes and synapses that you were the originator of - $('.rc-permission li').click(function () { + $('.rc-permission li').click(function() { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' Control.updateSelectedPermissions($(this).text()) }) }, // selectEdgeOnRightClickHandler - SmoothPanning: function () { + SmoothPanning: function() { const sx = Visualize.mGraph.canvas.scaleOffsetX const sy = Visualize.mGraph.canvas.scaleOffsetY const yVelocity = Mouse.changeInY // initial y velocity @@ -1742,11 +1738,11 @@ const JIT = { let easing = 1 // frictional value window.clearInterval(panningInt) - panningInt = setInterval(function () { + panningInt = setInterval(function() { myTimer() }, 1) - function myTimer () { + function myTimer() { Visualize.mGraph.canvas.translate(xVelocity * easing * 1 / sx, yVelocity * easing * 1 / sy) $(document).trigger(JIT.events.pan) easing = easing * 0.75 @@ -1754,7 +1750,7 @@ const JIT = { if (easing < 0.1) window.clearInterval(panningInt) } }, // SmoothPanning - renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { + renderMidArrow: function(from, to, dim, swap, canvas, placement, newSynapse) { const ctx = canvas.getCtx() // invert edge direction if (swap) { @@ -1795,7 +1791,7 @@ const JIT = { ctx.lineTo(v2.x, v2.y) ctx.stroke() }, // renderMidArrow - renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { + renderEdgeArrows: function(edgeHelper, adj, synapse, canvas) { const self = JIT const directionCat = synapse.get('category') @@ -1848,15 +1844,15 @@ const JIT = { }, 13, inv, canvas, 0.3) } }, // renderEdgeArrows - zoomIn: function (event) { + zoomIn: function(event) { Visualize.mGraph.canvas.scale(1.25, 1.25) $(document).trigger(JIT.events.zoom, [event]) }, - zoomOut: function (event) { + zoomOut: function(event) { Visualize.mGraph.canvas.scale(0.8, 0.8) $(document).trigger(JIT.events.zoom, [event]) }, - centerMap: function (canvas) { + centerMap: function(canvas) { const offsetScale = canvas.scaleOffsetX canvas.scale(1 / offsetScale, 1 / offsetScale) @@ -1866,7 +1862,7 @@ const JIT = { canvas.translate(-1 * offsetX, -1 * offsetY) }, - zoomToBox: function (event) { + zoomToBox: function(event) { const sX = Mouse.boxStartCoordinates.x const sY = Mouse.boxStartCoordinates.y const eX = Mouse.boxEndCoordinates.x @@ -1905,7 +1901,7 @@ const JIT = { Mouse.boxEndCoordinates = false Visualize.mGraph.plot() }, - zoomExtents: function (event, canvas, denySelected) { + zoomExtents: function(event, canvas, denySelected) { JIT.centerMap(canvas) let height = canvas.getSize().height let width = canvas.getSize().width @@ -1923,7 +1919,7 @@ const JIT = { } if (nodes.length > 1) { - nodes.forEach(function (n) { + nodes.forEach(function(n) { let x = n.pos.x let y = n.pos.y @@ -1982,7 +1978,7 @@ const JIT = { $(document).trigger(JIT.events.zoom, [event]) } else if (nodes.length === 1) { - nodes.forEach(function (n) { + nodes.forEach(function(n) { const x = n.pos.x const y = n.pos.y diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 522bb255..470e8cbe 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -12,9 +12,9 @@ import Visualize from './Visualize' import { Search } from './GlobalUI' const Listeners = { - init: function () { + init: function() { var self = this - $(document).on('keydown', function (e) { + $(document).on('keydown', function(e) { if (!(Active.Map || Active.Topic)) return const onCanvas = e.target.tagName === 'BODY' @@ -35,7 +35,7 @@ const Listeners = { Control.deselectAllEdges() e.preventDefault() - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { Control.selectNode(n, e) }) @@ -110,7 +110,7 @@ const Listeners = { } }) - $(window).resize(function () { + $(window).resize(function() { if (Visualize && Visualize.mGraph) { Util.resizeCanvas(Visualize.mGraph.canvas) } @@ -119,11 +119,11 @@ const Listeners = { Mobile.resizeTitle() }) }, - centerAndReveal: function (nodes, opts) { + centerAndReveal: function(nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Topic.centerOn(node.id, function () { + Topic.centerOn(node.id, function() { Topic.fetchRelatives(nodes) }) } else if (opts.center) { diff --git a/frontend/src/Metamaps/Loading.js b/frontend/src/Metamaps/Loading.js index b1fc2abb..50d2191b 100644 --- a/frontend/src/Metamaps/Loading.js +++ b/frontend/src/Metamaps/Loading.js @@ -2,13 +2,13 @@ const Loading = { loader: null, // needs CanvasLoader to be defined - hide: function () { + hide: function() { $('#loading').hide() }, - show: function () { + show: function() { $('#loading').show() }, - setup: function () { + setup: function() { if (!Loading.loader) Loading.loader = new CanvasLoader('loading') Loading.loader.setColor('#4fb5c0') // default is '#000000' Loading.loader.setDiameter(28) // default is 40 diff --git a/frontend/src/Metamaps/Map/CheatSheet.js b/frontend/src/Metamaps/Map/CheatSheet.js index be9fbfab..d791b5c7 100644 --- a/frontend/src/Metamaps/Map/CheatSheet.js +++ b/frontend/src/Metamaps/Map/CheatSheet.js @@ -1,28 +1,28 @@ /* global $ */ const CheatSheet = { - init: function () { + init: function() { // tab the cheatsheet $('#cheatSheet').tabs() $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') - // id = the id of a vimeo video - var switchVideo = function (element, id) { - $('.tutorialItem').removeClass('active') - $(element).addClass('active') - $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) - } + // // id = the id of a vimeo video + // var switchVideo = function(element, id) { + // $('.tutorialItem').removeClass('active') + // $(element).addClass('active') + // $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) + // } - $('#gettingStarted').click(function () { - // switchVideo(this,'88334167') - }) - $('#upYourSkillz').click(function () { - // switchVideo(this,'100118167') - }) - $('#advancedMapping').click(function () { - // switchVideo(this,'88334167') - }) + // $('#gettingStarted').click(function() { + // switchVideo(this,'88334167') + // }) + // $('#upYourSkillz').click(function() { + // switchVideo(this,'100118167') + // }) + // $('#advancedMapping').click(function() { + // switchVideo(this,'88334167') + // }) } } diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index bf56dc90..1b06daf5 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -35,23 +35,23 @@ const InfoBox = { data-bip-value="{{desc}}" >{{desc}}`, userImageUrl: '', - init: function (serverData) { + init: function(serverData) { var self = InfoBox $('.mapInfoIcon').click(self.toggleBox) - $('.mapInfoBox').click(function (event) { + $('.mapInfoBox').click(function(event) { event.stopPropagation() }) $('body').click(self.close) - self.attachEventListeners() + self.attachEventListeners() self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) self.userImageUrl = serverData['user.png'] var querystring = window.location.search.replace(/^\?/, '') - if (querystring == 'new') { + if (querystring === 'new') { self.open() $('.mapInfoBox').addClass('mapRequestTitle') $('#mapInfoName').trigger('click') @@ -59,7 +59,7 @@ const InfoBox = { $('#mapInfoName textarea').select() } }, - toggleBox: function (event) { + toggleBox: function(event) { var self = InfoBox if (self.isOpen) self.close() @@ -67,24 +67,24 @@ const InfoBox = { event.stopPropagation() }, - open: function () { + open: function() { var self = InfoBox $('.mapInfoIcon div').addClass('hide') if (!self.isOpen && !self.changing) { self.changing = true - $('.mapInfoBox').fadeIn(200, function () { + $('.mapInfoBox').fadeIn(200, function() { self.changing = false self.isOpen = true }) } }, - close: function () { + close: function() { var self = InfoBox $('.mapInfoIcon div').removeClass('hide') if (!self.changing) { self.changing = true - $('.mapInfoBox').fadeOut(200, function () { + $('.mapInfoBox').fadeOut(200, function() { self.changing = false self.isOpen = false self.hidePermissionSelect() @@ -92,7 +92,7 @@ const InfoBox = { }) } }, - load: function () { + load: function() { var self = InfoBox var map = Active.Map @@ -127,14 +127,14 @@ const InfoBox = { self.attachEventListeners() }, - attachEventListeners: function () { + attachEventListeners: function() { var self = InfoBox $('.mapInfoBox.canEdit .best_in_place').best_in_place() // because anyone who can edit the map can change the map title var bipName = $('.mapInfoBox .best_in_place_name') - bipName.unbind('best_in_place:activate').bind('best_in_place:activate', function () { + bipName.unbind('best_in_place:activate').bind('best_in_place:activate', function() { var $el = bipName.find('textarea') var el = $el[0] @@ -142,16 +142,16 @@ const InfoBox = { $('.mapInfoName').append('
      ') - var callback = function (data) { + var callback = function(data) { $('.nameCounter.forMap').html(data.all + '/140') } Countable.live(el, callback) }) - bipName.unbind('best_in_place:deactivate').bind('best_in_place:deactivate', function () { + bipName.unbind('best_in_place:deactivate').bind('best_in_place:deactivate', function() { $('.nameCounter.forMap').remove() }) - $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function () { + $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function() { var name = $(this).html() Active.Map.set('name', name) Active.Map.trigger('saved') @@ -162,7 +162,7 @@ const InfoBox = { window.history.replaceState('', `${name} | Metamaps`, window.location.pathname) }) - $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { + $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function() { var desc = $(this).html() Active.Map.set('desc', desc) Active.Map.trigger('saved') @@ -182,76 +182,76 @@ const InfoBox = { $('.yourMap .mapInfoDelete').unbind().click(self.deleteActiveMap) - $('.mapContributors span, #mapContribs').unbind().click(function (event) { + $('.mapContributors span, #mapContribs').unbind().click(function(event) { $('.mapContributors .tip').toggle() event.stopPropagation() }) - $('.mapContributors .tip').unbind().click(function (event) { + $('.mapContributors .tip').unbind().click(function(event) { event.stopPropagation() }) $('.mapContributors .tip li a').click(Router.intercept) - $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { + $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() { $('.mapContributors .tip').hide() }) - self.addTypeahead() + self.addTypeahead() }, - addTypeahead: function () { + addTypeahead: function() { var self = InfoBox if (!Active.Map) return // for autocomplete var collaborators = { - name: 'collaborators', - limit: 9999, - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile($('#collaboratorSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult", - profile: self.userImageUrl - }); - }, - suggestion: function(s) { - return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s); - }, + name: 'collaborators', + limit: 9999, + display: function(s) { return s.label }, + templates: { + notFound: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + rtype: 'noresult', + profile: self.userImageUrl + }) }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY', - }, - }) + suggestion: function(s) { + return Hogan.compile($('#collaboratorSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY' + } + }) } // for adding map collaborators, who will have edit rights if (Active.Mapper && Active.Mapper.id === Active.Map.get('user_id')) { $('.collaboratorSearchField').typeahead( { - highlight: false, + highlight: false }, [collaborators] ) $('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick) - $('.mapContributors .removeCollaborator').click(function () { + $('.mapContributors .removeCollaborator').click(function() { self.removeCollaborator(parseInt($(this).data('id'))) }) - } + } }, - removeCollaborator: function (collaboratorId) { + removeCollaborator: function(collaboratorId) { var self = InfoBox DataModel.Collaborators.remove(DataModel.Collaborators.get(collaboratorId)) - var mapperIds = DataModel.Collaborators.models.map(function (mapper) { return mapper.id }) + var mapperIds = DataModel.Collaborators.models.map(function(mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, - addCollaborator: function (newCollaboratorId) { + addCollaborator: function(newCollaboratorId) { var self = InfoBox if (DataModel.Collaborators.get(newCollaboratorId)) { @@ -261,7 +261,7 @@ const InfoBox = { function callback(mapper) { DataModel.Collaborators.add(mapper) - var mapperIds = DataModel.Collaborators.models.map(function (mapper) { return mapper.id }) + var mapperIds = DataModel.Collaborators.models.map(function(mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) var name = DataModel.Collaborators.get(newCollaboratorId).get('name') GlobalUI.notifyUser(name + ' will be notified by email') @@ -270,29 +270,28 @@ const InfoBox = { $.getJSON('/users/' + newCollaboratorId + '.json', callback) }, - handleResultClick: function (event, item) { + handleResultClick: function(event, item) { var self = InfoBox - self.addCollaborator(item.id) - $('.collaboratorSearchField').typeahead('val', '') + self.addCollaborator(item.id) + $('.collaboratorSearchField').typeahead('val', '') }, - updateNameDescPerm: function (name, desc, perm) { + updateNameDescPerm: function(name, desc, perm) { $('.mapInfoBox').removeClass('mapRequestTitle') $('.mapInfoName .best_in_place_name').html(name) $('.mapInfoDesc .best_in_place_desc').html(desc) $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm) }, - createContributorList: function () { - var self = InfoBox + createContributorList: function() { var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' string += '
        ' - relevantPeople.each(function (m) { + relevantPeople.each(function(m) { var isCreator = Active.Map.get('user_id') === m.get('id') string += '
      • ' + '' + m.get('name') - if (isCreator) string += ' (creator)' + if (isCreator) string += ' (creator)' string += '' if (activeMapperIsCreator && !isCreator) string += '' string += '
      • ' @@ -305,27 +304,30 @@ const InfoBox = { } return string }, - updateNumbers: function () { + updateNumbers: function() { if (!Active.Map) return - var self = InfoBox - var mapper = Active.Mapper + const self = InfoBox + var relevantPeople = Active.Map.get('permission') === 'commons' ? DataModel.Mappers : DataModel.Collaborators - var contributors_class = '' - if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' - else if (relevantPeople.length > 2) contributors_class = 'multiple' + let contributorsClass = '' + if (relevantPeople.length === 2) { + contributorsClass = 'multiple mTwo' + } else if (relevantPeople.length > 2) { + contributorsClass = 'multiple' + } - var contributors_image = self.userImageUrl + let contributorsImage = self.userImageUrl if (relevantPeople.length > 0) { // get the first contributor and use their image - contributors_image = relevantPeople.models[0].get('image') + contributorsImage = relevantPeople.models[0].get('image') } - $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class) + $('.mapContributors img').attr('src', contributorsImage).removeClass('multiple mTwo').addClass(contributorsClass) $('.mapContributors span').text(relevantPeople.length) $('.mapContributors .tip').html(self.createContributorList()) self.addTypeahead() - $('.mapContributors .tip').unbind().click(function (event) { + $('.mapContributors .tip').unbind().click(function(event) { event.stopPropagation() }) $('.mapTopics').text(DataModel.Topics.length) @@ -333,7 +335,7 @@ const InfoBox = { $('.mapEditedAt').html('Last edited: ' + Util.nowDateFormatted()) }, - onPermissionClick: function (event) { + onPermissionClick: function(event) { var self = InfoBox if (!self.selectingPermission) { @@ -350,14 +352,14 @@ const InfoBox = { event.stopPropagation() } }, - hidePermissionSelect: function () { + hidePermissionSelect: function() { var self = InfoBox self.selectingPermission = false $('.mapPermission').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow $('.mapPermission .permissionSelect').remove() }, - selectPermission: function (event) { + selectPermission: function(event) { var self = InfoBox self.selectingPermission = false @@ -372,7 +374,7 @@ const InfoBox = { $('.mapInfoBox').removeClass('shareable').addClass(shareable) event.stopPropagation() }, - deleteActiveMap: function () { + deleteActiveMap: function() { var confirmString = 'Are you sure you want to delete this map? ' confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' @@ -390,8 +392,7 @@ const InfoBox = { map.destroy() Router.home() GlobalUI.notifyUser('Map eliminated!') - } - else if (!authorized) { + } else if (!authorized) { window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") } } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index b616e95f..51038b87 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,6 +1,7 @@ /* global $ */ import outdent from 'outdent' +import { find as _find } from 'lodash' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -25,19 +26,19 @@ const Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' }, - init: function (serverData) { + init: function(serverData) { var self = Map - - $('#wrapper').mousedown(function (e){ - if(e.button === 1)return false; - }); - $('.starMap').click(function () { + $('#wrapper').mousedown(function(e) { + if (e.button === 1) return false + }) + + $('.starMap').click(function() { if ($(this).is('.starred')) self.unstar() else self.star() }) - $('.sidebarFork').click(function () { + $('.sidebarFork').click(function() { self.fork() }) @@ -51,26 +52,26 @@ const Map = { $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) }, - requestAccess: function () { + requestAccess: function() { $('.viewOnly').removeClass('sendRequest').addClass('sentRequest') const mapId = Active.Map.id $.post({ url: `/maps/${mapId}/access_request` }) - GlobalUI.notifyUser('Map creator will be notified of your request') + GlobalUI.notifyUser('Map creator will be notified of your request') }, - setAccessRequest: function (requests, activeMapper) { + setAccessRequest: function(requests, activeMapper) { let className = 'isViewOnly ' if (activeMapper) { - const request = _.find(requests, r => r.user_id === activeMapper.id) - if (!request) className += 'sendRequest' - else if (request && !request.answered) className += 'sentRequest' + const request = _find(requests, r => r.user_id === activeMapper.id) + if (!request) className += 'sendRequest' + else if (request && !request.answered) className += 'sentRequest' else if (request && request.answered && !request.approved) className += 'requestDenied' } $('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className) }, - launch: function (id) { - var start = function (data) { + launch: function(id) { + var start = function(data) { Active.Map = new DataModelMap(data.map) DataModel.Mappers = new DataModel.MapperCollection(data.mappers) DataModel.Collaborators = new DataModel.MapperCollection(data.collaborators) @@ -89,8 +90,7 @@ const Map = { // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { $('.wrapper').addClass('canEditMap') - } - else { + } else { Map.setAccessRequest(data.requests, mapper) } @@ -100,7 +100,7 @@ const Map = { $('.wrapper').addClass('commonsMap') } - Map.updateStar() + Map.updateStar() // set filter mapper H3 text $('#filter_by_mapper h3').html('MAPPERS') @@ -125,7 +125,7 @@ const Map = { Realtime.startActiveMap() Loading.hide() - + // for mobile $('#header_content').html(map.get('name')) } @@ -135,7 +135,7 @@ const Map = { success: start }) }, - end: function () { + end: function() { if (Active.Map) { $('.wrapper').removeClass('canEditMap commonsMap') AutoLayout.resetSpiral() @@ -151,10 +151,10 @@ const Map = { $('.viewOnly').removeClass('isViewOnly') } }, - updateStar: function () { + updateStar: function() { if (!Active.Mapper || !DataModel.Stars) return // update the star/unstar icon - if (DataModel.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { + if (DataModel.Stars.find(function(s) { return s.user_id === Active.Mapper.id })) { $('.starMap').addClass('starred') $('.starMap .tooltipsAbove').html('Unstar') } else { @@ -162,7 +162,7 @@ const Map = { $('.starMap .tooltipsAbove').html('Star') } }, - star: function () { + star: function() { var self = Map if (!Active.Map) return @@ -172,72 +172,72 @@ const Map = { GlobalUI.notifyUser('Map is now starred') self.updateStar() }, - unstar: function () { + unstar: function() { var self = Map if (!Active.Map) return $.post('/maps/' + Active.Map.id + '/unstar') - DataModel.Stars = DataModel.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id }) DataModel.Maps.Starred.remove(Active.Map) - self.updateStar() + self.updateStar() }, - fork: function () { + fork: function() { GlobalUI.openLightbox('forkmap') - var nodes_data = '', - synapses_data = '' - var nodes_array = [] - var synapses_array = [] + let nodesData = '' + let synapsesData = '' + let nodesArray = [] + let synapsesArray = [] // collect the unfiltered topics - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { // if the opacity is less than 1 then it's filtered if (n.getData('alpha') === 1) { var id = n.getData('topic').id - nodes_array.push(id) - var x, y + nodesArray.push(id) + let x, y if (n.pos.x && n.pos.y) { x = n.pos.x y = n.pos.y } else { - var x = Math.cos(n.pos.theta) * n.pos.rho - var y = Math.sin(n.pos.theta) * n.pos.rho + x = Math.cos(n.pos.theta) * n.pos.rho + y = Math.sin(n.pos.theta) * n.pos.rho } - nodes_data += id + '/' + x + '/' + y + ',' + nodesData += id + '/' + x + '/' + y + ',' } }) // collect the unfiltered synapses - DataModel.Synapses.each(function (synapse) { + DataModel.Synapses.each(function(synapse) { var desc = synapse.get('desc') var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 // make sure that both topics are being added, otherwise, it // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('topic1_id')) > -1 - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('topic2_id')) > -1 + var topicsNotFiltered = nodesArray.indexOf(synapse.get('topic1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodesArray.indexOf(synapse.get('topic2_id')) > -1 if (descNotFiltered && topicsNotFiltered) { - synapses_array.push(synapse.id) + synapsesArray.push(synapse.id) } }) - synapses_data = synapses_array.join() - nodes_data = nodes_data.slice(0, -1) + synapsesData = synapsesArray.join() + nodesData = nodesData.slice(0, -1) - GlobalUI.CreateMap.topicsToMap = nodes_data - GlobalUI.CreateMap.synapsesToMap = synapses_data + GlobalUI.CreateMap.topicsToMap = nodesData + GlobalUI.CreateMap.synapsesToMap = synapsesData }, - leavePrivateMap: function () { + leavePrivateMap: function() { var map = Active.Map DataModel.Maps.Active.remove(map) DataModel.Maps.Featured.remove(map) Router.home() GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, - cantEditNow: function () { - Realtime.turnOff(true); // true is for 'silence' + cantEditNow: function() { + Realtime.turnOff(true) // true is for 'silence' GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') Active.Map.trigger('changeByOther') }, - canEditNow: function () { + canEditNow: function() { var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' var c = window.confirm(confirmString) @@ -245,12 +245,12 @@ const Map = { Router.maps(Active.Map.id) } }, - editedByActiveMapper: function () { + editedByActiveMapper: function() { if (Active.Mapper) { DataModel.Mappers.add(Active.Mapper) } }, - exportImage: function () { + exportImage: function() { var canvas = {} canvas.canvas = document.createElement('canvas') @@ -263,7 +263,7 @@ const Map = { canvas.translateOffsetX = 0 canvas.denySelected = true - canvas.getSize = function () { + canvas.getSize = function() { if (this.size) return this.size var canvas = this.canvas this.size = { @@ -272,24 +272,24 @@ const Map = { } return this.size } - canvas.scale = function (x, y) { - var px = this.scaleOffsetX * x, - py = this.scaleOffsetY * y - var dx = this.translateOffsetX * (x - 1) / px, - dy = this.translateOffsetY * (y - 1) / py + canvas.scale = function(x, y) { + const px = this.scaleOffsetX * x + const py = this.scaleOffsetY * y + const dx = this.translateOffsetX * (x - 1) / px + const dy = this.translateOffsetY * (y - 1) / py this.scaleOffsetX = px this.scaleOffsetY = py this.getCtx().scale(x, y) this.translate(dx, dy) } - canvas.translate = function (x, y) { - var sx = this.scaleOffsetX, - sy = this.scaleOffsetY + canvas.translate = function(x, y) { + const sx = this.scaleOffsetX + const sy = this.scaleOffsetY this.translateOffsetX += x * sx this.translateOffsetY += y * sy this.getCtx().translate(x, y) } - canvas.getCtx = function () { + canvas.getCtx = function() { return this.canvas.getContext('2d') } // center it @@ -304,20 +304,20 @@ const Map = { // pass true to avoid basing it on a selection JIT.zoomExtents(null, canvas, true) - var c = canvas.canvas, - ctx = canvas.getCtx(), - scale = canvas.scaleOffsetX + const c = canvas.canvas + const ctx = canvas.getCtx() + const scale = canvas.scaleOffsetX // draw a grey background ctx.fillStyle = '#d8d9da' - var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), - yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) + const xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale) + const yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) // draw the graph - mGraph.graph.eachNode(function (node) { + mGraph.graph.eachNode(function(node) { var nodeAlpha = node.getData('alpha') - node.eachAdjacency(function (adj) { + node.eachAdjacency(function(adj) { var nodeTo = adj.nodeTo if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { mGraph.fx.plotLine(adj, canvas) @@ -340,7 +340,7 @@ const Map = { var today = new Date() var dd = today.getDate() - var mm = today.getMonth() + 1; // January is 0! + var mm = today.getMonth() + 1 // January is 0! var yyyy = today.getFullYear() if (dd < 10) { dd = '0' + dd @@ -359,7 +359,7 @@ const Map = { GlobalUI.notifyUser(downloadMessage) canvas.canvas.toBlob(imageBlob => { - const formData = new window.FormData(); + const formData = new window.FormData() formData.append('map[screenshot]', imageBlob, filename) $.ajax({ type: 'PATCH', @@ -368,10 +368,10 @@ const Map = { data: formData, processData: false, contentType: false, - success: function (data) { + success: function(data) { console.log('successfully uploaded map screenshot') }, - error: function () { + error: function() { console.log('failed to save map screenshot') } }) diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 03919378..bf13950e 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -5,7 +5,7 @@ import DataModel from './DataModel' const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve - get: function (id, callback) { + get: function(id, callback) { $.ajax({ url: `/users/${id}.json`, success: data => { diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index fddd90a4..dc4088c0 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -4,27 +4,27 @@ import Active from './Active' import Map from './Map' const Mobile = { - init: function () { + init: function() { var self = Mobile - + $('#menu_icon').click(self.toggleMenu) $('#mobile_menu li a').click(self.liClick) $('#header_content').click(self.titleClick) self.resizeTitle() }, - resizeTitle: function () { + resizeTitle: function() { // the 70 relates to padding $('#header_content').width($(document).width() - 70) }, - liClick: function () { + liClick: function() { var self = Mobile $('#header_content').html($(this).text()) self.toggleMenu() }, - toggleMenu: function () { + toggleMenu: function() { $('#mobile_menu').toggle() }, - titleClick: function () { + titleClick: function() { if (Active.Map) { Map.InfoBox.open() } diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index c79bd8d7..09d6e261 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -6,16 +6,16 @@ import Visualize from './Visualize' import JIT from './JIT' const Organize = { - arrange: function (layout, centerNode) { + arrange: function(layout, centerNode) { // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin - if (layout == 'grid') { - var numNodes = _.size(Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization - var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid - var GRIDSPACE = 400 - var row = 0 - var column = 0 - Visualize.mGraph.graph.eachNode(function (n) { - if (column == numColumns) { + if (layout === 'grid') { + const numNodes = _.size(Visualize.mGraph.graph.nodes) // this will always be an integer, the # of nodes on your graph visualization + const numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid + const GRIDSPACE = 400 + let row = 0 + let column = 0 + Visualize.mGraph.graph.eachNode(function(n) { + if (column === numColumns) { column = 0 row += 1 } @@ -26,25 +26,25 @@ const Organize = { column += 1 }) Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) - } else if (layout == 'grid_full') { + } else if (layout === 'grid_full') { // this will always be an integer, the # of nodes on your graph visualization - var numNodes = _.size(Visualize.mGraph.graph.nodes) - // var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid - // var GRIDSPACE = 400 - var height = Visualize.mGraph.canvas.getSize(0).height - var width = Visualize.mGraph.canvas.getSize(0).width - var totalArea = height * width - var cellArea = totalArea / numNodes - var ratio = height / width - var cellWidth = sqrt(cellArea / ratio) - var cellHeight = cellArea / cellWidth - var row = floor(height / cellHeight) - var column = floor(width / cellWidth) - var totalCells = row * column + const numNodes = _.size(Visualize.mGraph.graph.nodes) + const numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid + const height = Visualize.mGraph.canvas.getSize(0).height + const width = Visualize.mGraph.canvas.getSize(0).width + const totalArea = height * width + const cellArea = totalArea / numNodes + const ratio = height / width + const cellWidth = Math.sqrt(cellArea / ratio) + const cellHeight = cellArea / cellWidth + const GRIDSPACE = 400 + let row = Math.floor(height / cellHeight) + let column = Math.floor(width / cellWidth) + const totalCells = row * column if (totalCells) { - Visualize.mGraph.graph.eachNode(function (n) { - if (column == numColumns) { + Visualize.mGraph.graph.eachNode(function(n) { + if (column === numColumns) { column = 0 row += 1 } @@ -56,7 +56,7 @@ const Organize = { }) } Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) - } else if (layout == 'radial') { + } else if (layout === 'radial') { var centerX = centerNode.getPos().x var centerY = centerNode.getPos().y centerNode.setPos(centerNode.getPos(), 'end') @@ -65,16 +65,16 @@ const Organize = { var lineLength = 200 var usedNodes = {} usedNodes[centerNode.id] = centerNode - var radial = function (node, level, degree) { - if (level == 1) { + var radial = function(node, level, degree) { + if (level === 1) { var numLinksTemp = _.size(node.adjacencies) var angleTemp = 2 * Math.PI / numLinksTemp } else { angleTemp = 2 * Math.PI / 20 } - node.eachAdjacency(function (a) { - var isSecondLevelNode = (centerNode.adjacencies[a.nodeTo.id] != undefined && level > 1) - if (usedNodes[a.nodeTo.id] == undefined && !isSecondLevelNode) { + node.eachAdjacency(function(a) { + var isSecondLevelNode = (centerNode.adjacencies[a.nodeTo.id] !== undefined && level > 1) + if (usedNodes[a.nodeTo.id] === undefined && !isSecondLevelNode) { var newPos = new $jit.Complex() newPos.x = level * lineLength * Math.sin(degree) + centerX newPos.y = level * lineLength * Math.cos(degree) + centerY @@ -88,15 +88,13 @@ const Organize = { } radial(centerNode, 1, 0) Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) - } else if (layout == 'center_viewport') { - var lowX = 0, - lowY = 0, - highX = 0, - highY = 0 - var oldOriginX = Visualize.mGraph.canvas.translateOffsetX - var oldOriginY = Visualize.mGraph.canvas.translateOffsetY + } else if (layout === 'center_viewport') { + let lowX = 0 + let lowY = 0 + let highX = 0 + let highY = 0 - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { if (n.id === 1) { lowX = n.getPos().x lowY = n.getPos().y @@ -109,9 +107,9 @@ const Organize = { if (n.getPos().y > highY) highY = n.getPos().y }) console.log(lowX, lowY, highX, highY) - var newOriginX = (lowX + highX) / 2 - var newOriginY = (lowY + highY) / 2 - } else window.alert('please call function with a valid layout dammit!') + } else { + window.alert('please call function with a valid layout dammit!') + } } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 5d45d0fc..f8c93c3f 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -8,18 +8,18 @@ const PasteInput = { // thanks to https://github.com/kevva/url-regex URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'), - init: function () { + init: function() { var self = PasteInput // intercept dragged files // see http://stackoverflow.com/questions/6756583 - window.addEventListener("dragover", function(e) { - e = e || window.event; - e.preventDefault(); - }, false); - window.addEventListener("drop", function(e) { - e = e || window.event; - e.preventDefault(); + window.addEventListener('dragover', function(e) { + e = e || window.event + e.preventDefault() + }, false) + window.addEventListener('drop', function(e) { + e = e || window.event + e.preventDefault() var coords = Util.pixelsToCoords(Visualize.mGraph, { x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { self.handleFile(e.dataTransfer.files[0], coords) @@ -32,10 +32,10 @@ const PasteInput = { } }) } - }, false); + }, false) // allow pasting onto canvas (but don't break existing inputs/textareas) - $('body').bind('paste', function (e) { + $('body').bind('paste', function(e) { if (e.target.tagName === 'INPUT') return if (e.target.tagName === 'TEXTAREA') return @@ -50,7 +50,7 @@ const PasteInput = { fileReader.readAsText(file) fileReader.onload = function(e) { var text = e.currentTarget.result - if (text.substring(0,5) === '(.*)<\/string>[\s\S]*/m, '$1') } diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index 1e91975a..db1334de 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -1,15 +1,11 @@ /* global $ */ -import _ from 'lodash' import SimpleWebRTC from 'simplewebrtc' import SocketIoConnection from 'simplewebrtc/socketioconnection' import Active from '../Active' import DataModel from '../DataModel' -import GlobalUI from '../GlobalUI' import JIT from '../JIT' -import Synapse from '../Synapse' -import Topic from '../Topic' import Util from '../Util' import Views from '../Views' import Visualize from '../Visualize' @@ -67,7 +63,7 @@ import { synapseUpdated, synapseRemoved, synapseDeleted, - mapUpdated, + mapUpdated } from './receivable' import { @@ -110,15 +106,15 @@ let Realtime = { inConversation: false, localVideo: null, 'junto_spinner_darkgrey.gif': '', - init: function (serverData) { + init: function(serverData) { var self = Realtime self.addJuntoListeners() - self.socket = new SocketIoConnection({ url: serverData['REALTIME_SERVER']}) + self.socket = new SocketIoConnection({ url: serverData['REALTIME_SERVER'] }) self['junto_spinner_darkgrey.gif'] = serverData['junto_spinner_darkgrey.gif'] - - self.socket.on('connect', function () { + + self.socket.on('connect', function() { console.log('connected') subscribeToEvents(self, self.socket) @@ -126,7 +122,7 @@ let Realtime = { self.startActiveMap() } else self.disconnected = false }) - self.socket.on('disconnect', function () { + self.socket.on('disconnect', function() { self.disconnected = true }) @@ -136,7 +132,7 @@ let Realtime = { localVideoEl: self.videoId, remoteVideosEl: '', debug: true, - detectSpeakingEvents: false, //true, + detectSpeakingEvents: false, // true, autoAdjustMic: false, // true, autoRequestMedia: false, localVideo: { @@ -150,11 +146,11 @@ let Realtime = { }, nick: Active.Mapper.id }) - self.webrtc.webrtc.on('iceFailed', function (peer) { + self.webrtc.webrtc.on('iceFailed', function(peer) { console.log('local ice failure', peer) // local ice failure }) - self.webrtc.webrtc.on('connectivityError', function (peer) { + self.webrtc.webrtc.on('connectivityError', function(peer) { console.log('remote ice failure', peer) // remote ice failure }) @@ -186,33 +182,33 @@ let Realtime = { $('body').prepend(self.room.chat.$container) } // if Active.Mapper }, - addJuntoListeners: function () { + addJuntoListeners: function() { var self = Realtime - $(document).on(Views.ChatView.events.openTray, function () { + $(document).on(Views.ChatView.events.openTray, function() { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Views.ChatView.events.closeTray, function () { + $(document).on(Views.ChatView.events.closeTray, function() { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Views.ChatView.events.videosOn, function () { + $(document).on(Views.ChatView.events.videosOn, function() { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Views.ChatView.events.videosOff, function () { + $(document).on(Views.ChatView.events.videosOff, function() { $('#wrapper').addClass('hideVideos') }) - $(document).on(Views.ChatView.events.cursorsOn, function () { + $(document).on(Views.ChatView.events.cursorsOn, function() { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Views.ChatView.events.cursorsOff, function () { + $(document).on(Views.ChatView.events.cursorsOff, function() { $('#wrapper').addClass('hideCursors') }) }, - startActiveMap: function () { + startActiveMap: function() { var self = Realtime if (Active.Map && Active.Mapper) { if (Active.Map.authorizeToEdit(Active.Mapper)) { @@ -223,7 +219,7 @@ let Realtime = { self.room.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) } }, - endActiveMap: function () { + endActiveMap: function() { var self = Realtime $(document).off('.map') // leave the appropriate rooms to leave @@ -236,7 +232,7 @@ let Realtime = { self.room.chat.close() } }, - turnOn: function (notify) { + turnOn: function(notify) { var self = Realtime $('.collabCompass').show() self.room.chat.$container.show() @@ -254,16 +250,16 @@ let Realtime = { }) self.room.chat.addParticipant(self.activeMapper) }, - setupSocket: function () { + setupSocket: function() { var self = Realtime self.checkForCall() self.joinMap() }, - setupLocalSendables: function () { + setupLocalSendables: function() { var self = Realtime // local event listeners that trigger events - var sendCoords = function (event) { + var sendCoords = function(event) { var pixels = { x: event.pageX, y: event.pageY @@ -273,7 +269,7 @@ let Realtime = { } $(document).on('mousemove.map', sendCoords) - var zoom = function (event, e) { + var zoom = function(event, e) { if (e) { var pixels = { x: e.pageX, @@ -288,47 +284,47 @@ let Realtime = { $(document).on(JIT.events.pan + '.map', self.positionPeerIcons) - var dragTopic = function (event, positions) { + var dragTopic = function(event, positions) { self.dragTopic(positions) } $(document).on(JIT.events.topicDrag + '.map', dragTopic) - var createTopic = function (event, data) { + var createTopic = function(event, data) { self.createTopic(data) } $(document).on(JIT.events.newTopic + '.map', createTopic) - var deleteTopic = function (event, data) { + var deleteTopic = function(event, data) { self.deleteTopic(data) } $(document).on(JIT.events.deleteTopic + '.map', deleteTopic) - var removeTopic = function (event, data) { + var removeTopic = function(event, data) { self.removeTopic(data) } $(document).on(JIT.events.removeTopic + '.map', removeTopic) - var createSynapse = function (event, data) { + var createSynapse = function(event, data) { self.createSynapse(data) } $(document).on(JIT.events.newSynapse + '.map', createSynapse) - var deleteSynapse = function (event, data) { + var deleteSynapse = function(event, data) { self.deleteSynapse(data) } $(document).on(JIT.events.deleteSynapse + '.map', deleteSynapse) - var removeSynapse = function (event, data) { + var removeSynapse = function(event, data) { self.removeSynapse(data) } $(document).on(JIT.events.removeSynapse + '.map', removeSynapse) - var createMessage = function (event, data) { + var createMessage = function(event, data) { self.createMessage(data) } $(document).on(Views.Room.events.newMessage + '.map', createMessage) }, - countOthersInConversation: function () { + countOthersInConversation: function() { var self = Realtime var count = 0 for (var key in self.mappersOnMap) { @@ -336,7 +332,7 @@ let Realtime = { } return count }, - handleVideoAdded: function (v, id) { + handleVideoAdded: function(v, id) { var self = Realtime self.positionVideos() v.setParent($('#wrapper')) @@ -345,16 +341,15 @@ let Realtime = { }) $('#wrapper').append(v.$container) }, - positionVideos: function () { + positionVideos: function() { var self = Realtime var videoIds = Object.keys(self.room.videos) - var numOfVideos = videoIds.length - var numOfVideosToPosition = _.filter(videoIds, function (id) { - return !self.room.videos[id].manuallyPositioned - }).length + // var numOfVideos = videoIds.length + // var numOfVideosToPosition = _.filter(videoIds, function(id) { + // return !self.room.videos[id].manuallyPositioned + // }).length var screenHeight = $(document).height() - var screenWidth = $(document).width() var topExtraPadding = 20 var topPadding = 30 var leftPadding = 30 @@ -362,7 +357,7 @@ let Realtime = { var videoWidth = 180 var column = 0 var row = 0 - var yFormula = function () { + var yFormula = function() { var y = topExtraPadding + (topPadding + videoHeight) * row + topPadding if (y + videoHeight > screenHeight) { row = 0 @@ -372,7 +367,7 @@ let Realtime = { row++ return y } - var xFormula = function () { + var xFormula = function() { var x = (leftPadding + videoWidth) * column + leftPadding return x } @@ -385,7 +380,7 @@ let Realtime = { left: xFormula() + 'px' }) } - videoIds.forEach(function (id) { + videoIds.forEach(function(id) { var video = self.room.videos[id] if (!video.manuallyPositioned) { video.$container.css({ @@ -395,7 +390,7 @@ let Realtime = { } }) }, - callEnded: function () { + callEnded: function() { var self = Realtime self.room.conversationEnding() @@ -408,7 +403,7 @@ let Realtime = { self.localVideo.view.audioOn() self.localVideo.view.videoOn() }, - createCompass: function (name, id, image, color) { + createCompass: function(name, id, image, color) { var str = '

        ' + name + '

        ' str += '
        ' $('#compass' + id).remove() @@ -423,20 +418,15 @@ let Realtime = { 'background-color': color }) }, - positionPeerIcons: function () { + positionPeerIcons: function() { var self = Realtime for (var key in self.mappersOnMap) { self.positionPeerIcon(key) } }, - positionPeerIcon: function (id) { + positionPeerIcon: function(id) { var self = Realtime - var boundary = self.chatOpen ? '#wrapper' : document var mapper = self.mappersOnMap[id] - var xMax = $(boundary).width() - var yMax = $(boundary).height() - var compassDiameter = 56 - var compassArrowSize = 24 var origPixels = Util.coordsToPixels(Visualize.mGraph, mapper.coords) var pixels = self.limitPixelsToScreen(origPixels) @@ -448,12 +438,11 @@ let Realtime = { if (origPixels.x !== pixels.x || origPixels.y !== pixels.y) { var dy = origPixels.y - pixels.y // opposite var dx = origPixels.x - pixels.x // adjacent - var ratio = dy / dx var angle = Math.atan2(dy, dx) $('#compassArrow' + id).show().css({ transform: 'rotate(' + angle + 'rad)', - '-webkit-transform': 'rotate(' + angle + 'rad)', + '-webkit-transform': 'rotate(' + angle + 'rad)' }) if (dx > 0) { @@ -464,7 +453,7 @@ let Realtime = { $('#compass' + id).removeClass('labelLeft') } }, - limitPixelsToScreen: function (pixels) { + limitPixelsToScreen: function(pixels) { var self = Realtime var boundary = self.chatOpen ? '#wrapper' : document @@ -479,65 +468,65 @@ let Realtime = { yLimit = Math.max(0 + compassArrowSize, pixels.y) yLimit = Math.min(yLimit, yMax - compassDiameter) - return {x: xLimit,y: yLimit} + return {x: xLimit, y: yLimit} } } const sendables = [ - ['joinMap',joinMap], - ['leaveMap',leaveMap], - ['checkForCall',checkForCall], - ['acceptCall',acceptCall], - ['denyCall',denyCall], - ['denyInvite',denyInvite], - ['inviteToJoin',inviteToJoin], - ['inviteACall',inviteACall], - ['joinCall',joinCall], - ['leaveCall',leaveCall], - ['sendMapperInfo',sendMapperInfo], - ['sendCoords',sendCoords], - ['createMessage',createMessage], - ['dragTopic',dragTopic], - ['createTopic',createTopic], - ['updateTopic',updateTopic], - ['removeTopic',removeTopic], - ['deleteTopic',deleteTopic], - ['createSynapse',createSynapse], - ['updateSynapse',updateSynapse], - ['removeSynapse',removeSynapse], - ['deleteSynapse',deleteSynapse], - ['updateMap',updateMap] + ['joinMap', joinMap], + ['leaveMap', leaveMap], + ['checkForCall', checkForCall], + ['acceptCall', acceptCall], + ['denyCall', denyCall], + ['denyInvite', denyInvite], + ['inviteToJoin', inviteToJoin], + ['inviteACall', inviteACall], + ['joinCall', joinCall], + ['leaveCall', leaveCall], + ['sendMapperInfo', sendMapperInfo], + ['sendCoords', sendCoords], + ['createMessage', createMessage], + ['dragTopic', dragTopic], + ['createTopic', createTopic], + ['updateTopic', updateTopic], + ['removeTopic', removeTopic], + ['deleteTopic', deleteTopic], + ['createSynapse', createSynapse], + ['updateSynapse', updateSynapse], + ['removeSynapse', removeSynapse], + ['deleteSynapse', deleteSynapse], + ['updateMap', updateMap] ] sendables.forEach(sendable => { Realtime[sendable[0]] = sendable[1](Realtime) }) const subscribeToEvents = (Realtime, socket) => { - socket.on(JUNTO_UPDATED, juntoUpdated(Realtime)) - socket.on(INVITED_TO_CALL, invitedToCall(Realtime)) - socket.on(INVITED_TO_JOIN, invitedToJoin(Realtime)) - socket.on(CALL_ACCEPTED, callAccepted(Realtime)) - socket.on(CALL_DENIED, callDenied(Realtime)) - socket.on(INVITE_DENIED, inviteDenied(Realtime)) - socket.on(CALL_IN_PROGRESS, callInProgress(Realtime)) - socket.on(CALL_STARTED, callStarted(Realtime)) - socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime)) - socket.on(MAPPER_JOINED_CALL, mapperJoinedCall(Realtime)) - socket.on(MAPPER_LEFT_CALL, mapperLeftCall(Realtime)) - socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime)) - socket.on(NEW_MAPPER, newMapper(Realtime)) - socket.on(LOST_MAPPER, lostMapper(Realtime)) - socket.on(MESSAGE_CREATED, messageCreated(Realtime)) - socket.on(TOPIC_DRAGGED, topicDragged(Realtime)) - socket.on(TOPIC_CREATED, topicCreated(Realtime)) - socket.on(TOPIC_UPDATED, topicUpdated(Realtime)) - socket.on(TOPIC_REMOVED, topicRemoved(Realtime)) - socket.on(TOPIC_DELETED, topicDeleted(Realtime)) - socket.on(SYNAPSE_CREATED, synapseCreated(Realtime)) - socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime)) - socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) - socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime)) - socket.on(MAP_UPDATED, mapUpdated(Realtime)) + socket.on(JUNTO_UPDATED, juntoUpdated(Realtime)) + socket.on(INVITED_TO_CALL, invitedToCall(Realtime)) + socket.on(INVITED_TO_JOIN, invitedToJoin(Realtime)) + socket.on(CALL_ACCEPTED, callAccepted(Realtime)) + socket.on(CALL_DENIED, callDenied(Realtime)) + socket.on(INVITE_DENIED, inviteDenied(Realtime)) + socket.on(CALL_IN_PROGRESS, callInProgress(Realtime)) + socket.on(CALL_STARTED, callStarted(Realtime)) + socket.on(MAPPER_LIST_UPDATED, mapperListUpdated(Realtime)) + socket.on(MAPPER_JOINED_CALL, mapperJoinedCall(Realtime)) + socket.on(MAPPER_LEFT_CALL, mapperLeftCall(Realtime)) + socket.on(PEER_COORDS_UPDATED, peerCoordsUpdated(Realtime)) + socket.on(NEW_MAPPER, newMapper(Realtime)) + socket.on(LOST_MAPPER, lostMapper(Realtime)) + socket.on(MESSAGE_CREATED, messageCreated(Realtime)) + socket.on(TOPIC_DRAGGED, topicDragged(Realtime)) + socket.on(TOPIC_CREATED, topicCreated(Realtime)) + socket.on(TOPIC_UPDATED, topicUpdated(Realtime)) + socket.on(TOPIC_REMOVED, topicRemoved(Realtime)) + socket.on(TOPIC_DELETED, topicDeleted(Realtime)) + socket.on(SYNAPSE_CREATED, synapseCreated(Realtime)) + socket.on(SYNAPSE_UPDATED, synapseUpdated(Realtime)) + socket.on(SYNAPSE_REMOVED, synapseRemoved(Realtime)) + socket.on(SYNAPSE_DELETED, synapseDeleted(Realtime)) + socket.on(MAP_UPDATED, mapUpdated(Realtime)) } export default Realtime diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js index 4ae838ab..32b6bf0c 100644 --- a/frontend/src/Metamaps/Realtime/receivable.js +++ b/frontend/src/Metamaps/Realtime/receivable.js @@ -4,6 +4,8 @@ everthing in this file happens as a result of websocket events */ +import { indexOf } from 'lodash' + import { JUNTO_UPDATED } from './events' import Active from '../Active' @@ -31,7 +33,7 @@ export const synapseRemoved = self => data => { Control.hideEdge(edge) } - var index = _.indexOf(edge.getData('synapses'), synapse) + var index = indexOf(edge.getData('synapses'), synapse) edge.getData('mappings').splice(index, 1) edge.getData('synapses').splice(index, 1) if (edge.getData('displayIndex')) { @@ -49,8 +51,7 @@ export const synapseDeleted = self => data => { export const synapseCreated = self => data => { var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper - - function waitThenRenderSynapse () { + function waitThenRenderSynapse() { if (synapse && mapping && mapper) { topic1 = synapse.getTopic1() node1 = topic1.get('node') @@ -58,8 +59,7 @@ export const synapseCreated = self => data => { node2 = topic2.get('node') Synapse.renderSynapse(mapping, synapse, node1, node2, false) - } - else if (!cancel) { + } else if (!cancel) { setTimeout(waitThenRenderSynapse, 10) } } @@ -73,21 +73,21 @@ export const synapseCreated = self => data => { } $.ajax({ url: '/synapses/' + data.mappableid + '.json', - success: function (response) { + success: function(response) { DataModel.Synapses.add(response) synapse = DataModel.Synapses.get(response.id) }, - error: function () { + error: function() { cancel = true } }) $.ajax({ url: '/mappings/' + data.mappingid + '.json', - success: function (response) { + success: function(response) { DataModel.Mappings.add(response) mapping = DataModel.Mappings.get(response.id) }, - error: function () { + error: function() { cancel = true } }) @@ -112,11 +112,10 @@ export const topicDeleted = self => data => { export const topicCreated = self => data => { var topic, mapping, mapper, cancel - function waitThenRenderTopic () { + function waitThenRenderTopic() { if (topic && mapping && mapper) { Topic.renderTopic(mapping, topic, false, false) - } - else if (!cancel) { + } else if (!cancel) { setTimeout(waitThenRenderTopic, 10) } } @@ -130,21 +129,21 @@ export const topicCreated = self => data => { } $.ajax({ url: '/topics/' + data.mappableid + '.json', - success: function (response) { + success: function(response) { DataModel.Topics.add(response) topic = DataModel.Topics.get(response.id) }, - error: function () { + error: function() { cancel = true } }) $.ajax({ url: '/mappings/' + data.mappingid + '.json', - success: function (response) { + success: function(response) { DataModel.Mappings.add(response) mapping = DataModel.Mappings.get(response.id) }, - error: function () { + error: function() { cancel = true } }) @@ -163,20 +162,18 @@ export const mapUpdated = self => data => { var couldEditBefore = map.authorizeToEdit(Active.Mapper) var idBefore = map.id map.fetch({ - success: function (model, response) { - var idNow = model.id - var canEditNow = model.authorizeToEdit(Active.Mapper) - if (idNow !== idBefore) { - Map.leavePrivateMap() // this means the map has been changed to private - } - else if (couldEditBefore && !canEditNow) { - Map.cantEditNow() - } - else if (!couldEditBefore && canEditNow) { - Map.canEditNow() - } else { - model.trigger('changeByOther') - } + success: function(model, response) { + var idNow = model.id + var canEditNow = model.authorizeToEdit(Active.Mapper) + if (idNow !== idBefore) { + Map.leavePrivateMap() // this means the map has been changed to private + } else if (couldEditBefore && !canEditNow) { + Map.cantEditNow() + } else if (!couldEditBefore && canEditNow) { + Map.canEditNow() + } else { + model.trigger('changeByOther') + } } }) } @@ -187,9 +184,9 @@ export const topicUpdated = self => data => { if (topic) { var node = topic.get('node') topic.fetch({ - success: function (model) { - model.set({ node: node }) - model.trigger('changeByOther') + success: function(model) { + model.set({ node: node }) + model.trigger('changeByOther') } }) } @@ -201,9 +198,9 @@ export const synapseUpdated = self => data => { // edge reset necessary because fetch causes model reset var edge = synapse.get('edge') synapse.fetch({ - success: function (model) { - model.set({ edge: edge }) - model.trigger('changeByOther') + success: function(model) { + model.set({ edge: edge }) + model.trigger('changeByOther') } }) } @@ -225,7 +222,7 @@ export const topicDragged = self => positions => { export const peerCoordsUpdated = self => data => { if (!self.mappersOnMap[data.userid]) return - self.mappersOnMap[data.userid].coords = {x: data.usercoords.x,y: data.usercoords.y} + self.mappersOnMap[data.userid].coords = {x: data.usercoords.x, y: data.usercoords.y} self.positionPeerIcon(data.userid) } @@ -311,7 +308,7 @@ export const newMapper = self => data => { } export const callAccepted = self => userid => { - var username = self.mappersOnMap[userid].name + // const username = self.mappersOnMap[userid].name GlobalUI.notifyUser('Conversation starting...') self.joinCall() self.room.chat.invitationAnswered(userid) diff --git a/frontend/src/Metamaps/Realtime/sendable.js b/frontend/src/Metamaps/Realtime/sendable.js index 71abc35c..ef35cb85 100644 --- a/frontend/src/Metamaps/Realtime/sendable.js +++ b/frontend/src/Metamaps/Realtime/sendable.js @@ -1,3 +1,5 @@ +/* global $ */ + import Active from '../Active' import GlobalUI from '../GlobalUI' @@ -42,7 +44,7 @@ export const leaveMap = self => () => { } export const checkForCall = self => () => { - self.socket.emit(CHECK_FOR_CALL, { room: self.room.room, mapid: Active.Map.id }) + self.socket.emit(CHECK_FOR_CALL, { room: self.room.room, mapid: Active.Map.id }) } export const sendMapperInfo = self => userid => { @@ -60,7 +62,7 @@ export const sendMapperInfo = self => userid => { export const joinCall = self => () => { self.webrtc.off('readyToCall') - self.webrtc.once('readyToCall', function () { + self.webrtc.once('readyToCall', function() { self.videoInitialized = true self.readyToCall = true self.localVideo.view.manuallyPositioned = false diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 74648ac1..f1e2aa97 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -23,7 +23,7 @@ const _Router = Backbone.Router.extend({ 'maps/:id': 'maps', // #maps/7 'topics/:id': 'topics' // #topics/7 }, - home: function () { + home: function() { let self = this clearTimeout(this.timeoutId) @@ -37,8 +37,8 @@ const _Router = Backbone.Router.extend({ var classes = Active.Mapper ? 'homePage explorePage' : 'homePage' $('.wrapper').addClass(classes) - var navigate = function () { - self.timeoutId = setTimeout(function () { + var navigate = function() { + self.timeoutId = setTimeout(function() { self.navigate('') }, 300) } @@ -71,7 +71,7 @@ const _Router = Backbone.Router.extend({ Active.Map = null Active.Topic = null }, - explore: function (section, id) { + explore: function(section, id) { var self = this clearTimeout(this.timeoutId) @@ -84,16 +84,16 @@ const _Router = Backbone.Router.extend({ } else if (section === 'mapper') { $.ajax({ url: '/users/' + id + '.json', - success: function (response) { + success: function(response) { document.title = response.name + ' | Metamaps' }, - error: function () {} + error: function() {} }) } else if (section === 'mine') { document.title = 'Explore My Maps | Metamaps' } - if (Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) + if (Active.Mapper && section !== 'mapper') $('.homeButton a').attr('href', '/explore/' + section) $('.wrapper').removeClass('homePage mapPage topicPage') $('.wrapper').addClass('explorePage') @@ -113,23 +113,23 @@ const _Router = Backbone.Router.extend({ Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) - var navigate = function () { + var navigate = function() { var path = '/explore/' + self.currentPage // alter url if for mapper profile page if (self.currentPage === 'mapper') { path += '/' + DataModel.Maps.Mapper.mapperId } - + self.navigate(path) } - var navigateTimeout = function () { + var navigateTimeout = function() { self.timeoutId = setTimeout(navigate, 300) } if (DataModel.Maps[capitalize].length === 0) { Loading.show() Views.ExploreMaps.pending = true - setTimeout(function () { + setTimeout(function() { DataModel.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { @@ -149,7 +149,7 @@ const _Router = Backbone.Router.extend({ Active.Map = null Active.Topic = null }, - maps: function (id) { + maps: function(id) { clearTimeout(this.timeoutId) this.currentSection = 'map' @@ -175,7 +175,7 @@ const _Router = Backbone.Router.extend({ Map.end() Map.launch(id) }, - topics: function (id) { + topics: function(id) { clearTimeout(this.timeoutId) this.currentSection = 'topic' @@ -202,7 +202,7 @@ const _Router = Backbone.Router.extend({ const Router = new _Router() -Router.intercept = function (evt) { +Router.intercept = function(evt) { var segments var href = { @@ -227,7 +227,7 @@ Router.intercept = function (evt) { } } -Router.init = function () { +Router.init = function() { Backbone.history.start({ silent: true, pushState: true, diff --git a/frontend/src/Metamaps/Selected.js b/frontend/src/Metamaps/Selected.js index d23517b5..86654b8f 100644 --- a/frontend/src/Metamaps/Selected.js +++ b/frontend/src/Metamaps/Selected.js @@ -1,5 +1,5 @@ const Selected = { - reset: function () { + reset: function() { var self = Selected self.Nodes = [] self.Edges = [] diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 0345690d..d433cced 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -15,12 +15,12 @@ const noOp = () => {} const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve - get: function (id, callback = noOp) { + get: function(id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it - if (DataModel.Synapses.get(id) == undefined) { + if (DataModel.Synapses.get(id) === undefined) { $.ajax({ url: '/synapses/' + id + '.json', - success: function (data) { + success: function(data) { DataModel.Synapses.add(data) callback(DataModel.Synapses.get(id)) } @@ -28,9 +28,7 @@ const Synapse = { } else callback(DataModel.Synapses.get(id)) }, - renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { - var self = Synapse - + renderSynapse: function(mapping, synapse, node1, node2, createNewInDB) { var edgeOnViz var newedge = synapse.createEdge(mapping) @@ -42,7 +40,7 @@ const Synapse = { Control.selectEdge(edgeOnViz) - var mappingSuccessCallback = function (mappingModel, response) { + var mappingSuccessCallback = function(mappingModel, response) { var newSynapseData = { mappingid: mappingModel.id, mappableid: mappingModel.get('mappable_id') @@ -50,7 +48,7 @@ const Synapse = { $(document).trigger(JIT.events.newSynapse, [newSynapseData]) } - var synapseSuccessCallback = function (synapseModel, response) { + var synapseSuccessCallback = function(synapseModel, response) { if (Active.Map) { mapping.save({ mappable_id: synapseModel.id }, { success: mappingSuccessCallback @@ -62,7 +60,7 @@ const Synapse = { if (synapse.isNew()) { synapse.save(null, { success: synapseSuccessCallback, - error: function (model, response) { + error: function(model, response) { console.log('error saving synapse to database') } }) @@ -73,14 +71,14 @@ const Synapse = { } } }, - createSynapseLocally: function () { - var self = Synapse, - topic1, - topic2, - node1, - node2, - synapse, - mapping + createSynapseLocally: function() { + var self = Synapse + let topic1 + let topic2 + let node1 + let node2 + let synapse + let mapping $(document).trigger(Map.events.editedByActiveMapper) @@ -91,7 +89,7 @@ const Synapse = { node2 = topic2.get('node') var len = Selected.Nodes.length - if (len == 0) { + if (len === 0) { topic1 = DataModel.Topics.get(Create.newSynapse.topic1id) synapsesToCreate[0] = topic1.get('node') } else if (len > 0) { @@ -104,13 +102,13 @@ const Synapse = { synapse = new DataModel.Synapse({ desc: Create.newSynapse.description, topic1_id: topic1.isNew() ? topic1.cid : topic1.id, - topic2_id: topic2.isNew() ? topic2.cid : topic2.id, + topic2_id: topic2.isNew() ? topic2.cid : topic2.id }) DataModel.Synapses.add(synapse) mapping = new DataModel.Mapping({ mappable_type: 'Synapse', - mappable_id: synapse.cid, + mappable_id: synapse.cid }) DataModel.Mappings.add(mapping) @@ -120,23 +118,19 @@ const Synapse = { Create.newSynapse.hide() }, - getSynapseFromAutocomplete: function (id) { - var self = Synapse, - topic1, - topic2, - node1, - node2 + getSynapseFromAutocomplete: function(id) { + var self = Synapse self.get(id, synapse => { - var mapping = new DataModel.Mapping({ + const mapping = new DataModel.Mapping({ mappable_type: 'Synapse', - mappable_id: synapse.id, + mappable_id: synapse.id }) DataModel.Mappings.add(mapping) - topic1 = DataModel.Topics.get(Create.newSynapse.topic1id) - node1 = topic1.get('node') - topic2 = DataModel.Topics.get(Create.newSynapse.topic2id) - node2 = topic2.get('node') + const topic1 = DataModel.Topics.get(Create.newSynapse.topic1id) + const node1 = topic1.get('node') + const topic2 = DataModel.Topics.get(Create.newSynapse.topic2id) + const node2 = topic2.get('node') Create.newSynapse.hide() self.renderSynapse(mapping, synapse, node1, node2, true) }) diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 303b98cf..b7b58821 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -6,7 +6,7 @@ import Visualize from './Visualize' const SynapseCard = { openSynapseCard: null, - showCard: function (edge, e) { + showCard: function(edge, e) { var self = SynapseCard // reset so we don't interfere with other edges, but first, save its x and y @@ -18,20 +18,20 @@ const SynapseCard = { Control.deselectEdge(edge) var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 - var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse + var synapse = edge.getData('synapses')[index] // for now, just get the first synapse // create the wrapper around the form elements, including permissions // classes to make best_in_place happy - var edit_div = document.createElement('div') - edit_div.innerHTML = '
        ' - edit_div.setAttribute('id', 'edit_synapse') + var editDiv = document.createElement('div') + editDiv.innerHTML = '
        ' + editDiv.setAttribute('id', 'edit_synapse') if (synapse.authorizeToEdit(Active.Mapper)) { - edit_div.className = 'permission canEdit' - edit_div.className += synapse.authorizePermissionChange(Active.Mapper) ? ' yourEdge' : '' + editDiv.className = 'permission canEdit' + editDiv.className += synapse.authorizePermissionChange(Active.Mapper) ? ' yourEdge' : '' } else { - edit_div.className = 'permission cannotEdit' + editDiv.className = 'permission cannotEdit' } - $('#wrapper').append(edit_div) + $('#wrapper').append(editDiv) self.populateShowCard(edge, synapse) @@ -51,12 +51,12 @@ const SynapseCard = { self.openSynapseCard = edge }, - hideCard: function () { + hideCard: function() { $('#edit_synapse').remove() SynapseCard.openSynapseCard = null }, - populateShowCard: function (edge, synapse) { + populateShowCard: function(edge, synapse) { var self = SynapseCard self.add_synapse_count(edge) @@ -66,13 +66,13 @@ const SynapseCard = { self.add_perms_form(synapse) self.add_direction_form(synapse) }, - add_synapse_count: function (edge) { + add_synapse_count: function(edge) { var count = edge.getData('synapses').length $('#editSynUpperBar').append('
        ' + count + '
        ') }, - add_desc_form: function (synapse) { - var data_nil = 'Click to add description.' + add_desc_form: function(synapse) { + var dataNil = 'Click to add description.' // TODO make it so that this would work even in sandbox mode, // currently with Best_in_place it won't @@ -83,29 +83,29 @@ const SynapseCard = { $('#edit_synapse_desc').attr('data-bip-object', 'synapse') $('#edit_synapse_desc').attr('data-bip-attribute', 'desc') $('#edit_synapse_desc').attr('data-bip-type', 'textarea') - $('#edit_synapse_desc').attr('data-bip-nil', data_nil) + $('#edit_synapse_desc').attr('data-bip-nil', dataNil) $('#edit_synapse_desc').attr('data-bip-url', '/synapses/' + synapse.id) $('#edit_synapse_desc').attr('data-bip-value', synapse.get('desc')) $('#edit_synapse_desc').html(synapse.get('desc')) - // if edge data is blank or just whitespace, populate it with data_nil - if ($('#edit_synapse_desc').html().trim() == '') { + // if edge data is blank or just whitespace, populate it with dataNil + if ($('#edit_synapse_desc').html().trim() === '') { if (synapse.authorizeToEdit(Active.Mapper)) { - $('#edit_synapse_desc').html(data_nil) + $('#edit_synapse_desc').html(dataNil) } else { $('#edit_synapse_desc').html('(no description)') } } - $('#edit_synapse_desc').keypress(function (e) { + $('#edit_synapse_desc').keypress(function(e) { const ENTER = 13 if (e.which === ENTER) { $(this).data('bestInPlaceEditor').update() } }) - $('#edit_synapse_desc').bind('ajax:success', function () { + $('#edit_synapse_desc').bind('ajax:success', function() { var desc = $(this).html() - if (desc == data_nil) { + if (desc === dataNil) { synapse.set('desc', '') } else { synapse.set('desc', desc) @@ -115,7 +115,7 @@ const SynapseCard = { Visualize.mGraph.plot() }) }, - add_drop_down: function (edge, synapse) { + add_drop_down: function(edge, synapse) { var list, i, synapses, l, desc synapses = edge.getData('synapses') @@ -124,13 +124,13 @@ const SynapseCard = { if (l > 1) { // append the element that you click to show dropdown select $('#editSynUpperBar').append('') - $('#dropdownSynapses').click(function (e) { + $('#dropdownSynapses').click(function(e) { e.preventDefault() e.stopPropagation() // stop it from immediately closing it again $('#switchSynapseList').toggle() }) // hide the dropdown again if you click anywhere else on the synapse card - $('#edit_synapse').click(function () { + $('#edit_synapse').click(function() { $('#switchSynapseList').hide() }) @@ -150,7 +150,7 @@ const SynapseCard = { // attach click listeners to list items that // will cause it to switch the displayed synapse // when you click it - $('#switchSynapseList li').click(function (e) { + $('#switchSynapseList li').click(function(e) { e.stopPropagation() var index = parseInt($(this).attr('data-synapse-index')) edge.setData('displayIndex', index) @@ -159,26 +159,26 @@ const SynapseCard = { }) } }, - add_user_info: function (synapse) { + add_user_info: function(synapse) { var u = '
        ' u += ' ' u += '
        ' + synapse.get('user_name') + '
        ' $('#editSynLowerBar').append(u) // get mapper image - var setMapperImage = function (mapper) { + var setMapperImage = function(mapper) { $('#edgeUser img').attr('src', mapper.get('image')) } Mapper.get(synapse.get('user_id'), setMapperImage) }, - add_perms_form: function (synapse) { + add_perms_form: function(synapse) { // permissions - if owner, also allow permission editing $('#editSynLowerBar').append('
        ') // ability to change permission var selectingPermission = false - var permissionLiClick = function (event) { + var permissionLiClick = function(event) { selectingPermission = false var permission = $(this).attr('class') synapse.save({ @@ -190,7 +190,7 @@ const SynapseCard = { event.stopPropagation() } - var openPermissionSelect = function (event) { + var openPermissionSelect = function(event) { if (!selectingPermission) { selectingPermission = true $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow @@ -206,7 +206,7 @@ const SynapseCard = { } } - var hidePermissionSelect = function () { + var hidePermissionSelect = function() { selectingPermission = false $('#edit_synapse.yourEdge .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow $('#edit_synapse .permissionSelect').remove() @@ -218,7 +218,7 @@ const SynapseCard = { } }, // add_perms_form - add_direction_form: function (synapse) { + add_direction_form: function(synapse) { // directionality checkboxes $('#editSynLowerBar').append('
        ') $('#editSynLowerBar').append('
        ') @@ -227,14 +227,16 @@ const SynapseCard = { // determine which node is to the left and the right // if directly in a line, top is left + let left + let right if (edge.nodeFrom.pos.x < edge.nodeTo.pos.x || - edge.nodeFrom.pos.x == edge.nodeTo.pos.x && + edge.nodeFrom.pos.x === edge.nodeTo.pos.x && edge.nodeFrom.pos.y < edge.nodeTo.pos.y) { - var left = edge.nodeTo.getData('topic') - var right = edge.nodeFrom.getData('topic') + left = edge.nodeTo.getData('topic') + right = edge.nodeFrom.getData('topic') } else { - var left = edge.nodeFrom.getData('topic') - var right = edge.nodeTo.getData('topic') + left = edge.nodeFrom.getData('topic') + right = edge.nodeTo.getData('topic') } /* @@ -243,24 +245,24 @@ const SynapseCard = { * Else check the 'left' checkbox since the arrow is incoming. */ - var directionCat = synapse.get('category'); // both, none, from-to - if (directionCat == 'from-to') { - var from_to = [synapse.get('topic1_id'), synapse.get('topic2_id')] - if (from_to[0] == left.id) { + var directionCat = synapse.get('category') // both, none, from-to + if (directionCat === 'from-to') { + var fromTo = [synapse.get('topic1_id'), synapse.get('topic2_id')] + if (fromTo[0] === left.id) { // check left checkbox $('#edit_synapse_left').addClass('checked') } else { // check right checkbox $('#edit_synapse_right').addClass('checked') } - } else if (directionCat == 'both') { + } else if (directionCat === 'both') { // check both checkboxes $('#edit_synapse_left').addClass('checked') $('#edit_synapse_right').addClass('checked') } if (synapse.authorizeToEdit(Active.Mapper)) { - $('#edit_synapse_left, #edit_synapse_right').click(function () { + $('#edit_synapse_left, #edit_synapse_right').click(function() { $(this).toggleClass('checked') var leftChecked = $('#edit_synapse_left').is('.checked') diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 08ac0661..6b1aa8c1 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -23,20 +23,20 @@ const noOp = () => {} const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve - get: function (id, callback = noOp) { + get: function(id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it - if (DataModel.Topics.get(id) == undefined) { + if (DataModel.Topics.get(id) === undefined) { $.ajax({ url: '/topics/' + id + '.json', - success: function (data) { + success: function(data) { DataModel.Topics.add(data) callback(DataModel.Topics.get(id)) } }) } else callback(DataModel.Topics.get(id)) }, - launch: function (id) { - var start = function (data) { + launch: function(id) { + var start = function(data) { Active.Topic = new DataModel.Topic(data.topic) DataModel.Creators = new DataModel.MapperCollection(data.creators) DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives)) @@ -62,7 +62,7 @@ const Topic = { Filter.checkMetacodes() Filter.checkSynapses() Filter.checkMappers() - + // for mobile $('#header_content').html(Active.Topic.get('name')) } @@ -72,7 +72,7 @@ const Topic = { success: start }) }, - end: function () { + end: function() { if (Active.Topic) { $('.rightclickmenu').remove() TopicCard.hideCard() @@ -80,13 +80,13 @@ const Topic = { Filter.close() } }, - centerOn: function (nodeid, callback) { + centerOn: function(nodeid, callback) { // don't clash with fetchRelatives if (!Visualize.mGraph.busy) { Visualize.mGraph.onClick(nodeid, { hideLabels: false, duration: 1000, - onComplete: function () { + onComplete: function() { if (callback) callback() } }) @@ -94,21 +94,21 @@ const Topic = { Active.Topic = DataModel.Topics.get(nodeid) } }, - fetchRelatives: function (nodes, metacode_id) { + fetchRelatives: function(nodes, metacodeId) { var self = this var node = $.isArray(nodes) ? nodes[0] : nodes - var topics = DataModel.Topics.map(function (t) { return t.id }) - var topics_string = topics.join() + var topics = DataModel.Topics.map(function(t) { return t.id }) + var topicsString = topics.join() - var creators = DataModel.Creators.map(function (t) { return t.id }) - var creators_string = creators.join() + var creators = DataModel.Creators.map(function(t) { return t.id }) + var creatorsString = creators.join() var topic = node.getData('topic') - var successCallback; - successCallback = function (data) { + var successCallback + successCallback = function(data) { if (Visualize.mGraph.busy) { // don't clash with centerOn window.setTimeout(function() { successCallback(data) }, 100) @@ -131,12 +131,12 @@ const Topic = { var i, l, t, s - Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function(n) { t = DataModel.Topics.get(n.id) t.set({ node: n }, { silent: true }) t.updateNode() - n.eachAdjacency(function (edge) { + n.eachAdjacency(function(edge) { if (!edge.getData('init')) { edge.setData('init', true) @@ -150,31 +150,30 @@ const Topic = { }) }) if ($.isArray(nodes) && nodes.length > 1) { - self.fetchRelatives(nodes.slice(1), metacode_id) + self.fetchRelatives(nodes.slice(1), metacodeId) } } - var paramsString = metacode_id ? 'metacode=' + metacode_id + '&' : '' - paramsString += 'network=' + topics_string + '&creators=' + creators_string + let paramsString = metacodeId ? 'metacode=' + metacodeId + '&' : '' + paramsString += 'network=' + topicsString + '&creators=' + creatorsString $.ajax({ type: 'GET', url: '/topics/' + topic.id + '/relatives.json?' + paramsString, success: successCallback, - error: function () {} + error: function() {} }) }, // opts is additional options in a hash // TODO: move createNewInDB and permitCreateSynapseAfter into opts - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { - var self = Topic - + renderTopic: function(mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { var nodeOnViz, tempPos var newnode = topic.createNode() - var midpoint = {}, pixelPos + var midpoint = {} + var pixelPos if (!$.isEmptyObject(Visualize.mGraph.graph.nodes)) { Visualize.mGraph.graph.addNode(newnode) @@ -209,7 +208,7 @@ const Topic = { Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, - onComplete: function () { + onComplete: function() { JIT.tempNode = null JIT.tempNode2 = null JIT.tempInit = false @@ -220,7 +219,7 @@ const Topic = { Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, - onComplete: function () {} + onComplete: function() {} }) } } else { @@ -238,11 +237,11 @@ const Topic = { Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, - onComplete: function () {} + onComplete: function() {} }) } - var mappingSuccessCallback = function (mappingModel, response, topicModel) { + var mappingSuccessCallback = function(mappingModel, response, topicModel) { var newTopicData = { mappingid: mappingModel.id, mappableid: mappingModel.get('mappable_id') @@ -254,13 +253,13 @@ const Topic = { opts.success(topicModel) } } - var topicSuccessCallback = function (topicModel, response) { + var topicSuccessCallback = function(topicModel, response) { if (Active.Map) { mapping.save({ mappable_id: topicModel.id }, { - success: function (model, response) { + success: function(model, response) { mappingSuccessCallback(model, response, topicModel) }, - error: function (model, response) { + error: function(model, response) { console.log('error saving mapping to database') } }) @@ -275,7 +274,7 @@ const Topic = { if (topic.isNew()) { topic.save(null, { success: topicSuccessCallback, - error: function (model, response) { + error: function(model, response) { console.log('error saving topic to database') } }) @@ -286,7 +285,7 @@ const Topic = { } } }, - createTopicLocally: function () { + createTopicLocally: function() { var self = Topic if (Create.newTopic.name === '') { @@ -315,7 +314,7 @@ const Topic = { xloc: nextCoords ? nextCoords.x : Create.newTopic.x, yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_id: topic.cid, - mappable_type: 'Topic', + mappable_type: 'Topic' }) DataModel.Mappings.add(mapping) @@ -325,7 +324,7 @@ const Topic = { self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database }, - getTopicFromAutocomplete: function (id) { + getTopicFromAutocomplete: function(id) { var self = Topic // hide the 'double-click to add a topic' message @@ -344,7 +343,7 @@ const Topic = { xloc: nextCoords ? nextCoords.x : Create.newTopic.x, yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_type: 'Topic', - mappable_id: topic.id, + mappable_id: topic.id }) DataModel.Mappings.add(mapping) @@ -353,7 +352,7 @@ const Topic = { if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }) }, - getMapFromAutocomplete: function (data) { + getMapFromAutocomplete: function(data) { var self = Topic $(document).trigger(Map.events.editedByActiveMapper) @@ -371,7 +370,7 @@ const Topic = { xloc: Create.newTopic.x, yloc: Create.newTopic.y, mappable_id: topic.cid, - mappable_type: 'Topic', + mappable_type: 'Topic' }) DataModel.Mappings.add(mapping) @@ -383,7 +382,7 @@ const Topic = { // this blocked the enterKeyHandler from creating a new topic as well if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }, - getTopicFromSearch: function (event, id) { + getTopicFromSearch: function(event, id) { var self = Topic $(document).trigger(Map.events.editedByActiveMapper) @@ -394,7 +393,7 @@ const Topic = { xloc: nextCoords.x, yloc: nextCoords.y, mappable_type: 'Topic', - mappable_id: topic.id, + mappable_id: topic.id }) DataModel.Mappings.add(mapping) self.renderTopic(mapping, topic, true, true) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 6e798ee6..71140bdd 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -12,7 +12,7 @@ const TopicCard = { openTopicCard: null, // stores the topic that's currently open authorizedToEdit: false, // stores boolean for edit permission for open topic card RAILS_ENV: undefined, - init: function (serverData) { + init: function(serverData) { var self = TopicCard if (serverData.RAILS_ENV) { @@ -40,7 +40,7 @@ const TopicCard = { * Will open the Topic Card for the node that it's passed * @param {$jit.Graph.Node} node */ - showCard: function (node, opts) { + showCard: function(node, opts) { var self = TopicCard if (!opts) opts = {} var topic = node.getData('topic') @@ -55,16 +55,14 @@ const TopicCard = { } }) }, - hideCard: function () { + hideCard: function() { var self = TopicCard $('.showcard').fadeOut('fast') self.openTopicCard = null self.authorizedToEdit = false }, - embedlyCardRendered: function (iframe) { - var self = TopicCard - + embedlyCardRendered: function(iframe) { $('#embedlyLinkLoader').hide() // means that the embedly call returned 404 not found @@ -81,7 +79,7 @@ const TopicCard = { $('#linkremove').click(TopicCard.removeLink) } }, - removeLink: function () { + removeLink: function() { var self = TopicCard self.openTopicCard.save({ link: null @@ -93,43 +91,43 @@ const TopicCard = { }, showLinkLoader: function() { var loader = new CanvasLoader('embedlyLinkLoader') - loader.setColor('#4fb5c0'); // default is '#000000' + loader.setColor('#4fb5c0') // default is '#000000' loader.setDiameter(28) // default is 40 loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 + loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default }, showLink: function(topic) { var e = embedly('card', document.getElementById('embedlyLink')) - if (!e && TopicCard.RAILS_ENV != 'development') { + if (!e && TopicCard.RAILS_ENV !== 'development') { TopicCard.handleInvalidLink() } else if (!e) { $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() $('#embedlyLinkLoader').hide() } }, - bindShowCardListeners: function (topic) { + bindShowCardListeners: function(topic) { var self = TopicCard var showCard = document.getElementById('showcard') var authorized = self.authorizedToEdit // get mapper image - var setMapperImage = function (mapper) { + var setMapperImage = function(mapper) { $('.contributorIcon').attr('src', mapper.get('image')) } Mapper.get(topic.get('user_id'), setMapperImage) // starting embed.ly - var resetFunc = function () { + var resetFunc = function() { $('#addLinkInput input').val('') $('#addLinkInput input').focus() } - var inputEmbedFunc = function (event) { + var inputEmbedFunc = function(event) { var element = this - setTimeout(function () { + setTimeout(function() { var text = $(element).val() - if (event.type == 'paste' || (event.type == 'keyup' && event.which == 13)) { + if (event.type === 'paste' || (event.type === 'keyup' && event.which === 13)) { // TODO evaluate converting this to '//' no matter what (infer protocol) if (text.slice(0, 7) !== 'http://' && text.slice(0, 8) !== 'https://' && @@ -165,18 +163,18 @@ const TopicCard = { var selectingMetacode = false // attach the listener that shows the metacode title when you hover over the image - $('.showcard .metacodeImage').mouseenter(function () { + $('.showcard .metacodeImage').mouseenter(function() { $('.showcard .icon').css('z-index', '4') $('.showcard .metacodeTitle').show() }) - $('.showcard .linkItem.icon').mouseleave(function () { + $('.showcard .linkItem.icon').mouseleave(function() { if (!selectingMetacode) { $('.showcard .metacodeTitle').hide() $('.showcard .icon').css('z-index', '1') } }) - var metacodeLiClick = function () { + var metacodeLiClick = function() { selectingMetacode = false var metacodeId = parseInt($(this).attr('data-id')) var metacode = DataModel.Metacodes.get(metacodeId) @@ -193,7 +191,7 @@ const TopicCard = { $('.showcard .icon').css('z-index', '1') } - var openMetacodeSelect = function (event) { + var openMetacodeSelect = function(event) { var TOPICCARD_WIDTH = 300 var METACODESELECT_WIDTH = 404 var MAX_METACODELIST_HEIGHT = 270 @@ -219,7 +217,6 @@ const TopicCard = { var windowHeight = $(window).height() var showcardTop = parseInt($('.showcard').css('top')) var topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) - var heightOfSetList = $('.showcard .metacodeSelect').height() var distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { $('.metacodeSelect').addClass('onBottomEdge') @@ -230,7 +227,7 @@ const TopicCard = { } } - var hideMetacodeSelect = function () { + var hideMetacodeSelect = function() { selectingMetacode = false $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') $('.metacodeTitle').hide() @@ -240,13 +237,13 @@ const TopicCard = { if (authorized) { $('.showcard .metacodeTitle').click(openMetacodeSelect) $('.showcard').click(hideMetacodeSelect) - $('.metacodeSelect > ul > li').click(function (event) { + $('.metacodeSelect > ul > li').click(function(event) { event.stopPropagation() }) $('.metacodeSelect li li').click(metacodeLiClick) var bipName = $(showCard).find('.best_in_place_name') - bipName.bind('best_in_place:activate', function () { + bipName.bind('best_in_place:activate', function() { var $el = bipName.find('textarea') var el = $el[0] @@ -254,12 +251,12 @@ const TopicCard = { $('.showcard .title').append('
        ') - var callback = function (data) { + var callback = function(data) { $('.nameCounter.forTopic').html(data.all + '/140') } Countable.live(el, callback) }) - bipName.bind('best_in_place:deactivate', function () { + bipName.bind('best_in_place:deactivate', function() { $('.nameCounter.forTopic').remove() }) bipName.keypress(function(e) { @@ -270,7 +267,7 @@ const TopicCard = { }) // bind best_in_place ajax callbacks - bipName.bind('ajax:success', function () { + bipName.bind('ajax:success', function() { var name = Util.decodeEntities($(this).html()) topic.set('name', name) topic.trigger('saved') @@ -278,7 +275,7 @@ const TopicCard = { // this is for all subsequent renders after in-place editing the desc field const bipDesc = $(showCard).find('.best_in_place_desc') - bipDesc.bind('ajax:success', function () { + bipDesc.bind('ajax:success', function() { var desc = $(this).html() === $(this).data('bip-nil') ? '' : $(this).text() @@ -296,7 +293,7 @@ const TopicCard = { }) } - var permissionLiClick = function (event) { + var permissionLiClick = function(event) { selectingPermission = false var permission = $(this).attr('class') topic.save({ @@ -308,7 +305,7 @@ const TopicCard = { event.stopPropagation() } - var openPermissionSelect = function (event) { + var openPermissionSelect = function(event) { if (!selectingPermission) { selectingPermission = true $(this).addClass('minimize') // this line flips the drop down arrow to a pull up arrow @@ -324,7 +321,7 @@ const TopicCard = { } } - var hidePermissionSelect = function () { + var hidePermissionSelect = function() { selectingPermission = false $('.showcard .yourTopic .mapPerm').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow $('.showcard .permissionSelect').remove() @@ -336,15 +333,15 @@ const TopicCard = { $('.showcard').click(hidePermissionSelect) } - $('.links .mapCount').unbind().click(function (event) { + $('.links .mapCount').unbind().click(function(event) { $('.mapCount .tip').toggle() $('.showcard .hoverTip').toggleClass('hide') event.stopPropagation() }) - $('.mapCount .tip').unbind().click(function (event) { + $('.mapCount .tip').unbind().click(function(event) { event.stopPropagation() }) - $('.showcard').unbind('.hideTip').bind('click.hideTip', function () { + $('.showcard').unbind('.hideTip').bind('click.hideTip', function() { $('.mapCount .tip').hide() $('.showcard .hoverTip').removeClass('hide') }) @@ -353,26 +350,26 @@ const TopicCard = { var originalText = $('.showMore').html() $('.mapCount .tip .showMore').unbind().toggle( - function (event) { + function(event) { $('.extraText').toggleClass('hideExtra') $('.showMore').html('Show less...') }, - function (event) { + function(event) { $('.extraText').toggleClass('hideExtra') $('.showMore').html(originalText) }) - $('.mapCount .tip showMore').unbind().click(function (event) { + $('.mapCount .tip showMore').unbind().click(function(event) { event.stopPropagation() }) }, - handleInvalidLink: function () { + handleInvalidLink: function() { var self = TopicCard self.removeLink() GlobalUI.notifyUser('Invalid link') }, - populateShowCard: function (topic) { + populateShowCard: function(topic) { var self = TopicCard var showCard = document.getElementById('showcard') @@ -383,7 +380,7 @@ const TopicCard = { var html = self.generateShowcardHTML.render(topicForTemplate) if (topic.authorizeToEdit(Active.Mapper)) { - var perm = document.createElement('div') + let perm = document.createElement('div') var string = 'permission canEdit' if (topic.authorizePermissionChange(Active.Mapper)) string += ' yourTopic' @@ -391,7 +388,7 @@ const TopicCard = { perm.innerHTML = html showCard.appendChild(perm) } else { - var perm = document.createElement('div') + let perm = document.createElement('div') perm.className = 'permission cannotEdit' perm.innerHTML = html showCard.appendChild(perm) @@ -401,9 +398,7 @@ const TopicCard = { }, generateShowcardHTML: null, // will be initialized into a Hogan template within init function // generateShowcardHTML - buildObject: function (topic) { - var self = TopicCard - + buildObject: function(topic) { var nodeValues = {} var authorized = topic.authorizeToEdit(Active.Mapper) @@ -438,18 +433,18 @@ const TopicCard = { nodeValues.inmaps = '' if (inmapsAr.length < 6) { for (let i = 0; i < inmapsAr.length; i++) { - var url = '/maps/' + inmapsLinks[i] + const url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '
      • ' + inmapsAr[i] + '
      • ' } } else { for (let i = 0; i < 5; i++) { - var url = '/maps/' + inmapsLinks[i] + const url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '
      • ' + inmapsAr[i] + '
      • ' } - var extra = inmapsAr.length - 5 + const extra = inmapsAr.length - 5 nodeValues.inmaps += '
      • See ' + extra + ' more...
      • ' for (let i = 5; i < inmapsAr.length; i++) { - var url = '/maps/' + inmapsLinks[i] + const url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '
      • ' + inmapsAr[i] + '
      • ' } } diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 482cc03b..1e5c3b66 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -7,22 +7,22 @@ const Util = { // Line Splitter Function // copyright Stephen Chapman, 19th April 2006 // you may copy this code but please keep the copyright notice as well - splitLine: function (st, n) { + splitLine: function(st, n) { var b = '' - var s = st ? st : '' + var s = st || '' while (s.length > n) { var c = s.substring(0, n) var d = c.lastIndexOf(' ') var e = c.lastIndexOf('\n') - if (e != -1) d = e - if (d == -1) d = n + if (e !== -1) d = e + if (d === -1) d = n b += c.substring(0, d) + '\n' s = s.substring(d + 1) } return b + s }, - nowDateFormatted: function (date = new Date(Date.now())) { + nowDateFormatted: function(date = new Date(Date.now())) { const month = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1) const day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate() const year = date.getFullYear() @@ -30,33 +30,32 @@ const Util = { return month + '/' + day + '/' + year }, - decodeEntities: function (desc) { - var str, temp = document.createElement('p') + decodeEntities: function(desc) { + let temp = document.createElement('p') temp.innerHTML = desc // browser handles the topics - str = temp.textContent || temp.innerText + let str = temp.textContent || temp.innerText temp = null // delete the element return str }, // decodeEntities - getDistance: function (p1, p2) { + getDistance: function(p1, p2) { return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)) }, // Try using Visualize.mGraph - coordsToPixels: function (mGraph, coords) { + coordsToPixels: function(mGraph, coords) { if (mGraph) { - var canvas = mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY - var pixels = { + const canvas = mGraph.canvas + const s = canvas.getSize() + const p = canvas.getPos() + const ox = canvas.translateOffsetX + const oy = canvas.translateOffsetY + const sx = canvas.scaleOffsetX + const sy = canvas.scaleOffsetY + return { x: (coords.x / (1 / sx)) + p.x + s.width / 2 + ox, y: (coords.y / (1 / sy)) + p.y + s.height / 2 + oy } - return pixels } else { return { x: 0, @@ -66,29 +65,27 @@ const Util = { }, // Try using Visualize.mGraph - pixelsToCoords: function (mGraph, pixels) { - var coords + pixelsToCoords: function(mGraph, pixels) { if (mGraph) { - var canvas = mGraph.canvas, - s = canvas.getSize(), - p = canvas.getPos(), - ox = canvas.translateOffsetX, - oy = canvas.translateOffsetY, - sx = canvas.scaleOffsetX, - sy = canvas.scaleOffsetY - coords = { + const canvas = mGraph.canvas + const s = canvas.getSize() + const p = canvas.getPos() + const ox = canvas.translateOffsetX + const oy = canvas.translateOffsetY + const sx = canvas.scaleOffsetX + const sy = canvas.scaleOffsetY + return { x: (pixels.x - p.x - s.width / 2 - ox) * (1 / sx), - y: (pixels.y - p.y - s.height / 2 - oy) * (1 / sy), + y: (pixels.y - p.y - s.height / 2 - oy) * (1 / sy) } } else { - coords = { + return { x: 0, y: 0 } } - return coords }, - getPastelColor: function (opts = {}) { + getPastelColor: function(opts = {}) { const rseed = opts.rseed === undefined ? Math.random() : opts.rseed const gseed = opts.gseed === undefined ? Math.random() : opts.gseed const bseed = opts.bseed === undefined ? Math.random() : opts.bseed @@ -98,7 +95,7 @@ const Util = { return Util.colorLuminance('#' + r + g + b, -0.4) }, // darkens a hex value by 'lum' percentage - colorLuminance: function (hex, lum) { + colorLuminance: function(hex, lum) { // validate hex string hex = String(hex).replace(/[^0-9a-f]/gi, '') if (hex.length < 6) { @@ -107,25 +104,25 @@ const Util = { lum = lum || 0 // convert to decimal and change luminosity - var rgb = '#', c, i - for (i = 0; i < 3; i++) { - c = parseInt(hex.substr(i * 2, 2), 16) + var rgb = '#' + for (let i = 0; i < 3; i++) { + let c = parseInt(hex.substr(i * 2, 2), 16) c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16) rgb += ('00' + c).substr(c.length) } return rgb }, - openLink: function(url){ - var win = (url !== "") ? window.open(url, '_blank') : "empty"; - + openLink: function(url) { + var win = (url !== '') ? window.open(url, '_blank') : 'empty' + if (win) { - //Browser has allowed it to be opened - return true; + // Browser has allowed it to be opened + return true } else { - //Browser has blocked it - alert('Please allow popups in order to open the link'); - return false; + // Browser has blocked it + window.alert('Please allow popups in order to open the link') + return false } }, mdToHTML: text => { @@ -134,24 +131,24 @@ const Util = { return new HtmlRenderer({ safe: true }) .render(new Parser().parse(safeText)) }, - logCanvasAttributes: function(canvas){ + logCanvasAttributes: function(canvas) { const fakeMgraph = { canvas } return { scaleX: canvas.scaleOffsetX, scaleY: canvas.scaleOffsetY, - centreCoords: Util.pixelsToCoords(fakeMgraph, { x: canvas.canvases[0].size.width / 2, y: canvas.canvases[0].size.height / 2 }), - }; + centreCoords: Util.pixelsToCoords(fakeMgraph, { x: canvas.canvases[0].size.width / 2, y: canvas.canvases[0].size.height / 2 }) + } }, - resizeCanvas: function(canvas){ + resizeCanvas: function(canvas) { // Store the current canvas attributes, i.e. scale and map-coordinate at the centre of the user's screen - const oldAttr = Util.logCanvasAttributes(canvas); - + const oldAttr = Util.logCanvasAttributes(canvas) + // Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 canvas.resize($(window).width(), $(window).height()) - + // Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen canvas.scale(oldAttr.scaleX, oldAttr.scaleY) - const newAttr = Util.logCanvasAttributes(canvas); + const newAttr = Util.logCanvasAttributes(canvas) canvas.translate(newAttr.centreCoords.x - oldAttr.centreCoords.x, newAttr.centreCoords.y - oldAttr.centreCoords.y) } } diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 7175949f..4a549648 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -40,7 +40,7 @@ var Private = {
    `, - templates: function () { + templates: function() { underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g } @@ -48,7 +48,7 @@ var Private = { this.participantTemplate = underscore.template(Private.participantHTML) }, - createElements: function () { + createElements: function() { this.$unread = $('
    ') this.$button = $('
    Chat
    ') this.$messageInput = $('') @@ -71,7 +71,7 @@ var Private = { this.$messages = $('
    ') this.$container = $('
    ') }, - attachElements: function () { + attachElements: function() { this.$button.append(this.$unread) this.$juntoHeader.append(this.$videoToggle) @@ -88,40 +88,40 @@ var Private = { this.$container.append(this.$messages) this.$container.append(this.$messageInput) }, - addEventListeners: function () { + addEventListeners: function() { var self = this - this.participants.on('add', function (participant) { + this.participants.on('add', function(participant) { Private.addParticipant.call(self, participant) }) - this.participants.on('remove', function (participant) { + this.participants.on('remove', function(participant) { Private.removeParticipant.call(self, participant) }) - this.$button.on('click', function () { + this.$button.on('click', function() { Handlers.buttonClick.call(self) }) - this.$videoToggle.on('click', function () { + this.$videoToggle.on('click', function() { Handlers.videoToggleClick.call(self) }) - this.$cursorToggle.on('click', function () { + this.$cursorToggle.on('click', function() { Handlers.cursorToggleClick.call(self) }) - this.$soundToggle.on('click', function () { + this.$soundToggle.on('click', function() { Handlers.soundToggleClick.call(self) }) - this.$messageInput.on('keyup', function (event) { + this.$messageInput.on('keyup', function(event) { Handlers.keyUp.call(self, event) }) - this.$messageInput.on('focus', function () { + this.$messageInput.on('focus', function() { Handlers.inputFocus.call(self) }) - this.$messageInput.on('blur', function () { + this.$messageInput.on('blur', function() { Handlers.inputBlur.call(self) }) }, - initializeSounds: function (soundUrls) { + initializeSounds: function(soundUrls) { this.sound = new Howl({ src: soundUrls, sprite: { @@ -133,15 +133,15 @@ var Private = { } }) }, - incrementUnread: function () { + incrementUnread: function() { this.unreadMessages++ this.$unread.html(this.unreadMessages) this.$unread.show() }, - addMessage: function (message, isInitial, wasMe) { + addMessage: function(message, isInitial, wasMe) { if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) - function addZero (i) { + function addZero(i) { if (i < 10) { i = '0' + i } @@ -162,20 +162,20 @@ var Private = { if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat') }, - initialMessages: function () { + initialMessages: function() { var messages = this.messages.models for (var i = 0; i < messages.length; i++) { Private.addMessage.call(this, messages[i], true) } }, - handleInputMessage: function () { + handleInputMessage: function() { var message = { message: this.$messageInput.val() } this.$messageInput.val('') $(document).trigger(ChatView.events.message + '-' + this.room, [message]) }, - addParticipant: function (participant) { + addParticipant: function(participant) { var p = _.clone(participant.attributes) if (p.self) { p.selfClass = 'is-self' @@ -187,46 +187,46 @@ var Private = { var html = this.participantTemplate(p) this.$participants.append(html) }, - removeParticipant: function (participant) { + removeParticipant: function(participant) { this.$container.find('.participant-' + participant.get('id')).remove() } } var Handlers = { - buttonClick: function () { + buttonClick: function() { if (this.isOpen) this.close() else if (!this.isOpen) this.open() }, - videoToggleClick: function () { + videoToggleClick: function() { this.$videoToggle.toggleClass('active') this.videosShowing = !this.videosShowing $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) }, - cursorToggleClick: function () { + cursorToggleClick: function() { this.$cursorToggle.toggleClass('active') this.cursorsShowing = !this.cursorsShowing $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) }, - soundToggleClick: function () { + soundToggleClick: function() { this.alertSound = !this.alertSound this.$soundToggle.toggleClass('active') }, - keyUp: function (event) { + keyUp: function(event) { switch (event.which) { case 13: // enter Private.handleInputMessage.call(this) break } }, - inputFocus: function () { + inputFocus: function() { $(document).trigger(ChatView.events.inputFocus) }, - inputBlur: function () { + inputBlur: function() { $(document).trigger(ChatView.events.inputBlur) } } -const ChatView = function (messages, mapper, room) { +const ChatView = function(messages, mapper, room) { this.room = room this.mapper = mapper this.messages = messages // backbone collection @@ -249,7 +249,7 @@ const ChatView = function (messages, mapper, room) { }) } -ChatView.prototype.conversationInProgress = function (participating) { +ChatView.prototype.conversationInProgress = function(participating) { this.$conversationInProgress.show() this.$participants.addClass('is-live') if (participating) this.$participants.addClass('is-participating') @@ -258,7 +258,7 @@ ChatView.prototype.conversationInProgress = function (participating) { // hide invite to call buttons } -ChatView.prototype.conversationEnded = function () { +ChatView.prototype.conversationEnded = function() { this.$conversationInProgress.hide() this.$participants.removeClass('is-live') this.$participants.removeClass('is-participating') @@ -267,42 +267,42 @@ ChatView.prototype.conversationEnded = function () { this.$participants.find('.participant').removeClass('pending') } -ChatView.prototype.leaveConversation = function () { +ChatView.prototype.leaveConversation = function() { this.$participants.removeClass('is-participating') } -ChatView.prototype.mapperJoinedCall = function (id) { +ChatView.prototype.mapperJoinedCall = function(id) { this.$participants.find('.participant-' + id).addClass('active') } -ChatView.prototype.mapperLeftCall = function (id) { +ChatView.prototype.mapperLeftCall = function(id) { this.$participants.find('.participant-' + id).removeClass('active') } -ChatView.prototype.invitationPending = function (id) { +ChatView.prototype.invitationPending = function(id) { this.$participants.find('.participant-' + id).addClass('pending') } -ChatView.prototype.invitationAnswered = function (id) { +ChatView.prototype.invitationAnswered = function(id) { this.$participants.find('.participant-' + id).removeClass('pending') } -ChatView.prototype.addParticipant = function (participant) { +ChatView.prototype.addParticipant = function(participant) { this.participants.add(participant) } -ChatView.prototype.removeParticipant = function (username) { +ChatView.prototype.removeParticipant = function(username) { var p = this.participants.find(p => p.get('username') === username) if (p) { this.participants.remove(p) } } -ChatView.prototype.removeParticipants = function () { +ChatView.prototype.removeParticipants = function() { this.participants.remove(this.participants.models) } -ChatView.prototype.open = function () { +ChatView.prototype.open = function() { this.$container.css({ right: '0' }) @@ -314,12 +314,12 @@ ChatView.prototype.open = function () { $(document).trigger(ChatView.events.openTray) } -ChatView.prototype.addMessage = function (message, isInitial, wasMe) { +ChatView.prototype.addMessage = function(message, isInitial, wasMe) { this.messages.add(message) Private.addMessage.call(this, message, isInitial, wasMe) } -ChatView.prototype.scrollMessages = function (duration) { +ChatView.prototype.scrollMessages = function(duration) { duration = duration || 0 this.$messages.animate({ @@ -327,13 +327,13 @@ ChatView.prototype.scrollMessages = function (duration) { }, duration) } -ChatView.prototype.clearMessages = function () { +ChatView.prototype.clearMessages = function() { this.unreadMessages = 0 this.$unread.hide() this.$messages.empty() } -ChatView.prototype.close = function () { +ChatView.prototype.close = function() { this.$container.css({ right: '-300px' }) @@ -342,7 +342,7 @@ ChatView.prototype.close = function () { $(document).trigger(ChatView.events.closeTray) } -ChatView.prototype.remove = function () { +ChatView.prototype.remove = function() { this.$button.off() this.$container.remove() } diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 3f69fe8c..0b7d841a 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -4,6 +4,7 @@ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' +import DataModel from '../DataModel' import GlobalUI from '../GlobalUI' import Realtime from '../Realtime' import Loading from '../Loading' @@ -12,7 +13,7 @@ import Maps from '../../components/Maps' const ExploreMaps = { pending: false, mapper: null, - setCollection: function (collection) { + setCollection: function(collection) { var self = ExploreMaps if (self.collection) { @@ -25,7 +26,7 @@ const ExploreMaps = { self.collection.on('successOnFetch', self.handleSuccess) self.collection.on('errorOnFetch', self.handleError) }, - render: function (cb) { + render: function(cb) { var self = ExploreMaps if (!self.collection) return @@ -35,19 +36,19 @@ const ExploreMaps = { section: self.collection.id, maps: self.collection, juntoState: Realtime.juntoState, - moreToLoad: self.collection.page != 'loadedAll', + moreToLoad: self.collection.page !== 'loadedAll', user: self.collection.id === 'mapper' ? self.mapper : null, loadMore: self.loadMore, pending: self.pending, - onStar: function (map) { + onStar: function(map) { $.post('/maps/' + map.id + '/star') map.set('star_count', map.get('star_count') + 1) - if (Metamaps.Stars) Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: map.id }) - Metamaps.Maps.Starred.add(map) + if (DataModel.Stars) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: map.id }) + DataModel.Maps.Starred.add(map) GlobalUI.notifyUser('Map is now starred') self.render() }, - onRequest: function (map) { + onRequest: function(map) { $.post({ url: `/maps/${map.id}/access_request` }) @@ -62,47 +63,47 @@ const ExploreMaps = { if (cb) cb() Loading.hide() }, - loadMore: function () { + loadMore: function() { var self = ExploreMaps - if (self.collection.page != "loadedAll") { + if (self.collection.page !== 'loadedAll') { self.collection.getMaps() self.pending = true } self.render() }, - handleSuccess: function (cb) { + handleSuccess: function(cb) { var self = ExploreMaps self.pending = false if (self.collection && self.collection.id === 'mapper') { self.fetchUserThenRender(cb) } else { self.render(cb) - Metamaps.Loading.hide() + Loading.hide() } }, - handleError: function () { + handleError: function() { console.log('error loading maps!') // TODO - Metamaps.Loading.hide() + Loading.hide() }, - fetchUserThenRender: function (cb) { + fetchUserThenRender: function(cb) { var self = ExploreMaps if (self.mapper && self.mapper.id === self.collection.mapperId) { self.render(cb) - return Metamaps.Loading.hide() + return Loading.hide() } // first load the mapper object and then call the render function $.ajax({ url: '/users/' + self.collection.mapperId + '/details.json', - success: function (response) { + success: function(response) { self.mapper = response self.render(cb) - Metamaps.Loading.hide() + Loading.hide() }, - error: function () { + error: function() { self.render(cb) - Metamaps.Loading.hide() + Loading.hide() } }) } diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index f0821a10..f3327c6d 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -14,8 +14,6 @@ import ChatView from './ChatView' import VideoView from './VideoView' const Room = function(opts = {}) { - var self = this - this.isActiveRoom = false this.socket = opts.socket this.webrtc = opts.webrtc @@ -77,128 +75,124 @@ Room.prototype.setPeopleCount = function(count) { this.peopleCount = count } -Room.prototype.init = function () { - var self = this +Room.prototype.init = function() { + var self = this - $(document).on(VideoView.events.audioControlClick, function (event, videoView) { - if (!videoView.audioStatus) self.webrtc.mute() - else if (videoView.audioStatus) self.webrtc.unmute() - }) - $(document).on(VideoView.events.videoControlClick, function (event, videoView) { - if (!videoView.videoStatus) self.webrtc.pauseVideo() - else if (videoView.videoStatus) self.webrtc.resumeVideo() - }) + $(document).on(VideoView.events.audioControlClick, function(event, videoView) { + if (!videoView.audioStatus) self.webrtc.mute() + else if (videoView.audioStatus) self.webrtc.unmute() + }) + $(document).on(VideoView.events.videoControlClick, function(event, videoView) { + if (!videoView.videoStatus) self.webrtc.pauseVideo() + else if (videoView.videoStatus) self.webrtc.resumeVideo() + }) - this.webrtc.webrtc.off('peerStreamAdded') - this.webrtc.webrtc.off('peerStreamRemoved') - this.webrtc.on('peerStreamAdded', function (peer) { - var mapper = Realtime.mappersOnMap[peer.nick] - peer.avatar = mapper.image - peer.username = mapper.name - if (self.isActiveRoom) { - self.addVideo(peer) - } - }) - - this.webrtc.on('peerStreamRemoved', function (peer) { - if (self.isActiveRoom) { - self.removeVideo(peer) - } - }) - - this.webrtc.on('mute', function (data) { - var v = self.videos[data.id] - if (!v) return - - if (data.name === 'audio') { - v.audioStatus = false - } - else if (data.name === 'video') { - v.videoStatus = false - v.$avatar.show() - } - if (!v.audioStatus && !v.videoStatus) v.$container.hide() - }) - this.webrtc.on('unmute', function (data) { - var v = self.videos[data.id] - if (!v) return - - if (data.name === 'audio') { - v.audioStatus = true - } - else if (data.name === 'video') { - v.videoStatus = true - v.$avatar.hide() - } - v.$container.show() - }) - - var sendChatMessage = function (event, data) { - self.sendChatMessage(data) + this.webrtc.webrtc.off('peerStreamAdded') + this.webrtc.webrtc.off('peerStreamRemoved') + this.webrtc.on('peerStreamAdded', function(peer) { + var mapper = Realtime.mappersOnMap[peer.nick] + peer.avatar = mapper.image + peer.username = mapper.name + if (self.isActiveRoom) { + self.addVideo(peer) } - $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage) + }) + + this.webrtc.on('peerStreamRemoved', function(peer) { + if (self.isActiveRoom) { + self.removeVideo(peer) + } + }) + + this.webrtc.on('mute', function(data) { + var v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = false + } else if (data.name === 'video') { + v.videoStatus = false + v.$avatar.show() + } + if (!v.audioStatus && !v.videoStatus) v.$container.hide() + }) + this.webrtc.on('unmute', function(data) { + const v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = true + } else if (data.name === 'video') { + v.videoStatus = true + v.$avatar.hide() + } + v.$container.show() + }) + + var sendChatMessage = function(event, data) { + self.sendChatMessage(data) } + $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage) +} - Room.prototype.videoAdded = function (callback) { - this._videoAdded = callback +Room.prototype.videoAdded = function(callback) { + this._videoAdded = callback +} + +Room.prototype.addVideo = function(peer) { + const id = this.webrtc.getDomId(peer) + const video = attachMediaStream(peer.stream) + + const v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }) + + this.videos[peer.id] = v + if (this._videoAdded) this._videoAdded(v, peer.nick) +} + +Room.prototype.removeVideo = function(peer) { + var id = typeof peer === 'string' ? peer : peer.id + if (this.videos[id]) { + this.videos[id].remove() + delete this.videos[id] } +} - Room.prototype.addVideo = function (peer) { - var - id = this.webrtc.getDomId(peer), - video = attachMediaStream(peer.stream) - - var - v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }) - - this.videos[peer.id] = v - if (this._videoAdded) this._videoAdded(v, peer.nick) - } - - Room.prototype.removeVideo = function (peer) { - var id = typeof peer == 'string' ? peer : peer.id - if (this.videos[id]) { - this.videos[id].remove() - delete this.videos[id] - } - } - - Room.prototype.sendChatMessage = function (data) { - var self = this - //this.roomRef.child('messages').push(data) - if (self.chat.alertSound) self.chat.sound.play('sendchat') - var m = new DataModel.Message({ - message: data.message, - resource_id: Active.Map.id, - resource_type: "Map" - }) - m.save(null, { - success: function (model, response) { - self.addMessages(new DataModel.MessageCollection(model), false, true) - $(document).trigger(Room.events.newMessage, [model]) - }, - error: function (model, response) { - console.log('error!', response) - } - }) - } +Room.prototype.sendChatMessage = function(data) { + var self = this + // this.roomRef.child('messages').push(data) + if (self.chat.alertSound) self.chat.sound.play('sendchat') + var m = new DataModel.Message({ + message: data.message, + resource_id: Active.Map.id, + resource_type: 'Map' + }) + m.save(null, { + success: function(model, response) { + self.addMessages(new DataModel.MessageCollection(model), false, true) + $(document).trigger(Room.events.newMessage, [model]) + }, + error: function(model, response) { + console.log('error!', response) + } + }) +} // they should be instantiated as backbone models before they get // passed to this function - Room.prototype.addMessages = function (messages, isInitial, wasMe) { - var self = this +Room.prototype.addMessages = function(messages, isInitial, wasMe) { + var self = this - messages.models.forEach(function (message) { - self.chat.addMessage(message, isInitial, wasMe) - }) - } + messages.models.forEach(function(message) { + self.chat.addMessage(message, isInitial, wasMe) + }) +} /** * @class * @static */ Room.events = { - newMessage: "Room:newMessage" + newMessage: 'Room:newMessage' } export default Room diff --git a/frontend/src/Metamaps/Views/VideoView.js b/frontend/src/Metamaps/Views/VideoView.js index 401ece54..66a25ca7 100644 --- a/frontend/src/Metamaps/Views/VideoView.js +++ b/frontend/src/Metamaps/Views/VideoView.js @@ -1,189 +1,188 @@ /* global $ */ var Private = { - addControls: function() { - var self = this; + addControls: function() { + var self = this - this.$audioControl = $('
    '); - this.$videoControl = $('
    '); + this.$audioControl = $('
    ') + this.$videoControl = $('
    ') - this.$audioControl.on('click', function () { - Handlers.audioControlClick.call(self); - }); + this.$audioControl.on('click', function() { + Handlers.audioControlClick.call(self) + }) - this.$videoControl.on('click', function () { - Handlers.videoControlClick.call(self); - }); + this.$videoControl.on('click', function() { + Handlers.videoControlClick.call(self) + }) - this.$container.append(this.$audioControl); - this.$container.append(this.$videoControl); - }, - cancelClick: function() { - this.mouseIsDown = false; + this.$container.append(this.$audioControl) + this.$container.append(this.$videoControl) + }, + cancelClick: function() { + this.mouseIsDown = false - if (this.hasMoved) { + if (this.hasMoved) { - } - - $(document).trigger(VideoView.events.dragEnd); } -}; + + $(document).trigger(VideoView.events.dragEnd) + } +} var Handlers = { - mousedown: function(event) { - this.mouseIsDown = true; - this.hasMoved = false; - this.mouseMoveStart = { - x: event.pageX, - y: event.pageY - }; - this.posStart = { - x: parseInt(this.$container.css('left'), '10'), - y: parseInt(this.$container.css('top'), '10') - } - - $(document).trigger(VideoView.events.mousedown); - }, - mouseup: function(event) { - $(document).trigger(VideoView.events.mouseup, [this]); - - var storedTime = this.lastClick; - var now = Date.now(); - this.lastClick = now; - - if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { - $(document).trigger(VideoView.events.doubleClick, [this]); - } - }, - mousemove: function(event) { - var - diffX, - diffY, - newX, - newY; - - if (this.$parent && this.mouseIsDown) { - this.manuallyPositioned = true; - this.hasMoved = true; - diffX = event.pageX - this.mouseMoveStart.x; - diffY = this.mouseMoveStart.y - event.pageY; - newX = this.posStart.x + diffX; - newY = this.posStart.y - diffY; - this.$container.css({ - top: newY, - left: newX - }); - } - }, - audioControlClick: function() { - if (this.audioStatus) { - this.audioOff(); - } else { - this.audioOn(); - } - $(document).trigger(VideoView.events.audioControlClick, [this]); - }, - videoControlClick: function() { - if (this.videoStatus) { - this.videoOff(); - } else { - this.videoOn(); - } - $(document).trigger(VideoView.events.videoControlClick, [this]); - }, -}; - -var VideoView = function(video, $parent, id, isMyself, config) { - var self = this; - - this.$parent = $parent; // mapView - - this.video = video; - this.id = id; - - this.config = config; - - this.mouseIsDown = false; - this.mouseDownOffset = { x: 0, y: 0 }; - this.lastClick = null; - this.hasMoved = false; - - this.audioStatus = true; - this.videoStatus = true; - - this.$container = $('
    '); - this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')); - this.$container.attr('id', 'container_' + id); - - - var $vidContainer = $('
    '); - $vidContainer.addClass('video-cutoff'); - $vidContainer.append(this.video); - - this.avatar = config.avatar; - this.$avatar = $(''); - $vidContainer.append(this.$avatar); - - this.$container.append($vidContainer); - - this.$container.on('mousedown', function (event) { - Handlers.mousedown.call(self, event); - }); - - if (isMyself) { - Private.addControls.call(this); + mousedown: function(event) { + this.mouseIsDown = true + this.hasMoved = false + this.mouseMoveStart = { + x: event.pageX, + y: event.pageY + } + this.posStart = { + x: parseInt(this.$container.css('left'), '10'), + y: parseInt(this.$container.css('top'), '10') } - // suppress contextmenu - this.video.oncontextmenu = function () { return false; }; + $(document).trigger(VideoView.events.mousedown) + }, + mouseup: function(event) { + $(document).trigger(VideoView.events.mouseup, [this]) - if (this.$parent) this.setParent(this.$parent); -}; + var storedTime = this.lastClick + var now = Date.now() + this.lastClick = now + + if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { + $(document).trigger(VideoView.events.doubleClick, [this]) + } + }, + mousemove: function(event) { + var + diffX, + diffY, + newX, + newY + + if (this.$parent && this.mouseIsDown) { + this.manuallyPositioned = true + this.hasMoved = true + diffX = event.pageX - this.mouseMoveStart.x + diffY = this.mouseMoveStart.y - event.pageY + newX = this.posStart.x + diffX + newY = this.posStart.y - diffY + this.$container.css({ + top: newY, + left: newX + }) + } + }, + audioControlClick: function() { + if (this.audioStatus) { + this.audioOff() + } else { + this.audioOn() + } + $(document).trigger(VideoView.events.audioControlClick, [this]) + }, + videoControlClick: function() { + if (this.videoStatus) { + this.videoOff() + } else { + this.videoOn() + } + $(document).trigger(VideoView.events.videoControlClick, [this]) + } +} + +var VideoView = function(video, $parent, id, isMyself, config) { + var self = this + + this.$parent = $parent // mapView + + this.video = video + this.id = id + + this.config = config + + this.mouseIsDown = false + this.mouseDownOffset = { x: 0, y: 0 } + this.lastClick = null + this.hasMoved = false + + this.audioStatus = true + this.videoStatus = true + + this.$container = $('
    ') + this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')) + this.$container.attr('id', 'container_' + id) + + var $vidContainer = $('
    ') + $vidContainer.addClass('video-cutoff') + $vidContainer.append(this.video) + + this.avatar = config.avatar + this.$avatar = $('') + $vidContainer.append(this.$avatar) + + this.$container.append($vidContainer) + + this.$container.on('mousedown', function(event) { + Handlers.mousedown.call(self, event) + }) + + if (isMyself) { + Private.addControls.call(this) + } + + // suppress contextmenu + this.video.oncontextmenu = function() { return false } + + if (this.$parent) this.setParent(this.$parent) +} VideoView.prototype.setParent = function($parent) { - var self = this; - this.$parent = $parent; - this.$parent.off('.video' + this.id); - this.$parent.on('mouseup.video' + this.id, function (event) { - Handlers.mouseup.call(self, event); - Private.cancelClick.call(self); - }); - this.$parent.on('mousemove.video' + this.id, function (event) { - Handlers.mousemove.call(self, event); - }); + var self = this + this.$parent = $parent + this.$parent.off('.video' + this.id) + this.$parent.on('mouseup.video' + this.id, function(event) { + Handlers.mouseup.call(self, event) + Private.cancelClick.call(self) + }) + this.$parent.on('mousemove.video' + this.id, function(event) { + Handlers.mousemove.call(self, event) + }) } -VideoView.prototype.setAvatar = function (src) { - this.$avatar.attr('src', src); - this.avatar = src; +VideoView.prototype.setAvatar = function(src) { + this.$avatar.attr('src', src) + this.avatar = src } -VideoView.prototype.remove = function () { - this.$container.off(); - if (this.$parent) this.$parent.off('.video' + this.id); - this.$container.remove(); +VideoView.prototype.remove = function() { + this.$container.off() + if (this.$parent) this.$parent.off('.video' + this.id) + this.$container.remove() } -VideoView.prototype.videoOff = function () { - this.$videoControl.addClass('active'); - this.$avatar.show(); - this.videoStatus = false; +VideoView.prototype.videoOff = function() { + this.$videoControl.addClass('active') + this.$avatar.show() + this.videoStatus = false } -VideoView.prototype.videoOn = function () { - this.$videoControl.removeClass('active'); - this.$avatar.hide(); - this.videoStatus = true; +VideoView.prototype.videoOn = function() { + this.$videoControl.removeClass('active') + this.$avatar.hide() + this.videoStatus = true } -VideoView.prototype.audioOff = function () { - this.$audioControl.addClass('active'); - this.audioStatus = false; +VideoView.prototype.audioOff = function() { + this.$audioControl.addClass('active') + this.audioStatus = false } -VideoView.prototype.audioOn = function () { - this.$audioControl.removeClass('active'); - this.audioStatus = true; +VideoView.prototype.audioOn = function() { + this.$audioControl.removeClass('active') + this.audioStatus = true } /** @@ -191,12 +190,12 @@ VideoView.prototype.audioOn = function () { * @static */ VideoView.events = { - mousedown: "VideoView:mousedown", - mouseup: "VideoView:mouseup", - doubleClick: "VideoView:doubleClick", - dragEnd: "VideoView:dragEnd", - audioControlClick: "VideoView:audioControlClick", - videoControlClick: "VideoView:videoControlClick", -}; + mousedown: 'VideoView:mousedown', + mouseup: 'VideoView:mouseup', + doubleClick: 'VideoView:doubleClick', + dragEnd: 'VideoView:dragEnd', + audioControlClick: 'VideoView:audioControlClick', + videoControlClick: 'VideoView:videoControlClick' +} export default VideoView diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index 17b592d6..89d22ad7 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -6,9 +6,9 @@ import VideoView from './VideoView' import Room from './Room' import { JUNTO_UPDATED } from '../Realtime/events' -const Views = { +const Views = { init: () => { - $(document).on(JUNTO_UPDATED, () => ExploreMaps.render()) + $(document).on(JUNTO_UPDATED, () => ExploreMaps.render()) }, ExploreMaps, ChatView, diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 3760a33a..577c5418 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -17,54 +17,53 @@ const Visualize = { type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created touchDragNode: null, - init: function (serverData) { + init: function(serverData) { var self = Visualize if (serverData.VisualizeType) self.type = serverData.VisualizeType // disable awkward dragging of the canvas element that would sometimes happen - $('#infovis-canvas').on('dragstart', function (event) { + $('#infovis-canvas').on('dragstart', function(event) { event.preventDefault() }) // prevent touch events on the canvas from default behaviour - $('#infovis-canvas').bind('touchstart', function (event) { + $('#infovis-canvas').bind('touchstart', function(event) { event.preventDefault() self.mGraph.events.touched = true }) // prevent touch events on the canvas from default behaviour - $('#infovis-canvas').bind('touchmove', function (event) { + $('#infovis-canvas').bind('touchmove', function(event) { // JIT.touchPanZoomHandler(event) }) // prevent touch events on the canvas from default behaviour - $('#infovis-canvas').bind('touchend touchcancel', function (event) { - lastDist = 0 + $('#infovis-canvas').bind('touchend touchcancel', function(event) { if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) TopicCard.hideCurrentCard() self.mGraph.events.touched = self.mGraph.events.touchMoved = false Visualize.touchDragNode = false }) }, - computePositions: function () { - var self = Visualize, - mapping + computePositions: function() { + const self = Visualize - if (self.type == 'RGraph') { - var i, l, startPos, endPos, topic, synapse + if (self.type === 'RGraph') { + let i + let l - self.mGraph.graph.eachNode(function (n) { - topic = DataModel.Topics.get(n.id) + self.mGraph.graph.eachNode(function(n) { + const topic = DataModel.Topics.get(n.id) topic.set({ node: n }, { silent: true }) topic.updateNode() - n.eachAdjacency(function (edge) { + n.eachAdjacency(function(edge) { if (!edge.getData('init')) { edge.setData('init', true) l = edge.getData('synapseIDs').length for (i = 0; i < l; i++) { - synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) + const synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) synapse.set({ edge: edge }, { silent: true }) synapse.updateEdge() } @@ -75,34 +74,32 @@ const Visualize = { pos.setc(-200, -200) }) self.mGraph.compute('end') - } else if (self.type == 'ForceDirected') { - var i, l, startPos, endPos, topic, synapse - - self.mGraph.graph.eachNode(function (n) { - topic = DataModel.Topics.get(n.id) + } else if (self.type === 'ForceDirected') { + self.mGraph.graph.eachNode(function(n) { + const topic = DataModel.Topics.get(n.id) topic.set({ node: n }, { silent: true }) topic.updateNode() - mapping = topic.getMapping() + const mapping = topic.getMapping() - n.eachAdjacency(function (edge) { + n.eachAdjacency(function(edge) { if (!edge.getData('init')) { edge.setData('init', true) - l = edge.getData('synapseIDs').length - for (i = 0; i < l; i++) { - synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) + const l = edge.getData('synapseIDs').length + for (let i = 0; i < l; i++) { + const synapse = DataModel.Synapses.get(edge.getData('synapseIDs')[i]) synapse.set({ edge: edge }, { silent: true }) synapse.updateEdge() } } }) - startPos = new $jit.Complex(0, 0) - endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) + const startPos = new $jit.Complex(0, 0) + const endPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) n.setPos(startPos, 'start') n.setPos(endPos, 'end') }) - } else if (self.type == 'ForceDirected3D') { + } else if (self.type === 'ForceDirected3D') { self.mGraph.compute() } }, @@ -110,14 +107,14 @@ const Visualize = { * render does the heavy lifting of creating the engine that renders the graph with the properties we desire * */ - render: function () { - var self = Visualize, RGraphSettings, FDSettings + render: function() { + const self = Visualize - if (self.type == 'RGraph') { + if (self.type === 'RGraph') { // clear the previous canvas from #infovis $('#infovis').empty() - - RGraphSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) + + const RGraphSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) $jit.RGraph.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) $jit.RGraph.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) @@ -128,11 +125,11 @@ const Visualize = { RGraphSettings.levelDistance = JIT.RGraph.levelDistance self.mGraph = new $jit.RGraph(RGraphSettings) - } else if (self.type == 'ForceDirected') { + } else if (self.type === 'ForceDirected') { // clear the previous canvas from #infovis $('#infovis').empty() - - FDSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) + + const FDSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) $jit.ForceDirected.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) $jit.ForceDirected.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) @@ -141,10 +138,10 @@ const Visualize = { FDSettings.height = $('body').height() self.mGraph = new $jit.ForceDirected(FDSettings) - } else if (self.type == 'ForceDirected3D' && !self.mGraph) { + } else if (self.type === 'ForceDirected3D' && !self.mGraph) { // clear the previous canvas from #infovis $('#infovis').empty() - + // init ForceDirected3D self.mGraph = new $jit.ForceDirected3D(JIT.ForceDirected3D.graphSettings) self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position @@ -152,17 +149,16 @@ const Visualize = { self.mGraph.graph.empty() } + if (self.type === 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence') - if (self.type == 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence') - - function runAnimation () { + function runAnimation() { Loading.hide() // load JSON data, if it's not empty if (!self.loadLater) { // load JSON data. var rootIndex = 0 if (Active.Topic) { - var node = _.find(JIT.vizData, function (node) { + var node = _.find(JIT.vizData, function(node) { return node.id === Active.Topic.id }) rootIndex = _.indexOf(JIT.vizData, node) @@ -171,11 +167,11 @@ const Visualize = { // compute positions and plot. self.computePositions() self.mGraph.busy = true - if (self.type == 'RGraph') { + if (self.type === 'RGraph') { self.mGraph.fx.animate(JIT.RGraph.animate) - } else if (self.type == 'ForceDirected') { + } else if (self.type === 'ForceDirected') { self.mGraph.animate(JIT.ForceDirected.animateSavedLayout) - } else if (self.type == 'ForceDirected3D') { + } else if (self.type === 'ForceDirected3D') { self.mGraph.animate(JIT.ForceDirected.animateFDLayout) } } @@ -183,35 +179,37 @@ const Visualize = { // hold until all the needed metacode images are loaded // hold for a maximum of 80 passes, or 4 seconds of waiting time var tries = 0 - function hold () { - var unique = _.uniq(DataModel.Topics.models, function (metacode) { return metacode.get('metacode_id'); }), - requiredMetacodes = _.map(unique, function (metacode) { return metacode.get('metacode_id'); }), - loadedCount = 0 + function hold() { + const unique = _.uniq(DataModel.Topics.models, function(metacode) { return metacode.get('metacode_id') }) + const requiredMetacodes = _.map(unique, function(metacode) { return metacode.get('metacode_id') }) + let loadedCount = 0 - _.each(requiredMetacodes, function (metacode_id) { - var metacode = DataModel.Metacodes.get(metacode_id), - img = metacode ? metacode.get('image') : false + _.each(requiredMetacodes, function(metacodeId) { + const metacode = DataModel.Metacodes.get(metacodeId) + const img = metacode ? metacode.get('image') : false if (img && (img.complete || (typeof img.naturalWidth !== 'undefined' && img.naturalWidth !== 0))) { loadedCount += 1 } }) - if (loadedCount === requiredMetacodes.length || tries > 80) runAnimation() - else setTimeout(function () { tries++; hold() }, 50) + if (loadedCount === requiredMetacodes.length || tries > 80) { + runAnimation() + } else { + setTimeout(function() { tries++; hold() }, 50) + } } hold() // update the url now that the map is ready clearTimeout(Router.timeoutId) - Router.timeoutId = setTimeout(function () { + Router.timeoutId = setTimeout(function() { var m = Active.Map var t = Active.Topic if (m && window.location.pathname !== '/maps/' + m.id) { Router.navigate('/maps/' + m.id) - } - else if (t && window.location.pathname !== '/topics/' + t.id) { + } else if (t && window.location.pathname !== '/topics/' + t.id) { Router.navigate('/topics/' + t.id) } }, 800) @@ -221,7 +219,7 @@ const Visualize = { Visualize.mGraph.plot() JIT.centerMap(Visualize.mGraph.canvas) $('#infovis').empty() - }, + } } export default Visualize diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 696d1539..dfad4d95 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -70,13 +70,9 @@ Metamaps.Topic = Topic Metamaps.TopicCard = TopicCard Metamaps.Util = Util Metamaps.Views = Views -Metamaps.Views.ExploreMaps = ExploreMaps -Metamaps.Views.ChatView = ChatView -Metamaps.Views.VideoView = VideoView -Metamaps.Views.Room = Room Metamaps.Visualize = Visualize -document.addEventListener('DOMContentLoaded', function () { +document.addEventListener('DOMContentLoaded', function() { // initialize all the modules for (const prop in Metamaps) { // this runs the init function within each sub-object on the Metamaps one @@ -94,14 +90,14 @@ document.addEventListener('DOMContentLoaded', function () { Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) if (Metamaps.currentPage === 'mapper') { - ExploreMaps.fetchUserThenRender() + Views.ExploreMaps.fetchUserThenRender() } else { - ExploreMaps.render() + Views.ExploreMaps.render() } GlobalUI.showDiv('#explore') } else if (Metamaps.currentSection === '' && Active.Mapper) { - ExploreMaps.setCollection(DataModel.Maps.Active) - ExploreMaps.render() + Views.ExploreMaps.setCollection(DataModel.Maps.Active) + Views.ExploreMaps.render() GlobalUI.showDiv('#explore') } else if (Active.Map || Active.Topic) { Loading.show() diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index 43825421..9cc8b136 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -2,7 +2,7 @@ import React, { PropTypes, Component } from 'react' import Dropzone from 'react-dropzone' class ImportDialogBox extends Component { - constructor (props) { + constructor(props) { super(props) this.state = { diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index 031ec629..61a6f5e1 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -87,10 +87,6 @@ class MapCard extends Component { const hasMapper = hasMap && !hasConversation const mapperList = hasMap && Object.keys(hasMap).map(id => juntoState.connectedPeople[id]) - function capitalize (string) { - return string.charAt(0).toUpperCase() + string.slice(1) - } - const n = map.get('name') const d = map.get('desc') diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 4dc1ab23..0478c34a 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -31,22 +31,22 @@ class Maps extends Component { const { maps, user, currentUser } = this.props const numCards = maps.length + (user || currentUser ? 1 : 0) const mapSpaces = Math.floor(document.body.clientWidth / MAP_WIDTH) - const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT ? - document.body.clientWidth - MOBILE_VIEW_PADDING : - Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH + const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT + ? document.body.clientWidth - MOBILE_VIEW_PADDING + : Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH this.setState({ mapsWidth }) } scroll = () => { const { loadMore, moreToLoad, pending } = this.props const { maps } = this.refs - if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300 ) { + if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300) { loadMore() } } render = () => { - const { maps, currentUser, juntoState, pending, section, user, moreToLoad, loadMore, onStar, onRequest } = this.props + const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest } = this.props const style = { width: this.state.mapsWidth + 'px' } const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index c8ee33b1..7e4ff0ab 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -6,8 +6,8 @@ import Import from '../src/Metamaps/Import' const { expect } = chai -describe('Metamaps.Import.js', function () { - it('has a topic whitelist', function () { +describe('Metamaps.Import.js', function() { + it('has a topic whitelist', function() { expect(Import.topicWhitelist).to.deep.equal( ['id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission'] ) diff --git a/frontend/test/Metamaps.Util.spec.js b/frontend/test/Metamaps.Util.spec.js index effd5e30..e0366bd9 100644 --- a/frontend/test/Metamaps.Util.spec.js +++ b/frontend/test/Metamaps.Util.spec.js @@ -6,123 +6,123 @@ import Util from '../src/Metamaps/Util' const { expect } = chai -describe('Metamaps.Util.js', function () { - describe('splitLine', function () { - it('splits on words', function () { +describe('Metamaps.Util.js', function() { + describe('splitLine', function() { + it('splits on words', function() { expect(Util.splitLine('test test test', 10)) .to.equal('test test\ntest') }) // TODO this test seems like it's incorrect behaviour - it('splits mid-word if need be', function () { + it('splits mid-word if need be', function() { expect(Util.splitLine('test test', 2)) .to.equal('te\nt\nte\nt') }) - it('splits words over 30 chars', function () { + it('splits words over 30 chars', function() { expect(Util.splitLine('suprainterpostantidisestablishmentarianism', 30)) .to.equal('suprainterpostantidisestablish\nentarianism') }) }) - describe('nowDateFormatted', function () { + describe('nowDateFormatted', function() { it.skip('TODO need `Date`') }) - describe('decodeEntities', function () { + describe('decodeEntities', function() { it.skip('TODO need `document`') }) - describe('getDistance', function () { - it('(0,0) -> (0,0) = 0', function () { + describe('getDistance', function() { + it('(0,0) -> (0,0) = 0', function() { expect(Util.getDistance({ x: 0, y: 0 }, { x: 0, y: 0 })) .to.equal(0) }) - it('(-5,0) -> (5,0) = 10', function () { + it('(-5,0) -> (5,0) = 10', function() { expect(Util.getDistance({ x: -5, y: 0 }, { x: 5, y: 0 })) .to.equal(10) }) - it('(0,0) -> (5,7) = 8.6023', function () { + it('(0,0) -> (5,7) = 8.6023', function() { expect(Util.getDistance({ x: 0, y: 0 }, { x: 5, y: 7 }).toFixed(4)) .to.equal('8.6023') }) }) - describe('coordsToPixels', function () { - it('returns 0,0 for null canvas', function () { + describe('coordsToPixels', function() { + it('returns 0,0 for null canvas', function() { expect(Util.coordsToPixels(null, {}).x).to.equal(0) expect(Util.coordsToPixels(null, {}).y).to.equal(0) }) it.skip('TODO need initialized mGraph to test further') }) - describe('pixelsToCoords', function () { - it('returns 0,0 for null canvas', function () { + describe('pixelsToCoords', function() { + it('returns 0,0 for null canvas', function() { expect(Util.pixelsToCoords(null, {}).x).to.equal(0) expect(Util.pixelsToCoords(null, {}).y).to.equal(0) }) it.skip('TODO need initialized mGraph to test further') }) - describe('getPastelColor', function () { - it('1 => fefefe', function () { + describe('getPastelColor', function() { + it('1 => fefefe', function() { expect(Util.getPastelColor({ rseed: 1, gseed: 1, bseed: 1 })) .to.equal(Util.colorLuminance('#fefefe', -0.4)) }) - it('0 => 7f7f7f', function () { + it('0 => 7f7f7f', function() { expect(Util.getPastelColor({ rseed: 0, gseed: 0, bseed: 0 })) .to.equal(Util.colorLuminance('#7f7f7f', -0.4)) }) }) - describe('colorLuminance', function () { - describe('-0.4 lum', function () { - it('white => ?', function () { + describe('colorLuminance', function() { + describe('-0.4 lum', function() { + it('white => ?', function() { expect(Util.colorLuminance('#ffffff', -0.4)).to.equal('#999999') }) - it('black => ?', function () { + it('black => ?', function() { expect(Util.colorLuminance('#000000', -0.4)).to.equal('#000000') }) - it('7f7f7f => ?', function () { + it('7f7f7f => ?', function() { expect(Util.colorLuminance('#7f7f7f', -0.4)).to.equal('#4c4c4c') }) }) - describe('other lum values', function () { - it('-1', function () { + describe('other lum values', function() { + it('-1', function() { expect(Util.colorLuminance('#7f7f7f', -1)).to.equal('#000000') }) - it('-0.5', function () { + it('-0.5', function() { expect(Util.colorLuminance('#7f7f7f', -0.5)).to.equal('#404040') }) - it('0', function () { + it('0', function() { expect(Util.colorLuminance('#7f7f7f', 0)).to.equal('#7f7f7f') }) - it('0.5', function () { + it('0.5', function() { expect(Util.colorLuminance('#7f7f7f', 0.5)).to.equal('#bfbfbf') }) - it('1', function () { + it('1', function() { expect(Util.colorLuminance('#7f7f7f', 1)).to.equal('#fefefe') }) }) }) - describe('openLink', function () { + describe('openLink', function() { it.skip('TODO need `window`') }) - describe('mdToHTML', function () { - it('filters xss', function () { + describe('mdToHTML', function() { + it('filters xss', function() { const md = '' const html = '' expect(Util.mdToHTML(md).trim()).to.equal(html) }) - it('bold and italics', function () { + it('bold and italics', function() { const md = '**Bold** *Italics*' const html = '

    Bold Italics

    ' expect(Util.mdToHTML(md).trim()).to.equal(html) }) - it('links and images', function () { + it('links and images', function() { const md = '[Link](https://metamaps.cc) ![Image](https://example.org/image.png)' const html = '

    Link Image

    ' expect(Util.mdToHTML(md).trim()).to.equal(html) }) }) - describe('logCanvasAttributes', function () { + describe('logCanvasAttributes', function() { it.skip('TODO need a canvas') }) - describe('resizeCanvas', function () { + describe('resizeCanvas', function() { it.skip('TODO need a canvas') }) }) diff --git a/realtime/global.js b/realtime/global.js index 1df9d8ea..485085a6 100644 --- a/realtime/global.js +++ b/realtime/global.js @@ -19,14 +19,13 @@ const { UPDATE_MAP } = require('../frontend/src/Metamaps/Realtime/events') -module.exports = function (io, store) { +module.exports = function(io, store) { store.subscribe(() => { console.log(store.getState()) io.sockets.emit(JUNTO_UPDATED, store.getState()) }) - io.on('connection', function (socket) { - + io.on('connection', function(socket) { io.sockets.emit(JUNTO_UPDATED, store.getState()) socket.on(JOIN_MAP, data => store.dispatch({ type: JOIN_MAP, payload: data })) @@ -35,23 +34,23 @@ module.exports = function (io, store) { socket.on(LEAVE_CALL, () => store.dispatch({ type: LEAVE_CALL, payload: socket })) socket.on('disconnect', () => store.dispatch({ type: 'DISCONNECT', payload: socket })) - socket.on(UPDATE_TOPIC, function (data) { + socket.on(UPDATE_TOPIC, function(data) { socket.broadcast.emit(TOPIC_UPDATED, data) }) - socket.on(DELETE_TOPIC, function (data) { + socket.on(DELETE_TOPIC, function(data) { socket.broadcast.emit(TOPIC_DELETED, data) }) - socket.on(UPDATE_SYNAPSE, function (data) { + socket.on(UPDATE_SYNAPSE, function(data) { socket.broadcast.emit(SYNAPSE_UPDATED, data) }) - socket.on(DELETE_SYNAPSE, function (data) { + socket.on(DELETE_SYNAPSE, function(data) { socket.broadcast.emit(SYNAPSE_DELETED, data) }) - socket.on(UPDATE_MAP, function (data) { + socket.on(UPDATE_MAP, function(data) { socket.broadcast.emit(MAP_UPDATED, data) }) }) diff --git a/realtime/junto.js b/realtime/junto.js index 2e6f9780..a361c1e3 100644 --- a/realtime/junto.js +++ b/realtime/junto.js @@ -21,41 +21,40 @@ const { const { mapRoom, userMapRoom } = require('./rooms') -module.exports = function (io, store) { - io.on('connection', function (socket) { - - socket.on(CHECK_FOR_CALL, function (data) { +module.exports = function(io, store) { + io.on('connection', function(socket) { + socket.on(CHECK_FOR_CALL, function(data) { var callInProgress = Object.keys(io.nsps['/'].adapter.rooms[data.room] || {}).length if (callInProgress) socket.emit(CALL_IN_PROGRESS) }) - socket.on(INVITE_A_CALL, function (data) { + socket.on(INVITE_A_CALL, function(data) { socket.broadcast.in(userMapRoom(data.invited, data.mapid)).emit(INVITED_TO_CALL, data.inviter) }) - socket.on(INVITE_TO_JOIN, function (data) { + socket.on(INVITE_TO_JOIN, function(data) { socket.broadcast.in(userMapRoom(data.invited, data.mapid)).emit(INVITED_TO_JOIN, data.inviter) }) - socket.on(ACCEPT_CALL, function (data) { + socket.on(ACCEPT_CALL, function(data) { socket.broadcast.in(userMapRoom(data.inviter, data.mapid)).emit(CALL_ACCEPTED, data.invited) socket.broadcast.in(mapRoom(data.mapid)).emit(CALL_STARTED) }) - socket.on(DENY_CALL, function (data) { + socket.on(DENY_CALL, function(data) { socket.broadcast.in(userMapRoom(data.inviter, data.mapid)).emit(CALL_DENIED, data.invited) }) - socket.on(DENY_INVITE, function (data) { + socket.on(DENY_INVITE, function(data) { socket.broadcast.in(userMapRoom(data.inviter, data.mapid)).emit(INVITE_DENIED, data.invited) }) - socket.on(JOIN_CALL, function (data) { + socket.on(JOIN_CALL, function(data) { socket.broadcast.in(mapRoom(data.mapid)).emit(MAPPER_JOINED_CALL, data.id) }) - socket.on(LEAVE_CALL, function (data) { + socket.on(LEAVE_CALL, function(data) { socket.broadcast.in(mapRoom(data.mapid)).emit(MAPPER_LEFT_CALL, data.id) }) }) -} +} diff --git a/realtime/map.js b/realtime/map.js index 5e153209..fb3f078a 100644 --- a/realtime/map.js +++ b/realtime/map.js @@ -24,11 +24,10 @@ const { const { mapRoom, userMapRoom } = require('./rooms') -module.exports = function (io, store) { - io.on('connection', function (socket) { - +module.exports = function(io, store) { + io.on('connection', function(socket) { // this will ping everyone on a map that there's a person just joined the map - socket.on(JOIN_MAP, function (data) { + socket.on(JOIN_MAP, function(data) { socket.mapid = data.mapid socket.userid = data.userid socket.username = data.username @@ -52,12 +51,12 @@ module.exports = function (io, store) { socket.leave(userMapRoom(socket.userid, socket.mapid)) socket.broadcast.in(mapRoom(socket.mapid)).emit(LOST_MAPPER, data) socket.mapid = null - } + } socket.on(LEAVE_MAP, leaveMap) socket.on('disconnect', leaveMap) // this will ping a new person with awareness of who's already on the map - socket.on(SEND_MAPPER_INFO, function (data) { + socket.on(SEND_MAPPER_INFO, function(data) { var existingUser = { userid: data.userid, username: data.username, @@ -67,7 +66,7 @@ module.exports = function (io, store) { socket.broadcast.in(userMapRoom(data.userToNotify, data.mapid)).emit(MAPPER_LIST_UPDATED, existingUser) }) - socket.on(SEND_COORDS, function (data) { + socket.on(SEND_COORDS, function(data) { var peer = { userid: data.userid, usercoords: data.usercoords @@ -75,37 +74,37 @@ module.exports = function (io, store) { socket.broadcast.in(mapRoom(data.mapid)).emit(PEER_COORDS_UPDATED, peer) }) - socket.on(CREATE_MESSAGE, function (data) { + socket.on(CREATE_MESSAGE, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(MESSAGE_CREATED, data) }) - socket.on(DRAG_TOPIC, function (data) { + socket.on(DRAG_TOPIC, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_DRAGGED, data) }) - socket.on(CREATE_TOPIC, function (data) { + socket.on(CREATE_TOPIC, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_CREATED, data) }) - socket.on(REMOVE_TOPIC, function (data) { + socket.on(REMOVE_TOPIC, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(TOPIC_REMOVED, data) }) - socket.on(CREATE_SYNAPSE, function (data) { + socket.on(CREATE_SYNAPSE, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(SYNAPSE_CREATED, data) }) - socket.on(REMOVE_SYNAPSE, function (data) { + socket.on(REMOVE_SYNAPSE, function(data) { var mapId = data.mapid delete data.mapid socket.broadcast.in(mapRoom(mapId)).emit(SYNAPSE_REMOVED, data) diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index 9b07cfc3..ad5a1900 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -1,10 +1,9 @@ -var -io = require('socket.io')(), -signalling = require('./signal'), -junto = require('./junto'), -map = require('./map'), -global = require('./global'), -stunservers = [{"url": "stun:stun.l.google.com:19302"}] +const io = require('socket.io')() +const signalling = require('./signal') +const junto = require('./junto') +const map = require('./map') +const global = require('./global') +const stunservers = [{'url': 'stun:stun.l.google.com:19302'}] const { createStore } = require('redux') const reducer = require('./reducer') diff --git a/realtime/reducer.js b/realtime/reducer.js index 183ee081..ae2b7488 100644 --- a/realtime/reducer.js +++ b/realtime/reducer.js @@ -9,7 +9,7 @@ const { const NOT_IN_CONVERSATION = 0 const IN_CONVERSATION = 1 -const addMapperToMap = (map, userId) => { return Object.assign({}, map, { [userId]: NOT_IN_CONVERSATION })} +const addMapperToMap = (map, userId) => { return Object.assign({}, map, { [userId]: NOT_IN_CONVERSATION }) } const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { const { type, payload } = action @@ -19,56 +19,56 @@ const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { const callWillFinish = map && (type === LEAVE_CALL || type === 'DISCONNECT') && Object.keys(map).length === 2 switch (type) { - case JOIN_MAP: - return Object.assign({}, state, { - connectedPeople: Object.assign({}, connectedPeople, { - [payload.userid]: { - id: payload.userid, - username: payload.username, - avatar: payload.avatar - } - }), - liveMaps: Object.assign({}, liveMaps, { - [payload.mapid]: addMapperToMap(map || {}, payload.userid) + case JOIN_MAP: + return Object.assign({}, state, { + connectedPeople: Object.assign({}, connectedPeople, { + [payload.userid]: { + id: payload.userid, + username: payload.username, + avatar: payload.avatar + } + }), + liveMaps: Object.assign({}, liveMaps, { + [payload.mapid]: addMapperToMap(map || {}, payload.userid) + }) }) - }) - case LEAVE_MAP: + case LEAVE_MAP: // if the map will empty, remove it from liveMaps, if the map will not empty, just remove the mapper - const newLiveMaps = mapWillEmpty + const newLiveMaps = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: omit(map, payload.userid) }) - return { - connectedPeople: omit(connectedPeople, payload.userid), - liveMaps: omitBy(newLiveMaps, isNil) - } - case JOIN_CALL: + return { + connectedPeople: omit(connectedPeople, payload.userid), + liveMaps: omitBy(newLiveMaps, isNil) + } + case JOIN_CALL: // update the user (payload.id is user id) in the given map to be marked in the conversation - return Object.assign({}, state, { - liveMaps: Object.assign({}, liveMaps, { - [payload.mapid]: Object.assign({}, map, { - [payload.id]: IN_CONVERSATION + return Object.assign({}, state, { + liveMaps: Object.assign({}, liveMaps, { + [payload.mapid]: Object.assign({}, map, { + [payload.id]: IN_CONVERSATION + }) }) }) - }) - case LEAVE_CALL: - const newMap = callWillFinish + case LEAVE_CALL: + const newMap = callWillFinish ? mapValues(map, () => NOT_IN_CONVERSATION) : Object.assign({}, map, { [payload.userid]: NOT_IN_CONVERSATION }) - return Object.assign({}, state, { - liveMaps: Object.assign({}, liveMaps, { map: newMap }) - }) - case 'DISCONNECT': - const mapWithoutUser = omit(map, payload.userid) - const newMapWithoutUser = callWillFinish ? mapValues(mapWithoutUser, () => NOT_IN_CONVERSATION) : mapWithoutUser - const newLiveMapsWithoutUser = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: newMapWithoutUser }) - return { - connectedPeople: omit(connectedPeople, payload.userid), - liveMaps: omitBy(newLiveMapsWithoutUser, isNil) - } - default: - return state + return Object.assign({}, state, { + liveMaps: Object.assign({}, liveMaps, { map: newMap }) + }) + case 'DISCONNECT': + const mapWithoutUser = omit(map, payload.userid) + const newMapWithoutUser = callWillFinish ? mapValues(mapWithoutUser, () => NOT_IN_CONVERSATION) : mapWithoutUser + const newLiveMapsWithoutUser = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: newMapWithoutUser }) + return { + connectedPeople: omit(connectedPeople, payload.userid), + liveMaps: omitBy(newLiveMapsWithoutUser, isNil) + } + default: + return state } } diff --git a/realtime/signal.js b/realtime/signal.js index 39283709..0f7730d5 100644 --- a/realtime/signal.js +++ b/realtime/signal.js @@ -5,14 +5,14 @@ const uuid = require('node-uuid') function safeCb(cb) { if (typeof cb === 'function') { - return cb; + return cb } else { - return function () {}; + return function() {} } } module.exports = function(io, stunservers, state) { - io.on('connection', function (socket) { + io.on('connection', function(socket) { socket.resources = { screen: false, video: true, @@ -20,7 +20,7 @@ module.exports = function(io, stunservers, state) { } // pass a message to another id - socket.on('message', function (details) { + socket.on('message', function(details) { if (!details) return var otherClient = io.to(details.to) @@ -30,11 +30,11 @@ module.exports = function(io, stunservers, state) { otherClient.emit('message', details) }) - socket.on('shareScreen', function () { + socket.on('shareScreen', function() { socket.resources.screen = true }) - socket.on('unshareScreen', function (type) { + socket.on('unshareScreen', function(type) { socket.resources.screen = false removeFeed('screen') }) @@ -66,16 +66,16 @@ module.exports = function(io, stunservers, state) { // we don't want to pass "leave" directly because the // event type string of "socket end" gets passed too. - socket.on('disconnect', function () { + socket.on('disconnect', function() { removeFeed() }) - socket.on('leave', function () { + socket.on('leave', function() { removeFeed() }) - socket.on('create', function (name, cb) { - if (arguments.length == 2) { - cb = (typeof cb == 'function') ? cb : function () {} + socket.on('create', function(name, cb) { + if (arguments.length === 2) { + cb = (typeof cb === 'function') ? cb : function() {} name = name || uuid() } else { cb = name @@ -93,7 +93,7 @@ module.exports = function(io, stunservers, state) { // support for logging full webrtc traces to stdout // useful for large-scale error monitoring - socket.on('trace', function (data) { + socket.on('trace', function(data) { console.log('trace', JSON.stringify( [data.type, data.session, data.prefix, data.peer, data.time, data.value] )) @@ -106,13 +106,9 @@ module.exports = function(io, stunservers, state) { var result = { clients: {} } - Object.keys(sockets).forEach(function (id) { + Object.keys(sockets).forEach(function(id) { result.clients[id] = adapter.nsp.connected[id].resources }) return result } - - function socketsInRoom(name) { - return io.sockets.sockets(name).length - } } From 3759851621e2767a9ec56b44c831c561ddb19bc8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 8 Nov 2016 12:37:06 -0500 Subject: [PATCH 324/378] fix access#access route (#926) --- app/controllers/access_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index 302e9385..c48ac418 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -32,7 +32,7 @@ class AccessController < ApplicationController # POST maps/:id/access def access - user_ids = params[:access] || [] + user_ids = params[:access].to_a.map(&:to_i) || [] @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, From 83b58d43d5db78657d6c65f7d8c87aba72170ca7 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 8 Nov 2016 19:42:22 +0000 Subject: [PATCH 325/378] only remove user once they've left all maps --- realtime/realtime-server.js | 2 +- realtime/reducer.js | 29 ++++++++++++++++++++++------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index 9b07cfc3..72037eb7 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -16,4 +16,4 @@ signalling(io, stunservers, store) junto(io, store) map(io, store) -io.listen(5001) +io.listen(8081) diff --git a/realtime/reducer.js b/realtime/reducer.js index 183ee081..7146667f 100644 --- a/realtime/reducer.js +++ b/realtime/reducer.js @@ -1,4 +1,4 @@ -const { omit, omitBy, isNil, mapValues } = require('lodash') +const { find, omit, isNil, mapValues, values } = require('lodash') const { JOIN_MAP, LEAVE_MAP, @@ -9,7 +9,16 @@ const { const NOT_IN_CONVERSATION = 0 const IN_CONVERSATION = 1 -const addMapperToMap = (map, userId) => { return Object.assign({}, map, { [userId]: NOT_IN_CONVERSATION })} +const addMapperToMap = (map, userId) => Object.assign({}, map, { [userId]: NOT_IN_CONVERSATION }) +const userStillPresent = (userId, liveMaps) => { + if (!userId) return false + let stillPresent = false + const userIdString = userId.toString() + values(liveMaps).forEach(presentUsers => { + if (find(Object.keys(presentUsers), id => id === userIdString)) stillPresent = true + }) + return stillPresent +} const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { const { type, payload } = action @@ -37,10 +46,13 @@ const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { const newLiveMaps = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: omit(map, payload.userid) }) + delete newLiveMaps[undefined] + delete newLiveMaps[null] + const updateConnectedPeople = userStillPresent(payload.userid, newLiveMaps) ? connectedPeople : omit(connectedPeople, payload.userid) return { - connectedPeople: omit(connectedPeople, payload.userid), - liveMaps: omitBy(newLiveMaps, isNil) + connectedPeople: updateConnectedPeople, + liveMaps: newLiveMaps } case JOIN_CALL: // update the user (payload.id is user id) in the given map to be marked in the conversation @@ -57,15 +69,18 @@ const reducer = (state = { connectedPeople: {}, liveMaps: {} }, action) => { : Object.assign({}, map, { [payload.userid]: NOT_IN_CONVERSATION }) return Object.assign({}, state, { - liveMaps: Object.assign({}, liveMaps, { map: newMap }) + liveMaps: Object.assign({}, liveMaps, { [payload.mapid]: newMap }) }) case 'DISCONNECT': const mapWithoutUser = omit(map, payload.userid) const newMapWithoutUser = callWillFinish ? mapValues(mapWithoutUser, () => NOT_IN_CONVERSATION) : mapWithoutUser const newLiveMapsWithoutUser = mapWillEmpty ? omit(liveMaps, payload.mapid) : Object.assign({}, liveMaps, { [payload.mapid]: newMapWithoutUser }) + delete newLiveMapsWithoutUser[undefined] + delete newLiveMapsWithoutUser[null] + const newConnectedPeople = userStillPresent(payload.userid, newLiveMapsWithoutUser) ? connectedPeople : omit(connectedPeople, payload.userid) return { - connectedPeople: omit(connectedPeople, payload.userid), - liveMaps: omitBy(newLiveMapsWithoutUser, isNil) + connectedPeople: newConnectedPeople, + liveMaps: newLiveMapsWithoutUser } default: return state From d1f75c8c246bc0aa3935710dd657c575256e2d1f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 8 Nov 2016 19:45:51 +0000 Subject: [PATCH 326/378] oops! don't change rt server port --- realtime/realtime-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index 72037eb7..9b07cfc3 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -16,4 +16,4 @@ signalling(io, stunservers, store) junto(io, store) map(io, store) -io.listen(8081) +io.listen(5001) From e1441acde03d655212dab0ecb0a5548698ac64e8 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 8 Nov 2016 19:50:08 +0000 Subject: [PATCH 327/378] synapse wasn't updating calculated_permission --- frontend/src/Metamaps/Backbone/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index ff4c48b9..1547e145 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -270,8 +270,8 @@ _Backbone.init = function () { newOptions.success = function (model, response, opt) { if (s) s(model, response, opt) - model.trigger('saved') model.set('calculated_permission', model.get('permission')) + model.trigger('saved') if (permBefore === 'private' && model.get('permission') !== 'private') { model.trigger('noLongerPrivate') @@ -436,6 +436,7 @@ _Backbone.init = function () { newOptions.success = function (model, response, opt) { if (s) s(model, response, opt) + model.set('calculated_permission', model.get('permission')) model.trigger('saved') if (permBefore === 'private' && model.get('permission') !== 'private') { From 4533a0f2fee3ac51464a5fa8cf89ae2435158527 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 9 Nov 2016 09:34:10 -0500 Subject: [PATCH 328/378] merge changes from metamapscc instance branch (#913) * homepage video fallback * public/50x.html * remove blog from public/ html pages * remove video file since it's on metamaps.cc server * eslint --- app/assets/images/metamaps-intro-poster.webp | Bin 0 -> 32902 bytes app/assets/javascripts/application.js | 2 ++ .../javascripts/homepageVimeoFallback.js | 29 ++++++++++++++++++ public/404.html | 1 - public/503.html | 3 +- public/50x.html | 25 +++++++++++++++ 6 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/metamaps-intro-poster.webp create mode 100644 app/assets/javascripts/homepageVimeoFallback.js create mode 100644 public/50x.html diff --git a/app/assets/images/metamaps-intro-poster.webp b/app/assets/images/metamaps-intro-poster.webp new file mode 100644 index 0000000000000000000000000000000000000000..641b72df4c58e580f40d6c7be3f856f427fd4297 GIT binary patch literal 32902 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
  • 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', { + poster: '/assets/metamaps-intro-poster.webp', + width: '560', + height: '315', + class: 'homeVideo', + controls: '' + })) + $('.homeVideo').append($('', { + src: 'https://metamaps.cc/videos/metamaps-intro.mp4', + type: 'video/mp4' + })) + $('.homeVideo').append( + '

    You can watch our instruction video at ' + + '' + + 'https://metamaps.cc/videos/metamaps-intro.mp4.' + ) + } + }) + }// if +}) diff --git a/public/404.html b/public/404.html index d07c1069..5c9ec093 100644 --- a/public/404.html +++ b/public/404.html @@ -120,7 +120,6 @@

    Therefore there is nothing to see here, nothing to behold here, nothing to be here, maybe there is but definitely not here.

    Therefore, move along! Nothing is ending here...

    EXPLORE FEATURED MAPS - GO TO OUR BLOG
  • diff --git a/public/503.html b/public/503.html index 408ba46e..604d1487 100644 --- a/public/503.html +++ b/public/503.html @@ -116,9 +116,8 @@

    503 - Down for Maintenance

    Metamaps is offline!

    Don't worry, this is intentional. We're probably just turning on some great new features.

    -

    In the meantime, you can visit our blog, or aimlessly search YouTube until we're back online.

    +

    In the meantime, you can visit our Twitter, or aimlessly search YouTube until we're back online.

    GO TO OUR TWITTER FEED - GO TO OUR BLOG
    diff --git a/public/50x.html b/public/50x.html new file mode 100644 index 00000000..c7dae1d5 --- /dev/null +++ b/public/50x.html @@ -0,0 +1,25 @@ + + + + We're really sorry, but something went wrong (500) + + + + + +
    +

    We're really sorry, but something went wrong.

    +
    + + From ce2d462578f842985db36df659f521aa4747b394 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 8 Nov 2016 15:51:39 -0500 Subject: [PATCH 329/378] fix bugs on develop branch --- .example-env | 6 +++++- app/controllers/hacks_controller.rb | 11 +++++++++-- doc/production/first-deploy.md | 4 ++-- frontend/src/Metamaps/Import.js | 1 + frontend/src/Metamaps/PasteInput.js | 2 +- frontend/src/Metamaps/Realtime/index.js | 6 +++++- frontend/src/Metamaps/Views/ChatView.js | 4 ++-- frontend/src/Metamaps/Views/Room.js | 5 +++-- realtime/realtime-server.js | 2 +- 9 files changed, 29 insertions(+), 12 deletions(-) diff --git a/.example-env b/.example-env index 1afb010b..96b60f55 100644 --- a/.example-env +++ b/.example-env @@ -1,10 +1,14 @@ +# Node JS env +export NODE_REALTIME_PORT='5000' # should match REALTIME_SERVER, below + +# Rails env export DB_USERNAME='postgres' export DB_PASSWORD='3112' export DB_HOST='localhost' export DB_PORT='5432' export DB_NAME='metamap002' -export REALTIME_SERVER='http://localhost:5001' +export REALTIME_SERVER='http://localhost:5000' export MAILER_DEFAULT_URL='localhost:3000' export DEVISE_MAILER_SENDER='team@metamaps.cc' diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb index 1abe3e60..22df9cbb 100644 --- a/app/controllers/hacks_controller.rb +++ b/app/controllers/hacks_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # bad code that should be checked over before entering one of the # nice files from the right side of this repo class HacksController < ApplicationController @@ -11,7 +12,7 @@ class HacksController < ApplicationController response, url = get_with_redirects(url) title = get_encoded_title(response) render json: { success: true, title: title, url: url } - rescue StandardError => e + rescue StandardError render json: { success: false } end @@ -28,7 +29,13 @@ class HacksController < ApplicationController end def get_encoded_title(http_response) - title = http_response.body.sub(/.*(.*)<\/title>.*/m, '\1') + # ensure there's actually an html title tag + title = http_response.body.sub(%r{.*(<title>.*).*}m, '\1') + return '' unless title.starts_with?('') + return '' unless title.ends_with?('') + title = title.sub('', '').sub(%r{$}, '') + + # encode and trim the title to 140 usable characters charset = http_response['content-type'].sub(/.*charset=(.*);?.*/, '\1') charset = nil if charset == 'text/html' title = title.force_encoding(charset) if charset diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md index 413717dd..994e68c2 100644 --- a/doc/production/first-deploy.md +++ b/doc/production/first-deploy.md @@ -93,10 +93,10 @@ server to see what problems show up: #### Realtime server: sudo npm install -g forever - (crontab -u metamaps -l 2>/dev/null; echo "@reboot $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps - + (crontab -u metamaps -l 2>/dev/null; echo "@reboot env NODE_REALTIME_PORT=5000 $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps - mkdir -p /home/metamaps/logs - forever --append \ + env NODE_REALTIME_PORT=5000 forever --append \ -c /home/metamaps/metamaps/node_modules/.bin/babel-node \ -l /home/metamaps/logs/forever.realtime.log \ start /home/metamaps/metamaps/realtime/realtime-server.js diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index cde37a73..7d1b3aa3 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -375,6 +375,7 @@ const Import = { $.get('/hacks/load_url_title', { url }, function success(data, textStatus) { + if (data.trim() === '') return var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' if ($(selector).find('form').length > 0) { $(selector).find('textarea, input').val(data.title) diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index f8c93c3f..6227b23b 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -25,7 +25,7 @@ const PasteInput = { self.handleFile(e.dataTransfer.files[0], coords) } // OMG import bookmarks 😍 - if (e.dataTransfer.items.length > 0) { + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { e.dataTransfer.items[0].getAsString(function(text) { if (text.match(self.URL_REGEX)) { self.handle(text, coords) diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index db1334de..952b835c 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -172,7 +172,11 @@ let Realtime = { room: 'global', $video: self.localVideo.$video, myVideoView: self.localVideo.view, - config: { DOUBLE_CLICK_TOLERANCE: 200 } + config: { DOUBLE_CLICK_TOLERANCE: 200 }, + soundUrls: [ + serverData['sounds/MM_sounds.mp3'], + serverData['sounds/MM_sounds.ogg'] + ] }) self.room.videoAdded(self.handleVideoAdded) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 4a549648..fd77864f 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -226,7 +226,7 @@ var Handlers = { } } -const ChatView = function(messages, mapper, room) { +const ChatView = function(messages, mapper, room, opts = {}) { this.room = room this.mapper = mapper this.messages = messages // backbone collection @@ -243,7 +243,7 @@ const ChatView = function(messages, mapper, room) { Private.attachElements.call(this) Private.addEventListeners.call(this) Private.initialMessages.call(this) - Private.initializeSounds.call(this, room.soundUrls) + Private.initializeSounds.call(this, opts.soundUrls) this.$container.css({ right: '-300px' }) diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index f3327c6d..a3a79cc8 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -26,10 +26,11 @@ const Room = function(opts = {}) { this.messages = new Backbone.Collection() this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }) - this.chat = new ChatView(this.messages, this.currentMapper, this.room) + this.chat = new ChatView(this.messages, this.currentMapper, this.room, { + soundUrls: opts.soundUrls + }) this.videos = {} - this.soundUrls = opts.soundUrls this.init() } diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index ad5a1900..0150e84d 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -15,4 +15,4 @@ signalling(io, stunservers, store) junto(io, store) map(io, store) -io.listen(5001) +io.listen(parseInt(process.env.NODE_REALTIME_PORT) || 5000) From c03d6dd5f633f0b7250df46be96d1e46f1fd68c8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 13 Nov 2016 17:21:42 -0500 Subject: [PATCH 330/378] remove in_trash from schema to match production --- db/schema.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index b9445c1b..d16d4fb9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -75,7 +75,6 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.datetime "updated_at", null: false t.integer "mappable_id" t.string "mappable_type" - t.boolean "in_trash" t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree @@ -190,7 +189,6 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.text "permission" t.text "weight" t.integer "defer_to_map_id" - t.boolean "in_trash" t.index ["topic1_id", "topic1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree t.index ["topic1_id"], name: "index_synapses_on_topic1_id", using: :btree t.index ["topic2_id", "topic2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree @@ -225,7 +223,6 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.integer "audio_file_size" t.datetime "audio_updated_at" t.integer "defer_to_map_id" - t.boolean "in_trash" t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree t.index ["user_id"], name: "index_topics_on_user_id", using: :btree end From 55853c60f434dbb6b0b5613a6ae8c31227941063 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 13 Nov 2016 14:29:07 -0800 Subject: [PATCH 331/378] update user model with fixes, including style and recentMetacodes algorithm (#922) --- app/helpers/application_helper.rb | 8 ++++---- app/models/user.rb | 26 +++++++------------------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03cbfbf4..3221aa34 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,9 +15,9 @@ module ApplicationHelper @m = current_user.settings.metacodes set = metacodeset @metacodes = if set && set == 'Most' - Metacode.where(id: current_user.mostUsedMetacodes).to_a + Metacode.where(id: current_user.most_used_metacodes).to_a elsif set && set == 'Recent' - Metacode.where(id: current_user.recentMetacodes).to_a + Metacode.where(id: current_user.recent_metacodes).to_a elsif set set.metacodes.to_a else @@ -27,11 +27,11 @@ module ApplicationHelper end def user_most_used_metacodes - @metacodes = current_user.mostUsedMetacodes.map { |id| Metacode.find(id) } + @metacodes = current_user.most_used_metacodes.map { |id| Metacode.find(id) } end def user_recent_metacodes - @metacodes = current_user.recentMetacodes.map { |id| Metacode.find(id) } + @metacodes = current_user.recent_metacodes.map { |id| Metacode.find(id) } end def invite_link diff --git a/app/models/user.rb b/app/models/user.rb index 52d6ef09..23ef6440 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -64,32 +64,20 @@ class User < ApplicationRecord json['rtype'] = 'mapper' json end - + def all_accessible_maps - #TODO: is there a way to keep this an ActiveRecord relation? maps + shared_maps end - def recentMetacodes - array = [] - self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| - if array.length < 5 and array.index(t.metacode_id) == nil - array.push(t.metacode_id) - end - end - array + def recent_metacodes + topics.order(:created_at).pluck(:metacode_id).uniq.first(5) end - def mostUsedMetacodes - self.topics.to_a.reduce({}) { |memo, topic| - if memo[topic.metacode_id] == nil - memo[topic.metacode_id] = 1 - else - memo[topic.metacode_id] = memo[topic.metacode_id] + 1 - end - + def most_used_metacodes + topics.to_a.each_with_object(Hash.new(0)) do |topic, memo| + memo[topic.metacode_id] += 1 memo - }.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5) + end.to_a.sort { |a, b| b[1] <=> a[1] }.map { |i| i[0] }.slice(0, 5) end # generate a random 8 letter/digit code that they can use to invite people From 95b8b52224a539fa6a5b42d36ebe241e2a05334e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 24 Nov 2016 14:16:58 -0500 Subject: [PATCH 332/378] fix NoMethodError in topics#autocomplete_topic (#930) --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index f6c85120..ea56059b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -12,7 +12,7 @@ class TopicsController < ApplicationController term = params[:term] if term && !term.empty? @topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"') - @mapTopics = @topics.select { |t| t.metacode.name == 'Metamap' } + @mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' } # prioritize topics which point to maps, over maps @exclude = @mapTopics.length > 0 ? @mapTopics.map(&:name) : [''] @maps = policy_scope(Map).where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude).order('"name"') From 5d8ff3efce79701736748bc3e071307ef7743227 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 24 Nov 2016 14:40:14 -0500 Subject: [PATCH 333/378] realtime shouldn't poll forever if the dev server isn't up (#931) --- frontend/src/Metamaps/Realtime/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index 952b835c..318753f0 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -111,7 +111,13 @@ let Realtime = { self.addJuntoListeners() - self.socket = new SocketIoConnection({ url: serverData['REALTIME_SERVER'] }) + self.socket = new SocketIoConnection({ + url: serverData['REALTIME_SERVER'], + socketio: { + // don't poll forever if in development + reconnectionAttempts: serverData.RAILS_ENV === 'development' ? 5 : Infinity + } + }) self['junto_spinner_darkgrey.gif'] = serverData['junto_spinner_darkgrey.gif'] self.socket.on('connect', function() { From 90c5bc26fc313a9d50271ed9db63ad3bdfafcb15 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 27 Nov 2016 21:12:05 -0500 Subject: [PATCH 334/378] Active.Mapper was being initialized twice, causing errors (#934) --- frontend/src/Metamaps/DataModel/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/Metamaps/DataModel/index.js b/frontend/src/Metamaps/DataModel/index.js index 6235b879..6d41e19f 100644 --- a/frontend/src/Metamaps/DataModel/index.js +++ b/frontend/src/Metamaps/DataModel/index.js @@ -77,8 +77,6 @@ const DataModel = { if (serverData.Topics) self.Topics = new TopicCollection(serverData.Topics) // initialize global backbone models and collections - if (Active.Mapper) Active.Mapper = new self.Mapper(Active.Mapper) - var myCollection = serverData.Mine ? serverData.Mine : [] var sharedCollection = serverData.Shared ? serverData.Shared : [] var starredCollection = serverData.Starred ? serverData.Starred : [] From 4da3a9d55f621d7395f30d8559a1cdf545ffa103 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 28 Nov 2016 13:53:29 -0500 Subject: [PATCH 335/378] hide mobile menu if browser is resized to full size (#937) --- app/assets/stylesheets/mobile.scss.erb | 10 ++++++++++ frontend/src/Metamaps/Mobile.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 64794153..01fa5f61 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -229,6 +229,16 @@ list-style: none; } +/* + * the mobile menu, even if it's been opened by a user, should + * not show up if they resize their browser back to full size + */ +@media only screen and (max-width : 504px) { + #mobile_menu.visible { + display: block; + } +} + li.mobileMenuUser { border-bottom: 1px solid #BBB; } diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index dc4088c0..99ccc5ee 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -22,7 +22,7 @@ const Mobile = { self.toggleMenu() }, toggleMenu: function() { - $('#mobile_menu').toggle() + $('#mobile_menu').toggleClass('visible') }, titleClick: function() { if (Active.Map) { From 3f161c1076b618998e5c84c86926ac8e4e1910a9 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 29 Nov 2016 11:00:14 -0500 Subject: [PATCH 336/378] make topic titles have width of 25 chars (#933) --- frontend/src/Metamaps/JIT.js | 6 +++--- frontend/src/patched/JIT.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 0f21f70b..3520b946 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -228,7 +228,7 @@ const JIT = { ctx.fillStyle = '#FFF' ctx.textBaseline = 'alphabetic' - const arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') + const arrayOfLabelLines = Util.splitLine(desc, 25).split('\n') let lineWidths = [] for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) @@ -497,7 +497,7 @@ const JIT = { 'contains': function(node, pos) { const npos = node.pos.getc(true) const dim = node.getData('dim') - const arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n') + const arrayOfLabelLines = Util.splitLine(node.name, 25).split('\n') const ctx = Visualize.mGraph.canvas.getCtx() const height = 25 * arrayOfLabelLines.length @@ -1930,7 +1930,7 @@ const JIT = { minY = y } - let arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n') + let arrayOfLabelLines = Util.splitLine(n.name, 25).split('\n') let dim = n.getData('dim') let ctx = canvas.getCtx() diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index d4c3154a..e780604e 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -7580,7 +7580,7 @@ Graph.Label.Native = new Class({ //START METAMAPS CODE - var arrayOfLabelLines = Metamaps.Util.splitLine(node.name,30).split('\n'); + var arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 25).split('\n'); //render background ctx.fillStyle = ctx.strokeStyle = Metamaps.Settings.colors.labels.background; ctx.lineWidth = 2; From d1aa62d382397114345f8a658d1d90818eb39929 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 30 Nov 2016 11:46:09 -0500 Subject: [PATCH 337/378] fix policy scope error (#941) --- app/controllers/search_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index ae686d8f..c9fcc7db 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -70,6 +70,7 @@ class SearchController < ApplicationController builder = builder.where(user: user) if user @topics = builder.order(:name) else + skip_policy_scope @topics = [] end From a5f793fe5485256dabe971cf8f6ad7083f269fe4 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 2 Dec 2016 21:21:37 +0000 Subject: [PATCH 338/378] fixup topic card template --- app/views/layouts/_templates.html.erb | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index 9e298b3c..91dfcf7d 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -197,29 +197,29 @@
    {{metacode}} -
    +
    +
    +
    +
    {{{metacode_select}}}
    -
    -
    {{{metacode_select}}}
    +
    + +
    {{username}}
    +
    +
    +
    + {{map_count}} +
    Click to see which maps topic appears on
    +
      {{{inmaps}}}
    +
    + +
    + {{synapse_count}} +
    Click to see this topics synapses
    +
    +
    +
    -
    - -
    {{username}}
    -
    -
    -
    - {{map_count}} -
    Click to see which maps topic appears on
    -
      {{{inmaps}}}
    -
    - - -
    - {{synapse_count}} -
    Click to see this topics synapses
    -
    -
    -
    Date: Tue, 6 Dec 2016 12:36:06 -0500 Subject: [PATCH 339/378] add polyfill so chrome 49 can upload map thumbnail screenshots (#946) --- app/assets/javascripts/lib/canvas-to-blob.min.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/assets/javascripts/lib/canvas-to-blob.min.js diff --git a/app/assets/javascripts/lib/canvas-to-blob.min.js b/app/assets/javascripts/lib/canvas-to-blob.min.js new file mode 100644 index 00000000..1c82bba8 --- /dev/null +++ b/app/assets/javascripts/lib/canvas-to-blob.min.js @@ -0,0 +1,2 @@ +!function(t){"use strict";var e=t.HTMLCanvasElement&&t.HTMLCanvasElement.prototype,o=t.Blob&&function(){try{return Boolean(new Blob)}catch(t){return!1}}(),n=o&&t.Uint8Array&&function(){try{return 100===new Blob([new Uint8Array(100)]).size}catch(t){return!1}}(),r=t.BlobBuilder||t.WebKitBlobBuilder||t.MozBlobBuilder||t.MSBlobBuilder,a=/^data:((.*?)(;charset=.*?)?)(;base64)?,/,i=(o||r)&&t.atob&&t.ArrayBuffer&&t.Uint8Array&&function(t){var e,i,l,u,b,c,d,B,f;if(e=t.match(a),!e)throw new Error("invalid data URI");for(i=e[2]?e[1]:"text/plain"+(e[3]||";charset=US-ASCII"),l=!!e[4],u=t.slice(e[0].length),b=l?atob(u):decodeURIComponent(u),c=new ArrayBuffer(b.length),d=new Uint8Array(c),B=0;B Date: Tue, 6 Dec 2016 13:09:42 -0500 Subject: [PATCH 340/378] fix policy scope errors in search controller (#947) --- app/controllers/search_controller.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index c9fcc7db..c488c556 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -105,6 +105,7 @@ class SearchController < ApplicationController builder = builder.where(user: user) if user @maps = builder.order(:name) else + skip_policy_scope @maps = [] end @@ -120,10 +121,10 @@ class SearchController < ApplicationController term = term[7..-1] if term.downcase[0..6] == 'mapper:' search = term.downcase.strip + '%' - skip_policy_scope # TODO: builder = policy_scope(User) - builder = User.where('LOWER("name") like ?', search) + builder = policy_scope(User).where('LOWER("name") like ?', search) @mappers = builder.order(:name) else + skip_policy_scope @mappers = [] end render json: autocomplete_user_array_json(@mappers).to_json @@ -146,6 +147,7 @@ class SearchController < ApplicationController @synapses = @one + @two @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a else + skip_policy_scope @synapses = [] end From a133702be21621e45ba6eb6820463f90a8ef6c08 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 6 Dec 2016 16:46:46 -0500 Subject: [PATCH 341/378] Some topics and synapses were hidden from users erroneously (#944) * ensure topics and synapses have their permission match the map they're deferring to * update permission of topics and synapses as map perm changes, when defer_to_map * try enabling count threshold on rubocop * remove unused mk_permission functions * change *_count methods to use delegate to save lines in map.rb model * rubocop topic.rb --- .codeclimate.yml | 1 + app/models/map.rb | 36 +++++++++------------- app/models/synapse.rb | 6 +--- app/models/topic.rb | 25 ++++++--------- app/policies/synapse_policy.rb | 7 ++--- app/policies/topic_policy.rb | 7 ++--- frontend/src/Metamaps/DataModel/Synapse.js | 3 +- frontend/src/Metamaps/DataModel/Topic.js | 3 +- frontend/src/Metamaps/Import.js | 3 +- frontend/src/Metamaps/SynapseCard.js | 2 +- frontend/src/Metamaps/TopicCard.js | 4 +-- 11 files changed, 38 insertions(+), 59 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index fbd96af2..719b2807 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -8,6 +8,7 @@ engines: enabled: true config: languages: + count_threshold: 3 # rule of three ruby: mass_threshold: 36 # default: 18 javascript: diff --git a/app/models/map.rb b/app/models/map.rb index 86a89a24..36b2d284 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -32,14 +32,19 @@ class Map < ApplicationRecord # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ + after_save :update_deferring_topics_and_synapses, if: :permission_changed? + + delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` + delegate :count, to: :synapses, prefix: :synapse + delegate :count, to: :contributors, prefix: :contributor + delegate :count, to: :stars, prefix: :star + + delegate :name, to: :user, prefix: true + def mappings topicmappings.or(synapsemappings) end - def mk_permission - Perm.short(permission) - end - def contributors User.where(id: mappings.map(&:user_id).uniq) end @@ -48,28 +53,10 @@ class Map < ApplicationRecord User.where(id: user_id).or(User.where(id: collaborators)) end - def topic_count - topics.length - end - - def synapse_count - synapses.length - end - - delegate :name, to: :user, prefix: true - def user_image user.image.url(:thirtytwo) end - def contributor_count - contributors.length - end - - def star_count - stars.length - end - def collaborator_ids collaborators.map(&:id) end @@ -131,4 +118,9 @@ class Map < ApplicationRecord end removed.compact end + + def update_deferring_topics_and_synapses + Topic.where(defer_to_map_id: id).update_all(permission: permission) + Synapse.where(defer_to_map_id: id).update_all(permission: permission) + end end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 37c9c72d..08512e4f 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -36,11 +36,7 @@ class Synapse < ApplicationRecord end end - def calculated_permission - defer_to_map&.permission || permission - end - def as_json(_options = {}) - super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) + super(methods: [:user_name, :user_image, :collaborator_ids]) end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 85f670c3..256fc604 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -75,12 +75,8 @@ class Topic < ApplicationRecord Pundit.policy_scope(user, maps).map(&:id) end - def calculated_permission - defer_to_map&.permission || permission - end - def as_json(options = {}) - super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) + super(methods: [:user_name, :user_image, :collaborator_ids]) .merge(inmaps: inmaps(options[:user]), inmapsLinks: inmapsLinks(options[:user]), map_count: map_count(options[:user]), synapse_count: synapse_count(options[:user])) end @@ -129,15 +125,14 @@ class Topic < ApplicationRecord "Get: #{name}" end - def mk_permission - Perm.short(permission) - end - protected - def create_metamap? - if link == '' and metacode.name == 'Metamap' - @map = Map.create({ name: name, permission: permission, desc: '', arranged: true, user_id: user_id }) - self.link = Rails.application.routes.url_helpers.map_url(:host => ENV['MAILER_DEFAULT_URL'], :id => @map.id) - end - end + + def create_metamap? + return unless (link == '') && (metacode.name == 'Metamap') + + @map = Map.create(name: name, permission: permission, desc: '', + arranged: true, user_id: user_id) + self.link = Rails.application.routes.url_helpers + .map_url(host: ENV['MAILER_DEFAULT_URL'], id: @map.id) + end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 145f7432..e3190c18 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -2,11 +2,10 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - visible = %w(public commons) - return scope.where(permission: visible) unless user + return scope.where(permission: %w(public commons)) unless user - scope.where(permission: visible) - .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) + scope.where(permission: %w(public commons)) + .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index b29d9b44..64463b4a 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -2,11 +2,10 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - visible = %w(public commons) - return scope.where(permission: visible) unless user + return scope.where(permission: %w(public commons)) unless user - scope.where(permission: visible) - .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) + scope.where(permission: %w(public commons)) + .or(scope.where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index 5f2a6b88..e6a7f1c7 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -38,7 +38,6 @@ const Synapse = Backbone.Model.extend({ newOptions.success = function(model, response, opt) { if (s) s(model, response, opt) - model.set('calculated_permission', model.get('permission')) model.trigger('saved') if (permBefore === 'private' && model.get('permission') !== 'private') { @@ -85,7 +84,7 @@ const Synapse = Backbone.Model.extend({ ` }, authorizeToEdit: function(mapper) { - if (mapper && (this.get('calculated_permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true + if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true else return false }, authorizePermissionChange: function(mapper) { diff --git a/frontend/src/Metamaps/DataModel/Topic.js b/frontend/src/Metamaps/DataModel/Topic.js index dff635f2..0d71c973 100644 --- a/frontend/src/Metamaps/DataModel/Topic.js +++ b/frontend/src/Metamaps/DataModel/Topic.js @@ -37,7 +37,6 @@ const Topic = Backbone.Model.extend({ newOptions.success = function(model, response, opt) { if (s) s(model, response, opt) - model.set('calculated_permission', model.get('permission')) model.trigger('saved') if (permBefore === 'private' && model.get('permission') !== 'private') { @@ -82,7 +81,7 @@ const Topic = Backbone.Model.extend({ authorizeToEdit: function(mapper) { if (mapper && (this.get('user_id') === mapper.get('id') || - this.get('calculated_permission') === 'commons' || + this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')))) { return true } else { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 7d1b3aa3..deb71048 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -295,8 +295,7 @@ const Import = { permission: topicPermision, defer_to_map_id: deferToMapId, desc: desc || '', - link: link || '', - calculated_permission: Active.Map.get('permission') + link: link || '' }) DataModel.Topics.add(topic) diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index b7b58821..d2feb03a 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -174,7 +174,7 @@ const SynapseCard = { add_perms_form: function(synapse) { // permissions - if owner, also allow permission editing - $('#editSynLowerBar').append('
    ') + $('#editSynLowerBar').append('
    ') // ability to change permission var selectingPermission = false diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 71140bdd..3fa9a999 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -448,8 +448,8 @@ const TopicCard = { nodeValues.inmaps += '
  • ' + inmapsAr[i] + '
  • ' } } - nodeValues.permission = topic.get('calculated_permission') - nodeValues.mk_permission = topic.get('calculated_permission').substring(0, 2) + nodeValues.permission = topic.get('permission') + nodeValues.mk_permission = topic.get('permission').substring(0, 2) nodeValues.map_count = topic.get('map_count').toString() nodeValues.synapse_count = topic.get('synapse_count').toString() nodeValues.id = topic.isNew() ? topic.cid : topic.id From d6527ea80e0bcd6ea0e60e734e4340f7c76e38d1 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Fri, 9 Dec 2016 12:20:30 -0500 Subject: [PATCH 342/378] Create ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..f13def73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,7 @@ + + + + + +============ +100BD/C = 100*__*__/__ = ?? From 1317186f6379929f1f5135c29a9e786049e77d93 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Fri, 9 Dec 2016 13:40:58 -0500 Subject: [PATCH 343/378] Update ISSUE_TEMPLATE.md changed all place-holders to underscores, they ae easier to double-click so as to select/replace. --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f13def73..85ad4fb2 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,4 +4,4 @@ ============ -100BD/C = 100*__*__/__ = ?? +100BD/C = 100*__*__/__ = __ From d51e3f3b52162fdebb5752139a63e2fbbf53b7fe Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 11 Dec 2016 16:09:12 -0500 Subject: [PATCH 344/378] update npm deps, EXCEPT socket.io and backbone (#950) * update npm dependencies (with some exceptions) * update autolinker, remove underscore --- frontend/src/Metamaps/Views/ChatView.js | 16 ++++---- frontend/src/index.js | 2 +- package.json | 51 ++++++++++++------------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index fd77864f..590dd775 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -3,13 +3,12 @@ import Backbone from 'backbone' import { Howl } from 'howler' import Autolinker from 'autolinker' -import _ from 'lodash' -import underscore from 'underscore' +import { clone, template as lodashTemplate } from 'lodash' import outdent from 'outdent' // TODO is this line good or bad // Backbone.$ = window.$ -const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) var Private = { messageHTML: outdent` @@ -41,12 +40,13 @@ var Private = {
    `, templates: function() { - underscore.templateSettings = { + const templateSettings = { interpolate: /\{\{(.+?)\}\}/g } - this.messageTemplate = underscore.template(Private.messageHTML) - this.participantTemplate = underscore.template(Private.participantHTML) + this.messageTemplate = lodashTemplate(Private.messageHTML, templateSettings) + + this.participantTemplate = lodashTemplate(Private.participantHTML, templateSettings) }, createElements: function() { this.$unread = $('
    ') @@ -147,7 +147,7 @@ var Private = { } return i } - var m = _.clone(message.attributes) + var m = clone(message.attributes) m.timestamp = new Date(m.created_at) @@ -176,7 +176,7 @@ var Private = { $(document).trigger(ChatView.events.message + '-' + this.room, [message]) }, addParticipant: function(participant) { - var p = _.clone(participant.attributes) + var p = clone(participant.attributes) if (p.self) { p.selfClass = 'is-self' p.selfName = '(me)' diff --git a/frontend/src/index.js b/frontend/src/index.js index 67f69141..1d82af7c 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,5 +1,5 @@ // create global references -import _ from 'underscore' +import _ from 'lodash' import Metamaps from './Metamaps' window._ = _ diff --git a/package.json b/package.json index e33a6e0a..92d4ab25 100644 --- a/package.json +++ b/package.json @@ -18,46 +18,45 @@ }, "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { - "attachmediastream": "1.4.1", - "autolinker": "0.17.1", - "babel-cli": "6.14.0", - "babel-loader": "6.2.5", - "babel-plugin-lodash": "^3.2.9", - "babel-plugin-transform-class-properties": "6.11.5", - "babel-preset-es2015": "6.14.0", - "babel-preset-react": "6.11.1", + "attachmediastream": "1.4.2", + "autolinker": "1.4.0", + "babel-cli": "6.18.0", + "babel-loader": "6.2.9", + "babel-plugin-lodash": "3.2.10", + "babel-plugin-transform-class-properties": "6.19.0", + "babel-preset-es2015": "6.18.0", + "babel-preset-react": "6.16.0", "backbone": "1.0.0", "clipboard-js": "git://github.com/devvmh/clipboard.js#patch-1", - "commonmark": "0.26.0", + "commonmark": "0.27.0", "csv-parse": "1.1.7", "getScreenMedia": "git://github.com/devvmh/getScreenMedia#patch-1", "hark": "git://github.com/devvmh/hark#patch-1", - "howler": "2.0.1", + "howler": "2.0.2", "json-loader": "0.5.4", - "lodash": "4.16.1", + "lodash": "4.17.2", "node-uuid": "1.4.7", - "outdent": "0.2.1", - "react": "15.3.2", - "react-dom": "15.3.2", - "react-dropzone": "3.6.0", - "redux": "^3.6.0", - "simplewebrtc": "2.2.0", + "outdent": "0.3.0", + "react": "15.4.1", + "react-dom": "15.4.1", + "react-dropzone": "3.7.3", + "redux": "3.6.0", + "simplewebrtc": "2.2.1", "socket.io": "1.3.7", - "underscore": "1.4.4", - "webpack": "1.13.2" + "webpack": "1.14.0" }, "devDependencies": { - "babel-eslint": "^6.1.2", + "babel-eslint": "^7.1.1", "chai": "^3.5.0", "circular-dependency-plugin": "^2.0.0", - "eslint": "^3.5.0", - "eslint-config-standard": "^6.2.0", - "eslint-plugin-promise": "^2.0.1", - "eslint-plugin-react": "^6.3.0", + "eslint": "^3.11.1", + "eslint-config-standard": "^6.2.1", + "eslint-plugin-promise": "^3.4.0", + "eslint-plugin-react": "^6.8.0", "eslint-plugin-standard": "^2.0.1", - "mocha": "^3.0.2" + "mocha": "^3.2.0" }, "optionalDependencies": { - "raml2html": "4.0.0-beta5" + "raml2html": "4.0.1" } } From 6129a27ecfd7ca192b0f3507fbeeb50f4ae23fe4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 11 Dec 2016 16:21:36 -0500 Subject: [PATCH 345/378] hit Ctrl+A a second time to select all synapses, too (#968) --- frontend/src/Metamaps/Listeners.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 470e8cbe..756227b1 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -2,6 +2,7 @@ import Active from './Active' import Control from './Control' +import DataModel from './DataModel' import JIT from './JIT' import Mobile from './Mobile' import Realtime from './Realtime' @@ -31,14 +32,27 @@ const Listeners = { break case 65: // if a or A is pressed if ((e.ctrlKey || e.metaKey) && onCanvas) { - Control.deselectAllNodes() - Control.deselectAllEdges() - + const nodesCount = Object.keys(Visualize.mGraph.graph.nodes).length + const selectedNodesCount = Selected.Nodes.length e.preventDefault() - Visualize.mGraph.graph.eachNode(function(n) { - Control.selectNode(n, e) + + // Hit Ctrl+A once to select all nodes + Control.deselectAllNodes() + Visualize.mGraph.graph.eachNode(node => { + Control.selectNode(node, e) }) + // Hitting Ctrl+A a second time will select all edges too + Control.deselectAllEdges() + if (nodesCount === selectedNodesCount) { + DataModel.Synapses.models.forEach(synapse => { + const topic1id = synapse.get('topic1_id') + const topic2id = synapse.get('topic2_id') + const edge = Visualize.mGraph.graph.edges[topic1id][topic2id] + Control.selectEdge(edge, e) + }) + } + Visualize.mGraph.plot() } From 1ba339b3be7c1f83ed2b7a3261c5bb34aeb5b234 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 11 Dec 2016 17:15:09 -0500 Subject: [PATCH 346/378] subset of synapse creation changes (#970) * esc cancels topic and synapse creation now * close topic/synapse creation on right click * backspace and delete don't close synapse creation anymore * hitting tab saves the synapse you're creating --- frontend/src/Metamaps/Create.js | 24 ++++++++++++++++++------ frontend/src/Metamaps/JIT.js | 6 +++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 7c3c4ff3..177e951f 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -140,7 +140,13 @@ const Create = { }, newTopic: { init: function() { - $('#topic_name').keyup(function() { + $('#topic_name').keyup(function(e) { + const ESC = 27 + + if (e.keyCode === ESC) { + Create.newTopic.hide() + } // if + Create.newTopic.name = $(this).val() }) @@ -301,13 +307,11 @@ const Create = { $('#synapse_desc').keyup(function(e) { const ESC = 27 - const BACKSPACE = 8 - const DELETE = 46 - if (e.keyCode === BACKSPACE && $(this).val() === '' || - e.keyCode === DELETE && $(this).val() === '' || - e.keyCode === ESC) { + + if (e.keyCode === ESC) { Create.newSynapse.hide() } // if + Create.newSynapse.description = $(this).val() }) @@ -317,6 +321,14 @@ const Create = { } }) + $('#synapse_desc').keydown(function(e) { + const TAB = 9 + if (Create.newSynapse.beingCreated && e.keyCode === TAB) { + e.preventDefault() + Synapse.createSynapseLocally() + } + }) + $('#synapse_desc').bind('typeahead:select', function(event, datum, dataset) { if (datum.id) { // if they clicked on an existing synapse get it Synapse.getSynapseFromAutocomplete(datum.id) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 3520b946..903cc11d 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -420,6 +420,8 @@ const JIT = { $('.rightclickmenu').remove() if (Mouse.boxStartCoordinates) { + Create.newSynapse.hide() + Create.newTopic.hide() Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) @@ -434,7 +436,9 @@ const JIT = { } else if (node && !node.nodeFrom) { JIT.selectNodeOnRightClickHandler(node, e) } else { - // console.log('right clicked on open space') + // right click open space + Create.newSynapse.hide() + Create.newTopic.hide() } } }, From 7c0e0f731ff266dab50ac40b9ea1bf9144d0a1d8 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Mon, 12 Dec 2016 11:35:59 -0500 Subject: [PATCH 347/378] Update ISSUE_TEMPLATE.md changed multiplication sign from asterisks to x, because markdown treats asterisks as special. --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 85ad4fb2..bdb9f4ef 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,4 +4,4 @@ ============ -100BD/C = 100*__*__/__ = __ +100BD/C = 100x__x__/__ = __ From 0c521880142b70bec83c820f9df087cd7c3d66e3 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Mon, 12 Dec 2016 13:30:56 -0500 Subject: [PATCH 348/378] Update ISSUE_TEMPLATE.md reduced whitespace --- .github/ISSUE_TEMPLATE.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index bdb9f4ef..3cc318f9 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,5 @@ - - ============ 100BD/C = 100x__x__/__ = __ From f1e62fb6c1233b55e6620d5c32fa71a9038032c1 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Mon, 12 Dec 2016 13:47:00 -0500 Subject: [PATCH 349/378] Update ISSUE_TEMPLATE.md changed multiplication symbol so that double-clicking number placeholders works again. --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3cc318f9..01c9f67e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,4 +2,4 @@ ============ -100BD/C = 100x__x__/__ = __ +100BD/C = 100·__·__/__ = __ From 6f88c2a7ebafa2a4836a34c96006ff1a30c6e4c9 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Mon, 12 Dec 2016 13:49:26 -0500 Subject: [PATCH 350/378] Update ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 01c9f67e..dddeed14 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -2,4 +2,4 @@ ============ -100BD/C = 100·__·__/__ = __ +100BD/C = (100)(__)(__)/(__)=__ From 3b8a5d0c2e15a3d026c12d65d5d8312920950273 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 14 Dec 2016 13:23:40 -0500 Subject: [PATCH 351/378] Update message_policy.rb (#973) --- app/policies/message_policy.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index c32e29ed..8a140788 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -17,7 +17,8 @@ class MessagePolicy < ApplicationPolicy delegate :show?, to: :resource_policy def create? - record.resource.present? && resource_policy.update? + # we have currently decided to let any map that is visible to someone be commented on by them + record.resource.present? && resource_policy.show? end def update? From 85408a14d3267c1233a67cde5a0d5c1b33f88044 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 1 Nov 2016 11:18:27 +0800 Subject: [PATCH 352/378] Initial notification centre using mailboxer --- .codeclimate.yml | 2 + Gemfile | 1 + Gemfile.lock | 10 ++ app/assets/images/.DS_Store | Bin 16388 -> 0 bytes app/assets/images/topright_sprite.png | Bin 1646 -> 3445 bytes app/assets/images/user_sprite.png | Bin 1936 -> 2706 bytes app/assets/stylesheets/application.scss.erb | 6 +- app/assets/stylesheets/apps.css.erb | 8 ++ app/assets/stylesheets/clean.css.erb | 14 ++- app/assets/stylesheets/mobile.scss.erb | 28 ++++- app/assets/stylesheets/notifications.scss.erb | 71 +++++++++++++ app/controllers/access_controller.rb | 16 +-- app/controllers/notifications_controller.rb | 97 ++++++++++++++++++ app/controllers/users/sessions_controller.rb | 29 ++++-- app/controllers/users_controller.rb | 7 +- app/helpers/application_helper.rb | 16 +++ app/mailers/application_mailer.rb | 4 + app/models/user.rb | 17 +++ app/views/layouts/_account.html.erb | 8 +- app/views/layouts/_mobilemenu.html.erb | 12 ++- app/views/layouts/_upperelements.html.erb | 11 ++ app/views/layouts/application.html.erb | 6 +- app/views/layouts/doorkeeper.html.erb | 1 + .../message_mailer/new_message_email.html.erb | 20 ++++ .../message_mailer/new_message_email.text.erb | 10 ++ .../reply_message_email.html.erb | 20 ++++ .../reply_message_email.text.erb | 10 ++ .../new_notification_email.html.erb | 10 ++ .../new_notification_email.text.erb | 2 + .../map_mailer/access_request_email.html.erb | 27 ++--- .../map_mailer/access_request_email.text.erb | 2 +- .../map_mailer/invite_to_edit_email.html.erb | 29 ++---- .../map_mailer/invite_to_edit_email.text.erb | 2 +- app/views/notifications/_header.html.erb | 18 ++++ app/views/notifications/index.html.erb | 33 ++++++ app/views/notifications/mark_read.js.erb | 6 ++ app/views/notifications/mark_unread.js.erb | 6 ++ app/views/notifications/show.html.erb | 15 +++ app/views/shared/_back_to_mapping.html.erb | 3 + .../shared/_mailer_unsubscribe_link.html.erb | 3 + .../shared/_mailer_unsubscribe_link.text.erb | 5 + app/views/users/edit.html.erb | 38 ++++--- config/application.rb | 11 +- config/environments/development.rb | 14 +-- config/initializers/mailboxer.rb | 21 ++++ config/routes.rb | 35 +++++-- ...31231_create_mailboxer.mailboxer_engine.rb | 65 ++++++++++++ ...dd_conversation_optout.mailboxer_engine.rb | 15 +++ ...33_add_missing_indices.mailboxer_engine.rb | 20 ++++ ..._to_mailboxer_receipts.mailboxer_engine.rb | 8 ++ ...61125175229_add_emails_allowed_to_users.rb | 5 + db/schema.rb | 63 +++++++++++- doc/metamaps-qa-steps.md | 7 ++ 53 files changed, 778 insertions(+), 109 deletions(-) delete mode 100644 app/assets/images/.DS_Store mode change 100755 => 100644 app/assets/images/user_sprite.png create mode 100644 app/assets/stylesheets/notifications.scss.erb create mode 100644 app/controllers/notifications_controller.rb create mode 100644 app/views/mailboxer/message_mailer/new_message_email.html.erb create mode 100644 app/views/mailboxer/message_mailer/new_message_email.text.erb create mode 100644 app/views/mailboxer/message_mailer/reply_message_email.html.erb create mode 100644 app/views/mailboxer/message_mailer/reply_message_email.text.erb create mode 100644 app/views/mailboxer/notification_mailer/new_notification_email.html.erb create mode 100644 app/views/mailboxer/notification_mailer/new_notification_email.text.erb create mode 100644 app/views/notifications/_header.html.erb create mode 100644 app/views/notifications/index.html.erb create mode 100644 app/views/notifications/mark_read.js.erb create mode 100644 app/views/notifications/mark_unread.js.erb create mode 100644 app/views/notifications/show.html.erb create mode 100644 app/views/shared/_back_to_mapping.html.erb create mode 100644 app/views/shared/_mailer_unsubscribe_link.html.erb create mode 100644 app/views/shared/_mailer_unsubscribe_link.text.erb create mode 100644 config/initializers/mailboxer.rb create mode 100644 db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb create mode 100644 db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb create mode 100644 db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb create mode 100644 db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb create mode 100644 db/migrate/20161125175229_add_emails_allowed_to_users.rb diff --git a/.codeclimate.yml b/.codeclimate.yml index 719b2807..a187069d 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -20,6 +20,8 @@ engines: enabled: true rubocop: enabled: true + exclude_fingerprints: + - 74f18007b920e8d81148d2f6a2756534 ratings: paths: - 'Gemfile.lock' diff --git a/Gemfile b/Gemfile index 9fd59b62..0d8e8d7a 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,7 @@ gem 'exception_notification' gem 'httparty' gem 'json' gem 'kaminari' +gem 'mailboxer' gem 'paperclip' gem 'pg' gem 'pundit' diff --git a/Gemfile.lock b/Gemfile.lock index 92215068..d104cb51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,12 @@ GEM brakeman (3.4.0) builder (3.2.2) byebug (9.0.5) + carrierwave (0.11.2) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + json (>= 1.7) + mime-types (>= 1.16) + mimemagic (>= 0.3.0) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.8) @@ -125,6 +131,9 @@ GEM nokogiri (>= 1.5.9) mail (2.6.4) mime-types (>= 1.16, < 4) + mailboxer (0.14.0) + carrierwave (>= 0.5.8) + rails (>= 4.2.0) method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) @@ -284,6 +293,7 @@ DEPENDENCIES json json-schema kaminari + mailboxer paperclip pg pry-byebug diff --git a/app/assets/images/.DS_Store b/app/assets/images/.DS_Store deleted file mode 100644 index 2bbe5f6a30a17587916373a8968facc58339b0db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16388 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 diff --git a/app/assets/images/topright_sprite.png b/app/assets/images/topright_sprite.png index 163dd6f7330b3323bf0e6d498a0c270a28cafc5c..4c969887dd42f30ac81eab16de5969d56c4dd85e 100644 GIT binary patch literal 3445 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 X&6~pKtb@=s00000NkvXXu0mjfF#c`> literal 1646 zcmV-!29f!RP)B&0pP<309a}OunIn42_+1B$_$qpbd%>w2k)i-B~J#( z`n%+SfacYW_m`B{0l+aJb^#z48+?-fuL8h7(tl@Z`WAp~iU@_E-{0T&lFbg_^6y;; z5R^U=%BfgB9!q7$chdiaz@y*Z-hPqC{*(SIhyN7-NVOnTL%&ahPe8Db0aOVM@N-?) zEd>C&d~YTFr(Cpex<-t{xqPonX%Z?5_^#3wFD_aga&?}S@9Nisk1pR^PYnQA{Xg0! zTfYB?(g&%l=#bN)ng|7eCY*YpQ(9$&IxvF=0K^ag?(XiIh;&lrpnbHDK9em!_nY(| z5*c8hkpcEG3}8~vSY<>6zju|e7%-rF69AMJ5R>J$o12@@zLPFL{!6L?&H(~AM+o{M zXKTJ606{zCv>6q%R{>OSp#Kd5L%wcLqQun#UZf040YPZ+Efm`8Io&46Pz0(jWgU6l zS`Ri{Qyyy(2mlH^zbY9!_&hj#u6W5@8UQIE$Ps)B0=lX(xR!Z&@fCfJrWUOJ)&vd+ zKB*2i11Y0y-((B`#DE}Y@aa7m(m`8r%(YASseZwBNHh)sXm$Vy0YQo26Jfg2#a*du zVKv|qeYUCrQ^;8Wzy|~+gHP!_L=P|kMgqYY!6)PHL+krc1L{`*SUPkt=pdAr}wXO%~)^jEScnqwqRD`|&3PB41#MuErZE8#gfFV7AAW)k@JDIGB*?6SfDlRf~HeF7E#P;l{cXSwQ65dew_ zHXdzYuWq1iBZyFaJIZe&a&N}u6pE(=0Bj%o?guyku(64`QGX1>0EntiI@?a5t1awk z>a=5{ngI~mHr6zNw-|IVz?uOt0{~zZ5V;E|EZe{r|341js9i%d=wy^Hl!ytx2Q*ay zOO84l8yg!N8ylN6Aw+$3b+wbemw)r~0WQt1udiF_2Xvm1%VU5BgAXh~jxXi0PQGrW zS3FNb=yNpJLA3H^O^#KH1t8dObuy#{1O|YB0q_cd*|H?$dP?;$Alv5w=?sAJ0Ptai z0W38DG>*@&0R+-}$_$qpbd%>w2gm0FC}b6VJ^;|Xy7B&!^14u#8vwc3;P`w1buxZF zz_)2=n<7Gixu}*=rS2-=`<3|L8k#aC|<1Be!IEK0wCcTzYf;9uNfj=d|zW0RQ5Q z@57_*yFCj4_<*2f@F~5A=m7@6NFW#^_(bIOr6(C%xHb`K&;q93!TIfony9#Ft~2Cb zLz%Xr_m^RDwYG&ts{vE%xO9q|8USKS09f?E2FpN|8breRHzok69>Gp;E)Aq6u{cLN z1prW**2_aJMRXw=L2m-#?;5ng8#{uz+rA8dfR*Y|nKY5(nx++;dow1dP&_37(0uK) z9}o+s(Zw7CAgXcG*~&3jTbQ(v50j6N0g%)-?qkH5g8|kIfEfUQQ~@!&fI~fwe!=Ff s!^H{R-zxF9BNiYcXrh^PTzo~Ef^P(DV) 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 delta 1921 zcmV-{2Y&dH6_5{*B!3BTNLh0L01FcU01FcV0GgZ_0000PbVXQnQ*UN;cVTj606}DL zVr3vnZDD6+Qe|Oed2z{QJOBU(9Z5t%RCwC#T|I6iHxwQVDpEzdfpr7fC$Iz`z}_;| zj!&?1<1UGebgl$!`p73(r49^_Zy?=3?zRX52}l{hdC&f&P=7Rs!+#+wK7f!%qv1z> zeDCp*L()etFE7O*ORG3=$pmBq4qV!GH*Rijy7W;|n%~{sm5Tc(^f7I?&(DwO{?xW$ z=jZ2J{D+5!QgMHauetwPL#cFZoFw?!1T^>Nl!kjOTN3f`1Fr#dXR^aR(pQ$w!0#3n!PIpa%h)h^j7%n!))@9q%_u68xk5ax|MjCLj}# z3CILw0x|)IDm!BV8s+f_i>H%jsRuYLG|JoJ`wVbw5U%tbpGf=}Sl zRHq@IXQ*oh|FH__6Rh!V7E&~h24NChhzpCd^DuTC!c<@qPWUFKV$uzku*n}N-9q&i z4bSlEtuL56^c81(%pIC{gEhkH^eC~rLSeq6^p(>0Ad|bWEl4tD&iFc?U_2v)cY@Qxej@UT2p9zL1X5gX$O#w<>w=@Vrff* zzCdt{R`{|n@LC^$5#m@NWQsw|XTeE*m4cdJ1Ym?X76`G%4>lo!1=b&7Z+{8>@}E*r z6KfRdgdcK7h;8#8>bOzKNwK?LXA*HL!3jUgt1;3B&h(zQ($L?mqCao06HThT;(zm2 z8v1*Sui9GEj!^lo!r86SLMbZWRXDp^{NMzjuNT3Wp<)0-$Ed^)76^T7lHUtt0x|)a zfJ{IpAQO-Y$OL2pG69)@gOs}S19(!g9~kSIhQvL?F?|$#8WQ(RHJ~4X>|@Lunn)CU z%o`dJa9V`j2gG@uTC60Ag0EUhQh!GPGc^cQh&A%E*-V)Xzm9<4inlp5wwZ+o_yT#1 zkf*66!`ICTZdPeAdzFJ-F7T{s#g~aB!EYH9Vv3AxHbCEdRz4ZN$*k}NT3{fxj1GL{ z*lNk~O#`7mGUSY}HNU$JT>0quEz3g8k@=P4{uR_UbmgPs+lIo&0N5OJwSPvs!gupX zm5+w+G8X8d5d0R1x6O8imgyAnWs6UN4R4$E!f$&N;*mnz%_@!5TWDYLDYW&*_kB0G z&%MJz@bi3tCVmbr>qx)#$}_(m&L)rv$OL2pG69)@Oh6_e6OakW1RSJz%?Aj3i_4fh z%IiefTU@&CDEHYl)+=_;_J16vJbzemw&>91{9zvx=oM+^0Hfl!4O)7$7nfyea*_m| z;rV&~t=D#XtpDHQo17$pXSVpdt+OT?z&7@XR2r6>ZIgFt@C(Tbgt&?~J_xZVfC|nm zUN{t5JnphzI3B4-1&@y3YEa{`^tGvz;ev~Ea2K{`382-2;n$rdpnubVQgSl4rd}R? zqEzH(Bk%A{y*&Jc5q=W^8(}uBw!9%Oc&brLQv#gv5f?P#>j)6vvAKG3(e^qPi=^J+ zJN}269ekTbQnI)nQzLg-uDwAt{FWxlYbwC10n9hbOM>6hI5ANMir;Xdeis~MFai|8 z&Oq^-9emfLwseu;T7U4uCpAPhIkroWR%*njN_*q$d_X1j<2}p4QdcU#v!#|)Qa|3a zG^2O;DlO3wFw=Nc(_rM-^1SAYQknQ3`jEfnNX00000NkvXX Hu0mjfD); + background-image: url(<%= asset_path('user_sprite.png') %>); } .accountSettings .accountIcon { background-position: 0 0; @@ -3076,3 +3076,7 @@ script.data-gratipay-username { display: inline; float: left; } + +.inline { + display: inline-block; +} diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb index e6d75dd7..a88199af 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -129,3 +129,11 @@ box-sizing: border-box; border-radius: 2px; } + +.back-to-mapping { + margin: 1em; + width: auto; + max-width: 100%; + box-sizing: border-box; +} + diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index e4da394b..b25816f0 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -210,7 +210,10 @@ } .addMap { background-position: -96px 0; - margin-right:10px; +} +.notificationsIcon { + background-position: -128px 0; + margin-right: 10px; // make it look more natural next to the account menu icon } .importDialog:hover { background-position: 0 -32px; @@ -758,7 +761,7 @@ } .exploreMapsCenter .authedApps .exploreMapsIcon { - background-image: url(<%= asset_data_uri('user_sprite.png') %>); + background-image: url(<%= asset_path('user_sprite.png') %>); background-position: 0 -32px; } .exploreMapsCenter .myMaps .exploreMapsIcon { @@ -781,6 +784,10 @@ background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } +.exploreMapsCenter .notificationsLink .exploreMapsIcon { + background-image: url(<%= asset_path 'user_sprite.png' %>); + background-position: 0 -128px; +} .authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon { background-position-x: -32px; } @@ -799,6 +806,9 @@ .sharedMaps:hover .exploreMapsIcon, .sharedMaps.active .exploreMapsIcon { background-position: -128px -32px; } +.notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon { + background-position-x: -32px; +} .mapsWrapper { /*overflow-y: auto; */ diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 01fa5f61..e7eb9a7d 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -213,8 +213,17 @@ line-height: 50px; } +#mobile_header #menu_icon .unread-notifications-dot { + top: 5px; + left: 29px; + width: 12px; + height: 12px; + border: 3px solid #eee; + border-radius: 9px; +} + #mobile_menu { - display: none; + display: none; background: #EEE; position: fixed; top: 50px; @@ -222,11 +231,20 @@ padding: 10px; width: 200px; box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16); -} -#mobile_menu li { - padding: 10px; - list-style: none; + li { + padding: 10px; + list-style: none; + + &.notifications { + position: relative; + + .unread-notifications-dot { + top: 17px; + left: 0px; + } + } + } } /* diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb new file mode 100644 index 00000000..b25bf4e8 --- /dev/null +++ b/app/assets/stylesheets/notifications.scss.erb @@ -0,0 +1,71 @@ +$unread_notifications_dot_size: 8px; +.unread-notifications-dot { + width: $unread_notifications_dot_size; + height: $unread_notifications_dot_size; + background-color: #e22; + border-radius: $unread_notifications_dot_size / 2; + position: absolute; + top: 0; + right: 0; +} + +.upperRightUI { + .notificationsIcon { + position: relative; + } +} + +.controller-notifications { + ul.notifications { + list-style: none; + } + + $menu_bar_height: 6em; + .notificationPage, + .notificationsPage { + width: auto; + max-width: 100%; + box-sizing: border-box; + margin: 1em; + margin-top: 1em + $menu_bar_height; + + & > .title { + border-bottom: 1px solid #eee; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + } + + .back { + margin-top: 1em; + } + } + + + .notification { + .notification-subject { + width: 25%; + } + .notification-body { + width: 50%; + } + .notification-read-unread { + width: 10%; + } + + .notification-body, + .notification-subject, + .notification-read-unread { + display: inline-block; + vertical-align: top; + font-weight: 300; + } + + &.unread { + .notification-body, + .notification-subject, + .notification-read-unread { + font-weight: bold; + } + } + } +} diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index c48ac418..a0271981 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -21,7 +21,8 @@ class AccessController < ApplicationController def access_request request = AccessRequest.create(user: current_user, map: @map) # what about push notification to map owner? - MapMailer.access_request_email(request, @map).deliver_later + mail = MapMailer.access_request_email(request, @map) + @map.user.notify(mail.subject, mail.body) respond_to do |format| format.json do @@ -37,7 +38,9 @@ class AccessController < ApplicationController @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, # who we then send an email to - MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later + user = User.find(user_id) + mail = MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)) + user.notify(mail.subject, mail.body) end @map.remove_old_collaborators(user_ids) @@ -51,7 +54,7 @@ class AccessController < ApplicationController # GET maps/:id/approve_access/:request_id def approve_access request = AccessRequest.find(params[:request_id]) - request.approve() + request.approve respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was approved' } end @@ -60,7 +63,7 @@ class AccessController < ApplicationController # GET maps/:id/deny_access/:request_id def deny_access request = AccessRequest.find(params[:request_id]) - request.deny() + request.deny respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was turned down' } end @@ -69,7 +72,7 @@ class AccessController < ApplicationController # POST maps/:id/approve_access/:request_id def approve_access_post request = AccessRequest.find(params[:request_id]) - request.approve() + request.approve respond_to do |format| format.json do head :ok @@ -80,7 +83,7 @@ class AccessController < ApplicationController # POST maps/:id/deny_access/:request_id def deny_access_post request = AccessRequest.find(params[:request_id]) - request.deny() + request.deny respond_to do |format| format.json do head :ok @@ -94,5 +97,4 @@ class AccessController < ApplicationController @map = Map.find(params[:id]) authorize @map end - end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 00000000..4759ef20 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true +class NotificationsController < ApplicationController + before_action :set_receipts, only: [:index, :show, :mark_read, :mark_unread] + before_action :set_notification, only: [:show, :mark_read, :mark_unread] + before_action :set_receipt, only: [:show, :mark_read, :mark_unread] + + def index + @notifications = current_user.mailbox.notifications + + respond_to do |format| + format.html + format.json do + render json: @notifications.map do |notification| + receipt = @receipts.find_by(notification_id: notification.id) + notification.as_json.merge(is_read: receipt.is_read) + end + end + end + end + + def show + @receipt.update(is_read: true) + respond_to do |format| + format.html + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def mark_read + @receipt.update(is_read: true) + respond_to do |format| + format.js + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def mark_unread + @receipt.update(is_read: false) + respond_to do |format| + format.js + format.json do + render json: @notification.as_json.merge( + is_read: @receipt.is_read + ) + end + end + end + + def unsubscribe + unsubscribe_redirect_if_logged_out! + check_if_already_unsubscribed! + return if performed? # if one of these checks already redirected, we're done + + if current_user.update(emails_allowed: false) + redirect_to edit_user_path(current_user), + notice: 'You will no longer receive emails from Metamaps.' + else + flash[:alert] = 'Sorry, something went wrong. You have not been unsubscribed from emails.' + redirect_to edit_user_path(current_user) + end + end + + private + + def unsubscribe_redirect_if_logged_out! + return if current_user.present? + + flash[:notice] = 'Continue to unsubscribe from emails by logging in.' + redirect_to "#{sign_in_path}?redirect_to=#{unsubscribe_notifications_path}" + end + + def check_if_already_unsubscribed! + return if current_user.emails_allowed + + redirect_to edit_user_path(current_user), notice: 'You were already unsubscribed from emails.' + end + + def set_receipts + @receipts = current_user.mailboxer_notification_receipts + end + + def set_notification + @notification = current_user.mailbox.notifications.find_by(id: params[:id]) + end + + def set_receipt + @receipt = @receipts.find_by(notification_id: params[:id]) + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index fed670ae..1c9c0a1e 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,14 +1,25 @@ -class Users::SessionsController < Devise::SessionsController - protected +# frozen_string_literal: true +module Users + class SessionsController < Devise::SessionsController + after_action :store_location, only: [:new] - def after_sign_in_path_for(resource) - stored = stored_location_for(User) - return stored if stored + protected - if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url) - super - else - request.referer || root_path + def after_sign_in_path_for(resource) + stored = stored_location_for(User) + return stored if stored + + if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url) + super + else + request.referer || root_path + end + end + + private + + def store_location + store_location_for(User, params[:redirect_to]) if params[:redirect_to] end end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a9fff9de..6b771d6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,13 +13,12 @@ class UsersController < ApplicationController # GET /users/:id/edit def edit - @user = current_user - respond_with(@user) + @user = User.find(current_user.id) end # PUT /users/:id def update - @user = current_user + @user = User.find(current_user.id) if user_params[:password] == '' && user_params[:password_confirmation] == '' # not trying to change the password @@ -96,6 +95,6 @@ class UsersController < ApplicationController private def user_params - params.require(:user).permit(:name, :email, :image, :password, :password_confirmation) + params.require(:user).permit(:name, :email, :image, :password, :password_confirmation, :emails_allowed) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3221aa34..cc121cbe 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,4 +37,20 @@ module ApplicationHelper def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end + + def user_has_unread_notifications? + return @user_has_unread_notifications unless @user_has_unread_notifications.nil? + return (@user_has_unread_notifications = false) if current_user.nil? + current_user.mailboxer_notification_receipts.each do |receipt| + return (@user_has_unread_notifications = true) if receipt.is_read == false + end + @user_has_unread_notifications = false + end + + def user_unread_notification_count + return 0 if current_user.nil? + current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| + receipt.is_read ? total : total + 1 + end + end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 59a2175a..10961836 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -2,4 +2,8 @@ class ApplicationMailer < ActionMailer::Base default from: 'team@metamaps.cc' layout 'mailer' + + def deliver + raise NotImplementedError('Please use Mailboxer to send your emails.') + end end diff --git a/app/models/user.rb b/app/models/user.rb index 23ef6440..f6fcb60e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,6 +2,8 @@ require 'open-uri' class User < ApplicationRecord + acts_as_messageable # mailboxer notifications + has_many :topics has_many :synapses has_many :maps @@ -108,4 +110,19 @@ class User < ApplicationRecord def settings=(val) self[:settings] = val end + + # Mailboxer hooks and helper functions + + def mailboxer_email(_message) + return email if emails_allowed + # else return nil, which sends no email + end + + def mailboxer_notifications + mailbox.notifications + end + + def mailboxer_notification_receipts + mailbox.receipts.includes(:notification).where(mailbox_type: nil) + end end diff --git a/app/views/layouts/_account.html.erb b/app/views/layouts/_account.html.erb index 748e5f1b..3d66f687 100644 --- a/app/views/layouts/_account.html.erb +++ b/app/views/layouts/_account.html.erb @@ -18,14 +18,14 @@ <%= link_to "Admin", metacodes_path %> <% end %> -
  • -
    - Share Invite -
  • <%= link_to "Apps", oauth_authorized_applications_path %>
  • +
  • +
    + Share Invite +
  • <%= link_to "Sign Out", "/logout", id: "Logout" %> diff --git a/app/views/layouts/_mobilemenu.html.erb b/app/views/layouts/_mobilemenu.html.erb index e012a808..c557f516 100644 --- a/app/views/layouts/_mobilemenu.html.erb +++ b/app/views/layouts/_mobilemenu.html.erb @@ -2,7 +2,11 @@
    <%= yield(:mobile_title) %>
    - +
    • @@ -49,6 +53,12 @@
    • <%= link_to "Account", edit_user_url(current_user) %>
    • +
    • + <%= link_to "Notifications", notifications_path %> + <% if user_has_unread_notifications? %> +
      + <% end %> +
    • <%= link_to "Sign Out", "/logout", id: "Logout" %>
    • diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index 63de15fd..cc484272 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -71,6 +71,17 @@ <% end %> + <% if current_user.present? %> + <%= link_to notifications_path, target: '_blank', class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %> +
      + Notifications +
      + <% if user_has_unread_notifications? %> +
      + <% end %> + <% end %> + <% end %> + <% if !(controller_name == "sessions" && action_name == "new") %>
      diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 231c76e3..d48ec0a1 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -64,9 +64,13 @@

      <% if devise_error_messages? %> <%= devise_error_messages! %> - <% elsif notice %> + <% end %> + <% if notice %> <%= notice %> <% end %> + <% if alert %> + <%= alert %> + <% end %>

      diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb index 500a0f0e..a6a74f41 100644 --- a/app/views/layouts/doorkeeper.html.erb +++ b/app/views/layouts/doorkeeper.html.erb @@ -50,6 +50,7 @@ <% end %>

      + <%= render partial: 'shared/back_to_mapping' %>
    <% end %> diff --git a/app/views/mailboxer/message_mailer/new_message_email.html.erb b/app/views/mailboxer/message_mailer/new_message_email.html.erb new file mode 100644 index 00000000..c779e888 --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

    You have a new message: <%= @subject %>

    +

    + You have received a new message: +

    +
    +

    + <%= raw @message.body %> +

    +
    +

    + Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

    + + diff --git a/app/views/mailboxer/message_mailer/new_message_email.text.erb b/app/views/mailboxer/message_mailer/new_message_email.text.erb new file mode 100644 index 00000000..228ca58a --- /dev/null +++ b/app/views/mailboxer/message_mailer/new_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new message: <%= @subject %> +=============================================== + +You have received a new message: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/message_mailer/reply_message_email.html.erb b/app/views/mailboxer/message_mailer/reply_message_email.html.erb new file mode 100644 index 00000000..fd1286c5 --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.html.erb @@ -0,0 +1,20 @@ + + + + + + +

    You have a new reply: <%= @subject %>

    +

    + You have received a new reply: +

    +
    +

    + <%= raw @message.body %> +

    +
    +

    + Visit <%= link_to root_url, root_url %> and go to your inbox for more info. +

    + + diff --git a/app/views/mailboxer/message_mailer/reply_message_email.text.erb b/app/views/mailboxer/message_mailer/reply_message_email.text.erb new file mode 100644 index 00000000..c56bfb5e --- /dev/null +++ b/app/views/mailboxer/message_mailer/reply_message_email.text.erb @@ -0,0 +1,10 @@ +You have a new reply: <%= @subject %> +=============================================== + +You have received a new reply: + +----------------------------------------------- +<%= @message.body.html_safe? ? @message.body : strip_tags(@message.body) %> +----------------------------------------------- + +Visit <%= root_url %> and go to your inbox for more info. diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb new file mode 100644 index 00000000..23ee4087 --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -0,0 +1,10 @@ + + + + + + + <% binding.pry %> + <%= raw @notification.body.parts[1].encoded %> + + diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb new file mode 100644 index 00000000..fa39d477 --- /dev/null +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -0,0 +1,2 @@ +<% body = @notification.body.parts[0].encoded %> +<%= body.html_safe? ? body : strip_tags(body) %> diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb index ae4f4018..2e89eb6e 100644 --- a/app/views/map_mailer/access_request_email.html.erb +++ b/app/views/map_mailer/access_request_email.html.erb @@ -1,23 +1,16 @@ - - - - - - +
    + <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    -

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    +

    <%= @map.name %>

    -

    <%= @map.name %>

    +

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    -

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> -

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> +

    Make sense with Metamaps

    -

    Make sense with Metamaps

    -
    - - + <%= render partial: 'shared/mailer_unsubscribe_link' %> +
    diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb index ad698d9e..25ccb2bd 100644 --- a/app/views/map_mailer/access_request_email.text.erb +++ b/app/views/map_mailer/access_request_email.text.erb @@ -7,4 +7,4 @@ Decline [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] Make sense with Metamaps - +<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index 73067c48..cd2b7b2e 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -1,22 +1,15 @@ - - - - - - +
    + <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    +

    <%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    + <% if @map.desc %> +

    <%= @map.desc %>

    + <% end %> -

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    -

    <%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    - <% if @map.desc %> -

    <%= @map.desc %>

    - <% end %> + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> +

    Make sense with Metamaps

    -

    Make sense with Metamaps

    -
    - - + <%= render partial: 'shared/mailer_unsubscribe_link' %> +
    diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 80eecfed..7d3bf397 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -4,4 +4,4 @@ Make sense with Metamaps - +<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/notifications/_header.html.erb b/app/views/notifications/_header.html.erb new file mode 100644 index 00000000..f93f46a6 --- /dev/null +++ b/app/views/notifications/_header.html.erb @@ -0,0 +1,18 @@ +
    + +
    +

    + <% if devise_error_messages? %> + <%= devise_error_messages! %> + <% elsif notice %> + <%= notice %> + <% end %> +

    diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb new file mode 100644 index 00000000..59ac08a3 --- /dev/null +++ b/app/views/notifications/index.html.erb @@ -0,0 +1,33 @@ +<% content_for :title, 'Notifications | Metamaps' %> +<% content_for :mobile_title, 'Notifications' %> + +
    +
    +

    My Notifications

    +
      + <% @notifications.each do |notification| %> + <% receipt = @receipts.find_by(notification_id: notification.id) %> +
    • + <%= link_to notification_path(notification.id) do %> +
      + <%= notification.subject %> +
      +
      + <%= notification.body.truncate(70) %> +
      + <% end %> +
      + <% if receipt.is_read? %> + <%= link_to 'mark as unread', mark_unread_notification_path(notification.id), remote: true, method: :put %> + <% else %> + <%= link_to 'mark as read', mark_read_notification_path(notification.id), remote: true, method: :put %> + <% end %> +
      +
    • + <% end %> +
    +
    + <%= render partial: 'shared/back_to_mapping' %> +
    + +<%= render partial: 'notifications/header' %> diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb new file mode 100644 index 00000000..5b28f453 --- /dev/null +++ b/app/views/notifications/mark_read.js.erb @@ -0,0 +1,6 @@ +$('#notification-<%= @notification.id %> .notification-read-unread > a') + .text('mark as unread') + .attr('href', '<%= mark_unread_notification_path(@notification.id) %>') +$('#notification-<%= @notification.id %>') + .removeClass('unread') + .addClass('read') diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb new file mode 100644 index 00000000..46744388 --- /dev/null +++ b/app/views/notifications/mark_unread.js.erb @@ -0,0 +1,6 @@ +$('#notification-<%= @notification.id %> .notification-read-unread > a') + .text('mark as read') + .attr('href', '<%= mark_read_notification_path(@notification.id) %>') +$('#notification-<%= @notification.id %>') + .removeClass('read') + .addClass('unread') diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb new file mode 100644 index 00000000..be641f66 --- /dev/null +++ b/app/views/notifications/show.html.erb @@ -0,0 +1,15 @@ +<% content_for :title, 'Notifications | Metamaps' %> +<% content_for :mobile_title, 'Notifications' %> + +
    +
    +

    <%= @notification.subject %>

    + <%= @notification.body %> +
    + <%= link_to 'Back', notifications_path %> +
    +
    + <%= render partial: 'shared/back_to_mapping' %> +
    + +<%= render partial: 'notifications/header' %> diff --git a/app/views/shared/_back_to_mapping.html.erb b/app/views/shared/_back_to_mapping.html.erb new file mode 100644 index 00000000..682a71e6 --- /dev/null +++ b/app/views/shared/_back_to_mapping.html.erb @@ -0,0 +1,3 @@ + diff --git a/app/views/shared/_mailer_unsubscribe_link.html.erb b/app/views/shared/_mailer_unsubscribe_link.html.erb new file mode 100644 index 00000000..2bbe7a89 --- /dev/null +++ b/app/views/shared/_mailer_unsubscribe_link.html.erb @@ -0,0 +1,3 @@ + diff --git a/app/views/shared/_mailer_unsubscribe_link.text.erb b/app/views/shared/_mailer_unsubscribe_link.text.erb new file mode 100644 index 00000000..3a2f7d0d --- /dev/null +++ b/app/views/shared/_mailer_unsubscribe_link.text.erb @@ -0,0 +1,5 @@ + + +You can unsubscribe from all Metamaps emails by visiting the following link: + +<%= unsubscribe_notifications_url %> diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 92890a92..8427582a 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -33,23 +33,35 @@
    <%= @user.name %>
    - <%= form.label :name, "Name:", :class => "firstFieldText" %> - <%= form.text_field :name %> + <%= form.label :name, "Name:", class: 'firstFieldText' %> + <%= form.text_field :name %> +
    +
    + <%= form.label :email, "Email:", class: 'firstFieldText' %> + <%= form.email_field :email %> +
    +
    + <%= form.label :emails_allowed, class: 'firstFieldText' do %> + <%= form.check_box :emails_allowed, class: 'inline' %> + Send Metamaps notifications to my email. + <% end %>
    -
    <%= form.label :email, "Email:", :class => "firstFieldText" %> - <%= form.email_field :email %>
    Change Password
    -
    - <%= form.label :current_password, "Current Password:", :class => "firstFieldText" %> - <%= password_field_tag :current_password, params[:current_password] %> +
    + <%= form.label :current_password, "Current Password:", :class => "firstFieldText" %> + <%= password_field_tag :current_password, params[:current_password] %> +
    +
    + <%= form.label :password, "New Password:", :class => "firstFieldText" %> + <%= form.password_field :password, :autocomplete => :off%> +
    +
    + <%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %> + <%= form.password_field :password_confirmation, :autocomplete => :off%> +
    +
    Oops, don't change password
    -
    <%= form.label :password, "New Password:", :class => "firstFieldText" %> - <%= form.password_field :password, :autocomplete => :off%>
    -
    <%= form.label :password_confirmation, "Confirm New Password:", :class => "firstFieldText" %> - <%= form.password_field :password_confirmation, :autocomplete => :off%>
    -
    Oops, don't change password
    -
    <%= form.submit "Update", class: "update", onclick: "Metamaps.Account.showLoading()" %>
    diff --git a/config/application.rb b/config/application.rb index 0b98bfe8..ff5a621c 100644 --- a/config/application.rb +++ b/config/application.rb @@ -8,14 +8,15 @@ Bundler.require(*Rails.groups) module Metamaps class Application < Rails::Application - config.active_job.queue_adapter = :delayed_job - if ENV['ACTIVE_JOB_FRAMEWORK'] == 'sucker_punch' - config.active_job.queue_adapter = :sucker_punch - end - # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. + # + config.active_job.queue_adapter = if ENV['ACTIVE_JOB_FRAMEWORK'] == 'sucker_punch' + :sucker_punch + else + :delayed_job + end # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths << Rails.root.join('app', 'services') diff --git a/config/environments/development.rb b/config/environments/development.rb index 38741a18..8fef2145 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,19 +14,11 @@ Rails.application.configure do config.consider_all_requests_local = true config.action_controller.perform_caching = false - config.action_mailer.delivery_method = :smtp - config.action_mailer.smtp_settings = { - address: ENV['SMTP_SERVER'], - port: ENV['SMTP_PORT'], - user_name: ENV['SMTP_USERNAME'], - password: ENV['SMTP_PASSWORD'], - domain: ENV['SMTP_DOMAIN'], - authentication: 'plain', - enable_starttls_auto: true, - openssl_verify_mode: 'none' + config.action_mailer.delivery_method = :file + config.action_mailer.file_settings = { + location: 'tmp/mails' } config.action_mailer.default_url_options = { host: 'localhost:3000' } - # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true # Print deprecation notices to the Rails logger diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb new file mode 100644 index 00000000..9e1efe66 --- /dev/null +++ b/config/initializers/mailboxer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +Mailboxer.setup do |config| + # Configures if your application uses or not email sending for Notifications and Messages + config.uses_emails = true + + # Configures the default from for emails sent for Messages and Notifications + config.default_from = 'no-reply@metamaps.cc' + + # Configures the methods needed by mailboxer + config.email_method = :mailboxer_email + config.name_method = :name + + # Configures if you use or not a search engine and which one you are using + # Supported engines: [:solr,:sphinx] + config.search_enabled = false + config.search_engine = :solr + + # Configures maximum length of the message subject and body + config.subject_max_length = 255 + config.body_max_length = 32_000 +end diff --git a/config/routes.rb b/config/routes.rb index 8ba116a1..000784f6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,12 +20,25 @@ Metamaps::Application.routes.draw do post 'events/:event', action: :events get :contains - get :request_access, to: 'access#request_access' - get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access - get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access - post :access_request, to: 'access#access_request', default: { format: :json } - post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json } - post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json } + get :request_access, + to: 'access#request_access' + get 'approve_access/:request_id', + to: 'access#approve_access', + as: :approve_access + get 'deny_access/:request_id', + to: 'access#deny_access', + as: :deny_access + + post :access_request, + to: 'access#access_request', + default: { format: :json } + post 'approve_access/:request_id', + to: 'access#approve_access_post', + default: { format: :json } + post 'deny_access/:request_id', + to: 'access#deny_access_post', + default: { format: :json } + post :access, to: 'access#access', default: { format: :json } post :star, to: 'stars#create', default: { format: :json } @@ -36,6 +49,15 @@ Metamaps::Application.routes.draw do resources :mappings, except: [:index, :new, :edit] resources :messages, only: [:show, :create, :update, :destroy] + resources :notifications, only: [:index, :show] do + collection do + get :unsubscribe + end + member do + put :mark_read + put :mark_unread + end + end resources :metacode_sets, except: [:show] @@ -109,3 +131,4 @@ Metamaps::Application.routes.draw do get 'load_url_title' end end +# rubocop:enable Rubocop/Metrics/BlockLength diff --git a/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb b/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb new file mode 100644 index 00000000..99ed59b1 --- /dev/null +++ b/db/migrate/20161101031231_create_mailboxer.mailboxer_engine.rb @@ -0,0 +1,65 @@ +# This migration comes from mailboxer_engine (originally 20110511145103) +class CreateMailboxer < ActiveRecord::Migration + def self.up + #Tables + #Conversations + create_table :mailboxer_conversations do |t| + t.column :subject, :string, :default => "" + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false + end + #Receipts + create_table :mailboxer_receipts do |t| + t.references :receiver, :polymorphic => true + t.column :notification_id, :integer, :null => false + t.column :is_read, :boolean, :default => false + t.column :trashed, :boolean, :default => false + t.column :deleted, :boolean, :default => false + t.column :mailbox_type, :string, :limit => 25 + t.column :created_at, :datetime, :null => false + t.column :updated_at, :datetime, :null => false + end + #Notifications and Messages + create_table :mailboxer_notifications do |t| + t.column :type, :string + t.column :body, :text + t.column :subject, :string, :default => "" + t.references :sender, :polymorphic => true + t.column :conversation_id, :integer + t.column :draft, :boolean, :default => false + t.string :notification_code, :default => nil + t.references :notified_object, :polymorphic => true + t.column :attachment, :string + t.column :updated_at, :datetime, :null => false + t.column :created_at, :datetime, :null => false + t.boolean :global, default: false + t.datetime :expires + end + + #Indexes + #Conversations + #Receipts + add_index "mailboxer_receipts","notification_id" + + #Messages + add_index "mailboxer_notifications","conversation_id" + + #Foreign keys + #Conversations + #Receipts + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", :name => "receipts_on_notification_id", :column => "notification_id" + #Messages + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", :name => "notifications_on_conversation_id", :column => "conversation_id" + end + + def self.down + #Tables + remove_foreign_key "mailboxer_receipts", :name => "receipts_on_notification_id" + remove_foreign_key "mailboxer_notifications", :name => "notifications_on_conversation_id" + + #Indexes + drop_table :mailboxer_receipts + drop_table :mailboxer_conversations + drop_table :mailboxer_notifications + end +end diff --git a/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb b/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb new file mode 100644 index 00000000..c4f4555a --- /dev/null +++ b/db/migrate/20161101031232_add_conversation_optout.mailboxer_engine.rb @@ -0,0 +1,15 @@ +# This migration comes from mailboxer_engine (originally 20131206080416) +class AddConversationOptout < ActiveRecord::Migration + def self.up + create_table :mailboxer_conversation_opt_outs do |t| + t.references :unsubscriber, :polymorphic => true + t.references :conversation + end + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", :name => "mb_opt_outs_on_conversations_id", :column => "conversation_id" + end + + def self.down + remove_foreign_key "mailboxer_conversation_opt_outs", :name => "mb_opt_outs_on_conversations_id" + drop_table :mailboxer_conversation_opt_outs + end +end diff --git a/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb b/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb new file mode 100644 index 00000000..fde96718 --- /dev/null +++ b/db/migrate/20161101031233_add_missing_indices.mailboxer_engine.rb @@ -0,0 +1,20 @@ +# This migration comes from mailboxer_engine (originally 20131206080417) +class AddMissingIndices < ActiveRecord::Migration + def change + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_conversation_opt_outs, [:unsubscriber_id, :unsubscriber_type], + name: 'index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type' + add_index :mailboxer_conversation_opt_outs, :conversation_id + + add_index :mailboxer_notifications, :type + add_index :mailboxer_notifications, [:sender_id, :sender_type] + + # We'll explicitly specify its name, as the auto-generated name is too long and exceeds 63 + # characters limitation. + add_index :mailboxer_notifications, [:notified_object_id, :notified_object_type], + name: 'index_mailboxer_notifications_on_notified_object_id_and_type' + + add_index :mailboxer_receipts, [:receiver_id, :receiver_type] + end +end diff --git a/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb b/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb new file mode 100644 index 00000000..a820919e --- /dev/null +++ b/db/migrate/20161101031234_add_delivery_tracking_info_to_mailboxer_receipts.mailboxer_engine.rb @@ -0,0 +1,8 @@ +# This migration comes from mailboxer_engine (originally 20151103080417) +class AddDeliveryTrackingInfoToMailboxerReceipts < ActiveRecord::Migration + def change + add_column :mailboxer_receipts, :is_delivered, :boolean, default: false + add_column :mailboxer_receipts, :delivery_method, :string + add_column :mailboxer_receipts, :message_id, :string + end +end diff --git a/db/migrate/20161125175229_add_emails_allowed_to_users.rb b/db/migrate/20161125175229_add_emails_allowed_to_users.rb new file mode 100644 index 00000000..609e4309 --- /dev/null +++ b/db/migrate/20161125175229_add_emails_allowed_to_users.rb @@ -0,0 +1,5 @@ +class AddEmailsAllowedToUsers < ActiveRecord::Migration[5.0] + def change + add_column :users, :emails_allowed, :boolean, default: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d16d4fb9..5839929c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161105160340) do +ActiveRecord::Schema.define(version: 20161125175229) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -63,6 +63,59 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.index ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree end + create_table "mailboxer_conversation_opt_outs", force: :cascade do |t| + t.string "unsubscriber_type" + t.integer "unsubscriber_id" + t.integer "conversation_id" + t.index ["conversation_id"], name: "index_mailboxer_conversation_opt_outs_on_conversation_id", using: :btree + t.index ["unsubscriber_id", "unsubscriber_type"], name: "index_mailboxer_conversation_opt_outs_on_unsubscriber_id_type", using: :btree + end + + create_table "mailboxer_conversations", force: :cascade do |t| + t.string "subject", default: "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "mailboxer_notifications", force: :cascade do |t| + t.string "type" + t.text "body" + t.string "subject", default: "" + t.string "sender_type" + t.integer "sender_id" + t.integer "conversation_id" + t.boolean "draft", default: false + t.string "notification_code" + t.string "notified_object_type" + t.integer "notified_object_id" + t.string "attachment" + t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.boolean "global", default: false + t.datetime "expires" + t.index ["conversation_id"], name: "index_mailboxer_notifications_on_conversation_id", using: :btree + t.index ["notified_object_id", "notified_object_type"], name: "index_mailboxer_notifications_on_notified_object_id_and_type", using: :btree + t.index ["sender_id", "sender_type"], name: "index_mailboxer_notifications_on_sender_id_and_sender_type", using: :btree + t.index ["type"], name: "index_mailboxer_notifications_on_type", using: :btree + end + + create_table "mailboxer_receipts", force: :cascade do |t| + t.string "receiver_type" + t.integer "receiver_id" + t.integer "notification_id", null: false + t.boolean "is_read", default: false + t.boolean "trashed", default: false + t.boolean "deleted", default: false + t.string "mailbox_type", limit: 25 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "is_delivered", default: false + t.string "delivery_method" + t.string "message_id" + t.index ["notification_id"], name: "index_mailboxer_receipts_on_notification_id", using: :btree + t.index ["receiver_id", "receiver_type"], name: "index_mailboxer_receipts_on_receiver_id_and_receiver_type", using: :btree + end + create_table "mappings", force: :cascade do |t| t.text "category" t.integer "xloc" @@ -243,8 +296,8 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.string "password_salt", limit: 255 t.string "persistence_token", limit: 255 t.string "perishable_token", limit: 255 - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "code", limit: 8 t.string "joinedwithcode", limit: 8 t.text "settings" @@ -264,6 +317,7 @@ ActiveRecord::Schema.define(version: 20161105160340) do t.integer "image_file_size" t.datetime "image_updated_at" t.integer "generation" + t.boolean "emails_allowed", default: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end @@ -279,5 +333,8 @@ ActiveRecord::Schema.define(version: 20161105160340) do add_foreign_key "access_requests", "maps" add_foreign_key "access_requests", "users" + add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id" + add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" + add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "tokens", "users" end diff --git a/doc/metamaps-qa-steps.md b/doc/metamaps-qa-steps.md index a3f136ca..7c5c8480 100644 --- a/doc/metamaps-qa-steps.md +++ b/doc/metamaps-qa-steps.md @@ -32,6 +32,13 @@ Run these tests to be reasonably sure that your code changes haven't broken anyt - Add a number of synapses to one of your maps. Reload to see if they are still there. - Rearrange one of your maps and save the layout. Reload to see if the layout is preserved. +### Unsubscribing from Notifications + + - Log out + - Visit /notifications/unsubscribe. It should redirect you to the login page. + - Log in. + - It should redirect you to the user edit page, and you should be unsubscribed. + ### Misc - Login as admin. Change metacode sets. From c46e85529eb35425cd2a281e5f13f94d45ff38b6 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 28 Nov 2016 17:37:27 -0500 Subject: [PATCH 353/378] little style tweaks to css and content --- app/assets/stylesheets/apps.css.erb | 4 ++-- app/assets/stylesheets/clean.css.erb | 20 +++++++++++++------ app/assets/stylesheets/notifications.scss.erb | 8 ++++---- app/views/layouts/_upperelements.html.erb | 2 +- app/views/notifications/index.html.erb | 3 +++ 5 files changed, 24 insertions(+), 13 deletions(-) diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb index a88199af..f3f444c7 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -131,9 +131,9 @@ } .back-to-mapping { - margin: 1em; + margin: 1em auto; width: auto; - max-width: 100%; + max-width: 960px; box-sizing: border-box; } diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index b25816f0..8e0e7970 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -215,6 +215,9 @@ background-position: -128px 0; margin-right: 10px; // make it look more natural next to the account menu icon } +.notificationsIcon:hover { + background-position: -128px -32px; +} .importDialog:hover { background-position: 0 -32px; } @@ -226,7 +229,6 @@ } .addMap:hover { background-position: -96px -32px; - margin-right:10px; } @@ -474,7 +476,7 @@ background-position: -32px 0; } -.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, +.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .notificationsIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .importDialog:hover .tooltipsUnder, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -538,6 +540,9 @@ .sidebarFilterIcon .tooltipsUnder { margin-left: -4px; } +.notificationsIcon .tooltipsUnder { + left: -20px; +} .sidebarForkIcon .tooltipsUnder { margin-left: -34px; @@ -615,7 +620,7 @@ border-bottom: 5px solid transparent; } -.importDialog div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after { +.importDialog div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after, .notificationsIcon div:after { content: ''; position: absolute; right: 40%; @@ -629,6 +634,9 @@ .sidebarFilterIcon div:after { right: 37% !important; } +.notificationsIcon div:after { + right: 46% !important; +} .mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after, .openMetacodeSwitcher div:after, .pinCarousel div:after { content: ''; @@ -785,8 +793,8 @@ background-position: -96px 0; } .exploreMapsCenter .notificationsLink .exploreMapsIcon { - background-image: url(<%= asset_path 'user_sprite.png' %>); - background-position: 0 -128px; + background-image: url(<%= asset_path 'topright_sprite.png' %>); + background-position: -128px 0; } .authedApps:hover .exploreMapsIcon, .authedApps.active .exploreMapsIcon { background-position-x: -32px; @@ -807,7 +815,7 @@ background-position: -128px -32px; } .notificationsLink:hover .exploreMapsIcon, .notificationsLink.active .exploreMapsIcon { - background-position-x: -32px; + background-position-y: -32px; } .mapsWrapper { diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index b25bf4e8..a866af04 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -24,10 +24,11 @@ $unread_notifications_dot_size: 8px; .notificationPage, .notificationsPage { width: auto; - max-width: 100%; + max-width: 960px; box-sizing: border-box; - margin: 1em; + margin: 1em auto; margin-top: 1em + $menu_bar_height; + font-family: 'din-regular', Sans-Serif; & > .title { border-bottom: 1px solid #eee; @@ -57,14 +58,13 @@ $unread_notifications_dot_size: 8px; .notification-read-unread { display: inline-block; vertical-align: top; - font-weight: 300; } &.unread { .notification-body, .notification-subject, .notification-read-unread { - font-weight: bold; + font-family: 'din-medium', Sans-Serif; } } } diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index cc484272..8b2882ce 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -72,7 +72,7 @@ <% end %> <% if current_user.present? %> - <%= link_to notifications_path, target: '_blank', class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %> + <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %>
    Notifications
    diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 59ac08a3..eb576ce6 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -25,6 +25,9 @@
    <% end %> + <% if @notifications.count == 0 %> + You have ZERO unread notifications. Huzzah! + <% end %>
    <%= render partial: 'shared/back_to_mapping' %> From 9b95e91f1aea63d1e14254a5b89cfed045bedc10 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 29 Nov 2016 11:15:14 -0500 Subject: [PATCH 354/378] more style tweaks + brakeman fix --- app/assets/stylesheets/clean.css.erb | 10 +++++++- app/controllers/access_controller.rb | 4 ++-- .../new_notification_email.html.erb | 3 +-- .../new_notification_email.text.erb | 3 +-- app/views/notifications/index.html.erb | 2 +- app/views/notifications/show.html.erb | 2 +- config/brakeman.ignore | 24 +++++++++++++++++++ 7 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 config/brakeman.ignore diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 8e0e7970..f4352504 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -620,7 +620,12 @@ border-bottom: 5px solid transparent; } -.importDialog div:after, .sidebarFilterIcon div:after, .sidebarForkIcon div:after, .addMap div:after, .sidebarAccountIcon .tooltipsUnder:after, .notificationsIcon div:after { +.addMap div:after, +.importDialog div:after, +.sidebarForkIcon div:after, +.sidebarFilterIcon div:after, +.notificationsIcon div:after, +.sidebarAccountIcon .tooltipsUnder:after, content: ''; position: absolute; right: 40%; @@ -631,6 +636,9 @@ border-left: 5px solid transparent; border-right: 5px solid transparent; } +.notificationsIcon .unread-notifications-dot:after { + content: none; +} .sidebarFilterIcon div:after { right: 37% !important; } diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index a0271981..5f19d23e 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -22,7 +22,7 @@ class AccessController < ApplicationController request = AccessRequest.create(user: current_user, map: @map) # what about push notification to map owner? mail = MapMailer.access_request_email(request, @map) - @map.user.notify(mail.subject, mail.body) + @map.user.notify(mail.subject, mail.body.parts[1].body.to_s) respond_to do |format| format.json do @@ -40,7 +40,7 @@ class AccessController < ApplicationController # who we then send an email to user = User.find(user_id) mail = MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)) - user.notify(mail.subject, mail.body) + user.notify(mail.subject, mail.body.parts[1].body.to_s) end @map.remove_old_collaborators(user_ids) diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb index 23ee4087..ac4af493 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -4,7 +4,6 @@ - <% binding.pry %> - <%= raw @notification.body.parts[1].encoded %> + <%= raw @notification.body %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb index fa39d477..1c230d08 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -1,2 +1 @@ -<% body = @notification.body.parts[0].encoded %> -<%= body.html_safe? ? body : strip_tags(body) %> +<%= @notification.body.html_safe? ? @notification.body : strip_tags(@notification.body) %> diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index eb576ce6..bd8022e5 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -13,7 +13,7 @@ <%= notification.subject %>
    - <%= notification.body.truncate(70) %> + <%= strip_tags(notification.body).truncate(70) %>
    <% end %>
    diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb index be641f66..1d61ccc3 100644 --- a/app/views/notifications/show.html.erb +++ b/app/views/notifications/show.html.erb @@ -4,7 +4,7 @@

    <%= @notification.subject %>

    - <%= @notification.body %> + <%= raw @notification.body %>
    <%= link_to 'Back', notifications_path %>
    diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 00000000..9e29ff0d --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,24 @@ +{ + "ignored_warnings": [ + { + "warning_type": "Cross Site Scripting", + "warning_code": 2, + "fingerprint": "88694dca0bcc2226859746f9ed40cc682d6e5eaec1e73f2be557770a854ede0b", + "message": "Unescaped model attribute", + "file": "app/views/notifications/show.html.erb", + "line": 7, + "link": "http://brakemanscanner.org/docs/warning_types/cross_site_scripting", + "code": "current_user.mailbox.notifications.find_by(:id => params[:id]).body", + "render_path": [{"type":"controller","class":"NotificationsController","method":"show","line":24,"file":"app/controllers/notifications_controller.rb"}], + "location": { + "type": "template", + "template": "notifications/show" + }, + "user_input": "current_user.mailbox.notifications", + "confidence": "Weak", + "note": "" + } + ], + "updated": "2016-11-29 13:01:34 -0500", + "brakeman_version": "3.4.0" +} From b4ad51e69d2c4534427e14791cd438f57ec74910 Mon Sep 17 00:00:00 2001 From: Robert Best Date: Sun, 4 Dec 2016 20:02:24 +0000 Subject: [PATCH 355/378] reactify notification icon --- app/views/layouts/_upperelements.html.erb | 19 +++++---- app/views/notifications/mark_read.js.erb | 1 + app/views/notifications/mark_unread.js.erb | 1 + .../src/Metamaps/GlobalUI/NotificationIcon.js | 30 ++++++++++++++ frontend/src/Metamaps/GlobalUI/index.js | 4 +- frontend/src/Metamaps/index.js | 4 +- frontend/src/components/NotificationIcon.js | 39 +++++++++++++++++++ 7 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 frontend/src/Metamaps/GlobalUI/NotificationIcon.js create mode 100644 frontend/src/components/NotificationIcon.js diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index 8b2882ce..515bfcd4 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -71,15 +71,20 @@ <% end %> + <% if current_user.present? %> - <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %> -
    - Notifications -
    - <% if user_has_unread_notifications? %> -
    + + <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %> +
    + Notifications +
    + <% if user_has_unread_notifications? %> +
    + <% end %> <% end %> - <% end %> +
    <% end %> diff --git a/app/views/notifications/mark_read.js.erb b/app/views/notifications/mark_read.js.erb index 5b28f453..cbf2cf13 100644 --- a/app/views/notifications/mark_read.js.erb +++ b/app/views/notifications/mark_read.js.erb @@ -4,3 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a') $('#notification-<%= @notification.id %>') .removeClass('unread') .addClass('read') +Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount - 1) \ No newline at end of file diff --git a/app/views/notifications/mark_unread.js.erb b/app/views/notifications/mark_unread.js.erb index 46744388..3fffab24 100644 --- a/app/views/notifications/mark_unread.js.erb +++ b/app/views/notifications/mark_unread.js.erb @@ -4,3 +4,4 @@ $('#notification-<%= @notification.id %> .notification-read-unread > a') $('#notification-<%= @notification.id %>') .removeClass('read') .addClass('unread') +Metamaps.GlobalUI.NotificationIcon.render(Metamaps.GlobalUI.NotificationIcon.unreadNotificationsCount + 1) \ No newline at end of file diff --git a/frontend/src/Metamaps/GlobalUI/NotificationIcon.js b/frontend/src/Metamaps/GlobalUI/NotificationIcon.js new file mode 100644 index 00000000..2f13adba --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/NotificationIcon.js @@ -0,0 +1,30 @@ +/* global $ */ + +import React from 'react' +import ReactDOM from 'react-dom' + +import Active from '../Active' +import NotificationIconComponent from '../../components/NotificationIcon' + +const NotificationIcon = { + unreadNotificationsCount: null, + + init: function(serverData) { + const self = NotificationIcon + self.unreadNotificationsCount = serverData.unreadNotificationsCount + self.render() + }, + render: function(newUnreadCount = null) { + if (newUnreadCount !== null) { + NotificationIcon.unreadNotificationsCount = newUnreadCount + } + + if (Active.Mapper !== null) { + ReactDOM.render(React.createElement(NotificationIconComponent, { + unreadNotificationsCount: NotificationIcon.unreadNotificationsCount + }), $('#notification_icon').get(0)) + } + } +} + +export default NotificationIcon \ No newline at end of file diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 95e484f8..932e3319 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -8,6 +8,7 @@ import Search from './Search' import CreateMap from './CreateMap' import Account from './Account' import ImportDialog from './ImportDialog' +import NotificationIcon from './NotificationIcon' const GlobalUI = { notifyTimeout: null, @@ -19,6 +20,7 @@ const GlobalUI = { self.CreateMap.init(serverData) self.Account.init(serverData) self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox) + self.NotificationIcon.init(serverData) if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) @@ -127,5 +129,5 @@ const GlobalUI = { } } -export { Search, CreateMap, Account, ImportDialog } +export { Search, CreateMap, Account, ImportDialog, NotificationIcon } export default GlobalUI diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index dfad4d95..be218aff 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -8,7 +8,8 @@ import Create from './Create' import Debug from './Debug' import Filter from './Filter' import GlobalUI, { - Search, CreateMap, ImportDialog, Account as GlobalUIAccount + Search, CreateMap, ImportDialog, Account as GlobalUIAccount, + NotificationIcon } from './GlobalUI' import Import from './Import' import JIT from './JIT' @@ -47,6 +48,7 @@ Metamaps.GlobalUI.Search = Search Metamaps.GlobalUI.CreateMap = CreateMap Metamaps.GlobalUI.Account = GlobalUIAccount Metamaps.GlobalUI.ImportDialog = ImportDialog +Metamaps.GlobalUI.NotificationIcon = NotificationIcon Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners diff --git a/frontend/src/components/NotificationIcon.js b/frontend/src/components/NotificationIcon.js new file mode 100644 index 00000000..b886f557 --- /dev/null +++ b/frontend/src/components/NotificationIcon.js @@ -0,0 +1,39 @@ +import React, { PropTypes, Component } from 'react' + +class NotificationIcon extends Component { + constructor(props) { + super(props) + + this.state = { + } + } + + render = () => { + var linkClasses = "notificationsIcon upperRightEl upperRightIcon " + + if (this.props.unreadNotificationsCount > 0) { + linkClasses += "unread" + } else { + linkClasses += "read" + } + + return ( + +
    + Notifications +
    + {this.props.unreadNotificationsCount === 0 ? null : ( +
    + )} +
    + + + ) + } +} + +NotificationIcon.propTypes = { + unreadNotificationsCount: PropTypes.number +} + +export default NotificationIcon From 9debcdde39fb479af345ca3ebbfd0198b1d5395a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 8 Dec 2016 14:39:41 -0500 Subject: [PATCH 356/378] Integrate rails mailers with mailboxer --- app/assets/stylesheets/notifications.scss.erb | 10 ++++++++-- app/controllers/access_controller.rb | 13 ++++--------- app/helpers/application_helper.rb | 11 +---------- app/mailers/application_mailer.rb | 12 ++++++++++++ app/views/layouts/_mobilemenu.html.erb | 4 ++-- app/views/layouts/_upperelements.html.erb | 6 +++--- .../new_notification_email.html.erb | 11 +++-------- .../new_notification_email.text.erb | 3 ++- app/views/notifications/show.html.erb | 12 ++++++++---- config/initializers/mailboxer.rb | 11 +++++++++++ frontend/src/components/NotificationIcon.js | 2 +- 11 files changed, 55 insertions(+), 40 deletions(-) diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index a866af04..d7942135 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -30,7 +30,7 @@ $unread_notifications_dot_size: 8px; margin-top: 1em + $menu_bar_height; font-family: 'din-regular', Sans-Serif; - & > .title { + & > .notification-title { border-bottom: 1px solid #eee; padding-bottom: 0.25em; margin-bottom: 0.5em; @@ -42,7 +42,7 @@ $unread_notifications_dot_size: 8px; } - .notification { + .notificationsPage .notification { .notification-subject { width: 25%; } @@ -68,4 +68,10 @@ $unread_notifications_dot_size: 8px; } } } + + .notificationPage .notification-body { + p, div { + margin: 1em auto; + } + } } diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index 5f19d23e..a287ca1b 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -22,12 +22,10 @@ class AccessController < ApplicationController request = AccessRequest.create(user: current_user, map: @map) # what about push notification to map owner? mail = MapMailer.access_request_email(request, @map) - @map.user.notify(mail.subject, mail.body.parts[1].body.to_s) + @map.user.notify(mail.subject, 'access request', request, true, MAILBOXER_CODE_ACCESS_REQUEST) respond_to do |format| - format.json do - head :ok - end + format.json { head :ok } end end @@ -38,16 +36,13 @@ class AccessController < ApplicationController @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, # who we then send an email to - user = User.find(user_id) mail = MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)) - user.notify(mail.subject, mail.body.parts[1].body.to_s) + user.notify(mail.subject, 'invite to edit', UserMap.find_by(user_id: user_id, map: @map), true, MAILBOXER_CODE_INVITED_TO_EDIT) end @map.remove_old_collaborators(user_ids) respond_to do |format| - format.json do - head :ok - end + format.json { head :ok } end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index cc121cbe..45a5d565 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,18 +38,9 @@ module ApplicationHelper "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end - def user_has_unread_notifications? - return @user_has_unread_notifications unless @user_has_unread_notifications.nil? - return (@user_has_unread_notifications = false) if current_user.nil? - current_user.mailboxer_notification_receipts.each do |receipt| - return (@user_has_unread_notifications = true) if receipt.is_read == false - end - @user_has_unread_notifications = false - end - def user_unread_notification_count return 0 if current_user.nil? - current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| + @user_unread_notification_count ||= current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| receipt.is_read ? total : total + 1 end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 10961836..338f38ee 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -6,4 +6,16 @@ class ApplicationMailer < ActionMailer::Base def deliver raise NotImplementedError('Please use Mailboxer to send your emails.') end + + class << self + def mail_for_notification(notification) + if notification.notification_code == MAILBOXER_CODE_ACCESS_REQUEST + request = notification.notified_object + MapMailer.access_request_email(request, request.map) + elsif notification.notification_code == MAILBOXER_CODE_INVITED_TO_EDIT + user_map = notification.notified_object + MapMailer.invite_to_edit_email(user_map.map, user_map.map.user, user_map.user) + end + end + end end diff --git a/app/views/layouts/_mobilemenu.html.erb b/app/views/layouts/_mobilemenu.html.erb index c557f516..5ef3a66d 100644 --- a/app/views/layouts/_mobilemenu.html.erb +++ b/app/views/layouts/_mobilemenu.html.erb @@ -3,7 +3,7 @@ <%= yield(:mobile_title) %>
    @@ -55,7 +55,7 @@
  • <%= link_to "Notifications", notifications_path %> - <% if user_has_unread_notifications? %> + <% if user_unread_notification_count > 0 %>
    <% end %>
  • diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index 515bfcd4..1f499615 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -76,11 +76,11 @@ <% if current_user.present? %> - <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_has_unread_notifications? ? 'unread' : 'read'}" do %> + <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_unread_notification_count > 0 ? 'unread' : 'read'}" do %>
    - Notifications + Notifications (<%= user_unread_notification_count %> unread)
    - <% if user_has_unread_notifications? %> + <% if user_unread_notification_count > 0 %>
    <% end %> <% end %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb index ac4af493..d8fda23c 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -1,9 +1,4 @@ - - - - - - <%= raw @notification.body %> - - +<% mail = ApplicationMailer.mail_for_notification(@notification) %> +<% @notification.update(body: mail.html_part&.body&.decoded) %> +<%= raw mail.html_part&.body&.decoded %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb index 1c230d08..45fa8ae0 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -1 +1,2 @@ -<%= @notification.body.html_safe? ? @notification.body : strip_tags(@notification.body) %> +<% mail = ApplicationMailer.mail_for_notification(@notification) %> +<%= mail.text_part&.body&.decoded %> diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb index 1d61ccc3..1c555954 100644 --- a/app/views/notifications/show.html.erb +++ b/app/views/notifications/show.html.erb @@ -3,12 +3,16 @@
    -

    <%= @notification.subject %>

    - <%= raw @notification.body %> -
    - <%= link_to 'Back', notifications_path %> +

    <%= @notification.subject %>

    +
    + <%= raw @notification.body %>
    + +
    + <%= link_to 'Back', notifications_path %> +
    + <%= render partial: 'shared/back_to_mapping' %>
    diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb index 9e1efe66..115d80a4 100644 --- a/config/initializers/mailboxer.rb +++ b/config/initializers/mailboxer.rb @@ -1,4 +1,15 @@ # frozen_string_literal: true + +# notification codes to differentiate different types of notifications +# e.g. a notification might have { +# notified_object_type: 'Map', +# notified_object_id: 1, +# notification_code: MAILBOXER_CODE_ACCESS_REQUEST +# }, +# which would imply that this is an access request to Map.find(1) +MAILBOXER_CODE_ACCESS_REQUEST = 'ACCESS_REQUEST' +MAILBOXER_CODE_INVITED_TO_EDIT = 'INVITED_TO_EDIT' + Mailboxer.setup do |config| # Configures if your application uses or not email sending for Notifications and Messages config.uses_emails = true diff --git a/frontend/src/components/NotificationIcon.js b/frontend/src/components/NotificationIcon.js index b886f557..98782a75 100644 --- a/frontend/src/components/NotificationIcon.js +++ b/frontend/src/components/NotificationIcon.js @@ -20,7 +20,7 @@ class NotificationIcon extends Component { return (
    - Notifications + Notifications ({this.props.unreadNotificationsCount} unread)
    {this.props.unreadNotificationsCount === 0 ? null : (
    From 8e958ec9a80688c0efa25b22ebe25e3fba817ee7 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 9 Dec 2016 11:59:24 -0500 Subject: [PATCH 357/378] invite to edit notifications marked as read in system once map is visited --- app/controllers/maps_controller.rb | 1 + app/models/user_map.rb | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index d66456b8..6e1e0d77 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -8,6 +8,7 @@ class MapsController < ApplicationController def show respond_to do |format| format.html do + UserMap.where(map: @map, user: current_user).map(&:mark_invite_notifications_as_read) @allmappers = @map.contributors @allcollaborators = @map.editors @alltopics = policy_scope(@map.topics) diff --git a/app/models/user_map.rb b/app/models/user_map.rb index dc268047..3aa87b03 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -2,4 +2,10 @@ class UserMap < ApplicationRecord belongs_to :map belongs_to :user + + def mark_invite_notifications_as_read + Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end + end end From 3f6f020ce1c7ab6d4dcc21cac39fb66d3887ba41 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 9 Dec 2016 12:04:17 -0500 Subject: [PATCH 358/378] grant/deny buttons mark access request notifications as read --- app/controllers/access_controller.rb | 4 ++-- app/models/access_request.rb | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index a287ca1b..d3808a1a 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -49,7 +49,7 @@ class AccessController < ApplicationController # GET maps/:id/approve_access/:request_id def approve_access request = AccessRequest.find(params[:request_id]) - request.approve + request.approve # also marks mailboxer notification as read respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was approved' } end @@ -58,7 +58,7 @@ class AccessController < ApplicationController # GET maps/:id/deny_access/:request_id def deny_access request = AccessRequest.find(params[:request_id]) - request.deny + request.deny # also marks mailboxer notification as read respond_to do |format| format.html { redirect_to map_path(@map), notice: 'Request was turned down' } end diff --git a/app/models/access_request.rb b/app/models/access_request.rb index 185a04f0..504e0ff2 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -6,13 +6,23 @@ class AccessRequest < ApplicationRecord self.approved = true self.answered = true self.save - UserMap.create(user: self.user, map: self.map) - MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later + + Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end + + UserMap.create(user: user, map: map) + mail = MapMailer.invite_to_edit_email(map, map.user, user) + user.notify(mail.subject, 'invite to edit', self, true, MAILBOXER_CODE_INVITED_TO_EDIT) end def deny self.approved = false self.answered = true self.save + + Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) + end end end From 88e98c734298456adc8cdd4e82f26abc79705a86 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 9 Dec 2016 11:55:53 -0500 Subject: [PATCH 359/378] polish mailboxer with bug fixes --- app/assets/stylesheets/clean.css.erb | 2 +- app/controllers/access_controller.rb | 5 +++-- app/models/access_request.rb | 4 ++-- .../new_notification_email.html.erb | 6 ++++-- .../new_notification_email.text.erb | 4 +++- config/environments/production.rb | 10 +++++----- config/initializers/mailboxer.rb | 2 +- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index f4352504..fb55b29f 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -625,7 +625,7 @@ .sidebarForkIcon div:after, .sidebarFilterIcon div:after, .notificationsIcon div:after, -.sidebarAccountIcon .tooltipsUnder:after, +.sidebarAccountIcon .tooltipsUnder:after { content: ''; position: absolute; right: 40%; diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index d3808a1a..6f3518d5 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -36,8 +36,9 @@ class AccessController < ApplicationController @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, # who we then send an email to - mail = MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)) - user.notify(mail.subject, 'invite to edit', UserMap.find_by(user_id: user_id, map: @map), true, MAILBOXER_CODE_INVITED_TO_EDIT) + user = User.find(user_id) + mail = MapMailer.invite_to_edit_email(@map, current_user, user) + user.notify(mail.subject, 'invite to edit', UserMap.find_by(user: user, map: @map), true, MAILBOXER_CODE_INVITED_TO_EDIT) end @map.remove_old_collaborators(user_ids) diff --git a/app/models/access_request.rb b/app/models/access_request.rb index 504e0ff2..d062ab79 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -11,9 +11,9 @@ class AccessRequest < ApplicationRecord Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) end - UserMap.create(user: user, map: map) + user_map = UserMap.create(user: user, map: map) mail = MapMailer.invite_to_edit_email(map, map.user, user) - user.notify(mail.subject, 'invite to edit', self, true, MAILBOXER_CODE_INVITED_TO_EDIT) + user.notify(mail.subject, 'invite to edit', user_map, true, MAILBOXER_CODE_INVITED_TO_EDIT) end def deny diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb index d8fda23c..87d8da33 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -1,4 +1,6 @@ <% mail = ApplicationMailer.mail_for_notification(@notification) %> -<% @notification.update(body: mail.html_part&.body&.decoded) %> -<%= raw mail.html_part&.body&.decoded %> +<% if mail %> + <% @notification.update(body: mail.html_part&.body&.decoded) %> + <%= raw mail.html_part&.body&.decoded %> +<% end %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb index 45fa8ae0..58623b16 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -1,2 +1,4 @@ <% mail = ApplicationMailer.mail_for_notification(@notification) %> -<%= mail.text_part&.body&.decoded %> +<% if mail %> + <%= mail.text_part&.body&.decoded %> +<% end %> diff --git a/config/environments/production.rb b/config/environments/production.rb index d3f8794e..ab4769b6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,16 +1,16 @@ + # frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb - config.log_level = :warn - config.eager_load = true - - # 12 factor: log to stdout - logger = ActiveSupport::Logger.new(STDOUT) + # log to stdout + logger = Logger.new(STDOUT) logger.formatter = config.log_formatter + logger.level = :warn config.logger = ActiveSupport::TaggedLogging.new(logger) # Code is not reloaded between requests + config.eager_load = true config.cache_classes = true # Full error reports are disabled and caching is turned on diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb index 115d80a4..b937df92 100644 --- a/config/initializers/mailboxer.rb +++ b/config/initializers/mailboxer.rb @@ -15,7 +15,7 @@ Mailboxer.setup do |config| config.uses_emails = true # Configures the default from for emails sent for Messages and Notifications - config.default_from = 'no-reply@metamaps.cc' + config.default_from = 'team@metamaps.cc' # Configures the methods needed by mailboxer config.email_method = :mailboxer_email From 0960159265ee6197d09f1173ddccc69267c6927f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 11 Dec 2016 17:29:48 -0500 Subject: [PATCH 360/378] Mailboxer notification pagination --- app/controllers/access_controller.rb | 4 +++- app/controllers/notifications_controller.rb | 2 +- app/controllers/users_controller.rb | 4 +++- app/helpers/application_helper.rb | 2 +- app/models/access_request.rb | 6 +++--- app/models/user_map.rb | 2 +- app/views/notifications/index.html.erb | 7 +++++++ frontend/src/Metamaps/GlobalUI/NotificationIcon.js | 6 +++--- frontend/src/components/NotificationIcon.js | 13 ++++++------- 9 files changed, 28 insertions(+), 18 deletions(-) diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index 6f3518d5..5b6dea6e 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -38,7 +38,9 @@ class AccessController < ApplicationController # who we then send an email to user = User.find(user_id) mail = MapMailer.invite_to_edit_email(@map, current_user, user) - user.notify(mail.subject, 'invite to edit', UserMap.find_by(user: user, map: @map), true, MAILBOXER_CODE_INVITED_TO_EDIT) + user.notify(mail.subject, 'invite to edit', + UserMap.find_by(user: user, map: @map), + true, MAILBOXER_CODE_INVITED_TO_EDIT) end @map.remove_old_collaborators(user_ids) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 4759ef20..16049fc0 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -5,7 +5,7 @@ class NotificationsController < ApplicationController before_action :set_receipt, only: [:show, :mark_read, :mark_unread] def index - @notifications = current_user.mailbox.notifications + @notifications = current_user.mailbox.notifications.page(params[:page]).per(25) respond_to do |format| format.html diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6b771d6f..1defb323 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -95,6 +95,8 @@ class UsersController < ApplicationController private def user_params - params.require(:user).permit(:name, :email, :image, :password, :password_confirmation, :emails_allowed) + params.require(:user).permit( + :name, :email, :image, :password, :password_confirmation, :emails_allowed + ) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 45a5d565..96b5a2b2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -40,7 +40,7 @@ module ApplicationHelper def user_unread_notification_count return 0 if current_user.nil? - @user_unread_notification_count ||= current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| + @uunc ||= current_user.mailboxer_notification_receipts.reduce(0) do |total, receipt| receipt.is_read ? total : total + 1 end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb index d062ab79..c433f7cc 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -7,10 +7,10 @@ class AccessRequest < ApplicationRecord self.answered = true self.save - Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Notification.where(notified_object: self).find_each do |notification| Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) end - + user_map = UserMap.create(user: user, map: map) mail = MapMailer.invite_to_edit_email(map, map.user, user) user.notify(mail.subject, 'invite to edit', user_map, true, MAILBOXER_CODE_INVITED_TO_EDIT) @@ -21,7 +21,7 @@ class AccessRequest < ApplicationRecord self.answered = true self.save - Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Notification.where(notified_object: self).find_each do |notification| Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) end end diff --git a/app/models/user_map.rb b/app/models/user_map.rb index 3aa87b03..c1cdc58e 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -4,7 +4,7 @@ class UserMap < ApplicationRecord belongs_to :user def mark_invite_notifications_as_read - Mailboxer::Notification.where(notified_object: self).each do |notification| + Mailboxer::Notification.where(notified_object: self).find_each do |notification| Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) end end diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index bd8022e5..e030e483 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -30,6 +30,13 @@ <% end %>
    + + <% if @notifications.total_pages > 1 %> + + <% end %> + <%= render partial: 'shared/back_to_mapping' %>
    diff --git a/frontend/src/Metamaps/GlobalUI/NotificationIcon.js b/frontend/src/Metamaps/GlobalUI/NotificationIcon.js index 2f13adba..1e9ff3bd 100644 --- a/frontend/src/Metamaps/GlobalUI/NotificationIcon.js +++ b/frontend/src/Metamaps/GlobalUI/NotificationIcon.js @@ -8,7 +8,7 @@ import NotificationIconComponent from '../../components/NotificationIcon' const NotificationIcon = { unreadNotificationsCount: null, - + init: function(serverData) { const self = NotificationIcon self.unreadNotificationsCount = serverData.unreadNotificationsCount @@ -18,7 +18,7 @@ const NotificationIcon = { if (newUnreadCount !== null) { NotificationIcon.unreadNotificationsCount = newUnreadCount } - + if (Active.Mapper !== null) { ReactDOM.render(React.createElement(NotificationIconComponent, { unreadNotificationsCount: NotificationIcon.unreadNotificationsCount @@ -27,4 +27,4 @@ const NotificationIcon = { } } -export default NotificationIcon \ No newline at end of file +export default NotificationIcon diff --git a/frontend/src/components/NotificationIcon.js b/frontend/src/components/NotificationIcon.js index 98782a75..5a1e820f 100644 --- a/frontend/src/components/NotificationIcon.js +++ b/frontend/src/components/NotificationIcon.js @@ -9,14 +9,14 @@ class NotificationIcon extends Component { } render = () => { - var linkClasses = "notificationsIcon upperRightEl upperRightIcon " - + let linkClasses = 'notificationsIcon upperRightEl upperRightIcon ' + if (this.props.unreadNotificationsCount > 0) { - linkClasses += "unread" + linkClasses += 'unread' } else { - linkClasses += "read" + linkClasses += 'read' } - + return (
    @@ -26,8 +26,7 @@ class NotificationIcon extends Component {
    )}
    - - + ) } } From 6d8392d2e715c9daab69615be9fb95edb677d8cd Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 12 Dec 2016 16:07:34 -0500 Subject: [PATCH 361/378] Make mailboxer look good and update email templates --- app/assets/stylesheets/apps.css.erb | 16 ++-- app/assets/stylesheets/clean.css.erb | 2 + app/assets/stylesheets/mobile.scss.erb | 6 +- app/assets/stylesheets/notifications.scss.erb | 76 +++++++++++++------ .../authorized_applications/index.html.erb | 1 + app/views/layouts/_upperelements.html.erb | 4 +- app/views/layouts/application.html.erb | 2 +- app/views/layouts/doorkeeper.html.erb | 3 +- .../new_notification_email.html.erb | 12 ++- .../new_notification_email.text.erb | 4 + .../map_mailer/access_request_email.html.erb | 22 ++---- .../map_mailer/access_request_email.text.erb | 2 - .../map_mailer/invite_to_edit_email.html.erb | 22 ++---- .../map_mailer/invite_to_edit_email.text.erb | 4 - app/views/notifications/index.html.erb | 7 +- app/views/notifications/show.html.erb | 4 +- app/views/shared/_back_to_mapping.html.erb | 2 +- frontend/src/components/NotificationIcon.js | 2 +- 18 files changed, 102 insertions(+), 89 deletions(-) diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb index f3f444c7..46fa64b7 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -1,8 +1,8 @@ .centerContent { position: relative; - margin: 92px auto 0 auto; - padding: 20px 0 60px 20px; - width: 760px; + margin: 0 auto; + width: auto; + max-width: 960px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24); background: #fff; @@ -10,7 +10,7 @@ -moz-border-radius: 3px; border-radius: 3px; border: 1px solid #dcdcdc; - margin-bottom: 10px; + box-sizing: border-box; padding: 15px; } @@ -130,10 +130,8 @@ border-radius: 2px; } -.back-to-mapping { - margin: 1em auto; - width: auto; - max-width: 960px; - box-sizing: border-box; +.centerContent.withPadding { + margin-top: 1em; + margin-bottom: 1em; } diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index fb55b29f..3970f877 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -28,6 +28,8 @@ position: absolute; width: 100%; height: 100%; + box-sizing: border-box; + padding-top: 92px; } /*.animations { diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index e7eb9a7d..8cdb3ae6 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -2,7 +2,7 @@ display: none; } -@media only screen and (max-width : 720px) and (min-width : 504px) { +@media only screen and (max-width : 752px) and (min-width : 504px) { .sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField { width: 160px !important; } @@ -57,7 +57,7 @@ } #yield { - height: 100%; + padding-top: 50px; } .new_session, .new_user, .edit_user, .login, .forgotPassword { @@ -66,7 +66,7 @@ left: auto; width: 78%; padding: 16px 10%; - margin: 50px auto 0 auto; + margin: 0 auto; } .centerGreyForm input[type="text"], .centerGreyForm input[type="email"], .centerGreyForm input[type="password"] { diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index d7942135..d9602804 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -20,16 +20,14 @@ $unread_notifications_dot_size: 8px; list-style: none; } - $menu_bar_height: 6em; .notificationPage, .notificationsPage { - width: auto; - max-width: 960px; - box-sizing: border-box; - margin: 1em auto; - margin-top: 1em + $menu_bar_height; font-family: 'din-regular', Sans-Serif; + & a:hover { + text-decoration: none; + } + & > .notification-title { border-bottom: 1px solid #eee; padding-bottom: 0.25em; @@ -41,34 +39,62 @@ $unread_notifications_dot_size: 8px; } } - - .notificationsPage .notification { - .notification-subject { - width: 25%; - } - .notification-body { - width: 50%; - } - .notification-read-unread { - width: 10%; + .notificationsPage { + header { + margin-bottom: 0; } - .notification-body, - .notification-subject, - .notification-read-unread { - display: inline-block; - vertical-align: top; + .notification:first-child { + border-top: none; + } + .notification:last-child { + border-bottom: 1px solid #DCDCDC; } - &.unread { - .notification-body, - .notification-subject, + .notification { + padding: 10px; + border:1px solid #DCDCDC; + border-bottom: none; + + &:hover { + background: #F6F6F6; + } + + & > a { + float: left; + width: 85%; + box-sizing: border-box; + padding-right: 10px; + } + .notification-read-unread { - font-family: 'din-medium', Sans-Serif; + float: left; + width: 15%; + } + + .notification-body, + .notification-subject { + display: inline-block; + vertical-align: top; + } + + .notification-body { + margin-left: 15px; + } + + &.unread { + .notification-body, + .notification-subject, + .notification-read-unread { + font-family: 'din-medium', Sans-Serif; + } } } + } + + .notificationPage .notification-body { p, div { margin: 1em auto; diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb index 42c3127d..948efa30 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.erb +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -33,5 +33,6 @@ <% end %>
    +<%= render partial: 'shared/back_to_mapping' %>
    <%= render 'script' %> diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index 1f499615..7ef76bad 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -4,7 +4,7 @@
    @@ -78,7 +78,7 @@ <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_unread_notification_count > 0 ? 'unread' : 'read'}" do %>
    - Notifications (<%= user_unread_notification_count %> unread) + Notifications
    <% if user_unread_notification_count > 0 %>
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index d48ec0a1..2dd2a463 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -30,7 +30,7 @@
    - <%= render :partial => 'layouts/upperelements', :locals => { :appsPage => false } %> + <%= render :partial => 'layouts/upperelements', :locals => { :noHardHomeLink => controller_name == "notifications" ? true : false } %> <%= yield %> diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb index a6a74f41..960502d9 100644 --- a/app/views/layouts/doorkeeper.html.erb +++ b/app/views/layouts/doorkeeper.html.erb @@ -22,7 +22,7 @@
    - <%= render :partial => 'layouts/upperelements', :locals => {:appsPage => true } %> + <%= render :partial => 'layouts/upperelements', :locals => {:noHardHomeLink => true } %> <%= yield %> @@ -50,7 +50,6 @@ <% end %>

    - <%= render partial: 'shared/back_to_mapping' %>
    <% end %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb index 87d8da33..7f680869 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -1,6 +1,10 @@ <% mail = ApplicationMailer.mail_for_notification(@notification) %> -<% if mail %> - <% @notification.update(body: mail.html_part&.body&.decoded) %> - <%= raw mail.html_part&.body&.decoded %> -<% end %> +
    + <% if mail %> + <% @notification.update(body: mail.html_part&.body&.decoded) %> + <%= raw mail.html_part&.body&.decoded %> + <% end %> +

    Make sense with Metamaps

    + <%= render partial: 'shared/mailer_unsubscribe_link' %> +
    diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb index 58623b16..a98af833 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.text.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.text.erb @@ -2,3 +2,7 @@ <% if mail %> <%= mail.text_part&.body&.decoded %> <% end %> + +Make sense with Metamaps + +<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb index 2e89eb6e..74d666bd 100644 --- a/app/views/map_mailer/access_request_email.html.erb +++ b/app/views/map_mailer/access_request_email.html.erb @@ -1,16 +1,6 @@ -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> - -

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    - -

    <%= @map.name %>

    - -

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> -

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    - - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - -

    Make sense with Metamaps

    - - <%= render partial: 'shared/mailer_unsubscribe_link' %> -
    +<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    +

    <%= @map.name %>

    +

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    +<%= link_to 'Go to Map', map_url(@map), style: button_style %> diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb index 25ccb2bd..ef302a8b 100644 --- a/app/views/map_mailer/access_request_email.text.erb +++ b/app/views/map_mailer/access_request_email.text.erb @@ -5,6 +5,4 @@ Allow [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>] Decline [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] -Make sense with Metamaps -<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index cd2b7b2e..aba7cfd4 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -1,15 +1,7 @@ -
    - <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> - -

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    -

    <%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    - <% if @map.desc %> -

    <%= @map.desc %>

    - <% end %> - - <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> - -

    Make sense with Metamaps

    - - <%= render partial: 'shared/mailer_unsubscribe_link' %> -
    +<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    +

    <%= link_to @map.name, map_url(@map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    +<% if @map.desc %> +

    <%= @map.desc %>

    +<% end %> +<%= link_to 'Go to Map', map_url(@map), style: button_style %> diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 7d3bf397..4e822842 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -1,7 +1,3 @@ <%= @inviter.name %> has invited you to collaboratively edit the following map: <%= @map.name %> [<%= map_url(@map) %>] - -Make sense with Metamaps - -<%= render partial: 'shared/mailer_unsubscribe_link' %> diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index e030e483..f2c98adf 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -3,7 +3,9 @@
    -

    My Notifications

    +
      <% @notifications.each do |notification| %> <% receipt = @receipts.find_by(notification_id: notification.id) %> @@ -23,6 +25,7 @@ <%= link_to 'mark as read', mark_read_notification_path(notification.id), remote: true, method: :put %> <% end %>
    +
    <% end %> <% if @notifications.count == 0 %> @@ -32,7 +35,7 @@
    <% if @notifications.total_pages > 1 %> -
    -
    - <%= link_to 'Back', notifications_path %> +
    + <%= link_to 'Back to notifications', notifications_path %>
    <%= render partial: 'shared/back_to_mapping' %> diff --git a/app/views/shared/_back_to_mapping.html.erb b/app/views/shared/_back_to_mapping.html.erb index 682a71e6..342fd186 100644 --- a/app/views/shared/_back_to_mapping.html.erb +++ b/app/views/shared/_back_to_mapping.html.erb @@ -1,3 +1,3 @@ -
    + diff --git a/frontend/src/components/NotificationIcon.js b/frontend/src/components/NotificationIcon.js index 5a1e820f..e7225f04 100644 --- a/frontend/src/components/NotificationIcon.js +++ b/frontend/src/components/NotificationIcon.js @@ -20,7 +20,7 @@ class NotificationIcon extends Component { return (
    - Notifications ({this.props.unreadNotificationsCount} unread) + Notifications
    {this.props.unreadNotificationsCount === 0 ? null : (
    From 87228c27c1a286789a5ef8f8a191fa6346307439 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 13 Dec 2016 02:42:33 -0500 Subject: [PATCH 362/378] Fix mailboxer + email bugs --- app/assets/stylesheets/mobile.scss.erb | 3 ++- app/assets/stylesheets/notifications.scss.erb | 4 +++ app/controllers/access_controller.rb | 11 +++----- app/mailers/application_mailer.rb | 7 +++-- app/mailers/map_mailer.rb | 16 +++++++----- app/models/access_request.rb | 11 ++++++-- app/models/map.rb | 4 +++ app/services/notification_service.rb | 26 +++++++++++++++++++ .../authorized_applications/index.html.erb | 2 +- .../new_notification_email.html.erb | 6 +---- .../map_mailer/access_approved_email.html.erb | 8 ++++++ .../map_mailer/access_approved_email.text.erb | 4 +++ .../map_mailer/access_request_email.html.erb | 12 +++++---- .../map_mailer/access_request_email.text.erb | 10 ++++--- .../map_mailer/invite_to_edit_email.html.erb | 12 +++++---- .../map_mailer/invite_to_edit_email.text.erb | 6 +++-- app/views/notifications/index.html.erb | 6 +++-- app/views/notifications/show.html.erb | 2 +- app/views/shared/_back_to_mapping.html.erb | 3 --- app/views/shared/_go_to_maps.html.erb | 3 +++ .../shared/_mailer_unsubscribe_link.html.erb | 2 +- .../shared/_mailer_unsubscribe_link.text.erb | 2 +- config/initializers/mailboxer.rb | 3 ++- frontend/src/Metamaps/Map/InfoBox.js | 2 +- spec/mailers/previews/map_mailer_preview.rb | 7 ++++- 25 files changed, 120 insertions(+), 52 deletions(-) create mode 100644 app/services/notification_service.rb create mode 100644 app/views/map_mailer/access_approved_email.html.erb create mode 100644 app/views/map_mailer/access_approved_email.text.erb delete mode 100644 app/views/shared/_back_to_mapping.html.erb create mode 100644 app/views/shared/_go_to_maps.html.erb diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 8cdb3ae6..8deca0aa 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -240,8 +240,9 @@ position: relative; .unread-notifications-dot { - top: 17px; + top: 50%; left: 0px; + margin-top: -4px; } } } diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index d9602804..5058bc62 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -44,6 +44,10 @@ $unread_notifications_dot_size: 8px; margin-bottom: 0; } + .emptyInbox { + padding-top: 15px; + } + .notification:first-child { border-top: none; } diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index 5b6dea6e..a83fd128 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -20,9 +20,7 @@ class AccessController < ApplicationController # POST maps/:id/access_request def access_request request = AccessRequest.create(user: current_user, map: @map) - # what about push notification to map owner? - mail = MapMailer.access_request_email(request, @map) - @map.user.notify(mail.subject, 'access request', request, true, MAILBOXER_CODE_ACCESS_REQUEST) + NotificationService.access_request(request) respond_to do |format| format.json { head :ok } @@ -35,12 +33,9 @@ class AccessController < ApplicationController @map.add_new_collaborators(user_ids).each do |user_id| # add_new_collaborators returns array of added users, - # who we then send an email to + # who we then send a notification to user = User.find(user_id) - mail = MapMailer.invite_to_edit_email(@map, current_user, user) - user.notify(mail.subject, 'invite to edit', - UserMap.find_by(user: user, map: @map), - true, MAILBOXER_CODE_INVITED_TO_EDIT) + NotificationService.invite_to_edit(@map, current_user, user) end @map.remove_old_collaborators(user_ids) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 338f38ee..ebffb2df 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -11,8 +11,11 @@ class ApplicationMailer < ActionMailer::Base def mail_for_notification(notification) if notification.notification_code == MAILBOXER_CODE_ACCESS_REQUEST request = notification.notified_object - MapMailer.access_request_email(request, request.map) - elsif notification.notification_code == MAILBOXER_CODE_INVITED_TO_EDIT + MapMailer.access_request_email(request) + elsif notification.notification_code == MAILBOXER_CODE_ACCESS_APPROVED + request = notification.notified_object + MapMailer.access_approved_email(request) + elsif notification.notification_code == MAILBOXER_CODE_INVITE_TO_EDIT user_map = notification.notified_object MapMailer.invite_to_edit_email(user_map.map, user_map.map.user, user_map.user) end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index f6865ecd..bf0cec7b 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -2,17 +2,21 @@ class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' - def access_request_email(request, map) + def access_request_email(request) @request = request - @map = map - subject = @map.name + ' - request to edit' - mail(to: @map.user.email, subject: subject) + @map = request.map + mail(to: @map.user.email, subject: request.requested_text) + end + + def access_approved_email(request) + @request = request + @map = request.map + mail(to: request.user, subject: request.approved_text) end def invite_to_edit_email(map, inviter, invitee) @inviter = inviter @map = map - subject = @map.name + ' - invitation to edit' - mail(to: invitee.email, subject: subject) + mail(to: invitee.email, subject: map.invited_text) end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb index c433f7cc..e5416fff 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -12,8 +12,7 @@ class AccessRequest < ApplicationRecord end user_map = UserMap.create(user: user, map: map) - mail = MapMailer.invite_to_edit_email(map, map.user, user) - user.notify(mail.subject, 'invite to edit', user_map, true, MAILBOXER_CODE_INVITED_TO_EDIT) + NotificationService.access_approved(self) end def deny @@ -25,4 +24,12 @@ class AccessRequest < ApplicationRecord Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) end end + + def requested_text + self.map.name + ' - request to edit' + end + + def approved_text + self.map.name + ' - access approved' + end end diff --git a/app/models/map.rb b/app/models/map.rb index 36b2d284..5744b856 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -123,4 +123,8 @@ class Map < ApplicationRecord Topic.where(defer_to_map_id: id).update_all(permission: permission) Synapse.where(defer_to_map_id: id).update_all(permission: permission) end + + def invited_text + self.name + ' - invited to edit' + end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb new file mode 100644 index 00000000..05a268f4 --- /dev/null +++ b/app/services/notification_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +class NotificationService + + def self.renderer + renderer ||= ApplicationController.renderer.new( + http_host: ENV['MAILER_DEFAULT_URL'], + https: Rails.env.production? ? true : false + ) + end + + def self.access_request(request) + body = renderer.render(template: 'map_mailer/access_request_email', locals: { map: request.map, request: request }, layout: false) + request.map.user.notify(request.requested_text, body, request, false, MAILBOXER_CODE_ACCESS_REQUEST) + end + + def self.access_approved(request) + body = renderer.render(template: 'map_mailer/access_approved_email', locals: { map: request.map }, layout: false) + receipt = request.user.notify(request.approved_text, body, request, false, MAILBOXER_CODE_ACCESS_APPROVED) + end + + def self.invite_to_edit(map, inviter, invited) + user_map = UserMap.find_by(user: invited, map: map) + body = renderer.render(template: 'map_mailer/invite_to_edit_email', locals: { map: map, inviter: inviter }, layout: false) + invited.notify(map.invited_text, body, user_map, false, MAILBOXER_CODE_INVITE_TO_EDIT) + end +end diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb index 948efa30..d6391215 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.erb +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -33,6 +33,6 @@ <% end %>
    -<%= render partial: 'shared/back_to_mapping' %> +<%= render partial: 'shared/go_to_maps' %>
    <%= render 'script' %> diff --git a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb index 7f680869..ad90419f 100644 --- a/app/views/mailboxer/notification_mailer/new_notification_email.html.erb +++ b/app/views/mailboxer/notification_mailer/new_notification_email.html.erb @@ -1,10 +1,6 @@ -<% mail = ApplicationMailer.mail_for_notification(@notification) %>
    - <% if mail %> - <% @notification.update(body: mail.html_part&.body&.decoded) %> - <%= raw mail.html_part&.body&.decoded %> - <% end %> + <%= raw @notification.body %>

    Make sense with Metamaps

    <%= render partial: 'shared/mailer_unsubscribe_link' %>
    diff --git a/app/views/map_mailer/access_approved_email.html.erb b/app/views/map_mailer/access_approved_email.html.erb new file mode 100644 index 00000000..91fd77e0 --- /dev/null +++ b/app/views/map_mailer/access_approved_email.html.erb @@ -0,0 +1,8 @@ +<% map = @map || map %> +<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> +

    <%= map.user.name %> has responded to your access request and invited you to collaboratively edit the following map:

    +

    <%= link_to map.name, map_url(map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    +<% if map.desc %> +

    <%= map.desc %>

    +<% end %> +<%= link_to 'Go to Map', map_url(map), style: button_style %> diff --git a/app/views/map_mailer/access_approved_email.text.erb b/app/views/map_mailer/access_approved_email.text.erb new file mode 100644 index 00000000..2a8e54f6 --- /dev/null +++ b/app/views/map_mailer/access_approved_email.text.erb @@ -0,0 +1,4 @@ +<% map = @map || map %> +<%= map.user.name %> has responded to your access request and invited you to collaboratively edit the following map: + +<%= map.name %> [<%= map_url(map) %>] diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb index 74d666bd..759b97eb 100644 --- a/app/views/map_mailer/access_request_email.html.erb +++ b/app/views/map_mailer/access_request_email.html.erb @@ -1,6 +1,8 @@ +<% map = @map || map %> +<% request = @request || request %> <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -

    <%= @request.user.name %> is requesting access to collaboratively edit the following map:

    -

    <%= @map.name %>

    -

    <%= link_to "Allow", approve_access_map_url(id: @map.id, request_id: @request.id), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> -

    <%= link_to "Decline", deny_access_map_url(id: @map.id, request_id: @request.id), style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    -<%= link_to 'Go to Map', map_url(@map), style: button_style %> +

    <%= request.user.name %> is requesting access to collaboratively edit the following map:

    +

    <%= map.name %>

    +

    <%= link_to "Allow", approve_access_map_url(id: map.id, request_id: request.id), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

    <%= link_to "Decline", deny_access_map_url(id: map.id, request_id: request.id), style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

    +<%= link_to 'Go to Map', map_url(map), style: button_style %> diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb index ef302a8b..c99aa6e6 100644 --- a/app/views/map_mailer/access_request_email.text.erb +++ b/app/views/map_mailer/access_request_email.text.erb @@ -1,8 +1,10 @@ -<%= @request.user.name %> has requested to collaboratively edit the following map: +<% map = @map || map %> +<% request = @request || request %> +<%= request.user.name %> has requested to collaboratively edit the following map: -<%= @map.name %> [<%= map_url(@map) %>] +<%= map.name %> [<%= map_url(map) %>] -Allow [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>] -Decline [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] +Allow [<%= approve_access_map_url(id: map.id, request_id: request.id) %>] +Decline [<%= deny_access_map_url(id: map.id, request_id: request.id) %>] diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index aba7cfd4..f08cc377 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -1,7 +1,9 @@ +<% map = @map || map %> +<% inviter = @inviter || inviter %> <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -

    <%= @inviter.name %> has invited you to collaboratively edit the following map:

    -

    <%= link_to @map.name, map_url(@map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    -<% if @map.desc %> -

    <%= @map.desc %>

    +

    <%= inviter.name %> has invited you to collaboratively edit the following map:

    +

    <%= link_to map.name, map_url(map), style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

    +<% if map.desc %> +

    <%= map.desc %>

    <% end %> -<%= link_to 'Go to Map', map_url(@map), style: button_style %> +<%= link_to 'Go to Map', map_url(map), style: button_style %> diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 4e822842..b58cced9 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -1,3 +1,5 @@ -<%= @inviter.name %> has invited you to collaboratively edit the following map: +<% map = @map || map %> +<% inviter = @inviter || inviter %> +<%= inviter.name %> has invited you to collaboratively edit the following map: -<%= @map.name %> [<%= map_url(@map) %>] +<%= map.name %> [<%= map_url(map) %>] diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index f2c98adf..9e37c220 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -29,7 +29,9 @@ <% end %> <% if @notifications.count == 0 %> - You have ZERO unread notifications. Huzzah! +
    + You have ZERO unread notifications. Huzzah! +
    <% end %>
    @@ -40,7 +42,7 @@
    <% end %> - <%= render partial: 'shared/back_to_mapping' %> + <%= render partial: 'shared/go_to_maps' %>
    <%= render partial: 'notifications/header' %> diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb index 0af259ea..a003a0e1 100644 --- a/app/views/notifications/show.html.erb +++ b/app/views/notifications/show.html.erb @@ -13,7 +13,7 @@ <%= link_to 'Back to notifications', notifications_path %> - <%= render partial: 'shared/back_to_mapping' %> + <%= render partial: 'shared/go_to_maps' %> <%= render partial: 'notifications/header' %> diff --git a/app/views/shared/_back_to_mapping.html.erb b/app/views/shared/_back_to_mapping.html.erb deleted file mode 100644 index 342fd186..00000000 --- a/app/views/shared/_back_to_mapping.html.erb +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/app/views/shared/_go_to_maps.html.erb b/app/views/shared/_go_to_maps.html.erb new file mode 100644 index 00000000..04c88574 --- /dev/null +++ b/app/views/shared/_go_to_maps.html.erb @@ -0,0 +1,3 @@ + diff --git a/app/views/shared/_mailer_unsubscribe_link.html.erb b/app/views/shared/_mailer_unsubscribe_link.html.erb index 2bbe7a89..56730dd9 100644 --- a/app/views/shared/_mailer_unsubscribe_link.html.erb +++ b/app/views/shared/_mailer_unsubscribe_link.html.erb @@ -1,3 +1,3 @@ diff --git a/app/views/shared/_mailer_unsubscribe_link.text.erb b/app/views/shared/_mailer_unsubscribe_link.text.erb index 3a2f7d0d..ff851865 100644 --- a/app/views/shared/_mailer_unsubscribe_link.text.erb +++ b/app/views/shared/_mailer_unsubscribe_link.text.erb @@ -2,4 +2,4 @@ You can unsubscribe from all Metamaps emails by visiting the following link: -<%= unsubscribe_notifications_url %> +<%= unsubscribe_notifications_url(protocol: Rails.env.production? ? :https : :http) %> diff --git a/config/initializers/mailboxer.rb b/config/initializers/mailboxer.rb index b937df92..49824f50 100644 --- a/config/initializers/mailboxer.rb +++ b/config/initializers/mailboxer.rb @@ -8,7 +8,8 @@ # }, # which would imply that this is an access request to Map.find(1) MAILBOXER_CODE_ACCESS_REQUEST = 'ACCESS_REQUEST' -MAILBOXER_CODE_INVITED_TO_EDIT = 'INVITED_TO_EDIT' +MAILBOXER_CODE_ACCESS_APPROVED = 'ACCESS_APPROVED' +MAILBOXER_CODE_INVITE_TO_EDIT = 'INVITE_TO_EDIT' Mailboxer.setup do |config| # Configures if your application uses or not email sending for Notifications and Messages diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 1b06daf5..5fced292 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -264,7 +264,7 @@ const InfoBox = { var mapperIds = DataModel.Collaborators.models.map(function(mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) var name = DataModel.Collaborators.get(newCollaboratorId).get('name') - GlobalUI.notifyUser(name + ' will be notified by email') + GlobalUI.notifyUser(name + ' will be notified') self.updateNumbers() } diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb index 17ea7671..61e33eb8 100644 --- a/spec/mailers/previews/map_mailer_preview.rb +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -7,6 +7,11 @@ class MapMailerPreview < ActionMailer::Preview def access_request_email request = AccessRequest.first - MapMailer.access_request_email(request, request.map) + MapMailer.access_request_email(request) + end + + def access_approved_email + request = AccessRequest.first + MapMailer.access_approved_email(request) end end From 186129807e892ee09ea9c5de8686377c14517244 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 12 Dec 2016 22:28:10 -0500 Subject: [PATCH 363/378] fix spec, bugs, style --- app/controllers/access_controller.rb | 1 - app/controllers/application_controller.rb | 6 ++--- app/controllers/topics_controller.rb | 6 ++--- .../users/registrations_controller.rb | 5 +--- app/helpers/topics_helper.rb | 2 +- app/models/access_request.rb | 9 ++++--- app/models/map.rb | 12 ++++----- app/models/webhooks/slack/base.rb | 4 +-- app/policies/explore_policy.rb | 1 + app/policies/hack_policy.rb | 1 + app/policies/map_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- app/services/notification_service.rb | 1 - config/initializers/doorkeeper.rb | 8 +++--- config/initializers/rack-attack.rb | 25 +++++++++---------- config/locales/en.yml | 10 +++----- spec/api/v2/mappings_api_spec.rb | 1 - spec/api/v2/maps_api_spec.rb | 5 ++-- spec/api/v2/topics_api_spec.rb | 1 - spec/controllers/synapses_controller_spec.rb | 4 +-- spec/mailers/map_mailer_spec.rb | 5 ++-- spec/models/access_request_spec.rb | 7 +++--- 22 files changed, 54 insertions(+), 64 deletions(-) diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb index a83fd128..2441aa62 100644 --- a/app/controllers/access_controller.rb +++ b/app/controllers/access_controller.rb @@ -6,7 +6,6 @@ class AccessController < ApplicationController :deny_access, :deny_access_post, :request_access] after_action :verify_authorized - # GET maps/:id/request_access def request_access @map = nil diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4285682e..4bb5be10 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base helper_method :admin? def handle_unauthorized - if authenticated? and params[:controller] == 'maps' and params[:action] == 'show' + if authenticated? && (params[:controller] == 'maps') && (params[:action] == 'show') redirect_to request_access_map_path(params[:id]) elsif authenticated? redirect_to root_path, notice: "You don't have permission to see that page." @@ -41,13 +41,13 @@ class ApplicationController < ActionController::Base def require_no_user return true unless authenticated? redirect_to edit_user_path(user), notice: 'You must be logged out.' - return false + false end def require_user return true if authenticated? redirect_to sign_in_path, notice: 'You must be logged in.' - return false + false end def require_admin diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ea56059b..b54cc4f5 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -14,14 +14,14 @@ class TopicsController < ApplicationController @topics = policy_scope(Topic).where('LOWER("name") like ?', term.downcase + '%').order('"name"') @mapTopics = @topics.select { |t| t&.metacode&.name == 'Metamap' } # prioritize topics which point to maps, over maps - @exclude = @mapTopics.length > 0 ? @mapTopics.map(&:name) : [''] + @exclude = @mapTopics.length.positive? ? @mapTopics.map(&:name) : [''] @maps = policy_scope(Map).where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude).order('"name"') else @topics = [] @maps = [] end - @all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name } - + @all = @topics.to_a.concat(@maps.to_a).sort_by(&:name) + render json: autocomplete_array_json(@all).to_json end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 7c211f26..44bcb2de 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -21,13 +21,10 @@ class Users::RegistrationsController < Devise::RegistrationsController end end - private def store_location - if params[:redirect_to] - store_location_for(User, params[:redirect_to]) - end + store_location_for(User, params[:redirect_to]) if params[:redirect_to] end def configure_sign_up_params diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 58f53a6e..fa26095d 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -20,7 +20,7 @@ module TopicsHelper type: is_map ? metamapMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, - synapseCount: is_map ? 0 : t.synapses.count, + synapseCount: is_map ? 0 : t.synapses.count } end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb index e5416fff..fe68ce8f 100644 --- a/app/models/access_request.rb +++ b/app/models/access_request.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class AccessRequest < ApplicationRecord belongs_to :user belongs_to :map @@ -5,7 +6,7 @@ class AccessRequest < ApplicationRecord def approve self.approved = true self.answered = true - self.save + save Mailboxer::Notification.where(notified_object: self).find_each do |notification| Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) @@ -18,7 +19,7 @@ class AccessRequest < ApplicationRecord def deny self.approved = false self.answered = true - self.save + save Mailboxer::Notification.where(notified_object: self).find_each do |notification| Mailboxer::Receipt.where(notification: notification).update_all(is_read: true) @@ -26,10 +27,10 @@ class AccessRequest < ApplicationRecord end def requested_text - self.map.name + ' - request to edit' + map.name + ' - request to edit' end def approved_text - self.map.name + ' - access approved' + map.name + ' - access approved' end end diff --git a/app/models/map.rb b/app/models/map.rb index 5744b856..899992c6 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -18,11 +18,11 @@ class Map < ApplicationRecord # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, - styles: { - thumb: ['220x220#', :png] - #:full => ['940x630#', :png] - }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' + styles: { + thumb: ['220x220#', :png] + #:full => ['940x630#', :png] + }, + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -125,6 +125,6 @@ class Map < ApplicationRecord end def invited_text - self.name + ' - invited to edit' + name + ' - invited to edit' end end diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb index 2274e32c..ba4e9ea5 100644 --- a/app/models/webhooks/slack/base.rb +++ b/app/models/webhooks/slack/base.rb @@ -14,9 +14,7 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do 'something' end - def channel - webhook.channel - end + delegate :channel, to: :webhook def attachments [{ diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb index b4d52fe5..ce17d4f4 100644 --- a/app/policies/explore_policy.rb +++ b/app/policies/explore_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ExplorePolicy < ApplicationPolicy def active? true diff --git a/app/policies/hack_policy.rb b/app/policies/hack_policy.rb index b6fbf6ce..bdc9eaab 100644 --- a/app/policies/hack_policy.rb +++ b/app/policies/hack_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class HackPolicy < ApplicationPolicy def load_url_title? true diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index f670f59e..937d564c 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -16,7 +16,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission.in?(['commons', 'public']) || + record.permission.in?(%w(commons public)) || record.collaborators.include?(user) || record.user == user end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 64463b4a..bc80f657 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -22,7 +22,7 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission.in?(['commons', 'public']) || record.user == user + record.permission.in?(%w(commons public)) || record.user == user end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 05a268f4..41202345 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true class NotificationService - def self.renderer renderer ||= ApplicationController.renderer.new( http_host: ENV['MAILER_DEFAULT_URL'], diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 433b1c40..21b08bc2 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -9,20 +9,20 @@ Doorkeeper.configure do current_user else store_location_for(User, request.fullpath) - redirect_to(sign_in_url, notice: "Sign In to Connect") + redirect_to(sign_in_url, notice: 'Sign In to Connect') end end # If you want to restrict access to the web interface for adding oauth authorized applications, # you need to declare the block below. admin_authenticator do - if current_user && current_user.admin + if current_user&.admin current_user elsif current_user && !current_user.admin - redirect_to(root_url, notice: "Unauthorized") + redirect_to(root_url, notice: 'Unauthorized') else store_location_for(User, request.fullpath) - redirect_to(sign_in_url, notice: "Try signing in to do that") + redirect_to(sign_in_url, notice: 'Try signing in to do that') end end diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 1cb90f0f..0fd76889 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Rack::Attack Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new @@ -11,10 +12,8 @@ class Rack::Attack # Throttle POST requests to /login by IP address # # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" - throttle('logins/ip', :limit => 5, :period => 20.seconds) do |req| - if req.path == '/login' && req.post? - req.ip - end + throttle('logins/ip', limit: 5, period: 20.seconds) do |req| + req.ip if req.path == '/login' && req.post? end # Throttle POST requests to /login by email param @@ -25,17 +24,17 @@ class Rack::Attack # throttle logins for another user and force their login requests to be # denied, but that's not very common and shouldn't happen to you. (Knock # on wood!) - throttle("logins/email", :limit => 5, :period => 20.seconds) do |req| + throttle('logins/email', limit: 5, period: 20.seconds) do |req| if req.path == '/login' && req.post? # return the email if present, nil otherwise req.params['email'].presence end end - throttle('load_url_title/req/5mins/ip', :limit => 300, :period => 5.minutes) do |req| + throttle('load_url_title/req/5mins/ip', limit: 300, period: 5.minutes) do |req| req.ip if req.path == 'hacks/load_url_title' end - throttle('load_url_title/req/1s/ip', :limit => 5, :period => 1.second) do |req| + throttle('load_url_title/req/1s/ip', limit: 5, period: 1.second) do |req| # If the return value is truthy, the cache key for the return value # is incremented and compared with the limit. In this case: # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" @@ -46,16 +45,16 @@ class Rack::Attack end self.throttled_response = lambda do |env| - now = Time.now - match_data = env['rack.attack.match_data'] + now = Time.now + match_data = env['rack.attack.match_data'] period = match_data[:period] limit = match_data[:limit] - headers = { + headers = { 'X-RateLimit-Limit' => limit.to_s, - 'X-RateLimit-Remaining' => '0', - 'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s - } + 'X-RateLimit-Remaining' => '0', + 'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s + } [429, headers, ['']] end diff --git a/config/locales/en.yml b/config/locales/en.yml index 46d3db07..c4ada107 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,8 +1,4 @@ -# Sample localization file for English. Add more files in this directory for other locales. -# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. - en: - activerecord: - attributes: - user: - joinedwithcode: "Access code" + mailboxer: + notification_mailer: + subject: "%{subject}" diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index 6f225c6a..f8854e91 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -18,7 +18,6 @@ RSpec.describe 'mappings API', type: :request do it 'GET /api/v2/mappings/:id' do get "/api/v2/mappings/#{mapping.id}", params: { access_token: token } - expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) expect(JSON.parse(response.body)['data']['id']).to eq mapping.id diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index a7edeef2..fbf07903 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -1,4 +1,5 @@ -#t frozen_string_literal: true +# frozen_string_literal: true +# t frozen_string_literal: true require 'rails_helper' RSpec.describe 'maps API', type: :request do @@ -35,7 +36,7 @@ RSpec.describe 'maps API', type: :request do expect(response).to match_json_schema(:map) expect(JSON.parse(response.body)['data']['id']).to eq map.id end - + it 'POST /api/v2/maps' do post '/api/v2/maps', params: { map: map.attributes, access_token: token } diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb index 31d93b87..ac2fb56a 100644 --- a/spec/api/v2/topics_api_spec.rb +++ b/spec/api/v2/topics_api_spec.rb @@ -18,7 +18,6 @@ RSpec.describe 'topics API', type: :request do it 'GET /api/v2/topics/:id' do get "/api/v2/topics/#{topic.id}" - expect(response).to have_http_status(:success) expect(response).to match_json_schema(:topic) expect(JSON.parse(response.body)['data']['id']).to eq topic.id diff --git a/spec/controllers/synapses_controller_spec.rb b/spec/controllers/synapses_controller_spec.rb index 511971ad..7abeb2ee 100644 --- a/spec/controllers/synapses_controller_spec.rb +++ b/spec/controllers/synapses_controller_spec.rb @@ -53,9 +53,9 @@ RSpec.describe SynapsesController, type: :controller do expect(response.status).to eq 422 end it 'does not create a synapse' do - expect { + expect do post :create, format: :json, params: { synapse: invalid_attributes } - }.to change { + end.to change { Synapse.count }.by 0 end diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb index 5fed48f5..a920746b 100644 --- a/spec/mailers/map_mailer_spec.rb +++ b/spec/mailers/map_mailer_spec.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MapMailer, type: :mailer do describe 'access_request_email' do - let(:request) { create(:access_request) } let(:map) { create(:map) } - let(:mail) { described_class.access_request_email(request, map) } + let(:request) { create(:access_request, map: map) } + let(:mail) { described_class.access_request_email(request) } it { expect(mail.from).to eq ['team@metamaps.cc'] } it { expect(mail.to).to eq [map.user.email] } diff --git a/spec/models/access_request_spec.rb b/spec/models/access_request_spec.rb index 98490bf7..e8db280b 100644 --- a/spec/models/access_request_spec.rb +++ b/spec/models/access_request_spec.rb @@ -1,8 +1,7 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe AccessRequest, type: :model do - include ActiveJob::TestHelper # enqueued_jobs - let(:access_request) { create(:access_request) } describe 'approve' do @@ -13,7 +12,7 @@ RSpec.describe AccessRequest, type: :model do it { expect(access_request.approved).to be true } it { expect(access_request.answered).to be true } it { expect(UserMap.count).to eq 1 } - it { expect(enqueued_jobs.count).to eq 1 } + it { expect(Mailboxer::Notification.count).to eq 1 } end describe 'deny' do @@ -24,6 +23,6 @@ RSpec.describe AccessRequest, type: :model do it { expect(access_request.approved).to be false } it { expect(access_request.answered).to be true } it { expect(UserMap.count).to eq 0 } - it { expect(enqueued_jobs.count).to eq 0 } + it { expect(Mailboxer::Notification.count).to eq 0 } end end From 40a97a5ae91c801bf307b4d07728f7d6c94ff2e7 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 15 Dec 2016 17:06:51 -0500 Subject: [PATCH 364/378] these are output in the main layout file --- app/views/notifications/_header.html.erb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/views/notifications/_header.html.erb b/app/views/notifications/_header.html.erb index f93f46a6..794bfdf7 100644 --- a/app/views/notifications/_header.html.erb +++ b/app/views/notifications/_header.html.erb @@ -9,10 +9,3 @@ -

    - <% if devise_error_messages? %> - <%= devise_error_messages! %> - <% elsif notice %> - <%= notice %> - <% end %> -

    From 2d920cf66a58e7872ff30b8925fbc6fc4637b76d Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 15 Dec 2016 17:34:42 -0500 Subject: [PATCH 365/378] add maps links to nav locations --- app/assets/stylesheets/apps.css.erb | 7 ++----- .../authorized_applications/index.html.erb | 1 - app/views/layouts/doorkeeper.html.erb | 3 +++ app/views/notifications/_header.html.erb | 3 +++ app/views/notifications/index.html.erb | 4 +--- app/views/notifications/show.html.erb | 9 +++------ app/views/shared/_go_to_maps.html.erb | 3 --- frontend/src/components/Maps/Header.js | 12 ++++++------ 8 files changed, 18 insertions(+), 24 deletions(-) delete mode 100644 app/views/shared/_go_to_maps.html.erb diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb index 46fa64b7..5771e366 100644 --- a/app/assets/stylesheets/apps.css.erb +++ b/app/assets/stylesheets/apps.css.erb @@ -2,16 +2,13 @@ position: relative; margin: 0 auto; width: auto; - max-width: 960px; + max-width: 800px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24); background: #fff; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - border: 1px solid #dcdcdc; box-sizing: border-box; padding: 15px; + font-family: 'din-regular', sans-serif; } .centerContent .page-header { diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb index d6391215..42c3127d 100644 --- a/app/views/doorkeeper/authorized_applications/index.html.erb +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -33,6 +33,5 @@ <% end %> -<%= render partial: 'shared/go_to_maps' %> <%= render 'script' %> diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb index 960502d9..f4696a37 100644 --- a/app/views/layouts/doorkeeper.html.erb +++ b/app/views/layouts/doorkeeper.html.erb @@ -38,6 +38,9 @@
    Authorized Apps
    + +
    Maps +
    diff --git a/app/views/notifications/_header.html.erb b/app/views/notifications/_header.html.erb index 794bfdf7..2507b2ef 100644 --- a/app/views/notifications/_header.html.erb +++ b/app/views/notifications/_header.html.erb @@ -5,6 +5,9 @@
    Notifications
    + +
    Maps +
    diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index 9e37c220..ee075410 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -30,7 +30,7 @@ <% end %> <% if @notifications.count == 0 %>
    - You have ZERO unread notifications. Huzzah! + You have no notifications. More time for dancing.
    <% end %> @@ -41,8 +41,6 @@ <%= paginate @notifications %> <% end %> - - <%= render partial: 'shared/go_to_maps' %> <%= render partial: 'notifications/header' %> diff --git a/app/views/notifications/show.html.erb b/app/views/notifications/show.html.erb index a003a0e1..b56fb177 100644 --- a/app/views/notifications/show.html.erb +++ b/app/views/notifications/show.html.erb @@ -2,18 +2,15 @@ <% content_for :mobile_title, 'Notifications' %>
    +
    + <%= link_to 'Back to notifications', notifications_path %> +

    <%= @notification.subject %>

    <%= raw @notification.body %>
    - -
    - <%= link_to 'Back to notifications', notifications_path %> -
    - - <%= render partial: 'shared/go_to_maps' %>
    <%= render partial: 'notifications/header' %> diff --git a/app/views/shared/_go_to_maps.html.erb b/app/views/shared/_go_to_maps.html.erb deleted file mode 100644 index 04c88574..00000000 --- a/app/views/shared/_go_to_maps.html.erb +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/frontend/src/components/Maps/Header.js b/frontend/src/components/Maps/Header.js index c0a7e1cd..f360e7a5 100644 --- a/frontend/src/components/Maps/Header.js +++ b/frontend/src/components/Maps/Header.js @@ -36,6 +36,12 @@ class Header extends Component {
    + - Date: Thu, 15 Dec 2016 17:41:08 -0500 Subject: [PATCH 366/378] make it look better when its taking up the full screen width --- app/assets/stylesheets/mobile.scss.erb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 8deca0aa..54004dc7 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -18,6 +18,14 @@ width: 390px; } } +/* 800 is the max-width for centerContent */ +@media only screen and (max-width : 800px) { + .centerContent.withPadding { + margin-top: 0; + margin-bottom: 0; + } +} + /* Smartphones (portrait and landscape) ----------- the minimum space that two map cards can fit side by side */ @media only screen and (max-width : 504px) { From 28d960459ef2745a68d17bca4e2db0b4288131bc Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 15 Dec 2016 23:57:37 -0500 Subject: [PATCH 367/378] styling of notifs list --- app/assets/stylesheets/mobile.scss.erb | 18 +++++ app/assets/stylesheets/notifications.scss.erb | 75 +++++++++++++------ app/services/notification_service.rb | 19 ++++- app/views/notifications/index.html.erb | 12 ++- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index 54004dc7..fc34168d 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -8,6 +8,13 @@ } } +/* when this switches to two lines */ +@media only screen and (max-width : 728px) { + .controller-notifications .notificationsPage .notification .notification-read-unread a { + margin-top: -20px !important; + } +} + @media only screen and (max-width : 390px) { .map .mapCard .mobileMetadata { width: 190px; @@ -33,6 +40,17 @@ display: none !important; } + .notificationsPage .page-header { + display: none; + } + + .controller-notifications .notificationsPage .notification .notification-read-unread { + display: block !important; + } + .controller-notifications .notificationsPage .notification .notification-date { + display: none; + } + #mobile_header { display: block; } diff --git a/app/assets/stylesheets/notifications.scss.erb b/app/assets/stylesheets/notifications.scss.erb index 5058bc62..16f96407 100644 --- a/app/assets/stylesheets/notifications.scss.erb +++ b/app/assets/stylesheets/notifications.scss.erb @@ -48,20 +48,20 @@ $unread_notifications_dot_size: 8px; padding-top: 15px; } - .notification:first-child { - border-top: none; - } - .notification:last-child { - border-bottom: 1px solid #DCDCDC; - } - .notification { padding: 10px; - border:1px solid #DCDCDC; - border-bottom: none; + position: relative; &:hover { background: #F6F6F6; + + .notification-read-unread { + display:block; + } + + .notification-date { + display: none; + } } & > a { @@ -71,27 +71,58 @@ $unread_notifications_dot_size: 8px; padding-right: 10px; } - .notification-read-unread { + .notification-actor { float: left; - width: 15%; - } - .notification-body, - .notification-subject { - display: inline-block; - vertical-align: top; + img { + width: 32px; + height: 32px; + border-radius: 16px; + } } .notification-body { - margin-left: 15px; + margin-left: 50px; + + .in-bold { + font-family: 'din-medium', Sans-Serif; + } + + .action { + background: #4fb5c0; + color: #FFF; + padding: 2px 6px; + border-radius: 3px; + display: inline-block; + margin: 5px 0; + } + } + + .notification-date { + position: absolute; + top: 50%; + right: 10px; + color: #607d8b; + font-size: 13px; + line-height: 13px; + margin-top: -6px; + } + + .notification-read-unread { + display: none; + float: left; + width: 15%; + + a { + position: absolute; + top: 50%; + margin-top: -10px; + text-align: center; + } } &.unread { - .notification-body, - .notification-subject, - .notification-read-unread { - font-family: 'din-medium', Sans-Serif; - } + background: #EEE; } } diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 41202345..aa919edb 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -9,17 +9,30 @@ class NotificationService def self.access_request(request) body = renderer.render(template: 'map_mailer/access_request_email', locals: { map: request.map, request: request }, layout: false) - request.map.user.notify(request.requested_text, body, request, false, MAILBOXER_CODE_ACCESS_REQUEST) + request.map.user.notify(request.requested_text, body, request, false, MAILBOXER_CODE_ACCESS_REQUEST, true, request.user) end def self.access_approved(request) body = renderer.render(template: 'map_mailer/access_approved_email', locals: { map: request.map }, layout: false) - receipt = request.user.notify(request.approved_text, body, request, false, MAILBOXER_CODE_ACCESS_APPROVED) + receipt = request.user.notify(request.approved_text, body, request, false, MAILBOXER_CODE_ACCESS_APPROVED, true, request.map.user) end def self.invite_to_edit(map, inviter, invited) user_map = UserMap.find_by(user: invited, map: map) body = renderer.render(template: 'map_mailer/invite_to_edit_email', locals: { map: map, inviter: inviter }, layout: false) - invited.notify(map.invited_text, body, user_map, false, MAILBOXER_CODE_INVITE_TO_EDIT) + invited.notify(map.invited_text, body, user_map, false, MAILBOXER_CODE_INVITE_TO_EDIT, true, inviter) + end + + def self.text_for_notification(notification) + if notification.notification_code == MAILBOXER_CODE_ACCESS_REQUEST + map = notification.notified_object.map + 'wants permission to map with you on ' + map.name + '  
    Offer a response
    ' + elsif notification.notification_code == MAILBOXER_CODE_ACCESS_APPROVED + map = notification.notified_object.map + 'granted your request to edit map ' + map.name + '' + elsif notification.notification_code == MAILBOXER_CODE_INVITE_TO_EDIT + map = notification.notified_object.map + 'gave you edit access to map ' + map.name + '' + end end end diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index ee075410..6f66eccd 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -4,18 +4,19 @@
      <% @notifications.each do |notification| %> <% receipt = @receipts.find_by(notification_id: notification.id) %>
    • <%= link_to notification_path(notification.id) do %> -
      - <%= notification.subject %> +
      + <%= image_tag notification.sender.image(:thirtytwo) %>
      - <%= strip_tags(notification.body).truncate(70) %> +
      <%= notification.sender.name %>
      + <%= raw NotificationService.text_for_notification(notification) %>
      <% end %>
      @@ -25,6 +26,9 @@ <%= link_to 'mark as read', mark_read_notification_path(notification.id), remote: true, method: :put %> <% end %>
      +
      + <%= notification.created_at.strftime("%b %d") %> +
    • <% end %> From fb12c7e202a7467148028af0559bb32494256981 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 16 Dec 2016 16:51:52 -0500 Subject: [PATCH 368/378] Track everything we need to reconstruct maps (#984) * feature/more.events * keep mapping.user as the creator * cleanup cruft and include slack notifs * capture topic and synapse updates, store the old values * avoid the mapping gets deleted problem * include an indicator of which values changed * style cleanup * remove the hack in favor of a legit way * updated schema file --- app/controllers/api/v2/mappings_controller.rb | 21 ++++++++++ app/controllers/mappings_controller.rb | 15 +++---- app/models/event.rb | 12 ++---- app/models/events/new_mapping.rb | 11 ----- app/models/events/synapse_added_to_map.rb | 12 ++++++ app/models/events/synapse_removed_from_map.rb | 12 ++++++ app/models/events/synapse_updated.rb | 11 +++++ app/models/events/topic_added_to_map.rb | 12 ++++++ app/models/events/topic_moved_on_map.rb | 12 ++++++ app/models/events/topic_removed_from_map.rb | 12 ++++++ app/models/events/topic_updated.rb | 11 +++++ app/models/mapping.rb | 36 +++++++++++++++++ app/models/synapse.rb | 13 ++++++ app/models/topic.rb | 12 ++++++ app/models/webhooks/slack/base.rb | 40 ------------------- .../slack/conversation_started_on_map.rb | 20 ---------- .../webhooks/slack/synapse_added_to_map.rb | 22 +--------- .../slack/synapse_removed_from_map.rb | 8 ++++ .../webhooks/slack/topic_added_to_map.rb | 21 +--------- .../webhooks/slack/topic_moved_on_map.rb | 6 +++ .../webhooks/slack/topic_removed_from_map.rb | 6 +++ .../webhooks/slack/user_present_on_map.rb | 20 ---------- app/serializers/api/v2/mapping_serializer.rb | 1 + app/serializers/webhook_serializer.rb | 2 +- .../20161214140124_add_meta_to_events.rb | 5 +++ ...161216174257_add_updated_by_to_mappings.rb | 5 +++ db/schema.rb | 6 ++- doc/api/apis/mappings.raml | 4 +- doc/api/examples/mapping.json | 1 + doc/api/examples/mappings.json | 6 ++- doc/api/schemas/_mapping.json | 12 ++++++ 31 files changed, 233 insertions(+), 154 deletions(-) delete mode 100644 app/models/events/new_mapping.rb create mode 100644 app/models/events/synapse_added_to_map.rb create mode 100644 app/models/events/synapse_removed_from_map.rb create mode 100644 app/models/events/synapse_updated.rb create mode 100644 app/models/events/topic_added_to_map.rb create mode 100644 app/models/events/topic_moved_on_map.rb create mode 100644 app/models/events/topic_removed_from_map.rb create mode 100644 app/models/events/topic_updated.rb create mode 100644 app/models/webhooks/slack/synapse_removed_from_map.rb create mode 100644 app/models/webhooks/slack/topic_moved_on_map.rb create mode 100644 app/models/webhooks/slack/topic_removed_from_map.rb create mode 100644 db/migrate/20161214140124_add_meta_to_events.rb create mode 100644 db/migrate/20161216174257_add_updated_by_to_mappings.rb diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 4490e4af..186d6891 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -5,6 +5,27 @@ module Api def searchable_columns [] end + + def create + instantiate_resource + resource.user = current_user if current_user.present? + resource.updated_by = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + + def update + resource.updated_by = current_user if current_user.present? + update_action + respond_with_resource + end + + def destroy + resource.updated_by = current_user if current_user.present? + destroy_action + head :no_content + end end end end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index de2c8ea1..86db023e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -19,10 +19,10 @@ class MappingsController < ApplicationController @mapping = Mapping.new(mapping_params) authorize @mapping @mapping.user = current_user + @mapping.updated_by = current_user if @mapping.save render json: @mapping, status: :created - Events::NewMapping.publish!(@mapping, current_user) else render json: @mapping.errors, status: :unprocessable_entity end @@ -32,8 +32,10 @@ class MappingsController < ApplicationController def update @mapping = Mapping.find(params[:id]) authorize @mapping + @mapping.updated_by = current_user + @mapping.assign_attributes(mapping_params) - if @mapping.update_attributes(mapping_params) + if @mapping.save head :no_content else render json: @mapping.errors, status: :unprocessable_entity @@ -44,14 +46,7 @@ class MappingsController < ApplicationController def destroy @mapping = Mapping.find(params[:id]) authorize @mapping - - mappable = @mapping.mappable - if mappable.defer_to_map - mappable.permission = mappable.defer_to_map.permission - mappable.defer_to_map_id = nil - mappable.save - end - + @mapping.updated_by = current_user @mapping.destroy head :no_content diff --git a/app/models/event.rb b/app/models/event.rb index 02c6d698..cf974664 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class Event < ApplicationRecord - KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze + KINDS = %w(user_present_on_map conversation_started_on_map + topic_added_to_map topic_moved_on_map topic_removed_from_map + synapse_added_to_map synapse_removed_from_map + topic_updated synapse_updated).freeze - # has_many :notifications, dependent: :destroy belongs_to :eventable, polymorphic: true belongs_to :map belongs_to :user @@ -14,18 +16,12 @@ class Event < ApplicationRecord validates :kind, inclusion: { in: KINDS } validates :eventable, presence: true - # def notify!(user) - # notifications.create!(user: user) - # end - def belongs_to?(this_user) user_id == this_user.id end def notify_webhooks! - # group = self.discussion.group map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } - # group.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } end handle_asynchronously :notify_webhooks! end diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb deleted file mode 100644 index 889c69bc..00000000 --- a/app/models/events/new_mapping.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true -class Events::NewMapping < Event - # after_create :notify_users! - - def self.publish!(mapping, user) - create!(kind: mapping.mappable_type == 'Topic' ? 'topic_added_to_map' : 'synapse_added_to_map', - eventable: mapping, - map: mapping.map, - user: user) - end -end diff --git a/app/models/events/synapse_added_to_map.rb b/app/models/events/synapse_added_to_map.rb new file mode 100644 index 00000000..5afa885d --- /dev/null +++ b/app/models/events/synapse_added_to_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::SynapseAddedToMap < Event + # after_create :notify_users! + + def self.publish!(synapse, map, user, meta) + create!(kind: 'synapse_added_to_map', + eventable: synapse, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/synapse_removed_from_map.rb b/app/models/events/synapse_removed_from_map.rb new file mode 100644 index 00000000..b64035dd --- /dev/null +++ b/app/models/events/synapse_removed_from_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::SynapseRemovedFromMap < Event + # after_create :notify_users! + + def self.publish!(synapse, map, user, meta) + create!(kind: 'synapse_removed_from_map', + eventable: synapse, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/synapse_updated.rb b/app/models/events/synapse_updated.rb new file mode 100644 index 00000000..0d85cbe8 --- /dev/null +++ b/app/models/events/synapse_updated.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class Events::SynapseUpdated < Event + # after_create :notify_users! + + def self.publish!(synapse, user, meta) + create!(kind: 'synapse_updated', + eventable: synapse, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_added_to_map.rb b/app/models/events/topic_added_to_map.rb new file mode 100644 index 00000000..a3fa62cf --- /dev/null +++ b/app/models/events/topic_added_to_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicAddedToMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_added_to_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_moved_on_map.rb b/app/models/events/topic_moved_on_map.rb new file mode 100644 index 00000000..08d01277 --- /dev/null +++ b/app/models/events/topic_moved_on_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicMovedOnMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_moved_on_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_removed_from_map.rb b/app/models/events/topic_removed_from_map.rb new file mode 100644 index 00000000..2f03ec26 --- /dev/null +++ b/app/models/events/topic_removed_from_map.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class Events::TopicRemovedFromMap < Event + # after_create :notify_users! + + def self.publish!(topic, map, user, meta) + create!(kind: 'topic_removed_from_map', + eventable: topic, + map: map, + user: user, + meta: meta) + end +end diff --git a/app/models/events/topic_updated.rb b/app/models/events/topic_updated.rb new file mode 100644 index 00000000..fd41a4d6 --- /dev/null +++ b/app/models/events/topic_updated.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +class Events::TopicUpdated < Event + # after_create :notify_users! + + def self.publish!(topic, user, meta) + create!(kind: 'topic_updated', + eventable: topic, + user: user, + meta: meta) + end +end diff --git a/app/models/mapping.rb b/app/models/mapping.rb index f7219008..99d23db0 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -6,6 +6,7 @@ class Mapping < ApplicationRecord belongs_to :mappable, polymorphic: true belongs_to :map, class_name: 'Map', foreign_key: 'map_id', touch: true belongs_to :user + belongs_to :updated_by, class_name: 'User' validates :xloc, presence: true, unless: proc { |m| m.mappable_type == 'Synapse' } @@ -16,6 +17,10 @@ class Mapping < ApplicationRecord delegate :name, to: :user, prefix: true + after_create :after_created + after_update :after_updated + before_destroy :before_destroyed + def user_image user.image.url end @@ -23,4 +28,35 @@ class Mapping < ApplicationRecord def as_json(_options = {}) super(methods: [:user_name, :user_image]) end + + def after_created + if mappable_type == 'Topic' + meta = {'x': xloc, 'y': yloc, 'mapping_id': id} + Events::TopicAddedToMap.publish!(mappable, map, user, meta) + elsif mappable_type == 'Synapse' + Events::SynapseAddedToMap.publish!(mappable, map, user, meta) + end + end + + def after_updated + if mappable_type == 'Topic' and (xloc_changed? or yloc_changed?) + meta = {'x': xloc, 'y': yloc, 'mapping_id': id} + Events::TopicMovedOnMap.publish!(mappable, map, updated_by, meta) + end + end + + def before_destroyed + if mappable.defer_to_map + mappable.permission = mappable.defer_to_map.permission + mappable.defer_to_map_id = nil + mappable.save + end + + meta = {'mapping_id': id} + if mappable_type == 'Topic' + Events::TopicRemovedFromMap.publish!(mappable, map, updated_by, meta) + elsif mappable_type == 'Synapse' + Events::SynapseRemovedFromMap.publish!(mappable, map, updated_by, meta) + end + end end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 08512e4f..d14a18f4 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -22,6 +22,8 @@ class Synapse < ApplicationRecord where(topic1_id: topic_id).or(where(topic2_id: topic_id)) } + after_update :after_updated + delegate :name, to: :user, prefix: true def user_image @@ -39,4 +41,15 @@ class Synapse < ApplicationRecord def as_json(_options = {}) super(methods: [:user_name, :user_image, :collaborator_ids]) end + + def after_updated + attrs = ['desc', 'category', 'permission', 'defer_to_map_id'] + if attrs.any? {|k| changed_attributes.key?(k)} + new = self.attributes.select {|k| attrs.include?(k) } + old = changed_attributes.select {|k| attrs.include?(k) } + meta = new.merge(old) # we are prioritizing the old values, keeping them + meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } + Events::SynapseUpdated.publish!(self, user, meta) + end + end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 256fc604..e5ea90ee 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -16,6 +16,7 @@ class Topic < ApplicationRecord belongs_to :metacode before_create :create_metamap? + after_update :after_updated validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -135,4 +136,15 @@ class Topic < ApplicationRecord self.link = Rails.application.routes.url_helpers .map_url(host: ENV['MAILER_DEFAULT_URL'], id: @map.id) end + + def after_updated + attrs = ['name', 'desc', 'link', 'metacode_id', 'permission', 'defer_to_map_id'] + if attrs.any? {|k| changed_attributes.key?(k)} + new = self.attributes.select {|k| attrs.include?(k) } + old = changed_attributes.select {|k| attrs.include?(k) } + meta = new.merge(old) # we are prioritizing the old values, keeping them + meta['changed'] = changed_attributes.keys.select {|k| attrs.include?(k) } + Events::TopicUpdated.publish!(self, user, meta) + end + end end diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb index ba4e9ea5..27f95861 100644 --- a/app/models/webhooks/slack/base.rb +++ b/app/models/webhooks/slack/base.rb @@ -16,45 +16,14 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do delegate :channel, to: :webhook - def attachments - [{ - title: attachment_title, - text: attachment_text, - fields: attachment_fields, - fallback: attachment_fallback - }] - end - alias_method :read_attribute_for_serialization, :send private - # def motion_vote_field - # { - # title: "Vote on this proposal", - # value: "#{proposal_link(eventable, "yes")} · " + - # "#{proposal_link(eventable, "abstain")} · " + - # "#{proposal_link(eventable, "no")} · " + - # "#{proposal_link(eventable, "block")}" - # } - # end - def view_map_on_metamaps(text = nil) "<#{map_url(event.map)}|#{text || event.map.name}>" end - # def view_discussion_on_loomio(params = {}) - # { value: discussion_link(I18n.t(:"webhooks.slack.view_it_on_loomio"), params) } - # end - - # def proposal_link(proposal, position = nil) - # discussion_link position || proposal.name, { proposal: proposal.key, position: position } - # end - - # def discussion_link(text = nil, params = {}) - # "<#{discussion_url(eventable.map, params)}|#{text || eventable.discussion.title}>" - # end - def eventable @eventable ||= event.eventable end @@ -63,12 +32,3 @@ Webhooks::Slack::Base = Struct.new(:webhook, :event) do @author ||= eventable.author end end - -# webhooks: -# slack: -# motion_closed: "*%{name}* has closed" -# motion_closing_soon: "*%{name}* has a proposal closing in 24 hours" -# motion_outcome_created: "*%{author}* published an outcome in *%{name}*" -# motion_outcome_updated: "*%{author}* updated the outcome for *%{name}*" -# new_motion: "*%{author}* started a new proposal in *%{name}*" -# view_it_on_loomio: "View it on Loomio" diff --git a/app/models/webhooks/slack/conversation_started_on_map.rb b/app/models/webhooks/slack/conversation_started_on_map.rb index daf2270e..6b6595ce 100644 --- a/app/models/webhooks/slack/conversation_started_on_map.rb +++ b/app/models/webhooks/slack/conversation_started_on_map.rb @@ -3,24 +3,4 @@ class Webhooks::Slack::ConversationStartedOnMap < Webhooks::Slack::Base def text "There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}" end - # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb index 5157afa7..3d944878 100644 --- a/app/models/webhooks/slack/synapse_added_to_map.rb +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -1,25 +1,7 @@ # frozen_string_literal: true class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base def text - "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps}*" - end - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] + connector = eventable.desc.empty? ? '->' : eventable.desc + "\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was added as a connection by *#{event.user.name}* to the map *#{view_map_on_metamaps}*" end end diff --git a/app/models/webhooks/slack/synapse_removed_from_map.rb b/app/models/webhooks/slack/synapse_removed_from_map.rb new file mode 100644 index 00000000..06d31206 --- /dev/null +++ b/app/models/webhooks/slack/synapse_removed_from_map.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true +class Webhooks::Slack::SynapseRemovedFromMap < Webhooks::Slack::Base + def text + connector = eventable.desc.empty? ? '->' : eventable.desc + # todo express correct directionality of arrows when desc is empty + "\"*#{eventable.topic1.name}* #{connector} *#{eventable.topic2.name}*\" was removed by *#{event.user.name}* as a connection from the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index d3a19760..4f726069 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -1,26 +1,7 @@ # frozen_string_literal: true class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text - "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps}*" + "*#{eventable.name}* was added by *#{event.user.name}* to the map *#{view_map_on_metamaps}*" end # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/models/webhooks/slack/topic_moved_on_map.rb b/app/models/webhooks/slack/topic_moved_on_map.rb new file mode 100644 index 00000000..dfe088ed --- /dev/null +++ b/app/models/webhooks/slack/topic_moved_on_map.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class Webhooks::Slack::TopicMovedOnMap < Webhooks::Slack::Base + def text + "*#{eventable.name}* was moved by *#{event.user.name}* on the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/topic_removed_from_map.rb b/app/models/webhooks/slack/topic_removed_from_map.rb new file mode 100644 index 00000000..05a79c3b --- /dev/null +++ b/app/models/webhooks/slack/topic_removed_from_map.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +class Webhooks::Slack::TopicRemovedFromMap < Webhooks::Slack::Base + def text + "*#{eventable.name}* was removed by *#{event.user.name}* from the map *#{view_map_on_metamaps}*" + end +end diff --git a/app/models/webhooks/slack/user_present_on_map.rb b/app/models/webhooks/slack/user_present_on_map.rb index c3185e48..4cee2992 100644 --- a/app/models/webhooks/slack/user_present_on_map.rb +++ b/app/models/webhooks/slack/user_present_on_map.rb @@ -3,24 +3,4 @@ class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base def text "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" end - # TODO: it would be sweet if it sends it with the metacode as the icon_url - - def attachment_fallback - '' # {}"*#{eventable.name}*\n#{eventable.description}\n" - end - - def attachment_title - '' # proposal_link(eventable) - end - - def attachment_text - '' # "#{eventable.description}\n" - end - - def attachment_fields - [{ - title: 'nothing', - value: 'nothing' - }] # [motion_vote_field] - end end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb index 19e7318e..30c9bd7f 100644 --- a/app/serializers/api/v2/mapping_serializer.rb +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -14,6 +14,7 @@ module Api def self.embeddable { user: {}, + updated_by: {}, map: {} } end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb index a2acf869..c1f0e266 100644 --- a/app/serializers/webhook_serializer.rb +++ b/app/serializers/webhook_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class WebhookSerializer < ActiveModel::Serializer - attributes :text, :username, :icon_url # , :attachments + attributes :text, :username, :icon_url attribute :channel, if: :has_channel? def has_channel? diff --git a/db/migrate/20161214140124_add_meta_to_events.rb b/db/migrate/20161214140124_add_meta_to_events.rb new file mode 100644 index 00000000..edbee3d6 --- /dev/null +++ b/db/migrate/20161214140124_add_meta_to_events.rb @@ -0,0 +1,5 @@ +class AddMetaToEvents < ActiveRecord::Migration[5.0] + def change + add_column :events, :meta, :json + end +end diff --git a/db/migrate/20161216174257_add_updated_by_to_mappings.rb b/db/migrate/20161216174257_add_updated_by_to_mappings.rb new file mode 100644 index 00000000..e28b8281 --- /dev/null +++ b/db/migrate/20161216174257_add_updated_by_to_mappings.rb @@ -0,0 +1,5 @@ +class AddUpdatedByToMappings < ActiveRecord::Migration[5.0] + def change + add_reference :mappings, :updated_by, foreign_key: {to_table: :users} + end +end diff --git a/db/schema.rb b/db/schema.rb index 5839929c..b30d597e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161125175229) do +ActiveRecord::Schema.define(version: 20161216174257) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -49,6 +49,7 @@ ActiveRecord::Schema.define(version: 20161125175229) do t.integer "map_id" t.datetime "created_at" t.datetime "updated_at" + t.json "meta" t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree t.index ["map_id"], name: "index_events_on_map_id", using: :btree t.index ["user_id"], name: "index_events_on_user_id", using: :btree @@ -128,10 +129,12 @@ ActiveRecord::Schema.define(version: 20161125175229) do t.datetime "updated_at", null: false t.integer "mappable_id" t.string "mappable_type" + t.integer "updated_by_id" t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree t.index ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree + t.index ["updated_by_id"], name: "index_mappings_on_updated_by_id", using: :btree t.index ["user_id"], name: "index_mappings_on_user_id", using: :btree end @@ -336,5 +339,6 @@ ActiveRecord::Schema.define(version: 20161125175229) do add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id" add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" + add_foreign_key "mappings", "users", column: "updated_by_id" add_foreign_key "tokens", "users" end diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index 00298387..af4965cb 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -1,6 +1,6 @@ #type: collection get: - is: [ embeddable: { embedFields: "user,map" }, orderable, pageable ] + is: [ embeddable: { embedFields: "user,updated_by,map" }, orderable, pageable ] securedBy: [ null, token, oauth_2_0, cookie ] responses: 200: @@ -31,7 +31,7 @@ post: /{id}: #type: item get: - is: [ embeddable: { embedFields: "user,map" } ] + is: [ embeddable: { embedFields: "user,updated_by,map" } ] securedBy: [ null, token, oauth_2_0, cookie ] responses: 200: diff --git a/doc/api/examples/mapping.json b/doc/api/examples/mapping.json index c4aa87bf..93d38bdb 100644 --- a/doc/api/examples/mapping.json +++ b/doc/api/examples/mapping.json @@ -6,6 +6,7 @@ "mappable_id": 1, "mappable_type": "Synapse", "user_id": 1, + "updated_by_id": 1, "map_id": 1 } } diff --git a/doc/api/examples/mappings.json b/doc/api/examples/mappings.json index 5a4a99c3..99f2e58d 100644 --- a/doc/api/examples/mappings.json +++ b/doc/api/examples/mappings.json @@ -8,6 +8,7 @@ "mappable_type": "Topic", "updated_at": "2016-03-25T08:44:07.152Z", "user_id": 1, + "updated_by_id": 1, "xloc": -271, "yloc": 22 }, @@ -19,6 +20,7 @@ "mappable_type": "Topic", "updated_at": "2016-03-25T08:44:13.907Z", "user_id": 1, + "updated_by_id": 1, "xloc": -12, "yloc": 61 }, @@ -30,6 +32,7 @@ "mappable_type": "Topic", "updated_at": "2016-03-25T08:44:19.333Z", "user_id": 1, + "updated_by_id": 1, "xloc": -93, "yloc": -90 }, @@ -40,7 +43,8 @@ "mappable_id": 1, "mappable_type": "Synapse", "updated_at": "2016-03-25T08:44:21.337Z", - "user_id": 1 + "user_id": 1, + "updated_by_id": 1 } ], "page": { diff --git a/doc/api/schemas/_mapping.json b/doc/api/schemas/_mapping.json index 8789c5ec..efd12c92 100644 --- a/doc/api/schemas/_mapping.json +++ b/doc/api/schemas/_mapping.json @@ -35,6 +35,12 @@ }, "user": { "$ref": "_user.json" + }, + "updated_by_id": { + "$ref": "_id.json" + }, + "updated_by": { + "$ref": "_user.json" } }, "required": [ @@ -56,6 +62,12 @@ { "required": [ "user_id" ] }, { "required": [ "user" ] } ] + }, + { + "oneOf": [ + { "required": [ "updated_by_id" ] }, + { "required": [ "updated_by" ] } + ] } ] } From c604e69d77b936712d2d673777e46abb80b9d05c Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 16 Dec 2016 16:56:58 -0500 Subject: [PATCH 369/378] enable postgresql 9.4 in travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 99b9a655..3dca7316 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,3 +22,4 @@ script: addons: code_climate: repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e + postgresql: "9.4" From 7ca7f0862f955e3aac487ae32a12f295c57caf74 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 16 Dec 2016 17:08:57 -0500 Subject: [PATCH 370/378] fix mapping spec --- spec/controllers/mappings_controller_spec.rb | 1 + spec/factories/mappings.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/spec/controllers/mappings_controller_spec.rb b/spec/controllers/mappings_controller_spec.rb index 8d1c424d..e5f59db7 100644 --- a/spec/controllers/mappings_controller_spec.rb +++ b/spec/controllers/mappings_controller_spec.rb @@ -24,6 +24,7 @@ RSpec.describe MappingsController, type: :controller do post :create, params: { mapping: valid_attributes } + mapping.updated_by = controller.current_user expect(comparable(Mapping.last)).to eq comparable(mapping) end end diff --git a/spec/factories/mappings.rb b/spec/factories/mappings.rb index 1bcdf891..ec06b613 100644 --- a/spec/factories/mappings.rb +++ b/spec/factories/mappings.rb @@ -5,6 +5,7 @@ FactoryGirl.define do yloc 0 map user + association :updated_by, factory: :user association :mappable, factory: :topic factory :mapping_random_location do From 68f0e91259d9c706572d2622196f57b64d82c745 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 18 Dec 2016 16:17:51 -0500 Subject: [PATCH 371/378] track forks (#994) * track forks * update api and docs * fix tests --- app/controllers/maps_controller.rb | 2 +- app/models/map.rb | 1 + app/serializers/api/v2/map_serializer.rb | 1 + db/migrate/20161218183817_add_source_to_maps.rb | 5 +++++ db/schema.rb | 5 ++++- doc/api/apis/maps.raml | 12 ++++++++++-- doc/api/examples/map.json | 1 + doc/api/examples/map_starred.json | 1 + doc/api/examples/maps.json | 1 + doc/api/schemas/_map.json | 12 ++++++++++++ doc/api/schemas/_optid.json | 3 +++ frontend/src/Metamaps/GlobalUI/CreateMap.js | 1 + spec/api/v2/maps_api_spec.rb | 7 ++++--- spec/factories/maps.rb | 1 + 14 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20161218183817_add_source_to_maps.rb create mode 100644 doc/api/schemas/_optid.json diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 6e1e0d77..189ae550 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -124,7 +124,7 @@ class MapsController < ApplicationController end def create_map_params - params.permit(:name, :desc, :permission) + params.permit(:name, :desc, :permission, :source_id) end def update_map_params diff --git a/app/models/map.rb b/app/models/map.rb index 899992c6..79b4ae35 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Map < ApplicationRecord belongs_to :user + belongs_to :source, class_name: :Map has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index ff641c69..7e090d33 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -18,6 +18,7 @@ module Api def self.embeddable { user: {}, + source: {}, topics: {}, synapses: {}, mappings: {}, diff --git a/db/migrate/20161218183817_add_source_to_maps.rb b/db/migrate/20161218183817_add_source_to_maps.rb new file mode 100644 index 00000000..d6ce5fbf --- /dev/null +++ b/db/migrate/20161218183817_add_source_to_maps.rb @@ -0,0 +1,5 @@ +class AddSourceToMaps < ActiveRecord::Migration[5.0] + def change + add_reference :maps, :source, foreign_key: {to_table: :maps} + end +end diff --git a/db/schema.rb b/db/schema.rb index b30d597e..7d146be5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161216174257) do +ActiveRecord::Schema.define(version: 20161218183817) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -151,6 +151,8 @@ ActiveRecord::Schema.define(version: 20161216174257) do t.string "screenshot_content_type", limit: 255 t.integer "screenshot_file_size" t.datetime "screenshot_updated_at" + t.integer "source_id" + t.index ["source_id"], name: "index_maps_on_source_id", using: :btree t.index ["user_id"], name: "index_maps_on_user_id", using: :btree end @@ -340,5 +342,6 @@ ActiveRecord::Schema.define(version: 20161216174257) do add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id" add_foreign_key "mailboxer_receipts", "mailboxer_notifications", column: "notification_id", name: "receipts_on_notification_id" add_foreign_key "mappings", "users", column: "updated_by_id" + add_foreign_key "maps", "maps", column: "source_id" add_foreign_key "tokens", "users" end diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index d3361a70..fe9fed67 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -1,6 +1,6 @@ #type: collection get: - is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ] + is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,source,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ] securedBy: [ null, token, oauth_2_0, cookie ] queryParameters: user_id: @@ -23,6 +23,8 @@ post: description: description permission: description: commons, public, or private + source_id: + description: the id of the map this map is a fork of screenshot: description: url to a screenshot of the map contributor_ids: @@ -37,7 +39,7 @@ post: /{id}: #type: item get: - is: [ embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" } ] + is: [ embeddable: { embedFields: "user,source,topics,synapses,mappings,contributors,collaborators" } ] securedBy: [ null, token, oauth_2_0, cookie ] responses: 200: @@ -60,6 +62,9 @@ post: screenshot: description: url to a screenshot of the map required: false + source_id: + description: the id of the map this map is a fork of + required: false responses: 200: body: @@ -81,6 +86,9 @@ post: screenshot: description: url to a screenshot of the map required: false + source_id: + description: the id of the map this map is a fork of + required: false responses: 200: body: diff --git a/doc/api/examples/map.json b/doc/api/examples/map.json index 78711649..d50d36fd 100644 --- a/doc/api/examples/map.json +++ b/doc/api/examples/map.json @@ -9,6 +9,7 @@ "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "user_id": 1234, + "source_id": null, "topic_ids": [ 58, 59 diff --git a/doc/api/examples/map_starred.json b/doc/api/examples/map_starred.json index 4ac6c698..1fde87d6 100644 --- a/doc/api/examples/map_starred.json +++ b/doc/api/examples/map_starred.json @@ -9,6 +9,7 @@ "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "user_id": 1234, + "source_id": null, "topic_ids": [ 58, 59 diff --git a/doc/api/examples/maps.json b/doc/api/examples/maps.json index 1e2a7baf..687f8cc5 100644 --- a/doc/api/examples/maps.json +++ b/doc/api/examples/maps.json @@ -10,6 +10,7 @@ "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "user_id": 1234, + "source_id": 2, "topic_ids": [ 58, 59 diff --git a/doc/api/schemas/_map.json b/doc/api/schemas/_map.json index d8d9c138..7e2381f8 100644 --- a/doc/api/schemas/_map.json +++ b/doc/api/schemas/_map.json @@ -33,6 +33,12 @@ "user": { "$ref": "_user.json" }, + "source_id": { + "$ref": "_optid.json" + }, + "source": { + "$ref": "_map.json" + }, "topic_ids": { "type": "array", "items": { @@ -111,6 +117,12 @@ { "required": [ "user" ] } ] }, + { + "oneOf": [ + { "required": [ "source_id" ] }, + { "required": [ "source" ] } + ] + }, { "oneOf": [ { "required": [ "topic_ids" ] }, diff --git a/doc/api/schemas/_optid.json b/doc/api/schemas/_optid.json new file mode 100644 index 00000000..c34df067 --- /dev/null +++ b/doc/api/schemas/_optid.json @@ -0,0 +1,3 @@ +{ + "type": "integer|nil" +} diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js index 9a4d8770..e7db6219 100644 --- a/frontend/src/Metamaps/GlobalUI/CreateMap.js +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -61,6 +61,7 @@ const CreateMap = { if (GlobalUI.lightbox === 'forkmap') { self.newMap.set('topicsToMap', self.topicsToMap) self.newMap.set('synapsesToMap', self.synapsesToMap) + self.newMap.set('source_id', Active.Map.id) } var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index fbf07903..f7e30751 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -5,7 +5,8 @@ require 'rails_helper' RSpec.describe 'maps API', type: :request do let(:user) { create(:user, admin: true) } let(:token) { create(:token, user: user).token } - let(:map) { create(:map, user: user) } + let(:source) { create(:map, user: user) } + let(:map) { create(:map, user: user, source: source) } describe 'GET /api/v2/maps' do it 'returns all maps' do @@ -42,7 +43,7 @@ RSpec.describe 'maps API', type: :request do expect(response).to have_http_status(:success) expect(response).to match_json_schema(:map) - expect(Map.count).to eq 2 + expect(Map.count).to eq 3 end it 'PATCH /api/v2/maps/:id' do @@ -56,7 +57,7 @@ RSpec.describe 'maps API', type: :request do delete "/api/v2/maps/#{map.id}", params: { access_token: token } expect(response).to have_http_status(:no_content) - expect(Map.count).to eq 0 + expect(Map.count).to eq 1 end it 'POST /api/v2/maps/:id/stars' do diff --git a/spec/factories/maps.rb b/spec/factories/maps.rb index a95590e4..c69b43bb 100644 --- a/spec/factories/maps.rb +++ b/spec/factories/maps.rb @@ -4,6 +4,7 @@ FactoryGirl.define do sequence(:name) { |n| "Cool Map ##{n}" } permission :commons arranged { false } + source_id nil desc '' user end From 73e8f2d4c872eccc0069498cff209032fc1ecec5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 21 Dec 2016 03:56:29 -0500 Subject: [PATCH 372/378] re-implement chat in react (#997) * hidously mangle ChatView to start moving it to React * fix up Realtime/index.js - should be good now? * in theory this should compile * ok the MapChat renders using react... * move Handlers code into react - woot * try reintegrating backbone * fix wrapper styling * chat box opens and closes properly * make the unread count work * organize more sanely * refactor some of the ChatView functions * removed management of chatview from room * css can stop handling logic right about now * makin things work * don't need room here anymore * set raw html in message * make pending work * removeParticipant when mapper left was broken * re-enable scrolling, focus, and blur --- app/assets/stylesheets/junto.css.erb | 37 +- app/views/layouts/application.html.erb | 2 + frontend/src/Metamaps/Realtime/index.js | 50 +- frontend/src/Metamaps/Realtime/receivable.js | 41 +- frontend/src/Metamaps/Realtime/sendable.js | 17 +- frontend/src/Metamaps/Views/ChatView.js | 471 ++++++------------ frontend/src/Metamaps/Views/Room.js | 73 --- frontend/src/Metamaps/Views/index.js | 3 +- frontend/src/components/MapChat/Message.js | 35 ++ .../src/components/MapChat/Participant.js | 45 ++ frontend/src/components/MapChat/Unread.js | 7 + frontend/src/components/MapChat/index.js | 167 +++++++ realtime/realtime-server.js | 1 + 13 files changed, 474 insertions(+), 475 deletions(-) create mode 100644 frontend/src/components/MapChat/Message.js create mode 100644 frontend/src/components/MapChat/Participant.js create mode 100644 frontend/src/components/MapChat/Unread.js create mode 100644 frontend/src/components/MapChat/index.js diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb index 91b610fc..f738705f 100644 --- a/app/assets/stylesheets/junto.css.erb +++ b/app/assets/stylesheets/junto.css.erb @@ -90,13 +90,16 @@ left: 30px; top: 72px; } +#chat-box-wrapper { + height: 100%; + float: right; +} .chat-box { position: relative; display: flex; flex-direction: column; z-index: 1; width: 300px; - float: right; height: 100%; background: #424242; box-shadow: -8px 0px 16px 2px rgba(0, 0, 0, 0.23); @@ -114,7 +117,6 @@ background: url(<%= asset_path 'junto_spinner_dark.gif' %>) no-repeat 2px 8px, url(<%= asset_path 'tray_tab.png' %>) no-repeat !important; } .chat-box .chat-button .chat-unread { - display: none; background: #DAB539; position: absolute; top: -3px; @@ -176,7 +178,6 @@ overflow-y: auto; } .chat-box .participants .conversation-live { - display: none; padding: 5px 10px 5px 10px; background: #c04f4f; margin: 5px 10px; @@ -187,15 +188,6 @@ cursor: pointer; color: #EBFF00; } -.chat-box .participants .conversation-live .leave { - display: none; -} -.chat-box .participants.is-participating .conversation-live .leave { - display: block; -} -.chat-box .participants.is-participating .conversation-live .join { - display: none; -} .chat-box .participants .participant { width: 89%; padding: 8px 8px 2px 8px; @@ -225,32 +217,18 @@ padding: 2px 8px 0; text-align: left; } -.chat-box .participants .participant.is-self .chat-participant-invite-call, -.chat-box .participants .participant.is-self .chat-participant-invite-join { - display: none !important; -} -.chat-box .participants.is-live .participant .chat-participant-invite-call { - display: none; -} -.chat-box .participants .participant .chat-participant-invite-join { - display: none; -} -.chat-box .participants.is-live.is-participating .participant:not(.active) .chat-participant-invite-join { - display: block; -} .chat-box .participants .participant .chat-participant-invite-call, .chat-box .participants .participant .chat-participant-invite-join { float: right; background: #4FC059 url(<%= asset_path 'invitepeer16.png' %>) no-repeat center center; } -.chat-box .participants .participant.pending .chat-participant-invite-call, -.chat-box .participants .participant.pending .chat-participant-invite-join { +.chat-box .participants .participant .chat-participant-invite-call.pending, +.chat-box .participants .participant .chat-participant-invite-join.pending { background: #dab539 url(<%= asset_path 'ellipsis.gif' %>) no-repeat center center; } .chat-box .participants .participant .chat-participant-participating { float: right; - display: none; margin-top: 14px; } .chat-box .participants .participant .chat-participant-participating .green-dot { @@ -259,9 +237,6 @@ height: 12px; border-radius: 6px; } -.chat-box .participants .participant.active .chat-participant-participating { - display: block; -} .chat-box .chat-header { width: 276px; padding: 16px 8px 16px 16px; diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2dd2a463..26164843 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,6 +9,8 @@ controller-<%= controller_name %> action-<%= action_name %>"> +
      + <%= content_tag :div, class: "main" do %> diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index 318753f0..bbb28c30 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -8,6 +8,7 @@ import DataModel from '../DataModel' import JIT from '../JIT' import Util from '../Util' import Views from '../Views' +import { ChatView } from '../Views' import Visualize from '../Visualize' import { @@ -173,48 +174,37 @@ let Realtime = { self.room = new Views.Room({ webrtc: self.webrtc, socket: self.socket, - username: Active.Mapper ? Active.Mapper.get('name') : '', - image: Active.Mapper ? Active.Mapper.get('image') : '', room: 'global', $video: self.localVideo.$video, myVideoView: self.localVideo.view, - config: { DOUBLE_CLICK_TOLERANCE: 200 }, - soundUrls: [ - serverData['sounds/MM_sounds.mp3'], - serverData['sounds/MM_sounds.ogg'] - ] + config: { DOUBLE_CLICK_TOLERANCE: 200 } }) self.room.videoAdded(self.handleVideoAdded) - - if (!Active.Map) { - self.room.chat.$container.hide() - } - $('body').prepend(self.room.chat.$container) } // if Active.Mapper }, addJuntoListeners: function() { var self = Realtime - $(document).on(Views.ChatView.events.openTray, function() { + $(document).on(ChatView.events.openTray, function() { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Views.ChatView.events.closeTray, function() { + $(document).on(ChatView.events.closeTray, function() { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Views.ChatView.events.videosOn, function() { + $(document).on(ChatView.events.videosOn, function() { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Views.ChatView.events.videosOff, function() { + $(document).on(ChatView.events.videosOff, function() { $('#wrapper').addClass('hideVideos') }) - $(document).on(Views.ChatView.events.cursorsOn, function() { + $(document).on(ChatView.events.cursorsOn, function() { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Views.ChatView.events.cursorsOff, function() { + $(document).on(ChatView.events.cursorsOff, function() { $('#wrapper').addClass('hideCursors') }) }, @@ -226,7 +216,7 @@ let Realtime = { self.setupSocket() self.setupLocalSendables() } - self.room.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) + self.setupChat() // chat can happen on public maps too } }, endActiveMap: function() { @@ -236,16 +226,14 @@ let Realtime = { if (self.inConversation) self.leaveCall() self.leaveMap() $('.collabCompass').remove() - if (self.room) { - self.room.leave() - self.room.chat.$container.hide() - self.room.chat.close() - } + if (self.room) self.room.leave() + ChatView.hide() + ChatView.close() + ChatView.reset() }, turnOn: function(notify) { var self = Realtime $('.collabCompass').show() - self.room.chat.$container.show() self.room.room = 'map-' + Active.Map.id self.activeMapper = { id: Active.Mapper.id, @@ -258,7 +246,13 @@ let Realtime = { self.localVideo.view.$container.find('.video-cutoff').css({ border: '4px solid ' + self.activeMapper.color }) - self.room.chat.addParticipant(self.activeMapper) + }, + setupChat: function() { + const self = Realtime + ChatView.setNewMap() + ChatView.addParticipant(self.activeMapper) + ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) + ChatView.show() }, setupSocket: function() { var self = Realtime @@ -332,7 +326,7 @@ let Realtime = { var createMessage = function(event, data) { self.createMessage(data) } - $(document).on(Views.Room.events.newMessage + '.map', createMessage) + $(document).on(ChatView.events.newMessage + '.map', createMessage) }, countOthersInConversation: function() { var self = Realtime @@ -403,7 +397,7 @@ let Realtime = { callEnded: function() { var self = Realtime - self.room.conversationEnding() + ChatView.conversationEnded() self.room.leaveVideoOnly() self.inConversation = false self.localVideo.view.$container.hide().css({ diff --git a/frontend/src/Metamaps/Realtime/receivable.js b/frontend/src/Metamaps/Realtime/receivable.js index 32b6bf0c..b0a8ddb4 100644 --- a/frontend/src/Metamaps/Realtime/receivable.js +++ b/frontend/src/Metamaps/Realtime/receivable.js @@ -9,6 +9,7 @@ import { indexOf } from 'lodash' import { JUNTO_UPDATED } from './events' import Active from '../Active' +import { ChatView } from '../Views' import DataModel from '../DataModel' import GlobalUI from '../GlobalUI' import Control from '../Control' @@ -152,7 +153,7 @@ export const topicCreated = self => data => { } export const messageCreated = self => data => { - self.room.addMessages(new DataModel.MessageCollection(data)) + ChatView.addMessages(new DataModel.MessageCollection(data)) } export const mapUpdated = self => data => { @@ -230,10 +231,10 @@ export const lostMapper = self => data => { // data.userid // data.username delete self.mappersOnMap[data.userid] - self.room.chat.sound.play('leavemap') + ChatView.sound.play('leavemap') // $('#mapper' + data.userid).remove() $('#compass' + data.userid).remove() - self.room.chat.removeParticipant(data.username) + ChatView.removeParticipant(ChatView.participants.findWhere({id: data.userid})) GlobalUI.notifyUser(data.username + ' just left the map') @@ -262,8 +263,8 @@ export const mapperListUpdated = self => data => { } if (data.userid !== Active.Mapper.id) { - self.room.chat.addParticipant(self.mappersOnMap[data.userid]) - if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) + ChatView.addParticipant(self.mappersOnMap[data.userid]) + if (data.userinconversation) ChatView.mapperJoinedCall(data.userid) // create a div for the collaborators compass self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) @@ -291,8 +292,8 @@ export const newMapper = self => data => { // create an item for them in the realtime box if (data.userid !== Active.Mapper.id) { - self.room.chat.sound.play('joinmap') - self.room.chat.addParticipant(self.mappersOnMap[data.userid]) + ChatView.sound.play('joinmap') + ChatView.addParticipant(self.mappersOnMap[data.userid]) // create a div for the collaborators compass self.createCompass(data.username, data.userid, data.avatar, self.mappersOnMap[data.userid].color) @@ -311,24 +312,24 @@ export const callAccepted = self => userid => { // const username = self.mappersOnMap[userid].name GlobalUI.notifyUser('Conversation starting...') self.joinCall() - self.room.chat.invitationAnswered(userid) + ChatView.invitationAnswered(userid) } export const callDenied = self => userid => { var username = self.mappersOnMap[userid].name GlobalUI.notifyUser(username + " didn't accept your invitation") - self.room.chat.invitationAnswered(userid) + ChatView.invitationAnswered(userid) } export const inviteDenied = self => userid => { var username = self.mappersOnMap[userid].name GlobalUI.notifyUser(username + " didn't accept your invitation") - self.room.chat.invitationAnswered(userid) + ChatView.invitationAnswered(userid) } export const invitedToCall = self => inviter => { - self.room.chat.sound.stop(self.soundId) - self.soundId = self.room.chat.sound.play('sessioninvite') + ChatView.sound.stop(self.soundId) + self.soundId = ChatView.sound.play('sessioninvite') var username = self.mappersOnMap[inviter].name var notifyText = '' @@ -341,8 +342,8 @@ export const invitedToCall = self => inviter => { } export const invitedToJoin = self => inviter => { - self.room.chat.sound.stop(self.soundId) - self.soundId = self.room.chat.sound.play('sessioninvite') + ChatView.sound.stop(self.soundId) + self.soundId = ChatView.sound.play('sessioninvite') var username = self.mappersOnMap[inviter].name var notifyText = username + ' is inviting you to the conversation. Join?' @@ -355,16 +356,14 @@ export const invitedToJoin = self => inviter => { export const mapperJoinedCall = self => id => { var mapper = self.mappersOnMap[id] - if (mapper) { if (self.inConversation) { var username = mapper.name var notifyText = username + ' joined the call' GlobalUI.notifyUser(notifyText) } - mapper.inConversation = true - self.room.chat.mapperJoinedCall(id) + ChatView.mapperJoinedCall(id) } } @@ -377,7 +376,7 @@ export const mapperLeftCall = self => id => { GlobalUI.notifyUser(notifyText) } mapper.inConversation = false - self.room.chat.mapperLeftCall(id) + ChatView.mapperLeftCall(id) if ((self.inConversation && self.countOthersInConversation() === 0) || (!self.inConversation && self.countOthersInConversation() === 1)) { self.callEnded() @@ -392,8 +391,7 @@ export const callInProgress = self => () => { GlobalUI.notifyUser(notifyText, true) $('#toast button.yes').click(e => self.joinCall()) $('#toast button.no').click(e => GlobalUI.clearNotify()) - - self.room.conversationInProgress() + ChatView.conversationInProgress() } export const callStarted = self => () => { @@ -404,7 +402,6 @@ export const callStarted = self => () => { GlobalUI.notifyUser(notifyText, true) $('#toast button.yes').click(e => self.joinCall()) $('#toast button.no').click(e => GlobalUI.clearNotify()) - - self.room.conversationInProgress() + ChatView.conversationInProgress() } diff --git a/frontend/src/Metamaps/Realtime/sendable.js b/frontend/src/Metamaps/Realtime/sendable.js index ef35cb85..9a45d94b 100644 --- a/frontend/src/Metamaps/Realtime/sendable.js +++ b/frontend/src/Metamaps/Realtime/sendable.js @@ -1,6 +1,7 @@ /* global $ */ import Active from '../Active' +import { ChatView } from '../Views' import GlobalUI from '../GlobalUI' import { @@ -72,6 +73,7 @@ export const joinCall = self => () => { $('#wrapper').append(self.localVideo.view.$container) } self.room.join() + ChatView.conversationInProgress(true) }) self.inConversation = true self.socket.emit(JOIN_CALL, { @@ -80,7 +82,7 @@ export const joinCall = self => () => { }) self.webrtc.startLocalVideo() GlobalUI.clearNotify() - self.room.chat.mapperJoinedCall(Active.Mapper.id) + ChatView.mapperJoinedCall(Active.Mapper.id) } export const leaveCall = self => () => { @@ -89,7 +91,8 @@ export const leaveCall = self => () => { id: Active.Mapper.id }) - self.room.chat.mapperLeftCall(Active.Mapper.id) + ChatView.mapperLeftCall(Active.Mapper.id) + ChatView.leaveConversation() // the conversation will carry on without you self.room.leaveVideoOnly() self.inConversation = false self.localVideo.view.$container.hide() @@ -102,7 +105,7 @@ export const leaveCall = self => () => { } export const acceptCall = self => userid => { - self.room.chat.sound.stop(self.soundId) + ChatView.sound.stop(self.soundId) self.socket.emit(ACCEPT_CALL, { mapid: Active.Map.id, invited: Active.Mapper.id, @@ -114,7 +117,7 @@ export const acceptCall = self => userid => { } export const denyCall = self => userid => { - self.room.chat.sound.stop(self.soundId) + ChatView.sound.stop(self.soundId) self.socket.emit(DENY_CALL, { mapid: Active.Map.id, invited: Active.Mapper.id, @@ -124,7 +127,7 @@ export const denyCall = self => userid => { } export const denyInvite = self => userid => { - self.room.chat.sound.stop(self.soundId) + ChatView.sound.stop(self.soundId) self.socket.emit(DENY_INVITE, { mapid: Active.Map.id, invited: Active.Mapper.id, @@ -139,7 +142,7 @@ export const inviteACall = self => userid => { inviter: Active.Mapper.id, invited: userid }) - self.room.chat.invitationPending(userid) + ChatView.invitationPending(userid) GlobalUI.clearNotify() } @@ -149,7 +152,7 @@ export const inviteToJoin = self => userid => { inviter: Active.Mapper.id, invited: userid }) - self.room.chat.invitationPending(userid) + ChatView.invitationPending(userid) } export const sendCoords = self => coords => { diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 590dd775..55a7b076 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -2,128 +2,27 @@ import Backbone from 'backbone' import { Howl } from 'howler' -import Autolinker from 'autolinker' -import { clone, template as lodashTemplate } from 'lodash' -import outdent from 'outdent' +import React from 'react' +import ReactDOM from 'react-dom' // TODO is this line good or bad // Backbone.$ = window.$ -const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) +import Active from '../Active' +import DataModel from '../DataModel' +import Realtime from '../Realtime' +import MapChat from '../../components/MapChat' -var Private = { - messageHTML: outdent` -
      -
      -
      {{ message }}
      -
      {{ timestamp }}
      -
      -
      `, - participantHTML: outdent` -
      -
      - -
      -
      - {{ username }} {{ selfName }} -
      - - - -
      -
      -
      -
      `, - templates: function() { - const templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - } - - this.messageTemplate = lodashTemplate(Private.messageHTML, templateSettings) - - this.participantTemplate = lodashTemplate(Private.participantHTML, templateSettings) - }, - createElements: function() { - this.$unread = $('
      ') - this.$button = $('
      Chat
      ') - this.$messageInput = $('') - this.$juntoHeader = $('
      PARTICIPANTS
      ') - this.$videoToggle = $('
      ') - this.$cursorToggle = $('
      ') - this.$participants = $('
      ') - this.$conversationInProgress = $(outdent` -
      - LIVE - - LEAVE - - - JOIN - -
      `) - this.$chatHeader = $('
      CHAT
      ') - this.$soundToggle = $('
      ') - this.$messages = $('
      ') - this.$container = $('
      ') - }, - attachElements: function() { - this.$button.append(this.$unread) - - this.$juntoHeader.append(this.$videoToggle) - this.$juntoHeader.append(this.$cursorToggle) - - this.$chatHeader.append(this.$soundToggle) - - this.$participants.append(this.$conversationInProgress) - - this.$container.append(this.$juntoHeader) - this.$container.append(this.$participants) - this.$container.append(this.$chatHeader) - this.$container.append(this.$button) - this.$container.append(this.$messages) - this.$container.append(this.$messageInput) - }, - addEventListeners: function() { - var self = this - - this.participants.on('add', function(participant) { - Private.addParticipant.call(self, participant) - }) - - this.participants.on('remove', function(participant) { - Private.removeParticipant.call(self, participant) - }) - - this.$button.on('click', function() { - Handlers.buttonClick.call(self) - }) - this.$videoToggle.on('click', function() { - Handlers.videoToggleClick.call(self) - }) - this.$cursorToggle.on('click', function() { - Handlers.cursorToggleClick.call(self) - }) - this.$soundToggle.on('click', function() { - Handlers.soundToggleClick.call(self) - }) - this.$messageInput.on('keyup', function(event) { - Handlers.keyUp.call(self, event) - }) - this.$messageInput.on('focus', function() { - Handlers.inputFocus.call(self) - }) - this.$messageInput.on('blur', function() { - Handlers.inputBlur.call(self) - }) - }, - initializeSounds: function(soundUrls) { - this.sound = new Howl({ - src: soundUrls, +const ChatView = { + isOpen: false, + messages: new Backbone.Collection(), + conversationLive: false, + isParticipating: false, + mapChat: null, + domId: 'chat-box-wrapper', + init: function(urls) { + const self = ChatView + self.sound = new Howl({ + src: urls, sprite: { joinmap: [0, 561], leavemap: [1000, 592], @@ -133,226 +32,172 @@ var Private = { } }) }, - incrementUnread: function() { - this.unreadMessages++ - this.$unread.html(this.unreadMessages) - this.$unread.show() + setNewMap: function() { + const self = ChatView + self.conversationLive = false + self.isParticipating = false + self.alertSound = true // whether to play sounds on arrival of new messages or not + self.cursorsShowing = true + self.videosShowing = true + self.participants = new Backbone.Collection() + self.render() }, - addMessage: function(message, isInitial, wasMe) { - if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) - - function addZero(i) { - if (i < 10) { - i = '0' + i - } - return i - } - var m = clone(message.attributes) - - m.timestamp = new Date(m.created_at) - - var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate() - date += ' ' + addZero(m.timestamp.getHours()) + ':' + addZero(m.timestamp.getMinutes()) - m.timestamp = date - m.image = m.user_image - m.message = linker.link(m.message) - var $html = $(this.messageTemplate(m)) - this.$messages.append($html) - if (!isInitial) this.scrollMessages(200) - - if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat') + show: () => { + $('#' + ChatView.domId).show() }, - initialMessages: function() { - var messages = this.messages.models - for (var i = 0; i < messages.length; i++) { - Private.addMessage.call(this, messages[i], true) - } + hide: () => { + $('#' + ChatView.domId).hide() }, - handleInputMessage: function() { - var message = { - message: this.$messageInput.val() - } - this.$messageInput.val('') - $(document).trigger(ChatView.events.message + '-' + this.room, [message]) + render: () => { + if (!Active.Map) return + const self = ChatView + self.mapChat = ReactDOM.render(React.createElement(MapChat, { + conversationLive: self.conversationLive, + isParticipating: self.isParticipating, + onOpen: self.onOpen, + onClose: self.onClose, + leaveCall: Realtime.leaveCall, + joinCall: Realtime.joinCall, + inviteACall: Realtime.inviteACall, + inviteToJoin: Realtime.inviteToJoin, + participants: self.participants.models.map(p => p.attributes), + messages: self.messages.models.map(m => m.attributes), + videoToggleClick: self.videoToggleClick, + cursorToggleClick: self.cursorToggleClick, + soundToggleClick: self.soundToggleClick, + inputBlur: self.inputBlur, + inputFocus: self.inputFocus, + handleInputMessage: self.handleInputMessage + }), document.getElementById(ChatView.domId)) }, - addParticipant: function(participant) { - var p = clone(participant.attributes) - if (p.self) { - p.selfClass = 'is-self' - p.selfName = '(me)' - } else { - p.selfClass = '' - p.selfName = '' - } - var html = this.participantTemplate(p) - this.$participants.append(html) + onOpen: () => { + $(document).trigger(ChatView.events.openTray) }, - removeParticipant: function(participant) { - this.$container.find('.participant-' + participant.get('id')).remove() - } -} - -var Handlers = { - buttonClick: function() { - if (this.isOpen) this.close() - else if (!this.isOpen) this.open() + onClose: () => { + $(document).trigger(ChatView.events.closeTray) + }, + addParticipant: participant => { + ChatView.participants.add(participant) + ChatView.render() + }, + removeParticipant: participant => { + ChatView.participants.remove(participant) + ChatView.render() + }, + leaveConversation: () => { + ChatView.isParticipating = false + ChatView.render() + }, + mapperJoinedCall: id => { + const mapper = ChatView.participants.findWhere({id}) + mapper && mapper.set('isParticipating', true) + ChatView.render() + }, + mapperLeftCall: id => { + const mapper = ChatView.participants.findWhere({id}) + mapper && mapper.set('isParticipating', false) + ChatView.render() + }, + invitationPending: id => { + const mapper = ChatView.participants.findWhere({id}) + mapper && mapper.set('isPending', true) + ChatView.render() + }, + invitationAnswered: id => { + const mapper = ChatView.participants.findWhere({id}) + mapper && mapper.set('isPending', false) + ChatView.render() + }, + conversationInProgress: participating => { + ChatView.conversationLive = true + ChatView.isParticipating = participating + ChatView.render() + }, + conversationEnded: () => { + ChatView.conversationLive = false + ChatView.isParticipating = false + ChatView.participants.forEach(p => p.set({isParticipating: false, isPending: false})) + ChatView.render() + }, + close: () => { + ChatView.mapChat.close() + }, + open: () => { + ChatView.mapChat.open() }, videoToggleClick: function() { - this.$videoToggle.toggleClass('active') - this.videosShowing = !this.videosShowing - $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) + ChatView.videosShowing = !ChatView.videosShowing + $(document).trigger(ChatView.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) }, cursorToggleClick: function() { - this.$cursorToggle.toggleClass('active') - this.cursorsShowing = !this.cursorsShowing - $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) + ChatView.cursorsShowing = !ChatView.cursorsShowing + $(document).trigger(ChatView.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) }, soundToggleClick: function() { - this.alertSound = !this.alertSound - this.$soundToggle.toggleClass('active') + ChatView.alertSound = !ChatView.alertSound }, - keyUp: function(event) { - switch (event.which) { - case 13: // enter - Private.handleInputMessage.call(this) - break - } - }, - inputFocus: function() { + inputFocus: () => { $(document).trigger(ChatView.events.inputFocus) }, - inputBlur: function() { + inputBlur: () => { $(document).trigger(ChatView.events.inputBlur) + }, + addMessage: (message, isInitial, wasMe) => { + const self = ChatView + if (!isInitial) self.mapChat.newMessage() + if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat') + self.messages.add(message) + self.render() + if (!isInitial) self.mapChat.scroll() + }, + sendChatMessage: message => { + var self = ChatView + if (ChatView.alertSound) ChatView.sound.play('sendchat') + var m = new DataModel.Message({ + message: message.message, + resource_id: Active.Map.id, + resource_type: 'Map' + }) + m.save(null, { + success: function(model, response) { + self.addMessages(new DataModel.MessageCollection(model), false, true) + $(document).trigger(ChatView.events.newMessage, [model]) + }, + error: function(model, response) { + console.log('error!', response) + } + }) + }, + handleInputMessage: text => { + ChatView.sendChatMessage({message: text}) + }, + // they should be instantiated as backbone models before they get + // passed to this function + addMessages: (messages, isInitial, wasMe) => { + messages.models.forEach(m => ChatView.addMessage(m, isInitial, wasMe)) + }, + reset: () => { + ChatView.mapChat.reset() + ChatView.participants.reset() + ChatView.messages.reset() + ChatView.render() } } -const ChatView = function(messages, mapper, room, opts = {}) { - this.room = room - this.mapper = mapper - this.messages = messages // backbone collection +// ChatView.prototype.scrollMessages = function(duration) { +// duration = duration || 0 - this.isOpen = false - this.alertSound = true // whether to play sounds on arrival of new messages or not - this.cursorsShowing = true - this.videosShowing = true - this.unreadMessages = 0 - this.participants = new Backbone.Collection() - - Private.templates.call(this) - Private.createElements.call(this) - Private.attachElements.call(this) - Private.addEventListeners.call(this) - Private.initialMessages.call(this) - Private.initializeSounds.call(this, opts.soundUrls) - this.$container.css({ - right: '-300px' - }) -} - -ChatView.prototype.conversationInProgress = function(participating) { - this.$conversationInProgress.show() - this.$participants.addClass('is-live') - if (participating) this.$participants.addClass('is-participating') - this.$button.addClass('active') - -// hide invite to call buttons -} - -ChatView.prototype.conversationEnded = function() { - this.$conversationInProgress.hide() - this.$participants.removeClass('is-live') - this.$participants.removeClass('is-participating') - this.$button.removeClass('active') - this.$participants.find('.participant').removeClass('active') - this.$participants.find('.participant').removeClass('pending') -} - -ChatView.prototype.leaveConversation = function() { - this.$participants.removeClass('is-participating') -} - -ChatView.prototype.mapperJoinedCall = function(id) { - this.$participants.find('.participant-' + id).addClass('active') -} - -ChatView.prototype.mapperLeftCall = function(id) { - this.$participants.find('.participant-' + id).removeClass('active') -} - -ChatView.prototype.invitationPending = function(id) { - this.$participants.find('.participant-' + id).addClass('pending') -} - -ChatView.prototype.invitationAnswered = function(id) { - this.$participants.find('.participant-' + id).removeClass('pending') -} - -ChatView.prototype.addParticipant = function(participant) { - this.participants.add(participant) -} - -ChatView.prototype.removeParticipant = function(username) { - var p = this.participants.find(p => p.get('username') === username) - if (p) { - this.participants.remove(p) - } -} - -ChatView.prototype.removeParticipants = function() { - this.participants.remove(this.participants.models) -} - -ChatView.prototype.open = function() { - this.$container.css({ - right: '0' - }) - this.$messageInput.focus() - this.isOpen = true - this.unreadMessages = 0 - this.$unread.hide() - this.scrollMessages(0) - $(document).trigger(ChatView.events.openTray) -} - -ChatView.prototype.addMessage = function(message, isInitial, wasMe) { - this.messages.add(message) - Private.addMessage.call(this, message, isInitial, wasMe) -} - -ChatView.prototype.scrollMessages = function(duration) { - duration = duration || 0 - - this.$messages.animate({ - scrollTop: this.$messages[0].scrollHeight - }, duration) -} - -ChatView.prototype.clearMessages = function() { - this.unreadMessages = 0 - this.$unread.hide() - this.$messages.empty() -} - -ChatView.prototype.close = function() { - this.$container.css({ - right: '-300px' - }) - this.$messageInput.blur() - this.isOpen = false - $(document).trigger(ChatView.events.closeTray) -} - -ChatView.prototype.remove = function() { - this.$button.off() - this.$container.remove() -} +// this.$messages.animate({ +// scrollTop: this.$messages[0].scrollHeight +// }, duration) +// } /** * @class * @static */ ChatView.events = { - message: 'ChatView:message', + newMessage: 'ChatView:newMessage', openTray: 'ChatView:openTray', closeTray: 'ChatView:closeTray', inputFocus: 'ChatView:inputFocus', diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index a3a79cc8..3dc43708 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -9,8 +9,6 @@ import attachMediaStream from 'attachmediastream' import Active from '../Active' import DataModel from '../DataModel' import Realtime from '../Realtime' - -import ChatView from './ChatView' import VideoView from './VideoView' const Room = function(opts = {}) { @@ -19,38 +17,18 @@ const Room = function(opts = {}) { this.webrtc = opts.webrtc this.room = opts.room this.config = opts.config - this.peopleCount = 0 - this.$myVideo = opts.$video this.myVideo = opts.myVideoView - - this.messages = new Backbone.Collection() - this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }) - this.chat = new ChatView(this.messages, this.currentMapper, this.room, { - soundUrls: opts.soundUrls - }) - this.videos = {} - this.init() } Room.prototype.join = function(cb) { this.isActiveRoom = true this.webrtc.joinRoom(this.room, cb) - this.chat.conversationInProgress(true) // true indicates participation -} - -Room.prototype.conversationInProgress = function() { - this.chat.conversationInProgress(false) // false indicates not participating -} - -Room.prototype.conversationEnding = function() { - this.chat.conversationEnded() } Room.prototype.leaveVideoOnly = function() { - this.chat.leaveConversation() // the conversation will carry on without you for (var id in this.videos) { this.removeVideo(id) } @@ -66,14 +44,6 @@ Room.prototype.leave = function() { this.isActiveRoom = false this.webrtc.leaveRoom() this.webrtc.stopLocalVideo() - this.chat.conversationEnded() - this.chat.removeParticipants() - this.chat.clearMessages() - this.messages.reset() -} - -Room.prototype.setPeopleCount = function(count) { - this.peopleCount = count } Room.prototype.init = function() { @@ -129,11 +99,6 @@ Room.prototype.init = function() { } v.$container.show() }) - - var sendChatMessage = function(event, data) { - self.sendChatMessage(data) - } - $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage) } Room.prototype.videoAdded = function(callback) { @@ -158,42 +123,4 @@ Room.prototype.removeVideo = function(peer) { } } -Room.prototype.sendChatMessage = function(data) { - var self = this - // this.roomRef.child('messages').push(data) - if (self.chat.alertSound) self.chat.sound.play('sendchat') - var m = new DataModel.Message({ - message: data.message, - resource_id: Active.Map.id, - resource_type: 'Map' - }) - m.save(null, { - success: function(model, response) { - self.addMessages(new DataModel.MessageCollection(model), false, true) - $(document).trigger(Room.events.newMessage, [model]) - }, - error: function(model, response) { - console.log('error!', response) - } - }) -} - - // they should be instantiated as backbone models before they get - // passed to this function -Room.prototype.addMessages = function(messages, isInitial, wasMe) { - var self = this - - messages.models.forEach(function(message) { - self.chat.addMessage(message, isInitial, wasMe) - }) -} - -/** - * @class - * @static - */ -Room.events = { - newMessage: 'Room:newMessage' -} - export default Room diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index 89d22ad7..ab96e552 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -7,8 +7,9 @@ import Room from './Room' import { JUNTO_UPDATED } from '../Realtime/events' const Views = { - init: () => { + init: (serverData) => { $(document).on(JUNTO_UPDATED, () => ExploreMaps.render()) + ChatView.init([serverData['sounds/MM_sounds.mp3'],serverData['sounds/MM_sounds.ogg']]) }, ExploreMaps, ChatView, diff --git a/frontend/src/components/MapChat/Message.js b/frontend/src/components/MapChat/Message.js new file mode 100644 index 00000000..b9ddeda1 --- /dev/null +++ b/frontend/src/components/MapChat/Message.js @@ -0,0 +1,35 @@ +import React from 'react' +import Autolinker from 'autolinker' + +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) + +function addZero(i) { + if (i < 10) { + i = '0' + i + } + return i +} + +function formatDate(created_at) { + let date = new Date(created_at) + let formatted = (date.getMonth() + 1) + '/' + date.getDate() + formatted += ' ' + addZero(date.getHours()) + ':' + addZero(date.getMinutes()) + return formatted +} + +const Message = props => { + const { user_image, user_name, message, created_at } = props + const messageHtml = {__html: linker.link(message)} + return ( +
      +
      + +
      +
      +
      {formatDate(created_at)}
      +
      +
      + ) +} + +export default Message diff --git a/frontend/src/components/MapChat/Participant.js b/frontend/src/components/MapChat/Participant.js new file mode 100644 index 00000000..340c92a4 --- /dev/null +++ b/frontend/src/components/MapChat/Participant.js @@ -0,0 +1,45 @@ +import React, { PropTypes, Component } from 'react' + +class Participant extends Component { + render() { + const { conversationLive, mapperIsLive, isParticipating, isPending, id, self, image, username, selfName, color } = this.props + return ( +
      +
      + +
      +
      + {username} {self ? '(me)' : ''} +
      + {!self && !conversationLive &&
      + ) + } +} + +Participant.propTypes = { + conversationLive: PropTypes.bool, + mapperIsLive: PropTypes.bool, + isParticipating: PropTypes.bool, + isPending: PropTypes.bool, + color: PropTypes.string, // css color + id: PropTypes.number, + image: PropTypes.string, // image url + self: PropTypes.bool, + username: PropTypes.string, + inviteACall: PropTypes.func, + inviteToJoin: PropTypes.func +} + +export default Participant diff --git a/frontend/src/components/MapChat/Unread.js b/frontend/src/components/MapChat/Unread.js new file mode 100644 index 00000000..7bd1d23c --- /dev/null +++ b/frontend/src/components/MapChat/Unread.js @@ -0,0 +1,7 @@ +import React from 'react' + +const Unread = props => { + return props.count ?
      {props.count}
      : null +} + +export default Unread diff --git a/frontend/src/components/MapChat/index.js b/frontend/src/components/MapChat/index.js new file mode 100644 index 00000000..101b9755 --- /dev/null +++ b/frontend/src/components/MapChat/index.js @@ -0,0 +1,167 @@ +import React, { PropTypes, Component } from 'react' +import Unread from './Unread' +import Participant from './Participant' +import Message from './Message' + +class MapChat extends Component { + constructor(props) { + super(props) + + this.state = { + unreadMessages: 0, + open: false, + messageText: '', + alertSound: true, // whether to play sounds on arrival of new messages or not + cursorsShowing: true, + videosShowing: true + } + } + + reset = () => { + this.setState({ + unreadMessages: 0, + open: false, + messageText: '', + alertSound: true, // whether to play sounds on arrival of new messages or not + cursorsShowing: true, + videosShowing: true + }) + } + + close = () => { + this.setState({open: false}) + this.props.onClose() + this.messageInput.blur() + } + + open = () => { + this.scroll() + this.setState({open: true, unreadMessages: 0}) + this.props.onOpen() + this.messageInput.focus() + } + + newMessage = () => { + if (!this.state.open) this.setState({unreadMessages: this.state.unreadMessages + 1}) + } + + scroll = () => { + this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight + } + + toggleDrawer = () => { + if (this.state.open) this.close() + else if (!this.state.open) this.open() + } + + toggleAlertSound = () => { + this.setState({alertSound: !this.state.alertSound}) + this.props.soundToggleClick() + } + + toggleCursorsShowing = () => { + this.setState({cursorsShowing: !this.state.cursorsShowing}) + this.props.cursorToggleClick() + } + + toggleVideosShowing = () => { + this.setState({videosShowing: !this.state.videosShowing}) + this.props.videoToggleClick() + } + + handleChange = key => e => { + this.setState({ + [key]: e.target.value + }) + } + + handleTextareaKeyUp = e => { + if (e.which === 13) { + e.preventDefault() + const text = this.state.messageText + this.props.handleInputMessage(text) + this.setState({ messageText: '' }) + } + } + + render = () => { + const rightOffset = this.state.open ? '0' : '-300px' + const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props + const { videosShowing, cursorsShowing, alertSound, unreadMessages } = this.state + return ( +
      +
      + PARTICIPANTS +
      +
      +
      +
      + {conversationLive &&
      + LIVE + {isParticipating && + LEAVE + } + {!isParticipating && + JOIN + } +
      } + {participants.map(participant => + )} +
      +
      + CHAT +
      +
      +
      +
      Chat
      + +
      +
      this.messagesDiv = div}> + {messages.map(message => )} +
      +