FEATURE: Add menu to assigned text on posts (#550)

This adds a popup menu to assigned posts that makes it easier to unassign or 
reassign them.
This commit is contained in:
Andrei Prigorshnev 2024-03-15 14:30:11 +00:00 committed by GitHub
parent a1686e1ca9
commit b09a6618fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 221 additions and 26 deletions

View File

@ -0,0 +1,64 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
import DMenu from "float-kit/components/d-menu";
export default class AssignedToPost extends Component {
@service taskActions;
@action
unassign() {
this.taskActions.unassignPost(this.args.post);
}
@action
editAssignment() {
this.taskActions.showAssignPostModal(this.args.post);
}
<template>
{{#if @assignedToUser}}
{{icon "user-plus"}}
{{else}}
{{icon "group-plus"}}
{{/if}}
<span class="assign-text">
{{i18n "discourse_assign.assigned_to"}}
</span>
<a href={{@href}} class="assigned-to-username">
{{#if @assignedToUser}}
{{@assignedToUser.username}}
{{else}}
{{@assignedToGroup.name}}
{{/if}}
</a>
<DMenu @icon="ellipsis-h" class="btn-flat more-button">
<div class="popup-menu">
<ul>
<li>
<DButton
@action={{this.unassign}}
@icon="user-plus"
@label="discourse_assign.unassign.title"
class="popup-menu-btn"
/>
</li>
<li>
<DButton
@action={{this.editAssignment}}
@icon="group-plus"
@label="discourse_assign.reassign.title_w_ellipsis"
class="popup-menu-btn"
/>
</li>
</ul>
</div>
</DMenu>
</template>
}

View File

@ -1,6 +1,7 @@
import { getOwner } from "@ember/application"; import { getOwner } from "@ember/application";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { hbs } from "ember-cli-htmlbars";
import { h } from "virtual-dom"; import { h } from "virtual-dom";
import SearchAdvancedOptions from "discourse/components/search-advanced-options"; import SearchAdvancedOptions from "discourse/components/search-advanced-options";
import { renderAvatar } from "discourse/helpers/user-avatar"; import { renderAvatar } from "discourse/helpers/user-avatar";
@ -8,6 +9,7 @@ import { withPluginApi } from "discourse/lib/plugin-api";
import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer-dropdown"; import { registerTopicFooterDropdown } from "discourse/lib/register-topic-footer-dropdown";
import { escapeExpression } from "discourse/lib/utilities"; import { escapeExpression } from "discourse/lib/utilities";
import RawHtml from "discourse/widgets/raw-html"; import RawHtml from "discourse/widgets/raw-html";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import { iconHTML, iconNode } from "discourse-common/lib/icon-library"; import { iconHTML, iconNode } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
@ -646,19 +648,19 @@ function initialize(api) {
} }
}); });
api.createWidget("assigned-to", { api.createWidget("assigned-to-post", {
html(attrs) { html(attrs) {
let { assignedToUser, assignedToGroup, href } = attrs; return new RenderGlimmer(
this,
return h("p.assigned-to", [ "p.assigned-to",
assignedToUser ? iconNode("user-plus") : iconNode("group-plus"), hbs`<AssignedToPost @assignedToUser={{@data.assignedToUser}} @assignedToGroup={{@data.assignedToGroup}} @href={{@data.href}} @post={{@data.post}} />`,
h("span.assign-text", I18n.t("discourse_assign.assigned_to")), {
h( assignedToUser: attrs.post.assigned_to_user,
"a", assignedToGroup: attrs.post.assigned_to_group,
{ attributes: { class: "assigned-to-username", href } }, href: attrs.href,
assignedToUser ? assignedToUser.username : assignedToGroup.name post: attrs.post,
), }
]); );
}, },
}); });
@ -792,6 +794,7 @@ function initialize(api) {
if (data.type === "unassigned") { if (data.type === "unassigned") {
delete topic.indirectly_assigned_to[data.post_number]; delete topic.indirectly_assigned_to[data.post_number];
} }
this.appEvents.trigger("post-stream:refresh", { this.appEvents.trigger("post-stream:refresh", {
id: topic.postStream.posts[0].id, id: topic.postStream.posts[0].id,
}); });
@ -841,11 +844,13 @@ function initialize(api) {
? assignedToUserPath(assignedToUser) ? assignedToUserPath(assignedToUser)
: assignedToGroupPath(assignedToGroup); : assignedToGroupPath(assignedToGroup);
} }
if (href) { if (href) {
return dec.widget.attach("assigned-to", { return dec.widget.attach("assigned-to-post", {
assignedToUser, assignedToUser,
assignedToGroup, assignedToGroup,
href, href,
post: postModel,
}); });
} }
} }

View File

