FEATURE: API for sidebar (#17296)

This plugin API can be used to add to sections and links to sidebar
This commit is contained in:
Alan Guo Xiang Tan 2022-07-18 12:03:37 +08:00 committed by GitHub
parent 0ca1152c1c
commit 0d72a8c458
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 927 additions and 28 deletions

View File

@ -1,5 +1,7 @@
import GlimmerComponent from "discourse/components/glimmer"; import GlimmerComponent from "discourse/components/glimmer";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import { customSections as sidebarCustomSections } from "discourse/lib/sidebar/custom-sections";
import { cached } from "@glimmer/tracking";
export default class Sidebar extends GlimmerComponent { export default class Sidebar extends GlimmerComponent {
constructor() { constructor() {
@ -35,5 +37,13 @@ export default class Sidebar extends GlimmerComponent {
if (this.site.mobileView) { if (this.site.mobileView) {
document.removeEventListener("click", this.collapseSidebar); document.removeEventListener("click", this.collapseSidebar);
} }
this.customSections.forEach((customSection) => customSection.teardown());
}
@cached
get customSections() {
return sidebarCustomSections.map((customSection) => {
return new customSection({ sidebar: this });
});
} }
} }

View File

@ -1,3 +1,12 @@
import Component from "@ember/component"; import GlimmerComponent from "@glimmer/component";
import { htmlSafe } from "@ember/template";
export default Component.extend({}); export default class SectionLink extends GlimmerComponent {
get prefixCSS() {
const color = this.args.prefixColor;
if (!color || !color.match(/^\w{6}$/)) {
return htmlSafe("");
}
return htmlSafe("color: #" + color);
}
}

View File

@ -27,7 +27,22 @@ export default class SidebarSection extends GlimmerComponent {
} }
} }
@action
handleMultipleHeaderActions(id) {
this.args.headerActions
.find((headerAction) => headerAction.id === id)
.action();
}
get headerCaretIcon() { get headerCaretIcon() {
return this.displaySection ? "angle-down" : "angle-right"; return this.displaySection ? "angle-down" : "angle-right";
} }
get isSingleHeaderAction() {
return this.args.headerActions?.length === 1;
}
get isMultipleHeaderActions() {
return this.args.headerActions?.length > 1;
}
} }

View File

