FEATURE: Show note in tooltip (#327)

This commit is contained in:
Natalie Tay 2022-05-05 15:04:57 +08:00 committed by GitHub
parent 1ac2dfbae2
commit b314882a6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 68 additions and 24 deletions

View File

@ -2,6 +2,7 @@ import { renderAvatar } from "discourse/helpers/user-avatar";
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
import { escapeExpression } from "discourse/lib/utilities";
import { h } from "virtual-dom"; import { h } from "virtual-dom";
import { queryRegistry } from "discourse/widgets/widget"; import { queryRegistry } from "discourse/widgets/widget";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
@ -586,10 +587,19 @@ function initialize(api) {
api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true }); api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true });
api.addTagsHtmlCallback((topic, params = {}) => { api.addTagsHtmlCallback((topic, params = {}) => {
const [assignedToUser, assignedToGroup] = Object.values( const [assignedToUser, assignedToGroup, topicNote] = Object.values(
topic.getProperties("assigned_to_user", "assigned_to_group") topic.getProperties(
"assigned_to_user",
"assigned_to_group",
"assignment_note"
)
); );
const topicAssignee = {
assignee: assignedToUser || assignedToGroup,
note: topicNote,
};
let assignedToIndirectly; let assignedToIndirectly;
if (topic.get("indirectly_assigned_to")) { if (topic.get("indirectly_assigned_to")) {
assignedToIndirectly = Object.entries( assignedToIndirectly = Object.entries(
@ -603,17 +613,18 @@ function initialize(api) {
} }
const assignedTo = [] const assignedTo = []
.concat( .concat(
assignedToUser, topicAssignee,
assignedToGroup, assignedToIndirectly.map((assigned) => ({
assignedToIndirectly.map((assigned) => assigned.assigned_to) assignee: assigned.assigned_to,
note: assigned.assignment_note,
}))
) )
.filter((element) => element) .filter(({ assignee }) => assignee)
.flat() .flat();
.uniqBy((assignee) => assignee.assign_path);
if (assignedTo) { if (assignedTo) {
return assignedTo return assignedTo
.map((assignee) => { .map(({ assignee, note }) => {
let assignedPath; let assignedPath;
if (assignee.assignedToPostId) { if (assignee.assignedToPostId) {
assignedPath = `/p/${assignee.assignedToPostId}`; assignedPath = `/p/${assignee.assignedToPostId}`;
@ -629,7 +640,7 @@ function initialize(api) {
: ""; : "";
return `<${tagName} class="assigned-to discourse-tag simple" ${href}> return `<${tagName} class="assigned-to discourse-tag simple" ${href}>
${icon} ${icon}
<span>${name}</span> <span title="${escapeExpression(note)}">${name}</span>
</${tagName}>`; </${tagName}>`;
}) })
.join(""); .join("");
@ -769,7 +780,7 @@ function initialize(api) {
} }
const target = post || topic; const target = post || topic;
target.set("assignment_note", null); target.set("assignment_note", data.assignment_note);
if (data.assigned_type === "User") { if (data.assigned_type === "User") {
target.set( target.set(
"assigned_to_user_id", "assigned_to_user_id",

View File

@ -209,6 +209,8 @@ class ::Assigner
action_code[:user] = topic.assignment.present? ? "reassigned" : "assigned" action_code[:user] = topic.assignment.present? ? "reassigned" : "assigned"
action_code[:group] = topic.assignment.present? ? "reassigned_group" : "assigned_group" action_code[:group] = topic.assignment.present? ? "reassigned_group" : "assigned_group"
silent = silent || no_assignee_change?(assign_to)
@target.assignment&.destroy! @target.assignment&.destroy!
assignment = @target.create_assignment!(assigned_to_id: assign_to.id, assigned_to_type: type, assigned_by_user_id: @assigned_by.id, topic_id: topic.id, note: note) assignment = @target.create_assignment!(assigned_to_id: assign_to.id, assigned_to_type: type, assigned_by_user_id: @assigned_by.id, topic_id: topic.id, note: note)
@ -233,7 +235,8 @@ class ::Assigner
post_id: post_target? && @target.id, post_id: post_target? && @target.id,
post_number: post_target? && @target.post_number, post_number: post_target? && @target.post_number,
assigned_type: type, assigned_type: type,
assigned_to: serializer.new(assign_to, scope: Guardian.new, root: false).as_json assigned_to: serializer.new(assign_to, scope: Guardian.new, root: false).as_json,
assignment_note: note,
}, },
user_ids: allowed_user_ids user_ids: allowed_user_ids
) )
@ -385,7 +388,8 @@ class ::Assigner
topic_id: topic.id, topic_id: topic.id,
post_id: post_target? && @target.id, post_id: post_target? && @target.id,
post_number: post_target? && @target.post_number, post_number: post_target? && @target.post_number,
assigned_type: assignment.assigned_to.is_a?(User) ? "User" : "Group" assigned_type: assignment.assigned_to.is_a?(User) ? "User" : "Group",
assignment_note: nil,
}, },
user_ids: allowed_user_ids user_ids: allowed_user_ids
) )
@ -429,4 +433,8 @@ class ::Assigner
assignment&.note == note assignment&.note == note
end end
end end
def no_assignee_change?(assignee)
@target.assignment&.assigned_to_id == assignee.id
end
end end

View File

@ -30,12 +30,13 @@ module DiscourseAssign
def self.build_indirectly_assigned_to(post_assignments, topic) def self.build_indirectly_assigned_to(post_assignments, topic)
post_assignments.map do |post_id, assigned_map| post_assignments.map do |post_id, assigned_map|
assigned_to = assigned_map[:assigned_to] assigned_to = assigned_map[:assigned_to]
note = assigned_map[:assignment_note]
post_number = assigned_map[:post_number] post_number = assigned_map[:post_number]
if (assigned_to.is_a?(User)) if (assigned_to.is_a?(User))
[post_id, { assigned_to: build_assigned_to_user(assigned_to, topic), post_number: post_number }] [post_id, { assigned_to: build_assigned_to_user(assigned_to, topic), post_number: post_number, assignment_note: note }]
elsif assigned_to.is_a?(Group) elsif assigned_to.is_a?(Group)
[post_id, { assigned_to: build_assigned_to_group(assigned_to, topic), post_number: post_number }] [post_id, { assigned_to: build_assigned_to_group(assigned_to, topic), post_number: post_number, assignment_note: note }]
end end
end.to_h end.to_h
end end

View File

@ -430,7 +430,7 @@ after_initialize do
add_to_class(:topic, :indirectly_assigned_to) do add_to_class(:topic, :indirectly_assigned_to) do
return @indirectly_assigned_to if defined?(@indirectly_assigned_to) return @indirectly_assigned_to if defined?(@indirectly_assigned_to)
@indirectly_assigned_to = Assignment.where(topic_id: id, target_type: "Post", active: true).includes(:target).inject({}) do |acc, assignment| @indirectly_assigned_to = Assignment.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 } if assignment.target acc[assignment.target_id] = { assigned_to: assignment.assigned_to, post_number: assignment.target.post_number, assignment_note: assignment.note } if assignment.target
acc acc
end end
end end

View File

@ -47,13 +47,17 @@ describe Search do
result = Search.execute('in:assigned', guardian: guardian) result = Search.execute('in:assigned', guardian: guardian)
serializer = GroupedSearchResultSerializer.new(result, scope: guardian) serializer = GroupedSearchResultSerializer.new(result, scope: guardian)
indirectly_assigned_to = serializer.as_json[:topics].find { |topic| topic[:id] == post5.topic.id }[:indirectly_assigned_to] indirectly_assigned_to = serializer.as_json[:topics].find { |topic| topic[:id] == post5.topic.id }[:indirectly_assigned_to]
expect(indirectly_assigned_to).to eq(post5.id => { assigned_to: { expect(indirectly_assigned_to).to eq(post5.id => {
assign_icon: "user-plus", assigned_to: {
assign_path: "/u/#{user.username}/activity/assigned", assign_icon: "user-plus",
avatar_template: user.avatar_template, assign_path: "/u/#{user.username}/activity/assigned",
name: user.name, avatar_template: user.avatar_template,
username: user.username name: user.name,
}, post_number: post5.post_number }) username: user.username,
},
post_number: post5.post_number,
assignment_note: nil,
})
end end
end end
end end

View File

@ -52,7 +52,7 @@ RSpec.describe Assigner do
it "publishes topic assignment after assign and unassign" do it "publishes topic assignment after assign and unassign" do
messages = MessageBus.track_publish('/staff/topic-assignment') do messages = MessageBus.track_publish('/staff/topic-assignment') do
assigner = described_class.new(topic, moderator_2) assigner = described_class.new(topic, moderator_2)
assigner.assign(moderator) assigner.assign(moderator, note: "tomtom best mom")
assigner.unassign assigner.unassign
end end
@ -64,6 +64,7 @@ RSpec.describe Assigner do
post_number: false, post_number: false,
assigned_type: "User", assigned_type: "User",
assigned_to: BasicUserSerializer.new(moderator, scope: Guardian.new, root: false).as_json, assigned_to: BasicUserSerializer.new(moderator, scope: Guardian.new, root: false).as_json,
assignment_note: "tomtom best mom"
}) })
expect(messages[1].channel).to eq "/staff/topic-assignment" expect(messages[1].channel).to eq "/staff/topic-assignment"
@ -73,6 +74,7 @@ RSpec.describe Assigner do
post_id: false, post_id: false,
post_number: false, post_number: false,
assigned_type: "User", assigned_type: "User",
assignment_note: nil
}) })
end end

