From 0d72a8c458216698f55aaee54b8ffbe4d7750ce9 Mon Sep 17 00:00:00 2001 From: Alan Guo Xiang Tan Date: Mon, 18 Jul 2022 12:03:37 +0800 Subject: [PATCH] FEATURE: API for sidebar (#17296) This plugin API can be used to add to sections and links to sidebar --- .../discourse/app/components/sidebar.js | 10 + .../app/components/sidebar/section-link.js | 13 +- .../app/components/sidebar/section.js | 15 + .../discourse/app/lib/plugin-api.js | 129 ++++++- .../base-custom-sidebar-section-link.js | 106 ++++++ .../sidebar/base-custom-sidebar-section.js | 53 +++ .../app/lib/sidebar/custom-sections.js | 14 + .../app/templates/components/sidebar.hbs | 33 ++ .../components/sidebar/categories-section.hbs | 5 +- .../components/sidebar/messages-section.hbs | 2 +- .../components/sidebar/section-link.hbs | 38 ++ .../templates/components/sidebar/section.hbs | 61 +++- .../components/sidebar/tags-section.hbs | 5 +- .../components/sidebar/topics-section.hbs | 5 +- .../acceptance/sidebar-plugin-api-test.js | 335 ++++++++++++++++++ .../stylesheets/common/base/sidebar.scss | 131 ++++++- 16 files changed, 927 insertions(+), 28 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section-link.js create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js create mode 100644 app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js create mode 100644 app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js diff --git a/app/assets/javascripts/discourse/app/components/sidebar.js b/app/assets/javascripts/discourse/app/components/sidebar.js index 2235025463c..828c4b91c4a 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar.js +++ b/app/assets/javascripts/discourse/app/components/sidebar.js @@ -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 }); + }); } } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js index 87d5ddb040f..f34d267aa68 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section-link.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section-link.js @@ -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); + } +} diff --git a/app/assets/javascripts/discourse/app/components/sidebar/section.js b/app/assets/javascripts/discourse/app/components/sidebar/section.js index eec1b95969f..41c6c36b925 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/section.js @@ -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; + } } diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index bb2dd1457bc..a2a1c45812b 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -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 diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section-link.js new file mode 100644 index 00000000000..1004e5d0cd1 --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section-link.js @@ -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 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"; + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js new file mode 100644 index 00000000000..6de8e133ecc --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/base-custom-sidebar-section.js @@ -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"; + } +} diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js b/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js new file mode 100644 index 00000000000..88d594d376b --- /dev/null +++ b/app/assets/javascripts/discourse/app/lib/sidebar/custom-sections.js @@ -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); +} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs index 29cf9eade32..ad291073bd7 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar.hbs @@ -11,6 +11,39 @@ {{/if}} + {{#each this.customSections as |customSection|}} + + + {{#each customSection.links as |link|}} + + {{/each}} + + {{/each}} + {{!-- DO NOT USE, this outlet is temporary and will be removed. --}} {{!-- Outlet will be replaced with sidebar API. --}} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs index cd975451613..ccd781ff5bc 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/categories-section.hbs @@ -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|}} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/messages-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/messages-section.hbs index a9c9cc3f193..273d04331ff 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/messages-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/messages-section.hbs @@ -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"}} > diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs index 747286ac08a..2ff45cec2b2 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/section-link.hbs @@ -7,6 +7,22 @@ @current-when={{@currentWhen}} @title={{@title}} > + {{#if @prefixValue }} + + {{#if (eq @prefixType "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}} + + {{/if}} {{@content}} @@ -16,5 +32,27 @@ {{@badgeText}} {{/if}} + + {{#if @suffixValue}} + + {{#if (eq @suffixType "icon")}} + {{d-icon @suffixValue}} + {{/if}} + + {{/if}} + {{#if @hoverValue}} + + + + {{/if}} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/section.hbs index 73f28935176..dd74d6ade4a 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/section.hbs @@ -1,23 +1,58 @@
diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs index 7e37080e946..e50a5fd6788 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs @@ -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|}} diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs index 1c31a57a701..f634f5c5c57 100644 --- a/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs +++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/topics-section.hbs @@ -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|}} { + 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" + ); + }); +}); diff --git a/app/assets/stylesheets/common/base/sidebar.scss b/app/assets/stylesheets/common/base/sidebar.scss index 044a4b02791..fff18e2eef0 100644 --- a/app/assets/stylesheets/common/base/sidebar.scss +++ b/app/assets/stylesheets/common/base/sidebar.scss @@ -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; + } + } + } + } +}