mirror of
https://github.com/discourse/discourse.git
synced 2025-02-16 23:02:45 +08:00
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:
parent
4ea3d69979
commit
85774cc214
|
@ -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>
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -97,7 +97,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.award-badge {
|
.award-badge {
|
||||||
margin: 15px 0 0 15px;
|
|
||||||
float: left;
|
float: left;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -19,10 +19,7 @@ export default {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
api.renderInOutlet(
|
api.registerPluginHeaderActionComponent("chat", ChatAdminPluginActions);
|
||||||
"admin-plugin-config-page-actions-chat",
|
|
||||||
ChatAdminPluginActions
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user