From 6c7ce56e4faaceb26e686784c32e574079b56eb5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 21 Apr 2016 18:54:26 -0400 Subject: [PATCH] yeehaw add collaborators --- app/assets/images/addcollab_sprite.png | Bin 0 -> 322 bytes app/assets/images/mm_logo.png | Bin 0 -> 3117 bytes app/assets/images/removecollab_sprite.png | Bin 0 -> 418 bytes .../javascripts/src/Metamaps.Map.js.erb | 100 +++++++++++++++++- app/assets/stylesheets/application.css.erb | 95 ++++++++++++++++- app/controllers/main_controller.rb | 5 +- app/controllers/maps_controller.rb | 5 +- app/mailers/application_mailer.rb | 4 + app/mailers/map_mailer.rb | 11 ++ app/views/layouts/_templates.html.erb | 14 ++- app/views/layouts/mailer.html.erb | 5 + app/views/layouts/mailer.text.erb | 1 + .../map_mailer/invite_to_edit_email.html.erb | 13 +++ .../map_mailer/invite_to_edit_email.text.erb | 7 ++ app/views/maps/_mapinfobox.html.erb | 22 +++- config/environments/development.rb | 2 +- spec/mailers/map_mailer_spec.rb | 5 + spec/mailers/previews/map_mailer_preview.rb | 4 + 18 files changed, 277 insertions(+), 16 deletions(-) create mode 100644 app/assets/images/addcollab_sprite.png create mode 100644 app/assets/images/mm_logo.png create mode 100644 app/assets/images/removecollab_sprite.png create mode 100644 app/mailers/application_mailer.rb create mode 100644 app/mailers/map_mailer.rb create mode 100644 app/views/layouts/mailer.html.erb create mode 100644 app/views/layouts/mailer.text.erb create mode 100644 app/views/map_mailer/invite_to_edit_email.html.erb create mode 100644 app/views/map_mailer/invite_to_edit_email.text.erb create mode 100644 spec/mailers/map_mailer_spec.rb create mode 100644 spec/mailers/previews/map_mailer_preview.rb diff --git a/app/assets/images/addcollab_sprite.png b/app/assets/images/addcollab_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..d3f498727be02e86f7700c0c003182e60819827e GIT binary patch literal 322 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 G#r+R$Lgv{3 literal 0 HcmV?d00001 diff --git a/app/assets/images/removecollab_sprite.png b/app/assets/images/removecollab_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..2036da5e480b6a28603cb4fd6a2658f3f7f8f348 GIT binary patch literal 418 zcmV;T0bTxyP)WXXB1#m`9M318Ae5G`;TLSEb3)^dArJcxXp zYgX66(;G0h3AI2VH&GNx14wv@+$2fzJph`%lqQ_Kl#*yV3Xi%{Re>F1uQVt$ayCS+ z;W@b^!+PN^N`pXC0NeJ1hUW^9WLU5_=N1mgFdO*pgkxpcyxB&0(=jsa#Zwu2Oon{n z3SqG{8J>iN8^lX8>})$zhblvm)lYDQ-|4m!bzDiV#k+t1H~a`N0RI!LXwINArT_o{ M07*qoM6N<$g4WEcE&u=k literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/src/Metamaps.Map.js.erb b/app/assets/javascripts/src/Metamaps.Map.js.erb index f844a552..d5330e95 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js.erb +++ b/app/assets/javascripts/src/Metamaps.Map.js.erb @@ -434,6 +434,8 @@ Metamaps.Map.InfoBox = { self.attachEventListeners() + + self.generateBoxHTML = Hogan.compile($('#mapInfoBoxTemplate').html()) }, toggleBox: function (event) { @@ -474,7 +476,7 @@ Metamaps.Map.InfoBox = { var map = Metamaps.Active.Map - var obj = map.pick('permission', 'contributor_count', 'topic_count', 'synapse_count') + var obj = map.pick('permission', 'topic_count', 'synapse_count') var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) @@ -485,6 +487,7 @@ Metamaps.Map.InfoBox = { obj['desc'] = canEdit ? Hogan.compile(self.descHTML).render({id: map.id, desc: map.get('desc')}) : map.get('desc') obj['map_creator_tip'] = isCreator ? self.changePermissionText : '' + 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') : "<%= asset_path('user.png') %>" @@ -558,6 +561,85 @@ Metamaps.Map.InfoBox = { $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { $('.mapContributors .tip').hide() }) + + self.addTypeahead() + }, + addTypeahead: function () { + var self = Metamaps.Map.InfoBox + + // 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: "<%= asset_path('user.png') %>", + }); + }, + 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 (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) { + $('.collaboratorSearchField').typeahead( + { + highlight: false, + }, + [collaborators] + ) + $('.collaboratorSearchField').bind('typeahead:select', self.handleResultClick) + $('.mapContributors .removeCollaborator').click(function () { + self.removeCollaborator(parseInt($(this).data('id'))) + }) + } + }, + removeCollaborator: function (collaboratorId) { + 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 }) + self.updateNumbers() + }, + addCollaborator: function (newCollaboratorId) { + var self = Metamaps.Map.InfoBox + + if (Metamaps.Collaborators.get(newCollaboratorId)) { + Metamaps.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 }) + var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + Metamaps.GlobalUI.notifyUser(name + ' will be notified by email') + self.updateNumbers() + } + + $.getJSON('/users/' + newCollaboratorId + '.json', callback) + }, + handleResultClick: function (event, item) { + var self = Metamaps.Map.InfoBox + + self.addCollaborator(item.id) + $('.collaboratorSearchField').typeahead('val', '') }, updateNameDescPerm: function (name, desc, perm) { $('.mapInfoName .best_in_place_name').html(name) @@ -567,14 +649,24 @@ 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 string = '' string += '' + + if (activeMapperIsCreator) { + string += '
' + } return string }, updateNumbers: function () { @@ -594,6 +686,10 @@ Metamaps.Map.InfoBox = { $('.mapContributors img').attr('src', contributors_image).removeClass('multiple mTwo').addClass(contributors_class) $('.mapContributors span').text(relevantPeople.length) $('.mapContributors .tip').html(self.createContributorList()) + self.addTypeahead() + $('.mapContributors .tip').unbind().click(function (event) { + event.stopPropagation() + }) $('.mapTopics').text(Metamaps.Topics.length) $('.mapSynapses').text(Metamaps.Synapses.length) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index 5970c18b..fa2383b8 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -1569,6 +1569,11 @@ h3.filterBox { background-repeat: no-repeat; text-align: left; } + +.commonsMap .mapContributors { + visibility: hidden; +} + .mapContributors { position: relative; height: 30px; @@ -1576,6 +1581,7 @@ h3.filterBox { padding: 0; width: 64px; } + #mapContribs { float: left; border: 2px solid #424242; @@ -1591,7 +1597,7 @@ h3.filterBox { #mapContribs.multiple { box-shadow: 1px 1px 0 0 #B5B5B5,3px 2px 0 0 #424242,4px 3px 0 0 #B5B5B5,5px 4px 0 0 #424242; } -.mapContributors span { +.mapContributors span.count { height: 20px; padding-top: 5px; padding-left: 8px; @@ -1626,6 +1632,7 @@ h3.filterBox { .mapContributors .tip { top: 45px; left: -10px; + min-width: 200px; } .mapContributors .tip ul { @@ -1652,7 +1659,7 @@ h3.filterBox { .mapContributors .tip li a { color: white; } -.mapContributors div:after { +.mapContributors div.tip:after { content: ''; position: absolute; top: -4px; @@ -1672,6 +1679,90 @@ h3.filterBox { border-radius: 14px; } +.mapContributors span.twitter-typeahead { + padding: 0; +} + +.collabSearchField { + text-align: left; +} + +.collabNameWrapper { + float: left; +} + +.collabIconWrapper img.icon { + position: relative; + top: 0; + left: 0; +} + +.collabIconWrapper { + position: relative; + float: left; + padding: 0 4px; +} + +.mapContributors .collabName { + font-weight: normal; + font-size: 14px; + line-height: 28px; + color: #424242; + padding: 0 4px; +} + +span.removeCollaborator { + position: absolute; + top: 11px; + right: 8px; + height: 16px; + width: 16px; + background-image: url(<%= asset_data_uri('removecollab_sprite.png') %>); + cursor: pointer; +} + +span.removeCollaborator:hover { + background-position: -16px 0; +} + +span.addCollab { + width: 16px; + height: 16px; + background-image: url(<%= asset_data_uri('addcollab_sprite.png') %>); + display: inline-block; + vertical-align: middle; + margin: 0 12px 0 10px; +} + +input.collaboratorSearchField { + background: #FFFFFF; + height: 14px; + margin: 0; + padding: 10px 6px; + border: none; + border-radius: 2px; + outline: none; + font-size: 14px; + line-height: 14px; + color: #424242; + font-family: 'din-medium', helvetica, sans-serif; +} + +.tt-dataset.tt-dataset-collaborators { + padding: 2px; + background: #E0E0E0; + min-width: 156px; + border-radius: 2px; +} + +.tt-dataset.tt-dataset .collabResult { + padding: 4px; +} + +.collabResult.tt-suggestion.tt-cursor, .collabResult.tt-suggestion:hover { + background-color: #CCCCCC; +} + .mapInfoBox .mapPermission .tooltips { top: -20px; right: 36px; diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index d8ce06b6..3af9c926 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,7 +4,7 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper - after_action :verify_policy_scoped, except: :requestinvite + after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers] respond_to :html, :json @@ -133,8 +133,7 @@ class MainController < ApplicationController #remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == "mapper:" search = term.downcase + '%' - builder = policy_scope(User) # TODO do I need to policy scope? I guess yes to verify_policy_scoped - builder = builder.where('LOWER("name") like ?', search) + builder = User.where('LOWER("name") like ?', search) @mappers = builder.order(:name) else @mappers = [] diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 78bc869a..c079361b 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -241,10 +241,11 @@ class MapsController < ApplicationController not @map.collaborators.include?(user) end } - removed = @map.collaborators.select { |user| not userIds.include?(user.id) }.map(&:id) + removed = @map.collaborators.select { |user| not userIds.include?(user.id.to_s) }.map(&:id) added.each { |uid| um = UserMap.create({ user_id: uid.to_i, map_id: @map.id }) - # send email here + user = User.find(uid.to_i) + MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later } removed.each { |uid| @map.user_maps.select{ |um| um.user_id == uid }.each{ |um| um.destroy } diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..0d9431a3 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: "team@metamaps.cc" + layout 'mailer' +end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb new file mode 100644 index 00000000..a31f6fe0 --- /dev/null +++ b/app/mailers/map_mailer.rb @@ -0,0 +1,11 @@ +class MapMailer < ApplicationMailer + default from: "team@metamaps.cc" + + def invite_to_edit_email(map, inviter, invitee) + @inviter = inviter + @map = map + @url = map_url(@map) + subject = @map.name + ' - Invitation to edit' + mail(to: invitee.email, subject: subject) + end +end diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index b3099ad9..7e5a6293 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -12,7 +12,7 @@
- {{contributor_count}} + {{contributor_count}}
{{{contributor_list}}}
@@ -181,6 +181,18 @@
+ +