DEV: Add compatibility with the Glimmer Post Stream (#651)

This commit is contained in:
Sérgio Saquetim 2025-05-05 15:18:17 -03:00 committed by GitHub
parent bf52519dc7
commit bbb5147062
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 931 additions and 647 deletions

View File

@ -0,0 +1,124 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import icon from "discourse/helpers/d-icon";
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import { assignedToGroupPath, assignedToUserPath } from "../lib/url";
export default class AssignedToFirstPost extends Component {
@service siteSettings;
get assignedToUser() {
return this.args.post?.topic?.assigned_to_user;
}
get assignedToGroup() {
return this.args.post?.topic?.assigned_to_group;
}
get icon() {
return this.assignedToUser ? "user-plus" : "group-plus";
}
get indirectlyAssignedTo() {
return this.args.post?.topic?.indirectly_assigned_to;
}
get indirectAssignments() {
if (!this.indirectlyAssignedTo) {
return null;
}
return Object.keys(this.indirectlyAssignedTo).map((postId) => {
const postNumber = this.indirectlyAssignedTo[postId].post_number;
return {
postId,
assignee: this.indirectlyAssignedTo[postId].assigned_to,
postNumber,
url: `${this.args.post.topic.url}/${postNumber}`,
};
});
}
get isAssigned() {
return !!(
this.assignedToUser ||
this.assignedToGroup ||
this.args.post?.topic?.indirectly_assigned_to
);
}
get hasOnlyIndirectAssignments() {
return !this.assignedToUser && !this.assignedToGroup;
}
@bind
prioritizedAssigneeName(assignee) {
// if this code is ever replaced to use `prioritize_username_in_ux`, remove this function and use the helper
// userPrioritizedName instead
return this.siteSettings.prioritize_full_name_in_ux || !assignee.username
? assignee.name || assignee.username
: assignee.username;
}
<template>
{{#if this.isAssigned}}
<p class="assigned-to">
{{icon this.icon}}
{{#if this.assignedToUser}}
<span class="assignee">
<span class="assigned-to--user">
{{htmlSafe
(i18n
"discourse_assign.assigned_topic_to"
username=(this.prioritizedAssigneeName this.assignedToUser)
path=(assignedToUserPath this.assignedToUser)
)
}}
</span>
</span>
{{/if}}
{{#if this.assignedToGroup}}
<span class="assignee">
<span class="assigned-to--group">
{{htmlSafe
(i18n
"discourse_assign.assigned_topic_to"
username=this.assignedToGroup.name
path=(assignedToGroupPath this.assignedToGroup)
)
}}
</span>
</span>
{{/if}}
{{#if this.hasOnlyIndirectAssignments}}
<span class="assign-text">
{{i18n "discourse_assign.assigned"}}
</span>
{{/if}}
{{#if this.indirectlyAssignedTo}}
{{#each
this.indirectAssignments key="postId"
as |indirectAssignment|
}}
<span class="assignee">
<a href={{indirectAssignment.url}} class="assigned-indirectly">
{{i18n
"discourse_assign.assign_post_to_multiple"
post_number=indirectAssignment.postNumber
username=(this.prioritizedAssigneeName
indirectAssignment.assignee
)
}}
</a>
</span>
{{/each}}
{{/if}}
</p>
{{/if}}
</template>
}

View File

@ -0,0 +1,49 @@
import Component from "@glimmer/component";
import { assignedToGroupPath, assignedToUserPath } from "../lib/url";
import AssignedFirstPost from "./assigned-to-first-post";
import AssignedToPost from "./assigned-to-post";
export default class PostAssignmentsDisplay extends Component {
static shouldRender(args) {
return args.post;
}
get post() {
return this.args.outletArgs.post;
}
get assignedTo() {
return this.post.topic?.indirectly_assigned_to?.[this.post.id]?.assigned_to;
}
get assignedToUser() {
return this.assignedTo.username ? this.assignedTo : null;
}
get assignedToGroup() {
return !this.assignedToUser && this.assignedTo.name
? this.assignedTo
: null;
}
get assignedHref() {
return this.assignedToUser
? assignedToUserPath(this.assignedToUser)
: assignedToGroupPath(this.assignedToGroup);
}
<template>
{{#if this.post.firstPost}}
<AssignedFirstPost @post={{this.post}} />
{{else if this.assignedTo}}
<p class="assigned-to">
<AssignedToPost
@assignedToUser={{this.assignedToUser}}
@assignedToGroup={{this.assignedToGroup}}
@href={{this.assignedHref}}
@post={{this.post}}
/>
</p>
{{/if}}
</template>
}

View File

@ -6,6 +6,7 @@ import { hbs } from "ember-cli-htmlbars";
import { h } from "virtual-dom";
import { renderAvatar } from "discourse/helpers/user-avatar";
import discourseComputed from "discourse/lib/decorators";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import getURL from "discourse/lib/get-url";
import { iconHTML, iconNode } from "discourse/lib/icon-library";
import { withPluginApi } from "discourse/lib/plugin-api";
@ -18,7 +19,9 @@ import { i18n } from "discourse-i18n";
import AssignButton from "../components/assign-button";
import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user";
import EditTopicAssignments from "../components/modal/edit-topic-assignments";
import PostAssignmentsDisplay from "../components/post-assignments-display";
import TopicLevelAssignMenu from "../components/topic-level-assign-menu";
import { assignedToGroupPath, assignedToUserPath } from "../lib/url";
import { extendTopicModel } from "../models/topic";
const DEPENDENT_KEYS = [
@ -321,12 +324,6 @@ function initialize(api) {
}
}
api.addPostSmallActionClassesCallback((post) => {
if (post.actionCode.includes("assigned") && !siteSettings.assigns_public) {
return ["private-assign"];
}
});
api.addAdvancedSearchOptions(
api.getCurrentUser()?.can_assign
? {
@ -344,19 +341,6 @@ function initialize(api) {
: {}
);
function assignedToUserPath(assignedToUser) {
return getURL(
siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
}
function assignedToGroupPath(assignedToGroup) {
return getURL(`/g/${assignedToGroup.name}/assigned/everyone`);
}
api.modifyClass(
"model:bookmark",
(Superclass) =>
@ -391,41 +375,6 @@ function initialize(api) {
}
);
api.addPostSmallActionIcon("assigned", "user-plus");
api.addPostSmallActionIcon("assigned_to_post", "user-plus");
api.addPostSmallActionIcon("assigned_group", "group-plus");
api.addPostSmallActionIcon("assigned_group_to_post", "group-plus");
api.addPostSmallActionIcon("unassigned", "user-xmark");
api.addPostSmallActionIcon("unassigned_group", "group-times");
api.addPostSmallActionIcon("unassigned_from_post", "user-xmark");
api.addPostSmallActionIcon("unassigned_group_from_post", "group-times");
api.includePostAttributes("assigned_to_user", "assigned_to_group");
api.addPostSmallActionIcon("reassigned", "user-plus");
api.addPostSmallActionIcon("reassigned_group", "group-plus");
api.addPostTransformCallback((transformed) => {
if (
[
"assigned",
"unassigned",
"reassigned",
"assigned_group",
"unassigned_group",
"reassigned_group",
"assigned_to_post",
"assigned_group_to_post",
"unassigned_from_post",
"unassigned_group_from_post",
"details_change",
"note_change",
"status_change",
].includes(transformed.actionCode)
) {
transformed.isSmallAction = true;
transformed.canEdit = true;
}
});
api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true });
api.addTagsHtmlCallback((topic, params = {}) => {
@ -518,119 +467,6 @@ function initialize(api) {
return result;
});
api.createWidget("assigned-to-post", {
html(attrs) {
return new RenderGlimmer(
this,
"p.assigned-to",
hbs`
<AssignedToPost @assignedToUser={{@data.assignedToUser}} @assignedToGroup={{@data.assignedToGroup}}
@href={{@data.href}} @post={{@data.post}} />`,
{
assignedToUser: attrs.post.assigned_to_user,
assignedToGroup: attrs.post.assigned_to_group,
href: attrs.href,
post: attrs.post,
}
);
},
});
api.createWidget("assigned-to-first-post", {
html(attrs) {
const topic = attrs.topic;
const [assignedToUser, assignedToGroup, indirectlyAssignedTo] = [
topic.assigned_to_user,
topic.assigned_to_group,
topic.indirectly_assigned_to,
];
const assigneeElements = [];
const assignedHtml = (username, path, type) => {
return `<span class="assigned-to--${type}">${htmlSafe(
i18n("discourse_assign.assigned_topic_to", {
username,
path,
})
)}</span>`;
};
let displayedName = "";
if (assignedToUser) {
displayedName = this.siteSettings.prioritize_full_name_in_ux
? assignedToUser.name || assignedToUser.username
: assignedToUser.username;
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
displayedName,
assignedToUserPath(assignedToUser),
"user"
),
})
)
);
}
if (assignedToGroup) {
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
assignedToGroup.name,
assignedToGroupPath(assignedToGroup),
"group"
),
})
)
);
}
if (indirectlyAssignedTo) {
Object.keys(indirectlyAssignedTo).map((postId) => {
const assignee = indirectlyAssignedTo[postId].assigned_to;
const postNumber = indirectlyAssignedTo[postId].post_number;
displayedName =
this.siteSettings.prioritize_full_name_in_ux || !assignee.username
? assignee.name || assignee.username
: assignee.username;
assigneeElements.push(
h("span.assignee", [
h(
"a",
{
attributes: {
class: "assigned-indirectly",
href: `${topic.url}/${postNumber}`,
},
},
i18n("discourse_assign.assign_post_to_multiple", {
post_number: postNumber,
username: displayedName,
})
),
])
);
});
}
if (!isEmpty(assigneeElements)) {
return h("p.assigned-to", [
assignedToUser ? iconNode("user-plus") : iconNode("group-plus"),
assignedToUser || assignedToGroup
? ""
: h("span.assign-text", i18n("discourse_assign.assigned")),
assigneeElements,
]);
}
},
});
api.modifyClass(
"model:group",
(Superclass) =>
@ -717,6 +553,86 @@ function initialize(api) {
}
);
customizePost(api, siteSettings);
api.replaceIcon("notification.assigned", "user-plus");
api.replaceIcon(
"notification.discourse_assign.assign_group_notification",
"group-plus"
);
api.modifyClass(
"controller:preferences/notifications",
(Superclass) =>
class extends Superclass {
@action
save() {
this.saveAttrNames.push("custom_fields");
super.save(...arguments);
}
}
);
api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" });
}
function customizePost(api, siteSettings) {
api.addTrackedPostProperties("assigned_to_user", "assigned_to_group");
api.modifyClass(
"model:post",
(Superclass) =>
class extends Superclass {
get can_edit() {
return isAssignSmallAction(this.action_code) ? true : super.can_edit;
}
// overriding tracked properties requires overriding both the getter and the setter.
// otherwise the superclass will throw an error when the application sets the field value
set can_edit(value) {
super.can_edit = value;
}
get isSmallAction() {
return isAssignSmallAction(this.action_code)
? true
: super.isSmallAction;
}
}
);
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
PostAssignmentsDisplay
);
api.addPostSmallActionClassesCallback((post) => {
// TODO (glimmer-post-stream): only check for .action_code once the widget code is removed
const actionCode = post.action_code || post.actionCode;
if (actionCode.includes("assigned") && !siteSettings.assigns_public) {
return ["private-assign"];
}
});
api.addPostSmallActionIcon("assigned", "user-plus");
api.addPostSmallActionIcon("assigned_to_post", "user-plus");
api.addPostSmallActionIcon("assigned_group", "group-plus");
api.addPostSmallActionIcon("assigned_group_to_post", "group-plus");
api.addPostSmallActionIcon("unassigned", "user-xmark");
api.addPostSmallActionIcon("unassigned_group", "group-times");
api.addPostSmallActionIcon("unassigned_from_post", "user-xmark");
api.addPostSmallActionIcon("unassigned_group_from_post", "group-times");
api.addPostSmallActionIcon("reassigned", "user-plus");
api.addPostSmallActionIcon("reassigned_group", "group-plus");
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (dec) => {
const postModel = dec.getModel();
if (postModel) {
@ -752,26 +668,146 @@ function initialize(api) {
}
});
api.replaceIcon("notification.assigned", "user-plus");
api.replaceIcon(
"notification.discourse_assign.assign_group_notification",
"group-plus"
);
api.modifyClass(
"controller:preferences/notifications",
(Superclass) =>
class extends Superclass {
@action
save() {
this.saveAttrNames.push("custom_fields");
super.save(...arguments);
api.createWidget("assigned-to-post", {
html(attrs) {
return new RenderGlimmer(
this,
"p.assigned-to",
hbs`
<AssignedToPost @assignedToUser={{@data.assignedToUser}} @assignedToGroup={{@data.assignedToGroup}}
@href={{@data.href}} @post={{@data.post}} />`,
{
assignedToUser: attrs.post.assigned_to_user,
assignedToGroup: attrs.post.assigned_to_group,
href: attrs.href,
post: attrs.post,
}
}
);
);
},
});
api.addKeyboardShortcut("g a", "", { path: "/my/activity/assigned" });
api.createWidget("assigned-to-first-post", {
html(attrs) {
const topic = attrs.topic;
const [assignedToUser, assignedToGroup, indirectlyAssignedTo] = [
topic.assigned_to_user,
topic.assigned_to_group,
topic.indirectly_assigned_to,
];
const assigneeElements = [];
const assignedHtml = (username, path, type) => {
return `<span class="assigned-to--${type}">${htmlSafe(
i18n("discourse_assign.assigned_topic_to", {
username,
path,
})
)}</span>`;
};
let displayedName = "";
if (assignedToUser) {
displayedName = this.siteSettings.prioritize_full_name_in_ux
? assignedToUser.name || assignedToUser.username
: assignedToUser.username;
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
displayedName,
assignedToUserPath(assignedToUser),
"user"
),
})
)
);
}
if (assignedToGroup) {
assigneeElements.push(
h(
"span.assignee",
new RawHtml({
html: assignedHtml(
assignedToGroup.name,
assignedToGroupPath(assignedToGroup),
"group"
),
})
)
);
}
if (indirectlyAssignedTo) {
Object.keys(indirectlyAssignedTo).map((postId) => {
const assignee = indirectlyAssignedTo[postId].assigned_to;
const postNumber = indirectlyAssignedTo[postId].post_number;
displayedName =
!this.siteSettings.prioritize_username_in_ux || !assignee.username
? assignee.name || assignee.username
: assignee.username;
assigneeElements.push(
h("span.assignee", [
h(
"a",
{
attributes: {
class: "assigned-indirectly",
href: `${topic.url}/${postNumber}`,
},
},
i18n("discourse_assign.assign_post_to_multiple", {
post_number: postNumber,
username: displayedName,
})
),
])
);
});
}
if (!isEmpty(assigneeElements)) {
return h("p.assigned-to", [
assignedToUser ? iconNode("user-plus") : iconNode("group-plus"),
assignedToUser || assignedToGroup
? ""
: h("span.assign-text", i18n("discourse_assign.assigned")),
assigneeElements,
]);
}
},
});
// `addPostTransformCallback` doesn't have a direct translation in the new Glimmer API.
// We need to use a modify class in the post model instead
api.addPostTransformCallback((transformed) => {
if (isAssignSmallAction(transformed.actionCode)) {
transformed.isSmallAction = true;
transformed.canEdit = true;
}
});
}
function isAssignSmallAction(actionCode) {
return [
"assigned",
"unassigned",
"reassigned",
"assigned_group",
"unassigned_group",
"reassigned_group",
"assigned_to_post",
"assigned_group_to_post",
"unassigned_from_post",
"unassigned_group_from_post",
"details_change",
"note_change",
"status_change",
].includes(actionCode);
}
function customizePostMenu(api) {

View File

@ -0,0 +1,19 @@
import { getOwnerWithFallback } from "discourse/lib/get-owner";
import getURL from "discourse/lib/get-url";
export function assignedToUserPath(assignedToUser) {
const siteSettings = getOwnerWithFallback(this).lookup(
"service:site-settings"
);
return getURL(
siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
}
export function assignedToGroupPath(assignedToGroup) {
return getURL(`/g/${assignedToGroup.name}/assigned/everyone`);
}

View File

@ -17,136 +17,12 @@ describe "Assign | Assigning topics", type: :system do
sign_in(admin)
end
describe "with open topic" do
it "can assign and unassign" do
visit "/t/#{topic.id}"
%w[enabled disabled].each do |value|
before { SiteSetting.glimmer_post_stream_mode = value }
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
expect(find("#topic .assigned-to")).to have_content(staff_user.username)
topic_page.click_unassign_topic
expect(topic_page).to have_unassigned(user: staff_user, at_post: 3)
expect(page).to have_no_css("#topic .assigned-to")
end
it "can submit form with shortcut from texatea" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
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(user: staff_user, at_post: 2)
expect(find("#topic .assigned-to")).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}"
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
before { SiteSetting.assigns_public = false }
it "assigned small action post has 'private-assign' in class attribute" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(
user: staff_user,
at_post: 2,
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}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
end
it "can assign the previous assignee" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(page).to have_no_css("#post_4")
expect(find("#topic .assigned-to")).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
context "when glimmer_post_stream_mode=#{value}" do
describe "with open topic" do
it "can assign and unassign" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
@ -155,24 +31,154 @@ describe "Assign | Assigning topics", type: :system do
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-open").click
expect(find("#post_4")).to have_content(
I18n.t("js.action_codes.closed.disabled", when: "just now"),
)
expect(page).to have_no_css("#post_5")
expect(find("#topic .assigned-to")).to have_content(staff_user.username)
topic_page.click_unassign_topic
expect(topic_page).to have_unassigned(user: staff_user, at_post: 3)
expect(page).to have_no_css("#topic .assigned-to")
end
it "can submit form with shortcut from texatea" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
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(user: staff_user, at_post: 2)
expect(find("#topic .assigned-to")).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}"
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
before { SiteSetting.assigns_public = false }
it "assigned small action post has 'private-assign' in class attribute" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(
user: staff_user,
at_post: 2,
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}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
end
it "can assign the previous assignee" do
visit "/t/#{topic.id}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(page).to have_no_css("#post_4")
expect(find("#topic .assigned-to")).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}"
topic_page.click_assign_topic
assign_modal.assignee = staff_user
assign_modal.confirm
expect(assign_modal).to be_closed
expect(topic_page).to have_assigned(user: staff_user, at_post: 2)
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-close").click
expect(find("#post_3")).to have_content(
I18n.t("js.action_codes.closed.enabled", when: "just now"),
)
expect(page).to have_no_css("#post_4")
expect(page).to have_no_css("#topic .assigned-to")
find(".timeline-controls .toggle-admin-menu").click
find(".topic-admin-open").click
expect(find("#post_4")).to have_content(
I18n.t("js.action_codes.closed.disabled", when: "just now"),
)
expect(page).to have_no_css("#post_5")
expect(find("#topic .assigned-to")).to have_content(staff_user.username)
end
end
end
end
end