@ -45,6 +45,11 @@ export default class TaskActions extends Service {
}); });
} }
async unassignPost(post) {
await this.unassign(post.id, "Post");
delete post.topic.indirectly_assigned_to[post.id];
}
showAssignModal( showAssignModal(
target, target,
{ isAssigned = false, targetType = "Topic", onSuccess } { isAssigned = false, targetType = "Topic", onSuccess }
@ -62,6 +67,10 @@ export default class TaskActions extends Service {
}); });
} }
showAssignPostModal(post) {
return this.showAssignModal(post, { targetType: "Post" });
}
reassignUserToTopic(user, target, targetType = "Topic") { reassignUserToTopic(user, target, targetType = "Topic") {
return ajax("/assign/assign", { return ajax("/assign/assign", {
type: "PUT", type: "PUT",

View File

@ -26,6 +26,12 @@
.assignee:not(:last-child):after { .assignee:not(:last-child):after {
content: ", "; content: ", ";
} }
.more-button {
padding-left: 0.3em;
padding-right: 0.3em;
vertical-align: middle;
}
} }
.topic-body { .topic-body {
@ -44,19 +50,6 @@
align-items: center; align-items: center;
} }
.assigned-to-user {
display: flex;
align-items: center;
img.avatar {
margin-right: 0.3em;
}
.unassign {
margin-left: 0.5em;
}
}
.topic-assigned-to { .topic-assigned-to {
min-width: 15%; min-width: 15%;
width: 15%; width: 15%;

View File

@ -0,0 +1,124 @@
import { click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit";
import topicFixtures from "discourse/tests/fixtures/topic";
import {
acceptance,
publishToMessageBus,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import { cloneJSON } from "discourse-common/lib/object";
const username = "eviltrout";
const new_assignee_username = "new_assignee";
function topicWithAssignedPostResponse() {
const topic = cloneJSON(topicFixtures["/t/28830/1.json"]);
const secondPost = topic.post_stream.posts[1];
topic["indirectly_assigned_to"] = {
[secondPost.id]: {
assigned_to: {
username,
},
post_number: 1,
},
};
secondPost["assigned_to_user"] = { username };
return topic;
}
const selectors = {
assignedTo: ".post-stream article#post_2 .assigned-to",
moreButton: ".post-stream .topic-post .more-button",
popupMenu: {
unassign: ".popup-menu .popup-menu-btn svg.d-icon-user-plus",
editAssignment: ".popup-menu .popup-menu-btn svg.d-icon-group-plus",
},
modal: {
assignee: ".modal-container .select-kit-header-wrapper",
assigneeInput: ".modal-container .filter-input",
assignButton: ".d-modal__footer .btn-primary",
},
};
const topic = topicWithAssignedPostResponse();
const post = topic.post_stream.posts[1];
acceptance("Discourse Assign | Post popup menu", function (needs) {
needs.user();
needs.settings({
assign_enabled: true,
});
needs.pretender((server, helper) => {
server.get("/t/44.json", () => helper.response(topic));
server.put("/assign/assign", () => {
return helper.response({ success: true });
});
server.put("/assign/unassign", () => {
return helper.response({ success: true });
});
server.get("/assign/suggestions", () =>
helper.response({
assign_allowed_for_groups: [],
suggestions: [{ username: new_assignee_username }],
})
);
server.get("/u/search/users", () =>
helper.response({ users: [{ username: new_assignee_username }] })
);
});
needs.hooks.beforeEach(() => {
updateCurrentUser({ can_assign: true });
});
test("Unassigns the post", async function (assert) {
await visit("/t/assignment-topic/44");
await click(selectors.moreButton);
await click(selectors.popupMenu.unassign);
await publishToMessageBus("/staff/topic-assignment", {
type: "unassigned",
topic_id: topic.id,
post_id: post.id,
assigned_type: "User",
});
assert.dom(".popup-menu").doesNotExist("The popup menu is closed");
assert.dom(selectors.assignedTo).doesNotExist("The post is unassigned");
});
test("Reassigns the post", async function (assert) {
await visit("/t/assignment-topic/44");
await click(selectors.moreButton);
await click(selectors.popupMenu.editAssignment);
await click(selectors.modal.assignee);
await fillIn(selectors.modal.assigneeInput, new_assignee_username);
await click(selectors.modal.assignButton);
await publishToMessageBus("/staff/topic-assignment", {
type: "assigned",
topic_id: topic.id,
post_id: post.id,
assigned_type: "User",
assigned_to: {
username: new_assignee_username,
},
});
// todo: we can skip this one for now, I can fix it in a core PR
// assert.dom(".popup-menu").doesNotExist("The popup menu is closed");
assert
.dom(`${selectors.assignedTo} .assigned-to-username`)
.hasText(
new_assignee_username,
"The post is assigned to the new assignee"
);
});
});