DEV: replaces topic-notifications-options by DMenu

This commit introduces <NotificationsTracking /> which is a wrapper component around <DMenu /> which replaces the select-kit component <TopicNotificationsButton />.

Each tracking case has its dedicated component:

- topic -> `<TopicNotificationsTracking />`
- group -> `<GroupNotificationsTracking />`
- tag -> `<TagNotificationsTracking />`
- category -> `<CategoryNotificationsTracking />`
- chat thread -> `<ThreadNotificationsTracking />`
This commit is contained in:
Joffrey JAFFEUX 2024-12-16 16:46:33 +01:00
parent a141a096d1
commit edeb04fe48
26 changed files with 414 additions and 272 deletions

View File

@ -0,0 +1,16 @@
import NotificationsTracking from "discourse/components/notifications-tracking";
import { i18n } from "discourse-i18n";
const CategoryNotificationsTracking = <template>
<NotificationsTracking
@onChange={{@onChange}}
@levelId={{@levelId}}
@showCaret={{@showCaret}}
@showFullTitle={{@showFullTitle}}
@prefix="category.notifications"
@title={{i18n "category.notifications.title"}}
class="category-notifications-tracking"
/>
</template>;
export default CategoryNotificationsTracking;

View File

@ -119,11 +119,14 @@
{{#unless this.tag}}
{{! don't show category notification menu on tag pages }}
{{#if this.showCategoryNotifications}}
<CategoryNotificationsButton
@value={{this.categoryNotificationLevel}}
@category={{this.category}}
@onChange={{action "changeCategoryNotificationLevel"}}
/>
{{#unless this.category.deleted}}
<CategoryNotificationsTracking
@levelId={{this.categoryNotificationLevel}}
@showFullTitle={{false}}
@showCaret={{false}}
@onChange={{this.changeCategoryNotificationLevel}}
/>
{{/unless}}
{{/if}}
{{/unless}}
{{/if}}
@ -132,11 +135,11 @@
{{#unless this.category}}
{{! don't show tag notification menu on category pages }}
{{#if this.showTagNotifications}}
<TagNotificationsButton
<TagNotificationsTracking
@onChange={{this.changeTagNotificationLevel}}
@value={{this.tagNotification.notification_level}}
@levelId={{this.tagNotification.notification_level}}
/>
{{/if}}
{{/unless}}
{{/if}}
</div>
</div>

View File

@ -0,0 +1,14 @@
import NotificationsTracking from "discourse/components/notifications-tracking";
const GroupNotificationsTracking = <template>
<NotificationsTracking
@onChange={{@onChange}}
@levelId={{@levelId}}
@showCaret={{false}}
@showFullTitle={{false}}
@prefix="groups.notifications"
class="group-notifications-tracking"
/>
</template>;
export default GroupNotificationsTracking;

View File

@ -0,0 +1,159 @@
import Component from "@glimmer/component";
import { fn, hash } from "@ember/helper";
import { action } from "@ember/object";
import DButton from "discourse/components/d-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import PluginOutlet from "discourse/components/plugin-outlet";
import concatClass from "discourse/helpers/concat-class";
import { allLevels, buttonDetails } from "discourse/lib/notification-levels";
import icon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
function constructKey(prefix, level, suffix, key) {
let string = prefix + "." + level;
if (suffix) {
string += suffix;
}
return i18n(string + "." + key);
}
class NotificationsTrackingTrigger extends Component {
get showFullTitle() {
return this.args.showFullTitle ?? true;
}
get showCaret() {
return this.args.showCaret ?? true;
}
get title() {
return constructKey(
this.args.prefix,
this.args.selectedLevel.key,
this.args.suffix,
"title"
);
}
<template>
{{icon @selectedLevel.icon}}
{{#if this.showFullTitle}}
<span class="d-button-label">
{{this.title}}
</span>
{{/if}}
{{#if this.showCaret}}
{{icon "angle-down"}}
{{/if}}
</template>
}
export default class NotificationsTracking extends Component {
@action
registerDmenuApi(api) {
this.dmenuApi = api;
}
@action
async setNotificationLevel(level) {
await this.dmenuApi.close();
this.args.onChange?.(level);
}
@action
description(level) {
return constructKey(
this.args.prefix,
level.key,
this.args.suffix,
"description"
);
}
@action
label(level) {
return constructKey(this.args.prefix, level.key, this.args.suffix, "title");
}
@action
isSelectedClass(level) {
return this.args.levelId === level.id ? "-selected" : "";
}
get selectedLevel() {
return buttonDetails(this.args.levelId);
}
get levels() {
return this.args.levels ?? allLevels;
}
<template>
<DMenu
@identifier="notifications-tracking"
@modalForMobile={{true}}
@triggerClass={{concatClass
"notifications-tracking-trigger-btn"
@triggerClass
}}
@onRegisterApi={{this.registerDmenuApi}}
@title={{@title}}
data-level-id={{this.selectedLevel.id}}
data-level-name={{this.selectedLevel.key}}
...attributes
>
<:trigger>
<NotificationsTrackingTrigger
@showFullTitle={{@showFullTitle}}
@showCaret={{@showCaret}}
@selectedLevel={{this.selectedLevel}}
@suffix={{@suffix}}
@prefix={{@prefix}}
/>
</:trigger>
<:content>
<DropdownMenu as |dropdown|>
{{#each this.levels as |level|}}
<dropdown.item>
<DButton
class={{concatClass
"notifications-tracking-btn"
(this.isSelectedClass level)
}}
@action={{fn this.setNotificationLevel level.id}}
data-level-id={{level.id}}
data-level-name={{level.key}}
>
<div class="notifications-tracking-btn__icons">
<PluginOutlet
@name="notifications-tracking-icons"
@outletArgs={{hash
selectedLevelId=@levelId
level=level
topic=@topic
}}
>
{{icon level.icon}}
</PluginOutlet>
</div>
<div class="notifications-tracking-btn__texts">
<span class="notifications-tracking-btn__label">
{{this.label level}}
</span>
<span class="notifications-tracking-btn__description">
{{this.description level}}
</span>
</div>
</DButton>
</dropdown.item>
{{/each}}
</DropdownMenu>
</:content>
</DMenu>
</template>
}

View File

@ -0,0 +1,14 @@
import NotificationsTracking from "discourse/components/notifications-tracking";
const TagNotificationsTracking = <template>
<NotificationsTracking
@onChange={{@onChange}}
@levelId={{@levelId}}
@showCaret={{false}}
@showFullTitle={{false}}
@prefix="tagging.notifications"
class="tag-notifications-tracking"
/>
</template>;
export default TagNotificationsTracking;

View File

@ -0,0 +1,18 @@
import NotificationsTracking from "discourse/components/notifications-tracking";
import { topicLevels } from "discourse/lib/notification-levels";
import { i18n } from "discourse-i18n";
const TopicNotificationsTracking = <template>
<NotificationsTracking
@onChange={{@onChange}}
@levelId={{@levelId}}
@showCaret={{@showCaret}}
@showFullTitle={{@showFullTitle}}
@prefix="topic.notifications"
@title={{i18n "topic.notifications.title"}}
class="topic-notifications-tracking"
@levels={{topicLevels}}
/>
</template>;
export default TopicNotificationsTracking;

View File

@ -40,12 +40,12 @@
</UserNav::MessagesSecondaryNav>
{{#in-element this.navigationControlsButton}}
<GroupNotificationsButton
@value={{this.group.group_user.notification_level}}
<GroupNotificationsTracking
@levelId={{this.group.group_user.notification_level}}
@onChange={{this.changeGroupNotificationLevel}}
/>
{{/in-element}}
<div class="group-messages group-{{this.group.name}}">
{{outlet}}
</div>
</div>

View File

@ -1,7 +1,6 @@
import { visit } from "@ember/test-helpers";
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Topic Notifications button", function (needs) {
needs.user();
@ -13,24 +12,12 @@ acceptance("Topic Notifications button", function (needs) {
});
test("Updating topic notification level", async function (assert) {
const notificationOptions = selectKit(
"#topic-footer-buttons .topic-notifications-options"
);
await visit("/t/internationalization-localization/280");
await click(".topic-tracking-trigger");
await click(".topic-tracking-btn[data-level-id='3']");
assert.true(
notificationOptions.exists(),
"displays the notification options button in the topic's footer"
);
await notificationOptions.expand();
await notificationOptions.selectRowByValue("3");
assert.strictEqual(
notificationOptions.header().label(),
"Watching",
"displays the right notification level"
);
assert
.dom(".topic-tracking-trigger")
.hasText("Watching", "displays the right notification level");
});
});

View File

@ -0,0 +1,73 @@
import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import NotificationsTracking from "discourse/components/notifications-tracking";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { i18n } from "discourse-i18n";
function extractDescriptions(rows) {
return [...rows].map((el) =>
el
.querySelector(".notifications-tracking-btn__description")
.textContent.trim()
);
}
function getTranslations(type = "") {
return ["watching", "tracking", "regular", "muted"].map((key) => {
return i18n(`topic.notifications.${key}${type}.description`);
});
}
module("Integration | Component | TopicTracking", function (hooks) {
setupRenderingTest(hooks);
test("regular topic notification level descriptions", async function (assert) {
await render(<template><NotificationsTracking @levelId={{1}} /></template>);
await click(".notifications-tracking-trigger");
const uiTexts = extractDescriptions(
document.querySelectorAll(".notifications-tracking-btn")
);
const descriptions = getTranslations();
assert.strictEqual(
uiTexts.length,
descriptions.length,
"has the correct copy"
);
uiTexts.forEach((text, index) => {
assert.strictEqual(
text.trim(),
descriptions[index].trim(),
"has the correct copy"
);
});
});
test("PM topic notification level descriptions", async function (assert) {
await render(<template><NotificationsTracking @levelId={{1}} /></template>);
await click(".notifications-tracking-trigger");
const uiTexts = extractDescriptions(
document.querySelectorAll(".notifications-tracking-btn")
);
const descriptions = getTranslations("_pm");
assert.strictEqual(
uiTexts.length,
descriptions.length,
"has the correct copy"
);
uiTexts.forEach((text, index) => {
assert.strictEqual(
text.trim(),
descriptions[index].trim(),
"has the correct copy"
);
});
});
});

View File

@ -3,7 +3,6 @@ import { getOwner } from "@ember/owner";
import { render, settled } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n, { i18n } from "discourse-i18n";
import TopicNotificationsButton from "select-kit/components/topic-notifications-button";
@ -50,20 +49,16 @@ module(
<TopicNotificationsButton @topic={{state.topic}} @expanded={{true}} />
</template>);
assert.strictEqual(
selectKit().header().label(),
"Normal",
"has the correct label"
);
assert
.dom(".notifications-tracking-trigger")
.hasText("Normal", "has the correct label");
state.topic = buildTopic.call(this, { level: 2 });
await settled();
assert.strictEqual(
selectKit().header().label(),
"Tracking",
"correctly changes the label"
);
assert
.dom(".notifications-tracking-trigger")
.hasText("Tracking", "has the correct label");
});
test("the header has a localized title", async function (assert) {
@ -77,11 +72,9 @@ module(
<TopicNotificationsButton @topic={{topic}} @expanded={{true}} />
</template>);
assert.strictEqual(
selectKit().header().label(),
`${originalTranslation} PM`,
"has the correct label for PMs"
);
assert
.dom(".notifications-tracking-trigger")
.hasText(`${originalTranslation} PM`, "has the correct label for PMs");
});
test("notification reason text - user mailing list mode", async function (assert) {

View File

@ -1,100 +0,0 @@
import { getOwner } from "@ember/owner";
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import selectKit from "discourse/tests/helpers/select-kit-helper";
import { i18n } from "discourse-i18n";
import TopicNotificationsOptions from "select-kit/components/topic-notifications-options";
function extractDescriptions(rows) {
return [...rows].map((el) => el.querySelector(".desc").textContent.trim());
}
function getTranslations(type = "") {
return ["watching", "tracking", "regular", "muted"].map((key) => {
return i18n(`topic.notifications.${key}${type}.description`);
});
}
module(
"Integration | Component | select-kit/topic-notifications-options",
function (hooks) {
setupRenderingTest(hooks);
test("regular topic notification level descriptions", async function (assert) {
const store = getOwner(this).lookup("service:store");
const topic = store.createRecord("topic", {
id: 4563,
title: "Qunit Test Topic",
archetype: "regular",
details: {
notification_level: 1,
},
});
await render(<template>
<TopicNotificationsOptions
@value={{topic.details.notification_level}}
@topic={{topic}}
/>
</template>);
await selectKit().expand();
const uiTexts = extractDescriptions(selectKit().rows());
const descriptions = getTranslations();
assert.strictEqual(
uiTexts.length,
descriptions.length,
"has the correct copy"
);
uiTexts.forEach((text, index) => {
assert.strictEqual(
text.trim(),
descriptions[index].trim(),
"has the correct copy"
);
});
});
test("PM topic notification level descriptions", async function (assert) {
const store = getOwner(this).lookup("service:store");
const topic = store.createRecord("topic", {
id: 4563,
title: "Qunit Test Topic",
archetype: "private_message",
details: {
notification_level: 1,
},
});
await render(<template>
<TopicNotificationsOptions
@value={{topic.details.notification_level}}
@topic={{topic}}
/>
</template>);
await selectKit().expand();
const uiTexts = extractDescriptions(selectKit().rows());
const descriptions = getTranslations("_pm");
assert.strictEqual(
uiTexts.length,
descriptions.length,
"has the correct copy"
);
uiTexts.forEach((text, index) => {
assert.strictEqual(
text.trim(),
descriptions[index].trim(),
"has the correct copy"
);
});
});
}
);

View File

@ -1,16 +0,0 @@
import { readOnly } from "@ember/object/computed";
import { classNames } from "@ember-decorators/component";
import { i18n } from "discourse-i18n";
import NotificationOptionsComponent from "select-kit/components/notifications-button";
import { pluginApiIdentifiers, selectKitOptions } from "./select-kit";
@selectKitOptions({
i18nPrefix: "category.notifications",
showFullTitle: false,
headerAriaLabel: i18n("category.notifications.title"),
})
@pluginApiIdentifiers(["category-notifications-button"])
@classNames("category-notifications-button")
export default class CategoryNotificationsButton extends NotificationOptionsComponent {
@readOnly("category.deleted") isHidden;
}

View File

@ -1,14 +1,13 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import TopicNotificationsTracking from "discourse/components/topic-notifications-tracking";
import { NotificationLevels } from "discourse/lib/notification-levels";
import getURL from "discourse-common/lib/get-url";
import I18n, { i18n } from "discourse-i18n";
import TopicNotificationsOptions from "select-kit/components/topic-notifications-options";
const ParagraphWrapper = <template><p class="reason">{{yield}}</p></template>;
const EmptyWrapper = <template>
@ -112,20 +111,21 @@ export default class TopicNotificationsButton extends Component {
}
}
get suffix() {
return this.args.topic.archetype === "private_message" ? "_pm" : "";
}
<template>
<div class="topic-notifications-button" ...attributes>
<this.conditionalWrapper>
<TopicNotificationsOptions
@value={{this.notificationLevel}}
@topic={{@topic}}
<TopicNotificationsTracking
@levelId={{this.notificationLevel}}
@onChange={{this.changeTopicNotificationLevel}}
@options={{hash
icon=(if this.isLoading "spinner")
showFullTitle=@expanded
showCaret=@expanded
headerAriaLabel=(i18n "topic.notifications.title")
}}
@showFullTitle={{@expanded}}
@showCaret={{@expanded}}
@suffix={{this.suffix}}
/>
{{#if @expanded}}
<span class="text">{{htmlSafe this.reasonText}}</span>
{{/if}}

View File

@ -55,3 +55,4 @@
@import "user-stream";
@import "widget-dropdown";
@import "welcome-header";
@import "notifications-tracking";

View File

@ -0,0 +1,43 @@
.notifications-tracking-trigger-btn {
display: flex;
gap: 0.5em;
}
.notifications-tracking-btn {
display: flex;
flex: 1 0 auto;
box-sizing: border-box;
align-items: center;
&__icons {
display: flex;
align-self: flex-start;
margin-right: 0.75em;
}
&__texts {
line-height: var(--line-height-medium);
flex: 1 1 0%;
align-items: flex-start;
display: flex;
flex-wrap: wrap;
flex-direction: column;
}
&__label {
flex: 1 1 auto;
font-weight: bold;
font-size: var(--font-0);
color: var(--primary);
max-width: 100%;
@include ellipsis;
}
&__description {
flex: 1 1 auto;
font-size: var(--font-down-1);
color: var(--primary-medium);
white-space: normal;
text-align: left;
}
}

View File

@ -104,6 +104,10 @@
justify-content: flex-start;
background: rgba(0, 0, 0, 0);
&.-selected {
background: var(--d-hover);
}
&.btn-danger {
color: var(--danger);

View File

@ -1,20 +1,5 @@
.topic-notifications-button {
&.is-loading {
@include unselectable;
pointer-events: none;
.d-icon-spinner {
margin: 0;
}
.selected-name .d-icon {
display: none;
}
.topic-notifications-options {
opacity: 0.5;
}
}
display: contents;
}
// This is a weird fix for a weird issue in iOS/iPadOS, the browser freezes

View File

@ -57,16 +57,6 @@
grid-row-start: 1;
grid-column-start: 2;
}
.group-notifications-button {
margin-left: 8px;
.select-kit-header {
.selected-name .name {
display: none;
}
}
}
}
.user-messages-page {

View File

@ -1,5 +1,4 @@
import { hash } from "@ember/helper";
import CategoryNotificationsButton from "select-kit/components/category-notifications-button";
import CategoryNotificationsTracking from "discourse/components/category-notifications-tracking";
import BaseField from "./da-base-field";
import DAFieldDescription from "./da-field-description";
import DAFieldLabel from "./da-field-label";
@ -11,10 +10,9 @@ export default class CategoryNotficationLevelField extends BaseField {
<DAFieldLabel @label={{@label}} @field={{@field}} />
<div class="controls">
<CategoryNotificationsButton
@value={{@field.metadata.value}}
<CategoryNotificationsTracking
@levelId={{@field.metadata.value}}
@onChange={{this.mutValue}}
@options={{hash showFullTitle=true}}
/>
<DAFieldDescription @description={{@description}} />

View File

@ -1,19 +0,0 @@
import { classNames } from "@ember-decorators/component";
import NotificationsButtonComponent from "select-kit/components/notifications-button";
import {
pluginApiIdentifiers,
selectKitOptions,
} from "select-kit/components/select-kit";
import { threadNotificationButtonLevels } from "discourse/plugins/chat/discourse/lib/chat-notification-levels";
@classNames("thread-notifications-button")
@selectKitOptions({
i18nPrefix: "chat.thread.notifications",
showFullTitle: false,
btnCustomClasses: "btn-flat",
customStyle: true,
})
@pluginApiIdentifiers("thread-notifications-button")
export default class ChatThreadTrackingDropdown extends NotificationsButtonComponent {
content = threadNotificationButtonLevels;
}

View File

@ -5,7 +5,7 @@ import { service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { NotificationLevels } from "discourse/lib/notification-levels";
import ThreadTrackingDropdown from "discourse/plugins/chat/discourse/components/chat-thread-tracking-dropdown";
import ThreadNotificationsTracking from "discourse/plugins/chat/discourse/components/thread-notifications-tracking";
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
export default class ChatNavbarThreadTrackingDropdown extends Component {
@ -54,8 +54,8 @@ export default class ChatNavbarThreadTrackingDropdown extends Component {
}
<template>
<ThreadTrackingDropdown
@value={{this.threadNotificationLevel}}
<ThreadNotificationsTracking
@levelId={{this.threadNotificationLevel}}
@onChange={{this.updateThreadNotificationLevel}}
class={{concatClass
"c-navbar__thread-tracking-dropdown"

View File

@ -0,0 +1,17 @@
import NotificationsTracking from "discourse/components/notifications-tracking";
import { threadNotificationButtonLevels } from "discourse/plugins/chat/discourse/lib/chat-notification-levels";
const ThreadNotificationsTracking = <template>
<NotificationsTracking
@onChange={{@onChange}}
@levels={{threadNotificationButtonLevels}}
@levelId={{@levelId}}
@showCaret={{false}}
@showFullTitle={{false}}
@prefix="chat.thread.notifications"
class="thread-notifications-tracking"
@triggerClass="btn-transparent"
/>
</template>;
export default ThreadNotificationsTracking;

View File

@ -1,18 +0,0 @@
import { classNames } from "@ember-decorators/component";
import NotificationsButtonComponent from "select-kit/components/notifications-button";
import {
pluginApiIdentifiers,
selectKitOptions,
} from "select-kit/components/select-kit";
import { threadNotificationButtonLevels } from "discourse/plugins/chat/discourse/lib/chat-notification-levels";
@classNames("thread-notifications-button")
@selectKitOptions({
i18nPrefix: "chat.thread.notifications",
showFullTitle: false,
btnCustomClasses: "btn-flat",
})
@pluginApiIdentifiers("thread-notifications-button")
export default class ThreadNotificationsButton extends NotificationsButtonComponent {
content = threadNotificationButtonLevels;
}

View File

@ -51,15 +51,11 @@
</StyleguideExample>
<StyleguideExample
@title="<TopicNotificationOptions>"
@title="<TopicNotificationsTracking>"
@initialValue={{1}}
as |value|
>
<TopicNotificationsOptions
@topic={{@dummy.topic}}
@value={{value}}
@onChange={{fn (mut value)}}
/>
<TopicNotificationsTracking @levelId={{value}} @onChange={{fn (mut value)}} />
</StyleguideExample>
<StyleguideExample
@ -82,20 +78,8 @@
<CategoriesAdminDropdown @onChange={{@dummyAction}} />
</StyleguideExample>
<StyleguideExample @title="<CategoryNotificationsButton>">
<CategoryNotificationsButton
@category={{get @dummy "categories.0"}}
@value={{1}}
@onChange={{@dummyAction}}
/>
</StyleguideExample>
<StyleguideExample @title="<NotificationsButton>">
<NotificationsButton
@options={{hash i18nPrefix="groups.notifications"}}
@value={{2}}
@onChange={{@dummyAction}}
/>
<StyleguideExample @title="<CategoryNotificationsTracking>">
<CategoryNotificationsTracking @levelId={{1}} @onChange={{@dummyAction}} />
</StyleguideExample>
<StyleguideExample @title="<DropdownSelectBox>">
@ -167,4 +151,4 @@
<StyleguideExample @title="<IconPicker>">
<IconPicker @name="icon" />
</StyleguideExample>
</StyleguideExample>

View File

@ -12,11 +12,7 @@ module PageObjects
end
def has_tracking_status?(name)
select_kit =
PageObjects::Components::SelectKit.new(
"#topic-footer-buttons .topic-notifications-options",
)
expect(select_kit).to have_selected_name(name)
find("#topic-footer-buttons .topic-tracking-trigger[data-level-name='#{name}']")
end
end
end

View File

@ -263,7 +263,7 @@ module PageObjects
end
def click_notifications_button
find(".topic-notifications-button .select-kit-header").click
find(".topic-notifications-button .topic-tracking-trigger").click
end
def click_admin_menu_button
@ -272,7 +272,7 @@ module PageObjects
def watch_topic
click_notifications_button
find('li[data-name="watching"]').click
find('.topic-tracking-btn[data-level-name="watching"]').click
end
def close_topic