View File

@ -2,13 +2,21 @@ import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
acceptance("Discourse Assign | Assign disabled mobile", function (needs) {
needs.user({ can_assign: true });
needs.mobileView();
needs.settings({ assign_enabled: false });
["enabled", "disabled"].forEach((postStreamMode) => {
acceptance(
`Discourse Assign | Assign disabled mobile (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user({ can_assign: true });
needs.mobileView();
needs.settings({
assign_enabled: false,
glimmer_post_stream_mode: postStreamMode,
});
test("Footer dropdown does not contain button", async function (assert) {
await visit("/t/internationalization-localization/280");
assert.dom(".assign").doesNotExist();
});
test("Footer dropdown does not contain button", async function (assert) {
await visit("/t/internationalization-localization/280");
assert.dom(".assign").doesNotExist();
});
}
);
});

View File

@ -45,175 +45,191 @@ acceptance("Discourse Assign | Assign mobile", function (needs) {
});
});
acceptance("Discourse Assign | Assign desktop", function (needs) {
needs.user({ can_assign: true });
needs.settings({ assign_enabled: true });
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",
},
],
["enabled", "disabled"].forEach((postStreamMode) => {
acceptance(
`Discourse Assign | Assign desktop (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user({ can_assign: true });
needs.settings({
assign_enabled: true,
glimmer_post_stream_mode: postStreamMode,
});
});
});
test("Assigning user to a post", async function (assert) {
await visit("/t/internationalization-localization/280");
assert
.dom("#post_2 .post-action-menu__assign-post")
.doesNotExist("assign to post button is hidden");
await click("#post_2 button.show-more-actions");
assert
.dom("#post_2 .post-action-menu__assign-post")
.exists("assign to post button exists");
await click("#post_2 .post-action-menu__assign-post");
assert.dom(".assign.d-modal").exists("assign modal opens");
const menu = selectKit(".assign.d-modal .user-chooser");
assert.true(menu.isExpanded(), "user selector is expanded");
await click(".assign.d-modal .btn-primary");
assert.dom(".error-label").includesText("Choose a user to assign");
await menu.expand();
await menu.selectRowByIndex(0);
assert.strictEqual(menu.header().value(), "eviltrout");
assert.dom(".error-label").doesNotExist();
pretender.put("/assign/assign", ({ requestBody }) => {
const body = parsePostData(requestBody);
assert.strictEqual(body.target_type, "Post");
assert.strictEqual(body.username, "eviltrout");
assert.strictEqual(body.note, "a note!");
return response({ success: true });
});
await fillIn("#assign-modal-note", "a note!");
await click(".assign.d-modal .btn-primary");
assert.dom(".assign.d-modal").doesNotExist("assign modal closes");
});
test("Footer dropdown contains button", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
assert.dom(".assign.d-modal").exists("assign modal opens");
});
});
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.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",
},
],
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",
},
],
});
});
});
});
});
test("Modal contains status dropdown", async function (assert) {
pretender.put("/assign/assign", ({ requestBody }) => {
const body = parsePostData(requestBody);
assert.strictEqual(body.target_type, "Topic");
assert.strictEqual(body.target_id, "280");
assert.strictEqual(body.username, "eviltrout");
assert.strictEqual(body.status, "In Progress");
test("Assigning user to a post", async function (assert) {
await visit("/t/internationalization-localization/280");
return response({ success: true });
});
assert
.dom("#post_2 .post-action-menu__assign-post")
.doesNotExist("assign to post button is hidden");
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
await click("#post_2 button.show-more-actions");
assert
.dom("#post_2 .post-action-menu__assign-post")
.exists("assign to post button exists");
assert
.dom(".assign.d-modal #assign-status")
.exists("assign status dropdown exists");
await click("#post_2 .post-action-menu__assign-post");
assert.dom(".assign.d-modal").exists("assign modal opens");
const statusDropdown = selectKit("#assign-status");
assert.strictEqual(statusDropdown.header().value(), "New");
const menu = selectKit(".assign.d-modal .user-chooser");
assert.true(menu.isExpanded(), "user selector is expanded");
await statusDropdown.expand();
await statusDropdown.selectRowByValue("In Progress");
assert.strictEqual(statusDropdown.header().value(), "In Progress");
await click(".assign.d-modal .btn-primary");
assert.dom(".error-label").includesText("Choose a user to assign");
const menu = selectKit(".assign.d-modal .user-chooser");
await menu.expand();
await menu.selectRowByIndex(0);
await menu.expand();
await menu.selectRowByIndex(0);
assert.strictEqual(menu.header().value(), "eviltrout");
assert.dom(".error-label").doesNotExist();
await click(".assign.d-modal .btn-primary");
});
});
pretender.put("/assign/assign", ({ requestBody }) => {
const body = parsePostData(requestBody);
assert.strictEqual(body.target_type, "Post");
assert.strictEqual(body.username, "eviltrout");
assert.strictEqual(body.note, "a note!");
return response({ success: true });
});
acceptance("Discourse Assign | Assign Status disabled", function (needs) {
needs.user({
can_assign: true,
});
needs.settings({
assign_enabled: true,
enable_assign_status: false,
});
await fillIn("#assign-modal-note", "a note!");
await click(".assign.d-modal .btn-primary");
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",
},
],
assert.dom(".assign.d-modal").doesNotExist("assign modal closes");
});
});
});
test("Modal contains status dropdown", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
test("Footer dropdown contains button", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
assert
.dom(".assign.d-modal #assign-status")
.doesNotExist("assign status dropdown doesn't exists");
});
assert.dom(".assign.d-modal").exists("assign modal opens");
});
}
);
acceptance(
`Discourse Assign | Assign Status enabled (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user({
can_assign: true,
});
needs.settings({
assign_enabled: true,
enable_assign_status: true,
assign_statuses: "New|In Progress|Done",
glimmer_post_stream_mode: postStreamMode,
});
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",
},
],
});
});
});
test("Modal contains status dropdown", async function (assert) {
pretender.put("/assign/assign", ({ requestBody }) => {
const body = parsePostData(requestBody);
assert.strictEqual(body.target_type, "Topic");
assert.strictEqual(body.target_id, "280");
assert.strictEqual(body.username, "eviltrout");
assert.strictEqual(body.status, "In Progress");
return response({ success: true });
});
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
assert
.dom(".assign.d-modal #assign-status")
.exists("assign status dropdown exists");
const statusDropdown = selectKit("#assign-status");
assert.strictEqual(statusDropdown.header().value(), "New");
await statusDropdown.expand();
await statusDropdown.selectRowByValue("In Progress");
assert.strictEqual(statusDropdown.header().value(), "In Progress");
const menu = selectKit(".assign.d-modal .user-chooser");
await menu.expand();
await menu.selectRowByIndex(0);
await click(".assign.d-modal .btn-primary");
});
}
);
acceptance(
`Discourse Assign | Assign Status disabled (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user({
can_assign: true,
});
needs.settings({
assign_enabled: true,
enable_assign_status: false,
glimmer_post_stream_mode: postStreamMode,
});
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",
},
],
});
});
});
test("Modal contains status dropdown", async function (assert) {
await visit("/t/internationalization-localization/280");
await click("#topic-footer-button-assign");
assert
.dom(".assign.d-modal #assign-status")
.doesNotExist("assign status dropdown doesn't exists");
});
}
);
});
// See RemindAssignsFrequencySiteSettings