View File

@ -35,4 +35,10 @@ RSpec.describe TopicViewSerializer do
serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian) serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian)
expect(serializer.as_json[:topic_view][:assignment_note]).to eq("note me down") expect(serializer.as_json[:topic_view][:assignment_note]).to eq("note me down")
end end
it "includes indirectly_assigned_to notes in serializer" do
Assigner.new(post, user).assign(user, note: "note me down")
serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian)
expect(serializer.as_json[:topic_view][:indirectly_assigned_to][post.id][:assignment_note]).to eq("note me down")
end
end end

View File

@ -20,12 +20,14 @@ function assignCurrentUserToTopic(needs) {
avatar_template: avatar_template:
"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png", "/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
}; };
topic["assignment_note"] = "Shark Doododooo";
topic["indirectly_assigned_to"] = { topic["indirectly_assigned_to"] = {
2: { 2: {
assigned_to: { assigned_to: {
name: "Developers", name: "Developers",
}, },
post_number: 2, post_number: 2,
assignment_note: '<script>alert("xss")</script>',
}, },
}; };
return helper.response(topic); return helper.response(topic);
@ -98,6 +100,16 @@ acceptance("Discourse Assign | Assigned topic", function (needs) {
"shows assignment and indirect assignments in the first post" "shows assignment and indirect assignments in the first post"
); );
assert.ok(exists("#post_1 .assigned-to svg.d-icon-user-plus")); assert.ok(exists("#post_1 .assigned-to svg.d-icon-user-plus"));
assert.equal(
query(".discourse-tags .assigned-to[href='/t/28830'] span").title,
"Shark Doododooo",
"shows topic assign notes"
);
assert.equal(
query(".discourse-tags .assigned-to[href='/p/2'] span").title,
'<script>alert("xss")</script>',
"shows indirect assign notes"
);
assert.ok( assert.ok(
exists("#topic-footer-dropdown-reassign"), exists("#topic-footer-dropdown-reassign"),
"shows reassign dropdown at the bottom of the topic" "shows reassign dropdown at the bottom of the topic"