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