View File

@ -82,171 +82,197 @@ function assignNewUserToTopic(needs) {
});
}
acceptance("Discourse Assign | Assigned topic", function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
assigns_public: true,
enable_assign_status: true,
});
["enabled", "disabled"].forEach((postStreamMode) => {
acceptance(
`Discourse Assign | Assigned topic (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
assigns_public: true,
enable_assign_status: true,
glimmer_post_stream_mode: postStreamMode,
});
assignCurrentUserToTopic(needs);
assignCurrentUserToTopic(needs);
test("Shows user assignment info", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/44");
test("Shows user assignment info", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/44");
assert
.dom("#topic-title .assigned-to")
.hasText("eviltrout", "shows assignment in the header");
assert
.dom("#post_1 .assigned-to")
.hasText(
"Assigned topic to eviltrout#2 to Developers",
"shows assignment and indirect assignments in the first post"
);
assert.dom("#post_1 .assigned-to svg.d-icon-user-plus").exists();
assert.dom("#post_1 .assigned-to a[href='/']").exists();
assert
.dom(".discourse-tags .assigned-to[href='/t/28830'] span")
.hasAttribute("title", "Shark Doododooo", "shows topic assign notes");
assert
.dom(".discourse-tags .assigned-to[href='/p/2'] span")
.hasAttribute(
"title",
'<script>alert("xss")</script>',
"shows indirect assign notes"
);
assert
.dom("#topic-footer-dropdown-reassign")
.exists("shows reassign dropdown at the bottom of the topic");
});
assert
.dom("#topic-title .assigned-to")
.hasText("eviltrout", "shows assignment in the header");
test("Shows group assignment info", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/45");
assert
.dom("#post_1 .assigned-to")
.includesText(
"Assigned topic to eviltrout",
"shows assignment in the first post"
);
assert
.dom("#post_1 .assigned-to")
.includesText("#2 to Developers", "Also shows indirects assignments");
assert.dom("#post_1 .assigned-to svg.d-icon-user-plus").exists();
assert.dom("#post_1 .assigned-to a[href='/']").exists();
assert
.dom(".discourse-tags .assigned-to[href='/t/28830'] span")
.hasAttribute("title", "Shark Doododooo", "shows topic assign notes");
assert
.dom(".discourse-tags .assigned-to[href='/p/2'] span")
.hasAttribute(
"title",
'<script>alert("xss")</script>',
"shows indirect assign notes"
);
assert
.dom("#topic-footer-dropdown-reassign")
.exists("shows reassign dropdown at the bottom of the topic");
});
assert
.dom("#topic-title .assigned-to")
.hasText("Developers", "shows assignment in the header");
assert
.dom("#post_1 .assigned-to--group")
.hasText(
"Assigned topic to Developers",
"shows assignment in the first post"
);
assert.dom("#post_1 .assigned-to svg.d-icon-group-plus").exists();
assert
.dom("#post_1 .assigned-to a[href='/g/Developers/assigned/everyone']")
.exists();
assert
.dom("#topic-footer-dropdown-reassign")
.exists("shows reassign dropdown at the bottom of the topic");
});
test("Shows group assignment info", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/45");
test("User without assign ability cannot see footer button", async function (assert) {
updateCurrentUser({ can_assign: false, admin: false, moderator: false });
await visit("/t/assignment-topic/45");
assert
.dom("#topic-title .assigned-to")
.hasText("Developers", "shows assignment in the header");
assert
.dom("#post_1 .assigned-to--group")
.hasText(
"Assigned topic to Developers",
"shows assignment in the first post"
);
assert.dom("#post_1 .assigned-to svg.d-icon-group-plus").exists();
assert
.dom("#post_1 .assigned-to a[href='/g/Developers/assigned/everyone']")
.exists();
assert
.dom("#topic-footer-dropdown-reassign")
.exists("shows reassign dropdown at the bottom of the topic");
});
assert
.dom("#topic-footer-dropdown-reassign")
.doesNotExist(
"does not show reassign dropdown at the bottom of the topic"
);
});
test("User without assign ability cannot see footer button", async function (assert) {
updateCurrentUser({
can_assign: false,
admin: false,
moderator: false,
});
await visit("/t/assignment-topic/45");
test("Shows assignment notification", async function (assert) {
updateCurrentUser({ can_assign: true });
assert
.dom("#topic-footer-dropdown-reassign")
.doesNotExist(
"does not show reassign dropdown at the bottom of the topic"
);
});
await visit("/u/eviltrout/notifications");
test("Shows assignment notification", async function (assert) {
updateCurrentUser({ can_assign: true });
const notification = query(
"section.user-content .user-notifications-list li.notification"
);
await visit("/u/eviltrout/notifications");
assert.true(
notification.classList.contains("assigned"),
"with correct assigned class"
);
const notification = query(
"section.user-content .user-notifications-list li.notification"
);
assert.strictEqual(
notification.querySelector("a").title,
i18n("notifications.titles.assigned"),
"with correct title"
);
assert.strictEqual(
notification.querySelector("svg use").href["baseVal"],
"#user-plus",
"with correct icon"
);
});
});
acceptance("Discourse Assign | Reassign topic", function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
});
assignNewUserToTopic(needs);
test("Reassign Footer dropdown contains reassign buttons", async function (assert) {
updateCurrentUser({ can_assign: true });
const menu = selectKit("#topic-footer-dropdown-reassign");
await visit("/t/assignment-topic/44");
await menu.expand();
assert.true(menu.rowByValue("unassign").exists());
assert.true(menu.rowByValue("reassign").exists());
assert.true(menu.rowByValue("reassign-self").exists());
});
});
acceptance("Discourse Assign | Reassign topic | mobile", function (needs) {
needs.user();
needs.mobileView();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
});
assignNewUserToTopic(needs);
test("Mobile Footer dropdown contains reassign buttons", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/44");
await click(".topic-footer-mobile-dropdown-trigger");
assert.dom("#topic-footer-button-unassign-mobile").exists();
assert.dom("#topic-footer-button-reassign-self-mobile").exists();
assert.dom("#topic-footer-button-reassign-mobile").exists();
});
});
acceptance("Discourse Assign | Reassign topic conditionals", function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
});
assignCurrentUserToTopic(needs);
test("Reassign Footer dropdown won't display reassign-to-self button when already assigned to current user", async function (assert) {
updateCurrentUser({ can_assign: true });
const menu = selectKit("#topic-footer-dropdown-reassign");
await visit("/t/assignment-topic/44");
await menu.expand();
assert.false(menu.rowByValue("reassign-self").exists());
});
assert.true(
notification.classList.contains("assigned"),
"with correct assigned class"
);
assert.strictEqual(
notification.querySelector("a").title,
i18n("notifications.titles.assigned"),
"with correct title"
);
assert.strictEqual(
notification.querySelector("svg use").href["baseVal"],
"#user-plus",
"with correct icon"
);
});
}
);
acceptance(
`Discourse Assign | Reassign topic (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
glimmer_post_stream_mode: postStreamMode,
});
assignNewUserToTopic(needs);
test("Reassign Footer dropdown contains reassign buttons", async function (assert) {
updateCurrentUser({ can_assign: true });
const menu = selectKit("#topic-footer-dropdown-reassign");
await visit("/t/assignment-topic/44");
await menu.expand();
assert.true(menu.rowByValue("unassign").exists());
assert.true(menu.rowByValue("reassign").exists());
assert.true(menu.rowByValue("reassign-self").exists());
});
}
);
acceptance(
`Discourse Assign | Reassign topic | mobile (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user();
needs.mobileView();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
glimmer_post_stream_mode: postStreamMode,
});
assignNewUserToTopic(needs);
test("Mobile Footer dropdown contains reassign buttons", async function (assert) {
updateCurrentUser({ can_assign: true });
await visit("/t/assignment-topic/44");
await click(".topic-footer-mobile-dropdown-trigger");
assert.dom("#topic-footer-button-unassign-mobile").exists();
assert.dom("#topic-footer-button-reassign-self-mobile").exists();
assert.dom("#topic-footer-button-reassign-mobile").exists();
});
}
);
acceptance(
`Discourse Assign | Reassign topic conditionals (glimmer_post_stream_mode = ${postStreamMode})`,
function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
tagging_enabled: true,
assigns_user_url_path: "/",
glimmer_post_stream_mode: postStreamMode,
});
assignCurrentUserToTopic(needs);
test("Reassign Footer dropdown won't display reassign-to-self button when already assigned to current user", async function (assert) {
updateCurrentUser({ can_assign: true });
const menu = selectKit("#topic-footer-dropdown-reassign");
await visit("/t/assignment-topic/44");
await menu.expand();
assert.false(menu.rowByValue("reassign-self").exists());
});
}
);
});