DEV: Ship first pass of new user page navigation behind feature flag (#18285)

This commits introduces a new SiteSetting.enable_new_user_profile_nav_groups
feature flag. When configured, users of the configured groups will see
the new user page navigation links.

As of this commit, only the user activity navigation link has been
converted to the newly proposed dropdown of navigation links.

Mobile support has not been considered.
This commit is contained in:
Alan Guo Xiang Tan 2022-09-21 12:32:47 +08:00 committed by GitHub
parent 1413de2809
commit da3e72c2b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 414 additions and 79 deletions

View File

@ -0,0 +1,100 @@
<section class="user-primary-navigation">
<ul class="main-nav nav nav-pills user-nav">
{{#unless @user.profile_hidden}}
<li class="summary">
<LinkTo @route="user.summary">
{{d-icon "user"}}
{{i18n "user.summary.title"}}
</LinkTo>
</li>
<UserNav::DropdownList
@icon="stream"
@text={{i18n "user.activity_stream"}}
@isActive={{eq @currentParentRoute "userActivity"}}
@class="user-activity" >
<:submenu>
<DNavigationItem @route="userActivity.index">{{i18n "user.filters.all"}}</DNavigationItem>
<DNavigationItem @route="userActivity.topics">{{i18n "user_action_groups.4"}}</DNavigationItem>
<DNavigationItem @route="userActivity.replies">{{i18n "user_action_groups.5"}}</DNavigationItem>
{{#if @showRead}}
<DNavigationItem @route="userActivity.read" @title={{i18n "user.read_help"}}>
{{i18n "user.read"}}
</DNavigationItem>
{{/if}}
{{#if @showDrafts}}
<DNavigationItem @route="userActivity.drafts">
{{this.draftLabel}}
</DNavigationItem>
{{/if}}
{{#if (gt @user.pending_posts_count 0)}}
<DNavigationItem @route="userActivity.pending">
{{this.pendingLabel}}
</DNavigationItem>
{{/if}}
<DNavigationItem @route="userActivity.likesGiven">{{i18n "user_action_groups.1"}}</DNavigationItem>
{{#if @showBookmarks}}
<DNavigationItem @route="userActivity.bookmarks">{{i18n "user_action_groups.3"}}</DNavigationItem>
{{/if}}
<PluginOutlet @name="user-activity-bottom" @tagName="span" @connectorTagName="li" @args={{hash model=@user}} />
</:submenu>
</UserNav::DropdownList>
{{/unless}}
{{#if @showNotificationsTab}}
<li class="user-notifications">
<LinkTo @route="userNotifications">
{{d-icon "comment" class="glyph"}}{{i18n "user.notifications"}}
</LinkTo>
</li>
{{/if}}
{{#if @showPrivateMessages}}
<li class="private-messages">
<LinkTo @route="userPrivateMessages">
{{d-icon "far-envelope"}}
{{i18n "user.private_messages"}}
</LinkTo>
</li>
{{/if}}
{{#if @canInviteToForum}}
<li class="invited">
<LinkTo @route="userInvited">
{{d-icon "user-plus"}}
{{i18n "user.invited.title"}}
</LinkTo>
</li>
{{/if}}
{{#if @showBadges}}
<li class="badges">
<LinkTo @route="user.badges">
{{d-icon "certificate"}}
{{i18n "badges.title"}}
</LinkTo>
</li>
{{/if}}
<PluginOutlet
@name="user-main-nav"
@connectorTagName="li"
@args={{hash model=@user}} />
{{#if @user.can_edit}}
<li class="preferences">
<LinkTo @route="preferences">
{{d-icon "cog"}}
{{i18n "user.preferences"}}
</LinkTo>
</li>
{{/if}}
</ul>
</section>

View File

@ -0,0 +1,18 @@
import I18n from "I18n";
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
export default class UserNav extends Component {
@service currentUser;
@service site;
@service router;
get draftLabel() {
const count = this.currentUser.draft_count;
return count > 0
? I18n.t("drafts.label_with_count", { count })
: I18n.t("drafts.label");
}
}

View File

@ -0,0 +1,18 @@
<li class={{concat @class " user-nav-dropdown-list-item"}}>
<button type="button" class={{this.buttonClass}} {{on "click" this.toggleList}}>
{{d-icon @icon}}
<span>{{@text}}</span>
{{d-icon this.chevron class="user-nav-dropdown-chevron"}}
</button>
{{#if (and (has-block "submenu") this.displayList)}}
<div class="user-nav-dropdown-submenu-wrapper"
{{did-insert this.registerClickListener}}
{{will-destroy this.deregisterClickListener}} >
<ul class={{concat @submenuClass " user-nav-dropdown-submenu"}}>
{{yield to="submenu"}}
</ul>
</div>
{{/if}}
</li>

View File

@ -0,0 +1,54 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { bind } from "discourse-common/utils/decorators";
export default class UserNavDropdownList extends Component {
@tracked displayList = false;
get chevron() {
return this.displayList ? "chevron-up" : "chevron-down";
}
get defaultButtonClass() {
return "user-nav-dropdown-button";
}
get buttonClass() {
const props = [this.defaultButtonClass];
if (this.args.isActive) {
props.push("active");
}
return props.join(" ");
}
@action
toggleList() {
this.displayList = !this.displayList;
}
@bind
collapseList(e) {
const isClickOnButton = e.composedPath().some((element) => {
if (element?.classList?.contains(this.defaultButtonClass)) {
return true;
}
});
if (!isClickOnButton) {
this.displayList = false;
}
}
@action
registerClickListener() {
document.addEventListener("click", this.collapseList);
}
@action
deregisterClickListener() {
document.removeEventListener("click", this.collapseList);
}
}

View File

@ -1,6 +1,6 @@
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import EmberObject, { computed, set } from "@ember/object"; import EmberObject, { computed, set } from "@ember/object";
import { and, equal, gt, not, or } from "@ember/object/computed"; import { and, equal, gt, not, or, readOnly } from "@ember/object/computed";
import CanCheckEmails from "discourse/mixins/can-check-emails"; import CanCheckEmails from "discourse/mixins/can-check-emails";
import User from "discourse/models/user"; import User from "discourse/models/user";
import I18n from "I18n"; import I18n from "I18n";
@ -164,6 +164,8 @@ export default Controller.extend(CanCheckEmails, {
} }
}, },
currentParentRoute: readOnly("router.currentRoute.parent.name"),
userNotificationLevel: computed( userNotificationLevel: computed(
"currentUser.ignored_ids", "currentUser.ignored_ids",
"model.ignored", "model.ignored",

View File

@ -224,47 +224,63 @@
{{/unless}} {{/unless}}
</div> </div>
</section> </section>
<div class='user-content-wrapper'>
<section class="user-primary-navigation">
<MobileNav @class="main-nav" @desktopClass="nav nav-pills user-nav">
{{#unless this.model.profile_hidden}}
<li class="summary">
<LinkTo @route="user.summary">
{{d-icon "user"}}
{{i18n 'user.summary.title'}}
</LinkTo>
</li>
<li class="activity"> {{#if this.currentUser.redesigned_user_page_nav_enabled}}
<LinkTo @route="userActivity"> <div class="new-user-wrapper">
{{d-icon "stream"}} <UserNav
{{i18n 'user.activity_stream'}} @user={{this.model}}
</LinkTo> @showNotificationsTab={{this.showNotificationsTab}}
</li> @showPrivateMessages={{this.showPrivateMessages}}
{{/unless}} @canInviteToForum={{this.canInviteToForum}}
{{#if this.showNotificationsTab}} @showBadges={{this.showBadges}}
<li class="user-notifications"> @currentParentRoute={{this.currentParentRoute}}
<LinkTo @route="userNotifications"> @showRead={{this.showRead}}
{{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}} @showDrafts={{this.showDrafts}}
</LinkTo> @showBookmarks={{this.showBookmarks}} />
</li>
{{/if}} <div class="new-user-content-wrapper">
{{#if this.showPrivateMessages}} {{outlet}}
<li class="private-messages"><LinkTo @route="userPrivateMessages">{{d-icon "far-envelope"}}{{i18n 'user.private_messages'}}</LinkTo></li> </div>
{{/if}} </div>
{{#if this.canInviteToForum}} {{else}}
<li class="invited"><LinkTo @route="userInvited">{{d-icon "user-plus"}}{{i18n 'user.invited.title'}}</LinkTo></li> <div class='user-content-wrapper'>
{{/if}} <section class="user-primary-navigation">
{{#if this.showBadges}} <MobileNav @class="main-nav" @desktopClass="nav nav-pills user-nav">
<li class="badges"><LinkTo @route="user.badges">{{d-icon "certificate"}}{{i18n 'badges.title'}}</LinkTo></li> {{#unless this.model.profile_hidden}}
{{/if}} <li class="summary"><LinkTo @route="user.summary">{{i18n 'user.summary.title'}}</LinkTo></li>
<PluginOutlet @name="user-main-nav" @connectorTagName="li" @args={{hash model=this.model}} /> <li class="activity"><LinkTo @route="userActivity">{{i18n 'user.activity_stream'}}</LinkTo></li>
{{#if this.model.can_edit}} {{/unless}}
<li class="preferences"><LinkTo @route="preferences">{{d-icon "cog"}}{{i18n 'user.preferences'}}</LinkTo></li>
{{/if}} {{#if this.showNotificationsTab}}
</MobileNav> <li class="user-notifications">
</section> <LinkTo @route="userNotifications">
{{outlet}} {{d-icon "comment" class="glyph"}}{{i18n 'user.notifications'}}
</div> </LinkTo>
</li>
{{/if}}
{{#if this.showPrivateMessages}}
<li class="private-messages"><LinkTo @route="userPrivateMessages">{{d-icon "far-envelope"}}{{i18n 'user.private_messages'}}</LinkTo></li>
{{/if}}
{{#if this.canInviteToForum}}
<li class="invited"><LinkTo @route="userInvited">{{d-icon "user-plus"}}{{i18n 'user.invited.title'}}</LinkTo></li>
{{/if}}
{{#if this.showBadges}}
<li class="badges"><LinkTo @route="user.badges">{{d-icon "certificate"}}{{i18n 'badges.title'}}</LinkTo></li>
{{/if}}
<PluginOutlet @name="user-main-nav" @connectorTagName="li" @args={{hash model=this.model}} />
{{#if this.model.can_edit}}
<li class="preferences"><LinkTo @route="preferences">{{d-icon "cog"}}{{i18n 'user.preferences'}}</LinkTo></li>
{{/if}}
</MobileNav>
</section>
{{outlet}}
</div>
{{/if}}
</DSection> </DSection>
</div> </div>

View File

@ -1,43 +1,46 @@
<DSection @pageClass="user-activity" @class="user-secondary-navigation" @scrollTop={{false}}> {{#unless this.currentUser.redesigned_user_page_nav_enabled}}
<nav role="navigation"> <DSection @pageClass="user-activity" @class="user-secondary-navigation" @scrollTop={{false}}>
<MobileNav @class="activity-nav" @desktopClass="action-list activity-list nav-stacked"> <nav role="navigation">
<DNavigationItem @route="userActivity.index">{{i18n "user.filters.all"}}</DNavigationItem> <MobileNav @class="activity-nav" @desktopClass="action-list activity-list nav-stacked">
<DNavigationItem @route="userActivity.topics">{{i18n "user_action_groups.4"}}</DNavigationItem> <DNavigationItem @route="userActivity.index">{{i18n "user.filters.all"}}</DNavigationItem>
<DNavigationItem @route="userActivity.replies">{{i18n "user_action_groups.5"}}</DNavigationItem> <DNavigationItem @route="userActivity.topics">{{i18n "user_action_groups.4"}}</DNavigationItem>
<DNavigationItem @route="userActivity.replies">{{i18n "user_action_groups.5"}}</DNavigationItem>
{{#if this.user.showRead}} {{#if this.user.showRead}}
<DNavigationItem @route="userActivity.read" @title={{i18n "user.read_help"}}> <DNavigationItem @route="userActivity.read" @title={{i18n "user.read_help"}}>
{{i18n "user.read"}} {{i18n "user.read"}}
</DNavigationItem> </DNavigationItem>
{{/if}} {{/if}}
{{#if this.user.showDrafts}} {{#if this.user.showDrafts}}
<DNavigationItem @route="userActivity.drafts"> <DNavigationItem @route="userActivity.drafts">
{{this.draftLabel}} {{this.draftLabel}}
</DNavigationItem> </DNavigationItem>
{{/if}} {{/if}}
{{#if (gt this.model.pending_posts_count 0)}} {{#if (gt this.model.pending_posts_count 0)}}
<DNavigationItem @route="userActivity.pending"> <DNavigationItem @route="userActivity.pending">
{{this.pendingLabel}} {{this.pendingLabel}}
</DNavigationItem> </DNavigationItem>
{{/if}} {{/if}}
<DNavigationItem @route="userActivity.likesGiven">{{i18n "user_action_groups.1"}}</DNavigationItem> <DNavigationItem @route="userActivity.likesGiven">{{i18n "user_action_groups.1"}}</DNavigationItem>
{{#if this.user.showBookmarks}} {{#if this.user.showBookmarks}}
<DNavigationItem @route="userActivity.bookmarks">{{i18n "user_action_groups.3"}}</DNavigationItem> <DNavigationItem @route="userActivity.bookmarks">{{i18n "user_action_groups.3"}}</DNavigationItem>
{{/if}} {{/if}}
<PluginOutlet @name="user-activity-bottom" @tagName="span" @connectorTagName="li" @args={{hash model=this.model}} /> <PluginOutlet @name="user-activity-bottom" @tagName="span" @connectorTagName="li" @args={{hash model=this.model}} />
</MobileNav> </MobileNav>
</nav> </nav>
</DSection> </DSection>
{{#if this.canDownloadPosts}}
<section class="user-additional-controls"> {{#if this.canDownloadPosts}}
<DButton @action={{action "exportUserArchive"}} @class="btn-default" @label="user.download_archive.button_text" @icon="download" /> <section class="user-additional-controls">
</section> <DButton @action={{action "exportUserArchive"}} @class="btn-default" @label="user.download_archive.button_text" @icon="download" />
{{/if}} </section>
{{/if}}
{{/unless}}
<section class="user-content"> <section class="user-content">
{{outlet}} {{outlet}}

View File

@ -32,6 +32,7 @@
@import "magnific-popup"; @import "magnific-popup";
@import "menu-panel"; @import "menu-panel";
@import "modal"; @import "modal";
@import "new-user";
@import "not-found"; @import "not-found";
@import "onebox"; @import "onebox";
@import "personal-message"; @import "personal-message";

View File

@ -0,0 +1,81 @@
.new-user-wrapper {
.new-user-content-wrapper {
// Grid layout
width: 100%;
display: grid;
grid-template-columns: 1fr 5fr;
grid-template-rows: auto 1fr;
grid-gap: 20px;
.user-secondary-navigation {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
}
.user-content {
grid-column-start: 1;
grid-column-end: 3;
grid-row-start: 1;
grid-row-end: 3;
}
.user-additional-controls {
align-self: start;
justify-self: start;
grid-row-start: 2;
}
.user-secondary-navigation ~ .user-content {
grid-column-start: 2;
grid-column-end: 3;
}
}
.user-nav-dropdown-list-item {
position: relative;
}
.user-nav-dropdown-button {
background: transparent;
}
.user-nav-dropdown-submenu-wrapper {
position: absolute;
top: 2em;
min-width: 10em;
padding: 0;
box-shadow: shadow("dropdown");
z-index: z("dropdown");
}
.user-nav-dropdown-submenu {
background: var(--secondary);
list-style-type: none;
margin: 0;
li a {
padding: 0.5em 1em;
color: var(--primary);
.discourse-no-touch & {
&:hover {
background: var(--highlight-medium);
color: currentColor;
}
}
&.active {
background: var(--tertiary-low);
color: currentColor;
}
&:first-of-type {
padding-top: 0.5em;
}
&:last-of-type {
padding-bottom: 0.5em;
}
}
}
}

View File

@ -29,7 +29,8 @@
display: flex; display: flex;
margin-right: 0.5em; margin-right: 0.5em;
> a { > a,
button {
border: none; border: none;
padding: 6px 12px; padding: 6px 12px;
color: var(--primary); color: var(--primary);
@ -52,7 +53,8 @@
} }
} }
a.active { a.active,
button.active {
color: var(--secondary); color: var(--secondary);
background-color: var(--quaternary); background-color: var(--quaternary);

View File

@ -78,13 +78,15 @@ class CurrentUserSerializer < BasicUserSerializer
:sidebar_category_ids, :sidebar_category_ids,
:likes_notifications_disabled, :likes_notifications_disabled,
:grouped_unread_notifications, :grouped_unread_notifications,
:redesigned_user_menu_enabled :redesigned_user_menu_enabled,
:redesigned_user_page_nav_enabled
delegate :user_stat, to: :object, private: true delegate :user_stat, to: :object, private: true
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
def groups def groups
owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set
object.visible_groups.pluck(:id, :name, :has_messages).map do |id, name, has_messages| object.visible_groups.pluck(:id, :name, :has_messages).map do |id, name, has_messages|
group = { id: id, name: name, has_messages: has_messages } group = { id: id, name: name, has_messages: has_messages }
group[:owner] = true if owned_group_ids.include?(id) group[:owner] = true if owned_group_ids.include?(id)
@ -342,4 +344,12 @@ class CurrentUserSerializer < BasicUserSerializer
def include_unseen_reviewable_count? def include_unseen_reviewable_count?
redesigned_user_menu_enabled redesigned_user_menu_enabled
end end
def redesigned_user_page_nav_enabled
if SiteSetting.enable_new_user_profile_nav_groups.present?
GroupUser.exists?(user_id: object.id, group_id: SiteSetting.enable_new_user_profile_nav_groups.split("|"))
else
false
end
end
end end

View File

@ -2002,6 +2002,14 @@ developer:
type: tag_list type: tag_list
default: "" default: ""
client: true client: true
enable_new_user_profile_nav_groups:
client: true
type: group_list
list_type: compact
default: ""
allow_any: false
refresh: true
hidden: true
embedding: embedding:
embed_by_username: embed_by_username:

View File

@ -350,4 +350,26 @@ RSpec.describe CurrentUserSerializer do
expect(serializer.as_json[:likes_notifications_disabled]).to eq(false) expect(serializer.as_json[:likes_notifications_disabled]).to eq(false)
end end
end end
describe '#redesigned_user_page_nav_enabled' do
fab!(:group) { Fabricate(:group) }
fab!(:group2) { Fabricate(:group) }
it "is false when enable_new_user_profile_nav_groups site setting has not been set" do
expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(false)
end
it 'is false if user does not belong to any of the configured groups in the enable_new_user_profile_nav_groups site setting' do
SiteSetting.enable_new_user_profile_nav_groups = "#{group.id}|#{group2.id}"
expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(false)
end
it 'is true if user belongs one of the configured groups in the enable_new_user_profile_nav_groups site setting' do
SiteSetting.enable_new_user_profile_nav_groups = "#{group.id}|#{group2.id}"
group.add(user)
expect(serializer.as_json[:redesigned_user_page_nav_enabled]).to eq(true)
end
end
end end