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"
+ );
+ });
+});