diff --git a/app/assets/images/addcollab_sprite.png b/app/assets/images/addcollab_sprite.png new file mode 100644 index 00000000..d3f49872 Binary files /dev/null and b/app/assets/images/addcollab_sprite.png differ diff --git a/app/assets/images/mm_logo.png b/app/assets/images/mm_logo.png new file mode 100644 index 00000000..af104b09 Binary files /dev/null and b/app/assets/images/mm_logo.png differ diff --git a/app/assets/images/removecollab_sprite.png b/app/assets/images/removecollab_sprite.png new file mode 100644 index 00000000..2036da5e Binary files /dev/null and b/app/assets/images/removecollab_sprite.png differ 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 @@
+ +