From 80c42753e112095b5a24f0dbb07086df83016407 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Jun 2013 17:17:01 +1000 Subject: [PATCH] fix up find as you type for the invite into PM function allow mods to remove users from a PM --- .../discourse/components/autocomplete.js | 39 ++++++++++++++++--- .../controllers/invite_private_controller.js | 15 ++++++- .../discourse/controllers/topic_controller.js | 4 ++ .../javascripts/discourse/models/topic.js | 12 ++++++ .../discourse/routes/discourse_route.js | 3 ++ .../modal/invite_private.js.handlebars | 4 +- .../private_message.js.handlebars | 3 ++ .../views/modal/invite_private_view.js | 11 +----- .../discourse/views/user_selector_view.js | 4 +- .../stylesheets/application/compose.css.scss | 4 +- .../stylesheets/application/modal.css.scss | 7 ++++ app/controllers/topics_controller.rb | 19 ++++++++- app/models/topic.rb | 7 ++++ app/serializers/topic_view_serializer.rb | 2 +- config/routes.rb | 1 + lib/guardian.rb | 4 ++ spec/models/topic_spec.rb | 6 ++- 17 files changed, 119 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/components/autocomplete.js b/app/assets/javascripts/discourse/components/autocomplete.js index 36e6dc590ac..d4d7ecc0a62 100644 --- a/app/assets/javascripts/discourse/components/autocomplete.js +++ b/app/assets/javascripts/discourse/components/autocomplete.js @@ -46,6 +46,10 @@ $.fn.autocomplete = function(options) { if (options.transformComplete) { transformed = options.transformComplete(item); } + if (options.single){ + // dump what we have in single mode, just in case + inputSelectedItems = []; + } var d = $("
" + (transformed || item) + "
"); var prev = me.parent().find('.item:last'); if (prev.length === 0) { @@ -57,12 +61,16 @@ $.fn.autocomplete = function(options) { if (options.onChangeItems) { options.onChangeItems(inputSelectedItems); } - return d.find('a').click(function() { + + d.find('a').click(function() { closeAutocomplete(); inputSelectedItems.splice($.inArray(item), 1); $(this).parent().parent().remove(); + if (options.single) { + me.show(); + } if (options.onChangeItems) { - return options.onChangeItems(inputSelectedItems); + options.onChangeItems(inputSelectedItems); } }); }; @@ -71,6 +79,9 @@ $.fn.autocomplete = function(options) { if (term) { if (isInput) { me.val(""); + if(options.single){ + me.hide(); + } addInputSelectedItem(term); } else { if (options.transformComplete) { @@ -90,7 +101,11 @@ $.fn.autocomplete = function(options) { var height = this.height(); wrap = this.wrap("
").parent(); wrap.width(width); - this.width(150); + if(options.single) { + this.css("width","100%"); + } else { + this.width(150); + } this.attr('name', this.attr('name') + "-renamed"); var vals = this.val().split(","); _.each(vals,function(x) { @@ -98,7 +113,7 @@ $.fn.autocomplete = function(options) { if (options.reverseTransform) { x = options.reverseTransform(x); } - return addInputSelectedItem(x); + addInputSelectedItem(x); } }); this.val(""); @@ -185,10 +200,22 @@ $.fn.autocomplete = function(options) { if (oldClose) { oldClose(); } - return closeAutocomplete(); + closeAutocomplete(); }); $(this).keypress(function(e) { + + if(options.allowAny){ + if(inputSelectedItems.length === 0) { + inputSelectedItems.push(""); + } + inputSelectedItems.pop(); + inputSelectedItems.push(me.val()); + if (options.onChangeItems) { + options.onChangeItems(inputSelectedItems); + } + } + if (!options.key) return; // keep hunting backwards till you hit a @@ -264,7 +291,7 @@ $.fn.autocomplete = function(options) { // We're cancelling it, really. return true; } - closeAutocomplete(); + e.stopImmediatePropagation(); return false; case 38: selectedOption = selectedOption - 1; diff --git a/app/assets/javascripts/discourse/controllers/invite_private_controller.js b/app/assets/javascripts/discourse/controllers/invite_private_controller.js index 61fac5b72f1..2fbfac27420 100644 --- a/app/assets/javascripts/discourse/controllers/invite_private_controller.js +++ b/app/assets/javascripts/discourse/controllers/invite_private_controller.js @@ -9,6 +9,13 @@ **/ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, { + modalClass: 'invite', + + onShow: function(){ + this.set('controllers.modal.modalClass', 'invite-modal'); + this.set('emailOrUsername', ''); + }, + disabled: function() { if (this.get('saving')) return true; return this.blank('emailOrUsername'); @@ -27,10 +34,14 @@ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse. this.set('saving', true); this.set('error', false); // Invite the user to the private message - this.get('content').inviteUser(this.get('emailOrUsername')).then(function() { + this.get('content').inviteUser(this.get('emailOrUsername')).then(function(result) { // Success invitePrivateController.set('saving', false); invitePrivateController.set('finished', true); + + if(result && result.user) { + invitePrivateController.get('content.allowed_users').pushObject(result.user); + } }, function() { // Failure invitePrivateController.set('error', true); @@ -39,4 +50,4 @@ Discourse.InvitePrivateController = Discourse.ObjectController.extend(Discourse. return false; } -}); \ No newline at end of file +}); diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index dd49cbbbc7b..71773790737 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -429,6 +429,10 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected if (onPostRendered) { onPostRendered(post); } + }, + + removeAllowedUser: function(username) { + this.get('model').removeAllowedUser(username); } }); diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index 19863e49015..2bd9289c222 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -150,6 +150,17 @@ Discourse.Topic = Discourse.Model.extend({ }); }, + removeAllowedUser: function(username) { + var allowedUsers = this.get('allowed_users'); + + return Discourse.ajax("/t/" + this.get('id') + "/remove-allowed-user", { + type: 'PUT', + data: { username: username } + }).then(function(){ + allowedUsers.removeObject(allowedUsers.find(function(item){ return item.username === username; })); + }); + }, + favoriteTooltipKey: (function() { return this.get('starred') ? 'favorite.help.unstar' : 'favorite.help.star'; }).property('starred'), @@ -274,6 +285,7 @@ Discourse.Topic = Discourse.Model.extend({ lastPost = post; }); + topic.set('allowed_users', Em.A(result.allowed_users)); topic.set('loaded', true); } diff --git a/app/assets/javascripts/discourse/routes/discourse_route.js b/app/assets/javascripts/discourse/routes/discourse_route.js index 63ac2817602..55043edf6f6 100644 --- a/app/assets/javascripts/discourse/routes/discourse_route.js +++ b/app/assets/javascripts/discourse/routes/discourse_route.js @@ -56,6 +56,9 @@ Discourse.Route.reopenClass({ if (model) { controller.set('model', model); } + if(controller && controller.onShow) { + controller.onShow(); + } controller.set('flashMessage', null); } } diff --git a/app/assets/javascripts/discourse/templates/modal/invite_private.js.handlebars b/app/assets/javascripts/discourse/templates/modal/invite_private.js.handlebars index 4f6b5df1043..a427dc0acd3 100644 --- a/app/assets/javascripts/discourse/templates/modal/invite_private.js.handlebars +++ b/app/assets/javascripts/discourse/templates/modal/invite_private.js.handlebars @@ -10,7 +10,7 @@ {{i18n topic.invite_private.success}} {{else}} - {{textField value=emailOrUsername placeholderKey="topic.invite_private.email_or_username_placeholder"}} + {{userSelector single=true allowAny=true usernames=emailOrUsername placeholderKey="topic.invite_private.email_or_username_placeholder"}} {{/if}}
\ No newline at end of file + diff --git a/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars b/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars index 37b992cc726..830a86ed713 100644 --- a/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars +++ b/app/assets/javascripts/discourse/templates/topic_summary/private_message.js.handlebars @@ -13,6 +13,9 @@ {{unbound username}} + {{#if controller.model.can_remove_allowed_users}} + x + {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/views/modal/invite_private_view.js b/app/assets/javascripts/discourse/views/modal/invite_private_view.js index 312c7ae051a..12197ed84d6 100644 --- a/app/assets/javascripts/discourse/views/modal/invite_private_view.js +++ b/app/assets/javascripts/discourse/views/modal/invite_private_view.js @@ -8,14 +8,5 @@ **/ Discourse.InvitePrivateView = Discourse.ModalBodyView.extend({ templateName: 'modal/invite_private', - title: Em.String.i18n('topic.invite_private.title'), - - keyUp: function(e) { - // Add the invitee if they hit enter - if (e.keyCode === 13) { this.get('controller').invite(); } - return false; - } - + title: Em.String.i18n('topic.invite_private.title') }); - - diff --git a/app/assets/javascripts/discourse/views/user_selector_view.js b/app/assets/javascripts/discourse/views/user_selector_view.js index 7ce26b25542..96b6a4cce4e 100644 --- a/app/assets/javascripts/discourse/views/user_selector_view.js +++ b/app/assets/javascripts/discourse/views/user_selector_view.js @@ -1,6 +1,7 @@ Discourse.UserSelector = Discourse.TextField.extend({ didInsertElement: function(){ + var userSelectorView = this; var selected = []; var transformTemplate = Handlebars.compile("{{avatar this imageSize=\"tiny\"}} {{this.username}}"); @@ -9,7 +10,8 @@ Discourse.UserSelector = Discourse.TextField.extend({ template: Discourse.UserSelector.templateFunction(), disabled: this.get('disabled'), - + single: this.get('single'), + allowAny: this.get('allowAny'), dataSource: function(term) { var exclude = selected; if (userSelectorView.get('excludeCurrentUser')){ diff --git a/app/assets/stylesheets/application/compose.css.scss b/app/assets/stylesheets/application/compose.css.scss index 4d39911dc55..b25c2a01704 100644 --- a/app/assets/stylesheets/application/compose.css.scss +++ b/app/assets/stylesheets/application/compose.css.scss @@ -71,7 +71,7 @@ .autocomplete { - z-index: 9999; + z-index: 999999; position: absolute; width: 200px; background-color: $white; @@ -354,7 +354,7 @@ div.ac-wrap.disabled { div.ac-wrap { background-color: $white; border: 1px solid #cccccc; - padding: 5px 10px 0; + padding: 5px 10px; @include border-radius-all(3px); div.item { float: left; diff --git a/app/assets/stylesheets/application/modal.css.scss b/app/assets/stylesheets/application/modal.css.scss index d0eb5cf0b72..a0cd2778db8 100644 --- a/app/assets/stylesheets/application/modal.css.scss +++ b/app/assets/stylesheets/application/modal.css.scss @@ -222,3 +222,10 @@ .modal-tab { position: absolute; } + +.invite-modal { + overflow: visible; + .ember-text-field { + width: 550px; + } +} diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 318cbf3a4d7..b25f6bfc08d 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -135,13 +135,30 @@ class TopicsController < ApplicationController render nothing: true end + def remove_allowed_user + params.require(:username) + topic = Topic.where(id: params[:topic_id]).first + guardian.ensure_can_remove_allowed_users!(topic) + + if topic.remove_allowed_user(params[:username]) + render json: success_json + else + render json: failed_json, status: 422 + end + end + def invite params.require(:user) topic = Topic.where(id: params[:topic_id]).first guardian.ensure_can_invite_to!(topic) if topic.invite(current_user, params[:user]) - render json: success_json + user = User.find_by_username_or_email(params[:user]).first + if user + render_json_dump BasicUserSerializer.new(user, scope: guardian, root: 'user') + else + render json: success_json + end else render json: failed_json, status: 422 end diff --git a/app/models/topic.rb b/app/models/topic.rb index 24bb16c6764..ddf092e15ee 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -370,6 +370,13 @@ class Topic < ActiveRecord::Base [featured_user1_id, featured_user2_id, featured_user3_id, featured_user4_id].uniq.compact end + def remove_allowed_user(username) + user = User.where(username: username).first + if user + topic_allowed_users.where(user_id: user.id).first.destroy + end + end + # Invite a user to the topic by username or email. Returns success/failure def invite(invited_by, username_or_email) if private_message? diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index cfbc5877ecc..c5ab99daf13 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -23,7 +23,7 @@ class TopicViewSerializer < ApplicationSerializer end def self.guardian_attributes - [:can_moderate, :can_edit, :can_delete, :can_invite_to, :can_move_posts] + [:can_moderate, :can_edit, :can_delete, :can_invite_to, :can_move_posts, :can_remove_allowed_users] end attributes *topic_attributes diff --git a/config/routes.rb b/config/routes.rb index f5f93d8e164..93470f04d8e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -211,6 +211,7 @@ Discourse::Application.routes.draw do put 't/:topic_id/mute' => 'topics#mute', constraints: {topic_id: /\d+/} put 't/:topic_id/unmute' => 'topics#unmute', constraints: {topic_id: /\d+/} put 't/:topic_id/autoclose' => 'topics#autoclose', constraints: {topic_id: /\d+/} + put 't/:topic_id/remove-allowed-user' => 'topics#remove_allowed_user', constraints: {topic_id: /\d+/} get 't/:topic_id/:post_number' => 'topics#show', constraints: {topic_id: /\d+/, post_number: /\d+/} get 't/:slug/:topic_id.rss' => 'topics#feed', format: :rss, constraints: {topic_id: /\d+/} diff --git a/lib/guardian.rb b/lib/guardian.rb index a8840232421..998a7e35e36 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -195,6 +195,10 @@ class Guardian is_staff? && user.created_at >= 7.days.ago end + def can_remove_allowed_users?(topic) + is_staff? + end + # Support for ensure_{blah}! methods. def method_missing(method, *args, &block) if method.to_s =~ /^ensure_(.*)\!$/ diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index a304af49ca8..ab89de8f48f 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -410,9 +410,13 @@ describe Topic do context 'by username' do - it 'adds walter to the allowed users' do + it 'adds and removes walter to the allowed users' do topic.invite(topic.user, walter.username).should be_true topic.allowed_users.include?(walter).should be_true + + topic.remove_allowed_user(walter.username).should be_true + topic.reload + topic.allowed_users.include?(walter).should be_false end it 'creates a notification' do