diff --git a/.discourse-compatibility b/.discourse-compatibility index a351913..a0490f3 100644 --- a/.discourse-compatibility +++ b/.discourse-compatibility @@ -1,3 +1,4 @@ +2.9.0.beta8: 28bc8ab78a09551548c87f511ade3d64e1b04bc3 2.9.0.beta3: 46f200935dc9e5750c3f2740abd993e27a9b3f6c 2.8.2: 7bec9aeaf786defc9a133b8cb9ed24f1c2522400 2.8.0.beta8: f901c5fe97c272c884c1e6cfdad1ad1cbe0b36be diff --git a/app/controllers/discourse_assign/assign_controller.rb b/app/controllers/discourse_assign/assign_controller.rb index 319390f..41b13e3 100644 --- a/app/controllers/discourse_assign/assign_controller.rb +++ b/app/controllers/discourse_assign/assign_controller.rb @@ -174,6 +174,49 @@ module DiscourseAssign } end + def user_menu_assigns + assign_notifications = Notification.unread_type( + current_user, + Notification.types[:assigned], + user_menu_limit + ) + + if assign_notifications.size < user_menu_limit + opts = {} + ignored_assignment_ids = assign_notifications.filter_map do |notification| + notification.data_hash[:assignment_id] + end + opts[:ignored_assignment_ids] = ignored_assignment_ids if ignored_assignment_ids.present? + + assigns_list = TopicQuery.new( + current_user, + per_page: user_menu_limit - assign_notifications.size + ).list_messages_assigned(current_user, ignored_assignment_ids: ignored_assignment_ids) + end + + if assign_notifications.present? + serialized_notifications = ActiveModel::ArraySerializer.new( + assign_notifications, + each_serializer: NotificationSerializer, + scope: guardian + ) + end + + if assigns_list + serialized_assigns = serialize_data( + assigns_list, + TopicListSerializer, + scope: guardian, + root: false + )[:topics] + end + + render json: { + notifications: serialized_notifications || [], + topics: serialized_assigns || [], + } + end + private def translate_failure(reason, assign_to) @@ -238,5 +281,9 @@ module DiscourseAssign def ensure_assign_allowed raise Discourse::InvalidAccess.new unless current_user.can_assign? end + + def user_menu_limit + UsersController::USER_MENU_LIST_LIMIT + end end end diff --git a/assets/javascripts/discourse-assign/lib/user-menu/assign-item.js b/assets/javascripts/discourse-assign/lib/user-menu/assign-item.js new file mode 100644 index 0000000..8440e82 --- /dev/null +++ b/assets/javascripts/discourse-assign/lib/user-menu/assign-item.js @@ -0,0 +1,59 @@ +import UserMenuBaseItem from "discourse/lib/user-menu/base-item"; +import { postUrl } from "discourse/lib/utilities"; +import { htmlSafe } from "@ember/template"; +import { emojiUnescape } from "discourse/lib/text"; +import I18n from "I18n"; + +const ICON = "user-plus"; +const GROUP_ICON = "group-plus"; + +export default class UserMenuAssignItem extends UserMenuBaseItem { + constructor({ assign }) { + super(...arguments); + this.assign = assign; + } + + get className() { + return "assign"; + } + + get linkHref() { + return postUrl( + this.assign.slug, + this.assign.id, + (this.assign.last_read_post_number || 0) + 1 + ); + } + + get linkTitle() { + if (this.assign.assigned_to_group) { + return I18n.t("user.assigned_to_group", { + group_name: + this.assign.assigned_to_group.full_name || + this.assign.assigned_to_group.name, + }); + } else { + return I18n.t("user.assigned_to_you"); + } + } + + get icon() { + if (this.assign.assigned_to_group) { + return GROUP_ICON; + } else { + return ICON; + } + } + + get label() { + return null; + } + + get description() { + return htmlSafe(emojiUnescape(this.assign.fancy_title)); + } + + get topicId() { + return this.assign.id; + } +} diff --git a/assets/javascripts/discourse/components/user-menu/assigns-list-empty-state.js b/assets/javascripts/discourse/components/user-menu/assigns-list-empty-state.js new file mode 100644 index 0000000..8a402e0 --- /dev/null +++ b/assets/javascripts/discourse/components/user-menu/assigns-list-empty-state.js @@ -0,0 +1,3 @@ +import templateOnly from "@ember/component/template-only"; +// TODO: colocate this component's template +export default templateOnly(); diff --git a/assets/javascripts/discourse/components/user-menu/assigns-list.js b/assets/javascripts/discourse/components/user-menu/assigns-list.js new file mode 100644 index 0000000..5868164 --- /dev/null +++ b/assets/javascripts/discourse/components/user-menu/assigns-list.js @@ -0,0 +1,78 @@ +import UserMenuNotificationsList from "discourse/components/user-menu/notifications-list"; +import { ajax } from "discourse/lib/ajax"; +import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item"; +import UserMenuAssignItem from "discourse/plugins/discourse-assign/discourse-assign/lib/user-menu/assign-item"; +import Notification from "discourse/models/notification"; +import I18n from "I18n"; +import showModal from "discourse/lib/show-modal"; + +export default class UserMenuAssignNotificationsList extends UserMenuNotificationsList { + get dismissTypes() { + return ["assigned"]; + } + + get showDismiss() { + return this._unreadAssignedNotificationsCount > 0; + } + + get dismissTitle() { + return I18n.t("user.dismiss_assigned_tooltip"); + } + + get showAllHref() { + return `${this.currentUser.path}/activity/assigned`; + } + + get showAllTitle() { + return I18n.t("user_menu.view_all_assigned"); + } + + get itemsCacheKey() { + return "user-menu-assigns-tab"; + } + + get emptyStateComponent() { + return "user-menu/assigns-list-empty-state"; + } + + fetchItems() { + return ajax("/assign/user-menu-assigns.json").then((data) => { + const content = []; + data.notifications.forEach((rawNotification) => { + const notification = Notification.create(rawNotification); + content.push( + new UserMenuNotificationItem({ + notification, + currentUser: this.currentUser, + siteSettings: this.siteSettings, + site: this.site, + }) + ); + }); + content.push( + ...data.topics.map((assign) => new UserMenuAssignItem({ assign })) + ); + return content; + }); + } + + dismissWarningModal() { + const modalController = showModal("dismiss-notification-confirmation"); + modalController.set( + "confirmationMessage", + I18n.t("notifications.dismiss_confirmation.body.assigns", { + count: this._unreadAssignedNotificationsCount, + }) + ); + return modalController; + } + + get _unreadAssignedNotificationsCount() { + const key = `grouped_unread_high_priority_notifications.${this.site.notification_types.assigned}`; + // we're retrieving the value with get() so that Ember tracks the property + // and re-renders the UI when it changes. + // we can stop using `get()` when the User model is refactored into native + // class with @tracked properties. + return this.currentUser.get(key) || 0; + } +} diff --git a/assets/javascripts/discourse/initializers/assign-user-menu.js b/assets/javascripts/discourse/initializers/assign-user-menu.js new file mode 100644 index 0000000..6352222 --- /dev/null +++ b/assets/javascripts/discourse/initializers/assign-user-menu.js @@ -0,0 +1,40 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; + +export default { + name: "assign-user-menu", + + initialize(container) { + withPluginApi("1.2.0", (api) => { + if (api.registerUserMenuTab) { + const siteSettings = container.lookup("service:site-settings"); + if (!siteSettings.assign_enabled) { + return; + } + + const currentUser = api.getCurrentUser(); + if (!currentUser?.can_assign) { + return; + } + api.registerUserMenuTab((UserMenuTab) => { + return class extends UserMenuTab { + get id() { + return "assign-list"; + } + + get panelComponent() { + return "user-menu/assigns-list"; + } + + get icon() { + return "user-plus"; + } + + get count() { + return this.getUnreadCountForType("assigned"); + } + }; + }); + } + }); + }, +}; diff --git a/assets/javascripts/discourse/templates/components/user-menu/assigns-list-empty-state.hbs b/assets/javascripts/discourse/templates/components/user-menu/assigns-list-empty-state.hbs new file mode 100644 index 0000000..a8b92a6 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/user-menu/assigns-list-empty-state.hbs @@ -0,0 +1,14 @@ +
+ + {{i18n "user.no_assignments_title"}} + +
+

