From 3e3dc3815b6b2544596e618b13e4bccc2169e75b Mon Sep 17 00:00:00 2001 From: Ahmed Gagan Date: Fri, 11 Sep 2020 16:19:25 +0530 Subject: [PATCH] FEATURE: Assign bulk actions for topics lists (#110) - Allows unassigning and reassigning topics using bulk actions - Adds bulk actions UI to the group assigned page --- .../controllers/assign-user.js.es6 | 17 ++++ .../controllers/group-assigned-show.js.es6 | 10 +++ .../initializers/extend-for-assigns.js.es6 | 27 +++++++ .../components/assigned-topic-list-item.hbs | 9 ++- .../components/assigned-topic-list.hbs | 2 + .../components/basic-assigned-topic-list.hbs | 3 + .../discourse/templates/group-topics-list.hbs | 6 ++ config/locales/client.en.yml | 4 + plugin.rb | 23 ++++++ spec/components/topics_bulk_action_spec.rb | 78 +++++++++++++++++++ 10 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 spec/components/topics_bulk_action_spec.rb diff --git a/assets/javascripts/discourse-assign/controllers/assign-user.js.es6 b/assets/javascripts/discourse-assign/controllers/assign-user.js.es6 index 5d5800a..d41c0ef 100644 --- a/assets/javascripts/discourse-assign/controllers/assign-user.js.es6 +++ b/assets/javascripts/discourse-assign/controllers/assign-user.js.es6 @@ -1,7 +1,9 @@ import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as controller } from "@ember/controller"; export default Ember.Controller.extend({ + topicBulkActions: controller(), assignSuggestions: null, allowedGroups: null, taskActions: Ember.inject.service(), @@ -22,8 +24,19 @@ export default Ember.Controller.extend({ } }, + bulkAction(username) { + this.topicBulkActions.performAndRefresh({ + type: "assign", + username, + }); + }, + actions: { assignUser(user) { + if (this.isBulkAction) { + this.bulkAction(user.username); + return; + } this.setProperties({ "model.username": user.username, "model.allowedGroups": this.taskActions.allowedGroups, @@ -32,6 +45,10 @@ export default Ember.Controller.extend({ }, assign() { + if (this.isBulkAction) { + this.bulkAction(this.model.username); + return; + } let path = "/assign/assign"; if (Ember.isEmpty(this.get("model.username"))) { diff --git a/assets/javascripts/discourse-assign/controllers/group-assigned-show.js.es6 b/assets/javascripts/discourse-assign/controllers/group-assigned-show.js.es6 index 5ea1aaf..27a841a 100644 --- a/assets/javascripts/discourse-assign/controllers/group-assigned-show.js.es6 +++ b/assets/javascripts/discourse-assign/controllers/group-assigned-show.js.es6 @@ -1,4 +1,5 @@ import UserTopicsList from "discourse/controllers/user-topics-list"; +import { alias } from "@ember/object/computed"; import { debounce } from "@ember/runloop"; import discourseComputed from "discourse-common/utils/decorators"; import { INPUT_DELAY } from "discourse-common/config/environment"; @@ -9,6 +10,9 @@ export default UserTopicsList.extend({ order: null, ascending: false, q: "", + bulkSelectEnabled: false, + selected: [], + canBulkSelect: alias("currentUser.staff"), queryParams: ["order", "ascending", "q"], @@ -61,5 +65,11 @@ export default UserTopicsList.extend({ onChangeFilter(value) { debounce(this, this._setSearchTerm, value, INPUT_DELAY * 2); }, + toggleBulkSelect() { + this.toggleProperty("bulkSelectEnabled"); + }, + refresh() { + this.refreshModel(); + }, }, }); diff --git a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 index f67eb8e..5ce0e70 100644 --- a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 +++ b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 @@ -7,6 +7,9 @@ import { queryRegistry } from "discourse/widgets/widget"; import { getOwner } from "discourse-common/lib/get-owner"; import { htmlSafe } from "@ember/template"; import getURL from "discourse-common/lib/get-url"; +import { addBulkButton } from "discourse/controllers/topic-bulk-actions"; +import TopicButtonAction from "discourse/controllers/topic-bulk-actions"; +import { inject } from "@ember/controller"; import I18n from "I18n"; function titleForState(user) { @@ -320,6 +323,30 @@ export default { return; } + const currentUser = container.lookup("current-user:main"); + if (currentUser.can_assign) { + TopicButtonAction.reopen({ + assignUser: inject("assign-user"), + actions: { + showReAssign() { + this.set("assignUser.isBulkAction", true); + this.set("assignUser.model", { username: "" }); + this.send("changeBulkTemplate", "modal/assign-user"); + }, + unassignTopics() { + this.performAndRefresh({ type: "unassign" }); + }, + }, + }); + addBulkButton("showReAssign", "assign", { + icon: "user-plus", + class: "btn-default", + }); + addBulkButton("unassignTopics", "unassign", { + icon: "user-times", + class: "btn-default", + }); + } withPluginApi("0.8.11", (api) => initialize(api, container)); withPluginApi("0.8.28", (api) => registerTopicFooterButtons(api, container) diff --git a/assets/javascripts/discourse/templates/components/assigned-topic-list-item.hbs b/assets/javascripts/discourse/templates/components/assigned-topic-list-item.hbs index 44c956f..798e711 100644 --- a/assets/javascripts/discourse/templates/components/assigned-topic-list-item.hbs +++ b/assets/javascripts/discourse/templates/components/assigned-topic-list-item.hbs @@ -5,8 +5,13 @@ This causes the topic-post-badge to be considered the same word as "text" at the end of the link, preventing it from line wrapping onto its own line. --}} - - +{{#if bulkSelectEnabled}} + + + +{{/if}} + + {{~raw "topic-status" topic=topic}} {{~#if isPrivateMessage}} {{~d-icon "envelope" class="private-message-icon"}} diff --git a/assets/javascripts/discourse/templates/components/assigned-topic-list.hbs b/assets/javascripts/discourse/templates/components/assigned-topic-list.hbs index c09697b..26dcd79 100644 --- a/assets/javascripts/discourse/templates/components/assigned-topic-list.hbs +++ b/assets/javascripts/discourse/templates/components/assigned-topic-list.hbs @@ -1,6 +1,7 @@ {{#unless skipHeader}} {{raw "topic-list-header" + canBulkSelect=canBulkSelect toggleInTitle=toggleInTitle hideCategory=hideCategory showPosters=showPosters @@ -17,6 +18,7 @@ {{#each filteredTopics as |topic|}} {{assigned-topic-list-item topic=topic + bulkSelectEnabled=bulkSelectEnabled showTopicPostBadges=showTopicPostBadges hideCategory=hideCategory showPosters=showPosters diff --git a/assets/javascripts/discourse/templates/components/basic-assigned-topic-list.hbs b/assets/javascripts/discourse/templates/components/basic-assigned-topic-list.hbs index 4b8b01d..8013463 100644 --- a/assets/javascripts/discourse/templates/components/basic-assigned-topic-list.hbs +++ b/assets/javascripts/discourse/templates/components/basic-assigned-topic-list.hbs @@ -14,10 +14,13 @@ hideCategory=hideCategory topics=topics expandExcerpts=expandExcerpts + bulkSelectEnabled=bulkSelectEnabled + canBulkSelect=canBulkSelect selected=selected skipHeader=skipHeader tagsForUser=tagsForUser changeSort=changeSort + toggleBulkSelect=toggleBulkSelect unassign=unassign reassign=reassign onScroll=onScroll diff --git a/assets/javascripts/discourse/templates/group-topics-list.hbs b/assets/javascripts/discourse/templates/group-topics-list.hbs index 9c7d662..9f668d5 100644 --- a/assets/javascripts/discourse/templates/group-topics-list.hbs +++ b/assets/javascripts/discourse/templates/group-topics-list.hbs @@ -8,17 +8,23 @@ {{#load-more class="paginated-topics-list" selector=".paginated-topics-list .topic-list tr" action=(action "loadMore")}} + {{bulk-select-button + selected=selected + action=(action "refresh") + }} {{basic-assigned-topic-list topicList=model hideCategory=hideCategory showPosters=showPosters bulkSelectEnabled=bulkSelectEnabled + canBulkSelect=canBulkSelect selected=selected hasIncoming=hasIncoming incomingCount=incomingCount showInserted=(action "showInserted") tagsForUser=tagsForUser changeSort=(action "changeSort") + toggleBulkSelect=(action "toggleBulkSelect") unassign=(action "unassign") reassign=(action "reassign") onScroll=saveScrollPosition diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e31e653..db73b26 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -53,3 +53,7 @@ en: assign_event: name: "Assign Event" details: "When a user assigns or unassigns a topic." + topics: + bulk: + unassign: "Unassign Topics" + assign: "Assign Topics" diff --git a/plugin.rb b/plugin.rb index 69dba46..8c7acdf 100644 --- a/plugin.rb +++ b/plugin.rb @@ -398,6 +398,29 @@ after_initialize do end end + TopicsBulkAction.register_operation("assign") do + if @user.can_assign? + assign_user = User.find_by_username(@operation[:username]) + topics.each do |t| + TopicAssigner.new(t, @user).assign(assign_user) + end + end + end + + TopicsBulkAction.register_operation("unassign") do + if @user.can_assign? + topics.each do |t| + if guardian.can_assign? + TopicAssigner.new(t, @user).unassign + end + end + end + end + + if defined? register_permitted_bulk_action_parameter + register_permitted_bulk_action_parameter :username + end + if defined? UserBookmarkSerializer add_to_class(:user_bookmark_serializer, :assigned_to_user_id) do id = topic.custom_fields[TopicAssigner::ASSIGNED_TO_ID] diff --git a/spec/components/topics_bulk_action_spec.rb b/spec/components/topics_bulk_action_spec.rb new file mode 100644 index 0000000..a0f485f --- /dev/null +++ b/spec/components/topics_bulk_action_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'rails_helper' +require_relative '../support/assign_allowed_group' + +describe TopicsBulkAction do + fab!(:post) { Fabricate(:post) } + fab!(:post1) { Fabricate(:post) } + fab!(:post2) { Fabricate(:post) } + + before do + SiteSetting.assign_enabled = true + end + + let(:user) { Fabricate(:user) } + let(:user2) { Fabricate(:user) } + + include_context 'A group that is allowed to assign' + + before do + add_to_assign_allowed_group(user) + end + + describe "assign_topics" do + it "assigns multiple topics to user" do + TopicsBulkAction.new(user, [post.topic.id, post1.topic.id], { type: 'assign', username: user.username }).perform! + + assigned_topics = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics + + expect(assigned_topics.length).to eq(2) + + expect(assigned_topics).to contain_exactly(post.topic, post1.topic) + end + + it "doesn't allows to assign to user not in assign_allowed_group" do + TopicsBulkAction.new(user, [post.topic.id, post1.topic.id], { type: 'assign', username: user2.username }).perform! + + assigned_topics = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user2).topics + + expect(assigned_topics.length).to eq(0) + end + + it "user who is not in assign_allowed_group can't assign topics" do + TopicsBulkAction.new(user2, [post.topic.id, post1.topic.id, post2.topic.id], { type: 'assign', username: user.username }).perform! + + assigned_topics = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics + + expect(assigned_topics.length).to eq(0) + end + end + + describe "unassign_topics" do + it "unassigns multiple topics assigned to user" do + TopicsBulkAction.new(user, [post.topic.id, post1.topic.id, post2.topic.id], { type: 'assign', username: user.username }).perform! + + TopicsBulkAction.new(user, [post.topic.id, post1.topic.id], type: 'unassign').perform! + + assigned_topics = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics + + expect(assigned_topics.length).to eq(1) + + expect(assigned_topics).to contain_exactly(post2.topic) + end + + it "user who is not in assign_allowed_group can't unassign topics" do + TopicsBulkAction.new(user, [post.topic.id, post1.topic.id, post2.topic.id], { type: 'assign', username: user.username }).perform! + + TopicsBulkAction.new(user2, [post.topic.id, post1.topic.id], type: 'unassign').perform! + + assigned_topics = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics + + expect(assigned_topics.length).to eq(3) + + expect(assigned_topics).to contain_exactly(post.topic, post1.topic, post2.topic) + end + + end +end