DEV: Refactor new user menu files (#17879)

This commit includes the changes proposed in #17823. I've made these changes so that plugins that need to add tabs/lists with mixed item types - like the bookmarks tab that displays notifications and bookmarks - to the menu, don't have to write 2 templates like we currently do for the bookmarks/messages tabs (see user-menu/bookmark-notification-item.js that has been deleted in this commit).
This commit is contained in:
Osama Sayegh 2022-08-16 05:37:56 +03:00 committed by GitHub
parent b930f4886a
commit 75599fb88e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 907 additions and 834 deletions

View File

@ -1 +0,0 @@
{{component this.component item=@item}}

View File

@ -1,12 +0,0 @@
import Component from "@glimmer/component";
import Notification from "discourse/models/notification";
export default class UserMenuBookmarkNotificationItem extends Component {
get component() {
if (this.args.item.constructor === Notification) {
return "user-menu/notification-item";
} else {
return "user-menu/bookmark-item";
}
}
}

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -3,6 +3,8 @@ import { ajax } from "discourse/lib/ajax";
import Notification from "discourse/models/notification";
import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
import UserMenuBookmarkItem from "discourse/lib/user-menu/bookmark-item";
export default class UserMenuBookmarksList extends UserMenuNotificationsList {
get dismissTypes() {
@ -29,10 +31,6 @@ export default class UserMenuBookmarksList extends UserMenuNotificationsList {
return "user-menu-bookmarks-tab";
}
get itemComponent() {
return "user-menu/bookmark-notification-item";
}
get emptyStateComponent() {
return "user-menu/bookmarks-list-empty-state";
}
@ -50,10 +48,22 @@ export default class UserMenuBookmarksList extends UserMenuNotificationsList {
return ajax(`/u/${this.currentUser.username}/user-menu-bookmarks`).then(
(data) => {
const content = [];
data.notifications.forEach((notification) => {
content.push(Notification.create(notification));
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.bookmarks);
content.push(
...data.bookmarks.map((bookmark) => {
return new UserMenuBookmarkItem({ bookmark });
})
);
return content;
}
);

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -5,7 +5,7 @@
{{else if this.items.length}}
<ul>
{{#each this.items as |item|}}
{{component this.itemComponent item=item}}
<UserMenu::MenuItem @item={{item}}/>
{{/each}}
</ul>
<div class="panel-body-bottom">

View File

@ -9,7 +9,7 @@ export default class UserMenuItemsList extends Component {
constructor() {
super(...arguments);
this._load();
this.#load();
}
get itemsCacheKey() {}
@ -28,12 +28,6 @@ export default class UserMenuItemsList extends Component {
return "user-menu/items-list-empty-state";
}
get itemComponent() {
throw new Error(
`the itemComponent property must be implemented in ${this.constructor.name}`
);
}
fetchItems() {
throw new Error(
`the fetchItems method must be implemented in ${this.constructor.name}`
@ -41,15 +35,15 @@ export default class UserMenuItemsList extends Component {
}
refreshList() {
this._load();
this.#load();
}
dismissWarningModal() {
return null;
}
_load() {
const cached = this._getCachedItems();
#load() {
const cached = this.#getCachedItems();
if (cached?.length) {
this.items = cached;
} else {
@ -57,20 +51,20 @@ export default class UserMenuItemsList extends Component {
}
this.fetchItems()
.then((items) => {
this._setCachedItems(items);
this.#setCachedItems(items);
this.items = items;
})
.finally(() => (this.loading = false));
}
_getCachedItems() {
#getCachedItems() {
const key = this.itemsCacheKey;
if (key) {
return Session.currentProp(`user-menu-items:${key}`);
}
}
_setCachedItems(newItems) {
#setCachedItems(newItems) {
const key = this.itemsCacheKey;
if (key) {
Session.currentProp(`user-menu-items:${key}`, newItems);

View File

@ -1,35 +1,60 @@
import Component from "@glimmer/component";
import { emojiUnescape } from "discourse/lib/text";
import { escapeExpression } from "discourse/lib/utilities";
import { htmlSafe } from "@ember/template";
import { action } from "@ember/object";
export default class UserMenuItem extends Component {
get className() {}
get className() {
return this.#item.className;
}
get linkHref() {
throw new Error("not implemented");
return this.#item.linkHref;
}
get linkTitle() {
throw new Error("not implemented");
return this.#item.linkTitle;
}
get icon() {
throw new Error("not implemented");
return this.#item.icon;
}
get label() {
throw new Error("not implemented");
return this.#item.label;
}
get labelClass() {}
get labelClass() {
return this.#item.labelClass;
}
get description() {
throw new Error("not implemented");
const description = this.#item.description;
if (description) {
if (typeof description === "string") {
// do emoji unescape on all items
return htmlSafe(emojiUnescape(escapeExpression(description)));
}
// it's probably an htmlSafe object, don't try to unescape emojis
return description;
}
}
get descriptionClass() {}
get descriptionClass() {
return this.#item.descriptionClass;
}
get topicId() {}
get topicId() {
return this.#item.topicId;
}
get #item() {
return this.args.item;
}
@action
onClick() {}
onClick() {
return this.#item.onClick();
}
}

View File

@ -1 +0,0 @@
{{component this.component item=@item}}

View File

@ -1,12 +0,0 @@
import Component from "@glimmer/component";
import Notification from "discourse/models/notification";
export default class UserMenuMessageNotificationItem extends Component {
get component() {
if (this.args.item.constructor === Notification) {
return "user-menu/notification-item";
} else {
return "user-menu/message-item";
}
}
}

View File

@ -0,0 +1,3 @@
import templateOnly from "@ember/component/template-only";
export default templateOnly();

View File

@ -3,6 +3,8 @@ import { ajax } from "discourse/lib/ajax";
import Notification from "discourse/models/notification";
import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
export default class UserMenuMessagesList extends UserMenuNotificationsList {
get dismissTypes() {
@ -29,10 +31,6 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
return "user-menu-messages-tab";
}
get itemComponent() {
return "user-menu/message-notification-item";
}
get emptyStateComponent() {
return "user-menu/messages-list-empty-state";
}
@ -51,10 +49,22 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
`/u/${this.currentUser.username}/user-menu-private-messages`
).then((data) => {
const content = [];
data.notifications.forEach((notification) => {
content.push(Notification.create(notification));
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);
content.push(
...data.topics.map((topic) => {
return new UserMenuMessageItem({ message: topic });
})
);
return content;
});
}

View File

@ -5,9 +5,11 @@ import { ajax } from "discourse/lib/ajax";
import { postRNWebviewMessage } from "discourse/lib/utilities";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
export default class UserMenuNotificationsList extends UserMenuItemsList {
@service currentUser;
@service siteSettings;
@service site;
@service store;
@ -28,7 +30,7 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
}
get showDismiss() {
return this.items.some((item) => !item.read);
return this.items.some((item) => !item.notification.read);
}
get dismissTitle() {
@ -52,10 +54,6 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
}
}
get itemComponent() {
return "user-menu/notification-item";
}
fetchItems() {
const params = {
limit: 30,
@ -72,7 +70,16 @@ export default class UserMenuNotificationsList extends UserMenuItemsList {
return this.store
.findStale("notification", params)
.refresh()
.then((c) => c.content);
.then((c) => {
return c.content.map((notification) => {
return new UserMenuNotificationItem({
notification,
currentUser: this.currentUser,
siteSettings: this.siteSettings,
site: this.site,
});
});
});
}
dismissWarningModal() {

View File

@ -3,8 +3,14 @@ import { ajax } from "discourse/lib/ajax";
import UserMenuReviewable from "discourse/models/user-menu-reviewable";
import I18n from "I18n";
import getUrl from "discourse-common/lib/get-url";
import UserMenuReviewableItem from "discourse/lib/user-menu/reviewable-item";
import { inject as service } from "@ember/service";
export default class UserMenuReviewablesList extends UserMenuItemsList {
@service currentUser;
@service siteSettings;
@service site;
get showAllHref() {
return getUrl("/review");
}
@ -17,14 +23,15 @@ export default class UserMenuReviewablesList extends UserMenuItemsList {
return "pending-reviewables";
}
get itemComponent() {
return "user-menu/reviewable-item";
}
fetchItems() {
return ajax("/review/user-menu-list").then((data) => {
return data.reviewables.map((item) => {
return UserMenuReviewable.create(item);
return new UserMenuReviewableItem({
reviewable: UserMenuReviewable.create(item),
currentUser: this.currentUser,
siteSettings: this.siteSettings,
site: this.site,
});
});
});
}

View File

@ -1,8 +0,0 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import I18n from "I18n";
export default class extends NotificationItemBase {
get label() {
return I18n.t("notifications.watching_first_post_label");
}
}

View File

@ -1,17 +1,17 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import BookmarkReminder from "discourse/lib/notification-items/bookmark-reminder";
import Custom from "discourse/lib/notification-items/custom";
import GrantedBadge from "discourse/lib/notification-items/granted-badge";
import GroupMentioned from "discourse/lib/notification-items/group-mentioned";
import GroupMessageSummary from "discourse/lib/notification-items/group-message-summary";
import InviteeAccepted from "discourse/lib/notification-items/invitee-accepted";
import LikedConsolidated from "discourse/lib/notification-items/liked-consolidated";
import Liked from "discourse/lib/notification-items/liked";
import MembershipRequestAccepted from "discourse/lib/notification-items/membership-request-accepted";
import MembershipRequestConsolidated from "discourse/lib/notification-items/membership-request-consolidated";
import MovedPost from "discourse/lib/notification-items/moved-post";
import WatchingFirstPost from "discourse/lib/notification-items/watching-first-post";
import BookmarkReminder from "discourse/lib/notification-types/bookmark-reminder";
import Custom from "discourse/lib/notification-types/custom";
import GrantedBadge from "discourse/lib/notification-types/granted-badge";
import GroupMentioned from "discourse/lib/notification-types/group-mentioned";
import GroupMessageSummary from "discourse/lib/notification-types/group-message-summary";
import InviteeAccepted from "discourse/lib/notification-types/invitee-accepted";
import LikedConsolidated from "discourse/lib/notification-types/liked-consolidated";
import Liked from "discourse/lib/notification-types/liked";
import MembershipRequestAccepted from "discourse/lib/notification-types/membership-request-accepted";
import MembershipRequestConsolidated from "discourse/lib/notification-types/membership-request-consolidated";
import MovedPost from "discourse/lib/notification-types/moved-post";
import WatchingFirstPost from "discourse/lib/notification-types/watching-first-post";
const CLASS_FOR_TYPE = {
bookmark_reminder: BookmarkReminder,
@ -31,7 +31,7 @@ const CLASS_FOR_TYPE = {
let _customClassForType = {};
export function registerNotificationTypeRenderer(notificationType, func) {
_customClassForType[notificationType] = func(NotificationItemBase);
_customClassForType[notificationType] = func(NotificationTypeBase);
}
export function resetNotificationTypeRenderers() {
@ -46,6 +46,6 @@ export function getRenderDirector(
site
) {
const klass =
_customClassForType[type] || CLASS_FOR_TYPE[type] || NotificationItemBase;
_customClassForType[type] || CLASS_FOR_TYPE[type] || NotificationTypeBase;
return new klass({ notification, currentUser, siteSettings, site });
}

View File

@ -4,7 +4,7 @@ import { emojiUnescape } from "discourse/lib/text";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
export default class NotificationItemBase {
export default class NotificationTypeBase {
constructor({ notification, currentUser, siteSettings, site }) {
this.notification = notification;
this.currentUser = currentUser;
@ -16,7 +16,19 @@ export default class NotificationItemBase {
* @returns {string[]} An array of addtional classes that should be added to the <li> element of the notification item.
*/
get classNames() {
return [];
const classes = ["notification"];
if (this.notification.read) {
classes.push("read");
} else {
classes.push("unread");
}
if (this.notificationName) {
classes.push(this.notificationName.replace(/_/g, "-"));
}
if (this.notification.is_warning) {
classes.push("is-warning");
}
return classes;
}
/**
@ -76,11 +88,6 @@ export default class NotificationItemBase {
}
}
/**
* Function that is called when the notification item is clicked.
*/
onClick() {}
/**
* @returns {string[]} Include additional classes to the label.
*/

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
import getUrl from "discourse-common/lib/get-url";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkTitle() {
if (this.notification.data.bookmark_name) {
return I18n.t("notifications.titles.bookmark_reminder_with_name", {

View File

@ -1,7 +1,7 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkTitle() {
if (this.notification.data.title) {
return I18n.t(this.notification.data.title);

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import getURL from "discourse-common/lib/get-url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkHref() {
const badgeId = this.notification.data.badge_id;
if (badgeId) {

View File

@ -1,6 +1,6 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get label() {
return `${this.username} @${this.notification.data.group_name}`;
}

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get description() {
return I18n.t("notifications.group_message_summary", {
count: this.notification.data.inbox_count,

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkHref() {
return userPath(this.notification.data.display_username);
}

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkHref() {
// TODO(osama): serialize username with notifications
return userPath(

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { formatUsername } from "discourse/lib/utilities";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get label() {
if (this.count === 2) {
return I18n.t("notifications.liked_by_2_users", {

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { groupPath } from "discourse/lib/url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkHref() {
return groupPath(this.notification.data.group_name);
}

View File

@ -1,8 +1,8 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get linkHref() {
return userPath(
`${this.notification.username || this.currentUser.username}/messages`

View File

@ -1,7 +1,7 @@
import NotificationItemBase from "discourse/lib/notification-items/base";
import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
export default class extends NotificationItemBase {
export default class extends NotificationTypeBase {
get label() {
return I18n.t("notifications.user_moved_post", { username: this.username });
}

View File

@ -0,0 +1,8 @@
import NotificationTypeBase from "discourse/lib/notification-types/base";
import I18n from "I18n";
export default class extends NotificationTypeBase {
get label() {
return I18n.t("notifications.watching_first_post_label");
}
}

View File

@ -98,7 +98,7 @@ import { consolePrefix } from "discourse/lib/source-identifier";
import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/sidebar/custom-community-section-links";
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
import DiscourseURL from "discourse/lib/url";
import { registerNotificationTypeRenderer } from "discourse/lib/notification-item";
import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager";
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
// If you add any methods to the API ensure you bump up the version number
@ -1857,12 +1857,12 @@ class PluginApi {
/**
* EXPERIMENTAL. Do not use.
* Register a custom renderer for a notification type or override the
* renderer of an existing type. See lib/notification-items/base.js for
* renderer of an existing type. See lib/notification-types/base.js for
* documentation and the default renderer.
*
* ```
* api.registerNotificationTypeRenderer("your_notification_type", (NotificationItemBase) => {
* return class extends NotificationItemBase {
* api.registerNotificationTypeRenderer("your_notification_type", (NotificationTypeBase) => {
* return class extends NotificationTypeBase {
* get label() {
* return "some label";
* }
@ -1874,8 +1874,8 @@ class PluginApi {
* });
* ```
* @callback renderDirectorRegistererCallback
* @param {NotificationItemBase} The base class from which the returned class should inherit.
* @returns {NotificationItemBase} A class that inherits from NotificationItemBase.
* @param {NotificationTypeBase} The base class from which the returned class should inherit.
* @returns {NotificationTypeBase} A class that inherits from NotificationTypeBase.
*
* @param {string} notificationType - ID of the notification type (i.e. the key value of your notification type in the `Notification.types` enum on the server side).
* @param {renderDirectorRegistererCallback} func - Callback function that returns a subclass from the class it receives as its argument.

View File

@ -1,22 +0,0 @@
import ReviewableItemBase from "discourse/lib/reviewable-items/base";
import FlaggedPost from "discourse/lib/reviewable-items/flagged-post";
import QueuedPost from "discourse/lib/reviewable-items/queued-post";
import ReviewableUser from "discourse/lib/reviewable-items/user";
const CLASS_FOR_TYPE = {
ReviewableFlaggedPost: FlaggedPost,
ReviewableQueuedPost: QueuedPost,
ReviewableUser,
};
export function getRenderDirector(
type,
reviewable,
currentUser,
siteSettings,
site
) {
const klass = CLASS_FOR_TYPE[type] || ReviewableItemBase;
return new klass({ reviewable, currentUser, siteSettings, site });
}

View File

@ -0,0 +1,22 @@
import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import FlaggedPost from "discourse/lib/reviewable-types/flagged-post";
import QueuedPost from "discourse/lib/reviewable-types/queued-post";
import ReviewableUser from "discourse/lib/reviewable-types/user";
const CLASS_FOR_TYPE = {
ReviewableFlaggedPost: FlaggedPost,
ReviewableQueuedPost: QueuedPost,
ReviewableUser,
};
export function getRenderDirector(
type,
reviewable,
currentUser,
siteSettings,
site
) {
const klass = CLASS_FOR_TYPE[type] || ReviewableTypeBase;
return new klass({ reviewable, currentUser, siteSettings, site });
}

View File

@ -1,6 +1,6 @@
import I18n from "I18n";
export default class ReviewableItemBase {
export default class ReviewableTypeBase {
constructor({ reviewable, currentUser, siteSettings, site }) {
this.reviewable = reviewable;
this.currentUser = currentUser;

View File

@ -1,8 +1,8 @@
import ReviewableItemBase from "discourse/lib/reviewable-items/base";
import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
export default class extends ReviewableItemBase {
export default class extends ReviewableTypeBase {
get description() {
const title = this.reviewable.topic_fancy_title;
const postNumber = this.reviewable.post_number;

View File

@ -1,10 +1,10 @@
import ReviewableItemBase from "discourse/lib/reviewable-items/base";
import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import { htmlSafe } from "@ember/template";
import { escapeExpression } from "discourse/lib/utilities";
import { emojiUnescape } from "discourse/lib/text";
import I18n from "I18n";
export default class extends ReviewableItemBase {
export default class extends ReviewableTypeBase {
get actor() {
return I18n.t("user_menu.reviewable.queue");
}

View File

@ -1,7 +1,7 @@
import ReviewableItemBase from "discourse/lib/reviewable-items/base";
import ReviewableTypeBase from "discourse/lib/reviewable-types/base";
import I18n from "I18n";
export default class extends ReviewableItemBase {
export default class extends ReviewableTypeBase {
get description() {
return I18n.t("user_menu.reviewable.suspicious_user", {
username: this.reviewable.username,

View File

@ -0,0 +1,29 @@
export default class UserMenuBaseItem {
get className() {}
get linkHref() {
throw new Error("not implemented");
}
get linkTitle() {
throw new Error("not implemented");
}
get icon() {
throw new Error("not implemented");
}
get label() {
throw new Error("not implemented");
}
get labelClass() {}
get description() {
throw new Error("not implemented");
}
get descriptionClass() {}
get topicId() {}
}

View File

@ -1,7 +1,12 @@
import UserMenuItem from "discourse/components/user-menu/menu-item";
import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import { NO_REMINDER_ICON } from "discourse/models/bookmark";
export default class UserMenuBookmarkItem extends UserMenuItem {
export default class UserMenuBookmarkItem extends UserMenuBaseItem {
constructor({ bookmark }) {
super(...arguments);
this.bookmark = bookmark;
}
get className() {
return "bookmark";
}
@ -29,8 +34,4 @@ export default class UserMenuBookmarkItem extends UserMenuItem {
get topicId() {
return this.bookmark.topic_id;
}
get bookmark() {
return this.args.item;
}
}

View File

@ -1,9 +1,15 @@
import UserMenuItem from "discourse/components/user-menu/menu-item";
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";
export default class UserMenuMessageItem extends UserMenuItem {
export default class UserMenuMessageItem extends UserMenuBaseItem {
constructor({ message }) {
super(...arguments);
this.message = message;
}
get className() {
return "message";
}
@ -29,14 +35,10 @@ export default class UserMenuMessageItem extends UserMenuItem {
}
get description() {
return htmlSafe(this.message.fancy_title);
return htmlSafe(emojiUnescape(this.message.fancy_title));
}
get topicId() {
return this.message.id;
}
get message() {
return this.args.item;
}
}

View File

@ -1,45 +1,28 @@
import UserMenuItem from "discourse/components/user-menu/menu-item";
import { setTransientHeader } from "discourse/lib/ajax";
import { action } from "@ember/object";
import { getRenderDirector } from "discourse/lib/notification-item";
import getURL from "discourse-common/lib/get-url";
import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import cookie from "discourse/lib/cookie";
import { inject as service } from "@ember/service";
import getURL from "discourse-common/lib/get-url";
import { setTransientHeader } from "discourse/lib/ajax";
import { getRenderDirector } from "discourse/lib/notification-types-manager";
export default class UserMenuNotificationItem extends UserMenuItem {
@service currentUser;
@service siteSettings;
@service site;
constructor() {
export default class UserMenuNotificationItem extends UserMenuBaseItem {
constructor({ notification, currentUser, siteSettings, site }) {
super(...arguments);
this.notification = notification;
this.currentUser = currentUser;
this.siteSettings = siteSettings;
this.site = site;
this.renderDirector = getRenderDirector(
this.#notificationName,
this.notification,
this.currentUser,
this.siteSettings,
this.site
notification,
currentUser,
siteSettings,
site
);
}
get className() {
const classes = ["notification"];
if (this.notification.read) {
classes.push("read");
} else {
classes.push("unread");
}
if (this.#notificationName) {
classes.push(this.#notificationName.replace(/_/g, "-"));
}
if (this.notification.is_warning) {
classes.push("is-warning");
}
const extras = this.renderDirector.classNames;
if (extras?.length) {
classes.push(...extras);
}
return classes.join(" ");
return this.renderDirector.classNames?.join(" ") || "";
}
get linkHref() {
@ -74,21 +57,15 @@ export default class UserMenuNotificationItem extends UserMenuItem {
return this.notification.topic_id;
}
get notification() {
return this.args.item;
}
get #notificationName() {
return this.site.notificationLookup[this.notification.notification_type];
}
@action
onClick() {
if (!this.notification.read) {
this.notification.set("read", true);
setTransientHeader("Discourse-Clear-Notifications", this.notification.id);
cookie("cn", this.notification.id, { path: getURL("/") });
}
this.renderDirector.onClick();
}
}

View File

@ -1,16 +1,15 @@
import UserMenuItem from "discourse/components/user-menu/menu-item";
import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import getURL from "discourse-common/lib/get-url";
import { getRenderDirector } from "discourse/lib/reviewable-item";
import { inject as service } from "@ember/service";
import { getRenderDirector } from "discourse/lib/reviewable-types-manager";
export default class UserMenuReviewableItem extends UserMenuItem {
@service currentUser;
@service siteSettings;
@service site;
constructor() {
export default class UserMenuReviewableItem extends UserMenuBaseItem {
constructor({ reviewable, currentUser, siteSettings, site }) {
super(...arguments);
this.reviewable = this.args.item;
this.reviewable = reviewable;
this.currentUser = currentUser;
this.siteSettings = siteSettings;
this.site = site;
this.renderDirector = getRenderDirector(
this.reviewable.type,
this.reviewable,

View File

@ -1,4 +1,4 @@
import { getRenderDirector } from "discourse/lib/notification-item";
import { getRenderDirector } from "discourse/lib/notification-types-manager";
import sessionFixtures from "discourse/tests/fixtures/session-fixtures";
import User from "discourse/models/user";
import Site from "discourse/models/site";

View File

@ -73,7 +73,7 @@ import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
import { clearToolbarCallbacks } from "discourse/components/d-editor";
import { clearExtraHeaderIcons } from "discourse/widgets/header";
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
import { resetNotificationTypeRenderers } from "discourse/lib/notification-item";
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
export function currentUser() {

View File

@ -1,4 +1,4 @@
import { getRenderDirector } from "discourse/lib/reviewable-item";
import { getRenderDirector } from "discourse/lib/reviewable-types-manager";
import sessionFixtures from "discourse/tests/fixtures/session-fixtures";
import User from "discourse/models/user";
import Site from "discourse/models/site";

View File

@ -1,84 +0,0 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { render } from "@ember/test-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { hbs } from "ember-cli-htmlbars";
function getBookmark(overrides = {}) {
return deepMerge(
{
id: 6,
created_at: "2022-08-05T06:09:39.559Z",
updated_at: "2022-08-05T06:11:27.246Z",
name: "",
reminder_at: "2022-08-05T06:10:42.223Z",
reminder_at_ics_start: "20220805T061042Z",
reminder_at_ics_end: "20220805T071042Z",
pinned: false,
title: "Test poll topic hello world",
fancy_title: "Test poll topic hello world",
excerpt: "poll",
bookmarkable_id: 1009,
bookmarkable_type: "Post",
bookmarkable_url: "http://localhost:4200/t/this-bookmarkable-url/227/1",
tags: [],
tags_descriptions: {},
truncated: true,
topic_id: 227,
linked_post_number: 1,
deleted: false,
hidden: false,
category_id: 1,
closed: false,
archived: false,
archetype: "regular",
highest_post_number: 45,
last_read_post_number: 31,
bumped_at: "2022-04-21T15:14:37.359Z",
slug: "test-poll-topic-hello-world",
user: {
id: 1,
username: "somebody",
name: "Mr. Somebody",
avatar_template: "/letter_avatar_proxy/v4/letter/o/f05b48/{size}.png",
},
},
overrides
);
}
module("Integration | Component | user-menu | bookmark-item", function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::BookmarkItem @item={{this.bookmark}}/>`;
test("uses bookmarkable_url for the href", async function (assert) {
this.set("bookmark", getBookmark());
await render(template);
assert.ok(
query("li.bookmark a").href.endsWith("/t/this-bookmarkable-url/227/1")
);
});
test("item label is the bookmarked post author", async function (assert) {
this.set(
"bookmark",
getBookmark({ user: { username: "bookmarkPostAuthor" } })
);
await render(template);
assert.strictEqual(
query("li.bookmark .item-label").textContent.trim(),
"bookmarkPostAuthor"
);
});
test("item description is the bookmark title", async function (assert) {
this.set("bookmark", getBookmark({ title: "Custom bookmark title" }));
await render(template);
assert.strictEqual(
query("li.bookmark .item-description").textContent.trim(),
"Custom bookmark title"
);
});
});

View File

@ -0,0 +1,616 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { render, settled } from "@ember/test-helpers";
import { cloneJSON, deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import UserMenuReviewable from "discourse/models/user-menu-reviewable";
import { hbs } from "ember-cli-htmlbars";
import { withPluginApi } from "discourse/lib/plugin-api";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
import UserMenuBookmarkItem from "discourse/lib/user-menu/bookmark-item";
import UserMenuReviewableItem from "discourse/lib/user-menu/reviewable-item";
import PrivateMessagesFixture from "discourse/tests/fixtures/private-messages-fixtures";
import I18n from "I18n";
function getNotification(currentUser, siteSettings, site, overrides = {}) {
const notification = Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.mentioned,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
post_number: 113,
topic_id: 449,
fancy_title: "This is fancy title &lt;a&gt;!",
slug: "this-is-fancy-title",
data: {
topic_title: "this is title before it becomes fancy <a>!",
display_username: "osama",
original_post_id: 1,
original_post_type: 1,
original_username: "velesin",
},
},
overrides
)
);
return new UserMenuNotificationItem({
notification,
currentUser,
siteSettings,
site,
});
}
module(
"Integration | Component | user-menu | menu-item | with notification items",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::MenuItem @item={{this.item}}/>`;
test("pushes `read` to the classList if the notification is read and `unread` if it isn't", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
this.item.notification.read = false;
await render(template);
assert.notOk(exists("li.read"));
assert.ok(exists("li.unread"));
this.item.notification.read = true;
// await pauseTest();
await settled();
assert.ok(
exists("li.read"),
"the item re-renders when the read property is updated"
);
assert.notOk(
exists("li.unread"),
"the item re-renders when the read property is updated"
);
});
test("pushes the notification type name to the classList", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
await render(template);
let item = query("li");
assert.ok(item.classList.contains("mentioned"));
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
notification_type: NOTIFICATION_TYPES.private_message,
})
);
await settled();
assert.ok(
exists("li.private-message"),
"replaces underscores in type name with dashes"
);
});
test("pushes is-warning to the classList if the notification originates from a warning PM", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
is_warning: true,
})
);
await render(template);
assert.ok(exists("li.is-warning"));
});
test("doesn't push is-warning to the classList if the notification doesn't originate from a warning PM", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
await render(template);
assert.ok(!exists("li.is-warning"));
assert.ok(exists("li"));
});
test("the item's href links to the topic that the notification originates from", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
await render(template);
const link = query("li a");
assert.ok(link.href.endsWith("/t/this-is-fancy-title/449/113"));
});
test("the item's href links to the group messages if the notification is for a group messages", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
topic_id: null,
post_number: null,
slug: null,
data: {
group_id: 33,
group_name: "grouperss",
username: "ossaama",
},
})
);
await render(template);
const link = query("li a");
assert.ok(link.href.endsWith("/u/ossaama/messages/grouperss"));
});
test("the item's link has a title for accessibility", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
await render(template);
const link = query("li a");
assert.strictEqual(link.title, I18n.t("notifications.titles.mentioned"));
});
test("has elements for label and description", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site)
);
await render(template);
const label = query("li a .item-label");
const description = query("li a .item-description");
assert.strictEqual(
label.textContent.trim(),
"osama",
"the label's content is the username by default"
);
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"the description defaults to the fancy_title"
);
});
test("the description falls back to topic_title from data if fancy_title is absent", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
fancy_title: null,
})
);
await render(template);
const description = query("li a .item-description");
assert.strictEqual(
description.textContent.trim(),
"this is title before it becomes fancy <a>!",
"topic_title from data is rendered safely"
);
});
test("fancy_title is emoji-unescaped", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
fancy_title: "title with emoji :phone:",
})
);
await render(template);
assert.ok(
exists("li a .item-description img.emoji"),
"emojis are unescaped when fancy_title is used for description"
);
});
test("topic_title from data is emoji-unescaped safely", async function (assert) {
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
fancy_title: null,
data: {
topic_title: "unsafe title with <a> unescaped emoji :phone:",
},
})
);
await render(template);
const description = query("li a .item-description");
assert.strictEqual(
description.textContent.trim(),
"unsafe title with <a> unescaped emoji",
"topic_title is rendered safely"
);
assert.ok(
exists(".item-description img.emoji"),
"emoji is rendered correctly"
);
});
test("various aspects can be customized according to the notification's render director", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationTypeBase) => {
return class extends NotificationTypeBase {
get classNames() {
return ["additional", "classes"];
}
get linkHref() {
return "/somewhere/awesome";
}
get linkTitle() {
return "hello world this is unsafe '\"<span>";
}
get icon() {
return "wrench";
}
get label() {
return "notification label 666 <span>";
}
get description() {
return "notification description 123 <script>";
}
get labelClasses() {
return ["label-wrapper-1"];
}
get descriptionClasses() {
return ["description-class-1"];
}
};
}
);
});
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.ok(
exists("li.additional.classes"),
"extra classes are included on the item"
);
const link = query("li a");
assert.ok(
link.href.endsWith("/somewhere/awesome"),
"link href is customized"
);
assert.strictEqual(
link.title,
"hello world this is unsafe '\"<span>",
"link title is customized and rendered safely"
);
assert.ok(exists("svg.d-icon-wrench"), "icon is customized");
const label = query("li .item-label");
assert.ok(
label.classList.contains("label-wrapper-1"),
"label wrapper has additional classes"
);
assert.strictEqual(
label.textContent.trim(),
"notification label 666 <span>",
"label content is customized"
);
const description = query(".item-description");
assert.ok(
description.classList.contains("description-class-1"),
"description has additional classes"
);
assert.strictEqual(
description.textContent.trim(),
"notification description 123 <script>",
"description content is customized"
);
});
test("description can be omitted", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationTypeBase) => {
return class extends NotificationTypeBase {
get description() {
return null;
}
get label() {
return "notification label";
}
};
}
);
});
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.notOk(exists(".item-description"), "description is not rendered");
assert.ok(
query("li").textContent.trim(),
"notification label",
"only label content is displayed"
);
});
test("label can be omitted", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationTypeBase) => {
return class extends NotificationTypeBase {
get label() {
return null;
}
get description() {
return "notification description";
}
};
}
);
});
this.set(
"item",
getNotification(this.currentUser, this.siteSettings, this.site, {
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.ok(
query("li").textContent.trim(),
"notification description",
"only notification description is displayed"
);
assert.notOk(exists(".item-label"), "label is not rendered");
});
}
);
function getMessage(overrides = {}) {
const message = deepMerge(
cloneJSON(
PrivateMessagesFixture["/topics/private-messages/eviltrout.json"]
.topic_list.topics[0]
),
overrides
);
return new UserMenuMessageItem({ message });
}
module(
"Integration | Component | user-menu | menu-item | with message items",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::MenuItem @item={{this.item}}/>`;
test("item description is the fancy title of the message", async function (assert) {
this.set(
"item",
getMessage({ fancy_title: "This is a <b>safe</b> title!" })
);
await render(template);
assert.strictEqual(
query("li.message .item-description").textContent.trim(),
"This is a safe title!"
);
assert.strictEqual(
query("li.message .item-description b").textContent.trim(),
"safe",
"fancy title is not escaped"
);
});
}
);
function getBookmark(overrides = {}) {
const bookmark = deepMerge(
{
id: 6,
created_at: "2022-08-05T06:09:39.559Z",
updated_at: "2022-08-05T06:11:27.246Z",
name: "",
reminder_at: "2022-08-05T06:10:42.223Z",
reminder_at_ics_start: "20220805T061042Z",
reminder_at_ics_end: "20220805T071042Z",
pinned: false,
title: "Test poll topic hello world",
fancy_title: "Test poll topic hello world",
excerpt: "poll",
bookmarkable_id: 1009,
bookmarkable_type: "Post",
bookmarkable_url: "http://localhost:4200/t/this-bookmarkable-url/227/1",
tags: [],
tags_descriptions: {},
truncated: true,
topic_id: 227,
linked_post_number: 1,
deleted: false,
hidden: false,
category_id: 1,
closed: false,
archived: false,
archetype: "regular",
highest_post_number: 45,
last_read_post_number: 31,
bumped_at: "2022-04-21T15:14:37.359Z",
slug: "test-poll-topic-hello-world",
user: {
id: 1,
username: "somebody",
name: "Mr. Somebody",
avatar_template: "/letter_avatar_proxy/v4/letter/o/f05b48/{size}.png",
},
},
overrides
);
return new UserMenuBookmarkItem({ bookmark });
}
module(
"Integration | Component | user-menu | meun-item | with bookmark items",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::MenuItem @item={{this.item}}/>`;
test("uses bookmarkable_url for the href", async function (assert) {
this.set("item", getBookmark());
await render(template);
assert.ok(
query("li.bookmark a").href.endsWith("/t/this-bookmarkable-url/227/1")
);
});
test("item label is the bookmarked post author", async function (assert) {
this.set(
"item",
getBookmark({ user: { username: "bookmarkPostAuthor" } })
);
await render(template);
assert.strictEqual(
query("li.bookmark .item-label").textContent.trim(),
"bookmarkPostAuthor"
);
});
test("item description is the bookmark title", async function (assert) {
this.set("item", getBookmark({ title: "Custom bookmark title" }));
await render(template);
assert.strictEqual(
query("li.bookmark .item-description").textContent.trim(),
"Custom bookmark title"
);
});
}
);
function getReviewable(currentUser, siteSettings, site, overrides = {}) {
const reviewable = UserMenuReviewable.create(
Object.assign(
{
flagger_username: "sayo2",
id: 17,
pending: false,
post_number: 3,
topic_fancy_title: "anything hello world",
type: "Reviewable",
},
overrides
)
);
return new UserMenuReviewableItem({
reviewable,
currentUser,
siteSettings,
site,
});
}
module(
"Integration | Component | user-menu | menu-item | with reviewable items",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::MenuItem @item={{this.item}}/>`;
test("doesn't push `reviewed` to the classList if the reviewable is pending", async function (assert) {
this.set(
"item",
getReviewable(this.currentUser, this.siteSettings, this.site, {
pending: true,
})
);
await render(template);
assert.ok(!exists("li.reviewed"));
assert.ok(exists("li"));
});
test("pushes `reviewed` to the classList if the reviewable isn't pending", async function (assert) {
this.set(
"item",
getReviewable(this.currentUser, this.siteSettings, this.site, {
pending: false,
})
);
await render(template);
assert.ok(exists("li.reviewed"));
});
test("has elements for label and description", async function (assert) {
this.set(
"item",
getReviewable(this.currentUser, this.siteSettings, this.site)
);
await render(template);
const label = query("li .item-label");
const description = query("li .item-description");
assert.strictEqual(
label.textContent.trim(),
"sayo2",
"the label is the flagger_username"
);
assert.strictEqual(
description.textContent.trim(),
I18n.t("user_menu.reviewable.default_item", {
reviewable_id: this.item.reviewable.id,
}),
"displays the description for the reviewable"
);
});
test("the item's label is a placeholder that indicates deleted user if flagger_username is absent", async function (assert) {
this.set(
"item",
getReviewable(this.currentUser, this.siteSettings, this.site, {
flagger_username: null,
})
);
await render(template);
const label = query("li .item-label");
assert.strictEqual(
label.textContent.trim(),
I18n.t("user_menu.reviewable.deleted_user")
);
});
}
);

View File

@ -1,38 +0,0 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { render } from "@ember/test-helpers";
import { cloneJSON, deepMerge } from "discourse-common/lib/object";
import { hbs } from "ember-cli-htmlbars";
import PrivateMessagesFixture from "discourse/tests/fixtures/private-messages-fixtures";
function getMessage(overrides = {}) {
const data = cloneJSON(
PrivateMessagesFixture["/topics/private-messages/eviltrout.json"].topic_list
.topics[0]
);
return deepMerge(data, overrides);
}
module("Integration | Component | user-menu | message-item", function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::MessageItem @item={{this.message}}/>`;
test("item description is the fancy title of the message", async function (assert) {
this.set(
"message",
getMessage({ fancy_title: "This is a <b>safe</b> title!" })
);
await render(template);
assert.strictEqual(
query("li.message .item-description").textContent.trim(),
"This is a safe title!"
);
assert.strictEqual(
query("li.message .item-description b").textContent.trim(),
"safe",
"fancy title is not escaped"
);
});
});

View File

@ -1,397 +0,0 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { click, render, settled } from "@ember/test-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import { withPluginApi } from "discourse/lib/plugin-api";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.mentioned,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
post_number: 113,
topic_id: 449,
fancy_title: "This is fancy title &lt;a&gt;!",
slug: "this-is-fancy-title",
data: {
topic_title: "this is title before it becomes fancy <a>!",
display_username: "osama",
original_post_id: 1,
original_post_type: 1,
original_username: "velesin",
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::NotificationItem @item={{this.notification}}/>`;
test("pushes `read` to the classList if the notification is read and `unread` if it isn't", async function (assert) {
this.set("notification", getNotification());
this.notification.read = false;
await render(template);
assert.notOk(exists("li.read"));
assert.ok(exists("li.unread"));
this.notification.read = true;
await settled();
assert.ok(
exists("li.read"),
"the item re-renders when the read property is updated"
);
assert.notOk(
exists("li.unread"),
"the item re-renders when the read property is updated"
);
});
test("pushes the notification type name to the classList", async function (assert) {
this.set("notification", getNotification());
await render(template);
let item = query("li");
assert.ok(item.classList.contains("mentioned"));
this.set(
"notification",
getNotification({
notification_type: NOTIFICATION_TYPES.private_message,
})
);
await settled();
assert.ok(
exists("li.private-message"),
"replaces underscores in type name with dashes"
);
});
test("pushes is-warning to the classList if the notification originates from a warning PM", async function (assert) {
this.set("notification", getNotification({ is_warning: true }));
await render(template);
assert.ok(exists("li.is-warning"));
});
test("doesn't push is-warning to the classList if the notification doesn't originate from a warning PM", async function (assert) {
this.set("notification", getNotification());
await render(template);
assert.ok(!exists("li.is-warning"));
assert.ok(exists("li"));
});
test("the item's href links to the topic that the notification originates from", async function (assert) {
this.set("notification", getNotification());
await render(template);
const link = query("li a");
assert.ok(link.href.endsWith("/t/this-is-fancy-title/449/113"));
});
test("the item's href links to the group messages if the notification is for a group messages", async function (assert) {
this.set(
"notification",
getNotification({
topic_id: null,
post_number: null,
slug: null,
data: {
group_id: 33,
group_name: "grouperss",
username: "ossaama",
},
})
);
await render(template);
const link = query("li a");
assert.ok(link.href.endsWith("/u/ossaama/messages/grouperss"));
});
test("the item's link has a title for accessibility", async function (assert) {
this.set("notification", getNotification());
await render(template);
const link = query("li a");
assert.strictEqual(link.title, I18n.t("notifications.titles.mentioned"));
});
test("has elements for label and description", async function (assert) {
this.set("notification", getNotification());
await render(template);
const label = query("li a .item-label");
const description = query("li a .item-description");
assert.strictEqual(
label.textContent.trim(),
"osama",
"the label's content is the username by default"
);
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"the description defaults to the fancy_title"
);
});
test("the description falls back to topic_title from data if fancy_title is absent", async function (assert) {
this.set(
"notification",
getNotification({
fancy_title: null,
})
);
await render(template);
const description = query("li a .item-description");
assert.strictEqual(
description.textContent.trim(),
"this is title before it becomes fancy <a>!",
"topic_title from data is rendered safely"
);
});
test("fancy_title is emoji-unescaped", async function (assert) {
this.set(
"notification",
getNotification({
fancy_title: "title with emoji :phone:",
})
);
await render(template);
assert.ok(
exists("li a .item-description img.emoji"),
"emojis are unescaped when fancy_title is used for description"
);
});
test("topic_title from data is not emoji-unescaped", async function (assert) {
this.set(
"notification",
getNotification({
fancy_title: null,
data: {
topic_title: "unsafe title with unescaped emoji :phone:",
},
})
);
await render(template);
const description = query("li a .item-description");
assert.strictEqual(
description.textContent.trim(),
"unsafe title with unescaped emoji :phone:",
"emojis aren't unescaped when topic title is not safe"
);
assert.ok(!query("img"), "no <img> exists");
});
test("various aspects can be customized according to the notification's render director", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationItemBase) => {
return class extends NotificationItemBase {
get classNames() {
return ["additional", "classes"];
}
get linkHref() {
return "/somewhere/awesome";
}
get linkTitle() {
return "hello world this is unsafe '\"<span>";
}
get icon() {
return "wrench";
}
get label() {
return "notification label 666 <span>";
}
get description() {
return "notification description 123 <script>";
}
get labelClasses() {
return ["label-wrapper-1"];
}
get descriptionClasses() {
return ["description-class-1"];
}
};
}
);
});
this.set(
"notification",
getNotification({
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.ok(
exists("li.additional.classes"),
"extra classes are included on the item"
);
const link = query("li a");
assert.ok(
link.href.endsWith("/somewhere/awesome"),
"link href is customized"
);
assert.strictEqual(
link.title,
"hello world this is unsafe '\"<span>",
"link title is customized and rendered safely"
);
assert.ok(exists("svg.d-icon-wrench"), "icon is customized");
const label = query("li .item-label");
assert.ok(
label.classList.contains("label-wrapper-1"),
"label wrapper has additional classes"
);
assert.strictEqual(
label.textContent.trim(),
"notification label 666 <span>",
"label content is customized"
);
const description = query(".item-description");
assert.ok(
description.classList.contains("description-class-1"),
"description has additional classes"
);
assert.strictEqual(
description.textContent.trim(),
"notification description 123 <script>",
"description content is customized"
);
});
test("description can be omitted", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationItemBase) => {
return class extends NotificationItemBase {
get description() {
return null;
}
get label() {
return "notification label";
}
};
}
);
});
this.set(
"notification",
getNotification({
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.notOk(exists(".item-description"), "description is not rendered");
assert.ok(
query("li").textContent.trim(),
"notification label",
"only label content is displayed"
);
});
test("label can be omitted", async function (assert) {
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationItemBase) => {
return class extends NotificationItemBase {
get label() {
return null;
}
get description() {
return "notification description";
}
};
}
);
});
this.set(
"notification",
getNotification({
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
assert.ok(
query("li").textContent.trim(),
"notification description",
"only notification description is displayed"
);
assert.notOk(exists(".item-label"), "label is not rendered");
});
test("custom click handlers", async function (assert) {
let klass;
withPluginApi("0.1", (api) => {
api.registerNotificationTypeRenderer(
"linked",
(NotificationItemBase) => {
klass = class extends NotificationItemBase {
static onClickCalled = false;
get linkHref() {
return "#";
}
onClick() {
klass.onClickCalled = true;
}
};
return klass;
}
);
});
this.set(
"notification",
getNotification({
notification_type: NOTIFICATION_TYPES.linked,
})
);
await render(template);
await click("li a");
assert.ok(klass.onClickCalled);
});
}
);

View File

@ -1,75 +0,0 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import UserMenuReviewable from "discourse/models/user-menu-reviewable";
import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getReviewable(overrides = {}) {
return UserMenuReviewable.create(
Object.assign(
{
flagger_username: "sayo2",
id: 17,
pending: false,
post_number: 3,
topic_fancy_title: "anything hello world",
type: "Reviewable",
},
overrides
)
);
}
module(
"Integration | Component | user-menu | reviewable-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::ReviewableItem @item={{this.item}}/>`;
test("doesn't push `reviewed` to the classList if the reviewable is pending", async function (assert) {
this.set("item", getReviewable({ pending: true }));
await render(template);
assert.ok(!exists("li.reviewed"));
assert.ok(exists("li"));
});
test("pushes `reviewed` to the classList if the reviewable isn't pending", async function (assert) {
this.set("item", getReviewable({ pending: false }));
await render(template);
assert.ok(exists("li.reviewed"));
});
test("has elements for label and description", async function (assert) {
this.set("item", getReviewable());
await render(template);
const label = query("li .item-label");
const description = query("li .item-description");
assert.strictEqual(
label.textContent.trim(),
"sayo2",
"the label is the flagger_username"
);
assert.strictEqual(
description.textContent.trim(),
I18n.t("user_menu.reviewable.default_item", {
reviewable_id: this.item.id,
}),
"displays the description for the reviewable"
);
});
test("the item's label is a placeholder that indicates deleted user if flagger_username is absent", async function (assert) {
this.set("item", getReviewable({ flagger_username: null }));
await render(template);
const label = query("li .item-label");
assert.strictEqual(
label.textContent.trim(),
I18n.t("user_menu.reviewable.deleted_user")
);
});
}
);

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import { htmlSafe } from "@ember/template";
import Notification from "discourse/models/notification";
import I18n from "I18n";
@ -33,7 +33,7 @@ function getNotification(overrides = {}) {
);
}
discourseModule("Unit | Notification Items | bookmark-reminder", function () {
discourseModule("Unit | Notification Types | bookmark-reminder", function () {
test("linkTitle", function (assert) {
const notification = getNotification({
data: { bookmark_name: "My awesome bookmark" },

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import Notification from "discourse/models/notification";
import I18n from "I18n";
@ -28,7 +28,7 @@ function getNotification(overrides = {}) {
);
}
discourseModule("Unit | Notification Items | granted-badge", function () {
discourseModule("Unit | Notification Types | granted-badge", function () {
test("linkHref", function (assert) {
const notification = getNotification();
const director = createRenderDirector(

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import Notification from "discourse/models/notification";
function getNotification(overrides = {}) {
@ -34,7 +34,7 @@ function getNotification(overrides = {}) {
);
}
discourseModule("Unit | Notification Items | group-mentioned", function () {
discourseModule("Unit | Notification Types | group-mentioned", function () {
test("label", function (assert) {
const notification = getNotification();
const director = createRenderDirector(

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import Notification from "discourse/models/notification";
import I18n from "I18n";
@ -29,7 +29,7 @@ function getNotification(overrides = {}) {
}
discourseModule(
"Unit | Notification Items | group-message-summary",
"Unit | Notification Types | group-message-summary",
function () {
test("description", function (assert) {
const notification = getNotification();

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import Notification from "discourse/models/notification";
import I18n from "I18n";
@ -31,7 +31,7 @@ function getNotification(overrides = {}) {
);
}
discourseModule("Unit | Notification Items | liked-consolidated", function () {
discourseModule("Unit | Notification Types | liked-consolidated", function () {
test("linkHref", function (assert) {
const notification = getNotification();
const director = createRenderDirector(

View File

@ -2,7 +2,7 @@ import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { deepMerge } from "discourse-common/lib/object";
import { createRenderDirector } from "discourse/tests/helpers/notification-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/notification-types-helper";
import Notification from "discourse/models/notification";
import I18n from "I18n";
@ -33,7 +33,7 @@ function getNotification(overrides = {}) {
);
}
discourseModule("Unit | Notification Items | liked", function () {
discourseModule("Unit | Notification Types | liked", function () {
test("label", function (assert) {
const notification = getNotification();
const director = createRenderDirector(

View File

@ -1,6 +1,6 @@
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
import { test } from "qunit";
import { createRenderDirector } from "discourse/tests/helpers/reviewable-items-helper";
import { createRenderDirector } from "discourse/tests/helpers/reviewable-types-helper";
import { htmlSafe } from "@ember/template";
import { emojiUnescape } from "discourse/lib/text";
import UserMenuReviewable from "discourse/models/user-menu-reviewable";