DEV: Introduce <DPageHeader /> and <DPageSubheader /> components (#30146)

This converts the `<AdminPageHeader />` component and the
`<AdminPageSubheader />` components into new components
that can be used outside of admin, and updates the CSS classes.
Also introduces a `<DPageActionButton />` component and child
components for the header action buttons.

I have to keep the old admin-only components around for
now until plugins are updated, then we can remove it,
and remove the re-exports that are done within
admin-page-action-button.gjs
This commit is contained in:
Martin Brennan 2024-12-18 08:13:39 +10:00 committed by GitHub
parent ef754cdd9a
commit a879bcdc35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 789 additions and 489 deletions

View File

@ -1,8 +1,8 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import DPageSubheader from "discourse/components/d-page-subheader";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import AdminPageSubheader from "admin/components/admin-page-subheader";
import InstallThemeModal from "admin/components/modal/install-theme"; import InstallThemeModal from "admin/components/modal/install-theme";
import ThemesGrid from "admin/components/themes-grid"; import ThemesGrid from "admin/components/themes-grid";
@ -47,9 +47,9 @@ export default class AdminConfigAreasLookAndFeelThemes extends Component {
} }
<template> <template>
<AdminPageSubheader <DPageSubheader
@titleLabel="admin.config_areas.look_and_feel.themes.title" @titleLabel={{i18n "admin.config_areas.look_and_feel.themes.title"}}
@descriptionLabel="admin.customize.theme.themes_intro_new" @descriptionLabel={{i18n "admin.customize.theme.themes_intro_new"}}
@learnMoreUrl="https://meta.discourse.org/t/93648" @learnMoreUrl="https://meta.discourse.org/t/93648"
> >
<:actions as |actions|> <:actions as |actions|>
@ -60,7 +60,7 @@ export default class AdminConfigAreasLookAndFeelThemes extends Component {
class="admin-look-and-feel__install-theme" class="admin-look-and-feel__install-theme"
/> />
</:actions> </:actions>
</AdminPageSubheader> </DPageSubheader>
<div class="admin-detail"> <div class="admin-detail">
<ThemesGrid @themes={{@themes}} /> <ThemesGrid @themes={{@themes}} />

View File

@ -1,138 +1,25 @@
import { hash } from "@ember/helper"; // TODO (martin) Delete this once we have removed references from plugins.
import DButton from "discourse/components/d-button";
export const AdminPageActionButton = <template> import {
<DButton DangerActionListItem,
class="admin-page-action-button btn-small" DangerButton,
...attributes DefaultActionListItem,
@action={{@action}} DefaultButton,
@route={{@route}} DPageActionButton,
@routeModels={{@routeModels}} DPageActionListItem,
@label={{@label}} PrimaryActionListItem,
@title={{@title}} PrimaryButton,
@icon={{@icon}} WrappedActionListItem,
@isLoading={{@isLoading}} WrappedButton,
/> } from "discourse/components/d-page-action-button";
</template>;
// This is used for cases where there is another component, export { DangerActionListItem as DangerActionListItem };
// e.g. UppyBackupUploader, that is a button which cannot use export { DangerButton as DangerButton };
// PrimaryButton and so on directly. This should be used very rarely, export { DefaultActionListItem as DefaultActionListItem };
// most cases are covered by the other button types. export { DefaultButton as DefaultButton };
export const WrappedButton = <template> export { DPageActionButton as DPageActionButton };
<span class="admin-page-action-wrapped-button">{{yield}}</span> export { DPageActionListItem as DPageActionListItem };
</template>; export { PrimaryActionListItem as PrimaryActionListItem };
export { PrimaryButton as PrimaryButton };
// No buttons here pass in an @icon by design. They are okay to export { WrappedActionListItem as WrappedActionListItem };
// use on dropdown list items, but our UI guidelines do not allow them export { WrappedButton as WrappedButton };
// on regular buttons.
export const PrimaryButton = <template>
<AdminPageActionButton
class="btn-primary"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const DangerButton = <template>
<AdminPageActionButton
class="btn-danger"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const DefaultButton = <template>
<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"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</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,10 +1,10 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
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 DPageHeader from "discourse/components/d-page-header";
import NavItem from "discourse/components/nav-item"; import NavItem from "discourse/components/nav-item";
import { headerActionComponentForPlugin } from "discourse/lib/admin-plugin-header-actions"; import { headerActionComponentForPlugin } from "discourse/lib/admin-plugin-header-actions";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import AdminPageHeader from "./admin-page-header";
import AdminPluginConfigArea from "./admin-plugin-config-area"; import AdminPluginConfigArea from "./admin-plugin-config-area";
export default class AdminPluginConfigPage extends Component { export default class AdminPluginConfigPage extends Component {
@ -41,14 +41,14 @@ export default class AdminPluginConfigPage extends Component {
<template> <template>
<div class="admin-plugin-config-page"> <div class="admin-plugin-config-page">
<AdminPageHeader <DPageHeader
@titleLabelTranslated={{@plugin.nameTitleized}} @titleLabel={{@plugin.nameTitleized}}
@descriptionLabelTranslated={{@plugin.about}} @descriptionLabel={{@plugin.about}}
@learnMoreUrl={{@plugin.linkUrl}} @learnMoreUrl={{@plugin.linkUrl}}
@headerActionComponent={{this.headerActionComponent}} @headerActionComponent={{this.headerActionComponent}}
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/plugins" @path="/admin/plugins"
@label={{i18n "admin.plugins.title"}} @label={{i18n "admin.plugins.title"}}
@ -75,7 +75,7 @@ export default class AdminPluginConfigPage extends Component {
{{/each}} {{/each}}
{{/if}} {{/if}}
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="admin-plugin-config-page__content"> <div class="admin-plugin-config-page__content">
<div class={{this.mainAreaClasses}}> <div class={{this.mainAreaClasses}}>

View File

@ -1,14 +1,14 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { LinkTo } from "@ember/routing"; import { LinkTo } from "@ember/routing";
import concatClass from "discourse/helpers/concat-class";
import dIcon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
import { import {
DangerButton, DangerButton,
DefaultButton, DefaultButton,
PrimaryButton, PrimaryButton,
} from "admin/components/admin-page-action-button"; } from "discourse/components/d-page-action-button";
import concatClass from "discourse/helpers/concat-class";
import dIcon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class AdminSectionLandingItem extends Component { export default class AdminSectionLandingItem extends Component {
get title() { get title() {

View File

@ -1,10 +1,11 @@
<div class="badges"> <div class="badges">
<AdminPageHeader <DPageHeader
@titleLabel="admin.badges.title" @titleLabel={{i18n "admin.badges.title"}}
@descriptionLabel="admin.badges.page_description" @descriptionLabel={{i18n "admin.badges.page_description"}}
@learnMoreUrl="https://meta.discourse.org/t/understanding-and-using-badges/32540" @learnMoreUrl="https://meta.discourse.org/t/understanding-and-using-badges/32540"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/badges" @path="/admin/badges"
@label={{i18n "admin.badges.title"}} @label={{i18n "admin.badges.title"}}
@ -35,7 +36,7 @@
class="edit-groupings-btn" class="edit-groupings-btn"
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
<div class="admin-container"> <div class="admin-container">
<div class="content-list"> <div class="content-list">

View File

@ -1,4 +1,4 @@
<AdminPageSubheader @titleLabel="admin.backups.files_title"> <DPageSubheader @titleLabel={{i18n "admin.backups.files_title"}}>
<:actions as |actions|> <:actions as |actions|>
<actions.Wrapped as |wrapped|> <actions.Wrapped as |wrapped|>
{{#if this.localBackupStorage}} {{#if this.localBackupStorage}}
@ -15,7 +15,7 @@
{{/if}} {{/if}}
</actions.Wrapped> </actions.Wrapped>
</:actions> </:actions>
</AdminPageSubheader> </DPageSubheader>
{{#if this.status.restoreDisabled}} {{#if this.status.restoreDisabled}}
<div class="backup-message alert alert-info"> <div class="backup-message alert alert-info">

View File

@ -1,11 +1,11 @@
<div class="admin-backups admin-config-page"> <div class="admin-backups admin-config-page">
<DPageHeader
<AdminPageHeader @titleLabel={{i18n "admin.backups.title"}}
@titleLabel="admin.backups.title" @descriptionLabel={{i18n "admin.backups.description"}}
@descriptionLabel="admin.backups.description"
@learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710" @learnMoreUrl="https://meta.discourse.org/t/create-download-and-restore-a-backup-of-your-discourse-database/122710"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/backups" @path="/admin/backups"
@label={{i18n "admin.backups.title"}} @label={{i18n "admin.backups.title"}}
@ -32,7 +32,7 @@
/> />
<PluginOutlet @name="downloader" @connectorTagName="div" /> <PluginOutlet @name="downloader" @connectorTagName="div" />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<PluginOutlet @name="before-backup-list" @connectorTagName="div" /> <PluginOutlet @name="before-backup-list" @connectorTagName="div" />

View File

@ -1,6 +1,6 @@
<AdminPageHeader <DPageHeader
@titleLabel="admin.config_areas.about.header" @titleLabel={{i18n "admin.config_areas.about.header"}}
@descriptionLabelTranslated={{i18n @descriptionLabel={{i18n
"admin.config_areas.about.description" "admin.config_areas.about.description"
(hash basePath=(base-path)) (hash basePath=(base-path))
}} }}
@ -8,12 +8,13 @@
@learnMoreUrl="https://meta.discourse.org/t/understanding-and-customizing-the-about-page/332161" @learnMoreUrl="https://meta.discourse.org/t/understanding-and-customizing-the-about-page/332161"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/config/about" @path="/admin/config/about"
@label={{i18n "admin.config_areas.about.header"}} @label={{i18n "admin.config_areas.about.header"}}
/> />
</:breadcrumbs> </:breadcrumbs>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
<AdminConfigAreas::About @data={{this.model.site_settings}} /> <AdminConfigAreas::About @data={{this.model.site_settings}} />

View File

@ -1,9 +1,10 @@
<AdminPageHeader <DPageHeader
@titleLabel="admin.config_areas.flags.header" @titleLabel={{i18n "admin.config_areas.flags.header"}}
@descriptionLabel="admin.config_areas.flags.subheader" @descriptionLabel={{i18n "admin.config_areas.flags.subheader"}}
@learnMoreUrl="https://meta.discourse.org/t/moderation-flags/325589" @learnMoreUrl="https://meta.discourse.org/t/moderation-flags/325589"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/config/flags" @path="/admin/config/flags"
@label={{i18n "admin.config_areas.flags.header"}} @label={{i18n "admin.config_areas.flags.header"}}
@ -30,7 +31,7 @@
class="admin-flags-tabs__flags" class="admin-flags-tabs__flags"
/> />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
{{outlet}} {{outlet}}

View File

@ -1,9 +1,10 @@
<AdminPageHeader <DPageHeader
@titleLabel="admin.config_areas.look_and_feel.title" @titleLabel={{i18n "admin.config_areas.look_and_feel.title"}}
@descriptionLabel="admin.config_areas.look_and_feel.description" @descriptionLabel={{i18n "admin.config_areas.look_and_feel.description"}}
@learnMoreUrl="https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966" @learnMoreUrl="https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/config/look-and-feel" @path="/admin/config/look-and-feel"
@label={{i18n "admin.config_areas.look_and_feel.title"}} @label={{i18n "admin.config_areas.look_and_feel.title"}}
@ -16,7 +17,7 @@
@label="admin.config_areas.look_and_feel.themes.title" @label="admin.config_areas.look_and_feel.themes.title"
/> />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
{{outlet}} {{outlet}}

View File

@ -2,11 +2,11 @@
<PluginOutlet @name="admin-dashboard-top" @connectorTagName="div" /> <PluginOutlet @name="admin-dashboard-top" @connectorTagName="div" />
</span> </span>
<AdminPageHeader @hideTabs={{true}}> <DPageHeader @hideTabs={{true}}>
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin.dashboard.title"}} /> <DBreadcrumbsItem @path="/admin" @label={{i18n "admin.dashboard.title"}} />
</:breadcrumbs> </:breadcrumbs>
</AdminPageHeader> </DPageHeader>
{{#if this.showVersionChecks}} {{#if this.showVersionChecks}}
<div class="section-top"> <div class="section-top">

View File

@ -1,10 +1,11 @@
<div class="admin-emojis admin-config-page"> <div class="admin-emojis admin-config-page">
<AdminPageHeader <DPageHeader
@titleLabel="admin.emoji.title" @titleLabel={{i18n "admin.emoji.title"}}
@descriptionLabel="admin.emoji.description" @descriptionLabel={{i18n "admin.emoji.description"}}
@hideTabs={{this.hideTabs}} @hideTabs={{this.hideTabs}}
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/customize/emojis" @path="/admin/customize/emojis"
@label={{i18n "admin.emoji.title"}} @label={{i18n "admin.emoji.title"}}
@ -25,7 +26,7 @@
class="admin-emojis-tabs__emoji" class="admin-emojis-tabs__emoji"
/> />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
{{outlet}} {{outlet}}

View File

@ -1,10 +1,11 @@
<div class="admin-permalinks admin-config-page"> <div class="admin-permalinks admin-config-page">
<AdminPageHeader <DPageHeader
@titleLabel="admin.permalink.title" @titleLabel={{i18n "admin.permalink.title"}}
@descriptionLabel="admin.permalink.description" @descriptionLabel={{i18n "admin.permalink.description"}}
@learnMoreUrl="https://meta.discourse.org/t/redirect-old-forum-urls-to-new-discourse-urls/20930" @learnMoreUrl="https://meta.discourse.org/t/redirect-old-forum-urls-to-new-discourse-urls/20930"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/customize/permalinks" @path="/admin/customize/permalinks"
@label={{i18n "admin.permalink.title"}} @label={{i18n "admin.permalink.title"}}
@ -19,7 +20,7 @@
class="admin-permalinks__header-add-permalink" class="admin-permalinks__header-add-permalink"
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
<ConditionalLoadingSpinner @condition={{this.loading}}> <ConditionalLoadingSpinner @condition={{this.loading}}>
<div class="permalink-search"> <div class="permalink-search">

View File

@ -1,11 +1,12 @@
<div class="admin-plugins-list-container"> <div class="admin-plugins-list-container">
<AdminPageHeader <DPageHeader
@titleLabel="admin.plugins.installed" @titleLabel={{i18n "admin.plugins.installed"}}
@descriptionLabel="admin.plugins.description" @descriptionLabel={{i18n "admin.plugins.description"}}
@learnMoreUrl="https://www.discourse.org/plugins" @learnMoreUrl="https://www.discourse.org/plugins"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/plugins" @path="/admin/plugins"
@label={{i18n "admin.plugins.title"}} @label={{i18n "admin.plugins.title"}}
@ -32,7 +33,7 @@
{{/if}} {{/if}}
{{/each}} {{/each}}
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="alert alert-info -top-margin admin-plugins-howto"> <div class="alert alert-info -top-margin admin-plugins-howto">
{{dIcon "circle-info"}} {{dIcon "circle-info"}}

View File

@ -1,12 +1,12 @@
{{#if this.showTopNav}} {{#if this.showTopNav}}
<div class="admin-page-header"> <div class="d-page-header">
<DBreadcrumbsContainer /> <DBreadcrumbsContainer />
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} /> <DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/plugins" @path="/admin/plugins"
@label={{i18n "admin.plugins.title"}} @label={{i18n "admin.plugins.title"}}
/> />
<div class="admin-nav-submenu"> <div class="d-nav-submenu">
<HorizontalOverflowNav class="main-nav nav plugin-nav"> <HorizontalOverflowNav class="main-nav nav plugin-nav">
<NavItem @route="adminPlugins.index" @label="admin.plugins.title" /> <NavItem @route="adminPlugins.index" @label="admin.plugins.title" />
{{#each this.adminRoutes as |route|}} {{#each this.adminRoutes as |route|}}

View File

@ -1,14 +1,15 @@
<AdminPageHeader <DPageHeader
@titleLabel="admin.section_landing_pages.account.title" @titleLabel={{i18n "admin.section_landing_pages.account.title"}}
@hideTabs={{true}} @hideTabs={{true}}
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/section/account" @path="/admin/section/account"
@label={{i18n "admin.section_landing_pages.account.title"}} @label={{i18n "admin.section_landing_pages.account.title"}}
/> />
</:breadcrumbs> </:breadcrumbs>
</AdminPageHeader> </DPageHeader>
<AdminSectionLandingWrapper> <AdminSectionLandingWrapper>
<AdminSectionLandingItem <AdminSectionLandingItem

View File

@ -1,11 +1,12 @@
<div class="admin-user_fields admin-config-page"> <div class="admin-user_fields admin-config-page">
<AdminPageHeader <DPageHeader
@titleLabel="admin.user_fields.title" @titleLabel={{i18n "admin.user_fields.title"}}
@descriptionLabel="admin.user_fields.help" @descriptionLabel={{i18n "admin.user_fields.help"}}
@hideTabs={{true}} @hideTabs={{true}}
@learnMoreUrl="https://meta.discourse.org/t/creating-and-configuring-custom-user-fields/113192" @learnMoreUrl="https://meta.discourse.org/t/creating-and-configuring-custom-user-fields/113192"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/customize/user_fields" @path="/admin/customize/user_fields"
@label={{i18n "admin.user_fields.title"}} @label={{i18n "admin.user_fields.title"}}
@ -17,7 +18,7 @@
@label="admin.user_fields.add" @label="admin.user_fields.add"
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
<div class="admin-config-page__main-area"> <div class="admin-config-page__main-area">
<div class="user-fields"> <div class="user-fields">

View File

@ -1,4 +1,4 @@
<AdminPageSubheader @titleLabelTranslated={{this.title}}> <DPageSubheader @titleLabel={{this.title}}>
<:actions as |actions|> <:actions as |actions|>
{{#if this.canCheckEmails}} {{#if this.canCheckEmails}}
{{#if this.showEmails}} {{#if this.showEmails}}
@ -16,7 +16,7 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
</:actions> </:actions>
</AdminPageSubheader> </DPageSubheader>
<PluginOutlet @name="admin-users-list-show-before" /> <PluginOutlet @name="admin-users-list-show-before" />

View File

@ -1,10 +1,11 @@
<div class="admin-users admin-config-page"> <div class="admin-users admin-config-page">
<AdminPageHeader <DPageHeader
@titleLabel="admin.users.title" @titleLabel={{i18n "admin.users.title"}}
@descriptionLabel="admin.users.description" @descriptionLabel={{i18n "admin.users.description"}}
@learnMoreUrl="https://meta.discourse.org/t/accessing-a-user-s-admin-page/311859" @learnMoreUrl="https://meta.discourse.org/t/accessing-a-user-s-admin-page/311859"
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/users/list" @path="/admin/users/list"
@label={{i18n "admin.users.title"}} @label={{i18n "admin.users.title"}}
@ -72,7 +73,7 @@
class="admin-users-tabs__groups" class="admin-users-tabs__groups"
/> />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
</div> </div>
</div> </div>

View File

@ -1,10 +1,11 @@
<AdminPageHeader <DPageHeader
@titleLabel="admin.dashboard.new_features.title" @titleLabel={{i18n "admin.dashboard.new_features.title"}}
@descriptionLabel="admin.dashboard.new_features.subtitle" @descriptionLabel={{i18n "admin.dashboard.new_features.subtitle"}}
@learnMoreUrl="https://meta.discourse.org/tags/c/announcements/67/release-notes" @learnMoreUrl="https://meta.discourse.org/tags/c/announcements/67/release-notes"
@hideTabs={{true}} @hideTabs={{true}}
> >
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/whats-new" @path="/admin/whats-new"
@label={{i18n "admin.dashboard.new_features.title"}} @label={{i18n "admin.dashboard.new_features.title"}}
@ -16,7 +17,7 @@
@action={{this.checkForUpdates}} @action={{this.checkForUpdates}}
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
<div class="admin-container admin-config-page__main-area"> <div class="admin-container admin-config-page__main-area">
<div class="admin-config-area"> <div class="admin-config-area">

View File

@ -0,0 +1,124 @@
import { hash } from "@ember/helper";
import DButton from "discourse/components/d-button";
export const DPageActionButton = <template>
<DButton
class="d-page-action-button btn-small"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</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="d-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>
<DPageActionButton
class="btn-primary"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const DangerButton = <template>
<DPageActionButton
class="btn-danger"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const DefaultButton = <template>
<DPageActionButton
class="btn-default"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@isLoading={{@isLoading}}
/>
</template>;
export const DPageActionListItem = <template>
<li class="dropdown-menu__item d-page-action-list-item">
<DPageActionButton
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 d-page-action-list-item d-page-action-wrapped-list-item"
>
{{yield (hash buttonClass="btn-transparent")}}
</li>
</template>;
// It is not a mistake that there is no PrimaryActionListItem here, in a list
// there is no need for blue text.
export const DefaultActionListItem = <template>
<DPageActionListItem
class="btn-default"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</template>;
export const DangerActionListItem = <template>
<DPageActionListItem
class="btn-danger"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</template>;

View File

@ -0,0 +1,145 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import DBreadcrumbsContainer from "discourse/components/d-breadcrumbs-container";
import {
DangerActionListItem,
DangerButton,
DefaultActionListItem,
DefaultButton,
PrimaryButton,
WrappedActionListItem,
WrappedButton,
} from "discourse/components/d-page-action-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import HorizontalOverflowNav from "discourse/components/horizontal-overflow-nav";
import { bind } from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
const HEADLESS_ACTIONS = ["new", "edit"];
export default class DPageHeader extends Component {
@service site;
@service router;
@tracked shouldDisplay = true;
constructor() {
super(...arguments);
this.router.on("routeDidChange", this, this.#checkIfShouldDisplay);
this.#checkIfShouldDisplay();
}
willDestroy() {
super.willDestroy(...arguments);
this.router.off("routeDidChange", this, this.#checkIfShouldDisplay);
}
@bind
#checkIfShouldDisplay() {
if (this.args.shouldDisplay !== undefined) {
return (this.shouldDisplay = this.args.shouldDisplay);
}
const currentPath = this.router._router.currentPath;
if (!currentPath) {
return (this.shouldDisplay = true);
}
// NOTE: This has a little admin-specific logic in it, in future
// we could extract this out and have it a bit more generic,
// for now I think it's a fine tradeoff.
const pathSegments = currentPath.split(".");
this.shouldDisplay =
!pathSegments.includes("admin") ||
!HEADLESS_ACTIONS.find((segment) => pathSegments.includes(segment));
}
<template>
{{#if this.shouldDisplay}}
<div class="d-page-header">
<div class="d-page-header__breadcrumbs">
<DBreadcrumbsContainer />
{{yield to="breadcrumbs"}}
</div>
<div class="d-page-header__title-row">
{{#if @titleLabel}}
<h1 class="d-page-header__title">{{@titleLabel}}</h1>
{{/if}}
{{#if (or (has-block "actions") @headerActionComponent)}}
<div class="d-page-header__actions">
{{#if this.site.mobileView}}
<DMenu
@identifier="d-page-header-mobile-actions"
@title={{i18n "more_options"}}
@icon="ellipsis-vertical"
class="btn-small"
>
<:content>
<DropdownMenu class="d-page-header__mobile-actions">
{{#let
(hash
Primary=DefaultActionListItem
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>
{{#if @descriptionLabel}}
<p class="d-page-header__description">
{{htmlSafe @descriptionLabel}}
{{#if @learnMoreUrl}}
<span class="d-page-header__learn-more">{{htmlSafe
(i18n "learn_more_with_link" url=@learnMoreUrl)
}}</span>
{{/if}}
</p>
{{/if}}
{{#unless @hideTabs}}
<div class="d-nav-submenu">
<HorizontalOverflowNav class="d-nav-submenu__tabs">
{{yield to="tabs"}}
</HorizontalOverflowNav>
</div>
{{/unless}}
</div>
{{/if}}
</template>
}

View File

@ -0,0 +1,76 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import {
DangerActionListItem,
DangerButton,
DefaultActionListItem,
DefaultButton,
PrimaryButton,
WrappedActionListItem,
WrappedButton,
} from "discourse/components/d-page-action-button";
import DropdownMenu from "discourse/components/dropdown-menu";
import { i18n } from "discourse-i18n";
import DMenu from "float-kit/components/d-menu";
export default class DPageSubheader extends Component {
@service site;
<template>
<div class="d-page-subheader">
<div class="d-page-subheader__title-row">
<h2 class="d-page-subheader__title">{{@titleLabel}}</h2>
{{#if (has-block "actions")}}
<div class="d-page-subheader__actions">
{{#if this.site.mobileView}}
<DMenu
@identifier="d-page-subheader-mobile-actions"
@title={{i18n "more_options"}}
@icon="ellipsis-vertical"
class="btn-small"
>
<:content>
<DropdownMenu class="d-page-subheader__mobile-actions">
{{yield
(hash
Primary=DefaultActionListItem
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>
{{#if @descriptionLabel}}
<p class="d-page-subheader__description">
{{htmlSafe @descriptionLabel}}
{{#if @learnMoreUrl}}
<span class="d-page-subheader__learn-more">
{{htmlSafe
(i18n "learn_more_with_link" url=@learnMoreUrl)
}}</span>
{{/if}}
</p>
{{/if}}
</div>
</template>
}

View File

@ -3337,7 +3337,7 @@ class PluginApi {
} }
/** /**
* Registers a component class that will be rendered within the AdminPageHeader component * Registers a component class that will be rendered within the DPageHeader component
* only on plugins using the AdminPluginConfigPage and the new plugin "show" route. * only on plugins using the AdminPluginConfigPage and the new plugin "show" route.
* *
* This component will be passed an `@actions` argument, with Primary, Default, Danger, * This component will be passed an `@actions` argument, with Primary, Default, Danger,

View File

@ -71,28 +71,28 @@ acceptance("Admin - Users List", function (needs) {
await visit("/admin/users/list/active"); await visit("/admin/users/list/active");
assert.dom(".admin-page-subheader__title").hasText(activeTitle); assert.dom(".d-page-subheader__title").hasText(activeTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser); .includesText(activeUser);
await click('a[href="/admin/users/list/new"]'); await click('a[href="/admin/users/list/new"]');
assert.dom(".admin-page-subheader__title").hasText(suspectTitle); assert.dom(".d-page-subheader__title").hasText(suspectTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser); .includesText(suspectUser);
await click(".users-list .sortable:nth-child(4)"); await click(".users-list .sortable:nth-child(4)");
assert.dom(".admin-page-subheader__title").hasText(suspectTitle); assert.dom(".d-page-subheader__title").hasText(suspectTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser); .includesText(suspectUser);
await click('a[href="/admin/users/list/active"]'); await click('a[href="/admin/users/list/active"]');
assert.dom(".admin-page-subheader__title").hasText(activeTitle); assert.dom(".d-page-subheader__title").hasText(activeTitle);
assert assert
.dom(".users-list .user:nth-child(1) .username") .dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser); .includesText(activeUser);

View File

@ -1,177 +0,0 @@
import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { forceMobile } from "discourse/lib/mobile";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { i18n } from "discourse-i18n";
import AdminPageSubheader from "admin/components/admin-page-subheader";
module("Integration | Component | AdminPageSubheader", function (hooks) {
setupRenderingTest(hooks);
test("@titleLabel", async function (assert) {
await render(<template>
<AdminPageSubheader @titleLabel="admin.title" />
</template>);
assert
.dom(".admin-page-subheader__title")
.exists()
.hasText(i18n("admin.title"));
});
test("@titleLabelTranslated", async function (assert) {
await render(<template>
<AdminPageSubheader @titleLabelTranslated="Wow so cool" />
</template>);
assert.dom(".admin-page-subheader__title").exists().hasText("Wow so cool");
});
test("no @descriptionLabel and no @descriptionLabelTranslated", async function (assert) {
await render(<template><AdminPageSubheader /></template>);
assert.dom(".admin-page-subheader__description").doesNotExist();
});
test("@descriptionLabel", async function (assert) {
await render(<template>
<AdminPageSubheader @descriptionLabel="admin.badges.description" />
</template>);
assert
.dom(".admin-page-subheader__description")
.exists()
.hasText(i18n("admin.badges.description"));
});
test("@descriptionLabelTranslated", async function (assert) {
await render(<template>
<AdminPageSubheader
@descriptionLabelTranslated="Some description which supports <strong>HTML</strong>"
/>
</template>);
assert
.dom(".admin-page-subheader__description")
.exists()
.hasText("Some description which supports HTML");
assert.dom(".admin-page-subheader__description strong").exists();
});
test("no @learnMoreUrl", async function (assert) {
await render(<template><AdminPageSubheader /></template>);
assert.dom(".admin-page-subheader__learn-more").doesNotExist();
});
test("@learnMoreUrl", async function (assert) {
await render(<template>
<AdminPageSubheader
@descriptionLabel="admin.badges.description"
@learnMoreUrl="https://meta.discourse.org/t/96331"
/>
</template>);
assert.dom(".admin-page-subheader__learn-more").exists();
assert
.dom(".admin-page-subheader__learn-more a")
.hasText("Learn more…")
.hasAttribute("href", "https://meta.discourse.org/t/96331");
});
test("renders all types of action buttons in yielded <:actions>", async function (assert) {
let actionCalled = false;
const someAction = () => {
actionCalled = true;
};
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.Danger
@action={{someAction}}
@title="admin.badges.group_settings"
@label="admin.badges.group_settings"
@icon="gear"
class="edit-groupings-btn"
/>
</:actions>
</AdminPageSubheader>
</template>);
assert
.dom(
".admin-page-subheader__actions .admin-page-action-button.new-badge.btn.btn-small.btn-primary"
)
.exists();
assert
.dom(
".admin-page-subheader__actions .admin-page-action-button.award-badge.btn.btn-small.btn-default"
)
.exists();
assert
.dom(
".admin-page-subheader__actions .admin-page-action-button.edit-groupings-btn.btn.btn-small.btn-danger"
)
.exists();
await click(".edit-groupings-btn");
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

@ -1,14 +1,14 @@
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 DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item"; import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageHeader from "discourse/components/d-page-header";
import NavItem from "discourse/components/nav-item"; import NavItem from "discourse/components/nav-item";
import { forceMobile } from "discourse/lib/mobile"; 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-i18n"; import { i18n } from "discourse-i18n";
import AdminPageHeader from "admin/components/admin-page-header";
const AdminPageHeaderActionsTestComponent = <template> const DPageHeaderActionsTestComponent = <template>
<div class="admin-page-header-actions-test-component"> <div class="d-page-header-actions-test-component">
<@actions.Default <@actions.Default
@route="adminBadges.award" @route="adminBadges.award"
@routeModels="new" @routeModels="new"
@ -19,125 +19,96 @@ const AdminPageHeaderActionsTestComponent = <template>
</div> </div>
</template>; </template>;
module("Integration | Component | AdminPageHeader", function (hooks) { module("Integration | Component | DPageHeader", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("no @titleLabel or @titleLabelTranslated", async function (assert) { test("no @titleLabel", async function (assert) {
await render(<template><AdminPageHeader /></template>); await render(<template><DPageHeader /></template>);
assert.dom(".admin-page-header__title").doesNotExist(); assert.dom(".d-page-header__title").doesNotExist();
}); });
test("@titleLabel", async function (assert) { test("@titleLabel", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader @titleLabel="admin.title" /> <DPageHeader @titleLabel={{i18n "admin.title"}} />
</template>); </template>);
assert assert.dom(".d-page-header__title").exists().hasText(i18n("admin.title"));
.dom(".admin-page-header__title")
.exists()
.hasText(i18n("admin.title"));
});
test("@titleLabelTranslated", async function (assert) {
await render(<template>
<AdminPageHeader @titleLabelTranslated="Wow so cool" />
</template>);
assert.dom(".admin-page-header__title").exists().hasText("Wow so cool");
}); });
test("@shouldDisplay", async function (assert) { test("@shouldDisplay", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader <DPageHeader @titleLabel="Wow so cool" @shouldDisplay={{false}} />
@titleLabelTranslated="Wow so cool"
@shouldDisplay={{false}}
/>
</template>); </template>);
assert.dom(".admin-page-header").doesNotExist(); assert.dom(".admin-page-header").doesNotExist();
}); });
test("renders base breadcrumbs and yielded <:breadcrumbs>", async function (assert) { test("renders base breadcrumbs and yielded <:breadcrumbs>", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader @titleLabel="admin.titile"> <DPageHeader @titleLabel={{i18n "admin.titile"}}>
<:breadcrumbs> <:breadcrumbs>
<DBreadcrumbsItem <DBreadcrumbsItem
@path="/admin/badges" @path="/admin/badges"
@label={{i18n "admin.badges.title"}} @label={{i18n "admin.badges.title"}}
/> />
</:breadcrumbs> </:breadcrumbs>
</AdminPageHeader> </DPageHeader>
</template>); </template>);
assert assert
.dom(".admin-page-header__breadcrumbs .d-breadcrumbs__item") .dom(".d-page-header__breadcrumbs .d-breadcrumbs__item")
.exists({ count: 2 }); .exists({ count: 1 });
assert assert
.dom(".admin-page-header__breadcrumbs .d-breadcrumbs__item") .dom(".d-page-header__breadcrumbs .d-breadcrumbs__item:last-child")
.hasText(i18n("admin_title"));
assert
.dom(".admin-page-header__breadcrumbs .d-breadcrumbs__item:last-child")
.hasText(i18n("admin.badges.title")); .hasText(i18n("admin.badges.title"));
}); });
test("no @descriptionLabel and no @descriptionLabelTranslated", async function (assert) { test("no @descriptionLabel", async function (assert) {
await render(<template><AdminPageHeader /></template>); await render(<template><DPageHeader /></template>);
assert.dom(".admin-page-header__description").doesNotExist(); assert.dom(".d-page-header__description").doesNotExist();
}); });
test("@descriptionLabel", async function (assert) { test("@descriptionLabel", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader @descriptionLabel="admin.badges.description" /> <DPageHeader @descriptionLabel={{i18n "admin.badges.description"}} />
</template>); </template>);
assert assert
.dom(".admin-page-header__description") .dom(".d-page-header__description")
.exists() .exists()
.hasText(i18n("admin.badges.description")); .hasText(i18n("admin.badges.description"));
}); });
test("@descriptionLabelTranslated", async function (assert) {
await render(<template>
<AdminPageHeader
@descriptionLabelTranslated="Some description which supports <strong>HTML</strong>"
/>
</template>);
assert
.dom(".admin-page-header__description")
.exists()
.hasText("Some description which supports HTML");
assert.dom(".admin-page-header__description strong").exists();
});
test("no @learnMoreUrl", async function (assert) { test("no @learnMoreUrl", async function (assert) {
await render(<template><AdminPageHeader /></template>); await render(<template><DPageHeader /></template>);
assert.dom(".admin-page-header__learn-more").doesNotExist(); assert.dom(".d-page-header__learn-more").doesNotExist();
}); });
test("@learnMoreUrl", async function (assert) { test("@learnMoreUrl", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader <DPageHeader
@descriptionLabel="admin.badges.description" @descriptionLabel={{i18n "admin.badges.description"}}
@learnMoreUrl="https://meta.discourse.org/t/96331" @learnMoreUrl="https://meta.discourse.org/t/96331"
/> />
</template>); </template>);
assert.dom(".admin-page-header__learn-more").exists(); assert.dom(".d-page-header__learn-more").exists();
assert assert
.dom(".admin-page-header__learn-more a") .dom(".d-page-header__learn-more a")
.hasText("Learn more…") .hasText("Learn more…")
.hasAttribute("href", "https://meta.discourse.org/t/96331"); .hasAttribute("href", "https://meta.discourse.org/t/96331");
}); });
test("renders nav tabs in yielded <:tabs>", async function (assert) { test("renders nav tabs in yielded <:tabs>", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader> <DPageHeader>
<:tabs> <:tabs>
<NavItem <NavItem
@route="admin.backups.settings" @route="admin.backups.settings"
@label="settings" @label="settings"
class="admin-backups-tabs__settings" class="d-backups-tabs__settings"
/> />
</:tabs> </:tabs>
</AdminPageHeader> </DPageHeader>
</template>); </template>);
assert assert
.dom(".admin-nav-submenu__tabs .admin-backups-tabs__settings") .dom(".d-nav-submenu__tabs .d-backups-tabs__settings")
.exists() .exists()
.hasText(i18n("settings")); .hasText(i18n("settings"));
}); });
@ -149,7 +120,7 @@ module("Integration | Component | AdminPageHeader", function (hooks) {
}; };
await render(<template> await render(<template>
<AdminPageHeader> <DPageHeader>
<:actions as |actions|> <:actions as |actions|>
<actions.Primary <actions.Primary
@route="adminBadges.show" @route="adminBadges.show"
@ -175,22 +146,22 @@ module("Integration | Component | AdminPageHeader", function (hooks) {
class="edit-groupings-btn" class="edit-groupings-btn"
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
</template>); </template>);
assert assert
.dom( .dom(
".admin-page-header__actions .admin-page-action-button.new-badge.btn.btn-small.btn-primary" ".d-page-header__actions .d-page-action-button.new-badge.btn.btn-small.btn-primary"
) )
.exists(); .exists();
assert assert
.dom( .dom(
".admin-page-header__actions .admin-page-action-button.award-badge.btn.btn-small.btn-default" ".d-page-header__actions .d-page-action-button.award-badge.btn.btn-small.btn-default"
) )
.exists(); .exists();
assert assert
.dom( .dom(
".admin-page-header__actions .admin-page-action-button.edit-groupings-btn.btn.btn-small.btn-danger" ".d-page-header__actions .d-page-action-button.edit-groupings-btn.btn.btn-small.btn-danger"
) )
.exists(); .exists();
@ -200,18 +171,14 @@ module("Integration | Component | AdminPageHeader", function (hooks) {
test("@headerActionComponent is rendered with actions arg", async function (assert) { test("@headerActionComponent is rendered with actions arg", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader <DPageHeader @headerActionComponent={{DPageHeaderActionsTestComponent}} />
@headerActionComponent={{AdminPageHeaderActionsTestComponent}}
/>
</template>); </template>);
assert assert.dom(".d-page-header-actions-test-component .award-badge").exists();
.dom(".admin-page-header-actions-test-component .award-badge")
.exists();
}); });
}); });
module("Integration | Component | AdminPageHeader | Mobile", function (hooks) { module("Integration | Component | DPageHeader | Mobile", function (hooks) {
hooks.beforeEach(function () { hooks.beforeEach(function () {
forceMobile(); forceMobile();
}); });
@ -220,7 +187,7 @@ module("Integration | Component | AdminPageHeader | Mobile", function (hooks) {
test("action buttons become a dropdown on mobile", async function (assert) { test("action buttons become a dropdown on mobile", async function (assert) {
await render(<template> await render(<template>
<AdminPageHeader> <DPageHeader>
<:actions as |actions|> <:actions as |actions|>
<actions.Primary <actions.Primary
@route="adminBadges.show" @route="adminBadges.show"
@ -238,19 +205,19 @@ module("Integration | Component | AdminPageHeader | Mobile", function (hooks) {
class="award-badge" class="award-badge"
/> />
</:actions> </:actions>
</AdminPageHeader> </DPageHeader>
</template>); </template>);
assert assert
.dom( .dom(
".admin-page-header__actions .fk-d-menu__trigger.admin-page-header-mobile-actions-trigger" ".d-page-header__actions .fk-d-menu__trigger.d-page-header-mobile-actions-trigger"
) )
.exists(); .exists();
await click(".admin-page-header-mobile-actions-trigger"); await click(".d-page-header-mobile-actions-trigger");
assert assert
.dom(".dropdown-menu.admin-page-header__mobile-actions .new-badge") .dom(".dropdown-menu.d-page-header__mobile-actions .new-badge")
.exists(); .exists();
}); });
}); });

View File

@ -0,0 +1,154 @@
import { click, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import DPageSubheader from "discourse/components/d-page-subheader";
import { forceMobile } from "discourse/lib/mobile";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { i18n } from "discourse-i18n";
module("Integration | Component | DPageSubheader", function (hooks) {
setupRenderingTest(hooks);
test("@titleLabel", async function (assert) {
await render(<template>
<DPageSubheader @titleLabel={{i18n "admin.title"}} />
</template>);
assert
.dom(".d-page-subheader__title")
.exists()
.hasText(i18n("admin.title"));
});
test("no @descriptionLabel", async function (assert) {
await render(<template><DPageSubheader /></template>);
assert.dom(".d-page-subheader__description").doesNotExist();
});
test("@descriptionLabel", async function (assert) {
await render(<template>
<DPageSubheader @descriptionLabel={{i18n "admin.badges.description"}} />
</template>);
assert
.dom(".d-page-subheader__description")
.exists()
.hasText(i18n("admin.badges.description"));
});
test("no @learnMoreUrl", async function (assert) {
await render(<template><DPageSubheader /></template>);
assert.dom(".d-page-subheader__learn-more").doesNotExist();
});
test("@learnMoreUrl", async function (assert) {
await render(<template>
<DPageSubheader
@descriptionLabel={{i18n "admin.badges.description"}}
@learnMoreUrl="https://meta.discourse.org/t/96331"
/>
</template>);
assert.dom(".d-page-subheader__learn-more").exists();
assert
.dom(".d-page-subheader__learn-more a")
.hasText("Learn more…")
.hasAttribute("href", "https://meta.discourse.org/t/96331");
});
test("renders all types of action buttons in yielded <:actions>", async function (assert) {
let actionCalled = false;
const someAction = () => {
actionCalled = true;
};
await render(<template>
<DPageSubheader>
<: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.Danger
@action={{someAction}}
@title="admin.badges.group_settings"
@label="admin.badges.group_settings"
@icon="gear"
class="edit-groupings-btn"
/>
</:actions>
</DPageSubheader>
</template>);
assert
.dom(
".d-page-subheader__actions .d-page-action-button.new-badge.btn.btn-small.btn-primary"
)
.exists();
assert
.dom(
".d-page-subheader__actions .d-page-action-button.award-badge.btn.btn-small.btn-default"
)
.exists();
assert
.dom(
".d-page-subheader__actions .d-page-action-button.edit-groupings-btn.btn.btn-small.btn-danger"
)
.exists();
await click(".edit-groupings-btn");
assert.true(actionCalled);
});
});
module("Integration | Component | DPageSubheader | Mobile", function (hooks) {
hooks.beforeEach(function () {
forceMobile();
});
setupRenderingTest(hooks);
test("action buttons become a dropdown on mobile", async function (assert) {
await render(<template>
<DPageSubheader>
<: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>
</DPageSubheader>
</template>);
assert
.dom(
".d-page-subheader .fk-d-menu__trigger.d-page-subheader-mobile-actions-trigger"
)
.exists();
await click(".d-page-subheader-mobile-actions-trigger");
assert
.dom(".dropdown-menu.d-page-subheader__mobile-actions .new-badge")
.exists();
});
});

View File

@ -124,7 +124,7 @@
float: left; float: left;
max-width: 70%; max-width: 70%;
&.admin-page-action-button { &.d-page-action-button {
margin-top: 0; margin-top: 0;
@media (max-width: $mobile-breakpoint) { @media (max-width: $mobile-breakpoint) {

View File

@ -806,6 +806,7 @@
.admin-permalinks { .admin-permalinks {
@include breakpoint(tablet) { @include breakpoint(tablet) {
.admin-page-subheader, .admin-page-subheader,
.d-page-subheader,
.admin-config-area, .admin-config-area,
.admin-config-area__primary-content, .admin-config-area__primary-content,
.loading-container { .loading-container {

View File

@ -75,7 +75,7 @@
margin-bottom: var(--space-6); margin-bottom: var(--space-6);
} }
.admin-nav-submenu { .d-nav-submenu {
background: transparent; background: transparent;
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);

View File

@ -1,6 +1,7 @@
@import "badges"; @import "badges";
@import "banner"; @import "banner";
@import "d-breadcrumbs"; @import "d-breadcrumbs";
@import "d-page-header";
@import "d-stat-tiles"; @import "d-stat-tiles";
@import "bookmark-list"; @import "bookmark-list";
@import "bookmark-modal"; @import "bookmark-modal";

View File

@ -0,0 +1,92 @@
$mobile-breakpoint: 700px;
.d-page-header,
.d-page-subheader {
&__title-row {
display: flex;
justify-content: space-between;
align-items: stretch;
margin-bottom: var(--space-2);
h1,
h2 {
margin: 0;
}
h2 {
font-size: var(--font-up-2);
}
.d-page-header__actions {
display: flex;
align-items: center;
justify-content: flex-end;
@media (max-width: $mobile-breakpoint) {
flex-direction: column;
}
button {
margin-left: var(--space-2);
@media (max-width: $mobile-breakpoint) {
width: 100%;
margin-bottom: var(--space-2);
margin-left: 0;
}
}
}
}
.d-nav-submenu {
background: transparent;
border-bottom: 1px solid var(--primary-low);
.horizontal-overflow-nav {
background: transparent;
&:before {
display: none;
}
&:after {
display: none;
}
}
.nav-pills {
width: auto;
margin: 0;
}
}
}
.d-page-header {
&__title-row {
@media (max-width: $mobile-breakpoint) {
flex-direction: row;
align-items: center;
.d-page-header__actions {
button {
margin-bottom: 0;
}
}
}
}
}
.d-page-subheader {
&__title-row {
@media (max-width: $mobile-breakpoint) {
flex-direction: row;
align-items: center;
}
}
}
.d-page-action-list-item {
.btn-primary {
color: var(--primary);
}
}

View File

@ -4,9 +4,9 @@
/> />
<div class="discourse-chat-incoming-webhooks admin-detail"> <div class="discourse-chat-incoming-webhooks admin-detail">
<AdminPageSubheader <DPageSubheader
@titleLabel="chat.incoming_webhooks.title" @titleLabel={{i18n "chat.incoming_webhooks.title"}}
@descriptionLabel="chat.incoming_webhooks.instructions" @descriptionLabel={{i18n "chat.incoming_webhooks.instructions"}}
> >
<:actions as |actions|> <:actions as |actions|>
<actions.Primary <actions.Primary
@ -18,7 +18,7 @@
class="admin-incoming-webhooks-new" class="admin-incoming-webhooks-new"
/> />
</:actions> </:actions>
</AdminPageSubheader> </DPageSubheader>
<div class="incoming-chat-webhooks"> <div class="incoming-chat-webhooks">
{{#if this.model.incoming_chat_webhooks}} {{#if this.model.incoming_chat_webhooks}}

View File

@ -6,7 +6,7 @@ describe "Admin Chat Incoming Webhooks", type: :system do
let(:dialog) { PageObjects::Components::Dialog.new } let(:dialog) { PageObjects::Components::Dialog.new }
let(:admin_incoming_webhooks_page) { PageObjects::Pages::AdminIncomingWebhooks.new } let(:admin_incoming_webhooks_page) { PageObjects::Pages::AdminIncomingWebhooks.new }
let(:admin_header) { PageObjects::Components::AdminHeader.new } let(:d_page_header) { PageObjects::Components::DPageHeader.new }
before do before do
chat_system_bootstrap(current_user) chat_system_bootstrap(current_user)
@ -16,11 +16,11 @@ describe "Admin Chat Incoming Webhooks", type: :system do
it "can create incoming webhooks" do it "can create incoming webhooks" do
admin_incoming_webhooks_page.visit admin_incoming_webhooks_page.visit
expect(admin_header).to be_visible expect(d_page_header).to be_visible
admin_incoming_webhooks_page.click_new admin_incoming_webhooks_page.click_new
expect(admin_header).to be_hidden expect(d_page_header).to be_hidden
admin_incoming_webhooks_page.form.field("name").fill_in("Test webhook") admin_incoming_webhooks_page.form.field("name").fill_in("Test webhook")
admin_incoming_webhooks_page.form.field("description").fill_in("Some test content") admin_incoming_webhooks_page.form.field("description").fill_in("Some test content")

View File

@ -8,7 +8,7 @@ describe "Admin Flags Page", type: :system do
let(:admin_flags_page) { PageObjects::Pages::AdminFlags.new } let(:admin_flags_page) { PageObjects::Pages::AdminFlags.new }
let(:admin_flag_form_page) { PageObjects::Pages::AdminFlagForm.new } let(:admin_flag_form_page) { PageObjects::Pages::AdminFlagForm.new }
let(:flag_modal) { PageObjects::Modals::Flag.new } let(:flag_modal) { PageObjects::Modals::Flag.new }
let(:admin_header) { PageObjects::Components::AdminHeader.new } let(:d_page_header) { PageObjects::Components::DPageHeader.new }
before do before do
sign_in(admin) sign_in(admin)
@ -27,7 +27,7 @@ describe "Admin Flags Page", type: :system do
) )
admin_flags_page.visit admin_flags_page.visit
expect(admin_header).to be_visible expect(d_page_header).to be_visible
admin_flags_page.toggle("spam") admin_flags_page.toggle("spam")
topic_page.visit_topic(post.topic).open_flag_topic_modal topic_page.visit_topic(post.topic).open_flag_topic_modal
@ -81,8 +81,7 @@ describe "Admin Flags Page", type: :system do
expect(admin_flags_page).to have_add_flag_button_enabled expect(admin_flags_page).to have_add_flag_button_enabled
admin_flags_page.click_add_flag admin_flags_page.click_add_flag
expect(d_page_header).to be_hidden
expect(admin_header).to be_hidden
admin_flag_form_page admin_flag_form_page
.fill_in_name("Vulgar") .fill_in_name("Vulgar")
@ -115,7 +114,7 @@ describe "Admin Flags Page", type: :system do
# update # update
admin_flags_page.visit.click_edit_flag("custom_vulgar") admin_flags_page.visit.click_edit_flag("custom_vulgar")
expect(admin_header).to be_hidden expect(d_page_header).to be_hidden
admin_flag_form_page.fill_in_name("Tasteless").click_save admin_flag_form_page.fill_in_name("Tasteless").click_save
expect(admin_flags_page).to have_flags( expect(admin_flags_page).to have_flags(
@ -158,7 +157,7 @@ describe "Admin Flags Page", type: :system do
it "has settings tab" do it "has settings tab" do
admin_flags_page.visit admin_flags_page.visit
expect(admin_header).to have_tabs( expect(d_page_header).to have_tabs(
[I18n.t("admin_js.settings"), I18n.t("admin_js.admin.config_areas.flags.flags_tab")], [I18n.t("admin_js.settings"), I18n.t("admin_js.admin.config_areas.flags.flags_tab")],
) )

View File

@ -6,11 +6,11 @@ describe "Admin User Fields", type: :system do
before { sign_in(current_user) } before { sign_in(current_user) }
let(:user_fields_page) { PageObjects::Pages::AdminUserFields.new } let(:user_fields_page) { PageObjects::Pages::AdminUserFields.new }
let(:admin_header) { PageObjects::Components::AdminHeader.new } let(:page_header) { PageObjects::Components::DPageHeader.new }
it "correctly saves user fields" do it "correctly saves user fields" do
user_fields_page.visit user_fields_page.visit
expect(admin_header).to be_visible expect(page_header).to be_visible
user_fields_page.add_field(name: "Occupation", description: "What you do for work") user_fields_page.add_field(name: "Occupation", description: "What you do for work")
expect(user_fields_page).to have_user_field("Occupation") expect(user_fields_page).to have_user_field("Occupation")
@ -32,7 +32,7 @@ describe "Admin User Fields", type: :system do
user_fields_page.visit user_fields_page.visit
user_fields_page.click_add_field user_fields_page.click_add_field
expect(admin_header).to be_hidden expect(page_header).to be_hidden
form = page.find(".user-field") form = page.find(".user-field")
editable_label = I18n.t("admin_js.admin.user_fields.editable.title") editable_label = I18n.t("admin_js.admin.user_fields.editable.title")
@ -73,7 +73,7 @@ describe "Admin User Fields", type: :system do
form.find(".user-field-name").fill_in(with: "Favourite Transformer") form.find(".user-field-name").fill_in(with: "Favourite Transformer")
expect(admin_header).to be_hidden expect(page_header).to be_hidden
form.find(".btn-primary").click form.find(".btn-primary").click

View File

@ -2,17 +2,18 @@
module PageObjects module PageObjects
module Components module Components
# TODO (martin) Delete this after plugins have been updated to use DPageHeader
class AdminHeader < PageObjects::Pages::Base class AdminHeader < PageObjects::Pages::Base
def has_tabs?(names) def has_tabs?(names)
expect(page.all(".admin-nav-submenu__tabs a").map(&:text)).to eq(names) expect(page.all(".d-nav-submenu__tabs a").map(&:text)).to eq(names)
end end
def visible? def visible?
has_css?(".admin-page-header") has_css?(".d-page-header")
end end
def hidden? def hidden?
has_no_css?(".admin-page-header") has_no_css?(".d-page-header")
end end
end end
end end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module PageObjects
module Components
class DPageHeader < PageObjects::Pages::Base
def has_tabs?(names)
expect(page.all(".d-nav-submenu__tabs a").map(&:text)).to eq(names)
end
def visible?
has_css?(".d-page-header")
end
def hidden?
has_no_css?(".d-page-header")
end
end
end
end

View File

@ -21,7 +21,7 @@ module PageObjects
end end
def plugin_nav_tab_selector(plugin) def plugin_nav_tab_selector(plugin)
".admin-nav-submenu__tabs .admin-plugin-tab-nav-item[data-plugin-nav-tab-id=\"#{plugin}\"]" ".d-nav-submenu__tabs .admin-plugin-tab-nav-item[data-plugin-nav-tab-id=\"#{plugin}\"]"
end end
end end
end end

View File

@ -19,7 +19,7 @@ module PageObjects
end end
def click_add_field def click_add_field
page.find(".admin-page-header__actions .btn-primary").click page.find(".d-page-header__actions .btn-primary").click
end end
def click_edit def click_edit