FIX: Ensure assign plugin respects `prioritize_full_name_in_ux` site setting (#632)

* WIP

* more messing around

* undos, notes to self

* WIP notes

* completed logic and added tests for existing  code and implemented code

* fixed failing tests

* fixed failing tests

* added suggested changes

* added suggested changes

* fix merge

* fix linting issues

* possible flaky test

* removed comment

* further user name display changes

* implemented suggested change

* fixed bug in small action post when assigning or unassigning

* fixed breaking tests

* refactored test

---------

Co-authored-by: Juan David Martinez <juan@discourse.org>
This commit is contained in:
benj 2025-03-14 18:08:28 -05:00 committed by GitHub
parent 3b1ffefbe4
commit ec8a785df9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 313 additions and 17 deletions

View File

@ -9,6 +9,15 @@ import DMenu from "float-kit/components/d-menu";
export default class AssignedToPost extends Component { export default class AssignedToPost extends Component {
@service taskActions; @service taskActions;
@service siteSettings;
get nameOrUsername() {
if (this.siteSettings.prioritize_full_name_in_ux) {
return this.args.assignedToUser.name || this.args.assignedToUser.username;
} else {
return this.args.assignedToUser.username;
}
}
@action @action
unassign() { unassign() {
@ -33,7 +42,7 @@ export default class AssignedToPost extends Component {
<a href={{@href}} class="assigned-to-username"> <a href={{@href}} class="assigned-to-username">
{{#if @assignedToUser}} {{#if @assignedToUser}}
{{@assignedToUser.username}} {{this.nameOrUsername}}
{{else}} {{else}}
{{@assignedToGroup.name}} {{@assignedToGroup.name}}
{{/if}} {{/if}}

View File

@ -72,7 +72,12 @@ export default {
const content = []; const content = [];
if (this.topic.isAssigned()) { if (this.topic.isAssigned()) {
content.push(unassignFromTopicButton(this.topic)); content.push(
unassignFromTopicButton(
this.topic,
this.siteSettings.prioritize_full_name_in_ux
)
);
} }
if (this.topic.hasAssignedPosts()) { if (this.topic.hasAssignedPosts()) {
@ -131,9 +136,14 @@ function reassignToSelfButton() {
}; };
} }
function unassignFromTopicButton(topic) { function unassignFromTopicButton(topic, prioritize_full_name_in_ux) {
const username = let username =
topic.assigned_to_user?.username || topic.assigned_to_group?.name; topic.assigned_to_user?.username || topic.assigned_to_group?.name;
if (topic.assigned_to_user && prioritize_full_name_in_ux) {
username = topic.assigned_to_user?.name || topic.assigned_to_user?.username;
}
const icon = topic.assigned_to_user const icon = topic.assigned_to_user
? avatarHtml(topic.assigned_to_user, "small") ? avatarHtml(topic.assigned_to_user, "small")
: iconHTML("user-xmark"); : iconHTML("user-xmark");

View File

@ -30,6 +30,7 @@ const DEPENDENT_KEYS = [
"topic.assigned_to_group", "topic.assigned_to_group",
"currentUser.can_assign", "currentUser.can_assign",
"topic.assigned_to_user.username", "topic.assigned_to_user.username",
"topic.assigned_to_user.name",
]; ];
function defaultTitle(topic) { function defaultTitle(topic) {
@ -481,7 +482,11 @@ function initialize(api) {
} }
const icon = iconHTML(assignee.username ? "user-plus" : "group-plus"); const icon = iconHTML(assignee.username ? "user-plus" : "group-plus");
const name = assignee.username || assignee.name; const name =
siteSettings.prioritize_full_name_in_ux || !assignee.username
? assignee.name || assignee.username
: assignee.username;
const tagName = params.tagName || "a"; const tagName = params.tagName || "a";
const href = const href =
tagName === "a" tagName === "a"
@ -553,13 +558,18 @@ function initialize(api) {
)}</span>`; )}</span>`;
}; };
let displayedName = "";
if (assignedToUser) { if (assignedToUser) {
displayedName = this.siteSettings.prioritize_full_name_in_ux
? assignedToUser.name || assignedToUser.username
: assignedToUser.username;
assigneeElements.push( assigneeElements.push(
h( h(
"span.assignee", "span.assignee",
new RawHtml({ new RawHtml({
html: assignedHtml( html: assignedHtml(
assignedToUser.username, displayedName,
assignedToUserPath(assignedToUser), assignedToUserPath(assignedToUser),
"user" "user"
), ),
@ -581,10 +591,17 @@ function initialize(api) {
) )
); );
} }
if (indirectlyAssignedTo) { if (indirectlyAssignedTo) {
Object.keys(indirectlyAssignedTo).map((postId) => { Object.keys(indirectlyAssignedTo).map((postId) => {
const assignee = indirectlyAssignedTo[postId].assigned_to; const assignee = indirectlyAssignedTo[postId].assigned_to;
const postNumber = indirectlyAssignedTo[postId].post_number; const postNumber = indirectlyAssignedTo[postId].post_number;
displayedName =
this.siteSettings.prioritize_full_name_in_ux || !assignee.username
? assignee.name || assignee.username
: assignee.username;
assigneeElements.push( assigneeElements.push(
h("span.assignee", [ h("span.assignee", [
h( h(
@ -597,13 +614,14 @@ function initialize(api) {
}, },
i18n("discourse_assign.assign_post_to_multiple", { i18n("discourse_assign.assign_post_to_multiple", {
post_number: postNumber, post_number: postNumber,
username: assignee.username || assignee.name, username: displayedName,
}) })
), ),
]) ])
); );
}); });
} }
if (!isEmpty(assigneeElements)) { if (!isEmpty(assigneeElements)) {
return h("p.assigned-to", [ return h("p.assigned-to", [
assignedToUser ? iconNode("user-plus") : iconNode("group-plus"), assignedToUser ? iconNode("user-plus") : iconNode("group-plus"),

View File

@ -17,6 +17,7 @@ export class Assignment extends EmberObject {
static fromPost(post) { static fromPost(post) {
const assignment = new Assignment(); const assignment = new Assignment();
assignment.username = post.assigned_to.username; assignment.username = post.assigned_to.username;
assignment.name = post.assigned_to.name;
assignment.groupName = post.assigned_to.name; assignment.groupName = post.assigned_to.name;
assignment.status = post.assignment_status; assignment.status = post.assignment_status;
assignment.note = post.assignment_note; assignment.note = post.assignment_note;
@ -31,6 +32,7 @@ export class Assignment extends EmberObject {
// and models from server, that's why we have to call it "group_name" now // and models from server, that's why we have to call it "group_name" now
@tracked group_name; @tracked group_name;
@tracked username; @tracked username;
@tracked name;
@tracked status; @tracked status;
@tracked note; @tracked note;
targetId; targetId;

View File

@ -405,9 +405,7 @@ class ::Assigner
if SiteSetting.unassign_creates_tracking_post && !silent if SiteSetting.unassign_creates_tracking_post && !silent
post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper] post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper]
custom_fields = { custom_fields = small_action_username_or_name(assigned_to)
"action_code_who" => assigned_to.is_a?(User) ? assigned_to.username : assigned_to.name,
}
if post_target? if post_target?
custom_fields.merge!("action_code_path" => "/p/#{@target.id}") custom_fields.merge!("action_code_path" => "/p/#{@target.id}")
@ -493,10 +491,20 @@ class ::Assigner
Jobs.enqueue(:assign_notification, assignment_id: assignment.id) Jobs.enqueue(:assign_notification, assignment_id: assignment.id)
end end
def small_action_username_or_name(assign_to)
if (assign_to.is_a?(User) && SiteSetting.prioritize_full_name_in_ux) ||
!assign_to.try(:username)
custom_fields = { "action_code_who" => assign_to.name || assign_to.username }
else
custom_fields = {
"action_code_who" => assign_to.is_a?(User) ? assign_to.username : assign_to.name,
}
end
custom_fields
end
def add_small_action_post(action_code, assign_to, text) def add_small_action_post(action_code, assign_to, text)
custom_fields = { custom_fields = small_action_username_or_name(assign_to)
"action_code_who" => assign_to.is_a?(User) ? assign_to.username : assign_to.name,
}
if post_target? if post_target?
custom_fields.merge!( custom_fields.merge!(

View File

@ -19,6 +19,7 @@ RSpec.describe TopicViewSerializer do
it "includes assigned user in serializer" do it "includes assigned user in serializer" do
Assigner.new(topic, user).assign(user) Assigner.new(topic, user).assign(user)
serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian)
expect(serializer.as_json[:topic_view][:assigned_to_user][:name]).to eq(user.name)
expect(serializer.as_json[:topic_view][:assigned_to_user][:username]).to eq(user.username) expect(serializer.as_json[:topic_view][:assigned_to_user][:username]).to eq(user.username)
expect(serializer.as_json[:topic_view][:assigned_to_group]).to be nil expect(serializer.as_json[:topic_view][:assigned_to_group]).to be nil
end end

View File

@ -0,0 +1,188 @@
# frozen_string_literal: true
describe "Assign | Assigning posts", type: :system do
let(:topic_page) { PageObjects::Pages::Topic.new }
let(:assign_modal) { PageObjects::Modals::Assign.new }
fab!(:staff_user) { Fabricate(:user, groups: [Group[:staff]]) }
fab!(:admin)
fab!(:topic)
fab!(:post1) { Fabricate(:post, topic: topic) }
fab!(:post2) { Fabricate(:post, topic: topic) }
before do
SiteSetting.assign_enabled = true
SiteSetting.prioritize_full_name_in_ux = false
# The system tests in this file are flaky and auth token related so turning this on
SiteSetting.verbose_auth_token_logging = true
sign_in(admin)
end
def assign_post(post, assignee)
topic_page.click_assign_post(post)
assign_modal.assignee = assignee
assign_modal.confirm
end
describe "with open topic" do
before { SiteSetting.prioritize_full_name_in_ux = false }
it "can assign and unassign" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(user: staff_user, at_post: 3)
expect(topic_page.find_post_assign(post1.post_number)).to have_content(staff_user.username)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(staff_user.username)
visit "/t/#{topic.id}"
topic_page.click_unassign_post(post2)
expect(topic_page).to have_unassigned_from_post(user: staff_user, at_post: 4)
expect(page).to have_no_css("#topic .assigned-to")
end
it "can submit modal form with shortcuts" do
visit "/t/#{topic.id}"
topic_page.click_assign_post(post2)
assign_modal.assignee = staff_user
find("body").send_keys(:tab)
find("body").send_keys(:control, :enter)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(user: staff_user, at_post: 3)
expect(topic_page.find_post_assign(post1.post_number)).to have_content(staff_user.username)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(staff_user.username)
end
context "when prioritize_full_name_in_ux setting is enabled" do
before { SiteSetting.prioritize_full_name_in_ux = true }
it "shows the user's name after assign" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(topic_page.find_post_assign(post1.post_number)).to have_content(staff_user.name)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(staff_user.name)
end
it "show the user's username if there is no name" do
visit "/t/#{topic.id}"
staff_user.name = nil
staff_user.save
staff_user.reload
assign_post(post2, staff_user)
expect(topic_page.find_post_assign(post1.post_number)).to have_content(staff_user.username)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(staff_user.username)
end
end
context "when assigns are not public" do
before { SiteSetting.assigns_public = false }
it "assigned small action post has 'private-assign' in class attribute" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(
user: staff_user,
at_post: 3,
class_attribute: ".private-assign",
)
end
end
context "when unassign_on_close is set to true" do
before { SiteSetting.unassign_on_close = true }
it "unassigns the topic on close" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(user: staff_user, at_post: 3)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_4")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_5")
expect(page).to have_no_css("#topic .assigned-to")
end
it "can assign the previous assignee" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(user: staff_user, at_post: 3)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_4")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_5")
expect(page).to have_no_css("#topic .assigned-to")
assign_post(post2, staff_user)
expect(page).to have_no_css("#post_5")
expect(topic_page.find_post_assign(post1.post_number)).to have_content(staff_user.username)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(staff_user.username)
end
context "when reassign_on_open is set to true" do
before { SiteSetting.reassign_on_open = true }
it "reassigns the topic on open" do
visit "/t/#{topic.id}"
assign_post(post2, staff_user)
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned_post(user: staff_user, at_post: 3)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_4")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_5")
expect(page).to have_no_css("#topic .assigned-to")
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-open").click
expect(find("#post_5")).to have_content(
I18n.t("js.action_codes.closed.disabled", when: "just now"),
)
expect(page).to have_no_css("#post_6")
expect(topic_page.find_post_assign(post1.post_number)).to have_content(
staff_user.username,
)
expect(topic_page.find_post_assign(post2.post_number)).to have_content(
staff_user.username,
)
end
end
end
end
end

View File

@ -10,6 +10,7 @@ describe "Assign | Assigning topics", type: :system do
before do before do
SiteSetting.assign_enabled = true SiteSetting.assign_enabled = true
SiteSetting.prioritize_full_name_in_ux = false
# The system tests in this file are flaky and auth token related so turning this on # The system tests in this file are flaky and auth token related so turning this on
SiteSetting.verbose_auth_token_logging = true SiteSetting.verbose_auth_token_logging = true
@ -49,6 +50,31 @@ describe "Assign | Assigning topics", type: :system do
expect(find("#topic .assigned-to")).to have_content(staff_user.username) expect(find("#topic .assigned-to")).to have_content(staff_user.username)
end end
context "when prioritize_full_name_in_ux setting is enabled" do
before { SiteSetting.prioritize_full_name_in_ux = true }
it "shows the user's name after assign" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(find("#topic .assigned-to")).to have_content(staff_user.name)
end
it "show the user's username if there is no name" do
visit "/t/#{topic.id}"
staff_user.name = nil
staff_user.save
staff_user.reload
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(find("#topic .assigned-to")).to have_content(staff_user.name)
end
end
context "when assigns are not public" do context "when assigns are not public" do
before { SiteSetting.assigns_public = false } before { SiteSetting.assigns_public = false }

View File

@ -12,28 +12,62 @@ module PageObjects
find("[data-value='unassign']").click find("[data-value='unassign']").click
end end
def click_unassign_post(post)
find("#topic-footer-dropdown-reassign").click
data_value = "unassign-from-post-#{post.id}"
find("[data-value=\"#{data_value}\"]").click
end
def click_assign_post(post)
find_post_action_button(post, :show_more).click
assign_post = within_post(post) { find(".post-action-menu__assign-post") }
assign_post.click
end
def click_edit_topic_assignment def click_edit_topic_assignment
find("#topic-footer-dropdown-reassign").click find("#topic-footer-dropdown-reassign").click
find("[data-value='reassign']").click find("[data-value='reassign']").click
end end
def find_post_assign(post_number)
within("#post_#{post_number}") { find(".assigned-to") }
end
def has_assigned?(args) def has_assigned?(args)
has_assignment_action?(action: "assigned", **args) has_assignment_action?(action: "assigned", **args)
end end
def has_assigned_post?(args)
has_assignment_action?(action: "assigned_to_post", **args)
end
def has_unassigned?(args) def has_unassigned?(args)
has_assignment_action?(action: "unassigned", **args) has_assignment_action?(action: "unassigned", **args)
end end
def has_unassigned_from_post?(args)
has_assignment_action?(action: "unassigned_from_post", **args)
end
def has_assignment_action?(args) def has_assignment_action?(args)
assignee = args[:group]&.name || args[:user]&.username assignee = args[:group]&.name || args[:user]&.username
container = container =
args[:at_post] ? find("#post_#{args[:at_post]}#{args[:class_attribute] || ""}") : page args[:at_post] ? find("#post_#{args[:at_post]}#{args[:class_attribute] || ""}") : page
container.has_content?( post_content =
I18n.t("js.action_codes.#{args[:action]}", who: "@#{assignee}", when: "just now"), I18n.t(
) "js.action_codes.#{args[:action]}",
path: "",
who: "@#{assignee}",
when: "just now",
)
if args[:action] == "assigned_to_post" || args[:action] == "unassigned_from_post"
post_content.gsub!(%r{<a[^>]*>(.*?)</a>}, '\1')
end
container.has_content?(post_content)
end end
end end
end end