UX: Automatically collapse admin page header buttons on mobile (#29040)

This commit attempts to improve the mobile experience for
admin page header and subheader by automatically collapsing
all action buttons in these components into a DMenu when viewing
mobile.

This is done by using different "list" wrapper components and a
DMenu trigger and a DropdownMenu on mobile only, and uses has-block
to determine whether to render the DMenu trigger at all.

This also removes the `PluginOutlet` in `AdminPluginConfigPage`, it
was too inflexible for this `DropdownMenu` case, and since the `:actions`
were always rendering we couldn't rely on `has-block`. A new plugin API,
`registerPluginHeaderActionComponent`, has been introduced instead to
replace it.
This commit is contained in:
Martin Brennan 2024-10-08 08:28:32 +10:00 committed by GitHub
parent 4ea3d69979
commit 85774cc214
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 406 additions and 56 deletions

View File

@ -74,14 +74,13 @@ export default class AdminBackupsActions extends Component {
@action={{routeAction "rollback"}} @action={{routeAction "rollback"}}
@label="admin.backups.operations.rollback.label" @label="admin.backups.operations.rollback.label"
@title="admin.backups.operations.rollback.title" @title="admin.backups.operations.rollback.title"
@icon="truck-medical"
@disabled={{this.rollbackDisabled}} @disabled={{this.rollbackDisabled}}
@icon="truck-medical"
class="admin-backups__rollback" class="admin-backups__rollback"
/> />
{{/if}} {{/if}}
<@actions.Default <@actions.Default
@icon={{if this.site.isReadOnly "far-eye-slash" "far-eye"}}
@action={{this.toggleReadOnlyMode}} @action={{this.toggleReadOnlyMode}}
@disabled={{@backups.isOperationRunning}} @disabled={{@backups.isOperationRunning}}
@title={{if @title={{if
@ -94,6 +93,7 @@ export default class AdminBackupsActions extends Component {
"admin.backups.read_only.disable.label" "admin.backups.read_only.disable.label"
"admin.backups.read_only.enable.label" "admin.backups.read_only.enable.label"
}} }}
@icon={{if this.site.isReadOnly "far-eye-slash" "far-eye"}}
class="admin-backups__toggle-read-only" class="admin-backups__toggle-read-only"
/> />
</template> </template>

View File

@ -1,3 +1,4 @@
import { hash } from "@ember/helper";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
export const AdminPageActionButton = <template> export const AdminPageActionButton = <template>
@ -13,6 +14,18 @@ export const AdminPageActionButton = <template>
@isLoading={{@isLoading}} @isLoading={{@isLoading}}
/> />
</template>; </template>;
// This is used for cases where there is another component,
// e.g. UppyBackupUploader, that is a button which cannot use
// PrimaryButton and so on directly. This should be used very rarely,
// most cases are covered by the other button types.
export const WrappedButton = <template>
<span class="admin-page-action-wrapped-button">{{yield}}</span>
</template>;
// No buttons here pass in an @icon by design. They are okay to
// use on dropdown list items, but our UI guidelines do not allow them
// on regular buttons.
export const PrimaryButton = <template> export const PrimaryButton = <template>
<AdminPageActionButton <AdminPageActionButton
class="btn-primary" class="btn-primary"
@ -22,10 +35,10 @@ export const PrimaryButton = <template>
@routeModels={{@routeModels}} @routeModels={{@routeModels}}
@label={{@label}} @label={{@label}}
@title={{@title}} @title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}} @isLoading={{@isLoading}}
/> />
</template>; </template>;
export const DangerButton = <template> export const DangerButton = <template>
<AdminPageActionButton <AdminPageActionButton
class="btn-danger" class="btn-danger"
@ -35,12 +48,55 @@ export const DangerButton = <template>
@routeModels={{@routeModels}} @routeModels={{@routeModels}}
@label={{@label}} @label={{@label}}
@title={{@title}} @title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}} @isLoading={{@isLoading}}
/> />
</template>; </template>;
export const DefaultButton = <template> export const DefaultButton = <template>
<AdminPageActionButton <AdminPageActionButton
class="btn-default"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const AdminPageActionListItem = <template>
<li class="dropdown-menu__item admin-page-action-list-item">
<AdminPageActionButton
class="btn-transparent"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</li>
</template>;
// This is used for cases where there is another component,
// e.g. UppyBackupUploader, that is a button which cannot use
// PrimaryActionListItem and so on directly. This should be used very rarely,
// most cases are covered by the other list types.
export const WrappedActionListItem = <template>
<li
class="dropdown-menu__item admin-page-action-list-item admin-page-action-wrapped-list-item"
>
{{yield (hash buttonClass="btn-transparent")}}
</li>
</template>;
// It is not a mistake that `btn-default` is used here, in a list
// there is no need for blue text.
export const PrimaryActionListItem = <template>
<AdminPageActionListItem
class="btn-default" class="btn-default"
...attributes ...attributes
@action={{@action}} @action={{@action}}
@ -52,3 +108,31 @@ export const DefaultButton = <template>
@isLoading={{@isLoading}} @isLoading={{@isLoading}}
/> />
</template>; </template>;
export const DefaultActionListItem = <template>
<AdminPageActionListItem
class="btn-default"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</template>;
export const DangerActionListItem = <template>
<AdminPageActionListItem
class="btn-danger"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</template>;

