From 7a2fde72c64fbd4295417a6323a60fba3dbc6e5c Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Thu, 4 Aug 2022 14:50:18 -0300 Subject: [PATCH] FEATURE: Assign Status (#363) Adds a new plugin setting that when enabled adds a status field for every assignment. This setting defaults to off. The possible status for an assignment are customizable via yet another new setting, and the first one on this list will be the default status for new assignments. The status is not yet show anywhere except the assign modal and the small action posts in topics at the moment. Adding status to the assignment list for users and groups will be handled in the near future. Co-authored-by: Penar Musaraj --- .../discourse_assign/assign_controller.rb | 3 +- app/models/assignment.rb | 28 ++++++ .../controllers/assign-user.js | 23 +++++ .../initializers/extend-for-assigns.js | 7 +- .../discourse/services/task-actions.js | 2 + .../discourse/templates/modal/assign-user.hbs | 17 +++- config/locales/client.en.yml | 7 +- config/locales/server.en.yml | 14 +++ config/settings.yml | 9 ++ ...0220728171436_add_status_to_assignments.rb | 7 ++ lib/assigner.rb | 66 ++++++++++---- lib/discourse_assign/helpers.rb | 3 + lib/validators/assign_statuses_validator.rb | 29 +++++++ plugin.rb | 37 ++++++-- spec/components/search_spec.rb | 1 + spec/lib/assigner_spec.rb | 85 +++++++++++++++++- spec/requests/assign_controller_spec.rb | 24 ++++++ spec/serializers/post_serializer_spec.rb | 20 +++++ .../serializers/topic_view_serializer_spec.rb | 36 ++++++++ .../acceptance/assign-enabled-test.js | 86 +++++++++++++++++++ .../acceptance/assigned-topic-test.js | 2 + test/javascripts/unit/task-actions-test.js | 1 + 22 files changed, 475 insertions(+), 32 deletions(-) create mode 100644 db/migrate/20220728171436_add_status_to_assignments.rb create mode 100644 lib/validators/assign_statuses_validator.rb diff --git a/app/controllers/discourse_assign/assign_controller.rb b/app/controllers/discourse_assign/assign_controller.rb index 6de8740..319390f 100644 --- a/app/controllers/discourse_assign/assign_controller.rb +++ b/app/controllers/discourse_assign/assign_controller.rb @@ -56,6 +56,7 @@ module DiscourseAssign username = params.permit(:username)["username"] group_name = params.permit(:group_name)["group_name"] note = params.permit(:note)["note"].presence + status = params.permit(:status)["status"].presence assign_to = ( @@ -71,7 +72,7 @@ module DiscourseAssign target = target_type.constantize.where(id: target_id).first raise Discourse::NotFound unless target - assign = Assigner.new(target, current_user).assign(assign_to, note: note) + assign = Assigner.new(target, current_user).assign(assign_to, note: note, status: status) if assign[:success] render json: success_json diff --git a/app/models/assignment.rb b/app/models/assignment.rb index a2eb8fe..012199a 100644 --- a/app/models/assignment.rb +++ b/app/models/assignment.rb @@ -15,10 +15,26 @@ class Assignment < ActiveRecord::Base ) } + before_validation :default_status + + validate :validate_status, if: -> { SiteSetting.enable_assign_status } + def self.valid_type?(type) VALID_TYPES.include?(type.downcase) end + def self.statuses + SiteSetting.assign_statuses.split("|") + end + + def self.default_status + Assignment.statuses.first + end + + def self.status_enabled? + SiteSetting.enable_assign_status + end + def assigned_to_user? assigned_to_type == "User" end @@ -26,6 +42,18 @@ class Assignment < ActiveRecord::Base def assigned_to_group? assigned_to_type == "Group" end + + private + + def default_status + self.status ||= Assignment.default_status if SiteSetting.enable_assign_status + end + + def validate_status + if SiteSetting.enable_assign_status && !Assignment.statuses.include?(self.status) + errors.add(:status, :invalid) + end + end end # == Schema Information diff --git a/assets/javascripts/discourse-assign/controllers/assign-user.js b/assets/javascripts/discourse-assign/controllers/assign-user.js index 664c455..c14b835 100644 --- a/assets/javascripts/discourse-assign/controllers/assign-user.js +++ b/assets/javascripts/discourse-assign/controllers/assign-user.js @@ -1,6 +1,7 @@ import Controller, { inject as controller } from "@ember/controller"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { action } from "@ember/object"; +import discourseComputed from "discourse-common/utils/decorators"; import { not, or } from "@ember/object/computed"; import { inject as service } from "@ember/service"; import { isEmpty } from "@ember/utils"; @@ -42,6 +43,27 @@ export default Controller.extend(ModalFunctionality, { }); }, + @discourseComputed("siteSettings.enable_assign_status") + statusEnabled() { + return this.siteSettings.enable_assign_status; + }, + + @discourseComputed("siteSettings.assign_statuses") + availableStatuses() { + return this.siteSettings.assign_statuses.split("|").map((status) => { + return { id: status, name: status }; + }); + }, + + @discourseComputed("siteSettings.assign_statuses", "model.status") + status() { + return ( + this.model.status || + this.model.target.assignment_status || + this.siteSettings.assign_statuses.split("|")[0] + ); + }, + @action handleTextAreaKeydown(value, event) { if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { @@ -82,6 +104,7 @@ export default Controller.extend(ModalFunctionality, { target_id: this.get("model.target.id"), target_type: this.get("model.targetType"), note: this.get("model.note"), + status: this.get("model.status"), }, }) .then(() => { diff --git a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js index 471d3b1..6c3f0bd 100644 --- a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js +++ b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js @@ -577,7 +577,9 @@ function initialize(api) { "assigned_group_to_post", "unassigned_from_post", "unassigned_group_from_post", + "details_change", "note_change", + "status_change", ].includes(transformed.actionCode) ) { transformed.isSmallAction = true; @@ -592,7 +594,8 @@ function initialize(api) { topic.getProperties( "assigned_to_user", "assigned_to_group", - "assignment_note" + "assignment_note", + "assignment_status" ) ); @@ -617,6 +620,7 @@ function initialize(api) { topicAssignee, assignedToIndirectly.map((assigned) => ({ assignee: assigned.assigned_to, + status: assigned.assignment_status, note: assigned.assignment_note, })) ) @@ -782,6 +786,7 @@ function initialize(api) { const target = post || topic; target.set("assignment_note", data.assignment_note); + target.set("assignment_status", data.assignment_status); if (data.assigned_type === "User") { target.set( "assigned_to_user_id", diff --git a/assets/javascripts/discourse/services/task-actions.js b/assets/javascripts/discourse/services/task-actions.js index c7cae11..f906008 100644 --- a/assets/javascripts/discourse/services/task-actions.js +++ b/assets/javascripts/discourse/services/task-actions.js @@ -38,6 +38,7 @@ export default Service.extend({ group_name: target.assigned_to_group?.name, target, targetType: options.targetType, + status: target.assignment_status, }, }); }, @@ -49,6 +50,7 @@ export default Service.extend({ username: user.username, target_id: target.id, target_type: targetType, + status: target.assignment_status, }, }); }, diff --git a/assets/javascripts/discourse/templates/modal/assign-user.hbs b/assets/javascripts/discourse/templates/modal/assign-user.hbs index 7d79ca1..123ce89 100644 --- a/assets/javascripts/discourse/templates/modal/assign-user.hbs +++ b/assets/javascripts/discourse/templates/modal/assign-user.hbs @@ -25,8 +25,21 @@ {{/each}} - - {{textarea id="assign-modal-note" value=model.note key-down=(action "handleTextAreaKeydown")}} + {{#if this.statusEnabled}} +
+ + {{combo-box + id="assign-status" + content=availableStatuses + value=status + onChange=(action (mut model.status)) + }} +
+ {{/if}} +
+ + {{textarea id="assign-modal-note" value=model.note key-down=(action "handleTextAreaKeydown")}} +
{{/d-modal-body}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e3cdd88..b0c45bd 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -15,7 +15,9 @@ en: unassigned_group_from_post: "unassigned %{who} from post %{when}" reassigned: "Reassigned %{who} %{when}" reassigned_group: "Reassigned %{who} %{when}" + details_change: "changed assignment details for %{who} %{when}" note_change: "changed assignment note for %{who} %{when}" + status_change: "changed assignment status for %{who} %{when}" discourse_assign: add_unassigned_filter: "Add 'unassigned' filter to category" cant_act: "You cannot act on flags that have been assigned to other users" @@ -41,11 +43,11 @@ en: title: "Unassign from Post" help: "Unassign %{username} from Post" reassign: - title: "Reassign" + title: "Edit" title_w_ellipsis: "Edit assignment..." to_self: "Reassign to me" to_self_help: "Reassign Topic to me" - help: "Reassign Topic to a different user" + help: "Edit assignment details" reassign_modal: title: "Reassign Topic" description: "Enter the name of the user you'd like to Reassign this topic" @@ -55,6 +57,7 @@ en: description: "Enter the name of the user you'd like to assign this topic" assign: "Assign" note_label: Note + status_label: Status assign_post_modal: title: "Assign Post" description: "Enter the name of the user you'd like to assign this post" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b243e07..e119f4d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -15,6 +15,13 @@ en: remind_assigns_frequency: "Frequency for reminding users about assigned topics." max_assigned_topics: "Maximum number of topics that can be assigned to a user." assign_allowed_on_groups: "Users in these groups are allowed to assign topics and can be assigned topics." + enable_assign_status: "Add a customizable status field to every assignment." + assign_statuses: "List of statuses available to each assignment. The first status is the default status applied to every new assignment." + errors: + assign_statuses: + too_few: "There must be at least two different statuses available." + duplicate: "There are duplicate status values." + removed_in_use: "Can't remove a status from the list if there are existing assignments using this status." discourse_assign: assigned_to: "Topic assigned to @%{username}" unassigned: "Topic was unassigned" @@ -77,3 +84,10 @@ en: discourse_push_notifications: popup: assigned: "@%{username} assigned you" + activerecord: + errors: + models: + assignment: + attributes: + status: + invalid: "Selected status is invalid (it is not included in the assigned_status site setting)." diff --git a/config/settings.yml b/config/settings.yml index 08940f3..ee2f55e 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -33,3 +33,12 @@ plugins: default: "" allow_any: false refresh: true + enable_assign_status: + default: false + client: true + assign_statuses: + client: true + type: list + default: "New|In Progress|Done" + allow_any: true + validator: AssignStatusesValidator diff --git a/db/migrate/20220728171436_add_status_to_assignments.rb b/db/migrate/20220728171436_add_status_to_assignments.rb new file mode 100644 index 0000000..19c259a --- /dev/null +++ b/db/migrate/20220728171436_add_status_to_assignments.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddStatusToAssignments < ActiveRecord::Migration[6.1] + def change + add_column :assignments, :status, :text, null: true + end +end diff --git a/lib/assigner.rb b/lib/assigner.rb index 4de3fbc..6a45e1e 100644 --- a/lib/assigner.rb +++ b/lib/assigner.rb @@ -195,7 +195,7 @@ class ::Assigner topic.posts.where(post_number: 1).first end - def forbidden_reasons(assign_to:, type:, note:) + def forbidden_reasons(assign_to:, type:, note:, status:) case when assign_to.is_a?(User) && !can_assignee_see_target?(assign_to) if topic.private_message? @@ -211,9 +211,9 @@ class ::Assigner end when !can_be_assigned?(assign_to) assign_to.is_a?(User) ? :forbidden_assign_to : :forbidden_group_assign_to - when topic_same_assignee_and_note(assign_to, type, note) + when topic_same_assignee_and_details(assign_to, type, note, status) assign_to.is_a?(User) ? :already_assigned : :group_already_assigned - when post_same_assignee_and_note(assign_to, type, note) + when post_same_assignee_and_details(assign_to, type, note, status) assign_to.is_a?(User) ? :already_assigned : :group_already_assigned when Assignment.where(topic: topic).count >= ASSIGNMENTS_PER_TOPIC_LIMIT :too_many_assigns_for_topic @@ -222,32 +222,49 @@ class ::Assigner end end - def update_note(assign_to, note, skip_small_action_post: false) - @target.assignment.update!(note: note) + def update_details(assign_to, note, status, skip_small_action_post: false) + case + when @target.assignment.note != note && @target.assignment.status != status && status.present? + small_action_text = <<~TEXT + Status: #{@target.assignment.status} → #{status} + + #{note} + TEXT + change_type = "details" + when @target.assignment.note != note + small_action_text = note + change_type = "note" + when @target.assignment.status != status + small_action_text = "#{@target.assignment.status} → #{status}" + change_type = "status" + end + + @target.assignment.update!(note: note, status: status) queue_notification(assign_to, skip_small_action_post) assignment = @target.assignment - publish_assignment(assignment, assign_to, note) + publish_assignment(assignment, assign_to, note, status) # email is skipped, for now unless skip_small_action_post - action_code = "note_change" - add_small_action_post(action_code, assign_to, note) + action_code = "#{change_type}_change" + add_small_action_post(action_code, assign_to, small_action_text) end { success: true } end - def assign(assign_to, note: nil, skip_small_action_post: false) + def assign(assign_to, note: nil, skip_small_action_post: false, status: nil) assigned_to_type = assign_to.is_a?(User) ? "User" : "Group" - forbidden_reason = forbidden_reasons(assign_to: assign_to, type: assigned_to_type, note: note) + forbidden_reason = + forbidden_reasons(assign_to: assign_to, type: assigned_to_type, note: note, status: status) return { success: false, reason: forbidden_reason } if forbidden_reason if no_assignee_change?(assign_to) - return update_note(assign_to, note, skip_small_action_post: skip_small_action_post) + return update_details(assign_to, note, status, skip_small_action_post: skip_small_action_post) end action_code = {} @@ -265,13 +282,14 @@ class ::Assigner assigned_by_user_id: @assigned_by.id, topic_id: topic.id, note: note, + status: status, ) first_post.publish_change_to_clients!(:revised, reload_topic: true) queue_notification(assign_to, skip_small_action_post) - publish_assignment(assignment, assign_to, note) + publish_assignment(assignment, assign_to, note, status) if assignment.assigned_to_user? if !TopicUser.exists?( @@ -413,6 +431,7 @@ class ::Assigner post_number: post_target? && @target.post_number, assigned_type: assignment.assigned_to.is_a?(User) ? "User" : "Group", assignment_note: nil, + assignment_status: nil, }, user_ids: allowed_user_ids, ) @@ -433,7 +452,7 @@ class ::Assigner ) end - def add_small_action_post(action_code, assign_to, note) + def add_small_action_post(action_code, assign_to, text) custom_fields = { "action_code_who" => assign_to.is_a?(User) ? assign_to.username : assign_to.name, } @@ -446,7 +465,7 @@ class ::Assigner topic.add_moderator_post( @assigned_by, - note, + text, bump: false, post_type: SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper], action_code: action_code, @@ -454,7 +473,7 @@ class ::Assigner ) end - def publish_assignment(assignment, assign_to, note) + def publish_assignment(assignment, assign_to, note, status) serializer = assignment.assigned_to_user? ? BasicUserSerializer : BasicGroupSerializer MessageBus.publish( "/staff/topic-assignment", @@ -466,6 +485,7 @@ class ::Assigner assigned_type: assignment.assigned_to_type, assigned_to: serializer.new(assign_to, scope: Guardian.new, root: false).as_json, assignment_note: note, + assignment_status: status, }, user_ids: allowed_user_ids, ) @@ -491,19 +511,27 @@ class ::Assigner return "unassigned_group#{suffix}" if assignment.assigned_to_group? end - def topic_same_assignee_and_note(assign_to, type, note) + def topic_same_assignee_and_details(assign_to, type, note, status) topic.assignment&.assigned_to_id == assign_to.id && topic.assignment&.assigned_to_type == type && topic.assignment.active == true && - topic.assignment&.note == note + topic.assignment&.note == note && + ( + topic.assignment&.status == status || + topic.assignment&.status == Assignment.default_status && status.nil? + ) end - def post_same_assignee_and_note(assign_to, type, note) + def post_same_assignee_and_details(assign_to, type, note, status) @target.is_a?(Topic) && Assignment .where(topic_id: topic.id, target_type: "Post", active: true) .any? do |assignment| assignment.assigned_to_id == assign_to.id && assignment.assigned_to_type == type && - assignment&.note == note + assignment&.note == note && + ( + topic.assignment&.status == status || + topic.assignment&.status == Assignment.default_status && status.nil? + ) end end diff --git a/lib/discourse_assign/helpers.rb b/lib/discourse_assign/helpers.rb index 55288bb..0fe1f9a 100644 --- a/lib/discourse_assign/helpers.rb +++ b/lib/discourse_assign/helpers.rb @@ -32,6 +32,7 @@ module DiscourseAssign .map do |post_id, assigned_map| assigned_to = assigned_map[:assigned_to] note = assigned_map[:assignment_note] + status = assigned_map[:assignment_status] post_number = assigned_map[:post_number] if (assigned_to.is_a?(User)) @@ -41,6 +42,7 @@ module DiscourseAssign assigned_to: build_assigned_to_user(assigned_to, topic), post_number: post_number, assignment_note: note, + assignment_status: status, }, ] elsif assigned_to.is_a?(Group) @@ -50,6 +52,7 @@ module DiscourseAssign assigned_to: build_assigned_to_group(assigned_to, topic), post_number: post_number, assignment_note: note, + assignment_status: status, }, ] end diff --git a/lib/validators/assign_statuses_validator.rb b/lib/validators/assign_statuses_validator.rb new file mode 100644 index 0000000..6f37029 --- /dev/null +++ b/lib/validators/assign_statuses_validator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AssignStatusesValidator + def initialize(opts = {}) + @opts = opts + end + + def valid_value?(value) + statuses = value.split("|") + + case + when statuses.size < 2 + @reason = "too_few" + return false + when statuses.size != statuses.uniq.size + @reason = "duplicate" + return false + when Assignment.where.not(status: statuses).count > 0 + @reason = "removed_in_use" + return false + end + + true + end + + def error_message + I18n.t("site_settings.errors.assign_statuses.#{@reason}") + end +end diff --git a/plugin.rb b/plugin.rb index 72bcca4..1584873 100644 --- a/plugin.rb +++ b/plugin.rb @@ -19,6 +19,7 @@ register_svg_icon "user-times" load File.expand_path("../lib/discourse_assign/engine.rb", __FILE__) load File.expand_path("../lib/discourse_assign/helpers.rb", __FILE__) +load File.expand_path("../lib/validators/assign_statuses_validator.rb", __FILE__) Discourse::Application.routes.append do mount ::DiscourseAssign::Engine, at: "/assign" @@ -497,11 +498,16 @@ after_initialize do .where(topic_id: id, target_type: "Post", active: true) .includes(:target) .inject({}) do |acc, assignment| - acc[assignment.target_id] = { - assigned_to: assignment.assigned_to, - post_number: assignment.target.post_number, - assignment_note: assignment.note, - } if assignment.target + if assignment.target + acc[assignment.target_id] = { + assigned_to: assignment.assigned_to, + post_number: assignment.target.post_number, + assignment_note: assignment.note, + } + acc[assignment.target_id][ + :assignment_status + ] = assignment.status if SiteSetting.enable_assign_status + end acc end end @@ -563,6 +569,13 @@ after_initialize do (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assignment.present? end + add_to_serializer(:topic_view, :assignment_status, false) { object.topic.assignment.status } + + add_to_serializer(:topic_view, :include_assignment_status?, false) do + SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && + object.topic.assignment.present? + end + # SuggestedTopic serializer add_to_serializer(:suggested_topic, :assigned_to_user, false) do DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) @@ -613,6 +626,13 @@ after_initialize do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group) end + add_to_serializer(:topic_list_item, :assignment_status, false) { object.assignment.status } + + add_to_serializer(:topic_list_item, :include_assignment_status?, false) do + SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && + object.assignment.present? + end + # SearchTopicListItem serializer add_to_serializer(:search_topic_list_item, :assigned_to_user, false) do DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) @@ -718,6 +738,13 @@ after_initialize do (SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present? end + add_to_serializer(:post, :assignment_status, false) { object.assignment.status } + + add_to_serializer(:post, :include_assignment_status?, false) do + SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && + object.assignment.present? + end + # CurrentUser serializer add_to_serializer(:current_user, :can_assign) { object.can_assign? } diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 37b5edb..53e3091 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -69,6 +69,7 @@ describe Search do }, post_number: post5.post_number, assignment_note: nil, + assignment_status: nil, }, ) end diff --git a/spec/lib/assigner_spec.rb b/spec/lib/assigner_spec.rb index a088284..470fe3f 100644 --- a/spec/lib/assigner_spec.rb +++ b/spec/lib/assigner_spec.rb @@ -3,7 +3,10 @@ require "rails_helper" RSpec.describe Assigner do - before { SiteSetting.assign_enabled = true } + before do + SiteSetting.assign_enabled = true + SiteSetting.enable_assign_status = true + end let(:assign_allowed_group) { Group.find_by(name: "staff") } let(:pm_post) { Fabricate(:private_message_post) } @@ -53,11 +56,17 @@ RSpec.describe Assigner do expect(topic.posts.last.raw).to eq "tomtom best mom" end + it "can assign with status" do + assigner.assign(moderator, status: "In Progress") + + expect(topic.assignment.status).to eq "In Progress" + end + it "publishes topic assignment after assign and unassign" do messages = MessageBus.track_publish("/staff/topic-assignment") do assigner = described_class.new(topic, moderator_2) - assigner.assign(moderator, note: "tomtom best mom") + assigner.assign(moderator, note: "tomtom best mom", status: "In Progress") assigner.unassign end @@ -71,6 +80,7 @@ RSpec.describe Assigner do assigned_type: "User", assigned_to: BasicUserSerializer.new(moderator, scope: Guardian.new, root: false).as_json, assignment_note: "tomtom best mom", + assignment_status: "In Progress", }, ) @@ -83,6 +93,7 @@ RSpec.describe Assigner do post_number: false, assigned_type: "User", assignment_note: nil, + assignment_status: nil, }, ) end @@ -356,6 +367,76 @@ RSpec.describe Assigner do expect(small_action_post.action_code).to eq "note_change" end end + + describe "updating status" do + it "does not recreate assignment if no assignee change" do + assigner.assign(moderator) + + expect do assigner.assign(moderator, status: "Done") end.to_not change { + Assignment.last.id + } + end + + it "updates status" do + assigner.assign(moderator) + + assigner.assign(moderator, status: "Done") + + expect(Assignment.last.status).to eq "Done" + end + + it "queues notification" do + assigner.assign(moderator) + + expect_enqueued_with(job: :assign_notification) do + assigner.assign(moderator, status: "Done") + end + end + + it "publishes topic assignment with note" do + assigner.assign(moderator) + + messages = + MessageBus.track_publish("/staff/topic-assignment") do + assigner = described_class.new(topic, moderator_2) + assigner.assign(moderator, status: "Done") + end + + expect(messages[0].channel).to eq "/staff/topic-assignment" + expect(messages[0].data).to include( + { + type: "assigned", + topic_id: topic.id, + post_id: false, + post_number: false, + assigned_type: "User", + assigned_to: + BasicUserSerializer.new(moderator, scope: Guardian.new, root: false).as_json, + assignment_status: "Done", + }, + ) + end + + it "adds a note_change small action post" do + assigner.assign(moderator) + + assigner.assign(moderator, status: "Done") + + small_action_post = topic.posts.last + expect(small_action_post.action_code).to eq "status_change" + end + end + + describe "updating note and status at the same time" do + it "adds a note_change small action post" do + assigner.assign(moderator) + + assigner.assign(moderator, note: "This is a note!", status: "Done") + + small_action_post = topic.posts.last + expect(small_action_post.action_code).to eq "details_change" + end + end end context "assign_self_regex" do diff --git a/spec/requests/assign_controller_spec.rb b/spec/requests/assign_controller_spec.rb index 15b3c27..388a968 100644 --- a/spec/requests/assign_controller_spec.rb +++ b/spec/requests/assign_controller_spec.rb @@ -115,6 +115,7 @@ RSpec.describe DiscourseAssign::AssignController do before do sign_in(user) add_to_assign_allowed_group(user2) + SiteSetting.enable_assign_status = true end it "assigns topic to a user" do @@ -141,6 +142,29 @@ RSpec.describe DiscourseAssign::AssignController do expect(post.topic.reload.assignment.note).to eq("do dis pls") end + it "assigns topic with a set status to a user" do + put "/assign/assign.json", + params: { + target_id: post.topic_id, + target_type: "Topic", + username: user2.username, + status: "In Progress", + } + + expect(post.topic.reload.assignment.status).to eq("In Progress") + end + + it "assigns topic with default status to a user" do + put "/assign/assign.json", + params: { + target_id: post.topic_id, + target_type: "Topic", + username: user2.username, + } + + expect(post.topic.reload.assignment.status).to eq("New") + end + it "assigns topic to a group" do put "/assign/assign.json", params: { diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb index a50a67d..6deb138 100644 --- a/spec/serializers/post_serializer_spec.rb +++ b/spec/serializers/post_serializer_spec.rb @@ -39,4 +39,24 @@ describe PostSerializer do serializer = PostSerializer.new(post, scope: guardian) expect(serializer.as_json[:post][:assignment_note]).to eq("tomtom best") end + + context "when status is enabled" do + before { SiteSetting.enable_assign_status = true } + + it "includes status in serializer" do + Assigner.new(post, user).assign(user, status: "Done") + serializer = PostSerializer.new(post, scope: guardian) + expect(serializer.as_json[:post][:assignment_status]).to eq("Done") + end + end + + context "when status is disabled" do + before { SiteSetting.enable_assign_status = false } + + it "doesn't include status in serializer" do + Assigner.new(post, user).assign(user, status: "Done") + serializer = PostSerializer.new(post, scope: guardian) + expect(serializer.as_json[:post][:assignment_status]).not_to eq("Done") + end + end end diff --git a/spec/serializers/topic_view_serializer_spec.rb b/spec/serializers/topic_view_serializer_spec.rb index f7f7559..a113a42 100644 --- a/spec/serializers/topic_view_serializer_spec.rb +++ b/spec/serializers/topic_view_serializer_spec.rb @@ -45,4 +45,40 @@ RSpec.describe TopicViewSerializer do serializer.as_json[:topic_view][:indirectly_assigned_to][post.id][:assignment_note], ).to eq("note me down") end + + context "when status is enabled" do + before { SiteSetting.enable_assign_status = true } + + it "includes status in serializer" do + Assigner.new(topic, user).assign(user, status: "Done") + serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) + expect(serializer.as_json[:topic_view][:assignment_status]).to eq("Done") + end + + it "includes indirectly_assigned_to status in serializer" do + Assigner.new(post, user).assign(user, status: "Done") + serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) + expect( + serializer.as_json[:topic_view][:indirectly_assigned_to][post.id][:assignment_status], + ).to eq("Done") + end + end + + context "when status is disabled" do + before { SiteSetting.enable_assign_status = false } + + it "doesn't include status in serializer" do + Assigner.new(topic, user).assign(user, status: "Done") + serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) + expect(serializer.as_json[:topic_view][:assignment_status]).not_to eq("Done") + end + + it "doesn't include indirectly_assigned_to status in serializer" do + Assigner.new(post, user).assign(user, status: "Done") + serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) + expect( + serializer.as_json[:topic_view][:indirectly_assigned_to][post.id][:assignment_status], + ).not_to eq("Done") + end + end end diff --git a/test/javascripts/acceptance/assign-enabled-test.js b/test/javascripts/acceptance/assign-enabled-test.js index ca6775b..57d6be0 100644 --- a/test/javascripts/acceptance/assign-enabled-test.js +++ b/test/javascripts/acceptance/assign-enabled-test.js @@ -109,6 +109,92 @@ acceptance("Discourse Assign | Assign desktop", function (needs) { }); }); +acceptance("Discourse Assign | Assign Status enabled", function (needs) { + needs.user({ + can_assign: true, + }); + needs.settings({ + assign_enabled: true, + enable_assign_status: true, + assign_statuses: "New|In Progress|Done", + }); + needs.hooks.beforeEach(() => clearTopicFooterButtons()); + + needs.pretender((server, helper) => { + server.get("/assign/suggestions", () => { + return helper.response({ + success: true, + assign_allowed_groups: false, + assign_allowed_for_groups: [], + suggestions: [ + { + id: 19, + username: "eviltrout", + name: "Robin Ward", + avatar_template: + "/user_avatar/meta.discourse.org/eviltrout/{size}/5275_2.png", + }, + ], + }); + }); + + server.put("/assign/assign", () => { + return helper.response({ success: true }); + }); + }); + + test("Modal contains status dropdown", async (assert) => { + await visit("/t/internationalization-localization/280"); + await click("#topic-footer-button-assign"); + + assert.ok( + exists(".assign.modal-body #assign-status"), + "assign status dropdown exists" + ); + }); +}); + +acceptance("Discourse Assign | Assign Status disabled", function (needs) { + needs.user({ + can_assign: true, + }); + needs.settings({ assign_enabled: true, enable_assign_status: false }); + needs.hooks.beforeEach(() => clearTopicFooterButtons()); + + needs.pretender((server, helper) => { + server.get("/assign/suggestions", () => { + return helper.response({ + success: true, + assign_allowed_groups: false, + assign_allowed_for_groups: [], + suggestions: [ + { + id: 19, + username: "eviltrout", + name: "Robin Ward", + avatar_template: + "/user_avatar/meta.discourse.org/eviltrout/{size}/5275_2.png", + }, + ], + }); + }); + + server.put("/assign/assign", () => { + return helper.response({ success: true }); + }); + }); + + test("Modal contains status dropdown", async (assert) => { + await visit("/t/internationalization-localization/280"); + await click("#topic-footer-button-assign"); + + assert.notOk( + exists(".assign.modal-body #assign-status"), + "assign status dropdown doesn't exists" + ); + }); +}); + // See RemindAssignsFrequencySiteSettings const remindersFrequency = [ { diff --git a/test/javascripts/acceptance/assigned-topic-test.js b/test/javascripts/acceptance/assigned-topic-test.js index 7123ce0..867cbb8 100644 --- a/test/javascripts/acceptance/assigned-topic-test.js +++ b/test/javascripts/acceptance/assigned-topic-test.js @@ -23,6 +23,7 @@ function assignCurrentUserToTopic(needs) { "/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png", }; topic["assignment_note"] = "Shark Doododooo"; + topic["assignment_status"] = "New"; topic["indirectly_assigned_to"] = { 2: { assigned_to: { @@ -89,6 +90,7 @@ acceptance("Discourse Assign | Assigned topic", function (needs) { tagging_enabled: true, assigns_user_url_path: "/", assigns_public: true, + enable_assign_status: true, }); assignCurrentUserToTopic(needs); diff --git a/test/javascripts/unit/task-actions-test.js b/test/javascripts/unit/task-actions-test.js index 6382f62..e16af0f 100644 --- a/test/javascripts/unit/task-actions-test.js +++ b/test/javascripts/unit/task-actions-test.js @@ -28,6 +28,7 @@ discourseModule("Unit | Service | task-actions", function () { group_name: "cats", target, targetType: "Topic", + status: undefined, }, }); });