@ -95,6 +95,7 @@ import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
import { downloadCalendar } from "discourse/lib/download-calendar"; import { downloadCalendar } from "discourse/lib/download-calendar";
import { consolePrefix } from "discourse/lib/source-identifier"; import { consolePrefix } from "discourse/lib/source-identifier";
import { addSectionLink } from "discourse/lib/sidebar/custom-topics-section-links"; import { addSectionLink } from "discourse/lib/sidebar/custom-topics-section-links";
import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
// If you add any methods to the API ensure you bump up the version number // If you add any methods to the API ensure you bump up the version number
// based on Semantic Versioning 2.0.0. Please update the changelog at // based on Semantic Versioning 2.0.0. Please update the changelog at
@ -1634,11 +1635,11 @@ class PluginApi {
* api.addTopicsSectionLink((baseSectionLink) => { * api.addTopicsSectionLink((baseSectionLink) => {
* return class CustomSectionLink extends baseSectionLink { * return class CustomSectionLink extends baseSectionLink {
* get name() { * get name() {
* returns "bookmarked"; * return "bookmarked";
* } * }
* *
* get route() { * get route() {
* returns "userActivity.bookmarks"; * return "userActivity.bookmarks";
* } * }
* *
* get model() { * get model() {
@ -1680,6 +1681,130 @@ class PluginApi {
addTopicsSectionLink(arg) { addTopicsSectionLink(arg) {
addSectionLink(arg); addSectionLink(arg);
} }
/**
* EXPERIMENTAL. Do not use.
* Support for adding a Sidebar section by returning a class which extends from the BaseCustomSidebarSection
* class interface. See `lib/sidebar/base-custom-sidebar-section.js` for documentation on the BaseCustomSidebarSection class
* interface.
*
* ```
* api.addSidebarSection((BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
* return class extends BaseCustomSidebarSection {
* get name() {
* return "chat-channels";
* }
*
* get route() {
* return "chat";
* }
*
* get title() {
* return I18n.t("sidebar.sections.chat.title");
* }
*
* get text() {
* return I18n.t("sidebar.sections.chat.text");
* }
*
* get actionsIcon() {
* return "cog";
* }
*
* get actions() {
* return [
* { id: "browseChannels", title: "Browse channel", action: () => {} },
* { id: "settings", title: "Settings", action: () => {} },
* ];
* }
*
* get links() {
* return [
* new (class extends BaseCustomSidebarSectionLink {
* get name() {
* "dev"
* }
* get route() {
* return "chat.channel";
* }
* get model() {
* return {
* channelId: "1",
* channelTitle: "dev channel"
* };
* }
* get title() {
* return "dev channel";
* }
* get text() {
* return "dev channel";
* }
* get prefixValue() {
* return "icon";
* }
* get prefixValue() {
* return "hashtag";
* }
* get prefixColor() {
* return "000000";
* }
* get prefixBadge() {
* return "lock";
* }
* get suffixType() {
* return "icon";
* }
* get suffixValue() {
* return "circle";
* }
* get suffixCSSClass() {
* return "unread";
* }
* })(),
* new (class extends BaseCustomSidebarSectionLink {
* get name() {
* "random"
* }
* get route() {
* return "chat.channel";
* }
* get model() {
* return {
* channelId: "2",
* channelTitle: "random channel"
* };
* }
* get currentWhen() {
* return true;
* }
* get title() {
* return "random channel";
* }
* get text() {
* return "random channel";
* }
* get hoverType() {
* return "icon";
* }
* get hoverValue() {
* return "times";
* }
* get hoverAction() {
* return () => {};
* }
* get hoverTitle() {
* return "button title attribute"
* }
* })()
* ];
* }
* }
* })
* ```
*/
addSidebarSection(func) {
addSidebarSection(func);
}
} }
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number // from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number

View File

@ -0,0 +1,106 @@
/**
* Base class representing a sidebar section link interface.
*/
export default class BaseCustomSidebarSectionLink {
/**
* @returns {string} The name of the section link. Needs to be dasherized and lowercase.
*/
get name() {
this._notImplemented();
}
/**
* @returns {string} Ember route
*/
get route() {
this._notImplemented();
}
/**
* @returns {Object} Model for <LinkTo> component. See https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo
*/
get model() {}
/**
* @returns {boolean} Used to determine when this LinkComponent is active
*/
get currentWhen() {}
/**
* @returns {string} Title for the link
*/
get title() {
this._notImplemented();
}
/**
* @returns {string} Text for the link
*/
get text() {
this._notImplemented();
}
/**
* @returns {string} Prefix type for the link. Accepted value: icon, image, text
*/
get prefixType() {}
/**
* @returns {string} Prefix value for the link. Accepted value: icon name, image url, text
*/
get prefixValue() {}
/**
* @returns {string} Prefix hex color
*/
get prefixColor() {}
/**
* @returns {string} Prefix badge icon
*/
get prefixBadge() {}
/**
* @returns {string} CSS class for prefix
*/
get PrefixCSSClass() {}
/**
* @returns {string} Suffix type for the link. Accepted value: icon
*/
get SuffixType() {}
/**
* @returns {string} Suffix value for the link. Accepted value: icon name
*/
get SuffixValue() {}
/**
* @returns {string} CSS class for suffix
*/
get SuffixCSSClass() {}
/**
* @returns {string} Type of the hover button. Accepted value: icon
*/
get hoverType() {}
/**
* @returns {string} Value for the hover button. Accepted value: icon name
*/
get hoverValue() {}
/**
* @returns {Function} Action for hover button
*/
get hoverAction() {}
/**
* @returns {string} Title attribute for the hover button
*/
get hoverTitle() {}
_notImplemented() {
throw "not implemented";
}
}

View File

@ -0,0 +1,53 @@
/**
* Base class representing a sidebar section header interface.
*/
export default class BaseCustomSidebarSection {
constructor({ sidebar } = {}) {
this.sidebar = sidebar;
}
/**
* Called when sidebar component is torn down.
*/
teardown() {}
/**
* @returns {string} The name of the section header. Needs to be dasherized and lowercase.
*/
get name() {
this._notImplemented();
}
/**
* @returns {string} Title for the header
*/
get title() {
this._notImplemented();
}
/**
* @returns {string} Text for the header
*/
get text() {
this._notImplemented();
}
/**
* @returns {Array} Actions for header options button
*/
get actions() {}
/**
* @returns {string} Icon for header options button
*/
get actionsIcon() {}
/**
* @returns {BaseCustomSidebarSectionLink[]} Links for section
*/
get links() {}
_notImplemented() {
throw "not implemented";
}
}

View File

@ -0,0 +1,14 @@
import BaseCustomSidebarSection from "discourse/lib/sidebar/base-custom-sidebar-section";
import BaseCustomSidebarSectionLink from "discourse/lib/sidebar/base-custom-sidebar-section-link";
export const customSections = [];
export function addSidebarSection(func) {
customSections.push(
func.call(this, BaseCustomSidebarSection, BaseCustomSidebarSectionLink)
);
}
export function resetSidebarSection() {
customSections.splice(0, customSections.length);
}

View File

@ -11,6 +11,39 @@
<Sidebar::MessagesSection /> <Sidebar::MessagesSection />
{{/if}} {{/if}}
{{#each this.customSections as |customSection|}}
<Sidebar::Section
@sectionName={{customSection.name}}
@headerRoute={{customSection.route}}
@headerLinkText={{customSection.text}}
@headerLinkTitle={{customSection.title}}
@headerActionsIcon={{customSection.actionsIcon}}
@headerActions={{customSection.actions}}>
{{#each customSection.links as |link|}}
<Sidebar::SectionLink
@linkName={{link.name}}
@route={{link.route}}
@model={{link.model}}
@title={{link.title}}
@prefixColor={{link.prefixColor}}
@prefixBadge={{link.prefixBadge}}
@prefixType={{link.prefixType}}
@prefixValue={{link.prefixValue}}
@prefixCSSClass={{link.prefixCSSClass}}
@suffixType={{link.suffixType}}
@suffixValue={{link.suffixValue}}
@suffixCSSClass={{link.suffixCSSClass}}
@hoverType={{link.hoverType}}
@hoverValue={{link.hoverValue}}
@hoverAction={{link.hoverAction}}
@hoverTitle={{link.hoverTitle}}
@currentWhen={{link.currentWhen}}
@content={{link.text}} />
{{/each}}
</Sidebar::Section>
{{/each}}
{{!-- DO NOT USE, this outlet is temporary and will be removed. --}} {{!-- DO NOT USE, this outlet is temporary and will be removed. --}}
{{!-- Outlet will be replaced with sidebar API. --}} {{!-- Outlet will be replaced with sidebar API. --}}
<PluginOutlet @name="after-sidebar" /> <PluginOutlet @name="after-sidebar" />

View File

@ -3,9 +3,8 @@
@headerRoute="discovery.categories" @headerRoute="discovery.categories"
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}} @headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}} @headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}}
@headerAction={{this.editTracked}} @headerActions={{array (hash action=this.editTracked title=(i18n "sidebar.sections.categories.header_action_title"))}}
@headerActionTitle={{i18n "sidebar.sections.categories.header_action_title"}} @headerActionsIcon="pencil-alt" >
@headerActionIcon="pencil-alt" >
{{#if (gt this.sectionLinks.length 0)}} {{#if (gt this.sectionLinks.length 0)}}
{{#each this.sectionLinks as |sectionLink|}} {{#each this.sectionLinks as |sectionLink|}}

View File

@ -2,8 +2,8 @@
@sectionName="messages" @sectionName="messages"
@headerRoute="userPrivateMessages.index" @headerRoute="userPrivateMessages.index"
@headerModel={{this.currentUser}} @headerModel={{this.currentUser}}
@headerAction={{fn (route-action "composePrivateMessage") null null}}
@headerActionIcon="plus" @headerActionIcon="plus"
@headerActions={{array (hash action=(fn (route-action "composePrivateMessage") null null))}}
@headerLinkText={{i18n "sidebar.sections.messages.header_link_text"}} @headerLinkText={{i18n "sidebar.sections.messages.header_link_text"}}
@headerLinkTitle={{i18n "sidebar.sections.messages.header_link_title"}} > @headerLinkTitle={{i18n "sidebar.sections.messages.header_link_title"}} >

View File

@ -7,6 +7,22 @@
@current-when={{@currentWhen}} @current-when={{@currentWhen}}
@title={{@title}} @title={{@title}}
> >
{{#if @prefixValue }}
<span class="sidebar-section-link-prefix {{@prefixType}} {{@prefixCSSClass}}" style={{this.prefixCSS}}>
{{#if (eq @prefixType "image")}}
<img src={{@prefixValue}} class="prefix-image">
{{/if}}
{{#if (eq @prefixType "text")}}
{{@prefixValue}}
{{/if}}
{{#if (eq @prefixType "icon")}}
{{d-icon @prefixValue class="prefix-icon"}}
{{/if}}
{{#if @prefixBadge}}
{{d-icon @prefixBadge class="prefix-badge"}}
{{/if}}
</span>
{{/if}}
<span class="sidebar-section-link-content-text"> <span class="sidebar-section-link-content-text">
{{@content}} {{@content}}
</span> </span>
@ -16,5 +32,27 @@
{{@badgeText}} {{@badgeText}}
</span> </span>
{{/if}} {{/if}}
{{#if @suffixValue}}
<span class="sidebar-section-link-suffix {{@suffixType}} {{@suffixCSSClass}}">
{{#if (eq @suffixType "icon")}}
{{d-icon @suffixValue}}
{{/if}}
</span>
{{/if}}
</Sidebar::SectionLinkTo> </Sidebar::SectionLinkTo>
{{#if @hoverValue}}
<span class="sidebar-section-link-hover">
<button
type="button"
title={{@hoverTitle}}
class="sidebar-section-hover-button"
{{on "click" @hoverAction}}
>
{{#if (eq @hoverType "icon")}}
{{d-icon @hoverValue class="hover-icon"}}
{{/if}}
</button>
</span>
{{/if}}
</div> </div>

View File

@ -1,23 +1,58 @@
<div class={{concat "sidebar-section-wrapper sidebar-section-" @sectionName}}> <div class={{concat "sidebar-section-wrapper sidebar-section-" @sectionName}}>
<div class="sidebar-section-header"> <div class="sidebar-section-header">
<button type="button" class="sidebar-section-header-caret" title={{i18n "sidebar.toggle_section"}} {{on "click" this.toggleSectionDisplay}}> <button
type="button"
class="sidebar-section-header-caret"
title="toggle section"
{{on "click" this.toggleSectionDisplay}}
>
{{d-icon this.headerCaretIcon}} {{d-icon this.headerCaretIcon}}
</button> </button>
<LinkTo {{#if @headerRoute}}
@route={{@headerRoute}} <LinkTo
@query={{@headerQuery}} @route={{@headerRoute}}
@models={{if @headerModel (array @headerModel) (if @headerModels @headerModels (array))}} @query={{@headerQuery}}
class="sidebar-section-header-link" @models={{if
title={{@headerLinkTitle}}> @headerModel
(array @headerModel)
(if @headerModels @headerModels (array))
}}
class="sidebar-section-header-link"
title={{@headerLinkTitle}}
>
{{@headerLinkText}} {{@headerLinkText}}
</LinkTo> </LinkTo>
{{else}}
<span
title={{@headerLinkTitle}}
class="sidebar-section-header-text"
>
{{@headerLinkText}}
</span>
{{/if}}
{{#if @headerAction}} {{#if this.isSingleHeaderAction}}
<button type="button" class="sidebar-section-header-button" {{on "click" @headerAction}} title={{@headerActionTitle}}> {{#each @headerActions as |headerAction|}}
{{d-icon @headerActionIcon}} <button
</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="edit-channels-dropdown"
/>
{{/if}} {{/if}}
</div> </div>

View File

@ -3,9 +3,8 @@
@headerRoute="tags" @headerRoute="tags"
@headerLinkText={{i18n "sidebar.sections.tags.header_link_text"}} @headerLinkText={{i18n "sidebar.sections.tags.header_link_text"}}
@headerLinkTitle={{i18n "sidebar.sections.tags.header_link_title"}} @headerLinkTitle={{i18n "sidebar.sections.tags.header_link_title"}}
@headerAction={{this.editTracked}} @headerActions={{array (hash action=this.editTracked title=(i18n "sidebar.sections.tags.header_action_title"))}}
@headerActionTitle={{i18n "sidebar.sections.tags.header_action_title"}} @headerActionsIcon="pencil-alt" >
@headerActionIcon="pencil-alt" >
{{#if (gt this.sectionLinks.length 0)}} {{#if (gt this.sectionLinks.length 0)}}
{{#each this.sectionLinks as |sectionLink|}} {{#each this.sectionLinks as |sectionLink|}}

View File

@ -4,9 +4,8 @@
@headerQuery={{hash f=undefined}} @headerQuery={{hash f=undefined}}
@headerLinkText={{i18n "sidebar.sections.topics.header_link_text"}} @headerLinkText={{i18n "sidebar.sections.topics.header_link_text"}}
@headerLinkTitle={{i18n "sidebar.sections.topics.header_link_title"}} @headerLinkTitle={{i18n "sidebar.sections.topics.header_link_title"}}
@headerActionIcon="plus" @headerActionsIcon="plus"
@headerAction={{this.composeTopic}} @headerActions={{array (hash action=this.composeTopic title=(i18n "sidebar.sections.topics.header_action_title"))}}>
@headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}>
{{#each this.sectionLinks as |sectionLink|}} {{#each this.sectionLinks as |sectionLink|}}
<Sidebar::SectionLink <Sidebar::SectionLink

View File

@ -0,0 +1,335 @@
import { test } from "qunit";
import { click, visit } from "@ember/test-helpers";
import {
acceptance,
exists,
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
import { withPluginApi } from "discourse/lib/plugin-api";
import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
acceptance("Sidebar - section API", function (needs) {
needs.user({ experimental_sidebar_enabled: true });
needs.hooks.afterEach(() => {
resetSidebarSection();
});
test("Multiple header actions and links", async function (assert) {
withPluginApi("1.3.0", (api) => {
api.addSidebarSection(
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
return class extends BaseCustomSidebarSection {
get name() {
return "test-chat-channels";
}
get route() {
return "discovery.latest";
}
get model() {
return false;
}
get title() {
return "chat channels title";
}
get text() {
return "chat channels text";
}
get actionsIcon() {
return "cog";
}
get actions() {
return [
{
id: "browseChannels",
title: "Browse channels",
action: () => {},
},
{
id: "settings",
title: "Settings",
action: () => {},
},
];
}
get links() {
return [
new (class extends BaseCustomSidebarSectionLink {
get name() {
"random-channel";
}
get route() {
return "discovery.latest";
}
get model() {
return false;
}
get title() {
return "random channel title";
}
get text() {
return "random channel text";
}
get prefixType() {
return "icon";
}
get prefixValue() {
return "hashtag";
}
get prefixColor() {
return "FF0000";
}
get prefixBadge() {
return "lock";
}
get suffixType() {
return "icon";
}
get suffixValue() {
return "circle";
}
get suffixCSSClass() {
return "unread";
}
})(),
new (class extends BaseCustomSidebarSectionLink {
get name() {
"dev-channel";
}
get route() {
return "discovery.latest";
}
get model() {
return false;
}
get title() {
return "dev channel title";
}
get text() {
return "dev channel text";
}
get prefixColor() {
return "alert";
}
get prefixType() {
return "text";
}
get prefixValue() {
return "test text";
}
})(),
new (class extends BaseCustomSidebarSectionLink {
get name() {
"fun-channel";
}
get route() {
return "discovery.latest";
}
get model() {
return false;
}
get title() {
return "fun channel title";
}
get text() {
return "fun channel text";
}
get prefixType() {
return "image";
}
get prefixValue() {
return "/test.png";
}
get hoverType() {
return "icon";
}
get hoverValue() {
return "times";
}
get hoverAction() {
return () => {};
}
get hoverTitle() {
return "hover button title attribute";
}
})(),
];
}
};
}
);
});
await visit("/");
assert.strictEqual(
query(".sidebar-section-test-chat-channels .sidebar-section-header a")
.title,
"chat channels title",
"displays header with correct title attribute"
);
assert.strictEqual(
query(
".sidebar-section-test-chat-channels .sidebar-section-header a"
).textContent.trim(),
"chat channels text",
"displays header with correct text"
);
await click(
".sidebar-section-test-chat-channels .edit-channels-dropdown summary"
);
assert.strictEqual(
queryAll(
".sidebar-section-test-chat-channels .edit-channels-dropdown .select-kit-collection li"
).length,
2,
"displays two actions"
);
const actions = queryAll(
".sidebar-section-test-chat-channels .edit-channels-dropdown .select-kit-collection li"
);
assert.strictEqual(
actions[0].textContent.trim(),
"Browse channels",
"displays first header action with correct text"
);
assert.strictEqual(
actions[1].textContent.trim(),
"Settings",
"displays second header action with correct text"
);
const links = queryAll(
".sidebar-section-test-chat-channels .sidebar-section-content a"
);
assert.strictEqual(
links[0].textContent.trim(),
"random channel text",
"displays first link with correct text"
);
assert.strictEqual(
links[0].title,
"random channel title",
"displays first link with correct title attribute"
);
assert.strictEqual(
links[0].children.item(0).style.color,
"rgb(255, 0, 0)",
"has correct prefix color"
);
assert.strictEqual(
$(links[0].children.item(0).children.item(0)).hasClass("d-icon-hashtag"),
true,
"displays prefix icon"
);
assert.strictEqual(
$(links[0].children.item(0).children.item(1)).hasClass("d-icon-lock"),
true,
"displays prefix icon badge"
);
assert.strictEqual(
$(links[0].children.item(2).children.item(0)).hasClass("d-icon-circle"),
true,
"displays suffix icon"
);
assert.strictEqual(
$(links[1].children[1])[0].textContent.trim(),
"dev channel text",
"displays second link with correct text"
);
assert.strictEqual(
links[1].title,
"dev channel title",
"displays second link with correct title attribute"
);
assert.strictEqual(
links[1].children.item(0).style.color,
"",
"has no color style when value is invalid"
);
assert.strictEqual(
$(links[1].children)[0].textContent.trim(),
"test text",
"displays prefix text"
);
assert.strictEqual(
$(links[2].children[1])[0].textContent.trim(),
"fun channel text",
"displays third link with correct text"
);
assert.strictEqual(
links[2].title,
"fun channel title",
"displays third link with correct title attribute"
);
assert.strictEqual(
$(links[2].children.item(0).children).attr("src"),
"/test.png",
"uses correct prefix image url"
);
assert.strictEqual(
query(".sidebar-section-link-hover button").title,
"hover button title attribute",
"displays hover button with correct title"
);
});
test("Single header action and no links", async function (assert) {
withPluginApi("1.3.0", (api) => {
api.addSidebarSection((BaseCustomSidebarSection) => {
return class extends BaseCustomSidebarSection {
get name() {
return "test-chat-channels";
}
get route() {
return "discovery.latest";
}
get model() {
return false;
}
get title() {
return "chat channels title";
}
get text() {
return "chat channels text";
}
get actionsIcon() {
return "cog";
}
get actions() {
return [
{
id: "browseChannels",
title: "Browse channels",
action: () => {},
},
];
}
get links() {
return [];
}
};
});
});
await visit("/");
assert.strictEqual(
query(
".sidebar-section-test-chat-channels .sidebar-section-header a"
).textContent.trim(),
"chat channels text",
"displays header with correct text"
);
assert.ok(
exists("button.sidebar-section-header-button"),
"displays single header action button"
);
assert.ok(
!exists(".sidebar-section-test-chat-channels .sidebar-section-content a"),
"displays no links"
);
});
});

View File

@ -118,13 +118,16 @@
align-items: stretch; align-items: stretch;
} }
.sidebar-section-header-link { .sidebar-section-header-link,
.sidebar-section-header-text {
@include ellipsis; @include ellipsis;
flex: 1 1 auto; flex: 1 1 auto;
color: var(--primary); color: var(--primary);
font-size: var(--font-down-1); font-size: var(--font-down-1);
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
}
.sidebar-section-header-link {
&:visited { &:visited {
color: var(--primary); color: var(--primary);
} }
@ -134,6 +137,23 @@
} }
} }
.select-kit {
.btn {
background: transparent;
&:hover {
background: var(--primary-low);
}
}
.d-icon {
font-size: var(--font-down-1);
color: var(--primary-medium);
margin-right: 0;
}
summary {
padding: 0.25em 0.5em;
}
}
.sidebar-section-header-button { .sidebar-section-header-button {
background: none; background: none;
border: none; border: none;
@ -148,6 +168,16 @@
background: var(--primary-low); background: var(--primary-low);
} }
} }
.select-kit-collection {
.texts {
font-size: var(--font-0);
text-transform: none;
line-height: var(--line-height-medium);
.name {
font-size: var(--font-0);
}
}
}
.sidebar-section-link-wrapper { .sidebar-section-link-wrapper {
margin-left: 1.5em; margin-left: 1.5em;
@ -257,3 +287,102 @@
margin-left: 0.5em; margin-left: 0.5em;
} }
} }
#main-outlet-wrapper .sidebar-section-wrapper {
.sidebar-section-link-prefix {
&.image {
img {
border-radius: 50%;
width: 20px;
aspect-ratio: auto 20 / 20;
height: 20px;
margin-right: 0.75em;
}
&.active img {
box-shadow: 0px 0px 0px 1px var(--success);
border: 1px solid var(--secondary);
}
}
&.text {
display: flex;
border-radius: 3px;
background: rgba(var(--primary-rgb), 0.1);
text-align: center;
font-weight: 700;
font-size: var(--font-down-1);
align-items: center;
padding: 0.25rem 0.5rem;
margin-right: 0.75em;
}
&.icon {
position: relative;
margin-right: 0.75em;
svg.prefix-badge {
position: absolute;
background-color: var(--secondary);
border-radius: 50%;
padding: 2px 2px 3px;
color: var(--primary-high);
height: 0.5rem;
width: 0.5rem;
margin-left: -0.4rem;
}
}
}
.sidebar-section-link-suffix.icon {
align-items: center;
display: flex;
margin-left: 0.5em;
svg {
width: 0.75em;
height: 0.75em;
}
&.urgent svg {
color: $success;
}
&.unread svg {
color: var(--tertiary-med-or-tertiary);
}
}
&.sidebar-section-chat-dms {
.sidebar-section-content {
.sidebar-section-link-wrapper {
display: inline-flex;
.sidebar-section-hover-button {
display: none;
color: var(--primary-medium);
align-self: center;
}
.sidebar-section-link-hover {
align-self: center;
}
}
.sidebar-section-link-wrapper:hover {
background: var(--primary-low);
transition: background-color 0.25s;
padding-right: 0.5em;
.sidebar-section-hover-button {
display: block;
}
}
a.sidebar-section-link {
width: calc(var(--d-sidebar-width) - 50px);
&:hover {
background: initial;
}
}
.sidebar-section-hover-button {
border: none;
background: transparent;
padding-left: 0.25em;
padding-right: 0.25em;
margin-left: 0.25em;
svg {
height: 0.75em;
width: 0.75em;
}
}
}
}
}