diff --git a/assets/javascripts/discourse/components/topic-level-assign-menu.js b/assets/javascripts/discourse/components/topic-level-assign-menu.js
new file mode 100644
index 0000000..adc1d56
--- /dev/null
+++ b/assets/javascripts/discourse/components/topic-level-assign-menu.js
@@ -0,0 +1,227 @@
+import { getOwner } from "@ember/application";
+import { htmlSafe } from "@ember/template";
+import { renderAvatar } from "discourse/helpers/user-avatar";
+import { iconHTML } from "discourse-common/lib/icon-library";
+import I18n from "I18n";
+
+const DEPENDENT_KEYS = [
+ "topic.assigned_to_user",
+ "topic.assigned_to_group",
+ "currentUser.can_assign",
+ "topic.assigned_to_user.username",
+];
+
+export default {
+ id: "reassign",
+ dependentKeys: DEPENDENT_KEYS,
+ classNames: ["reassign"],
+
+ async action(id) {
+ if (!this.currentUser?.can_assign) {
+ return;
+ }
+
+ const taskActions = getOwner(this).lookup("service:task-actions");
+ const firstPostId = this.topic.postStream.firstPostId;
+
+ switch (id) {
+ case "unassign": {
+ this.set("topic.assigned_to_user", null);
+ this.set("topic.assigned_to_group", null);
+
+ await taskActions.unassign(this.topic.id);
+ this.appEvents.trigger("post-stream:refresh", { id: firstPostId });
+ break;
+ }
+ case "reassign-self": {
+ this.set("topic.assigned_to_user", null);
+ this.set("topic.assigned_to_group", null);
+
+ await taskActions.reassignUserToTopic(this.currentUser, this.topic);
+ this.appEvents.trigger("post-stream:refresh", { id: firstPostId });
+ break;
+ }
+ case "reassign": {
+ await taskActions.showAssignModal(this.topic, {
+ targetType: "Topic",
+ isAssigned: this.topic.isAssigned(),
+ onSuccess: () =>
+ this.appEvents.trigger("post-stream:refresh", { id: firstPostId }),
+ });
+ break;
+ }
+ default: {
+ if (id.startsWith("unassign-from-post-")) {
+ const postId = extractPostId(id);
+ await taskActions.unassign(postId, "Post");
+ delete this.topic.indirectly_assigned_to[postId];
+ this.appEvents.trigger("post-stream:refresh", { id: firstPostId });
+ }
+ }
+ }
+ },
+
+ noneItem() {
+ const topic = this.topic;
+
+ if (topic.assigned_to_user || topic.hasAssignedPosts()) {
+ return unassignUsersButton(topic.uniqueAssignees());
+ } else if (topic.assigned_to_group) {
+ return unassignGroupButton(topic.assigned_to_group);
+ }
+ },
+ content() {
+ const content = [];
+
+ if (this.topic.isAssigned()) {
+ content.push(unassignFromTopicButton(this.topic));
+ }
+
+ if (this.topic.hasAssignedPosts()) {
+ content.push(...unassignFromPostButtons(this.topic));
+ }
+
+ if (this.topic.isAssigned() && !this.topic.isAssignedTo(this.currentUser)) {
+ content.push(reassignToSelfButton());
+ }
+
+ content.push(editAssignmentsButton());
+
+ return content;
+ },
+
+ displayed() {
+ return (
+ this.currentUser?.can_assign &&
+ !this.site.mobileView &&
+ (this.topic.isAssigned() || this.topic.hasAssignedPosts())
+ );
+ },
+};
+
+function unassignGroupButton(group) {
+ const label = I18n.t("discourse_assign.unassign.title");
+ return {
+ id: null,
+ name: I18n.t("discourse_assign.reassign_modal.title"),
+ label: htmlSafe(
+ `${label} @${group.name}...`
+ ),
+ };
+}
+
+function unassignUsersButton(users) {
+ let avatars = "";
+ if (users.length === 1) {
+ avatars = avatarHtml(users[0], "tiny");
+ } else if (users.length > 1) {
+ avatars =
+ avatarHtml(users[0], "tiny", "overlap") + avatarHtml(users[1], "tiny");
+ }
+
+ const label = `${I18n.t(
+ "discourse_assign.topic_level_menu.unassign_with_ellipsis"
+ )}`;
+
+ return {
+ id: null,
+ name: htmlSafe(
+ I18n.t("discourse_assign.topic_level_menu.unassign_with_ellipsis")
+ ),
+ label: htmlSafe(`${avatars}${label}`),
+ };
+}
+
+function avatarHtml(user, size, classes) {
+ return renderAvatar(user, {
+ imageSize: size,
+ extraClasses: classes,
+ ignoreTitle: true,
+ });
+}
+
+function extractPostId(buttonId) {
+ // buttonId format is "unassign-from-post-${postId}"
+ const start = buttonId.lastIndexOf("-") + 1;
+ return buttonId.substring(start);
+}
+
+function editAssignmentsButton() {
+ const icon = iconHTML("pencil-alt");
+ const label = I18n.t("discourse_assign.topic_level_menu.edit_assignments");
+ return {
+ id: "reassign",
+ name: htmlSafe(label),
+ label: htmlSafe(`${icon} ${label}`),
+ };
+}
+
+function reassignToSelfButton() {
+ const icon = iconHTML("user-plus");
+ const label = I18n.t(
+ "discourse_assign.topic_level_menu.reassign_topic_to_me"
+ );
+ return {
+ id: "reassign-self",
+ name: htmlSafe(label),
+ label: htmlSafe(`${icon} ${label}`),
+ };
+}
+
+function unassignFromTopicButton(topic) {
+ const username =
+ topic.assigned_to_user?.username || topic.assigned_to_group?.name;
+ const icon = topic.assigned_to_user
+ ? avatarHtml(topic.assigned_to_user, "small")
+ : iconHTML("user-times");
+ const label = I18n.t(
+ "discourse_assign.topic_level_menu.unassign_from_topic",
+ { username }
+ );
+
+ return {
+ id: "unassign",
+ name: htmlSafe(label),
+ label: htmlSafe(`${icon} ${label}`),
+ };
+}
+
+function unassignFromPostButtons(topic) {
+ if (!topic.hasAssignedPosts()) {
+ return [];
+ }
+
+ const max_buttons = 10;
+ return Object.entries(topic.indirectly_assigned_to)
+ .slice(0, max_buttons)
+ .map(([postId, assignment]) => unassignFromPostButton(postId, assignment));
+}
+
+function unassignFromPostButton(postId, assignment) {
+ let assignee, icon;
+ const assignedToUser = !!assignment.assigned_to.username;
+ if (assignedToUser) {
+ assignee = assignment.assigned_to.username;
+ icon = avatarHtml(assignment.assigned_to, "small");
+ } else {
+ assignee = assignment.assigned_to.name;
+ icon = iconHTML("group-times");
+ }
+
+ const label = I18n.t("discourse_assign.topic_level_menu.unassign_from_post", {
+ assignee,
+ post_number: assignment.post_number,
+ });
+ const dataName = I18n.t(
+ "discourse_assign.topic_level_menu.unassign_from_post_help",
+ {
+ assignee,
+ post_number: assignment.post_number,
+ }
+ );
+ return {
+ id: `unassign-from-post-${postId}`,
+ name: htmlSafe(dataName),
+ label: htmlSafe(`${icon} ${label}`),
+ };
+}
diff --git a/assets/javascripts/discourse/initializers/extend-for-assigns.js b/assets/javascripts/discourse/initializers/extend-for-assigns.js
index 77d6890..dcde7df 100644
--- a/assets/javascripts/discourse/initializers/extend-for-assigns.js
+++ b/assets/javascripts/discourse/initializers/extend-for-assigns.js
@@ -16,6 +16,7 @@ import discourseComputed from "discourse-common/utils/decorators";
import I18n from "I18n";
import BulkAssign from "../components/bulk-actions/assign-user";
import BulkActionsAssignUser from "../components/bulk-actions/bulk-assign-user";
+import TopicLevelAssignMenu from "../components/topic-level-assign-menu";
const PLUGIN_ID = "discourse-assign";
@@ -37,142 +38,52 @@ function defaultTitle(topic) {
}
}
-function includeIsAssignedOnTopic(api) {
+function extendTopicModel(api) {
api.modifyClass("model:topic", {
pluginId: PLUGIN_ID,
+
+ assignees() {
+ const result = [];
+
+ if (this.assigned_to_user) {
+ result.push(this.assigned_to_user);
+ }
+
+ const postAssignees = this.assignedPosts().map((p) => p.assigned_to);
+ result.push(...postAssignees);
+ return result;
+ },
+
+ uniqueAssignees() {
+ const map = new Map();
+ this.assignees().forEach((user) => map.set(user.username, user));
+ return [...map.values()];
+ },
+
+ assignedPosts() {
+ if (!this.indirectly_assigned_to) {
+ return [];
+ }
+
+ return Object.values(this.indirectly_assigned_to);
+ },
+
isAssigned() {
return this.assigned_to_user || this.assigned_to_group;
},
+
+ isAssignedTo(user) {
+ return this.assigned_to_user?.username === user.username;
+ },
+
+ hasAssignedPosts() {
+ return !!this.assignedPosts().length;
+ },
});
}
function registerTopicFooterButtons(api) {
- registerTopicFooterDropdown({
- id: "reassign",
-
- async action(id) {
- if (!this.currentUser?.can_assign) {
- return;
- }
-
- const taskActions = getOwner(this).lookup("service:task-actions");
-
- switch (id) {
- case "unassign": {
- this.set("topic.assigned_to_user", null);
- this.set("topic.assigned_to_group", null);
-
- await taskActions.unassign(this.topic.id);
-
- this.appEvents.trigger("post-stream:refresh", {
- id: this.topic.postStream.firstPostId,
- });
- break;
- }
- case "reassign-self": {
- this.set("topic.assigned_to_user", null);
- this.set("topic.assigned_to_group", null);
-
- await taskActions.reassignUserToTopic(this.currentUser, this.topic);
-
- this.appEvents.trigger("post-stream:refresh", {
- id: this.topic.postStream.firstPostId,
- });
- break;
- }
- case "reassign": {
- await taskActions.showAssignModal(this.topic, {
- targetType: "Topic",
- isAssigned: this.topic.isAssigned(),
- onSuccess: () =>
- this.appEvents.trigger("post-stream:refresh", {
- id: this.topic.postStream.firstPostId,
- }),
- });
- break;
- }
- }
- },
-
- noneItem() {
- const user = this.topic.assigned_to_user;
- const group = this.topic.assigned_to_group;
- const label = I18n.t("discourse_assign.unassign.title_w_ellipsis");
- const groupLabel = I18n.t("discourse_assign.unassign.title");
-
- if (user) {
- return {
- id: null,
- name: I18n.t("discourse_assign.reassign_modal.title"),
- label: htmlSafe(
- `${renderAvatar(user, {
- imageSize: "tiny",
- ignoreTitle: true,
- })}${label}`
- ),
- };
- } else if (group) {
- return {
- id: null,
- name: I18n.t("discourse_assign.reassign_modal.title"),
- label: htmlSafe(
- `${groupLabel} @${group.name}...`
- ),
- };
- }
- },
- dependentKeys: DEPENDENT_KEYS,
- classNames: ["reassign"],
- content() {
- const content = [
- {
- id: "unassign",
- name: I18n.t("discourse_assign.unassign.help", {
- username:
- this.topic.assigned_to_user?.username ||
- this.topic.assigned_to_group?.name,
- }),
- label: htmlSafe(
- `${iconHTML("user-times")} ${I18n.t(
- "discourse_assign.unassign.title"
- )}`
- ),
- },
- ];
- if (
- this.topic.isAssigned() &&
- this.topic.assigned_to_user?.username !== this.currentUser.username
- ) {
- content.push({
- id: "reassign-self",
- name: I18n.t("discourse_assign.reassign.to_self_help"),
- label: htmlSafe(
- `${iconHTML("user-plus")} ${I18n.t(
- "discourse_assign.reassign.to_self"
- )}`
- ),
- });
- }
- content.push({
- id: "reassign",
- name: I18n.t("discourse_assign.reassign.help"),
- label: htmlSafe(
- `${iconHTML("group-plus")} ${I18n.t(
- "discourse_assign.reassign.title_w_ellipsis"
- )}`
- ),
- });
- return content;
- },
-
- displayed() {
- return (
- this.currentUser?.can_assign &&
- !this.site.mobileView &&
- this.topic.isAssigned()
- );
- },
- });
+ registerTopicFooterDropdown(TopicLevelAssignMenu);
api.registerTopicFooterButton({
id: "assign",
@@ -224,7 +135,11 @@ function registerTopicFooterButtons(api) {
classNames: ["assign"],
dependentKeys: DEPENDENT_KEYS,
displayed() {
- return this.currentUser?.can_assign && !this.topic.isAssigned();
+ return (
+ this.currentUser?.can_assign &&
+ !this.topic.isAssigned() &&
+ !this.topic.hasAssignedPosts()
+ );
},
});
@@ -918,7 +833,7 @@ export default {
}
withPluginApi("0.13.0", (api) => {
- includeIsAssignedOnTopic(api);
+ extendTopicModel(api);
initialize(api);
registerTopicFooterButtons(api);
diff --git a/assets/stylesheets/assigns.scss b/assets/stylesheets/assigns.scss
index be0519e..ffd7d4d 100644
--- a/assets/stylesheets/assigns.scss
+++ b/assets/stylesheets/assigns.scss
@@ -174,10 +174,19 @@
width: 1.33em;
height: 1.33em;
}
+ .avatar.overlap {
+ z-index: 1;
+ margin-right: -0.4em;
+ }
+
.d-icon,
i.fa {
color: var(--primary-medium);
}
+ .d-icon {
+ margin-left: 0.165em;
+ margin-right: 0.165em;
+ }
}
// Group assigns sidebar nav
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b079061..8905016 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -57,6 +57,13 @@ en:
help: "Edit assignment details"
reassign_modal:
title: "Reassign Topic"
+ topic_level_menu:
+ edit_assignments: "Edit assignments..."
+ reassign_topic_to_me: "Reassign topic to me"
+ unassign_with_ellipsis: "Unassign..."
+ unassign_from_post: "Unassign @%{assignee} from #%{post_number}"
+ unassign_from_post_help: "Unassign @%{assignee} from post #%{post_number}"
+ unassign_from_topic: "Unassign @%{username} from topic"
assign_modal:
title: "Assign Topic"
reassign_title: "Reassign Topic"
diff --git a/test/javascripts/acceptance/post-popup-menu-test.js b/test/javascripts/acceptance/post-popup-menu-test.js
index 10e7f60..4dd6637 100644
--- a/test/javascripts/acceptance/post-popup-menu-test.js
+++ b/test/javascripts/acceptance/post-popup-menu-test.js
@@ -1,33 +1,14 @@
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";
+import topicWithAssignedPost from "../fixtures/topic-with-assigned-post";
-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",
@@ -42,7 +23,7 @@ const selectors = {
},
};
-const topic = topicWithAssignedPostResponse();
+const topic = topicWithAssignedPost();
const post = topic.post_stream.posts[1];
acceptance("Discourse Assign | Post popup menu", function (needs) {
@@ -111,7 +92,7 @@ acceptance("Discourse Assign | Post popup menu", function (needs) {
},
});
- // todo: we can skip this one for now, I can fix it in a core PR
+ // todo: we can skip this one for now, It will be fixed it in a core PR
// assert.dom(".popup-menu").doesNotExist("The popup menu is closed");
assert
diff --git a/test/javascripts/acceptance/topic-level-assign-menu-test.js b/test/javascripts/acceptance/topic-level-assign-menu-test.js
new file mode 100644
index 0000000..d57ca3d
--- /dev/null
+++ b/test/javascripts/acceptance/topic-level-assign-menu-test.js
@@ -0,0 +1,49 @@
+import { click, visit } from "@ember/test-helpers";
+import { test } from "qunit";
+import {
+ acceptance,
+ publishToMessageBus,
+ updateCurrentUser,
+} from "discourse/tests/helpers/qunit-helpers";
+import topicWithAssignedPost from "../fixtures/topic-with-assigned-post";
+
+const topic = topicWithAssignedPost();
+const post = topic.post_stream.posts[1];
+
+acceptance("Discourse Assign | Topic level assign 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/unassign", () => {
+ return helper.response({ success: true });
+ });
+ });
+
+ needs.hooks.beforeEach(() => {
+ updateCurrentUser({ can_assign: true });
+ });
+
+ test("Unassign button unassigns the post", async function (assert) {
+ await visit("/t/assignment-topic/44");
+
+ await click("#topic-footer-dropdown-reassign .btn");
+ await click(`li[data-value='unassign-from-post-${post.id}']`);
+ await publishToMessageBus("/staff/topic-assignment", {
+ type: "unassigned",
+ topic_id: topic.id,
+ post_id: post.id,
+ assigned_type: "User",
+ });
+
+ assert
+ .dom("#topic-footer-dropdown-reassign-body ul[role='menu']")
+ .doesNotExist("The menu is closed");
+ assert
+ .dom(".post-stream article#post_2 .assigned-to")
+ .doesNotExist("The post is unassigned");
+ });
+});
diff --git a/test/javascripts/fixtures/topic-with-assigned-post.js b/test/javascripts/fixtures/topic-with-assigned-post.js
new file mode 100644
index 0000000..278585a
--- /dev/null
+++ b/test/javascripts/fixtures/topic-with-assigned-post.js
@@ -0,0 +1,20 @@
+import topicFixtures from "discourse/tests/fixtures/topic";
+import { cloneJSON } from "discourse-common/lib/object";
+
+export default function topicWithAssignedPost() {
+ const username = "eviltrout";
+ 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;
+}