FEATURE: API for sidebar (#17296)
This plugin API can be used to add to sections and links to sidebar
This commit is contained in:
parent
0ca1152c1c
commit
0d72a8c458
|
@ -1,5 +1,7 @@
|
|||
import GlimmerComponent from "discourse/components/glimmer";
|
||||
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 {
|
||||
constructor() {
|
||||
|
@ -35,5 +37,13 @@ export default class Sidebar extends GlimmerComponent {
|
|||
if (this.site.mobileView) {
|
||||
document.removeEventListener("click", this.collapseSidebar);
|
||||
}
|
||||
this.customSections.forEach((customSection) => customSection.teardown());
|
||||
}
|
||||
|
||||
@cached
|
||||
get customSections() {
|
||||
return sidebarCustomSections.map((customSection) => {
|
||||
return new customSection({ sidebar: this });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,22 @@ export default class SidebarSection extends GlimmerComponent {
|
|||
}
|
||||
}
|
||||
|
||||
@action
|
||||
handleMultipleHeaderActions(id) {
|
||||
this.args.headerActions
|
||||
.find((headerAction) => headerAction.id === id)
|
||||
.action();
|
||||
}
|
||||
|
||||
get headerCaretIcon() {
|
||||
return this.displaySection ? "angle-down" : "angle-right";
|
||||
}
|
||||
|
||||
get isSingleHeaderAction() {
|
||||
return this.args.headerActions?.length === 1;
|
||||
}
|
||||
|
||||
get isMultipleHeaderActions() {
|
||||
return this.args.headerActions?.length > 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,6 +95,7 @@ import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
|
|||
import { downloadCalendar } from "discourse/lib/download-calendar";
|
||||
import { consolePrefix } from "discourse/lib/source-identifier";
|
||||
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
|
||||
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||
|
@ -1634,11 +1635,11 @@ class PluginApi {
|
|||
* api.addTopicsSectionLink((baseSectionLink) => {
|
||||
* return class CustomSectionLink extends baseSectionLink {
|
||||
* get name() {
|
||||
* returns "bookmarked";
|
||||
* return "bookmarked";
|
||||
* }
|
||||
*
|
||||
* get route() {
|
||||
* returns "userActivity.bookmarks";
|
||||
* return "userActivity.bookmarks";
|
||||
* }
|
||||
*
|
||||
* get model() {
|
||||
|
@ -1680,6 +1681,130 @@ class PluginApi {
|
|||
addTopicsSectionLink(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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -11,6 +11,39 @@
|
|||
<Sidebar::MessagesSection />
|
||||
{{/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. --}}
|
||||
{{!-- Outlet will be replaced with sidebar API. --}}
|
||||
<PluginOutlet @name="after-sidebar" />
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
@headerRoute="discovery.categories"
|
||||
@headerLinkText={{i18n "sidebar.sections.categories.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.categories.header_link_title"}}
|
||||
@headerAction={{this.editTracked}}
|
||||
@headerActionTitle={{i18n "sidebar.sections.categories.header_action_title"}}
|
||||
@headerActionIcon="pencil-alt" >
|
||||
@headerActions={{array (hash action=this.editTracked title=(i18n "sidebar.sections.categories.header_action_title"))}}
|
||||
@headerActionsIcon="pencil-alt" >
|
||||
|
||||
{{#if (gt this.sectionLinks.length 0)}}
|
||||
{{#each this.sectionLinks as |sectionLink|}}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
@sectionName="messages"
|
||||
@headerRoute="userPrivateMessages.index"
|
||||
@headerModel={{this.currentUser}}
|
||||
@headerAction={{fn (route-action "composePrivateMessage") null null}}
|
||||
@headerActionIcon="plus"
|
||||
@headerActions={{array (hash action=(fn (route-action "composePrivateMessage") null null))}}
|
||||
@headerLinkText={{i18n "sidebar.sections.messages.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.messages.header_link_title"}} >
|
||||
|
||||
|
|
|
@ -7,6 +7,22 @@
|
|||
@current-when={{@currentWhen}}
|
||||
@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">
|
||||
{{@content}}
|
||||
</span>
|
||||
|
@ -16,5 +32,27 @@
|
|||
{{@badgeText}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if @suffixValue}}
|
||||
<span class="sidebar-section-link-suffix {{@suffixType}} {{@suffixCSSClass}}">
|
||||
{{#if (eq @suffixType "icon")}}
|
||||
{{d-icon @suffixValue}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</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>
|
||||
|
|
|
@ -1,23 +1,58 @@
|
|||
<div class={{concat "sidebar-section-wrapper sidebar-section-" @sectionName}}>
|
||||
<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}}
|
||||
</button>
|
||||
|
||||
<LinkTo
|
||||
@route={{@headerRoute}}
|
||||
@query={{@headerQuery}}
|
||||
@models={{if @headerModel (array @headerModel) (if @headerModels @headerModels (array))}}
|
||||
class="sidebar-section-header-link"
|
||||
title={{@headerLinkTitle}}>
|
||||
{{#if @headerRoute}}
|
||||
<LinkTo
|
||||
@route={{@headerRoute}}
|
||||
@query={{@headerQuery}}
|
||||
@models={{if
|
||||
@headerModel
|
||||
(array @headerModel)
|
||||
(if @headerModels @headerModels (array))
|
||||
}}
|
||||
class="sidebar-section-header-link"
|
||||
title={{@headerLinkTitle}}
|
||||
>
|
||||
|
||||
{{@headerLinkText}}
|
||||
</LinkTo>
|
||||
{{@headerLinkText}}
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
<span
|
||||
title={{@headerLinkTitle}}
|
||||
class="sidebar-section-header-text"
|
||||
>
|
||||
{{@headerLinkText}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if @headerAction}}
|
||||
<button type="button" class="sidebar-section-header-button" {{on "click" @headerAction}} title={{@headerActionTitle}}>
|
||||
{{d-icon @headerActionIcon}}
|
||||
</button>
|
||||
{{#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="edit-channels-dropdown"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
@headerRoute="tags"
|
||||
@headerLinkText={{i18n "sidebar.sections.tags.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.tags.header_link_title"}}
|
||||
@headerAction={{this.editTracked}}
|
||||
@headerActionTitle={{i18n "sidebar.sections.tags.header_action_title"}}
|
||||
@headerActionIcon="pencil-alt" >
|
||||
@headerActions={{array (hash action=this.editTracked title=(i18n "sidebar.sections.tags.header_action_title"))}}
|
||||
@headerActionsIcon="pencil-alt" >
|
||||
|
||||
{{#if (gt this.sectionLinks.length 0)}}
|
||||
{{#each this.sectionLinks as |sectionLink|}}
|
||||
|
|
|
@ -4,9 +4,8 @@
|
|||
@headerQuery={{hash f=undefined}}
|
||||
@headerLinkText={{i18n "sidebar.sections.topics.header_link_text"}}
|
||||
@headerLinkTitle={{i18n "sidebar.sections.topics.header_link_title"}}
|
||||
@headerActionIcon="plus"
|
||||
@headerAction={{this.composeTopic}}
|
||||
@headerActionTitle={{i18n "sidebar.sections.topics.header_action_title"}}>
|
||||
@headerActionsIcon="plus"
|
||||
@headerActions={{array (hash action=this.composeTopic title=(i18n "sidebar.sections.topics.header_action_title"))}}>
|
||||
|
||||
{{#each this.sectionLinks as |sectionLink|}}
|
||||
<Sidebar::SectionLink
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
});
|
||||
});
|
|
@ -118,13 +118,16 @@
|
|||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sidebar-section-header-link {
|
||||
.sidebar-section-header-link,
|
||||
.sidebar-section-header-text {
|
||||
@include ellipsis;
|
||||
flex: 1 1 auto;
|
||||
color: var(--primary);
|
||||
font-size: var(--font-down-1);
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
.sidebar-section-header-link {
|
||||
&:visited {
|
||||
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 {
|
||||
background: none;
|
||||
border: none;
|
||||
|
@ -148,6 +168,16 @@
|
|||
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 {
|
||||
margin-left: 1.5em;
|
||||
|
@ -257,3 +287,102 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue