DEV: implements <DropdownMenu /> (#26917)

DropdownMenu component is meant as a way to describe the content of menus.

Syntax:

```
<DropdownMenu as |dm|>
  <dm.item class="test">
    First
  </dm.item>
  <dm.divider class="foo" />
  <dm.item class="bar">
    Second
  </dm.item>
</DropdownMenu>
```
This commit is contained in:
Joffrey JAFFEUX 2024-05-08 09:08:42 +02:00 committed by GitHub
parent 21bce2d07e
commit cf8b81771f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 468 additions and 411 deletions

View File

@ -4,6 +4,7 @@ import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { and, not, or } from "truth-helpers"; import { and, not, or } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
export default class AdminPostMenu extends Component { export default class AdminPostMenu extends Component {
@ -46,20 +47,20 @@ export default class AdminPostMenu extends Component {
} }
<template> <template>
<ul> <DropdownMenu as |dropdown|>
{{#if this.currentUser.staff}} {{#if this.currentUser.staff}}
<li> <dropdown.item>
<DButton <DButton
@label="review.moderation_history" @label="review.moderation_history"
@icon="list" @icon="list"
class="btn btn-transparent moderation-history" class="btn btn-transparent moderation-history"
@href={{this.reviewUrl}} @href={{this.reviewUrl}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if (and this.currentUser.staff (not @data.transformedPost.isWhisper))}} {{#if (and this.currentUser.staff (not @data.transformedPost.isWhisper))}}
<li> <dropdown.item>
<DButton <DButton
@label={{if @label={{if
@data.transformedPost.isModeratorAction @data.transformedPost.isModeratorAction
@ -73,11 +74,11 @@ export default class AdminPostMenu extends Component {
}} }}
@action={{fn this.topicAction "togglePostType"}} @action={{fn this.topicAction "togglePostType"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if @data.transformedPost.canEditStaffNotes}} {{#if @data.transformedPost.canEditStaffNotes}}
<li> <dropdown.item>
<DButton <DButton
@icon="user-shield" @icon="user-shield"
@label={{if @label={{if
@ -93,18 +94,18 @@ export default class AdminPostMenu extends Component {
}} }}
@action={{fn this.topicAction "changeNotice"}} @action={{fn this.topicAction "changeNotice"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if (and this.currentUser.staff @data.transformedPost.hidden)}} {{#if (and this.currentUser.staff @data.transformedPost.hidden)}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.unhide" @label="post.controls.unhide"
@icon="far-eye" @icon="far-eye"
class="btn btn-transparent unhide-post" class="btn btn-transparent unhide-post"
@action={{fn this.topicAction "unhidePost"}} @action={{fn this.topicAction "unhidePost"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if {{#if
@ -116,7 +117,7 @@ export default class AdminPostMenu extends Component {
) )
) )
}} }}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.change_owner" @label="post.controls.change_owner"
@icon="user" @icon="user"
@ -124,23 +125,23 @@ export default class AdminPostMenu extends Component {
class="btn btn-transparent change-owner" class="btn btn-transparent change-owner"
@action={{fn this.topicAction "changePostOwner"}} @action={{fn this.topicAction "changePostOwner"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if (and @data.transformedPost.user_id this.currentUser.staff)}} {{#if (and @data.transformedPost.user_id this.currentUser.staff)}}
{{#if this.siteSettings.enable_badges}} {{#if this.siteSettings.enable_badges}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.grant_badge" @label="post.controls.grant_badge"
@icon="certificate" @icon="certificate"
class="btn btn-transparent grant-badge" class="btn btn-transparent grant-badge"
@action={{fn this.topicAction "grantBadge"}} @action={{fn this.topicAction "grantBadge"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if @data.transformedPost.locked}} {{#if @data.transformedPost.locked}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.unlock_post" @label="post.controls.unlock_post"
@icon="unlock" @icon="unlock"
@ -151,9 +152,9 @@ export default class AdminPostMenu extends Component {
}} }}
@action={{fn this.topicAction "unlockPost"}} @action={{fn this.topicAction "unlockPost"}}
/> />
</li> </dropdown.item>
{{else}} {{else}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.lock_post" @label="post.controls.lock_post"
@icon="lock" @icon="lock"
@ -161,24 +162,24 @@ export default class AdminPostMenu extends Component {
class="btn btn-transparent lock-post" class="btn btn-transparent lock-post"
@action={{fn this.topicAction "lockPost"}} @action={{fn this.topicAction "lockPost"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if @data.transformedPost.canPermanentlyDelete}} {{#if @data.transformedPost.canPermanentlyDelete}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.permanently_delete" @label="post.controls.permanently_delete"
@icon="trash-alt" @icon="trash-alt"
class="btn btn-transparent permanently-delete" class="btn btn-transparent permanently-delete"
@action={{fn this.topicAction "permanentlyDeletePost"}} @action={{fn this.topicAction "permanentlyDeletePost"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if (or @data.transformedPost.canManage @data.transformedPost.canWiki)}} {{#if (or @data.transformedPost.canManage @data.transformedPost.canWiki)}}
{{#if @data.transformedPost.wiki}} {{#if @data.transformedPost.wiki}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.unwiki" @label="post.controls.unwiki"
@icon="far-edit" @icon="far-edit"
@ -188,43 +189,43 @@ export default class AdminPostMenu extends Component {
}} }}
@action={{fn this.topicAction "toggleWiki"}} @action={{fn this.topicAction "toggleWiki"}}
/> />
</li> </dropdown.item>
{{else}} {{else}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.wiki" @label="post.controls.wiki"
@icon="far-edit" @icon="far-edit"
class="btn btn-transparent wiki" class="btn btn-transparent wiki"
@action={{fn this.topicAction "toggleWiki"}} @action={{fn this.topicAction "toggleWiki"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#if @data.transformedPost.canPublishPage}} {{#if @data.transformedPost.canPublishPage}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.publish_page" @label="post.controls.publish_page"
@icon="file" @icon="file"
class="btn btn-transparent publish-page" class="btn btn-transparent publish-page"
@action={{fn this.topicAction "showPagePublish"}} @action={{fn this.topicAction "showPagePublish"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#if @data.transformedPost.canManage}} {{#if @data.transformedPost.canManage}}
<li> <dropdown.item>
<DButton <DButton
@label="post.controls.rebake" @label="post.controls.rebake"
@icon="sync-alt" @icon="sync-alt"
class="btn btn-transparent rebuild-html" class="btn btn-transparent rebuild-html"
@action={{fn this.topicAction "rebakePost"}} @action={{fn this.topicAction "rebakePost"}}
/> />
</li> </dropdown.item>
{{/if}} {{/if}}
{{#each this.extraButtons as |button|}} {{#each this.extraButtons as |button|}}
<li> <dropdown.item>
<DButton <DButton
@label={{button.label}} @label={{button.label}}
@translatedLabel={{button.translatedLabel}} @translatedLabel={{button.translatedLabel}}
@ -232,8 +233,8 @@ export default class AdminPostMenu extends Component {
class={{concatClass "btn btn-transparent" button.className}} class={{concatClass "btn btn-transparent" button.className}}
@action={{fn this.extraAction button}} @action={{fn this.extraAction button}}
/> />
</li> </dropdown.item>
{{/each}} {{/each}}
</ul> </DropdownMenu>
</template> </template>
} }

View File

@ -5,6 +5,7 @@ import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import BookmarkModal from "discourse/components/modal/bookmark"; import BookmarkModal from "discourse/components/modal/bookmark";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { import {
@ -249,65 +250,69 @@ export default class BookmarkMenu extends Component {
@arrow={{false}} @arrow={{false}}
> >
<:content> <:content>
<div class="bookmark-menu__body"> <DropdownMenu as |dropdown|>
{{#unless this.showEditDeleteMenu}} {{#unless this.showEditDeleteMenu}}
<div class="bookmark-menu__title">{{icon "check-circle"}}<span <dropdown.item class="bookmark-menu__title">
>{{i18n "bookmarks.bookmarked_success"}}</span> {{icon "check-circle"}}
</div> <span>{{i18n "bookmarks.bookmarked_success"}}</span>
</dropdown.item>
{{/unless}} {{/unless}}
{{#if this.showEditDeleteMenu}} {{#if this.showEditDeleteMenu}}
{{#if this.site.mobileView}} {{#if this.site.mobileView}}
<div class="bookmark-menu__title">{{icon "bookmark"}}<span>{{i18n <dropdown.item class="bookmark-menu__title">
"bookmarks.bookmark" {{icon "bookmark"}}
}}</span> <span>{{i18n "bookmarks.bookmark"}}</span>
</div> </dropdown.item>
{{/if}} {{/if}}
<ul class="bookmark-menu__actions">
<li class="bookmark-menu__row -edit" data-menu-option-id="edit"> <dropdown.item
<DButton class="bookmark-menu__row -edit"
@icon="pencil-alt" data-menu-option-id="edit"
@label="edit" >
@action={{this.onEditBookmark}} <DButton
@class="bookmark-menu__row-btn btn-transparent" @icon="pencil-alt"
/> @label="edit"
</li> @action={{this.onEditBookmark}}
<li @class="bookmark-menu__row-btn btn-transparent"
class="bookmark-menu__row --remove" />
role="button" </dropdown.item>
tabindex="0" <dropdown.item
data-menu-option-id="delete" class="bookmark-menu__row --remove"
role="button"
tabindex="0"
data-menu-option-id="delete"
>
<DButton
@icon="trash-alt"
@label="delete"
@action={{this.onRemoveBookmark}}
@class="bookmark-menu__row-btn btn-transparent btn-danger"
/>
</dropdown.item>
{{else}}
<dropdown.item class="bookmark-menu__row-title">
{{i18n "bookmarks.also_set_reminder"}}
</dropdown.item>
<dropdown.divider />
{{#each this.reminderAtOptions as |option|}}
<dropdown.item
class="bookmark-menu__row"
data-menu-option-id={{option.id}}
> >
<DButton <DButton
@icon="trash-alt" @label={{option.label}}
@label="delete" @translatedTitle={{this.reminderShortcutTimeTitle option}}
@action={{this.onRemoveBookmark}} @action={{fn this.onChooseReminderOption option}}
@class="bookmark-menu__row-btn btn-transparent btn-danger" @class="bookmark-menu__row-btn btn-transparent"
/> />
</li> </dropdown.item>
</ul> {{/each}}
{{else}}
<span class="bookmark-menu__row-title">{{i18n
"bookmarks.also_set_reminder"
}}</span>
<ul class="bookmark-menu__actions">
{{#each this.reminderAtOptions as |option|}}
<li
class="bookmark-menu__row"
data-menu-option-id={{option.id}}
>
<DButton
@label={{option.label}}
@translatedTitle={{this.reminderShortcutTimeTitle option}}
@action={{fn this.onChooseReminderOption option}}
@class="bookmark-menu__row-btn btn-transparent"
/>
</li>
{{/each}}
</ul>
{{/if}} {{/if}}
</div> </DropdownMenu>
</:content> </:content>
</DMenu> </DMenu>
</template> </template>

View File

@ -1,22 +1,29 @@
import Component from "@glimmer/component";
import { fn } from "@ember/helper";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import BulkTopicActions, { import BulkTopicActions, {
addBulkDropdownAction, addBulkDropdownAction,
} from "discourse/components/modal/bulk-topic-actions"; } from "discourse/components/modal/bulk-topic-actions";
import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; import DMenu from "float-kit/components/d-menu";
const _customButtons = []; const _customButtons = [];
const _customOnSelection = {}; const _customOnSelection = {};
export function addBulkDropdownButton(opts) { export function addBulkDropdownButton(opts) {
_customButtons.push({ _customButtons.push({
id: opts.label, id: opts.id,
icon: opts.icon, icon: opts.icon,
name: i18n(opts.label), name: i18n(opts.label),
visible: opts.visible, visible: opts.visible,
class: opts.class,
}); });
addBulkDropdownAction(opts.label, opts.action); addBulkDropdownAction(opts.id, opts.action);
const actionOpts = { const actionOpts = {
label: opts.label, label: opts.label,
setComponent: true, setComponent: true,
@ -24,27 +31,17 @@ export function addBulkDropdownButton(opts) {
if (opts.actionType === "performAndRefresh") { if (opts.actionType === "performAndRefresh") {
actionOpts.setComponent = false; actionOpts.setComponent = false;
} }
_customOnSelection[opts.label] = actionOpts; _customOnSelection[opts.id] = actionOpts;
} }
export default DropdownSelectBoxComponent.extend({ export default class BulkSelectTopicsDropdown extends Component {
classNames: ["bulk-select-topics-dropdown"], @service modal;
headerIcon: null, @service router;
showFullTitle: true, @service currentUser;
selectKitOptions: { @service siteSettings;
showCaret: true,
showFullTitle: true,
none: "select_kit.components.bulk_select_topics_dropdown.title",
},
modal: service(), get buttons() {
router: service(), let options = [
currentUser: service(),
siteSettings: service(),
computeContent() {
let options = [];
options = options.concat([
{ {
id: "update-category", id: "update-category",
icon: "pencil-alt", icon: "pencil-alt",
@ -119,12 +116,12 @@ export default DropdownSelectBoxComponent.extend({
name: i18n("topic_bulk_actions.delete_topics.name"), name: i18n("topic_bulk_actions.delete_topics.name"),
visible: ({ currentUser }) => currentUser.staff, visible: ({ currentUser }) => currentUser.staff,
}, },
]); ];
return [...options, ..._customButtons].filter(({ visible }) => { return [...options, ..._customButtons].filter(({ visible }) => {
if (visible) { if (visible) {
return visible({ return visible({
topics: this.bulkSelectHelper.selected, topics: this.args.bulkSelectHelper.selected,
currentUser: this.currentUser, currentUser: this.currentUser,
siteSettings: this.siteSettings, siteSettings: this.siteSettings,
}); });
@ -132,7 +129,7 @@ export default DropdownSelectBoxComponent.extend({
return true; return true;
} }
}); });
}, }
showBulkTopicActionsModal(actionName, title, opts = {}) { showBulkTopicActionsModal(actionName, title, opts = {}) {
let allowSilent = false; let allowSilent = false;
@ -160,14 +157,14 @@ export default DropdownSelectBoxComponent.extend({
action: actionName, action: actionName,
title, title,
description, description,
bulkSelectHelper: this.bulkSelectHelper, bulkSelectHelper: this.args.bulkSelectHelper,
refreshClosure: () => this.router.refresh(), refreshClosure: () => this.router.refresh(),
allowSilent, allowSilent,
initialAction, initialAction,
initialActionLabel, initialActionLabel,
}, },
}); });
}, }
@action @action
onSelect(id) { onSelect(id) {
@ -228,5 +225,43 @@ export default DropdownSelectBoxComponent.extend({
}); });
} }
} }
},
}); this.dMenu.close();
}
@action
onRegisterApi(api) {
this.dMenu = api;
}
<template>
<DMenu
@modalForMobile={{true}}
@autofocus={{true}}
@identifier="bulk-select-topics-dropdown"
@onRegisterApi={{this.onRegisterApi}}
>
<:trigger>
<span class="d-button-label">
{{i18n "select_kit.components.bulk_select_topics_dropdown.title"}}
</span>
{{icon "angle-down"}}
</:trigger>
<:content>
<DropdownMenu as |dropdown|>
{{#each this.buttons as |button|}}
<dropdown.item>
<DButton
@translatedLabel={{button.name}}
@icon={{button.icon}}
class={{concatClass "btn-transparent" button.id button.class}}
@action={{fn this.onSelect button.id}}
/>
</dropdown.item>
{{/each}}
</DropdownMenu>
</:content>
</DMenu>
</template>
}

View File

@ -0,0 +1,17 @@
import { hash } from "@ember/helper";
const DropdownItem = <template>
<li class="dropdown-menu__item" ...attributes>{{yield}}</li>
</template>;
const DropdownDivider = <template>
<li ...attributes><hr class="dropdown-menu__divider" /></li>
</template>;
const DropdownMenu = <template>
<ul class="dropdown-menu" ...attributes>
{{yield (hash item=DropdownItem divider=DropdownDivider)}}
</ul>
</template>;
export default DropdownMenu;

View File

@ -4,6 +4,7 @@ import { action } from "@ember/object";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { and, not, or } from "truth-helpers"; import { and, not, or } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import getURL from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
@ -85,249 +86,230 @@ export default class TopicAdminMenu extends Component {
<span class="topic-admin-menu-button-container"> <span class="topic-admin-menu-button-container">
<span class="topic-admin-menu-button"> <span class="topic-admin-menu-button">
<DMenu <DMenu
@identifier="topic-admin-menu"
@onRegisterApi={{this.onRegisterApi}} @onRegisterApi={{this.onRegisterApi}}
@triggerClass="toggle-admin-menu" @triggerClass="toggle-admin-menu"
@modalForMobile={{true}} @modalForMobile={{true}}
@autofocus={{true}}
> >
<:trigger> <:trigger>
{{icon "wrench"}} {{icon "wrench"}}
</:trigger> </:trigger>
<:content> <:content>
<div class="popup-menu topic-admin-popup-menu"> <DropdownMenu as |dropdown|>
<ul> {{#if
<ul class="topic-admin-menu-topic"> (or
{{#if this.currentUser.canManageTopic
(or this.details.can_split_merge_topic
this.currentUser.canManageTopic )
this.details.can_split_merge_topic }}
) <dropdown.item class="topic-admin-multi-select">
}} <DButton
<li class="topic-admin-multi-select"> class="btn-transparent"
<DButton @label="topic.actions.multi_select"
class="btn-transparent" @action={{fn this.onButtonAction "toggleMultiSelect"}}
@label="topic.actions.multi_select" @icon="tasks"
@action={{fn this.onButtonAction "toggleMultiSelect"}} />
@icon="tasks" </dropdown.item>
/> {{/if}}
</li>
{{/if}}
{{#if {{#if
(or (or
this.currentUser.canManageTopic this.currentUser.canManageTopic
this.details.can_moderate_category this.details.can_moderate_category
) )
}} }}
{{#if this.canDelete}} {{#if this.canDelete}}
<li class="topic-admin-delete"> <dropdown.item class="topic-admin-delete">
<DButton <DButton
@label="topic.actions.delete" @label="topic.actions.delete"
@action={{fn this.onButtonAction "deleteTopic"}} @action={{fn this.onButtonAction "deleteTopic"}}
@icon="far-trash-alt" @icon="far-trash-alt"
class="popup-menu-btn-danger btn-danger btn-transparent" class="popup-menu-btn-danger btn-danger btn-transparent"
/> />
</li> </dropdown.item>
{{else if this.canRecover}} {{else if this.canRecover}}
<li class="topic-admin-recover"> <dropdown.item class="topic-admin-recover">
<DButton <DButton
class="btn-transparent" class="btn-transparent"
@label="topic.actions.recover" @label="topic.actions.recover"
@action={{fn this.onButtonAction "recoverTopic"}} @action={{fn this.onButtonAction "recoverTopic"}}
@icon="undo" @icon="undo"
/> />
</li> </dropdown.item>
{{/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>
{{#if (or this.currentUser.staff this.extraButtons.length)}}
<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>
{{/if}} {{/if}}
</ul> {{/if}}
</div>
{{#if this.details.can_close_topic}}
<dropdown.item
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"}}
/>
</dropdown.item>
{{/if}}
{{#if
(and
this.details.can_pin_unpin_topic
(not this.isPrivateMessage)
(or this.visible this.featured)
)
}}
<dropdown.item 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"
/>
</dropdown.item>
{{/if}}
{{#if
(and
this.details.can_archive_topic (not this.isPrivateMessage)
)
}}
<dropdown.item 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"
/>
</dropdown.item>
{{/if}}
{{#if this.details.can_toggle_topic_visibility}}
<dropdown.item 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"}}
/>
</dropdown.item>
{{/if}}
{{#if (and this.details.can_convert_topic)}}
<dropdown.item 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"}}
/>
</dropdown.item>
{{/if}}
<dropdown.divider />
{{#if this.currentUser.canManageTopic}}
<dropdown.item class="admin-topic-timer-update">
<DButton
class="btn-transparent"
@label="topic.actions.timed_update"
@action={{fn this.onButtonAction "showTopicTimerModal"}}
@icon="far-clock"
/>
</dropdown.item>
{{#if this.currentUser.staff}}
<dropdown.item class="topic-admin-change-timestamp">
<DButton
class="btn-transparent"
@label="topic.change_timestamp.title"
@action={{fn this.onButtonAction "showChangeTimestamp"}}
@icon="calendar-alt"
/>
</dropdown.item>
{{/if}}
<dropdown.item class="topic-admin-reset-bump-date">
<DButton
class="btn-transparent"
@label="topic.actions.reset_bump_date"
@action={{fn this.onButtonAction "resetBumpDate"}}
@icon="anchor"
/>
</dropdown.item>
<dropdown.item class="topic-admin-slow-mode">
<DButton
class="btn-transparent"
@label="topic.actions.slow_mode"
@action={{fn
this.onButtonAction
"showTopicSlowModeUpdate"
}}
@icon="hourglass-start"
/>
</dropdown.item>
<dropdown.divider />
{{/if}}
{{#if (or this.currentUser.staff this.extraButtons.length)}}
{{#if this.currentUser.staff}}
<dropdown.item class="topic-admin-moderation-history">
<DButton
class="btn-transparent"
@label="review.moderation_history"
@href={{this.topicModerationHistoryUrl}}
@icon="list"
/>
</dropdown.item>
{{/if}}
{{#each this.extraButtons as |button|}}
<dropdown.item>
<DButton
@label={{button.label}}
@translatedLabel={{button.translatedLabel}}
@icon={{button.icon}}
class={{concatClass "btn-transparent" button.className}}
@action={{fn this.onExtraButtonAction button.action}}
/>
</dropdown.item>
{{/each}}
{{/if}}
</DropdownMenu>
</:content> </:content>
</DMenu> </DMenu>
</span> </span>

View File

@ -1,5 +1,6 @@
import $ from "jquery"; import $ from "jquery";
import { h } from "virtual-dom"; import { h } from "virtual-dom";
import { addBulkDropdownButton } from "discourse/components/bulk-select-topics-dropdown";
import { import {
addApiImageWrapperButtonClickEvent, addApiImageWrapperButtonClickEvent,
addComposerUploadHandler, addComposerUploadHandler,
@ -141,7 +142,6 @@ import {
replaceIcon, replaceIcon,
} from "discourse-common/lib/icon-library"; } from "discourse-common/lib/icon-library";
import { addImageWrapperButton } from "discourse-markdown-it/features/image-controls"; import { addImageWrapperButton } from "discourse-markdown-it/features/image-controls";
import { addBulkDropdownButton } from "select-kit/components/bulk-select-topics-dropdown";
import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser"; import { CUSTOM_USER_SEARCH_OPTIONS } from "select-kit/components/user-chooser";
import { modifySelectKit } from "select-kit/mixins/plugin-api"; import { modifySelectKit } from "select-kit/mixins/plugin-api";

View File

@ -1,7 +1,7 @@
import EmberObject from "@ember/object"; import EmberObject from "@ember/object";
import BulkSelectTopicsDropdown from "discourse/components/bulk-select-topics-dropdown";
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer"; import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import BulkSelectTopicsDropdown from "select-kit/components/bulk-select-topics-dropdown";
export default class extends EmberObject { export default class extends EmberObject {
get selectedCount() { get selectedCount() {

View File

@ -827,8 +827,8 @@ export default createWidget("post-menu", {
this.menu.show(event.target, { this.menu.show(event.target, {
identifier: "admin-post-menu", identifier: "admin-post-menu",
component: AdminPostMenu, component: AdminPostMenu,
extraClassName: "popup-menu",
modalForMobile: true, modalForMobile: true,
autofocus: true,
data: { data: {
scheduleRerender: this.scheduleRerender.bind(this), scheduleRerender: this.scheduleRerender.bind(this),
transformedPost: this.attrs, transformedPost: this.attrs,

View File

@ -0,0 +1,37 @@
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import DropdownMenu from "discourse/components/dropdown-menu";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
module("Integration | Component | <DropdownMenu />", function (hooks) {
setupRenderingTest(hooks);
test("dropdown menu", async function (assert) {
await render(<template><DropdownMenu class="test" /></template>);
assert
.dom("ul.dropdown-menu.test")
.exists("it renders the dropdown menu with custom class");
});
test("dropdown menu item", async function (assert) {
await render(<template>
<DropdownMenu as |dm|><dm.item class="test">test</dm.item></DropdownMenu>
</template>);
assert
.dom("li.dropdown-menu__item.test")
.exists("it renders the item with custom class")
.hasText("test");
});
test("dropdown menu divider", async function (assert) {
await render(<template>
<DropdownMenu as |dm|><dm.divider class="test" /></DropdownMenu>
</template>);
assert
.dom("li.test hr.dropdown-menu__divider")
.exists("it renders the divider with custom class");
});
});

View File

@ -1,7 +1,9 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { concat } from "@ember/helper";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { and } from "truth-helpers"; import { and } from "truth-helpers";
import DModal from "discourse/components/d-modal"; import DModal from "discourse/components/d-modal";
import concatClass from "discourse/helpers/concat-class";
import DFloatBody from "float-kit/components/d-float-body"; import DFloatBody from "float-kit/components/d-float-body";
export default class DInlineFloat extends Component { export default class DInlineFloat extends Component {
@ -15,6 +17,10 @@ export default class DInlineFloat extends Component {
@hideHeader={{true}} @hideHeader={{true}}
data-identifier={{@instance.options.identifier}} data-identifier={{@instance.options.identifier}}
data-content data-content
class={{concatClass
"fk-d-menu-modal"
(concat @instance.options.identifier "-content")
}}
> >
{{#if @instance.options.component}} {{#if @instance.options.component}}
<@instance.options.component <@instance.options.component

View File

@ -99,6 +99,8 @@ export default class DMenu extends Component {
(concat this.options.identifier "-content") (concat this.options.identifier "-content")
}} }}
@inline={{(isTesting)}} @inline={{(isTesting)}}
data-identifier={{@instance.options.identifier}}
data-content
> >
{{#if (has-block)}} {{#if (has-block)}}
{{yield this.componentArgs}} {{yield this.componentArgs}}

View File

@ -48,4 +48,4 @@
@import "user-stream-item"; @import "user-stream-item";
@import "user-stream"; @import "user-stream";
@import "widget-dropdown"; @import "widget-dropdown";
@import "admin-post-menu"; @import "dropdown-menu";

View File

@ -1,32 +0,0 @@
[data-content][data-identifier="admin-post-menu"] {
.d-modal__body {
padding: 0;
}
ul {
padding: 0.5rem;
margin: 0;
list-style: none;
li .btn {
width: 100%;
justify-content: flex-start;
}
li {
margin-bottom: 2px;
border: none;
&:last-child {
margin-bottom: 0;
}
}
.btn {
justify-content: left;
text-align: left;
width: 100%;
padding: 0.5em;
}
}
}

View File

@ -1,22 +1,16 @@
.bookmark-menu-content { .bookmark-menu-content {
.bookmark-menu__body { .dropdown-menu {
background: var(--secondary); padding: 0;
display: flex;
flex-direction: column;
min-width: 200px; min-width: 200px;
} }
.bookmark-menu__actions {
margin: 0;
padding: 0;
list-style: none;
}
.bookmark-menu__title { .bookmark-menu__title {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75em; gap: 0.75em;
background: var(--tertiary-low); background: var(--tertiary-low);
color: var(--tertiary); color: var(--tertiary);
padding: 0.75rem; padding: 0.75rem 1rem;
font-weight: bold; font-weight: bold;
.d-icon { .d-icon {
@ -25,7 +19,6 @@
} }
.bookmark-menu__row { .bookmark-menu__row {
width: 100%;
display: flex; display: flex;
&:hover, &:hover,
@ -39,19 +32,18 @@
} }
.bookmark-menu__row-title { .bookmark-menu__row-title {
padding: 0.75rem; padding: 0.75rem 1rem;
border-bottom: 1px solid var(--primary-low);
font-weight: bold; font-weight: bold;
} }
.bookmark-menu__row-btn { .bookmark-menu__row-btn {
margin: 0; margin: 0;
padding: 0.75rem !important;
width: 100%; width: 100%;
text-align: left; text-align: left;
justify-content: left !important; justify-content: left !important;
gap: 0.75em; gap: 0.75rem;
color: var(--primary); color: var(--primary);
&:hover, &:hover,
&:focus { &:focus {
color: var(--primary) !important; color: var(--primary) !important;

View File

@ -0,0 +1,17 @@
.dropdown-menu {
padding: 0;
margin: 0;
list-style: none;
&__item {
.btn {
padding: 0.65rem 1rem;
width: 100%;
justify-content: flex-start;
}
}
&__divider {
margin: 0rem;
}
}

View File

@ -3,3 +3,4 @@
@import "user-stream-item"; @import "user-stream-item";
@import "more-topics"; @import "more-topics";
@import "bookmark-menu"; @import "bookmark-menu";
@import "dropdown-menu";

View File

@ -1,11 +0,0 @@
.bookmark-menu {
&__row {
border-bottom: 1px solid var(--primary-low);
&:last-child {
border-bottom: none;
}
}
&__row-title {
padding: 0.75rem;
}
}

View File

@ -0,0 +1,7 @@
.dropdown-menu {
&__item {
.btn {
padding: 0.75rem 1rem;
}
}
}

View File

@ -8,7 +8,7 @@ module PageObjects
end end
def open? def open?
has_css?(".bookmark-menu__body") has_css?(".bookmark-menu-content")
end end
end end
end end

View File

@ -20,14 +20,12 @@ module PageObjects
def has_bulk_select_topics_dropdown? def has_bulk_select_topics_dropdown?
page.has_css?( page.has_css?(
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown", "#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics .bulk-select-topics-dropdown",
) )
end end
def click_bulk_select_topics_dropdown def click_bulk_select_topics_dropdown
find( find("#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics .bulk-select-topics-dropdown").click
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown",
).click
end end
def click_bulk_button(name) def click_bulk_button(name)
@ -68,7 +66,7 @@ module PageObjects
private private
def bulk_select_dropdown_item(name) def bulk_select_dropdown_item(name)
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown li[data-value='#{name}']" ".bulk-select-topics-dropdown-content li.dropdown-menu__item .btn.#{name}"
end end
end end
end end

View File

@ -76,14 +76,14 @@ describe "Topic page", type: :system do
visit("/t/#{topic.slug}/#{topic.id}") visit("/t/#{topic.slug}/#{topic.id}")
expect(".topic-admin-menu-button-container").to be_present expect(".toggle-admin-menu").to be_present
send_keys([:shift, "a"]) send_keys([:shift, "a"])
expect(page).to have_css(".topic-admin-popup-menu") expect(page).to have_css(".topic-admin-menu-content")
send_keys([:shift, "a"]) send_keys([:shift, "a"])
expect(page).to have_no_css(".topic-admin-popup-menu") expect(page).to have_no_css(".topic-admin-menu-content")
end end
end end