mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 15:52:11 +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 Component from "@glimmer/component";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import DAG from "discourse/lib/dag";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
import eq from "truth-helpers/helpers/eq";
|
||||||
import not from "truth-helpers/helpers/not";
|
import not from "truth-helpers/helpers/not";
|
||||||
import or from "truth-helpers/helpers/or";
|
import or from "truth-helpers/helpers/or";
|
||||||
import MountWidget from "../mount-widget";
|
|
||||||
import Dropdown from "./dropdown";
|
import Dropdown from "./dropdown";
|
||||||
import PanelPortal from "./panel-portal";
|
import PanelPortal from "./panel-portal";
|
||||||
import UserDropdown from "./user-dropdown";
|
import UserDropdown from "./user-dropdown";
|
||||||
|
|
||||||
let _extraHeaderIcons = [];
|
let headerIcons;
|
||||||
export function addToHeaderIcons(icon) {
|
resetHeaderIcons();
|
||||||
_extraHeaderIcons.push(icon);
|
|
||||||
|
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() {
|
export function clearExtraHeaderIcons() {
|
||||||
_extraHeaderIcons = [];
|
resetHeaderIcons();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Icons extends Component {
|
export default class Icons extends Component {
|
||||||
|
@ -23,51 +33,44 @@ export default class Icons extends Component {
|
||||||
@service header;
|
@service header;
|
||||||
@service search;
|
@service search;
|
||||||
|
|
||||||
_isStringType = (icon) => typeof icon === "string";
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ul class="icons d-header-icons">
|
<ul class="icons d-header-icons">
|
||||||
{{#each _extraHeaderIcons as |Icon|}}
|
{{#each (headerIcons.resolve) as |entry|}}
|
||||||
{{#if (this._isStringType Icon)}}
|
{{#if (eq entry.key "search")}}
|
||||||
<MountWidget @widget={{Icon}} />
|
<Dropdown
|
||||||
{{else}}
|
@title="search.title"
|
||||||
{{#let
|
@icon="search"
|
||||||
(component PanelPortal panelElement=@panelElement)
|
@iconId={{@searchButtonId}}
|
||||||
as |panelPortal|
|
@onClick={{@toggleSearchMenu}}
|
||||||
}}
|
@active={{this.search.visible}}
|
||||||
<Icon @panelPortal={{panelPortal}} />
|
@href={{getURL "/search"}}
|
||||||
{{/let}}
|
@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}}
|
{{/if}}
|
||||||
{{/each}}
|
{{/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>
|
</ul>
|
||||||
</template>
|
</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 { addPluginDocumentTitleCounter } from "discourse/components/d-document";
|
||||||
import { addToolbarCallback } from "discourse/components/d-editor";
|
import { addToolbarCallback } from "discourse/components/d-editor";
|
||||||
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
|
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 { forceDropdownForMenuPanels as glimmerForceDropdownForMenuPanels } from "discourse/components/glimmer-site-header";
|
||||||
import { addGlobalNotice } from "discourse/components/global-notice";
|
import { addGlobalNotice } from "discourse/components/global-notice";
|
||||||
import { _addBulkButton } from "discourse/components/modal/topic-bulk-actions";
|
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 { addPluginOutletDecorator } from "discourse/components/plugin-connector";
|
||||||
import {
|
import {
|
||||||
addPluginReviewableParam,
|
addPluginReviewableParam,
|
||||||
|
@ -98,10 +100,7 @@ import {
|
||||||
import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
|
import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
|
||||||
import { setNotificationsLimit } from "discourse/routes/user-notifications";
|
import { setNotificationsLimit } from "discourse/routes/user-notifications";
|
||||||
import { addComposerSaveErrorCallback } from "discourse/services/composer";
|
import { addComposerSaveErrorCallback } from "discourse/services/composer";
|
||||||
import {
|
import { attachAdditionalPanel } from "discourse/widgets/header";
|
||||||
addToHeaderIcons,
|
|
||||||
attachAdditionalPanel,
|
|
||||||
} from "discourse/widgets/header";
|
|
||||||
import { addPostClassesCallback } from "discourse/widgets/post";
|
import { addPostClassesCallback } from "discourse/widgets/post";
|
||||||
import { addDecorator } from "discourse/widgets/post-cooked";
|
import { addDecorator } from "discourse/widgets/post-cooked";
|
||||||
import {
|
import {
|
||||||
|
@ -144,7 +143,7 @@ import { modifySelectKit } from "select-kit/mixins/plugin-api";
|
||||||
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
|
||||||
// using the format described at https://keepachangelog.com/en/1.0.0/.
|
// 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 = [
|
const DEPRECATED_HEADER_WIDGETS = [
|
||||||
"header",
|
"header",
|
||||||
|
@ -802,16 +801,16 @@ class PluginApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Called whenever the "page" changes. This allows us to set up analytics
|
* Called whenever the "page" changes. This allows us to set up analytics
|
||||||
and other tracking.
|
* and other tracking.
|
||||||
|
*
|
||||||
To get notified when the page changes, you can install a hook like so:
|
* To get notified when the page changes, you can install a hook like so:
|
||||||
|
*
|
||||||
```javascript
|
* ```javascript
|
||||||
api.onPageChange((url, title) => {
|
* api.onPageChange((url, title) => {
|
||||||
console.log('the page changed to: ' + url + ' and title ' + title);
|
* console.log('the page changed to: ' + url + ' and title ' + title);
|
||||||
});
|
* });
|
||||||
```
|
* ```
|
||||||
**/
|
**/
|
||||||
onPageChange(fn) {
|
onPageChange(fn) {
|
||||||
const callback = wrapWithErrorHandler(fn, "broken_page_change_alert");
|
const callback = wrapWithErrorHandler(fn, "broken_page_change_alert");
|
||||||
|
@ -819,13 +818,13 @@ class PluginApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Listen for a triggered `AppEvent` from Discourse.
|
* Listen for a triggered `AppEvent` from Discourse.
|
||||||
|
*
|
||||||
```javascript
|
* ```javascript
|
||||||
api.onAppEvent('inserted-custom-html', () => {
|
* api.onAppEvent('inserted-custom-html', () => {
|
||||||
console.log('a custom footer was rendered');
|
* console.log('a custom footer was rendered');
|
||||||
});
|
* });
|
||||||
```
|
* ```
|
||||||
**/
|
**/
|
||||||
onAppEvent(name, fn) {
|
onAppEvent(name, fn) {
|
||||||
const appEvents = this._lookupContainer("service:app-events");
|
const appEvents = this._lookupContainer("service:app-events");
|
||||||
|
@ -833,18 +832,18 @@ class PluginApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Registers a function to generate custom avatar CSS classes
|
* Registers a function to generate custom avatar CSS classes
|
||||||
for a particular user.
|
* for a particular user.
|
||||||
|
*
|
||||||
Takes a function that will accept a user as a parameter
|
* Takes a function that will accept a user as a parameter
|
||||||
and return an array of CSS classes to apply.
|
* and return an array of CSS classes to apply.
|
||||||
|
*
|
||||||
```javascript
|
* ```javascript
|
||||||
api.customUserAvatarClasses(user => {
|
* api.customUserAvatarClasses(user => {
|
||||||
if (get(user, 'primary_group_name') === 'managers') {
|
* if (get(user, 'primary_group_name') === 'managers') {
|
||||||
return ['managers'];
|
* return ['managers'];
|
||||||
}
|
* }
|
||||||
});
|
* });
|
||||||
**/
|
**/
|
||||||
customUserAvatarClasses(fn) {
|
customUserAvatarClasses(fn) {
|
||||||
registerCustomAvatarHelper(fn);
|
registerCustomAvatarHelper(fn);
|
||||||
|
@ -967,7 +966,7 @@ class PluginApi {
|
||||||
**/
|
**/
|
||||||
addHeaderPanel(name, toggle, transformAttrs) {
|
addHeaderPanel(name, toggle, transformAttrs) {
|
||||||
deprecated(
|
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",
|
id: "discourse.add-header-panel",
|
||||||
url: "https://meta.discourse.org/t/296544",
|
url: "https://meta.discourse.org/t/296544",
|
||||||
|
@ -1688,11 +1687,12 @@ class PluginApi {
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
*
|
*
|
||||||
|
* ```javascript
|
||||||
* let aPlugin = {
|
* let aPlugin = {
|
||||||
'after:highlightElement': ({ el, result, text }) => {
|
* "after:highlightElement": ({ el, result, text }) => {
|
||||||
console.log(el);
|
* console.log(el);
|
||||||
}
|
* }
|
||||||
}
|
* }
|
||||||
* api.registerHighlightJSPlugin(aPlugin);
|
* api.registerHighlightJSPlugin(aPlugin);
|
||||||
**/
|
**/
|
||||||
registerHighlightJSPlugin(plugin) {
|
registerHighlightJSPlugin(plugin) {
|
||||||
|
@ -1705,7 +1705,6 @@ class PluginApi {
|
||||||
* Example:
|
* Example:
|
||||||
*
|
*
|
||||||
* api.addGlobalNotice("text", "foo", { html: "<p>bar</p>" })
|
* api.addGlobalNotice("text", "foo", { html: "<p>bar</p>" })
|
||||||
*
|
|
||||||
**/
|
**/
|
||||||
addGlobalNotice(text, id, options) {
|
addGlobalNotice(text, id, options) {
|
||||||
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
|
* @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) {
|
decoratePluginOutlet(outletName, callback, opts) {
|
||||||
deprecated(
|
deprecated(
|
||||||
|
@ -1804,22 +1802,70 @@ class PluginApi {
|
||||||
/**
|
/**
|
||||||
* Allows adding icons to the category-link html
|
* Allows adding icons to the category-link html
|
||||||
*
|
*
|
||||||
* ```
|
* ```javascript
|
||||||
* api.addCategoryLinkIcon((category) => {
|
* api.addCategoryLinkIcon((category) => {
|
||||||
* if (category.someProperty) {
|
* if (category.someProperty) {
|
||||||
return "eye"
|
* return "eye"
|
||||||
}
|
* }
|
||||||
* });
|
* });
|
||||||
* ```
|
* ```
|
||||||
*
|
|
||||||
**/
|
**/
|
||||||
addCategoryLinkIcon(renderer) {
|
addCategoryLinkIcon(renderer) {
|
||||||
addExtraIconRenderer(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.
|
* in a theme or plugin via an initializer prior to calling this function.
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
|
@ -1827,26 +1873,21 @@ class PluginApi {
|
||||||
* createWidget("some-widget")
|
* 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) {
|
||||||
addToHeaderIcons(icon);
|
deprecated(
|
||||||
addToGlimmerHeaderIcons(icon);
|
"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
|
* 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
|
||||||
*
|
*
|
||||||
* ```
|
* ```javascript
|
||||||
* api.downloadCalendar("title of the event", [
|
* api.downloadCalendar("title of the event",
|
||||||
* {
|
* [
|
||||||
startsAt: "2021-10-12T15:00:00.000Z",
|
* {
|
||||||
endsAt: "2021-10-12T16:00:00.000Z",
|
* startsAt: "2021-10-12T15:00:00.000Z",
|
||||||
},
|
* endsAt: "2021-10-12T16:00:00.000Z",
|
||||||
* ],
|
* },
|
||||||
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
|
* ],
|
||||||
|
* "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
|
||||||
* );
|
* );
|
||||||
* ```
|
* ```
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
downloadCalendar(title, dates, recurrenceRule = null) {
|
downloadCalendar(title, dates, recurrenceRule = null) {
|
||||||
downloadCalendar(title, dates, recurrenceRule);
|
downloadCalendar(title, dates, recurrenceRule);
|
||||||
|
@ -2103,9 +2144,10 @@ class PluginApi {
|
||||||
* Add custom user search options.
|
* Add custom user search options.
|
||||||
* It is heavily correlated with `register_groups_callback_for_users_search_controller_action` which allows defining custom filter.
|
* It is heavily correlated with `register_groups_callback_for_users_search_controller_action` which allows defining custom filter.
|
||||||
* Example usage:
|
* Example usage:
|
||||||
|
*
|
||||||
* ```
|
* ```
|
||||||
* api.addUserSearchOption("adminsOnly");
|
* api.addUserSearchOption("adminsOnly");
|
||||||
|
*
|
||||||
* register_groups_callback_for_users_search_controller_action(:admins_only) do |groups, user|
|
* register_groups_callback_for_users_search_controller_action(:admins_only) do |groups, user|
|
||||||
* groups.where(name: "admins")
|
* groups.where(name: "admins")
|
||||||
* end
|
* end
|
||||||
|
@ -2426,7 +2468,7 @@ class PluginApi {
|
||||||
* This is intended to replace the admin-menu plugin outlet from
|
* This is intended to replace the admin-menu plugin outlet from
|
||||||
* the old admin horizontal nav.
|
* the old admin horizontal nav.
|
||||||
*
|
*
|
||||||
* ```
|
* ```javascript
|
||||||
* api.addAdminSidebarSectionLink("root", {
|
* api.addAdminSidebarSectionLink("root", {
|
||||||
* name: "unique_link_name",
|
* name: "unique_link_name",
|
||||||
* label: "admin.some.i18n.label.key",
|
* label: "admin.some.i18n.label.key",
|
||||||
|
@ -2434,7 +2476,7 @@ class PluginApi {
|
||||||
* href: "(optional) can be used instead of the route",
|
* 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 {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 {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.
|
* @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 { hbs } from "ember-cli-htmlbars";
|
||||||
import $ from "jquery";
|
import $ from "jquery";
|
||||||
import { h } from "virtual-dom";
|
import { h } from "virtual-dom";
|
||||||
|
import { headerIconsDAG } from "discourse/components/glimmer-header/icons";
|
||||||
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
|
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
|
||||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||||
import scrollLock from "discourse/lib/scroll-lock";
|
import scrollLock from "discourse/lib/scroll-lock";
|
||||||
|
@ -22,14 +23,11 @@ import I18n from "discourse-i18n";
|
||||||
const SEARCH_BUTTON_ID = "search-button";
|
const SEARCH_BUTTON_ID = "search-button";
|
||||||
export const PANEL_WRAPPER_ID = "additional-panel-wrapper";
|
export const PANEL_WRAPPER_ID = "additional-panel-wrapper";
|
||||||
|
|
||||||
let _extraHeaderIcons = [];
|
let _extraHeaderIcons;
|
||||||
|
clearExtraHeaderIcons();
|
||||||
export function addToHeaderIcons(icon) {
|
|
||||||
_extraHeaderIcons.push(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearExtraHeaderIcons() {
|
export function clearExtraHeaderIcons() {
|
||||||
_extraHeaderIcons = [];
|
_extraHeaderIcons = headerIconsDAG();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dropdown = {
|
export const dropdown = {
|
||||||
|
@ -249,15 +247,13 @@ createWidget("header-icons", {
|
||||||
|
|
||||||
const icons = [];
|
const icons = [];
|
||||||
|
|
||||||
if (_extraHeaderIcons) {
|
const resolvedIcons = _extraHeaderIcons.resolve();
|
||||||
_extraHeaderIcons.forEach((icon) => {
|
resolvedIcons.forEach((icon) => {
|
||||||
if (typeof icon === "string") {
|
if (["search", "user-menu", "hamburger"].includes(icon.key)) {
|
||||||
icons.push(this.attach(icon));
|
return;
|
||||||
} else {
|
}
|
||||||
icons.push(this.attach("extra-icon", { component: icon }));
|
icons.push(this.attach("extra-icon", { component: icon.value }));
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = this.attach("header-dropdown", {
|
const search = this.attach("header-dropdown", {
|
||||||
title: "search.title",
|
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/),
|
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).
|
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
|
## [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
|
## [1.26.0] - 2024-02-21
|
||||||
|
|
||||||
|
|
|
@ -169,7 +169,7 @@ export default {
|
||||||
api.addCardClickListenerSelector(".chat-drawer-outlet");
|
api.addCardClickListenerSelector(".chat-drawer-outlet");
|
||||||
|
|
||||||
if (this.chatService.userCanChat) {
|
if (this.chatService.userCanChat) {
|
||||||
api.addToHeaderIcons(ChatHeaderIcon);
|
api.headerIcons.add("chat", ChatHeaderIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
api.addStyleguideSection?.({
|
api.addStyleguideSection?.({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user