DEV: Refactor topic admin menu to use `<DMenu>` (#26678)

* DEV: Refactor topic admin menu to use `<DMenu>`

This PR also introduces a new plugin API to add buttons to the topic admin menu

```javascript
api.addTopicAdminMenuButton((topic) => {
  return {
    action: () => {
      alert('Sunrise!');
    },
    icon: 'sun',
    className: 'sunrise-button',
    label: 'actions.rise',
  };
});
```

The plugins that needed to be updated are:

- [discourse-zoom](https://github.com/discourse/discourse-zoom/pull/73)
- [discourse-salesforce](https://github.com/discourse/discourse-salesforce/pull/74)
- [discourse-topic-noindex](https://github.com/discourse/discourse-topic-noindex/pull/11)
This commit is contained in:
Jan Cernik 2024-04-29 10:44:38 -05:00 committed by GitHub
parent 9fb888923d
commit 8dd883d4e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 464 additions and 763 deletions

View File

@ -52,7 +52,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="review.moderation_history"
@icon="list"
class="btn popup-menu-btn moderation-history"
class="btn btn-transparent moderation-history"
@href={{this.reviewUrl}}
/>
</li>
@ -68,7 +68,7 @@ export default class AdminPostMenu extends Component {
}}
@icon="shield-alt"
class={{concatClass
"btn popup-menu-btn toggle-post-type"
"btn btn-transparent toggle-post-type"
(if @data.transformedPost.isModeratorAction "btn-success")
}}
@action={{fn this.topicAction "togglePostType"}}
@ -87,7 +87,7 @@ export default class AdminPostMenu extends Component {
}}
title="post.controls.unhide"
class={{concatClass
"btn popup-menu-btn"
"btn btn-transparent"
(if @data.transformedPost.notice "change-notice" "add-notice")
(if @data.transformedPost.notice "btn-success")
}}
@ -101,7 +101,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.unhide"
@icon="far-eye"
class="btn popup-menu-btn unhide-post"
class="btn btn-transparent unhide-post"
@action={{fn this.topicAction "unhidePost"}}
/>
</li>
@ -121,7 +121,7 @@ export default class AdminPostMenu extends Component {
@label="post.controls.change_owner"
@icon="user"
title="post.controls.lock_post_description"
class="btn popup-menu-btn change-owner"
class="btn btn-transparent change-owner"
@action={{fn this.topicAction "changePostOwner"}}
/>
</li>
@ -133,7 +133,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.grant_badge"
@icon="certificate"
class="btn popup-menu-btn grant-badge"
class="btn btn-transparent grant-badge"
@action={{fn this.topicAction "grantBadge"}}
/>
</li>
@ -146,7 +146,7 @@ export default class AdminPostMenu extends Component {
@icon="unlock"
title="post.controls.unlock_post_description"
class={{concatClass
"btn popup-menu-btn unlock-post"
"btn btn-transparent unlock-post"
(if @data.post.locked "btn-success")
}}
@action={{fn this.topicAction "unlockPost"}}
@ -158,7 +158,7 @@ export default class AdminPostMenu extends Component {
@label="post.controls.lock_post"
@icon="lock"
title="post.controls.lock_post_description"
class="btn popup-menu-btn lock-post"
class="btn btn-transparent lock-post"
@action={{fn this.topicAction "lockPost"}}
/>
</li>
@ -170,7 +170,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.permanently_delete"
@icon="trash-alt"
class="btn popup-menu-btn permanently-delete"
class="btn btn-transparent permanently-delete"
@action={{fn this.topicAction "permanentlyDeletePost"}}
/>
</li>
@ -183,7 +183,7 @@ export default class AdminPostMenu extends Component {
@label="post.controls.unwiki"
@icon="far-edit"
class={{concatClass
"btn popup-menu-btn wiki wikied"
"btn btn-transparent wiki wikied"
(if @data.transformedPost.wiki "btn-success")
}}
@action={{fn this.topicAction "toggleWiki"}}
@ -194,7 +194,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.wiki"
@icon="far-edit"
class="btn popup-menu-btn wiki"
class="btn btn-transparent wiki"
@action={{fn this.topicAction "toggleWiki"}}
/>
</li>
@ -206,7 +206,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.publish_page"
@icon="file"
class="btn popup-menu-btn publish-page"
class="btn btn-transparent publish-page"
@action={{fn this.topicAction "showPagePublish"}}
/>
</li>
@ -217,7 +217,7 @@ export default class AdminPostMenu extends Component {
<DButton
@label="post.controls.rebake"
@icon="sync-alt"
class="btn popup-menu-btn rebuild-html"
class="btn btn-transparent rebuild-html"
@action={{fn this.topicAction "rebakePost"}}
/>
</li>
@ -229,7 +229,7 @@ export default class AdminPostMenu extends Component {
@label={{button.label}}
@translatedLabel={{button.translatedLabel}}
@icon={{button.icon}}
class={{concatClass "btn popup-menu-btn" button.className}}
class={{concatClass "btn btn-transparent" button.className}}
@action={{fn this.extraAction button}}
/>
</li>

View File

@ -1,11 +0,0 @@
import MountWidget from "discourse/components/mount-widget";
export default MountWidget.extend({
classNames: "topic-admin-menu-button-container",
tagName: "span",
widget: "topic-admin-menu-button",
buildArgs() {
return this.getProperties("topic", "openUpwards", "rightSide");
},
});

View File

@ -0,0 +1,311 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { and, not, or } from "truth-helpers";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
import getURL from "discourse-common/lib/get-url";
import DMenu from "float-kit/components/d-menu";
export default class TopicAdminMenu extends Component {
@service adminTopicMenuButtons;
@service currentUser;
@action
onRegisterApi(api) {
this.dMenu = api;
}
@action
onButtonAction(buttonAction) {
this.args[buttonAction]?.();
this.dMenu.close();
}
@action
onExtraButtonAction(buttonAction) {
buttonAction?.();
this.dMenu.close();
}
get extraButtons() {
return this.adminTopicMenuButtons.callbacks
.map((callback) => {
return callback(this.args.topic);
})
.filter(Boolean);
}
get details() {
return this.args.topic.get("details");
}
get isPrivateMessage() {
return this.args.topic.get("isPrivateMessage");
}
get featured() {
return (
!!this.args.topic.get("pinned_at") || this.args.topic.get("isBanner")
);
}
get visible() {
return this.args.topic.get("visible");
}
get canDelete() {
return this.details.get("can_delete");
}
get canRecover() {
return this.details.get("can_recover");
}
get archived() {
return this.args.topic.get("archived");
}
get topicModerationHistoryUrl() {
return getURL(`/review?topic_id=${this.args.topic.id}&status=all`);
}
<template>
<span class="topic-admin-menu-button-container">
<span class="topic-admin-menu-button">
<DMenu
@onRegisterApi={{this.onRegisterApi}}
@triggerClass="toggle-admin-menu"
@modalForMobile={{true}}
>
<:trigger>
{{icon "wrench"}}
</:trigger>
<:content>
<div class="popup-menu topic-admin-popup-menu">
<ul>
<ul class="topic-admin-menu-topic">
{{#if
(or
this.currentUser.canManageTopic
this.details.can_split_merge_topic
)
}}
<li class="topic-admin-multi-select">
<DButton
class="btn-transparent"
@label="topic.actions.multi_select"
@action={{fn this.onButtonAction "toggleMultiSelect"}}
@icon="tasks"
/>
</li>
{{/if}}
{{#if
(or
this.currentUser.canManageTopic
this.details.can_moderate_category
)
}}
{{#if this.canDelete}}
<li class="topic-admin-delete">
<DButton
@label="topic.actions.delete"
@action={{fn this.onButtonAction "deleteTopic"}}
@icon="far-trash-alt"
class="popup-menu-btn-danger btn-danger btn-transparent"
/>
</li>
{{else if this.canRecover}}
<li class="topic-admin-recover">
<DButton
class="btn-transparent"
@label="topic.actions.recover"
@action={{fn this.onButtonAction "recoverTopic"}}
@icon="undo"
/>
</li>
{{/if}}
{{/if}}
{{#if this.details.can_close_topic}}
<li
class={{if
@topic.closed
"topic-admin-open"
"topic-admin-close"
}}
>
<DButton
class="btn-transparent"
@label={{if
@topic.closed
"topic.actions.open"
"topic.actions.close"
}}
@action={{fn this.onButtonAction "toggleClosed"}}
@icon={{if @topic.closed "unlock" "lock"}}
/>
</li>
{{/if}}
{{#if
(and
this.details.can_pin_unpin_topic
(not this.isPrivateMessage)
(or this.visible this.featured)
)
}}
<li class="topic-admin-pin">
<DButton
class="btn-transparent"
@label={{if
this.featured
"topic.actions.unpin"
"topic.actions.pin"
}}
@action={{fn this.onButtonAction "showFeatureTopic"}}
@icon="thumbtack"
/>
</li>
{{/if}}
{{#if
(and
this.details.can_archive_topic (not this.isPrivateMessage)
)
}}
<li class="topic-admin-archive">
<DButton
class="btn-transparent"
@label={{if
this.archived
"topic.actions.unarchive"
"topic.actions.archive"
}}
@action={{fn this.onButtonAction "toggleArchived"}}
@icon="folder"
/>
</li>
{{/if}}
{{#if this.details.can_toggle_topic_visibility}}
<li class="topic-admin-visible">
<DButton
class="btn-transparent"
@label={{if
this.visible
"topic.actions.invisible"
"topic.actions.visible"
}}
@action={{fn this.onButtonAction "toggleVisibility"}}
@icon={{if this.visible "far-eye-slash" "far-eye"}}
/>
</li>
{{/if}}
{{#if (and this.details.can_convert_topic)}}
<li class="topic-admin-convert">
<DButton
class="btn-transparent"
@label={{if
this.isPrivateMessage
"topic.actions.make_public"
"topic.actions.make_private"
}}
@action={{fn
this.onButtonAction
(if
this.isPrivateMessage
"convertToPublicTopic"
"convertToPrivateMessage"
)
}}
@icon={{if this.isPrivateMessage "comment" "envelope"}}
/>
</li>
{{/if}}
</ul>
<ul class="topic-admin-menu-time">
{{#if this.currentUser.canManageTopic}}
<li class="admin-topic-timer-update">
<DButton
class="btn-transparent"
@label="topic.actions.timed_update"
@action={{fn this.onButtonAction "showTopicTimerModal"}}
@icon="far-clock"
/>
</li>
{{#if this.currentUser.staff}}
<li class="topic-admin-change-timestamp">
<DButton
class="btn-transparent"
@label="topic.change_timestamp.title"
@action={{fn
this.onButtonAction
"showChangeTimestamp"
}}
@icon="calendar-alt"
/>
</li>
{{/if}}
<li class="topic-admin-reset-bump-date">
<DButton
class="btn-transparent"
@label="topic.actions.reset_bump_date"
@action={{fn this.onButtonAction "resetBumpDate"}}
@icon="anchor"
/>
</li>
<li class="topic-admin-slow-mode">
<DButton
class="btn-transparent"
@label="topic.actions.slow_mode"
@action={{fn
this.onButtonAction
"showTopicSlowModeUpdate"
}}
@icon="hourglass-start"
/>
</li>
{{/if}}
</ul>
<ul class="topic-admin-menu-undefined">
{{#if this.currentUser.staff}}
<li class="topic-admin-moderation-history">
<DButton
class="btn-transparent"
@label="review.moderation_history"
@href={{this.topicModerationHistoryUrl}}
@icon="list"
/>
</li>
{{/if}}
{{#each this.extraButtons as |button|}}
<li>
<DButton
@label={{button.label}}
@translatedLabel={{button.translatedLabel}}
@icon={{button.icon}}
class={{concatClass "btn-transparent" button.className}}
@action={{fn this.onExtraButtonAction button.action}}
/>
</li>
{{/each}}
</ul>
</ul>
</div>
</:content>
</DMenu>
</span>
</span>
</template>
}

View File

@ -1,5 +1,5 @@
<div class="topic-footer-main-buttons">
<TopicAdminMenuButton
<TopicAdminMenu
@topic={{this.topic}}
@openUpwards="true"
@toggleMultiSelect={{this.toggleMultiSelect}}

View File

@ -40,7 +40,7 @@
@name="timeline-controls-before"
@outletArgs={{hash model=@model}}
/>
<TopicAdminMenuButton
<TopicAdminMenu
@topic={{@model}}
@addKeyboardTargetClass={{true}}
@toggleMultiSelect={{@toggleMultiSelect}}
@ -175,7 +175,7 @@
@showCaret={{false}}
/>
{{#if @mobileView}}
<TopicAdminMenuButton
<TopicAdminMenu
@topic={{@model}}
@addKeyboardTargetClass={{true}}
@openUpwards={{true}}

View File

@ -150,7 +150,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.29.0";
export const PLUGIN_API_VERSION = "1.31.0";
const DEPRECATED_HEADER_WIDGETS = [
"header",
@ -675,6 +675,30 @@ class PluginApi {
.addButton(callback);
}
/**
* Add a new button in the topic admin menu.
*
* Example:
*
* ```
* api.addTopicAdminMenuButton((topic) => {
* return {
* action: () => {
* alert('You clicked on the coffee button!');
* },
* icon: 'coffee',
* className: 'hot-coffee',
* label: 'coffee.title',
* };
* });
* ```
**/
addTopicAdminMenuButton(callback) {
this.container
.lookup("service:admin-topic-menu-buttons")
.addButton(callback);
}
/**
* Remove existing button below a post with your plugin.
*

View File

@ -0,0 +1,9 @@
import Service from "@ember/service";
export default class AdminTopicMenuButtons extends Service {
callbacks = [];
addButton(callback) {
this.callbacks.push(callback);
}
}

View File

@ -280,7 +280,7 @@
}}
/>
</span>
<TopicAdminMenuButton
<TopicAdminMenu
@topic={{this.model}}
@openUpwards="true"
@rightSide="true"

View File

@ -828,6 +828,7 @@ export default createWidget("post-menu", {
identifier: "admin-post-menu",
component: AdminPostMenu,
extraClassName: "popup-menu",
modalForMobile: true,
data: {
scheduleRerender: this.scheduleRerender.bind(this),
transformedPost: this.attrs,

View File

@ -1,409 +0,0 @@
import $ from "jquery";
import { h } from "virtual-dom";
import { headerOffset } from "discourse/lib/offset-calculator";
import { applyDecorators, createWidget } from "discourse/widgets/widget";
createWidget("admin-menu-button", {
tagName: "li",
buildClasses(attrs) {
return attrs.className;
},
html(attrs) {
let className;
if (attrs.buttonClass) {
className = attrs.buttonClass;
}
return this.attach("button", {
className,
action: attrs.action,
url: attrs.url,
icon: attrs.icon,
label: attrs.fullLabel || `topic.${attrs.label}`,
secondaryAction: "hideAdminMenu",
});
},
});
createWidget("topic-admin-menu-button", {
tagName: "span.topic-admin-menu-button",
buildKey: () => "topic-admin-menu-button",
defaultState() {
return { expanded: false, position: null };
},
html(attrs, state) {
const result = [];
const menu = this.attach("topic-admin-menu", {
position: state.position,
topic: attrs.topic,
openUpwards: attrs.openUpwards,
rightSide: this.site.desktopView && attrs.rightSide,
actionButtons: [],
});
// We don't show the button when expanded on the right side on desktop
if (
menu.attrs.actionButtons.length &&
(!(attrs.rightSide && state.expanded) || this.site.mobileView)
) {
result.push(
this.attach("button", {
className:
"btn-default popup-menu-button toggle-admin-menu" +
(attrs.addKeyboardTargetClass ? " keyboard-target-admin-menu" : ""),
title: "topic_admin_menu",
icon: "wrench",
action: "showAdminMenu",
sendActionEvent: true,
})
);
}
if (state.expanded) {
result.push(menu);
}
return result;
},
hideAdminMenu() {
this.state.expanded = false;
this.state.position = null;
},
showAdminMenu(e) {
this.state.expanded = true;
let button;
if (e === undefined) {
button = document.querySelector(".keyboard-target-admin-menu");
} else {
button = e.target.closest("button");
}
const position = { top: button.offsetTop, left: button.offsetLeft };
const spacing = 3;
const menuWidth = 212;
const rtl = document.documentElement.classList.contains("html.rtl");
const buttonDOMRect = button.getBoundingClientRect();
position.outerHeight = buttonDOMRect.height;
if (this.attrs.openUpwards) {
if (rtl) {
position.left -= buttonDOMRect.width + spacing;
} else {
position.left += buttonDOMRect.width + spacing;
}
} else {
if (rtl) {
if (buttonDOMRect.left < menuWidth) {
position.left += 0;
} else {
position.left -= menuWidth - buttonDOMRect.width;
}
} else {
const offsetRight = window.innerWidth - buttonDOMRect.right;
if (offsetRight < menuWidth) {
position.left -= menuWidth - buttonDOMRect.width;
}
}
position.top += buttonDOMRect.height + spacing;
}
this.state.position = position;
},
didRenderWidget() {
let menuButtons = document.querySelectorAll(
".topic-admin-popup-menu button"
);
if (menuButtons && menuButtons[0]) {
menuButtons[0].focus();
}
},
topicToggleActions() {
this.state.expanded ? this.hideAdminMenu() : this.showAdminMenu();
},
});
export default createWidget("topic-admin-menu", {
tagName: "div.popup-menu.topic-admin-popup-menu",
buildClasses(attrs) {
if (attrs.rightSide) {
return "right-side";
}
},
init(attrs) {
const topic = attrs.topic;
const details = topic.get("details");
const isPrivateMessage = topic.get("isPrivateMessage");
const featured = topic.get("pinned_at") || topic.get("isBanner");
const visible = topic.get("visible");
// Admin actions
if (
this.get("currentUser.canManageTopic") ||
details.can_split_merge_topic
) {
this.addActionButton({
className: "topic-admin-multi-select",
buttonClass: "popup-menu-btn",
action: "toggleMultiSelect",
icon: "tasks",
label: "actions.multi_select",
button_group: "topic",
});
}
if (
this.get("currentUser.canManageTopic") ||
details.get("can_moderate_category")
) {
if (details.get("can_delete")) {
this.addActionButton({
className: "topic-admin-delete",
buttonClass: "popup-menu-btn-danger",
action: "deleteTopic",
icon: "far-trash-alt",
label: "actions.delete",
button_group: "topic",
});
}
if (topic.get("deleted") && details.get("can_recover")) {
this.addActionButton({
className: "topic-admin-recover",
buttonClass: "popup-menu-btn",
action: "recoverTopic",
icon: "undo",
label: "actions.recover",
button_group: "topic",
});
}
}
if (this.currentUser && details.get("can_close_topic")) {
if (topic.get("closed")) {
this.addActionButton({
className: "topic-admin-open",
buttonClass: "popup-menu-btn",
action: "toggleClosed",
icon: "unlock",
label: "actions.open",
button_group: "topic",
});
} else {
this.addActionButton({
className: "topic-admin-close",
buttonClass: "popup-menu-btn",
action: "toggleClosed",
icon: "lock",
label: "actions.close",
button_group: "topic",
});
}
}
if (this.get("currentUser.canManageTopic")) {
this.addActionButton({
className: "admin-topic-timer-update",
buttonClass: "popup-menu-btn",
action: "showTopicTimerModal",
icon: "far-clock",
label: "actions.timed_update",
button_group: "time",
});
}
if (
details.get("can_pin_unpin_topic") &&
!isPrivateMessage &&
(topic.get("visible") || featured)
) {
this.addActionButton({
className: "topic-admin-pin",
buttonClass: "popup-menu-btn",
action: "showFeatureTopic",
icon: "thumbtack",
label: featured ? "actions.unpin" : "actions.pin",
button_group: "topic",
});
}
if (this.get("currentUser.canManageTopic")) {
if (this.currentUser.get("staff")) {
this.addActionButton({
className: "topic-admin-change-timestamp",
buttonClass: "popup-menu-btn",
action: "showChangeTimestamp",
icon: "calendar-alt",
label: "change_timestamp.title",
button_group: "time",
});
}
this.addActionButton({
className: "topic-admin-reset-bump-date",
buttonClass: "popup-menu-btn",
action: "resetBumpDate",
icon: "anchor",
label: "actions.reset_bump_date",
button_group: "time",
});
}
if (this.currentUser && details.get("can_archive_topic")) {
if (!isPrivateMessage) {
this.addActionButton({
className: "topic-admin-archive",
buttonClass: "popup-menu-btn",
action: "toggleArchived",
icon: "folder",
label: topic.get("archived")
? "actions.unarchive"
: "actions.archive",
button_group: "topic",
});
}
}
if (details.get("can_toggle_topic_visibility")) {
this.addActionButton({
className: "topic-admin-visible",
buttonClass: "popup-menu-btn",
action: "toggleVisibility",
icon: visible ? "far-eye-slash" : "far-eye",
label: visible ? "actions.invisible" : "actions.visible",
button_group: "topic",
});
}
if (this.get("currentUser.canManageTopic")) {
if (details.get("can_convert_topic")) {
this.addActionButton({
className: "topic-admin-convert",
buttonClass: "popup-menu-btn",
action: isPrivateMessage
? "convertToPublicTopic"
: "convertToPrivateMessage",
icon: isPrivateMessage ? "comment" : "envelope",
label: isPrivateMessage
? "actions.make_public"
: "actions.make_private",
button_group: "topic",
});
}
this.addActionButton({
className: "topic-admin-slow-mode",
buttonClass: "popup-menu-btn",
action: "showTopicSlowModeUpdate",
icon: "hourglass-start",
label: "actions.slow_mode",
button_group: "time",
});
if (this.currentUser.get("staff")) {
this.addActionButton({
icon: "list",
buttonClass: "popup-menu-btn",
fullLabel: "review.moderation_history",
url: `/review?topic_id=${topic.id}&status=all`,
});
}
}
},
buildAttributes(attrs) {
let { top, left, outerHeight } = attrs.position;
const position = this.site.mobileView ? "fixed" : "absolute";
const approxMenuHeight = attrs.actionButtons.length * 42;
if (attrs.rightSide) {
return;
}
if (attrs.openUpwards) {
const documentHeight = $(document).height();
const mainHeight = $(".ember-application").height();
let bottom =
documentHeight - top - 70 - $(".ember-application").offset().top;
if (documentHeight > mainHeight) {
bottom = bottom - (documentHeight - mainHeight) - outerHeight;
}
if (top < approxMenuHeight) {
bottom =
bottom - (approxMenuHeight - outerHeight - top) - headerOffset();
}
if (this.site.mobileView) {
bottom = 50;
left = 0;
}
return {
style: `position: ${position}; bottom: ${bottom}px; left: ${left}px;`,
};
} else {
return {
style: `position: ${position}; top: ${top}px; left: ${left}px;`,
};
}
},
addActionButton(button) {
this.attrs.actionButtons.push(button);
},
html(attrs) {
const extraButtons = applyDecorators(
this,
"adminMenuButtons",
this.attrs,
this.state
);
const actionButtons = attrs.actionButtons
.concat(extraButtons)
.filter(Boolean);
const buttonMap = actionButtons.reduce(
(prev, current) =>
prev.set(current.button_group, [
...(prev.get(current.button_group) || []),
current,
]),
new Map()
);
let combinedButtonLists = [];
for (const [group, buttons] of buttonMap.entries()) {
let buttonList = [];
buttons.forEach((button) => {
buttonList.push(this.attach("admin-menu-button", button));
});
combinedButtonLists.push(h(`ul.topic-admin-menu-${group}`, buttonList));
}
return h("ul", combinedButtonLists);
},
clickOutside() {
this.sendWidgetAction("hideAdminMenu");
},
});

View File

@ -1,10 +1,12 @@
import { click, visit } from "@ember/test-helpers";
import { test } from "qunit";
import { withPluginApi } from "discourse/lib/plugin-api";
import {
acceptance,
exists,
updateCurrentUser,
} from "discourse/tests/helpers/qunit-helpers";
import I18n from "discourse-i18n";
acceptance("Topic - Admin Menu Anonymous Users", function () {
test("Enter as a regular user", async function (assert) {
@ -44,16 +46,34 @@ acceptance("Topic - Admin Menu", function (needs) {
);
});
test("Toggle the menu as admin focuses the first item", async function (assert) {
test("Button added using addTopicAdminMenuButton", async function (assert) {
updateCurrentUser({ admin: true });
this.set("actionCalled", false);
withPluginApi("1.31.0", (api) => {
api.addTopicAdminMenuButton(() => {
return {
className: "extra-button",
icon: "heart",
label: "yes_value",
action: () => {
this.set("actionCalled", true);
},
};
});
});
await visit("/t/internationalization-localization/280");
assert.ok(exists("#topic"), "The topic was rendered");
await click(".toggle-admin-menu");
assert.strictEqual(
document.activeElement,
document.querySelector(".topic-admin-multi-select > button")
assert.ok(
exists(".extra-button svg.d-icon-heart"),
"The icon was rendered"
);
assert
.dom(".extra-button .d-button-label")
.hasText(I18n.t("yes_value"), "The label was rendered");
await click(".extra-button");
assert.ok(this.actionCalled, "The action was called");
});
});

View File

@ -1,79 +0,0 @@
import { getOwner } from "@ember/application";
import { render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit";
import Category from "discourse/models/category";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists } from "discourse/tests/helpers/qunit-helpers";
const createArgs = (topic) => {
return {
topic,
openUpwards: "true",
toggleMultiSelect: () => {},
deleteTopic: () => {},
recoverTopic: () => {},
toggleClosed: () => {},
toggleArchived: () => {},
toggleVisibility: () => {},
showTopicTimerModal: () => {},
showFeatureTopic: () => {},
showChangeTimestamp: () => {},
resetBumpDate: () => {},
convertToPublicTopic: () => {},
convertToPrivateMessage: () => {},
};
};
module(
"Integration | Component | Widget | topic-admin-menu-button",
function (hooks) {
setupRenderingTest(hooks);
test("topic-admin-menu-button is present for admin/moderators", async function (assert) {
this.currentUser.setProperties({
admin: true,
moderator: true,
id: 123,
});
const store = getOwner(this).lookup("service:store");
const topic = store.createRecord("topic", {
user_id: this.currentUser.id,
});
topic.set("category_id", Category.create({ read_restricted: true }).id);
this.siteSettings.allow_featured_topic_on_user_profiles = true;
this.set("args", createArgs(topic));
await render(
hbs`<MountWidget @widget="topic-admin-menu-button" @args={{this.args}} />`
);
assert.ok(exists(".toggle-admin-menu"), "admin wrench is present");
});
test("topic-admin-menu-button hides for non-admin when there is no action", async function (assert) {
this.currentUser.setProperties({
admin: false,
moderator: false,
id: 123,
});
const store = getOwner(this).lookup("service:store");
const topic = store.createRecord("topic", {
user_id: this.currentUser.id,
});
topic.set("category_id", Category.create({ read_restricted: true }).id);
this.siteSettings.allow_featured_topic_on_user_profiles = true;
this.set("args", createArgs(topic));
await render(
hbs`<MountWidget @widget="topic-admin-menu-button" @args={{this.args}} />`
);
assert.ok(!exists(".toggle-admin-menu"), "admin wrench is not present");
});
}
);

View File

@ -1,26 +1,50 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import { and } from "truth-helpers";
import DModal from "discourse/components/d-modal";
import DFloatBody from "float-kit/components/d-float-body";
const DInlineFloat = <template>
{{#if @instance.expanded}}
<DFloatBody
@instance={{@instance}}
@trapTab={{@trapTab}}
@mainClass={{@mainClass}}
@innerClass={{@innerClass}}
@role={{@role}}
@portalOutletElement={{@portalOutletElement}}
@inline={{@inline}}
>
{{#if @instance.options.component}}
<@instance.options.component
@data={{@instance.options.data}}
@close={{@instance.close}}
/>
{{else}}
{{@instance.options.content}}
{{/if}}
</DFloatBody>
{{/if}}
</template>;
export default class DInlineFloat extends Component {
@service site;
export default DInlineFloat;
<template>
{{#if @instance.expanded}}
{{#if (and this.site.mobileView @instance.options.modalForMobile)}}
<DModal
@closeModal={{@instance.close}}
@hideHeader={{true}}
data-identifier={{@instance.options.identifier}}
data-content
>
{{#if @instance.options.component}}
<@instance.options.component
@data={{@instance.options.data}}
@close={{@instance.close}}
/>
{{else}}
{{@instance.options.content}}
{{/if}}
</DModal>
{{else}}
<DFloatBody
@instance={{@instance}}
@trapTab={{@trapTab}}
@mainClass={{@mainClass}}
@innerClass={{@innerClass}}
@role={{@role}}
@portalOutletElement={{@portalOutletElement}}
@inline={{@inline}}
>
{{#if @instance.options.component}}
<@instance.options.component
@data={{@instance.options.data}}
@close={{@instance.close}}
/>
{{else}}
{{@instance.options.content}}
{{/if}}
</DFloatBody>
{{/if}}
{{/if}}
</template>
}

View File

@ -77,6 +77,7 @@ export default class DMenu extends Component {
"fk-d-menu__trigger"
(if this.menuInstance.expanded "-expanded")
(concat this.options.identifier "-trigger")
@triggerClass
}}
id={{this.menuInstance.id}}
data-identifier={{this.options.identifier}}

View File

@ -37,7 +37,6 @@
@import "not-found";
@import "onebox";
@import "personal-message";
@import "popup-menu";
@import "redirection";
@import "reviewables";
@import "revise-and-reject-post-reviewable";

View File

@ -1,66 +0,0 @@
.popup-menu {
background-color: var(--secondary);
width: 14em;
border: 1px solid var(--primary-low);
z-index: z("dropdown");
box-shadow: var(--shadow-card);
ul {
margin: 0;
list-style: none;
li {
border-bottom: 1px solid rgba(var(--primary-low-rgb), 0.5);
&:last-child {
border: none;
}
}
}
.btn {
justify-content: left;
text-align: left;
background: none;
width: 100%;
padding: 0.5em;
border-radius: 0;
margin: 0;
.d-icon {
color: var(--primary-medium);
align-self: flex-start;
margin-right: 0.75em;
margin-top: 0.1em; // vertical alignment
}
&:focus,
&:hover {
color: var(--primary);
background: var(--d-hover);
.d-icon {
color: var(--primary-medium);
}
}
&.popup-menu-btn-danger {
.d-icon {
color: var(--danger);
}
.d-button-label {
color: var(--primary);
}
&:focus,
&:hover {
.d-icon,
.d-button-label {
color: var(--danger);
}
background: var(--danger-low);
}
}
}
}

View File

@ -1,42 +1,10 @@
// Styles for the topic admin menu
.topic-admin-popup-menu {
@include breakpoint(mobile-extra-large) {
width: calc(100% - 20px);
margin: 0 10px;
padding: 0;
padding-bottom: env(safe-area-inset-bottom);
z-index: z("modal", "popover");
@keyframes slideUp {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}
animation: slideUp 0.3s;
@media (prefers-reduced-motion) {
animation-duration: 0s;
}
}
.mobile-view & {
z-index: z("modal", "popover");
}
.btn {
padding: 0.5rem;
.d-icon {
font-size: var(--font-down-1);
margin-top: 2px; // vertical alignment
margin-right: 0.75em;
}
}
ul {
margin: 0;
list-style: none;
li {
border: none;
&:not(:last-of-type) {
@ -45,81 +13,22 @@
}
}
ul {
padding: 0.5rem;
padding: 0.5em;
&:not(:last-of-type) {
border-bottom: 1px solid var(--primary-low);
}
}
}
}
.modal-body.feature-topic {
max-height: 70vh !important;
padding: 0 1em;
input.date-picker {
margin: 0;
}
.feature-section {
display: block;
padding: 1.25em 0;
&:not(:last-of-type) {
border-bottom: 1px solid var(--primary-low);
}
.desc {
display: inline-block;
vertical-align: middle;
p:first-of-type {
margin: 0;
}
p {
margin: 10px 0 0;
max-width: 31em;
}
}
.with-validation {
position: relative;
> span {
display: flex;
align-items: flex-start;
> .d-icon {
padding-top: 0.75em;
margin-right: 0.5em;
}
}
.btn {
justify-content: left;
text-align: left;
width: 100%;
padding: 0.5em;
}
}
}
// Select posts
.selected-posts {
border: 1px solid var(--tertiary-medium);
background-color: var(--tertiary-low);
.btn {
border: none;
color: var(--secondary);
font-weight: normal;
margin-bottom: 10px;
&:not(.btn-danger) {
background: var(--tertiary);
border-color: var(--tertiary);
&[href] {
color: var(--secondary);
}
&:hover {
color: var(--secondary);
background: var(--tertiary-high);
}
&:active {
@include linear-gradient(var(--tertiary-hover), var(--tertiary));
color: var(--secondary);
}
}
&[disabled] {
text-shadow: 0 1px 0 rgba(var(--primary-rgb), 0.2);
@include linear-gradient(var(--tertiary), var(--tertiary-hover));
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.33);
}
.fk-d-menu {
.topic-admin-popup-menu {
width: 14em;
}
}

View File

@ -148,23 +148,9 @@
display: flex;
}
}
.topic-admin-popup-menu.right-side {
position: absolute;
bottom: 0;
right: 0;
left: auto;
transition: bottom 0.5s;
transform: translateZ(
0
); // iOS11 Rendering bug https://meta.discourse.org/t/wrench-menu-not-disappearing-on-ios/94297
}
&.docked {
position: initial;
.topic-admin-popup-menu.right-side {
bottom: unset;
right: 10px;
}
}
html:not(.footer-nav-visible) & {

View File

@ -1,4 +1,8 @@
[data-content][data-identifier="admin-post-menu"] {
.d-modal__body {
padding: 0;
}
ul {
padding: 0.5rem;
margin: 0;
@ -17,5 +21,12 @@
margin-bottom: 0;
}
}
.btn {
justify-content: left;
text-align: left;
width: 100%;
padding: 0.5em;
}
}
}

View File

@ -135,39 +135,6 @@ sub sub {
flex-wrap: wrap;
}
@media screen and (max-height: 600px) {
.topic-admin-popup-menu {
box-sizing: border-box;
padding: 0.25em;
width: unset;
ul {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@media screen and (max-width: 550px) {
grid-template-columns: 1fr 1fr;
}
ul {
display: contents;
}
.d-button-label {
@include ellipsis;
}
.popup-menu-btn {
@include ellipsis;
}
li {
border: 0;
min-width: 0;
}
}
}
}
.container.posts .topic-navigation {
// better positioning for the docked progress bar on large screens using mobile view
grid-area: posts;

View File

@ -7,6 +7,10 @@ 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.31.0] - 2024-04-22
- Adds `addTopicAdminMenuButton` which allows to register a new button in the topic admin menu.
## [1.30.0] - 2024-03-20
- Added `addAdminPluginConfigurationNav`, which defines a list of links used in the adminPlugins.show page for a specific plugin, and displays them either in an inner sidebar or in a top horizontal nav.