+ {{html-safe (i18n + "user.no_assignments_body" + icon=(d-icon "user-plus") + preferencesUrl=(get-url "/my/preferences/notifications") + )}} +

+
+
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b0c45bd..cd30f66 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -93,6 +93,9 @@ en: Your assigned topics and messages will be listed here. You will also receive a periodic reminder notification of your assignments, which you can adjust in your user preferences.

To assign a topic or message to yourself or to someone else, look for the %{icon} assign button at the bottom. + dismiss_assigned_tooltip: "Mark all unread assign notifications as read" + assigned_to_group: "assigned to %{group_name}" + assigned_to_you: "assigned to you" admin: web_hooks: assign_event: @@ -138,3 +141,10 @@ en: assigned: "%{username} assigned you" titles: assigned: "Assigned" + dismiss_confirmation: + body: + assigns: + one: "Are you sure? You have %{count} unread assign notification." + other: "Are you sure? You have %{count} unread assign notifications." + user_menu: + view_all_assigned: "view all assigned" diff --git a/config/routes.rb b/config/routes.rb index 02ddb36..a5fd25e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,4 +7,5 @@ DiscourseAssign::Engine.routes.draw do get "/suggestions" => "assign#suggestions" get "/assigned" => "assign#assigned" get "/members/:group_name" => "assign#group_members" + get "/user-menu-assigns" => "assign#user_menu_assigns" end diff --git a/plugin.rb b/plugin.rb index 34a72b3..cf481fa 100644 --- a/plugin.rb +++ b/plugin.rb @@ -380,23 +380,30 @@ after_initialize do next results end - add_to_class(:topic_query, :list_messages_assigned) do |user| + add_to_class(:topic_query, :list_messages_assigned) do |user, ignored_assignment_ids: nil| list = default_results(include_pms: true) + where_clause = +"(" + where_clause << "(assigned_to_id = :user_id AND assigned_to_type = 'User' AND active)" + if @options[:filter] != :direct + where_clause << "OR (assigned_to_id IN (group_users.group_id) AND assigned_to_type = 'Group' AND active)" + end + where_clause << ")" + + if ignored_assignment_ids.present? + where_clause << "AND assignments.id NOT IN (:ignored_assignment_ids)" + end topic_ids_sql = +<<~SQL SELECT topic_id FROM assignments LEFT JOIN group_users ON group_users.user_id = :user_id - WHERE - (assigned_to_id = :user_id AND assigned_to_type = 'User' AND active) + WHERE #{where_clause} SQL - topic_ids_sql << <<~SQL if @options[:filter] != :direct - OR (assigned_to_id IN (group_users.group_id) AND assigned_to_type = 'Group' AND active) - SQL - - sql = "topics.id IN (#{topic_ids_sql})" - - list = list.where(sql, user_id: user.id).includes(:allowed_users) + where_args = { user_id: user.id } + if ignored_assignment_ids.present? + where_args[:ignored_assignment_ids] = ignored_assignment_ids + end + list = list.where("topics.id IN (#{topic_ids_sql})", **where_args).includes(:allowed_users) create_list(:assigned, { unordered: true }, list) end diff --git a/spec/requests/assign_controller_spec.rb b/spec/requests/assign_controller_spec.rb index 388a968..74556ee 100644 --- a/spec/requests/assign_controller_spec.rb +++ b/spec/requests/assign_controller_spec.rb @@ -392,4 +392,133 @@ RSpec.describe DiscourseAssign::AssignController do expect(response.status).to eq(200) end end + + describe "#user_menu_assigns" do + fab!(:unread_assigned_topic) { Fabricate(:post).topic } + fab!(:read_assigned_topic) { Fabricate(:post).topic } + + fab!(:unread_assigned_post) { Fabricate(:post, topic: Fabricate(:post).topic) } + fab!(:read_assigned_post) { Fabricate(:post, topic: Fabricate(:post).topic) } + + fab!(:read_assigned_post_in_same_topic) { Fabricate(:post, topic: Fabricate(:post).topic) } + fab!(:unread_assigned_post_in_same_topic) { Fabricate(:post, topic: read_assigned_post_in_same_topic.topic) } + + fab!(:another_user_unread_assigned_topic) { Fabricate(:post).topic } + fab!(:another_user_read_assigned_topic) { Fabricate(:post).topic } + + before do + Jobs.run_immediately! + + [ + unread_assigned_topic, + read_assigned_topic, + unread_assigned_post, + read_assigned_post, + unread_assigned_post_in_same_topic, + read_assigned_post_in_same_topic, + ].each do |target| + Assigner.new(target, normal_admin).assign(user) + end + + Notification + .where( + notification_type: Notification.types[:assigned], + read: false, + user_id: user.id, + topic_id: [ + read_assigned_topic.id, + read_assigned_post.topic.id, + read_assigned_post_in_same_topic.topic.id + ] + ) + .where.not( + topic_id: read_assigned_post_in_same_topic.topic.id, + post_number: unread_assigned_post_in_same_topic.post_number + ) + .update_all(read: true) + + Assigner.new(another_user_read_assigned_topic, normal_admin).assign(user2) + Assigner.new(another_user_unread_assigned_topic, normal_admin).assign(user2) + Notification + .where( + notification_type: Notification.types[:assigned], + read: false, + user_id: user2.id, + topic_id: another_user_read_assigned_topic, + ) + .update_all(read: true) + end + + context "when logged out" do + it "responds with 403" do + get "/assign/user-menu-assigns.json" + expect(response.status).to eq(403) + end + end + + context "when logged in" do + before do + sign_in(user) + end + + it "responds with 403 if the current user can't assign" do + user.update!(admin: false) + user.group_users.where(group_id: default_allowed_group.id).destroy_all + get "/assign/user-menu-assigns.json" + expect(response.status).to eq(403) + end + + it "responds with 403 if the assign_enabled setting is disabled" do + SiteSetting.assign_enabled = false + get "/assign/user-menu-assigns.json" + expect(response.status).to eq(403) + end + + it "sends an array of unread assigned notifications" do + get "/assign/user-menu-assigns.json" + expect(response.status).to eq(200) + + notifications = response.parsed_body["notifications"] + expect(notifications.map { |n| [n["topic_id"], n["post_number"]] }).to eq([ + [unread_assigned_topic.id, 1], + [unread_assigned_post.topic.id, unread_assigned_post.post_number], + [unread_assigned_post_in_same_topic.topic.id, unread_assigned_post_in_same_topic.post_number] + ]) + end + + it "responds with an array of assigned topics that are not associated with any of the unread assigned notifications" do + get "/assign/user-menu-assigns.json" + expect(response.status).to eq(200) + + topics = response.parsed_body["topics"] + expect(topics.map { |t| t["id"] }).to eq([ + read_assigned_post_in_same_topic.topic.id, + read_assigned_post.topic.id, + read_assigned_topic.id, + ]) + end + + it "fills up the remaining of the UsersController::USER_MENU_LIST_LIMIT limit with assigned topics" do + stub_const(UsersController, "USER_MENU_LIST_LIMIT", 3) do + get "/assign/user-menu-assigns.json" + end + expect(response.status).to eq(200) + + notifications = response.parsed_body["notifications"] + expect(notifications.size).to eq(3) + topics = response.parsed_body["topics"] + expect(topics.size).to eq(0) + + stub_const(UsersController, "USER_MENU_LIST_LIMIT", 4) do + get "/assign/user-menu-assigns.json" + end + expect(response.status).to eq(200) + + notifications = response.parsed_body["notifications"] + expect(notifications.size).to eq(3) + topics = response.parsed_body["topics"] + expect(topics.size).to eq(1) + end + end + end end diff --git a/test/javascripts/acceptance/assigns-tab-user-menu-test.js b/test/javascripts/acceptance/assigns-tab-user-menu-test.js new file mode 100644 index 0000000..8ef8a37 --- /dev/null +++ b/test/javascripts/acceptance/assigns-tab-user-menu-test.js @@ -0,0 +1,451 @@ +import { + acceptance, + exists, + query, + queryAll, + updateCurrentUser, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import I18n from "I18n"; + +const USER_MENU_ASSIGN_RESPONSE = { + notifications: [ + { + id: 1716, + user_id: 1, + notification_type: 34, + read: false, + high_priority: true, + created_at: "2022-08-11T21:32:32.404Z", + post_number: 1, + topic_id: 227, + fancy_title: "Test poll topic please bear with me", + slug: "test-poll-topic-please-bear-with-me", + data: { + message: "discourse_assign.assign_notification", + display_username: "tony", + topic_title: "Test poll topic please bear with me", + assignment_id: 2, + }, + }, + ], + topics: [ + { + id: 209, + title: "Howdy this a test topic!", + fancy_title: "Howdy this my test topic with emoji :heart:!", + slug: "howdy-this-a-test-topic", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2022-03-10T20:09:25.772Z", + last_posted_at: "2022-03-10T20:09:25.959Z", + bumped: true, + bumped_at: "2022-03-10T20:09:25.959Z", + archetype: "regular", + unseen: false, + last_read_post_number: 2, + unread: 0, + new_posts: 0, + unread_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + thumbnails: null, + tags: [], + tags_descriptions: {}, + views: 11, + like_count: 7, + has_summary: false, + last_poster_username: "osama", + category_id: 1, + pinned_globally: false, + featured_link: null, + assigned_to_user: { + id: 1, + username: "osama", + name: "Osama.OG", + avatar_template: "/letter_avatar_proxy/v4/letter/o/f05b48/{size}.png", + assign_icon: "user-plus", + assign_path: "/u/osama/activity/assigned", + }, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 1, + primary_group_id: 45, + flair_group_id: 45, + }, + ], + }, + { + id: 173, + title: "Owners elegance entrance startled spirits losing", + fancy_title: + "Owners elegance entrance :car: startled spirits losing", + slug: "owners-elegance-entrance-startled-spirits-losing", + posts_count: 7, + reply_count: 0, + highest_post_number: 7, + image_url: null, + created_at: "2021-07-11T04:50:17.029Z", + last_posted_at: "2021-12-24T17:21:03.418Z", + bumped: true, + bumped_at: "2021-12-24T17:21:03.418Z", + archetype: "regular", + unseen: false, + last_read_post_number: 3, + unread: 0, + new_posts: 0, + unread_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 1, + bookmarked: false, + liked: false, + thumbnails: null, + tags: ["music", "job-application"], + tags_descriptions: {}, + views: 23, + like_count: 24, + has_summary: false, + last_poster_username: "ambrose.bradtke", + category_id: 1, + pinned_globally: false, + featured_link: null, + assigned_to_group: { + id: 45, + automatic: false, + name: "Team", + user_count: 4, + mentionable_level: 99, + messageable_level: 99, + visibility_level: 0, + primary_group: true, + title: "", + grant_trust_level: null, + incoming_email: null, + has_messages: true, + flair_url: null, + flair_bg_color: "", + flair_color: "", + bio_raw: "", + bio_cooked: null, + bio_excerpt: null, + public_admission: true, + public_exit: true, + allow_membership_requests: false, + full_name: "", + default_notification_level: 3, + membership_request_template: "", + members_visibility_level: 0, + can_see_members: true, + can_admin_group: true, + publish_read_state: true, + assign_icon: "group-plus", + assign_path: "/g/Team/assigned/everyone", + }, + posters: [ + { + extras: null, + description: "Original Poster", + user_id: 26, + primary_group_id: null, + flair_group_id: null, + }, + { + extras: null, + description: "Frequent Poster", + user_id: 16, + primary_group_id: null, + flair_group_id: null, + }, + { + extras: null, + description: "Frequent Poster", + user_id: 22, + primary_group_id: null, + flair_group_id: null, + }, + { + extras: null, + description: "Frequent Poster", + user_id: 12, + primary_group_id: null, + flair_group_id: null, + }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 13, + primary_group_id: null, + flair_group_id: null, + }, + ], + }, + ], +}; + +acceptance( + "Discourse Assign | experimental user menu | user cannot assign", + function (needs) { + needs.user({ redesigned_user_menu_enabled: true, can_assign: false }); + needs.settings({ + assign_enabled: true, + }); + + test("the assigns tab is not shown", async function (assert) { + await visit("/"); + await click(".d-header-icons .current-user"); + assert.notOk(exists("#user-menu-button-assign-list")); + }); + } +); + +acceptance( + "Discourse Assign | experimental user menu | assign_enabled setting is disabled", + function (needs) { + needs.user({ redesigned_user_menu_enabled: true, can_assign: true }); + needs.settings({ + assign_enabled: false, + }); + + test("the assigns tab is not shown", async function (assert) { + await visit("/"); + await click(".d-header-icons .current-user"); + assert.notOk(exists("#user-menu-button-assign-list")); + }); + } +); + +acceptance("Discourse Assign | experimental user menu", function (needs) { + needs.user({ + redesigned_user_menu_enabled: true, + can_assign: true, + grouped_unread_high_priority_notifications: { + 34: 173, // assigned notification type + }, + }); + + needs.settings({ + assign_enabled: true, + }); + + let forceEmptyState = false; + let markRead = false; + let requestBody; + + needs.pretender((server, helper) => { + server.get("/assign/user-menu-assigns.json", () => { + if (forceEmptyState) { + return helper.response({ notifications: [], topics: [] }); + } else { + return helper.response(USER_MENU_ASSIGN_RESPONSE); + } + }); + + server.put("/notifications/mark-read", (request) => { + requestBody = request.requestBody; + markRead = true; + return helper.response({ success: true }); + }); + }); + + needs.hooks.afterEach(() => { + forceEmptyState = false; + markRead = false; + requestBody = null; + }); + + test("assigns tab", async function (assert) { + await visit("/"); + await click(".d-header-icons .current-user"); + assert.ok(exists("#user-menu-button-assign-list"), "assigns tab exists"); + assert.ok( + exists("#user-menu-button-assign-list .d-icon-user-plus"), + "assigns tab has the user-plus icon" + ); + assert.strictEqual( + query( + "#user-menu-button-assign-list .badge-notification" + ).textContent.trim(), + "173", + "assigns tab has a count badge" + ); + + updateCurrentUser({ + grouped_unread_high_priority_notifications: {}, + }); + + assert.notOk( + exists("#user-menu-button-assign-list .badge-notification"), + "badge count disappears when it goes to zero" + ); + assert.ok( + exists("#user-menu-button-assign-list"), + "assigns tab still exists" + ); + }); + + test("displays unread assign notifications on top and fills the remaining space with read assigns", async function (assert) { + await visit("/"); + await click(".d-header-icons .current-user"); + await click("#user-menu-button-assign-list"); + + const notifications = queryAll("#quick-access-assign-list .notification"); + assert.strictEqual( + notifications.length, + 1, + "there is one unread notification" + ); + assert.ok( + notifications[0].classList.contains("unread"), + "the notification is unread" + ); + assert.ok( + notifications[0].classList.contains("assigned"), + "the notification is of type assigned" + ); + + const assigns = queryAll("#quick-access-assign-list .assign"); + assert.strictEqual(assigns.length, 2, "there are 2 assigns"); + + const userAssign = assigns[0]; + const groupAssign = assigns[1]; + assert.ok( + userAssign.querySelector(".d-icon-user-plus"), + "user assign has the right icon" + ); + assert.ok( + groupAssign.querySelector(".d-icon-group-plus"), + "group assign has the right icon" + ); + + assert.ok( + userAssign + .querySelector("a") + .href.endsWith("/t/howdy-this-a-test-topic/209/3"), + "user assign links to the first unread post (last read post + 1)" + ); + assert.ok( + groupAssign + .querySelector("a") + .href.endsWith( + "/t/owners-elegance-entrance-startled-spirits-losing/173/4" + ), + "group assign links to the first unread post (last read post + 1)" + ); + + assert.strictEqual( + userAssign.textContent.trim(), + "Howdy this my test topic with emoji !", + "user assign contains the topic title" + ); + assert.ok( + userAssign.querySelector(".item-description img.emoji"), + "emojis are rendered in user assign" + ); + assert.ok( + userAssign.querySelector(".item-description b").textContent.trim(), + "my test topic", + "user assign topic title is trusted" + ); + + assert.strictEqual( + groupAssign.textContent.trim().replaceAll(/\s+/g, " "), + "Owners elegance entrance startled spirits losing", + "group assign contains the topic title" + ); + assert.ok( + groupAssign.querySelector(".item-description i img.emoji"), + "emojis are rendered in group assign" + ); + assert.strictEqual( + groupAssign + .querySelector(".item-description i") + .textContent.trim() + .replaceAll(/\s+/g, " "), + "elegance entrance startled", + "group assign topic title is trusted" + ); + + assert.strictEqual( + userAssign.querySelector("a").title, + I18n.t("user.assigned_to_you"), + "user assign has the right title" + ); + assert.strictEqual( + groupAssign.querySelector("a").title, + I18n.t("user.assigned_to_group", { group_name: "Team" }), + "group assign has the right title" + ); + }); + + test("dismiss button", async function (assert) { + await visit("/"); + await click(".d-header-icons .current-user"); + await click("#user-menu-button-assign-list"); + + assert.ok( + exists("#user-menu-button-assign-list .badge-notification"), + "badge count is visible before dismissing" + ); + + await click(".notifications-dismiss"); + assert.notOk(markRead, "mark-read request isn't sent"); + assert.strictEqual( + query(".dismiss-notification-confirmation.modal-body").textContent.trim(), + I18n.t("notifications.dismiss_confirmation.body.assigns", { count: 173 }), + "dismiss confirmation modal is shown" + ); + + await click(".modal-footer .btn-primary"); + assert.ok(markRead, "mark-read request is sent"); + assert.notOk(exists(".notifications-dismiss"), "dismiss button is gone"); + assert.notOk( + exists("#user-menu-button-assign-list .badge-notification"), + "badge count is gone after dismissing" + ); + assert.strictEqual( + requestBody, + "dismiss_types=assigned", + "mark-read request is sent with the right params" + ); + }); + + test("empty state", async function (assert) { + forceEmptyState = true; + await visit("/"); + await click(".d-header-icons .current-user"); + await click("#user-menu-button-assign-list"); + + assert.strictEqual( + query(".empty-state-title").textContent.trim(), + I18n.t("user.no_assignments_title"), + "empty state title is rendered" + ); + const emptyStateBody = query(".empty-state-body"); + assert.ok(emptyStateBody, "empty state body exists"); + assert.ok( + emptyStateBody.querySelector(".d-icon-user-plus"), + "empty state body has user-plus icon" + ); + assert.ok( + emptyStateBody + .querySelector("a") + .href.endsWith("/my/preferences/notifications"), + "empty state body has user-plus icon" + ); + }); +});