mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 09:12:45 +08:00
FEATURE: Introduce APIs for manipulating header icons (#25916)
This commit is contained in:
parent
405bfba3c0
commit
b788c08712
|
@ -1,20 +1,30 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DAG from "discourse/lib/dag";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import not from "truth-helpers/helpers/not";
|
||||
import or from "truth-helpers/helpers/or";
|
||||
import MountWidget from "../mount-widget";
|
||||
import Dropdown from "./dropdown";
|
||||
import PanelPortal from "./panel-portal";
|
||||
import UserDropdown from "./user-dropdown";
|
||||
|
||||
let _extraHeaderIcons = [];
|
||||
export function addToHeaderIcons(icon) {
|
||||
_extraHeaderIcons.push(icon);
|
||||
let headerIcons;
|
||||
resetHeaderIcons();
|
||||
|
||||
function resetHeaderIcons() {
|
||||
headerIcons = new DAG({ defaultPosition: { before: "search" } });
|
||||
headerIcons.add("search");
|
||||
headerIcons.add("hamburger", undefined, { after: "search" });
|
||||
headerIcons.add("user-menu", undefined, { after: "hamburger" });
|
||||
}
|
||||
|
||||
export function headerIconsDAG() {
|
||||
return headerIcons;
|
||||
}
|
||||
|
||||
export function clearExtraHeaderIcons() {
|
||||
_extraHeaderIcons = [];
|
||||
resetHeaderIcons();
|
||||
}
|
||||
|
||||
export default class Icons extends Component {
|
||||
|
@ -23,51 +33,44 @@ export default class Icons extends Component {
|
|||
@service header;
|
||||
@service search;
|
||||
|
||||
_isStringType = (icon) => typeof icon === "string";
|
||||
|
||||
<template>
|
||||
<ul class="icons d-header-icons">
|
||||
{{#each _extraHeaderIcons as |Icon|}}
|
||||
{{#if (this._isStringType Icon)}}
|
||||
<MountWidget @widget={{Icon}} />
|
||||
{{else}}
|
||||
{{#let
|
||||
(component PanelPortal panelElement=@panelElement)
|
||||
as |panelPortal|
|
||||
}}
|
||||
<Icon @panelPortal={{panelPortal}} />
|
||||
{{/let}}
|
||||
{{#each (headerIcons.resolve) as |entry|}}
|
||||
{{#if (eq entry.key "search")}}
|
||||
<Dropdown
|
||||
@title="search.title"
|
||||
@icon="search"
|
||||
@iconId={{@searchButtonId}}
|
||||
@onClick={{@toggleSearchMenu}}
|
||||
@active={{this.search.visible}}
|
||||
@href={{getURL "/search"}}
|
||||
@className="search-dropdown"
|
||||
@targetSelector=".search-menu-panel"
|
||||
/>
|
||||
{{else if (eq entry.key "hamburger")}}
|
||||
{{#if (or (not @sidebarEnabled) this.site.mobileView)}}
|
||||
<Dropdown
|
||||
@title="hamburger_menu"
|
||||
@icon="bars"
|
||||
@iconId="toggle-hamburger-menu"
|
||||
@active={{this.header.hamburgerVisible}}
|
||||
@onClick={{@toggleHamburger}}
|
||||
@className="hamburger-dropdown"
|
||||
/>
|
||||
{{/if}}
|
||||
{{else if (eq entry.key "user-menu")}}
|
||||
{{#if this.currentUser}}
|
||||
<UserDropdown
|
||||
@active={{this.header.userVisible}}
|
||||
@toggleUserMenu={{@toggleUserMenu}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{else if entry.value}}
|
||||
<entry.value
|
||||
@panelPortal={{component PanelPortal panelElement=@panelElement}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
<Dropdown
|
||||
@title="search.title"
|
||||
@icon="search"
|
||||
@iconId={{@searchButtonId}}
|
||||
@onClick={{@toggleSearchMenu}}
|
||||
@active={{this.search.visible}}
|
||||
@href={{getURL "/search"}}
|
||||
@className="search-dropdown"
|
||||
@targetSelector=".search-menu-panel"
|
||||
/>
|
||||
|
||||
{{#if (or (not @sidebarEnabled) this.site.mobileView)}}
|
||||
<Dropdown
|
||||
@title="hamburger_menu"
|
||||
@icon="bars"
|
||||
@iconId="toggle-hamburger-menu"
|
||||
@active={{this.header.hamburgerVisible}}
|
||||
@onClick={{@toggleHamburger}}
|
||||
@className="hamburger-dropdown"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.currentUser}}
|
||||
<UserDropdown
|
||||
@active={{this.header.userVisible}}
|
||||
@toggleUserMenu={{@toggleUserMenu}}
|
||||
/>
|
||||
{{/if}}
|
||||
</ul>
|
||||
</template>
|
||||
}
|
||||
|
|
109
app/assets/javascripts/discourse/app/lib/dag.js
Normal file
109
app/assets/javascripts/discourse/app/lib/dag.js
Normal file
|
@ -0,0 +1,109 @@
|
|||
import DAGMap from "dag-map";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
function ensureArray(val) {
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}
|
||||
|
||||
export default class DAG {
|
||||
#defaultPosition;
|
||||
#rawData = new Map();
|
||||
#dag = new DAGMap();
|
||||
|
||||
constructor(args) {
|
||||
// allows for custom default positioning of new items added to the DAG, eg
|
||||
// new DAG({ defaultPosition: { before: "foo", after: "bar" } });
|
||||
this.#defaultPosition = args?.defaultPosition || {};
|
||||
}
|
||||
|
||||
#defaultPositionForKey(key) {
|
||||
const pos = { ...this.#defaultPosition };
|
||||
if (ensureArray(pos.before).includes(key)) {
|
||||
delete pos.before;
|
||||
}
|
||||
if (ensureArray(pos.after).includes(key)) {
|
||||
delete pos.after;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a key/value pair to the map. Can optionally specify before/after position requirements.
|
||||
*
|
||||
* @param {string} key The key of the item to be added. Can be referenced by other member's postition parameters.
|
||||
* @param {any} value
|
||||
* @param {Object} position
|
||||
* @param {string | string[]} position.before A key or array of keys of items which should appear before this one.
|
||||
* @param {string | string[]} position.after A key or array of keys of items which should appear after this one.
|
||||
*/
|
||||
add(key, value, position) {
|
||||
position ||= this.#defaultPositionForKey(key);
|
||||
const { before, after } = position;
|
||||
this.#rawData.set(key, {
|
||||
value,
|
||||
before,
|
||||
after,
|
||||
});
|
||||
this.#dag.add(key, value, before, after);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the map by key. no-op if the key does not exist.
|
||||
*
|
||||
* @param {string} key The key of the item to be removed.
|
||||
*/
|
||||
delete(key) {
|
||||
this.#rawData.delete(key);
|
||||
this.#refreshDAG();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the positioning rules of an existing item in the map. Will replace all existing rules. No-op if the key does not exist.
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {string | string[]} position.before A key or array of keys of items which should appear before this one.
|
||||
* @param {string | string[]} position.after A key or array of keys of items which should appear after this one.
|
||||
*/
|
||||
reposition(key, { before, after }) {
|
||||
const node = this.#rawData.get(key);
|
||||
if (node) {
|
||||
node.before = before;
|
||||
node.after = after;
|
||||
}
|
||||
this.#refreshDAG();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an item exists in the map.
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*
|
||||
*/
|
||||
has(key) {
|
||||
return this.#rawData.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the resolved key/value pairs in the map. The order of the pairs is determined by the before/after rules.
|
||||
* @returns {Array<[key: string, value: any]}>} An array of key/value pairs.
|
||||
*
|
||||
*/
|
||||
@bind
|
||||
resolve() {
|
||||
const result = [];
|
||||
this.#dag.each((key, value) => result.push({ key, value }));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* DAGMap doesn't support removing or modifying keys, so we
|
||||
* need to completely recreate it from the raw data
|
||||
*/
|
||||
#refreshDAG() {
|
||||
const newDAG = new DAGMap();
|
||||
for (const [key, { value, before, after }] of this.#rawData) {
|
||||
newDAG.add(key, value, before, after);
|
||||
}
|
||||
this.#dag = newDAG;
|
||||
}
|
||||
}
|
|
@ -9,11 +9,13 @@ import {
|
|||
import { addPluginDocumentTitleCounter } from "discourse/components/d-document";
|
||||
import { addToolbarCallback } from "discourse/components/d-editor";
|
||||
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
|
||||
import { addToHeaderIcons as addToGlimmerHeaderIcons } from "discourse/components/glimmer-header/icons";
|
||||
import { headerIconsDAG } from "discourse/components/glimmer-header/icons";
|
||||
import { forceDropdownForMenuPanels as glimmerForceDropdownForMenuPanels } from "discourse/components/glimmer-site-header";
|
||||
import { addGlobalNotice } from "discourse/components/global-notice";
|
||||
import { _addBulkButton } from "discourse/components/modal/topic-bulk-actions";
|
||||
import { addWidgetCleanCallback } from "discourse/components/mount-widget";
|
||||
import MountWidget, {
|
||||
addWidgetCleanCallback,
|
||||
} from "discourse/components/mount-widget";
|
||||
import { addPluginOutletDecorator } from "discourse/components/plugin-connector";
|
||||
import {
|
||||
addPluginReviewableParam,
|
||||
|
@ -98,10 +100,7 @@ import {
|
|||
import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
|
||||
import { setNotificationsLimit } from "discourse/routes/user-notifications";
|
||||
import { addComposerSaveErrorCallback } from "discourse/services/composer";
|
||||
import {
|
||||
addToHeaderIcons,
|
||||
attachAdditionalPanel,
|
||||
} from "discourse/widgets/header";
|
||||
import { attachAdditionalPanel } from "discourse/widgets/header";
|
||||
import { addPostClassesCallback } from "discourse/widgets/post";
|
||||
import { addDecorator } from "discourse/widgets/post-cooked";
|
||||
import {
|
||||
|
@ -144,7 +143,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
|||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
||||
|
||||
export const PLUGIN_API_VERSION = "1.27.0";
|
||||
export const PLUGIN_API_VERSION = "1.28.0";
|
||||
|
||||
const DEPRECATED_HEADER_WIDGETS = [
|
||||
"header",
|
||||
|
@ -802,16 +801,16 @@ class PluginApi {
|
|||
}
|
||||
|
||||
/**
|
||||
Called whenever the "page" changes. This allows us to set up analytics
|
||||
and other tracking.
|
||||
|
||||
To get notified when the page changes, you can install a hook like so:
|
||||
|
||||
```javascript
|
||||
api.onPageChange((url, title) => {
|
||||
console.log('the page changed to: ' + url + ' and title ' + title);
|
||||
});
|
||||
```
|
||||
* Called whenever the "page" changes. This allows us to set up analytics
|
||||
* and other tracking.
|
||||
*
|
||||
* To get notified when the page changes, you can install a hook like so:
|
||||
*
|
||||
* ```javascript
|
||||
* api.onPageChange((url, title) => {
|
||||
* console.log('the page changed to: ' + url + ' and title ' + title);
|
||||
* });
|
||||
* ```
|
||||
**/
|
||||
onPageChange(fn) {
|
||||
const callback = wrapWithErrorHandler(fn, "broken_page_change_alert");
|
||||
|
@ -819,13 +818,13 @@ class PluginApi {
|
|||
}
|
||||
|
||||
/**
|
||||
Listen for a triggered `AppEvent` from Discourse.
|
||||
|
||||
```javascript
|
||||
api.onAppEvent('inserted-custom-html', () => {
|
||||
console.log('a custom footer was rendered');
|
||||
});
|
||||
```
|
||||
* Listen for a triggered `AppEvent` from Discourse.
|
||||
*
|
||||
* ```javascript
|
||||
* api.onAppEvent('inserted-custom-html', () => {
|
||||
* console.log('a custom footer was rendered');
|
||||
* });
|
||||
* ```
|
||||
**/
|
||||
onAppEvent(name, fn) {
|
||||
const appEvents = this._lookupContainer("service:app-events");
|
||||
|
@ -833,18 +832,18 @@ class PluginApi {
|
|||
}
|
||||
|
||||
/**
|
||||
Registers a function to generate custom avatar CSS classes
|
||||
for a particular user.
|
||||
|
||||
Takes a function that will accept a user as a parameter
|
||||
and return an array of CSS classes to apply.
|
||||
|
||||
```javascript
|
||||
api.customUserAvatarClasses(user => {
|
||||
if (get(user, 'primary_group_name') === 'managers') {
|
||||
return ['managers'];
|
||||
}
|
||||
});
|
||||
* Registers a function to generate custom avatar CSS classes
|
||||
* for a particular user.
|
||||
*
|
||||
* Takes a function that will accept a user as a parameter
|
||||
* and return an array of CSS classes to apply.
|
||||
*
|
||||
* ```javascript
|
||||
* api.customUserAvatarClasses(user => {
|
||||
* if (get(user, 'primary_group_name') === 'managers') {
|
||||
* return ['managers'];
|
||||
* }
|
||||
* });
|
||||
**/
|
||||
customUserAvatarClasses(fn) {
|
||||
registerCustomAvatarHelper(fn);
|
||||
|
@ -967,7 +966,7 @@ class PluginApi {
|
|||
**/
|
||||
addHeaderPanel(name, toggle, transformAttrs) {
|
||||
deprecated(
|
||||
"addHeaderPanel has been removed. Use api.addToHeaderIcons instead.",
|
||||
"addHeaderPanel will be removed as part of the glimmer header upgrade. Use api.headerIcons instead.",
|
||||
{
|
||||
id: "discourse.add-header-panel",
|
||||
url: "https://meta.discourse.org/t/296544",
|
||||
|
@ -1688,11 +1687,12 @@ class PluginApi {
|
|||
*
|
||||
* Example:
|
||||
*
|
||||
* ```javascript
|
||||
* let aPlugin = {
|
||||
'after:highlightElement': ({ el, result, text }) => {
|
||||
console.log(el);
|
||||
}
|
||||
}
|
||||
* "after:highlightElement": ({ el, result, text }) => {
|
||||
* console.log(el);
|
||||
* }
|
||||
* }
|
||||
* api.registerHighlightJSPlugin(aPlugin);
|
||||
**/
|
||||
registerHighlightJSPlugin(plugin) {
|
||||
|
@ -1705,7 +1705,6 @@ class PluginApi {
|
|||
* Example:
|
||||
*
|
||||
* api.addGlobalNotice("text", "foo", { html: "<p>bar</p>" })
|
||||
*
|
||||
**/
|
||||
addGlobalNotice(text, id, options) {
|
||||
addGlobalNotice(text, id, options);
|
||||
|
@ -1744,7 +1743,6 @@ class PluginApi {
|
|||
* ```
|
||||
*
|
||||
* @deprecated because modifying an Ember-rendered DOM tree can lead to very unexpected errors. Use CSS or plugin outlet connectors instead
|
||||
*
|
||||
**/
|
||||
decoratePluginOutlet(outletName, callback, opts) {
|
||||
deprecated(
|
||||
|
@ -1804,22 +1802,70 @@ class PluginApi {
|
|||
/**
|
||||
* Allows adding icons to the category-link html
|
||||
*
|
||||
* ```
|
||||
* ```javascript
|
||||
* api.addCategoryLinkIcon((category) => {
|
||||
* if (category.someProperty) {
|
||||
return "eye"
|
||||
}
|
||||
* if (category.someProperty) {
|
||||
* return "eye"
|
||||
* }
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
addCategoryLinkIcon(renderer) {
|
||||
addExtraIconRenderer(renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a widget or a component to the header-icon ul.
|
||||
* Allows for manipulation of the header icons. This includes, adding, removing, or modifying the order of icons.
|
||||
*
|
||||
* If adding a widget it must already be created. You can create new widgets
|
||||
* Only the passing of components is supported, and by default the icons are added to the left of exisiting icons.
|
||||
*
|
||||
* Example: Add the chat icon to the header icons after the search icon
|
||||
* ```
|
||||
* api.headerIcons.add(
|
||||
* "chat",
|
||||
* ChatIconComponent,
|
||||
* { after: "search" }
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* Example: Remove the chat icon from the header icons
|
||||
* ```
|
||||
* api.headerIcons.delete("chat")
|
||||
* ```
|
||||
*
|
||||
* Example: Reposition the chat icon to be before the user-menu icon and after the hamburger icon
|
||||
* ```
|
||||
* api.headerIcons.reposition("chat", { before: "user-menu", after: "hamburger" })
|
||||
* ```
|
||||
*
|
||||
* Example: Check if the chat icon is present in the header icons (returns true of false)
|
||||
* ```
|
||||
* api.headerIcons.has("chat")
|
||||
* ```
|
||||
*
|
||||
* Additionally, you can utilize the `@panelPortal` argument to create a dropdown panel. This can be useful when
|
||||
* you want create a button in the header that opens a dropdown panel with additional content.
|
||||
*
|
||||
* ```
|
||||
* const IconWithDropdown = <template>
|
||||
* <DButton @icon="icon" @onClick={{this.toggleVisible}} />
|
||||
* {{#if this.visible}}
|
||||
* <@panelPortal>
|
||||
* <div>Panel</div>
|
||||
* </@panelPortal>
|
||||
* {{/if}}
|
||||
* </template>;
|
||||
*
|
||||
* api.headerIcons.add("icon-name", IconWithDropdown, { before: "search" })
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
get headerIcons() {
|
||||
return headerIconsDAG();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a widget to the header-icon ul. The widget must already be created. You can create new widgets
|
||||
* in a theme or plugin via an initializer prior to calling this function.
|
||||
*
|
||||
* ```
|
||||
|
@ -1827,26 +1873,21 @@ class PluginApi {
|
|||
* createWidget("some-widget")
|
||||
* ```
|
||||
*
|
||||
* If adding a component you can pass the component directly. Additionally, you can
|
||||
* utilize the `@panelPortal` argument to create a dropdown panel. This can be useful when
|
||||
* you want create a button in the header that opens a dropdown panel with additional content.
|
||||
*
|
||||
* ```
|
||||
* api.addToHeaderIcons(
|
||||
<template>
|
||||
<span>Icon</span>
|
||||
|
||||
<@panelPortal>
|
||||
<div>Panel</div>
|
||||
</@panelPortal>
|
||||
</template>
|
||||
);
|
||||
* ```
|
||||
*
|
||||
**/
|
||||
addToHeaderIcons(icon) {
|
||||
addToHeaderIcons(icon);
|
||||
addToGlimmerHeaderIcons(icon);
|
||||
deprecated(
|
||||
"addToHeaderIcons has been deprecated. Use api.headerIcons instead.",
|
||||
{
|
||||
id: "discourse.add-header-icons",
|
||||
url: "https://meta.discourse.org/t/296544",
|
||||
}
|
||||
);
|
||||
|
||||
this.headerIcons.add(
|
||||
icon,
|
||||
<template><MountWidget @widget={{icon}} /></template>,
|
||||
{ before: "search" }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2032,17 +2073,17 @@ class PluginApi {
|
|||
/**
|
||||
* Download calendar modal which allow to pick between ICS and Google Calendar. Optionally, recurrence rule can be specified - https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10
|
||||
*
|
||||
* ```
|
||||
* api.downloadCalendar("title of the event", [
|
||||
* {
|
||||
startsAt: "2021-10-12T15:00:00.000Z",
|
||||
endsAt: "2021-10-12T16:00:00.000Z",
|
||||
},
|
||||
* ],
|
||||
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
|
||||
* ```javascript
|
||||
* api.downloadCalendar("title of the event",
|
||||
* [
|
||||
* {
|
||||
* startsAt: "2021-10-12T15:00:00.000Z",
|
||||
* endsAt: "2021-10-12T16:00:00.000Z",
|
||||
* },
|
||||
* ],
|
||||
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
downloadCalendar(title, dates, recurrenceRule = null) {
|
||||
downloadCalendar(title, dates, recurrenceRule);
|
||||
|
@ -2103,9 +2144,10 @@ class PluginApi {
|
|||
* Add custom user search options.
|
||||
* It is heavily correlated with `register_groups_callback_for_users_search_controller_action` which allows defining custom filter.
|
||||
* Example usage:
|
||||
*
|
||||
* ```
|
||||
* api.addUserSearchOption("adminsOnly");
|
||||
|
||||
*
|
||||
* register_groups_callback_for_users_search_controller_action(:admins_only) do |groups, user|
|
||||
* groups.where(name: "admins")
|
||||
* end
|
||||
|
@ -2426,7 +2468,7 @@ class PluginApi {
|
|||
* This is intended to replace the admin-menu plugin outlet from
|
||||
* the old admin horizontal nav.
|
||||
*
|
||||
* ```
|
||||
* ```javascript
|
||||
* api.addAdminSidebarSectionLink("root", {
|
||||
* name: "unique_link_name",
|
||||
* label: "admin.some.i18n.label.key",
|
||||
|
@ -2434,7 +2476,7 @@ class PluginApi {
|
|||
* href: "(optional) can be used instead of the route",
|
||||
* }
|
||||
* ```
|
||||
|
||||
*
|
||||
* @param {String} sectionName - The name of the admin sidebar section to add the link to.
|
||||
* @param {Object} link - A link object representing a section link for the sidebar.
|
||||
* @param {string} link.name - The name of the link. Needs to be dasherized and lowercase.
|
|
@ -2,6 +2,7 @@ import { schedule } from "@ember/runloop";
|
|||
import { hbs } from "ember-cli-htmlbars";
|
||||
import $ from "jquery";
|
||||
import { h } from "virtual-dom";
|
||||
import { headerIconsDAG } from "discourse/components/glimmer-header/icons";
|
||||
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import scrollLock from "discourse/lib/scroll-lock";
|
||||
|
@ -22,14 +23,11 @@ import I18n from "discourse-i18n";
|
|||
const SEARCH_BUTTON_ID = "search-button";
|
||||
export const PANEL_WRAPPER_ID = "additional-panel-wrapper";
|
||||
|
||||
let _extraHeaderIcons = [];
|
||||
|
||||
export function addToHeaderIcons(icon) {
|
||||
_extraHeaderIcons.push(icon);
|
||||
}
|
||||
let _extraHeaderIcons;
|
||||
clearExtraHeaderIcons();
|
||||
|
||||
export function clearExtraHeaderIcons() {
|
||||
_extraHeaderIcons = [];
|
||||
_extraHeaderIcons = headerIconsDAG();
|
||||
}
|
||||
|
||||
export const dropdown = {
|
||||
|
@ -249,15 +247,13 @@ createWidget("header-icons", {
|
|||
|
||||
const icons = [];
|
||||
|
||||
if (_extraHeaderIcons) {
|
||||
_extraHeaderIcons.forEach((icon) => {
|
||||
if (typeof icon === "string") {
|
||||
icons.push(this.attach(icon));
|
||||
} else {
|
||||
icons.push(this.attach("extra-icon", { component: icon }));
|
||||
}
|
||||
});
|
||||
}
|
||||
const resolvedIcons = _extraHeaderIcons.resolve();
|
||||
resolvedIcons.forEach((icon) => {
|
||||
if (["search", "user-menu", "hamburger"].includes(icon.key)) {
|
||||
return;
|
||||
}
|
||||
icons.push(this.attach("extra-icon", { component: icon.value }));
|
||||
});
|
||||
|
||||
const search = this.attach("header-dropdown", {
|
||||
title: "search.title",
|
||||
|
|
81
app/assets/javascripts/discourse/tests/unit/lib/dag-test.js
Normal file
81
app/assets/javascripts/discourse/tests/unit/lib/dag-test.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { setupTest } from "ember-qunit";
|
||||
import { module, test } from "qunit";
|
||||
import DAG from "discourse/lib/dag";
|
||||
|
||||
module("Unit | Lib | DAG", function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
let dag;
|
||||
|
||||
test("should add items to the map", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.ok(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
});
|
||||
|
||||
test("should remove an item from the map", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
dag.delete("key2");
|
||||
|
||||
assert.ok(dag.has("key1"));
|
||||
assert.notOk(dag.has("key2"));
|
||||
assert.ok(dag.has("key3"));
|
||||
});
|
||||
|
||||
test("should reposition an item in the map", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
dag.reposition("key3", { before: "key1" });
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
|
||||
assert.deepEqual(keys, ["key3", "key1", "key2"]);
|
||||
});
|
||||
|
||||
test("should resolve the map in the correct order", function (assert) {
|
||||
dag = new DAG();
|
||||
dag.add("key1", "value1");
|
||||
dag.add("key2", "value2");
|
||||
dag.add("key3", "value3");
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
|
||||
assert.deepEqual(keys, ["key1", "key2", "key3"]);
|
||||
});
|
||||
|
||||
test("allows for custom before and after default positioning", function (assert) {
|
||||
dag = new DAG({ defaultPosition: { before: "key3", after: "key2" } });
|
||||
dag.add("key1", "value1", {});
|
||||
dag.add("key2", "value2", { after: "key1" });
|
||||
dag.add("key3", "value3", { after: "key2" });
|
||||
dag.add("key4", "value4");
|
||||
|
||||
const resolved = dag.resolve();
|
||||
const keys = resolved.map((pair) => pair.key);
|
||||
|
||||
assert.deepEqual(keys, ["key1", "key2", "key4", "key3"]);
|
||||
});
|
||||
|
||||
test("throws on bad positioning", function (assert) {
|
||||
dag = new DAG();
|
||||
|
||||
assert.throws(
|
||||
() => dag.add("key1", "value1", { before: "key1" }),
|
||||
/cycle detected/
|
||||
);
|
||||
});
|
||||
});
|
|
@ -7,9 +7,13 @@ in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.28.0] - 2024-02-21
|
||||
|
||||
- added `headerIcons` which allows for manipulation of the header icons. This includes, adding, removing, or modifying the order of icons.
|
||||
|
||||
## [1.27.0] - 2024-02-21
|
||||
|
||||
- Updated `addToHeaderIcons` to take a component instead of just a widget (masked a string). Additionally, you can can now utilize the `@panelPortal` argument to create a dropdown panel. This can be useful when * you want create a button in the header that opens a dropdown panel with additional content.
|
||||
- deprecated `addToHeaderIcons` in favor of `headerIcons`
|
||||
|
||||
## [1.26.0] - 2024-02-21
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ export default {
|
|||
api.addCardClickListenerSelector(".chat-drawer-outlet");
|
||||
|
||||
if (this.chatService.userCanChat) {
|
||||
api.addToHeaderIcons(ChatHeaderIcon);
|
||||
api.headerIcons.add("chat", ChatHeaderIcon);
|
||||
}
|
||||
|
||||
api.addStyleguideSection?.({
|
||||
|
|
Loading…
Reference in New Issue
Block a user