mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: API to set combined/separated sidebar mode. (#22753)
New API to change sidebar mode. We defined two: Separated - only sections belonging to specific panel are displayed, and buttons to switch the panel are available as well. Combined - all sections are displayed together and switch panel buttons are not visible. In addition, as a part of refactoring, a new service called SidebarState was introduced.
This commit is contained in:
parent
5d1c1e5f62
commit
85e2f67367
@ -1,13 +1,9 @@
|
|||||||
<DSection @pageClass="has-sidebar" @id="d-sidebar" @class="sidebar-container">
|
<DSection @pageClass="has-sidebar" @id="d-sidebar" @class="sidebar-container">
|
||||||
{{#if this.showSwitchPanelButtonsOnTop}}
|
{{#if this.showSwitchPanelButtonsOnTop}}
|
||||||
<Sidebar::SwitchPanelButtons
|
<Sidebar::SwitchPanelButtons @buttons={{this.switchPanelButtons}} />
|
||||||
@currentPanel={{this.currentPanel}}
|
|
||||||
@buttons={{this.switchPanelButtons}}
|
|
||||||
@setCurrentPanelKey={{this.setCurrentPanelKey}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.showMainPanel}}
|
{{#if this.sidebarState.showMainPanel}}
|
||||||
<Sidebar::Sections
|
<Sidebar::Sections
|
||||||
@currentUser={{this.currentUser}}
|
@currentUser={{this.currentUser}}
|
||||||
@collapsableSections={{true}}
|
@collapsableSections={{true}}
|
||||||
@ -17,16 +13,11 @@
|
|||||||
<Sidebar::ApiPanels
|
<Sidebar::ApiPanels
|
||||||
@currentUser={{this.currentUser}}
|
@currentUser={{this.currentUser}}
|
||||||
@collapsableSections={{true}}
|
@collapsableSections={{true}}
|
||||||
@panel={{this.currentPanel}}
|
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#unless this.showSwitchPanelButtonsOnTop}}
|
{{#unless this.showSwitchPanelButtonsOnTop}}
|
||||||
<Sidebar::SwitchPanelButtons
|
<Sidebar::SwitchPanelButtons @buttons={{this.switchPanelButtons}} />
|
||||||
@currentPanel={{this.currentPanel}}
|
|
||||||
@buttons={{this.switchPanelButtons}}
|
|
||||||
@setCurrentPanelKey={{this.setCurrentPanelKey}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
<Sidebar::Footer />
|
<Sidebar::Footer />
|
||||||
|
@ -1,19 +1,13 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
|
||||||
import {
|
|
||||||
currentPanelKey,
|
|
||||||
customPanels as sidebarCustomPanels,
|
|
||||||
} from "discourse/lib/sidebar/custom-sections";
|
|
||||||
|
|
||||||
export default class Sidebar extends Component {
|
export default class Sidebar extends Component {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
@service site;
|
@service site;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@tracked currentPanelKey = currentPanelKey;
|
@service sidebarState;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
@ -23,26 +17,22 @@ export default class Sidebar extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMainPanel() {
|
|
||||||
return this.currentPanelKey === "main";
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentPanel() {
|
|
||||||
return sidebarCustomPanels.find(
|
|
||||||
(panel) => panel.key === this.currentPanelKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showSwitchPanelButtonsOnTop() {
|
get showSwitchPanelButtonsOnTop() {
|
||||||
return this.siteSettings.default_sidebar_switch_panel_position === "top";
|
return this.siteSettings.default_sidebar_switch_panel_position === "top";
|
||||||
}
|
}
|
||||||
|
|
||||||
get switchPanelButtons() {
|
get switchPanelButtons() {
|
||||||
if (sidebarCustomPanels.length === 1 || !this.currentUser) {
|
if (
|
||||||
|
this.sidebarState.combinedMode ||
|
||||||
|
this.sidebarState.panels.length === 1 ||
|
||||||
|
!this.currentUser
|
||||||
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return sidebarCustomPanels.filter((panel) => panel !== this.currentPanel);
|
return this.sidebarState.panels.filter(
|
||||||
|
(panel) => panel !== this.sidebarState.currentPanel
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
@ -71,9 +61,4 @@ export default class Sidebar extends Component {
|
|||||||
document.removeEventListener("click", this.collapseSidebar);
|
document.removeEventListener("click", this.collapseSidebar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
setCurrentPanelKey(key) {
|
|
||||||
this.currentPanelKey = key;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,3 @@
|
|||||||
<div class="sidebar-sections">
|
<div class="sidebar-sections">
|
||||||
<Sidebar::ApiSections
|
<Sidebar::ApiSections @collapsable={{@collapsableSections}} />
|
||||||
@sections={{this.customSections}}
|
|
||||||
@collapsable={{@collapsableSections}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
@ -1,20 +0,0 @@
|
|||||||
import Component from "@glimmer/component";
|
|
||||||
import { getOwner, setOwner } from "@ember/application";
|
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
|
|
||||||
export default class SidebarApiPanels extends Component {
|
|
||||||
@service siteSettings;
|
|
||||||
@service currentUser;
|
|
||||||
@service site;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
|
|
||||||
this.customSections =
|
|
||||||
this.args.panel?.sections?.map((customSection) => {
|
|
||||||
const section = new customSection();
|
|
||||||
setOwner(section, getOwner(this));
|
|
||||||
return section;
|
|
||||||
}) || [];
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,44 @@
|
|||||||
|
<Sidebar::Section
|
||||||
|
@sectionName={{this.section.name}}
|
||||||
|
@headerLinkText={{this.section.text}}
|
||||||
|
@headerLinkTitle={{this.section.title}}
|
||||||
|
@headerActionsIcon={{this.section.actionsIcon}}
|
||||||
|
@headerActions={{this.section.actions}}
|
||||||
|
@willDestroy={{this.section.willDestroy}}
|
||||||
|
@collapsable={{@collapsable}}
|
||||||
|
@displaySection={{this.section.displaySection}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{{#each this.section.links as |link|}}
|
||||||
|
<Sidebar::SectionLink
|
||||||
|
@linkName={{link.name}}
|
||||||
|
@class={{link.classNames}}
|
||||||
|
@route={{link.route}}
|
||||||
|
@model={{link.model}}
|
||||||
|
@models={{link.models}}
|
||||||
|
@href={{link.href}}
|
||||||
|
@title={{link.title}}
|
||||||
|
@contentCSSClass={{link.contentCSSClass}}
|
||||||
|
@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}}
|
||||||
|
@didInsert={{link.didInsert}}
|
||||||
|
@willDestroy={{link.willDestroy}}
|
||||||
|
@content={{link.text}}
|
||||||
|
@contentComponent={{component
|
||||||
|
link.contentComponent
|
||||||
|
status=link.contentComponentArgs
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
</Sidebar::Section>
|
@ -0,0 +1,10 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { getOwner, setOwner } from "@ember/application";
|
||||||
|
|
||||||
|
export default class SidebarApiSection extends Component {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.section = new this.args.sectionConfig();
|
||||||
|
setOwner(this.section, getOwner(this));
|
||||||
|
}
|
||||||
|
}
|
@ -1,46 +1,6 @@
|
|||||||
{{#each @sections as |customSection|}}
|
{{#each this.sections as |sectionConfig|}}
|
||||||
<Sidebar::Section
|
<Sidebar::ApiSection
|
||||||
@sectionName={{customSection.name}}
|
@sectionConfig={{sectionConfig}}
|
||||||
@headerLinkText={{customSection.text}}
|
|
||||||
@headerLinkTitle={{customSection.title}}
|
|
||||||
@headerActionsIcon={{customSection.actionsIcon}}
|
|
||||||
@headerActions={{customSection.actions}}
|
|
||||||
@willDestroy={{customSection.willDestroy}}
|
|
||||||
@collapsable={{@collapsable}}
|
@collapsable={{@collapsable}}
|
||||||
@displaySection={{customSection.displaySection}}
|
/>
|
||||||
>
|
|
||||||
|
|
||||||
{{#each customSection.links as |link|}}
|
|
||||||
<Sidebar::SectionLink
|
|
||||||
@linkName={{link.name}}
|
|
||||||
@class={{link.classNames}}
|
|
||||||
@route={{link.route}}
|
|
||||||
@model={{link.model}}
|
|
||||||
@models={{link.models}}
|
|
||||||
@href={{link.href}}
|
|
||||||
@title={{link.title}}
|
|
||||||
@contentCSSClass={{link.contentCSSClass}}
|
|
||||||
@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}}
|
|
||||||
@didInsert={{link.didInsert}}
|
|
||||||
@willDestroy={{link.willDestroy}}
|
|
||||||
@content={{link.text}}
|
|
||||||
@contentComponent={{component
|
|
||||||
link.contentComponent
|
|
||||||
status=link.contentComponentArgs
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
</Sidebar::Section>
|
|
||||||
{{/each}}
|
{{/each}}
|
@ -0,0 +1,14 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class SidebarApiSections extends Component {
|
||||||
|
@service sidebarState;
|
||||||
|
|
||||||
|
get sections() {
|
||||||
|
if (this.sidebarState.combinedMode) {
|
||||||
|
return this.sidebarState.panels.map((panel) => panel.sections).flat();
|
||||||
|
} else {
|
||||||
|
return this.sidebarState.currentPanel.sections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,12 @@ import { action } from "@ember/object";
|
|||||||
|
|
||||||
export default class SwitchPanelButtons extends Component {
|
export default class SwitchPanelButtons extends Component {
|
||||||
@service router;
|
@service router;
|
||||||
|
@service sidebarState;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
switchPanel(currentPanel, panel) {
|
switchPanel(currentPanel, panel) {
|
||||||
currentPanel.lastKnownURL = this.router.currentURL;
|
this.sidebarState.currentPanel.lastKnownURL = this.router.currentURL;
|
||||||
this.args.setCurrentPanelKey(panel.key);
|
this.sidebarState.setPanel(panel.key);
|
||||||
const url = panel.lastKnownURL || panel.switchButtonDefaultUrl;
|
const url = panel.lastKnownURL || panel.switchButtonDefaultUrl;
|
||||||
if (url === "/") {
|
if (url === "/") {
|
||||||
this.router.transitionTo("discovery.latest");
|
this.router.transitionTo("discovery.latest");
|
||||||
|
@ -10,8 +10,5 @@
|
|||||||
<Sidebar::User::MessagesSection @collapsable={{@collapsableSections}} />
|
<Sidebar::User::MessagesSection @collapsable={{@collapsableSections}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<Sidebar::ApiSections
|
<Sidebar::ApiSections @collapsable={{@collapsableSections}} />
|
||||||
@sections={{this.customSections}}
|
|
||||||
@collapsable={{@collapsableSections}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
@ -1,5 +1,4 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { getOwner, setOwner } from "@ember/application";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class SidebarUserSections extends Component {
|
export default class SidebarUserSections extends Component {
|
||||||
@ -7,17 +6,6 @@ export default class SidebarUserSections extends Component {
|
|||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(...arguments);
|
|
||||||
|
|
||||||
this.customSections =
|
|
||||||
this.args.panel?.sections?.map((customSection) => {
|
|
||||||
const section = new customSection();
|
|
||||||
setOwner(section, getOwner(this));
|
|
||||||
return section;
|
|
||||||
}) || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
get enableMessagesSection() {
|
get enableMessagesSection() {
|
||||||
return this.currentUser?.can_send_private_messages;
|
return this.currentUser?.can_send_private_messages;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,6 @@ import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/s
|
|||||||
import {
|
import {
|
||||||
addSidebarPanel,
|
addSidebarPanel,
|
||||||
addSidebarSection,
|
addSidebarSection,
|
||||||
setSidebarPanel,
|
|
||||||
} from "discourse/lib/sidebar/custom-sections";
|
} from "discourse/lib/sidebar/custom-sections";
|
||||||
import {
|
import {
|
||||||
registerCustomCategoryLockIcon,
|
registerCustomCategoryLockIcon,
|
||||||
@ -2068,7 +2067,23 @@ class PluginApi {
|
|||||||
* Support for setting a Sidebar panel.
|
* Support for setting a Sidebar panel.
|
||||||
*/
|
*/
|
||||||
setSidebarPanel(name) {
|
setSidebarPanel(name) {
|
||||||
setSidebarPanel(name);
|
this._lookupContainer("service:sidebar-state").setPanel(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXPERIMENTAL. Do not use.
|
||||||
|
* Set combined sidebar section mode. In this mode, sections from all panels are displayed together.
|
||||||
|
*/
|
||||||
|
setCombinedSidebarMode() {
|
||||||
|
this._lookupContainer("service:sidebar-state").setCombinedMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXPERIMENTAL. Do not use.
|
||||||
|
* Set separated sidebar section mode. In this mode, only sections from the current panel are displayed.
|
||||||
|
*/
|
||||||
|
setSeparatedSidebarMode() {
|
||||||
|
this._lookupContainer("service:sidebar-state").setSeparatedMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,10 +32,6 @@ export function addSidebarPanel(func) {
|
|||||||
customPanels.push(new panelClass());
|
customPanels.push(new panelClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSidebarPanel(name) {
|
|
||||||
currentPanelKey = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addSidebarSection(func, panelKey) {
|
export function addSidebarSection(func, panelKey) {
|
||||||
const panel = customPanels.find((p) => p.key === panelKey);
|
const panel = customPanels.find((p) => p.key === panelKey);
|
||||||
if (!panel) {
|
if (!panel) {
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import Service from "@ember/service";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
import {
|
||||||
|
currentPanelKey,
|
||||||
|
customPanels as panels,
|
||||||
|
} from "discourse/lib/sidebar/custom-sections";
|
||||||
|
|
||||||
|
const COMBINED_MODE = "combined";
|
||||||
|
const SEPARATED_MODE = "separated";
|
||||||
|
|
||||||
|
export default class SidebarState extends Service {
|
||||||
|
@tracked currentPanelKey = currentPanelKey;
|
||||||
|
@tracked panels = panels;
|
||||||
|
@tracked mode = COMBINED_MODE;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
this.#reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
setPanel(name) {
|
||||||
|
this.currentPanelKey = name;
|
||||||
|
this.mode = SEPARATED_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
get currentPanel() {
|
||||||
|
return this.panels.find((panel) => panel.key === this.currentPanelKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSeparatedMode() {
|
||||||
|
this.mode = SEPARATED_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCombinedMode() {
|
||||||
|
this.mode = COMBINED_MODE;
|
||||||
|
this.currentPanelKey = "main";
|
||||||
|
}
|
||||||
|
|
||||||
|
get combinedMode() {
|
||||||
|
return this.mode === COMBINED_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
get showMainPanel() {
|
||||||
|
return this.currentPanelKey === "main";
|
||||||
|
}
|
||||||
|
|
||||||
|
#reset() {
|
||||||
|
this.currentPanelKey = currentPanelKey;
|
||||||
|
this.panels = panels;
|
||||||
|
this.mode = COMBINED_MODE;
|
||||||
|
}
|
||||||
|
}
|
@ -1049,5 +1049,23 @@ acceptance("Sidebar - Plugin API", function (needs) {
|
|||||||
assert
|
assert
|
||||||
.dom("#d-sidebar .sidebar__panel-switch-button + .sidebar-sections")
|
.dom("#d-sidebar .sidebar__panel-switch-button + .sidebar-sections")
|
||||||
.exists();
|
.exists();
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(
|
||||||
|
".sidebar-section[data-section-name='test-chat-channels'] .sidebar-section-header-text"
|
||||||
|
)
|
||||||
|
.doesNotExist();
|
||||||
|
|
||||||
|
withPluginApi(PLUGIN_API_VERSION, (api) => {
|
||||||
|
api.setCombinedSidebarMode();
|
||||||
|
});
|
||||||
|
await visit("/");
|
||||||
|
assert.dom(".sidebar__panel-switch-button").doesNotExist();
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(
|
||||||
|
".sidebar-section[data-section-name='test-chat-channels'] .sidebar-section-header-text"
|
||||||
|
)
|
||||||
|
.exists();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -83,8 +83,8 @@ import {
|
|||||||
import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
|
import { clearTagsHtmlCallbacks } from "discourse/lib/render-tags";
|
||||||
import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
import { clearToolbarCallbacks } from "discourse/components/d-editor";
|
||||||
import { clearExtraHeaderIcons } from "discourse/widgets/header";
|
import { clearExtraHeaderIcons } from "discourse/widgets/header";
|
||||||
import { resetSidebarPanels } from "discourse/lib/sidebar/custom-sections";
|
|
||||||
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
|
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
|
||||||
|
import { resetSidebarPanels } from "discourse/lib/sidebar/custom-sections";
|
||||||
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||||
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
||||||
import { resetMentions } from "discourse/lib/link-mentions";
|
import { resetMentions } from "discourse/lib/link-mentions";
|
||||||
@ -217,8 +217,8 @@ export function testCleanup(container, app) {
|
|||||||
clearResolverOptions();
|
clearResolverOptions();
|
||||||
clearTagsHtmlCallbacks();
|
clearTagsHtmlCallbacks();
|
||||||
clearToolbarCallbacks();
|
clearToolbarCallbacks();
|
||||||
resetSidebarPanels();
|
|
||||||
resetNotificationTypeRenderers();
|
resetNotificationTypeRenderers();
|
||||||
|
resetSidebarPanels();
|
||||||
clearExtraHeaderIcons();
|
clearExtraHeaderIcons();
|
||||||
resetOnKeyDownCallbacks();
|
resetOnKeyDownCallbacks();
|
||||||
resetUserMenuTabs();
|
resetUserMenuTabs();
|
||||||
|
Loading…
Reference in New Issue
Block a user