UX: Remove section heading for community section (#22405)

Why is this change being made?

We've decided that the previous "community" section should look more
like a primary section that holds the most important navigation links
for the site and the word "community" doesn't quite fit that
description. Therefore, we've made the decision to drop the
section heading for the community section. 

As part of removing the section heading, the following changes are made
as well:

1. Button to customize the section has been moved to the "footer" of the
   "More..." section when `navigation_menu` site setting is set to `sidebar`. 
   When `navigation_menu` is set to `header dropdown`, a button to customize 
   the section is shown inline.

2. The section will no longer be collapsable.

3. The title of the section is no longer customisable as it is no longer
   displayed. As a technical note, we have not dropped any previous
   customisations of the section's title previously in case we have to
   bring back the header in the future.

4. The new topic button that was previously present in the header has
   been removed alongside the header. Admins can add a custom section
   link to the `/new-topic` route if there would like to make it easier for
   users to create a new topic in the sidebar.
This commit is contained in:
Alan Guo Xiang Tan
2023-07-11 09:40:37 +08:00
committed by GitHub
parent 41c3b42412
commit ab053ac669
36 changed files with 561 additions and 432 deletions

View File

@ -6,38 +6,46 @@
class="sidebar-section-form-modal"
>
<:body>
<form class="form-horizontal">
<div class="input-group">
<label for="section-name">{{i18n
"sidebar.sections.custom.title.label"
}}</label>
<Input
name="section-name"
@type="text"
@value={{this.transformedModel.title}}
class={{this.transformedModel.titleCssClass}}
{{on
"input"
(action (mut this.transformedModel.title) value="target.value")
}}
/>
{{#if this.transformedModel.invalidTitleMessage}}
<div class="title warning">
{{this.transformedModel.invalidTitleMessage}}
</div>
{{/if}}
</div>
<form class="form-horizontal sidebar-section-form">
{{#unless this.transformedModel.hideTitleInput}}
<div class="sidebar-section-form__input-wrapper">
<label for="section-name">
{{i18n "sidebar.sections.custom.title.label"}}
</label>
<Input
name="section-name"
@type="text"
@value={{this.transformedModel.title}}
class={{this.transformedModel.titleCssClass}}
{{on
"input"
(action (mut this.transformedModel.title) value="target.value")
}}
/>
{{#if this.transformedModel.invalidTitleMessage}}
<div class="title warning">
{{this.transformedModel.invalidTitleMessage}}
</div>
{{/if}}
</div>
{{/unless}}
<div class="row-wrapper header">
<div class="input-group link-icon">
<label>{{i18n "sidebar.sections.custom.links.icon.label"}}</label>
</div>
<div class="input-group link-name">
<label>{{i18n "sidebar.sections.custom.links.name.label"}}</label>
</div>
<div class="input-group link-url">
<label>{{i18n "sidebar.sections.custom.links.value.label"}}</label>
</div>
</div>
{{#each this.activeLinks as |link|}}
<Sidebar::SectionFormLink
@link={{link}}

View File

@ -29,6 +29,7 @@ class Section {
id,
publicSection,
sectionType,
hideTitleInput,
}) {
this.title = title;
this.public = publicSection;
@ -36,6 +37,7 @@ class Section {
this.links = links;
this.secondaryLinks = secondaryLinks;
this.id = id;
this.hideTitleInput = hideTitleInput;
}
get valid() {
@ -243,26 +245,29 @@ export default class SidebarSectionForm extends Component {
@cached
get transformedModel() {
if (this.model) {
const section = this.model?.section;
if (section) {
return new Section({
title: this.model.title,
publicSection: this.model.public,
sectionType: this.model.section_type,
links: this.model.links.reduce((acc, link) => {
title: section.title,
publicSection: section.public,
sectionType: section.section_type,
links: section.links.reduce((acc, link) => {
if (link.segment === "primary") {
this.nextObjectId++;
acc.push(this.initLink(link));
}
return acc;
}, A()),
secondaryLinks: this.model.links.reduce((acc, link) => {
secondaryLinks: section.links.reduce((acc, link) => {
if (link.segment === "secondary") {
this.nextObjectId++;
acc.push(this.initLink(link));
}
return acc;
}, A()),
id: this.model.id,
id: section.id,
hideTitleInput: this.model.hideSectionHeader,
});
} else {
return new Section({

View File

@ -6,6 +6,7 @@
@headerActions={{this.section.headerActions}}
@headerActionsIcon={{this.section.headerActionIcon}}
@class={{this.section.dragCss}}
@hideSectionHeader={{this.section.hideSectionHeader}}
>
{{#each this.section.links as |link|}}
{{#if link.externalOrFullReload}}
@ -61,12 +62,25 @@
{{/each}}
{{#if this.section.moreLinks}}
{{#if this.isDesktopDropdownMode}}
{{#if this.navigationMenu.isDesktopDropdownMode}}
{{#each this.section.moreLinks as |sectionLink|}}
<Sidebar::MoreSectionLink @sectionLink={{sectionLink}} />
{{/each}}
{{#if this.section.moreSectionButtonAction}}
<Sidebar::SectionLinkButton
@action={{this.section.moreSectionButtonAction}}
@icon={{this.section.moreSectionButtonIcon}}
@text={{this.section.moreSectionButtonText}}
/>
{{/if}}
{{else if this.section.moreLinks}}
<Sidebar::MoreSectionLinks @sectionLinks={{this.section.moreLinks}} />
<Sidebar::MoreSectionLinks
@sectionLinks={{this.section.moreLinks}}
@moreButtonAction={{this.section.moreSectionButtonAction}}
@moreButtonText={{this.section.moreSectionButtonText}}
@moreButtonIcon={{this.section.moreSectionButtonIcon}}
/>
{{/if}}
{{/if}}
</Sidebar::Section>

View File

@ -4,9 +4,12 @@ import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
import Section from "discourse/lib/sidebar/section";
import CommunitySection from "discourse/lib/sidebar/community-section";
import AdminCommunitySection from "discourse/lib/sidebar/user/community-section/admin-section";
import CommonCommunitySection from "discourse/lib/sidebar/common/community-section/section";
export default class SidebarCustomSection extends Component {
@service currentUser;
@service navigationMenu;
@service site;
@service siteSettings;
@ -22,19 +25,14 @@ export default class SidebarCustomSection extends Component {
super.willDestroy();
}
get isDesktopDropdownMode() {
const headerDropdownMode =
this.siteSettings.navigation_menu === "header dropdown";
return !this.site.mobileView && headerDropdownMode;
}
#initializeSection() {
let sectionClass = Section;
switch (this.args.sectionData.section_type) {
case "community":
sectionClass = CommunitySection;
sectionClass = this.currentUser?.admin
? AdminCommunitySection
: CommonCommunitySection;
break;
}

View File

@ -20,11 +20,21 @@
>
<div class="sidebar-more-section-links-details-content">
<div class="sidebar-more-section-links-details-content-main">
<ul class="sidebar-more-section-links-details-content-main">
{{#each this.sectionLinks as |sectionLink|}}
<Sidebar::MoreSectionLink @sectionLink={{sectionLink}} />
{{/each}}
</div>
</ul>
{{#if @moreButtonAction}}
<div class="sidebar-more-section-links-details-content-footer">
<Sidebar::SectionLinkButton
@action={{@moreButtonAction}}
@icon={{@moreButtonIcon}}
@text={{@moreButtonText}}
/>
</div>
{{/if}}
</div>
</div>
</div>

View File

@ -48,9 +48,9 @@ export default class SidebarMoreSectionLinks extends Component {
@bind
closeDetails(event) {
if (this.open) {
const isLinkClick = event.target.className.includes(
"sidebar-section-link"
);
const isLinkClick =
event.target.className.includes("sidebar-section-link") ||
event.target.className.includes("sidebar-section-link-button");
if (isLinkClick || this.#isOutsideDetailsClick(event)) {
this.open = false;

View File

@ -0,0 +1,15 @@
<div class="sidebar-section-link-wrapper">
<button
type="button"
class="btn btn-flat sidebar-section-link-button sidebar-row"
{{on "click" @action}}
>
<span class="sidebar-section-link-prefix icon">
{{d-icon @icon}}
</span>
<span class="sidebar-section-link-content-text">
{{@text}}
</span>
</button>
</div>

View File

@ -3,54 +3,59 @@
class={{concat-class "sidebar-section-wrapper sidebar-section" @class}}
data-section-name={{@sectionName}}
>
<div class="sidebar-section-header-wrapper sidebar-row">
<Sidebar::SectionHeader
@collapsable={{@collapsable}}
@sidebarSectionContentID={{this.sidebarSectionContentID}}
@toggleSectionDisplay={{this.toggleSectionDisplay}}
@isExpanded={{this.displaySectionContent}}
>
{{#if @collapsable}}
<span class="sidebar-section-header-caret">
{{d-icon this.headerCaretIcon}}
{{#unless @hideSectionHeader}}
<div class="sidebar-section-header-wrapper sidebar-row">
<Sidebar::SectionHeader
@collapsable={{@collapsable}}
@sidebarSectionContentID={{this.sidebarSectionContentID}}
@toggleSectionDisplay={{this.toggleSectionDisplay}}
@isExpanded={{this.displaySectionContent}}
>
{{#if @collapsable}}
<span class="sidebar-section-header-caret">
{{d-icon this.headerCaretIcon}}
</span>
{{/if}}
<span class="sidebar-section-header-text">
{{@headerLinkText}}
</span>
{{#if @indicatePublic}}
<span class="sidebar-section-header-global-indicator">
{{d-icon "globe"}}
<DTooltip @placement="top">{{d-icon "shield-alt"}}
{{i18n "sidebar.sections.global_section"}}
</DTooltip>
</span>
{{/if}}
</Sidebar::SectionHeader>
{{#if this.isSingleHeaderAction}}
{{#each @headerActions as |headerAction|}}
<button
type="button"
class="sidebar-section-header-button"
{{on "click" headerAction.action}}
title={{headerAction.title}}
>
{{d-icon @headerActionsIcon}}
</button>
{{/each}}
{{/if}}
<span class="sidebar-section-header-text">
{{@headerLinkText}}
</span>
{{#if @indicatePublic}}
<span class="sidebar-section-header-global-indicator">
{{d-icon "globe"}}
<DTooltip @placement="top">{{d-icon "shield-alt"}}
{{i18n "sidebar.sections.global_section"}}
</DTooltip>
</span>
{{#if this.isMultipleHeaderActions}}
<DropdownSelectBox
@options={{hash
icon=@headerActionsIcon
placementStrategy="absolute"
}}
@content={{@headerActions}}
@onChange={{action "handleMultipleHeaderActions"}}
@class="sidebar-section-header-dropdown"
/>
{{/if}}
</Sidebar::SectionHeader>
{{#if this.isSingleHeaderAction}}
{{#each @headerActions as |headerAction|}}
<button
type="button"
class="sidebar-section-header-button"
{{on "click" headerAction.action}}
title={{headerAction.title}}
>
{{d-icon @headerActionsIcon}}
</button>
{{/each}}
{{/if}}
{{#if this.isMultipleHeaderActions}}
<DropdownSelectBox
@options={{hash icon=@headerActionsIcon placementStrategy="absolute"}}
@content={{@headerActions}}
@onChange={{action "handleMultipleHeaderActions"}}
@class="sidebar-section-header-dropdown"
/>
{{/if}}
</div>
</div>
{{/unless}}
{{#if this.displaySectionContent}}
<ul class="sidebar-section-content" id={{this.sidebarSectionContentID}}>

View File

@ -1,12 +1,8 @@
import I18n from "I18n";
import SectionLink from "discourse/lib/sidebar/section-link";
import Composer from "discourse/models/composer";
import { getOwner, setOwner } from "@ember/application";
import { setOwner } from "@ember/application";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import PermissionType from "discourse/models/permission-type";
import EverythingSectionLink from "discourse/lib/sidebar/common/community-section/everything-section-link";
import MyPostsSectionLink from "discourse/lib/sidebar/user/community-section/my-posts-section-link";
import AdminSectionLink from "discourse/lib/sidebar/user/community-section/admin-section-link";
@ -20,7 +16,6 @@ import {
customSectionLinks,
secondaryCustomSectionLinks,
} from "discourse/lib/sidebar/custom-community-section-links";
import SidebarSectionForm from "discourse/components/modal/sidebar-section-form";
const SPECIAL_LINKS_MAP = {
"/latest": EverythingSectionLink,
@ -46,6 +41,7 @@ export default class CommunitySection {
@tracked moreLinks;
reorderable = false;
hideSectionHeader = true;
constructor({ section, owner }) {
setOwner(this, owner);
@ -137,64 +133,4 @@ export default class CommunitySection {
overridenIcon,
});
}
get decoratedTitle() {
return I18n.t(
`sidebar.sections.${this.section.title.toLowerCase()}.header_link_text`,
{ defaultValue: this.section.title }
);
}
get headerActions() {
if (this.currentUser?.admin) {
return [
{
action: this.editSection,
title: I18n.t(
"sidebar.sections.community.header_action_edit_section_title"
),
},
];
}
if (this.currentUser) {
return [
{
action: this.composeTopic,
title: I18n.t(
"sidebar.sections.community.header_action_create_topic_title"
),
},
];
}
}
get headerActionIcon() {
return this.currentUser?.admin ? "pencil-alt" : "plus";
}
@action
editSection() {
return this.modal.show(SidebarSectionForm, {
model: this.section,
});
}
@action
composeTopic() {
const composerArgs = {
action: Composer.CREATE_TOPIC,
draftKey: Composer.NEW_TOPIC_KEY,
};
const controller = getOwner(this).lookup("controller:navigation/category");
const category = controller.category;
if (category && category.permission === PermissionType.FULL) {
composerArgs.categoryId = category.id;
}
next(() => {
getOwner(this).lookup("controller:composer").open(composerArgs);
});
}
}

View File

@ -45,6 +45,7 @@ export default class SectionLink {
if (event.button === 0 || event.targetTouches) {
this.startMouseY = this.#calcMouseY(event);
this.willDrag = true;
discourseLater(
() => {
this.delayedStart(event);
@ -53,9 +54,11 @@ export default class SectionLink {
);
}
}
delayedStart(event) {
if (this.willDrag) {
const currentMouseY = this.#calcMouseY(event);
if (currentMouseY === this.startMouseY) {
event.stopPropagation();
event.preventDefault();
@ -80,24 +83,29 @@ export default class SectionLink {
@bind
dragMove(event) {
this.startMouseY = this.#calcMouseY(event);
if (!this.drag) {
return;
}
event.stopPropagation();
event.preventDefault();
const currentMouseY = this.#calcMouseY(event);
const distance = currentMouseY - this.mouseY;
if (!this.linkHeight) {
this.linkHeight = document.getElementsByClassName(
"sidebar-section-link-wrapper"
)[0].clientHeight;
}
if (distance >= this.linkHeight) {
if (this.section.links.indexOf(this) !== this.section.links.length - 1) {
this.section.moveLinkDown(this);
this.mouseY = currentMouseY;
}
}
if (distance <= -this.linkHeight) {
if (this.section.links.indexOf(this) !== 0) {
this.section.moveLinkUp(this);

View File

@ -42,7 +42,7 @@ export default class Section {
{
action: () => {
return this.modal.show(SidebarSectionForm, {
model: this.section,
model: this,
});
},
title: I18n.t("sidebar.sections.custom.edit"),
@ -78,6 +78,7 @@ export default class Section {
this.links = this.links.removeObject(link);
this.links.splice(position, 0, link);
}
@bind
reorder() {
return ajax(`/sidebar_sections/reorder`, {

View File

@ -0,0 +1,30 @@
import I18n from "I18n";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import CommonCommunitySection from "discourse/lib/sidebar/common/community-section/section";
import SidebarSectionForm from "discourse/components/modal/sidebar-section-form";
export default class extends CommonCommunitySection {
@service modal;
@service navigationMenu;
@action
moreSectionButtonAction() {
return this.modal.show(SidebarSectionForm, { model: this });
}
get moreSectionButtonText() {
return I18n.t(
`sidebar.sections.community.edit_section.${
this.navigationMenu.isDesktopDropdownMode
? "header_dropdown"
: "sidebar"
}`
);
}
get moreSectionButtonIcon() {
return "pencil-alt";
}
}

View File

@ -0,0 +1,13 @@
import Service, { inject as service } from "@ember/service";
export default class NavigationMenu extends Service {
@service site;
@service siteSettings;
get isDesktopDropdownMode() {
const headerDropdownMode =
this.siteSettings.navigation_menu === "header dropdown";
return !this.site.mobileView && headerDropdownMode;
}
}

View File

@ -386,6 +386,7 @@ createWidget("revamped-hamburger-menu-wrapper", {
click(event) {
if (
event.target.closest(".sidebar-section-header-button") ||
event.target.closest(".sidebar-section-link-button") ||
event.target.closest(".sidebar-section-link")
) {
this.sendWidgetAction("toggleHamburger");

View File

@ -46,72 +46,6 @@ acceptance("Sidebar - Logged on user - Community Section", function (needs) {
});
});
test("clicking on section header button", async function (assert) {
await visit("/");
await click(
".sidebar-section[data-section-name='community'] .sidebar-section-header-button"
);
assert.ok(exists("#reply-control"), "it opens the composer");
});
test("clicking on section header button while viewing a category", async function (assert) {
await visit("/c/bug");
await click(
".sidebar-section[data-section-name='community'] .sidebar-section-header-button"
);
assert.ok(exists("#reply-control"), "it opens the composer");
assert.strictEqual(
query(".category-input .selected-name .category-name").textContent,
"bug",
"the current category is prefilled in the composer input"
);
});
test("clicking on section header link", async function (assert) {
await visit("/t/280");
assert.ok(
exists(
".sidebar-section[data-section-name='community'] .sidebar-section-content"
),
"shows content section"
);
assert.strictEqual(
query(
".sidebar-section[data-section-name='community'] .sidebar-section-header"
).title,
I18n.t("sidebar.toggle_section"),
"caret has the right title"
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-section-header"
);
assert.notOk(
exists(
".sidebar-section[data-section-name='community'] .sidebar-section-content"
),
"hides the content of the section"
);
await click(
".sidebar-section[data-section-name='community'] .sidebar-section-header"
);
assert.ok(
exists(
".sidebar-section[data-section-name='community'] .sidebar-section-content"
),
"shows content section"
);
});
test("clicking on more... link", async function (assert) {
await visit("/");

View File

@ -208,7 +208,7 @@ acceptance(
assert.ok(
exists(
".sidebar-section[data-section-name='community'] .sidebar-section-header[aria-expanded='true'][aria-controls='sidebar-section-content-community']"
".sidebar-section[data-section-name='categories'] .sidebar-section-header[aria-expanded='true'][aria-controls='sidebar-section-content-categories']"
),
"accessibility attributes are set correctly on sidebar section header when section is expanded"
);
@ -217,7 +217,7 @@ acceptance(
assert.ok(
exists(
".sidebar-section[data-section-name='community'] .sidebar-section-header[aria-expanded='false'][aria-controls='sidebar-section-content-community']"
".sidebar-section[data-section-name='categories'] .sidebar-section-header[aria-expanded='false'][aria-controls='sidebar-section-content-categories']"
),
"accessibility attributes are set correctly on sidebar section header when section is collapsed"
);

View File

@ -7,6 +7,7 @@
color: var(--d-sidebar-highlight-color);
}
}
height: var(--d-sidebar-row-height);
color: var(--d-sidebar-link-color);
display: flex;
@ -23,6 +24,7 @@
font-size: var(--font-down-1);
}
}
.sidebar-more-section-links-details-content {
background-color: var(--d-sidebar-background);
transition: background-color 0.25s;
@ -34,14 +36,28 @@
padding: 0.33rem calc(var(--d-sidebar-row-horizontal-padding) / 3);
}
}
.sidebar-more-section-links-details-content-main {
position: sticky;
margin: 0;
}
.sidebar-more-section-links-details-content-footer {
border-top: 2px solid var(--primary-low);
display: flex;
width: 100%;
.sidebar-section-link-wrapper {
width: 100%;
}
}
.sidebar-more-section-links-details-content-wrapper {
position: absolute;
width: 100%;
z-index: z("modal", "content") + 1;
}
.sidebar-more-section-links-details {
position: relative;
}

View File

@ -1,6 +1,7 @@
:root {
--d-sidebar-section-link-prefix-margin-right: 0.35rem;
--d-sidebar-section-link-prefix-width: 1.35rem;
--d-sidebar-section-link-icon-size: 0.8em;
}
.sidebar-section-link-wrapper {
@ -81,6 +82,26 @@
}
}
.sidebar-section-link-button {
color: var(--d-sidebar-link-color);
background-color: var(--secondary);
width: 100%;
justify-content: flex-start;
&:hover {
color: var(--d-sidebar-link-color);
background-color: var(--primary-low);
.d-icon {
color: var(--d-sidebar-link-icon-color);
}
}
.d-icon {
color: var(--d-sidebar-link-icon-color);
}
}
.sidebar-section-link[data-link-name="personal-messages-sent"],
.sidebar-section-link[data-link-name="personal-messages-new"],
.sidebar-section-link[data-link-name="personal-messages-archive"],
@ -130,7 +151,7 @@
color: var(--d-sidebar-link-icon-color);
svg {
font-size: 0.8em;
font-size: var(--d-sidebar-section-link-icon-size);
}
.prefix-badge {

View File

@ -128,6 +128,7 @@
}
}
}
.sidebar-section-form-modal {
.draggable {
display: flex;
@ -162,6 +163,15 @@
.value.warning {
position: absolute;
}
.sidebar-section-form__input-wrapper {
margin-bottom: 1em;
input {
width: 100%;
}
}
.row-wrapper {
display: grid;
grid-template-columns: 2em 4.5em repeat(2, 1fr) 2em;
@ -172,9 +182,10 @@
border-top: 2px solid transparent;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
&.header {
padding-bottom: 0;
padding-top: 1em;
label {
margin-bottom: 0;
}

View File

@ -4473,9 +4473,9 @@ en:
header_action_title: "Edit your sidebar categories"
configure_defaults: "Configure defaults"
community:
header_link_text: "Community"
header_action_create_topic_title: "Create a topic"
header_action_edit_section_title: "Edit Community section"
edit_section:
sidebar: "Customize this section"
header_dropdown: "Customize"
links:
about:
content: "About"

View File

@ -3,7 +3,7 @@
RSpec.describe "Deleted message", type: :system do
let(:chat_page) { PageObjects::Pages::Chat.new }
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:sidebar_component) { PageObjects::Components::Sidebar.new }
let(:sidebar_component) { PageObjects::Components::NavigationMenu::Sidebar.new }
fab!(:current_user) { Fabricate(:admin) }
fab!(:channel_1) { Fabricate(:category_channel) }

View File

@ -14,7 +14,7 @@ RSpec.describe "Navigation", type: :system do
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
let(:side_panel_page) { PageObjects::Pages::ChatSidePanel.new }
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
let(:sidebar_component) { PageObjects::Components::Sidebar.new }
let(:sidebar_component) { PageObjects::Components::NavigationMenu::Sidebar.new }
let(:chat_drawer_page) { PageObjects::Pages::ChatDrawer.new }
before do

View File

@ -2,7 +2,7 @@
RSpec.describe "Sidebar navigation menu", type: :system do
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
let(:sidebar_component) { PageObjects::Components::Sidebar.new }
let(:sidebar_component) { PageObjects::Components::NavigationMenu::Sidebar.new }
fab!(:current_user) { Fabricate(:user) }

View File

@ -4,7 +4,7 @@ describe "Custom sidebar sections", type: :system do
fab!(:user) { Fabricate(:user) }
fab!(:admin) { Fabricate(:admin) }
let(:section_modal) { PageObjects::Modals::SidebarSectionForm.new }
let(:sidebar) { PageObjects::Components::Sidebar.new }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
before { user.user_option.update!(external_links_in_new_tab: true) }
@ -112,24 +112,36 @@ describe "Custom sidebar sections", type: :system do
it "allows the user to reorder links in custom section" do
sidebar_section = Fabricate(:sidebar_section, title: "My section", user: user)
sidebar_url_1 = Fabricate(:sidebar_url, name: "Sidebar Tags", value: "/tags")
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1)
sidebar_url_2 = Fabricate(:sidebar_url, name: "Sidebar Categories", value: "/categories")
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_2)
sidebar_url_1 =
Fabricate(:sidebar_url, name: "Sidebar Tags", value: "/tags").tap do |sidebar_url|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url)
end
sidebar_url_2 =
Fabricate(:sidebar_url, name: "Sidebar Categories", value: "/categories").tap do |sidebar_url|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url)
end
sidebar_url_3 =
Fabricate(:sidebar_url, name: "Sidebar Latest", value: "/latest").tap do |sidebar_url|
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url)
end
sign_in user
visit("/latest")
expect(sidebar.primary_section_links("my-section")).to eq(
["Sidebar Tags", "Sidebar Categories"],
["Sidebar Tags", "Sidebar Categories", "Sidebar Latest"],
)
tags_link = find(".sidebar-section-link[data-link-name='Sidebar Tags']")
categories_link = find(".sidebar-section-link[data-link-name='Sidebar Categories']")
tags_link.drag_to(categories_link, html5: true, delay: 0.4)
latest_link = find(".sidebar-section-link[data-link-name='Sidebar Latest']")
tags_link.drag_to(latest_link, html5: true, delay: 0.4)
expect(sidebar.primary_section_links("my-section")).to eq(
["Sidebar Categories", "Sidebar Tags"],
["Sidebar Categories", "Sidebar Tags", "Sidebar Latest"],
)
end
@ -199,40 +211,6 @@ describe "Custom sidebar sections", type: :system do
expect(sidebar).to have_no_section("Edited public section")
end
it "allows admin to edit community section and reset to default" do
sign_in admin
visit("/latest")
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-v],
)
sidebar.edit_custom_section("Community")
section_modal.fill_link("Topics", "/latest", "paper-plane")
section_modal.fill_name("Edited community section")
section_modal.topics_link.drag_to(section_modal.review_link, delay: 0.4)
section_modal.save
expect(sidebar).to have_section("Edited community section")
expect(sidebar.primary_section_links("edited-community-section")).to eq(
["My Posts", "Topics", "Review", "Admin", "More"],
)
expect(sidebar.primary_section_icons("edited-community-section")).to eq(
%w[user paper-plane flag wrench ellipsis-v],
)
sidebar.edit_custom_section("Edited community section")
section_modal.reset
expect(sidebar).to have_section("Community")
expect(sidebar.primary_section_links("community")).to eq(
["Topics", "My Posts", "Review", "Admin", "More"],
)
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-v],
)
end
it "shows anonymous public sections" do
sidebar_section = Fabricate(:sidebar_section, title: "Public section", public: true)
sidebar_url_1 = Fabricate(:sidebar_url, name: "Sidebar Tags", value: "/tags")

View File

@ -19,7 +19,7 @@ RSpec.describe "Editing sidebar categories navigation", type: :system do
Fabricate(:category, parent_category_id: category.id, name: "category subcategory")
end
let(:sidebar) { PageObjects::Components::Sidebar.new }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
before { sign_in(user) }

View File

@ -0,0 +1,67 @@
# frozen_string_literal: true
RSpec.describe "Editing Sidebar Community Section", type: :system do
fab!(:admin) { Fabricate(:admin) }
fab!(:user) { Fabricate(:user) }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
let(:sidebar_header_dropdown) { PageObjects::Components::NavigationMenu::HeaderDropdown.new }
it "should not display the edit section button to non admins" do
sign_in(user)
visit("/latest")
sidebar.click_community_section_more_button
expect(sidebar).to have_no_customize_community_section_button
end
it "allows admin to edit community section and reset to default" do
sign_in(admin)
visit("/latest")
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-v],
)
modal = sidebar.click_community_section_more_button.click_customize_community_section_button
modal.fill_link("Topics", "/latest", "paper-plane")
modal.topics_link.drag_to(modal.review_link, delay: 0.4)
modal.save
expect(sidebar.primary_section_links("community")).to eq(
["My Posts", "Topics", "Review", "Admin", "More"],
)
expect(sidebar.primary_section_icons("community")).to eq(
%w[user paper-plane flag wrench ellipsis-v],
)
modal = sidebar.click_community_section_more_button.click_customize_community_section_button
modal.reset
expect(sidebar).to have_section("Community")
expect(sidebar.primary_section_links("community")).to eq(
["Topics", "My Posts", "Review", "Admin", "More"],
)
expect(sidebar.primary_section_icons("community")).to eq(
%w[layer-group user flag wrench ellipsis-v],
)
end
it "should allow admins to open modal to edit the section when `navigation_menu` site setting is `header dropdown`" do
SiteSetting.navigation_menu = "header dropdown"
sign_in(admin)
visit("/latest")
modal = sidebar_header_dropdown.open.click_customize_community_section_button
expect(modal).to be_visible
end
end

View File

@ -24,7 +24,7 @@ RSpec.describe "Editing sidebar tags navigation", type: :system do
# This tag should not be displayed in the modal as it has not been used in a topic
fab!(:tag5) { Fabricate(:tag, name: "tag5") }
let(:sidebar) { PageObjects::Components::Sidebar.new }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
before { sign_in(user) }

View File

@ -0,0 +1,129 @@
# frozen_string_literal: true
module PageObjects
module Components
module NavigationMenu
class Base < PageObjects::Components::Base
def community_section
find(".sidebar-section[data-section-name='community']")
end
SIDEBAR_SECTION_LINK_SELECTOR = "sidebar-section-link"
def click_section_link(name)
find(".#{SIDEBAR_SECTION_LINK_SELECTOR}", text: name).click
end
def has_one_active_section_link?
has_css?(".#{SIDEBAR_SECTION_LINK_SELECTOR}--active", count: 1)
end
def has_section_link?(name, href: nil, active: false, target: nil)
section_link_present?(name, href: href, active: active, target: target, present: true)
end
def has_no_section_link?(name, href: nil, active: false)
section_link_present?(name, href: href, active: active, present: false)
end
def has_section?(name)
has_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end
def has_no_section?(name)
has_no_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end
def has_categories_section?
has_section?("Categories")
end
def has_tags_section?
has_section?("Tags")
end
def has_no_tags_section?
has_no_section?("Tags")
end
def has_all_tags_section_link?
has_section_link?(I18n.t("js.sidebar.all_tags"))
end
def has_tag_section_links?(tags)
tag_names = tags.map(&:name)
tag_section_links =
all(
".sidebar-section[data-section-name='tags'] .sidebar-section-link-wrapper[data-tag-name]",
count: tag_names.length,
)
expect(tag_section_links.map(&:text)).to eq(tag_names)
end
def primary_section_links(slug)
all("[data-section-name='#{slug}'] .sidebar-section-link-wrapper").map(&:text)
end
def primary_section_icons(slug)
all("[data-section-name='#{slug}'] .sidebar-section-link-wrapper use").map do |icon|
icon[:href].delete_prefix("#")
end
end
def has_category_section_link?(category)
page.has_link?(category.name, class: "sidebar-section-link")
end
def click_add_section_button
click_button(add_section_button_text)
end
def has_no_add_section_button?
page.has_no_button?(add_section_button_text)
end
def click_edit_categories_button
within(".sidebar-section[data-section-name='categories']") do
click_button(class: "sidebar-section-header-button", visible: false)
end
PageObjects::Modals::SidebarEditCategories.new
end
def click_edit_tags_button
within(".sidebar-section[data-section-name='tags']") do
click_button(class: "sidebar-section-header-button", visible: false)
end
PageObjects::Modals::SidebarEditTags.new
end
def edit_custom_section(name)
name = name.parameterize
find(".sidebar-section[data-section-name='#{name}']").hover
find(
".sidebar-section[data-section-name='#{name}'] button.sidebar-section-header-button",
).click
end
private
def section_link_present?(name, href: nil, active: false, target: nil, present:)
attributes = { exact_text: name }
attributes[:href] = href if href
attributes[:class] = SIDEBAR_SECTION_LINK_SELECTOR
attributes[:class] += "--active" if active
attributes[:target] = target if target
page.public_send(present ? :has_link? : :has_no_link?, **attributes)
end
def add_section_button_text
I18n.t("js.sidebar.sections.custom.add")
end
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module PageObjects
module Components
module NavigationMenu
class HeaderDropdown < Base
def open
find(".header-dropdown-toggle.hamburger-dropdown").click
expect(page).to have_css(".sidebar-hamburger-dropdown")
self
end
def click_customize_community_section_button
community_section.click_button(
I18n.t("js.sidebar.sections.community.edit_section.header_dropdown"),
)
expect(page).to have_no_css(".sidebar-hamburger-dropdown")
PageObjects::Modals::SidebarSectionForm.new
end
end
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module PageObjects
module Components
module NavigationMenu
class Sidebar < Base
def open_on_mobile
click_button("toggle-hamburger-menu")
end
def visible?
page.has_css?("#d-sidebar")
end
def not_visible?
page.has_no_css?("#d-sidebar")
end
def has_no_customize_community_section_button?
community_section.has_no_button?(class: "sidebar-section-link-button")
end
def click_customize_community_section_button
community_section.click_button(
I18n.t("js.sidebar.sections.community.edit_section.sidebar"),
)
expect(community_section).to have_no_css(".sidebar-more-section-links-details")
PageObjects::Modals::SidebarSectionForm.new
end
def click_community_section_more_button
community_section.click_button(class: "sidebar-more-section-links-details-summary")
expect(community_section).to have_css(".sidebar-more-section-links-details")
self
end
def custom_section_modal_title
find("#discourse-modal-title")
end
end
end
end
end

View File

@ -1,138 +0,0 @@
# frozen_string_literal: true
module PageObjects
module Components
class Sidebar < PageObjects::Components::Base
def open_on_mobile
click_button("toggle-hamburger-menu")
end
def visible?
page.has_css?("#d-sidebar")
end
def not_visible?
page.has_no_css?("#d-sidebar")
end
def has_category_section_link?(category)
page.has_link?(category.name, class: "sidebar-section-link")
end
def click_add_section_button
click_button(add_section_button_text)
end
def has_no_add_section_button?
page.has_no_button?(add_section_button_text)
end
def click_edit_categories_button
within(".sidebar-section[data-section-name='categories']") do
click_button(class: "sidebar-section-header-button", visible: false)
end
PageObjects::Modals::SidebarEditCategories.new
end
def click_edit_tags_button
within(".sidebar-section[data-section-name='tags']") do
click_button(class: "sidebar-section-header-button", visible: false)
end
PageObjects::Modals::SidebarEditTags.new
end
def edit_custom_section(name)
find(".sidebar-section[data-section-name='#{name.parameterize}']").hover
find(
".sidebar-section[data-section-name='#{name.parameterize}'] button.sidebar-section-header-button",
).click
end
SIDEBAR_SECTION_LINK_SELECTOR = "sidebar-section-link"
def click_section_link(name)
find(".#{SIDEBAR_SECTION_LINK_SELECTOR}", text: name).click
end
def has_one_active_section_link?
has_css?(".#{SIDEBAR_SECTION_LINK_SELECTOR}--active", count: 1)
end
def has_section_link?(name, href: nil, active: false, target: nil)
section_link_present?(name, href: href, active: active, target: target, present: true)
end
def has_no_section_link?(name, href: nil, active: false)
section_link_present?(name, href: href, active: active, present: false)
end
def custom_section_modal_title
find("#discourse-modal-title")
end
def has_section?(name)
has_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end
def has_no_section?(name)
has_no_css?(".sidebar-sections [data-section-name='#{name.parameterize}']")
end
def has_categories_section?
has_section?("Categories")
end
def has_tags_section?
has_section?("Tags")
end
def has_no_tags_section?
has_no_section?("Tags")
end
def has_all_tags_section_link?
has_section_link?(I18n.t("js.sidebar.all_tags"))
end
def has_tag_section_links?(tags)
tag_names = tags.map(&:name)
tag_section_links =
all(
".sidebar-section[data-section-name='tags'] .sidebar-section-link-wrapper[data-tag-name]",
count: tag_names.length,
)
expect(tag_section_links.map(&:text)).to eq(tag_names)
end
def primary_section_links(slug)
all("[data-section-name='#{slug}'] .sidebar-section-link-wrapper").map(&:text)
end
def primary_section_icons(slug)
all("[data-section-name='#{slug}'] .sidebar-section-link-wrapper use").map do |icon|
icon[:href].delete_prefix("#")
end
end
private
def section_link_present?(name, href: nil, active: false, target: nil, present:)
attributes = { exact_text: name }
attributes[:href] = href if href
attributes[:class] = SIDEBAR_SECTION_LINK_SELECTOR
attributes[:class] += "--active" if active
attributes[:target] = target if target
page.public_send(present ? :has_link? : :has_no_link?, **attributes)
end
def add_section_button_text
I18n.t("js.sidebar.sections.custom.add")
end
end
end
end

View File

@ -23,9 +23,9 @@ module PageObjects
page.has_no_css?(".sidebar-footer-actions-keyboard-shortcuts")
end
def click_community_header_button
def click_categories_header_button
page.click_button(
I18n.t("js.sidebar.sections.community.header_action_create_topic_title"),
I18n.t("js.sidebar.sections.categories.header_action_title"),
class: "sidebar-section-header-button",
)
end

View File

@ -34,16 +34,24 @@ module PageObjects
def reset
find(".reset-link").click
find(".dialog-footer .btn-primary").click
closed?
self
end
def save
find("#save-section").click
closed?
self
end
def visible?
page.has_css?(".sidebar-section-form-modal")
end
def closed?
page.has_no_css?(".sidebar-section-form-modal")
end
def has_disabled_save?
find_button("Save", disabled: true)
end

View File

@ -25,7 +25,7 @@ describe "Viewing sidebar as anonymous user", type: :system do
Fabricate(:tag, name: "tag 6").tap { |tag| Fabricate.times(1, :topic, tags: [tag]) }
end
let(:sidebar) { PageObjects::Components::Sidebar.new }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
describe "when viewing the tags section" do
it "should not display the tags section when tagging has been disabled" do

View File

@ -53,9 +53,8 @@ describe "Viewing sidebar mobile", type: :system, mobile: true do
expect(sidebar_dropdown).to be_visible
sidebar_dropdown.click_community_header_button
sidebar_dropdown.click_categories_header_button
expect(composer).to be_opened
expect(sidebar_dropdown).to be_hidden
end

View File

@ -5,6 +5,8 @@ describe "Viewing sidebar", type: :system do
fab!(:user) { Fabricate(:user) }
fab!(:category_sidebar_section_link) { Fabricate(:category_sidebar_section_link, user: user) }
let(:sidebar) { PageObjects::Components::NavigationMenu::Sidebar.new }
before { sign_in(user) }
describe "when using the legacy navigation menu" do
@ -13,8 +15,6 @@ describe "Viewing sidebar", type: :system do
it "should display the sidebar when `navigation_menu` query param is 'sidebar'" do
visit("/latest?navigation_menu=sidebar")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_visible
expect(sidebar).to have_category_section_link(category_sidebar_section_link.linkable)
expect(page).not_to have_css(".hamburger-dropdown")
@ -23,8 +23,6 @@ describe "Viewing sidebar", type: :system do
it "should display the sidebar dropdown menu when `navigation_menu` query param is 'header_dropdown'" do
visit("/latest?navigation_menu=header_dropdown")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_not_visible
header_dropdown = PageObjects::Components::SidebarHeaderDropdown.new
@ -40,8 +38,6 @@ describe "Viewing sidebar", type: :system do
it "should display the sidebar when `navigation_menu` query param is 'sidebar'" do
visit("/latest?navigation_menu=sidebar")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_visible
expect(page).not_to have_css(".hamburger-dropdown")
end
@ -49,8 +45,6 @@ describe "Viewing sidebar", type: :system do
it "should display the legacy dropdown menu when `navigation_menu` query param is 'legacy'" do
visit("/latest?navigation_menu=legacy")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_not_visible
legacy_header_dropdown = PageObjects::Components::LegacyHeaderDropdown.new
@ -66,8 +60,6 @@ describe "Viewing sidebar", type: :system do
it "should display the legacy dropdown menu when `navigation_menu` query param is 'legacy'" do
visit("/latest?navigation_menu=legacy")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_not_visible
legacy_header_dropdown = PageObjects::Components::LegacyHeaderDropdown.new
@ -79,8 +71,6 @@ describe "Viewing sidebar", type: :system do
it "should display the sidebar dropdown menu when `navigation_menu` query param is 'header_dropdown'" do
visit("/latest?navigation_menu=header_dropdown")
sidebar = PageObjects::Components::Sidebar.new
expect(sidebar).to be_not_visible
header_dropdown = PageObjects::Components::SidebarHeaderDropdown.new