View File

@ -1,17 +1,28 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container"; import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DropdownMenu from "discourse/components/dropdown-menu";
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav"; import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import { import {
DangerActionListItem,
DangerButton, DangerButton,
DefaultActionListItem,
DefaultButton, DefaultButton,
PrimaryActionListItem,
PrimaryButton, PrimaryButton,
WrappedActionListItem,
WrappedButton,
} from "admin/components/admin-page-action-button"; } from "admin/components/admin-page-action-button";
import DMenu from "float-kit/components/d-menu";
export default class AdminPageHeader extends Component { export default class AdminPageHeader extends Component {
@service site;
get title() { get title() {
if (this.args.titleLabelTranslated) { if (this.args.titleLabelTranslated) {
return this.args.titleLabelTranslated; return this.args.titleLabelTranslated;
@ -41,14 +52,54 @@ export default class AdminPageHeader extends Component {
<h1 class="admin-page-header__title">{{this.title}}</h1> <h1 class="admin-page-header__title">{{this.title}}</h1>
{{/if}} {{/if}}
<div class="admin-page-header__actions"> {{#if (or (has-block "actions") @headerActionComponent)}}
{{yield <div class="admin-page-header__actions">
(hash {{#if this.site.mobileView}}
Primary=PrimaryButton Default=DefaultButton Danger=DangerButton <DMenu
) @identifier="admin-page-header-mobile-actions"
to="actions" @title={{i18n "more_options"}}
}} @icon="ellipsis-vertical"
</div> class="btn-small"
>
<:content>
<DropdownMenu class="admin-page-header__mobile-actions">
{{#let
(hash
Primary=PrimaryActionListItem
Default=DefaultActionListItem
Danger=DangerActionListItem
Wrapped=WrappedActionListItem
)
as |actions|
}}
{{#if (has-block "actions")}}
{{yield actions to="actions"}}
{{else}}
<@headerActionComponent @actions={{actions}} />
{{/if}}
{{/let}}
</DropdownMenu>
</:content>
</DMenu>
{{else}}
{{#let
(hash
Primary=PrimaryButton
Default=DefaultButton
Danger=DangerButton
Wrapped=WrappedButton
)
as |actions|
}}
{{#if (has-block "actions")}}
{{yield actions to="actions"}}
{{else}}
<@headerActionComponent @actions={{actions}} />
{{/if}}
{{/let}}
{{/if}}
</div>
{{/if}}
</div> </div>
{{#if this.description}} {{#if this.description}}

View File

@ -1,14 +1,24 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import DropdownMenu from "discourse/components/dropdown-menu";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import { import {
DangerActionListItem,
DangerButton, DangerButton,
DefaultActionListItem,
DefaultButton, DefaultButton,
PrimaryActionListItem,
PrimaryButton, PrimaryButton,
WrappedActionListItem,
WrappedButton,
} from "admin/components/admin-page-action-button"; } from "admin/components/admin-page-action-button";
import DMenu from "float-kit/components/d-menu";
export default class AdminPageSubheader extends Component { export default class AdminPageSubheader extends Component {
@service site;
get title() { get title() {
if (this.args.titleLabelTranslated) { if (this.args.titleLabelTranslated) {
return this.args.titleLabelTranslated; return this.args.titleLabelTranslated;
@ -29,14 +39,42 @@ export default class AdminPageSubheader extends Component {
<div class="admin-page-subheader"> <div class="admin-page-subheader">
<div class="admin-page-subheader__title-row"> <div class="admin-page-subheader__title-row">
<h3 class="admin-page-subheader__title">{{this.title}}</h3> <h3 class="admin-page-subheader__title">{{this.title}}</h3>
<div class="admin-page-subheader__actions"> {{#if (has-block "actions")}}
{{yield <div class="admin-page-subheader__actions">
(hash {{#if this.site.mobileView}}
Primary=PrimaryButton Default=DefaultButton Danger=DangerButton <DMenu
) @identifier="admin-page-subheader-mobile-actions"
to="actions" @title={{i18n "more_options"}}
}} @icon="ellipsis-vertical"
</div> class="btn-small"
>
<:content>
<DropdownMenu class="admin-page-subheader__mobile-actions">
{{yield
(hash
Primary=PrimaryActionListItem
Default=DefaultActionListItem
Danger=DangerActionListItem
Wrapped=WrappedActionListItem
)
to="actions"
}}
</DropdownMenu>
</:content>
</DMenu>
{{else}}
{{yield
(hash
Primary=PrimaryButton
Default=DefaultButton
Danger=DangerButton
Wrapped=WrappedButton
)
to="actions"
}}
{{/if}}
</div>
{{/if}}
</div> </div>
{{#if this.description}} {{#if this.description}}

View File

@ -1,9 +1,8 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { service } from "@ember/service"; import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import NavItem from "discourse/components/nav-item"; import NavItem from "discourse/components/nav-item";
import PluginOutlet from "discourse/components/plugin-outlet"; import { headerActionComponentForPlugin } from "discourse/lib/admin-plugin-header-actions";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import AdminPageHeader from "./admin-page-header"; import AdminPageHeader from "./admin-page-header";
import AdminPluginConfigArea from "./admin-plugin-config-area"; import AdminPluginConfigArea from "./admin-plugin-config-area";
@ -25,7 +24,11 @@ export default class AdminPluginConfigPage extends Component {
} }
get actionsOutletName() { get actionsOutletName() {
return `admin-plugin-config-page-actions-${this.args.plugin.kebabCaseName}`; return `admin-plugin-config-page-actions-${this.args.plugin.dasherizedName}`;
}
get headerActionComponent() {
return headerActionComponentForPlugin(this.args.plugin.dasherizedName);
} }
linkText(navLink) { linkText(navLink) {
@ -42,6 +45,7 @@ export default class AdminPluginConfigPage extends Component {
@titleLabelTranslated={{@plugin.nameTitleized}} @titleLabelTranslated={{@plugin.nameTitleized}}
@descriptionLabelTranslated={{@plugin.about}} @descriptionLabelTranslated={{@plugin.about}}
@learnMoreUrl={{@plugin.linkUrl}} @learnMoreUrl={{@plugin.linkUrl}}
@headerActionComponent={{this.headerActionComponent}}
> >
<:breadcrumbs> <:breadcrumbs>
@ -71,14 +75,6 @@ export default class AdminPluginConfigPage extends Component {
{{/each}} {{/each}}
{{/if}} {{/if}}
</:tabs> </:tabs>
<:actions as |actions|>
<div class={{this.actionsOutletName}}>
<PluginOutlet
@name={{this.actionsOutletName}}
@outletArgs={{hash plugin=@plugin actions=actions}}
/>
</div>
</:actions>
</AdminPageHeader> </AdminPageHeader>
<div class="admin-plugin-config-page__content"> <div class="admin-plugin-config-page__content">

View File

@ -1,5 +1,5 @@
import { cached, tracked } from "@glimmer/tracking"; import { cached, tracked } from "@glimmer/tracking";
import { capitalize } from "@ember/string"; import { capitalize, dasherize } from "@ember/string";
import { snakeCaseToCamelCase } from "discourse-common/lib/case-converter"; import { snakeCaseToCamelCase } from "discourse-common/lib/case-converter";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
@ -24,8 +24,8 @@ export default class AdminPlugin {
return this.name.replaceAll("-", "_"); return this.name.replaceAll("-", "_");
} }
get kebabCaseName() { get dasherizedName() {
return this.name.replaceAll(" ", "-").replaceAll("_", "-"); return dasherize(this.name);
} }
get translatedCategoryName() { get translatedCategoryName() {

View File

@ -1,13 +1,19 @@
<AdminPageSubheader @titleLabel="admin.backups.files_title"> <AdminPageSubheader @titleLabel="admin.backups.files_title">
<:actions> <:actions as |actions|>
{{#if this.localBackupStorage}} <actions.Wrapped as |wrapped|>
<UppyBackupUploader {{#if this.localBackupStorage}}
@done={{route-action "uploadSuccess"}} <UppyBackupUploader
@localBackupStorage={{this.localBackupStorage}} class={{wrapped.buttonClass}}
/> @done={{route-action "uploadSuccess"}}
{{else}} @localBackupStorage={{this.localBackupStorage}}
<UppyBackupUploader @done={{route-action "remoteUploadSuccess"}} /> />
{{/if}} {{else}}
<UppyBackupUploader
class={{wrapped.buttonClass}}
@done={{route-action "remoteUploadSuccess"}}
/>
{{/if}}
</actions.Wrapped>
</:actions> </:actions>
</AdminPageSubheader> </AdminPageSubheader>
@ -34,10 +40,9 @@
<tr class="backup-item-row" data-backup-filename={{backup.filename}}> <tr class="backup-item-row" data-backup-filename={{backup.filename}}>
<td class="backup-filename">{{backup.filename}}</td> <td class="backup-filename">{{backup.filename}}</td>
<td class="backup-size">{{human-size backup.size}}</td> <td class="backup-size">{{human-size backup.size}}</td>
<td class="backup-controls"> <td class="backup-controls admin-table-row-controls">
<DButton <DButton
@action={{fn this.download backup}} @action={{fn this.download backup}}
@icon="download"
@title="admin.backups.operations.download.title" @title="admin.backups.operations.download.title"
@label="admin.backups.operations.download.label" @label="admin.backups.operations.download.label"
class="btn-default btn-small backup-item-row__download" class="btn-default btn-small backup-item-row__download"

View File

@ -2,8 +2,9 @@
class="btn btn-small btn-primary admin-backups-upload" class="btn btn-small btn-primary admin-backups-upload"
disabled={{this.uploading}} disabled={{this.uploading}}
title={{i18n "admin.backups.upload.title"}} title={{i18n "admin.backups.upload.title"}}
...attributes
> >
{{d-icon "upload"}}{{this.uploadButtonText}} {{this.uploadButtonText}}
<input <input
class="hidden-upload-field" class="hidden-upload-field"
disabled={{this.uploading}} disabled={{this.uploading}}

View File

@ -0,0 +1,13 @@
let pluginHeaderActionComponents = new Map();
export function registerPluginHeaderActionComponent(pluginId, componentClass) {
pluginHeaderActionComponents.set(pluginId, componentClass);
}
export function clearPluginHeaderActionComponents() {
pluginHeaderActionComponents = new Map();
}
export function headerActionComponentForPlugin(pluginId) {
return pluginHeaderActionComponents.get(pluginId);
}

View File

@ -61,6 +61,7 @@ import {
PLUGIN_NAV_MODE_TOP, PLUGIN_NAV_MODE_TOP,
registerAdminPluginConfigNav, registerAdminPluginConfigNav,
} from "discourse/lib/admin-plugin-config-nav"; } from "discourse/lib/admin-plugin-config-nav";
import { registerPluginHeaderActionComponent } from "discourse/lib/admin-plugin-header-actions";
import classPrepend, { import classPrepend, {
withPrependsRolledBack, withPrependsRolledBack,
} from "discourse/lib/class-prepend"; } from "discourse/lib/class-prepend";
@ -3252,6 +3253,24 @@ class PluginApi {
addLegacyAboutPageStat(name); addLegacyAboutPageStat(name);
} }
/**
* Registers a component class that will be rendered within the AdminPageHeader component
* only on plugins using the AdminPluginConfigPage and the new plugin "show" route.
*
* This component will be passed an `@actions` argument, with Primary, Default, Danger,
* and Wrapped keys, which can be used for various different types of buttons (Wrapped
* should be used only in very rare scenarios).
*
* This component would be used for actions that should be present on the entire UI
* for that plugin -- one example is "Create export" for chat.
*
* @param {string} pluginId - The `dasherizedName` of the plugin using this component.
* @param {Class} componentClass - The JS class of the component to render.
*/
registerPluginHeaderActionComponent(pluginId, componentClass) {
registerPluginHeaderActionComponent(pluginId, componentClass);
}
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
#deprecatedWidgetOverride(widgetName, override) { #deprecatedWidgetOverride(widgetName, override) {
// insert here the code to handle widget deprecations, e.g. for the header widgets we used: // insert here the code to handle widget deprecations, e.g. for the header widgets we used:

View File

@ -35,6 +35,7 @@ import { clearHTMLCache } from "discourse/helpers/custom-html";
import { resetUsernameDecorators } from "discourse/helpers/decorate-username-selector"; import { resetUsernameDecorators } from "discourse/helpers/decorate-username-selector";
import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete"; import { resetBeforeAuthCompleteCallbacks } from "discourse/instance-initializers/auth-complete";
import { resetAdminPluginConfigNav } from "discourse/lib/admin-plugin-config-nav"; import { resetAdminPluginConfigNav } from "discourse/lib/admin-plugin-config-nav";
import { clearPluginHeaderActionComponents } from "discourse/lib/admin-plugin-header-actions";
import { rollbackAllPrepends } from "discourse/lib/class-prepend"; import { rollbackAllPrepends } from "discourse/lib/class-prepend";
import { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options"; import { clearPopupMenuOptions } from "discourse/lib/composer/custom-popup-menu-options";
import { clearDesktopNotificationHandlers } from "discourse/lib/desktop-notifications"; import { clearDesktopNotificationHandlers } from "discourse/lib/desktop-notifications";
@ -251,6 +252,7 @@ export function testCleanup(container, app) {
clearAboutPageActivities(); clearAboutPageActivities();
clearLegacyAboutPageStats(); clearLegacyAboutPageStats();
resetWidgetCleanCallbacks(); resetWidgetCleanCallbacks();
clearPluginHeaderActionComponents();
} }
function cleanupCssGeneratorTags() { function cleanupCssGeneratorTags() {

View File

@ -2,10 +2,23 @@ import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit"; import { module, test } from "qunit";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import NavItem from "discourse/components/nav-item"; import NavItem from "discourse/components/nav-item";
import { forceMobile } from "discourse/lib/mobile";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import AdminPageHeader from "admin/components/admin-page-header"; import AdminPageHeader from "admin/components/admin-page-header";
const AdminPageHeaderActionsTestComponent = <template>
<div class="admin-page-header-actions-test-component">
<@actions.Default
@route="adminBadges.award"
@routeModels="new"
@icon="upload"
@label="admin.badges.mass_award.title"
class="award-badge"
/>
</div>
</template>;
module("Integration | Component | AdminPageHeader", function (hooks) { module("Integration | Component | AdminPageHeader", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
@ -174,4 +187,60 @@ module("Integration | Component | AdminPageHeader", function (hooks) {
await click(".edit-groupings-btn"); await click(".edit-groupings-btn");
assert.true(actionCalled); assert.true(actionCalled);
}); });
test("@headerActionComponent is rendered with actions arg", async function (assert) {
await render(<template>
<AdminPageHeader
@headerActionComponent={{AdminPageHeaderActionsTestComponent}}
/>
</template>);
assert
.dom(".admin-page-header-actions-test-component .award-badge")
.exists();
});
});
module("Integration | Component | AdminPageHeader | Mobile", function (hooks) {
hooks.beforeEach(function () {
forceMobile();
});
setupRenderingTest(hooks);
test("action buttons become a dropdown on mobile", async function (assert) {
await render(<template>
<AdminPageHeader>
<:actions as |actions|>
<actions.Primary
@route="adminBadges.show"
@routeModels="new"
@icon="plus"
@label="admin.badges.new"
class="new-badge"
/>
<actions.Default
@route="adminBadges.award"
@routeModels="new"
@icon="upload"
@label="admin.badges.mass_award.title"
class="award-badge"
/>
</:actions>
</AdminPageHeader>
</template>);
assert
.dom(
".admin-page-header__actions .fk-d-menu__trigger.admin-page-header-mobile-actions-trigger"
)
.exists();
await click(".admin-page-header-mobile-actions-trigger");
assert
.dom(".dropdown-menu.admin-page-header__mobile-actions .new-badge")
.exists();
});
}); });

View File

@ -1,5 +1,6 @@
import { click, render } from "@ember/test-helpers"; import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { forceMobile } from "discourse/lib/mobile";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import i18n from "discourse-common/helpers/i18n"; import i18n from "discourse-common/helpers/i18n";
import AdminPageSubheader from "admin/components/admin-page-subheader"; import AdminPageSubheader from "admin/components/admin-page-subheader";
@ -127,3 +128,50 @@ module("Integration | Component | AdminPageSubheader", function (hooks) {
assert.true(actionCalled); assert.true(actionCalled);
}); });
}); });
module(
"Integration | Component | AdminPageSubheader | Mobile",
function (hooks) {
hooks.beforeEach(function () {
forceMobile();
});
setupRenderingTest(hooks);
test("action buttons become a dropdown on mobile", async function (assert) {
await render(<template>
<AdminPageSubheader>
<:actions as |actions|>
<actions.Primary
@route="adminBadges.show"
@routeModels="new"
@icon="plus"
@label="admin.badges.new"
class="new-badge"
/>
<actions.Default
@route="adminBadges.award"
@routeModels="new"
@icon="upload"
@label="admin.badges.mass_award.title"
class="award-badge"
/>
</:actions>
</AdminPageSubheader>
</template>);
assert
.dom(
".admin-page-subheader .fk-d-menu__trigger.admin-page-subheader-mobile-actions-trigger"
)
.exists();
await click(".admin-page-subheader-mobile-actions-trigger");
assert
.dom(".dropdown-menu.admin-page-subheader__mobile-actions .new-badge")
.exists();
});
}
);

View File

@ -6,10 +6,6 @@
align-items: stretch; align-items: stretch;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
@media (max-width: $mobile-breakpoint) {
flex-direction: column;
}
h1, h1,
h3 { h3 {
margin: 0; margin: 0;
@ -58,3 +54,33 @@
} }
} }
} }
.admin-page-header {
&__title-row {
@media (max-width: $mobile-breakpoint) {
flex-direction: row;
align-items: center;
.admin-page-header__actions {
button {
margin-bottom: 0;
}
}
}
}
}
.admin-page-subheader {
&__title-row {
@media (max-width: $mobile-breakpoint) {
flex-direction: row;
align-items: center;
}
}
}
.admin-page-action-list-item {
.btn-primary {
color: var(--primary);
}
}

View File

@ -97,7 +97,6 @@
} }
.award-badge { .award-badge {
margin: 15px 0 0 15px;
float: left; float: left;
max-width: 70%; max-width: 70%;

View File

@ -29,10 +29,11 @@ export default class ChatAdminPluginActions extends Component {
} }
<template> <template>
<@outletArgs.actions.Primary <@actions.Primary
@label="chat.admin.export_messages.create_export" @label="chat.admin.export_messages.create_export"
@title="chat.admin.export_messages.create_export" @title="chat.admin.export_messages.create_export"
@action={{this.confirmExportMessages}} @action={{this.confirmExportMessages}}
@icon="right-from-bracket"
class="admin-chat-export" class="admin-chat-export"
/> />
</template> </template>

View File

@ -14,6 +14,7 @@
@title="chat.incoming_webhooks.new" @title="chat.incoming_webhooks.new"
@route="adminPlugins.show.discourse-chat-incoming-webhooks.new" @route="adminPlugins.show.discourse-chat-incoming-webhooks.new"
@routeModels="chat" @routeModels="chat"
@icon="plus"
class="admin-incoming-webhooks-new" class="admin-incoming-webhooks-new"
/> />
</:actions> </:actions>

View File

@ -19,10 +19,7 @@ export default {
}, },
]); ]);
api.renderInOutlet( api.registerPluginHeaderActionComponent("chat", ChatAdminPluginActions);
"admin-plugin-config-page-actions-chat",
ChatAdminPluginActions
);
}); });
}, },
}; };