UX: reorganize user prefs for experimental user nav (#18764)

Trying out changes to reduce the number of nav items in the experimental horizontal user nav. These changes should only appear with the redesigned_user_page_nav_enabled feature flag.

1. Created a new "Tracking" route. This combines some tracking-related settings from Notifications and Category and Tag tracking (which were separate tabs previously). Don't love the layout yet, but it's something that we can work on.

2. Moved some user-related settings out of Notifications and to the
Users tab. These seem more user-related to me, and it's nice that we can
associate enabling messages with the setting to limit who can send
messages.

3. Moved the App tab (lists app permissions) to be within the Security tab. It's very similar to Recently Used Devices.
This commit is contained in:
Kris 2022-11-17 20:09:04 -05:00 committed by GitHub
parent 60abe99add
commit a6c787345c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 482 additions and 173 deletions

View File

@ -4,58 +4,58 @@
<span>{{i18n "user.preferences_nav.account"}}</span>
</LinkTo>
</li>
<li class="nav-security">
<LinkTo @route="preferences.security">
{{d-icon "lock"}}
<span>{{i18n "user.preferences_nav.security"}}</span>
</LinkTo>
</li>
<li class="nav-profile">
<LinkTo @route="preferences.profile">
{{d-icon "user"}}
<span>{{i18n "user.preferences_nav.profile"}}</span>
</LinkTo>
</li>
<li class="nav-emails">
<LinkTo @route="preferences.emails">
{{d-icon "envelope"}}
<span>{{i18n "user.preferences_nav.emails"}}</span>
</LinkTo>
</li>
<li class="nav-notifications">
<LinkTo @route="preferences.notifications">
{{d-icon "bell"}}
<span>{{i18n "user.preferences_nav.notifications"}}</span>
</LinkTo>
</li>
{{#if @model.can_change_tracking_preferences}}
<li class="indent nav-categories">
<LinkTo @route="preferences.categories">
{{d-icon "folder"}}
<span>{{i18n "user.preferences_nav.categories"}}</span>
<li class="nav-tracking">
<LinkTo @route="preferences.tracking">
{{d-icon "plus"}}
<span>{{i18n "user.preferences_nav.tracking"}}</span>
</LinkTo>
</li>
{{/if}}
<li class="indent nav-users">
<LinkTo @route="preferences.users">
{{d-icon "users"}}
<span>{{i18n "user.preferences_nav.users"}}</span>
</LinkTo>
</li>
{{#if (and @model.can_change_tracking_preferences @siteSettings.tagging_enabled)}}
<li class="indent nav-tags">
<LinkTo @route="preferences.tags">
{{d-icon "tag"}}
<span>{{i18n "user.preferences_nav.tags"}}</span>
</LinkTo>
</li>
{{/if}}
<li class="nav-interface">
<LinkTo @route="preferences.interface">
{{d-icon "desktop"}}
<span>{{i18n "user.preferences_nav.interface"}}</span>
</LinkTo>
</li>
{{#if @siteSettings.enable_experimental_sidebar_hamburger}}
<li class="indent nav-sidebar">
<LinkTo @route="preferences.sidebar">
@ -64,11 +64,6 @@
</LinkTo>
</li>
{{/if}}
<PluginOutlet @name="user-preferences-nav-under-interface" @connectorTagName="div" @args={{hash model=@model}} />
<li class="nav-apps">
<LinkTo @route="preferences.apps">
{{d-icon "mobile-alt"}}
<span>{{i18n "user.preferences_nav.apps"}}</span>
</LinkTo>
</li>
<PluginOutlet @name="user-preferences-nav" @connectorTagName="li" @args={{hash model=@model}} />

View File

@ -0,0 +1,6 @@
<div class="control-group private-messages">
<label class="control-label">{{i18n "user.private_messages"}}</label>
<div class="controls">
<PreferenceCheckbox @labelKey="user.allow_private_messages" @checked={{@model.user_option.allow_private_messages}} />
</div>
</div>

View File

@ -0,0 +1,53 @@
<div class="control-group category-notifications">
<label class="control-label">{{i18n "user.categories_settings"}}</label>
<div class="controls tracking-controls tracking-controls--watched-categories">
<label>{{d-icon "d-watching"}} {{i18n "user.watched_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.watchingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.watchedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.watchedCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls--tracked-categories">
<label>{{d-icon "d-tracking"}} {{i18n "user.tracked_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.trackingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.trackedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.trackedCategories)}} />
</div>
<div class="instructions">{{i18n "user.tracked_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls--watched-first-categories">
<label>{{d-icon "d-watching-first"}} {{i18n "user.watched_first_post_categories"}}</label>
<CategorySelector @categories={{@model.watchedFirstPostCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.watchedFirstPostCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_first_post_categories_instructions"}}</div>
{{#if @siteSettings.mute_all_categories_by_default}}
<div class="controls tracking-controls tracking-controls--regular-categories">
<label>{{d-icon "d-regular"}} {{i18n "user.regular_categories"}}</label>
<CategorySelector @categories={{@model.regularCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.regularCategories)}} />
</div>
<div class="instructions">{{i18n "user.regular_categories_instructions"}}</div>
{{else}}
<div class="controls tracking-controls tracking-controls--muted-categories">
<label>{{d-icon "d-muted"}} {{i18n "user.muted_categories"}}</label>
{{#if @canSee}}
<a class="show-tracking" href={{@model.mutedTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{@model.mutedCategories}} @blockedCategories={{@selectedCategories}} @onChange={{action (mut @model.mutedCategories)}} />
</div>
<div class="instructions">{{i18n (if @hideMutedTags "user.muted_categories_instructions" "user.muted_categories_instructions_dont_hide")}}</div>
{{/if}}
</div>
<PluginOutlet @name="user-preferences-categories" @tagName="span" @connectorTagName="div" @args={{hash model=@model save=@save}} />
<br>
<PluginOutlet @name="user-custom-controls" @tagName="span" @connectorTagName="div" @args={{hash model=@model}} />

View File

@ -0,0 +1,45 @@
{{#if @siteSettings.tagging_enabled}}
<div class="control-group tag-notifications">
<label class="control-label">{{i18n "user.tag_settings"}}</label>
<div class="controls tracking-controls">
<label>{{d-icon "d-watching" class="icon watching"}} {{i18n "user.watched_tags"}}</label>
<TagChooser @tags={{@model.watched_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.watched_tags_instructions"}}</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-tracking" class="icon tracking"}} {{i18n "user.tracked_tags"}}</label>
<TagChooser @tags={{@model.tracked_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.tracked_tags_instructions"}}</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-watching-first" class="icon watching-first-post"}} {{i18n "user.watched_first_post_tags"}}</label>
<TagChooser @tags={{@model.watching_first_post_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">
{{i18n "user.watched_first_post_tags_instructions"}}
</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-muted" class="icon muted"}} {{i18n "user.muted_tags"}}</label>
<TagChooser @tags={{@model.muted_tags}} @blockedTags={{@selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.muted_tags_instructions"}}</div>
</div>
<PluginOutlet @name="user-preferences-tags" @connectorTagName="div" @args={{hash model=@model save=@save}} />
<PluginOutlet @name="user-custom-controls" @connectorTagName="div" @args={{hash model=@model}} />
{{/if}}

View File

@ -0,0 +1,16 @@
<div class="user-preferences_tracking-topics-controls">
<div class="controls controls-dropdown">
<label>{{i18n "user.new_topic_duration.label"}}</label>
<ComboBox @class="duration" @valueProperty="value" @content={{@considerNewTopicOptions}} @value={{@model.user_option.new_topic_duration_minutes}} @onChange={{action (mut @model.user_option.new_topic_duration_minutes)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.auto_track_topics"}}</label>
<ComboBox @valueProperty="value" @content={{@autoTrackDurations}} @value={{@model.user_option.auto_track_topics_after_msecs}} @onChange={{action (mut @model.user_option.auto_track_topics_after_msecs)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.notification_level_when_replying"}}</label>
<ComboBox @valueProperty="value" @content={{@notificationLevelsForReplying}} @value={{@model.user_option.notification_level_when_replying}} @onChange={{action (mut @model.user_option.notification_level_when_replying)}} />
</div>
</div>

View File

@ -0,0 +1,26 @@
{{#if @model.userApiKeys}}
<div class="control-group apps">
<label class="control-label">{{i18n "user.apps"}}</label>
<div class="controls">
{{#each @model.userApiKeys as |key|}}
<div>
<span>{{key.application_name}}</span>
{{#if key.revoked}}
<DButton @action={{route-action "undoRevokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.undo_revoke_access" />
{{else}}
<DButton @action={{route-action "revokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.revoke_access" />
{{/if}}
<p>
<ul>
{{#each key.scopes as |scope|}}
<li>{{scope}}</li>
{{/each}}
</ul>
</p>
<p><span>{{i18n "user.api_approved"}}</span> {{bound-date key.created_at}}</p>
<p><span>{{i18n "user.api_last_used_at"}}</span> {{bound-date key.last_used_at}}</p>
</div>
{{/each}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,157 @@
import Controller from "@ember/controller";
import { NotificationLevels } from "discourse/lib/notification-levels";
import I18n from "I18n";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
export default class extends Controller {
@service currentUser;
@service siteSettings;
@tracked saved = false;
saveAttrNames = [
"new_topic_duration_minutes",
"auto_track_topics_after_msecs",
"notification_level_when_replying",
"muted_category_ids",
"regular_category_ids",
"watched_category_ids",
"tracked_category_ids",
"watched_first_post_category_ids",
"muted_tags",
"tracked_tags",
"watched_tags",
"watching_first_post_tags",
];
likeNotificationFrequencies = [
{ name: I18n.t("user.like_notification_frequency.always"), value: 0 },
{
name: I18n.t("user.like_notification_frequency.first_time_and_daily"),
value: 1,
},
{ name: I18n.t("user.like_notification_frequency.first_time"), value: 2 },
{ name: I18n.t("user.like_notification_frequency.never"), value: 3 },
];
autoTrackDurations = [
{ name: I18n.t("user.auto_track_options.never"), value: -1 },
{ name: I18n.t("user.auto_track_options.immediately"), value: 0 },
{
name: I18n.t("user.auto_track_options.after_30_seconds"),
value: 30000,
},
{ name: I18n.t("user.auto_track_options.after_1_minute"), value: 60000 },
{
name: I18n.t("user.auto_track_options.after_2_minutes"),
value: 120000,
},
{
name: I18n.t("user.auto_track_options.after_3_minutes"),
value: 180000,
},
{
name: I18n.t("user.auto_track_options.after_4_minutes"),
value: 240000,
},
{
name: I18n.t("user.auto_track_options.after_5_minutes"),
value: 300000,
},
{
name: I18n.t("user.auto_track_options.after_10_minutes"),
value: 600000,
},
];
notificationLevelsForReplying = [
{
name: I18n.t("topic.notifications.watching.title"),
value: NotificationLevels.WATCHING,
},
{
name: I18n.t("topic.notifications.tracking.title"),
value: NotificationLevels.TRACKING,
},
{
name: I18n.t("topic.notifications.regular.title"),
value: NotificationLevels.REGULAR,
},
];
considerNewTopicOptions = [
{ name: I18n.t("user.new_topic_duration.not_viewed"), value: -1 },
{ name: I18n.t("user.new_topic_duration.after_1_day"), value: 60 * 24 },
{ name: I18n.t("user.new_topic_duration.after_2_days"), value: 60 * 48 },
{
name: I18n.t("user.new_topic_duration.after_1_week"),
value: 7 * 60 * 24,
},
{
name: I18n.t("user.new_topic_duration.after_2_weeks"),
value: 2 * 7 * 60 * 24,
},
{ name: I18n.t("user.new_topic_duration.last_here"), value: -2 },
];
get canSee() {
return this.currentUser.id === this.model.id;
}
@computed(
"model.watched_tags.[]",
"model.watching_first_post_tags.[]",
"model.tracked_tags.[]",
"model.muted_tags.[]"
)
get selectedTags() {
return []
.concat(
this.model.watched_tags,
this.model.watching_first_post_tags,
this.model.tracked_tags,
this.model.muted_tags
)
.filter((t) => t);
}
@computed(
"model.watchedCategories",
"model.watchedFirstPostCategories",
"model.trackedCategories",
"model.mutedCategories"
)
get selectedCategories() {
return []
.concat(
this.modelwatchedCategories,
this.model.watchedFirstPostCategories,
this.model.tracakedCategories,
this.model.mutedCategories
)
.filter((t) => t);
}
@computed("siteSettings.remove_muted_tags_from_latest")
get hideMutedTags() {
return this.siteSettings.remove_muted_tags_from_latest !== "never";
}
get canSave() {
return this.canSee || this.currentUser.admin;
}
@action
save() {
this.saved = false;
return this.model
.save(this.saveAttrNames)
.then(() => {
this.saved = true;
})
.catch(popupAjaxError);
}
}

View File

@ -51,6 +51,7 @@ export default Controller.extend({
this._super(...arguments);
this.saveAttrNames = [
"allow_private_messages",
"muted_usernames",
"allowed_pm_usernames",
"enable_allowed_pm_users",
@ -72,11 +73,6 @@ export default Controller.extend({
return !allowPrivateMessages;
},
@discourseComputed("currentUser.can_send_private_messages")
showMessageSettings() {
return this.currentUser?.can_send_private_messages;
},
@action
save() {
this.set("saved", false);

View File

@ -161,6 +161,7 @@ export default function () {
this.route("profile");
this.route("emails");
this.route("notifications");
this.route("tracking");
this.route("categories");
this.route("users");
this.route("tags");

View File

@ -0,0 +1,5 @@
import RestrictedUserRoute from "discourse/routes/restricted-user";
export default RestrictedUserRoute.extend({
showFooter: true,
});

View File

@ -3,8 +3,7 @@
<div class="user-navigation user-navigation-secondary">
<HorizontalOverflowNav>
<UserNav::PreferencesNav
@user={{this.model}}
@viewingSelf={{this.viewingSelf}}
@currentUser={{this.currentUser}}
@model={{this.model}}
@siteSettings={{this.siteSettings}}/>
</HorizontalOverflowNav>
@ -79,6 +78,7 @@
</LinkTo>
</li>
{{/if}}
<PluginOutlet @name="user-preferences-nav" @tagName="span" @connectorTagName="li" @args={{hash model=this.model}} />
</MobileNav>
</DSection>

View File

@ -1,28 +1,3 @@
{{#if this.model.userApiKeys}}
<div class="control-group apps">
<label class="control-label">{{i18n "user.apps"}}</label>
<div class="controls">
{{#each this.model.userApiKeys as |key|}}
<div>
<span>{{key.application_name}}</span>
{{#if key.revoked}}
<DButton @action={{route-action "undoRevokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.undo_revoke_access" />
{{else}}
<DButton @action={{route-action "revokeApiKey"}} @actionParam={{key}} @class="btn" @label="user.revoke_access" />
{{/if}}
<p>
<ul>
{{#each key.scopes as |scope|}}
<li>{{scope}}</li>
{{/each}}
</ul>
</p>
<p><span>{{i18n "user.api_approved"}}</span> {{bound-date key.created_at}}</p>
<p><span>{{i18n "user.api_last_used_at"}}</span> {{bound-date key.last_used_at}}</p>
</div>
{{/each}}
</div>
</div>
{{/if}}
<UserPreferences::UserApiKeys @model={{@model}}/>
<PluginOutlet @name="user-preferences-apps" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />

View File

@ -1,53 +1,10 @@
<div class="control-group category-notifications">
<label class="control-label">{{i18n "user.categories_settings"}}</label>
<div class="controls tracking-controls tracking-controls--watched-categories">
<label>{{d-icon "d-watching"}} {{i18n "user.watched_categories"}}</label>
{{#if this.canSee}}
<a class="show-tracking" href={{this.model.watchingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{this.model.watchedCategories}} @blockedCategories={{this.selectedCategories}} @onChange={{action (mut this.model.watchedCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls--tracked-categories">
<label>{{d-icon "d-tracking"}} {{i18n "user.tracked_categories"}}</label>
{{#if this.canSee}}
<a class="show-tracking" href={{this.model.trackingTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{this.model.trackedCategories}} @blockedCategories={{this.selectedCategories}} @onChange={{action (mut this.model.trackedCategories)}} />
</div>
<div class="instructions">{{i18n "user.tracked_categories_instructions"}}</div>
<div class="controls tracking-controls tracking-controls--watched-first-categories">
<label>{{d-icon "d-watching-first"}} {{i18n "user.watched_first_post_categories"}}</label>
<CategorySelector @categories={{this.model.watchedFirstPostCategories}} @blockedCategories={{this.selectedCategories}} @onChange={{action (mut this.model.watchedFirstPostCategories)}} />
</div>
<div class="instructions">{{i18n "user.watched_first_post_categories_instructions"}}</div>
{{#if this.siteSettings.mute_all_categories_by_default}}
<div class="controls tracking-controls tracking-controls--regular-categories">
<label>{{d-icon "d-regular"}} {{i18n "user.regular_categories"}}</label>
<CategorySelector @categories={{this.model.regularCategories}} @blockedCategories={{this.selectedCategories}} @onChange={{action (mut this.model.regularCategories)}} />
</div>
<div class="instructions">{{i18n "user.regular_categories_instructions"}}</div>
{{else}}
<div class="controls tracking-controls tracking-controls--muted-categories">
<label>{{d-icon "d-muted"}} {{i18n "user.muted_categories"}}</label>
{{#if this.canSee}}
<a class="show-tracking" href={{this.model.mutedTopicsPath}}>{{i18n "user.tracked_topics_link"}}</a>
{{/if}}
<CategorySelector @categories={{this.model.mutedCategories}} @blockedCategories={{this.selectedCategories}} @onChange={{action (mut this.model.mutedCategories)}} />
</div>
<div class="instructions">{{i18n (if this.hideMutedTags "user.muted_categories_instructions" "user.muted_categories_instructions_dont_hide")}}</div>
{{/if}}
</div>
<PluginOutlet @name="user-preferences-categories" @tagName="span" @connectorTagName="div" @args={{hash model=this.model save=(action "save")}} />
<br>
<PluginOutlet @name="user-custom-controls" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
<UserPreferences::Categories
@canSee={{this.canSee}}
@model={{this.model}}
@selectedCategories={{this.selectedCategories}}
@hideMutedTags={{this.hideMutedTags}}
@save={{action "save"}}
@siteSettings={{this.siteSettings}}/>
{{#if this.canSave}}
<SaveControls @model={{this.model}} @action={{action "save"}} @saved={{this.saved}} />

View File

@ -1,20 +1,13 @@
<div class="control-group notifications">
<label class="control-label">{{i18n "user.notifications"}}</label>
<div class="controls controls-dropdown">
<label>{{i18n "user.new_topic_duration.label"}}</label>
<ComboBox @class="duration" @valueProperty="value" @content={{this.considerNewTopicOptions}} @value={{this.model.user_option.new_topic_duration_minutes}} @onChange={{action (mut this.model.user_option.new_topic_duration_minutes)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.auto_track_topics"}}</label>
<ComboBox @valueProperty="value" @content={{this.autoTrackDurations}} @value={{this.model.user_option.auto_track_topics_after_msecs}} @onChange={{action (mut this.model.user_option.auto_track_topics_after_msecs)}} />
</div>
<div class="controls controls-dropdown">
<label>{{i18n "user.notification_level_when_replying"}}</label>
<ComboBox @valueProperty="value" @content={{this.notificationLevelsForReplying}} @value={{this.model.user_option.notification_level_when_replying}} @onChange={{action (mut this.model.user_option.notification_level_when_replying)}} />
</div>
{{#unless this.currentUser.redesigned_user_page_nav_enabled}}
<UserPreferences::TopicTracking
@considerNewTopicOptions={{this.considerNewTopicOptions}}
@model={{this.model}}
@autoTrackDurations={{this.autoTrackDurations}}
@notificationLevelsForReplying={{this.notificationLevelsForReplying}} />
{{/unless}}
<div class="controls controls-dropdown">
<label>{{i18n "user.like_notification_frequency.title"}}</label>
@ -34,13 +27,9 @@
<UserNotificationSchedule @model={{this.model}} />
{{#if this.showMessageSettings}}
<div class="control-group private-messages">
<label class="control-label">{{i18n "user.private_messages"}}</label>
<div class="controls">
<PreferenceCheckbox @labelKey="user.allow_private_messages" @checked={{this.model.user_option.allow_private_messages}} />
</div>
</div>
{{#unless this.currentUser.redesigned_user_page_nav_enabled}}
<UserPreferences::AllowPrivateMessages @model={{this.model}} />
{{/unless}}
{{/if}}
<PluginOutlet @name="user-preferences-notifications" @tagName="span" @connectorTagName="div" @args={{hash model=this.model save=(action "save")}} />

View File

@ -71,6 +71,10 @@
</div>
{{/if}}
{{#if this.currentUser.redesigned_user_page_nav_enabled}}
<UserPreferences::UserApiKeys @model={{@model}}/>
{{/if}}
<PluginOutlet @name="user-preferences-security" @tagName="span" @connectorTagName="div" @args={{hash model=this.model save=(action "save")}} />
<br>

View File

@ -1,50 +1,7 @@
{{#if this.siteSettings.tagging_enabled}}
<div class="control-group tag-notifications">
<label class="control-label">{{i18n "user.tag_settings"}}</label>
<div class="controls tracking-controls">
<label>{{d-icon "d-watching" class="icon watching"}} {{i18n "user.watched_tags"}}</label>
<TagChooser @tags={{this.model.watched_tags}} @blockedTags={{this.selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.watched_tags_instructions"}}</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-tracking" class="icon tracking"}} {{i18n "user.tracked_tags"}}</label>
<TagChooser @tags={{this.model.tracked_tags}} @blockedTags={{this.selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.tracked_tags_instructions"}}</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-watching-first" class="icon watching-first-post"}} {{i18n "user.watched_first_post_tags"}}</label>
<TagChooser @tags={{this.model.watching_first_post_tags}} @blockedTags={{this.selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">
{{i18n "user.watched_first_post_tags_instructions"}}
</div>
<div class="controls tracking-controls">
<label>{{d-icon "d-muted" class="icon muted"}} {{i18n "user.muted_tags"}}</label>
<TagChooser @tags={{this.model.muted_tags}} @blockedTags={{this.selectedTags}} @everyTag={{true}} @unlimitedTagCount={{true}} @options={{hash
allowAny=false
}} />
</div>
<div class="instructions">{{i18n "user.muted_tags_instructions"}}</div>
</div>
<PluginOutlet @name="user-preferences-tags" @tagName="span" @connectorTagName="div" @args={{hash model=this.model save=(action "save")}} />
<br>
<PluginOutlet @name="user-custom-controls" @tagName="span" @connectorTagName="div" @args={{hash model=this.model}} />
<UserPreferences::Tags
@model={{this.model}}
@selectedTags={{this.selectedTags}}
@save={{action "save"}}
@siteSettings={{this.siteSettings}}/>
<SaveControls @model={{this.model}} @action={{action "save"}} @saved={{this.saved}} />
{{/if}}

View File

@ -0,0 +1,29 @@
<DSection @pageClass="user-preferences-tracking" />
<div class="user-preferences_tracking-topics-wrapper">
<label class="control-label">{{i18n "user.topics_settings"}}</label>
<UserPreferences::TopicTracking
@considerNewTopicOptions={{this.considerNewTopicOptions}}
@model={{this.model}}
@autoTrackDurations={{this.autoTrackDurations}}
@notificationLevelsForReplying={{this.notificationLevelsForReplying}} />
</div>
<div class="user-preferences_tracking-categories-tags-wrapper">
<UserPreferences::Categories
@canSee={{this.canSee}}
@model={{this.model}}
@selectedCategories={{this.selectedCategories}}
@hideMutedTags={{this.hideMutedTags}}
@siteSettings={{this.siteSettings}} />
<UserPreferences::Tags
@model={{this.model}}
@selectedTags={{this.selectedTags}}
@save={{this.save}}
@siteSettings={{this.siteSettings}} />
</div>
{{#if this.canSave}}
<SaveControls @model={{this.model}} @action={{this.save}} @saved={{this.saved}} />
{{/if}}

View File

@ -21,7 +21,11 @@
<div class="instructions">{{i18n "user.muted_users_instructions"}}</div>
</div>
{{#if this.showMessageSettings}}
{{#if this.currentUser.can_send_private_messages}}
{{#if this.currentUser.redesigned_user_page_nav_enabled}}
<UserPreferences::AllowPrivateMessages @model={{this.model}} />
{{/if}}
<div class="control-group user-allow-pm">
<div class="controls">
<PreferenceCheckbox @labelKey="user.allow_private_messages_from_specific_users" @checked={{this.model.user_option.enable_allowed_pm_users}} @disabled={{this.disableAllowPmUsersSetting}} />

View File

@ -91,6 +91,7 @@ acceptance("User Preferences", function (needs) {
"/u/eviltrout/preferences/account",
"defaults to account tab"
);
assert.ok(exists(".user-preferences"), "it shows the preferences");
const savePreferences = async () => {
@ -112,25 +113,32 @@ acceptance("User Preferences", function (needs) {
await savePreferences();
await click(".preferences-nav .nav-notifications a");
await selectKit(
".control-group.notifications .combo-box.duration"
).expand();
await selectKit(
".control-group.notifications .combo-box.duration"
).selectRowByValue(1440);
await savePreferences();
await click(".preferences-nav .nav-categories a");
const categorySelector = selectKit(
".tracking-controls .category-selector "
);
await categorySelector.expand();
await categorySelector.fillInFilter("faq");
await savePreferences();
this.siteSettings.tagging_enabled = false;
await visit("/");
await visit("/u/eviltrout/preferences");
assert.ok(
!exists(".preferences-nav .nav-tags a"),
"tags tab isn't there when tags are disabled"
@ -564,7 +572,7 @@ acceptance("Ignored users", function (needs) {
});
});
acceptance("Security", function (needs) {
acceptance("User Preferences - Security", function (needs) {
needs.user();
needs.pretender(preferencesPretender);

View File

@ -0,0 +1,60 @@
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance(
"New User Menu - Horizontal nav and preferences relocation",
function (needs) {
needs.user({
redesigned_user_page_nav_enabled: true,
user_api_keys: [
{
id: 1,
application_name: "Discourse Hub",
scopes: ["Read and clear notifications"],
created_at: "2020-11-14T00:57:09.093Z",
last_used_at: "2022-09-22T18:55:41.672Z",
},
],
});
test("Horizontal user nav exists", async function (assert) {
await visit("/u/eviltrout/preferences");
assert.ok(exists(".horizontal-overflow-nav"), "horizontal nav exists");
});
test("User Tracking page exists", async function (assert) {
await visit("/u/eviltrout/preferences");
assert.ok(exists(".nav-tracking"), "the new tracking page link exists");
});
test("User Categories page no longer exists", async function (assert) {
await visit("/u/eviltrout/preferences");
assert.ok(!exists(".nav-categories"), "Categories tab no longer exists");
});
test("Can save category tracking preferences", async function (assert) {
await visit("/u/eviltrout/preferences/tracking");
const categorySelector = selectKit(
".tracking-controls .category-selector"
);
const savePreferences = async () => {
assert.notOk(exists(".saved"), "it hasn't been saved yet");
await click(".save-changes");
assert.ok(exists(".saved"), "it displays the saved message");
};
await categorySelector.expand();
await categorySelector.fillInFilter("faq");
await savePreferences();
});
test("Can view user api keys on security page", async function (assert) {
await visit("/u/eviltrout/preferences/security");
assert.ok(exists(".control-group.apps"), "User can see apps section");
});
}
);

View File

@ -184,3 +184,26 @@
margin: 1em 0;
}
}
.user-preferences-tracking-page {
.user-preferences .form-vertical {
width: 100%;
}
}
.user-preferences_tracking-topics-wrapper {
margin-bottom: 3em;
.control-label {
margin-bottom: 1em;
}
}
.user-preferences_tracking-categories-tags-wrapper {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(16em, 1fr));
gap: 2em;
.control-group {
width: 100%;
min-width: 16em;
}
}

View File

@ -1326,6 +1326,7 @@ en:
profile: "Profile"
emails: "Emails"
notifications: "Notifications"
tracking: "Tracking"
categories: "Categories"
users: "Users"
tags: "Tags"
@ -1604,6 +1605,7 @@ en:
other_settings: "Other"
categories_settings: "Categories"
topics_settings: "Topics"
new_topic_duration:
label: "Consider topics new when"

View File

@ -494,6 +494,7 @@ Discourse::Application.routes.draw do
put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", format: :json, constraints: { username: RouteFormat.username }
delete "#{root_path}/:username/preferences/email" => "users#destroy_email", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/tracking" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/users" => "users#preferences", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormat.username }