Merge branch 'main' into feature/wizard-look-and-feel-improvements

This commit is contained in:
Martin Brennan 2024-12-18 09:19:21 +10:00
commit 8d3c3493f5
91 changed files with 1336 additions and 874 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,8 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import icon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class WebhookStatus extends Component { export default class WebhookStatus extends Component {
iconNames = ["far-circle", "circle-xmark", "circle", "circle"]; statusClasses = ["--inactive", "--critical", "--success", "--inactive"];
iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"];
get status() { get status() {
const lastStatus = this.args.webhook.get("last_delivery_status"); const lastStatus = this.args.webhook.get("last_delivery_status");
@ -15,16 +13,17 @@ export default class WebhookStatus extends Component {
return i18n(`admin.web_hooks.delivery_status.${this.status.name}`); return i18n(`admin.web_hooks.delivery_status.${this.status.name}`);
} }
get iconName() { get statusClass() {
return this.iconNames[this.status.id - 1]; return this.statusClasses[this.status.id - 1];
}
get iconClass() {
return this.iconClasses[this.status.id - 1];
} }
<template> <template>
{{icon this.iconName class=this.iconClass}} <div role="status" class="status-label {{this.statusClass}}">
{{this.deliveryStatus}} <div class="status-label-indicator">
</div>
<div class="status-label-text">
{{this.deliveryStatus}}
</div>
</div>
</template> </template>
} }

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

@ -14,19 +14,34 @@
{{#if this.model}} {{#if this.model}}
<LoadMore @selector=".web-hooks tr" @action={{this.loadMore}}> <LoadMore @selector=".web-hooks tr" @action={{this.loadMore}}>
<table class="web-hooks grid"> <table class="d-admin-table web-hooks">
<thead> <thead>
<tr> <tr>
<th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
<th>{{i18n "admin.web_hooks.payload_url"}}</th> <th>{{i18n "admin.web_hooks.payload_url"}}</th>
<th>{{i18n "admin.web_hooks.description_label"}}</th> <th>{{i18n "admin.web_hooks.description_label"}}</th>
<th>{{i18n "admin.web_hooks.controls"}}</th> <th>{{i18n "admin.web_hooks.delivery_status.title"}}</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each this.model as |webhook|}} {{#each this.model as |webhook|}}
<tr> <tr class="d-admin-row__content">
<td class="delivery-status"> <td class="d-admin-row__overview payload-url">
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}>
{{webhook.payload_url}}
</LinkTo>
</td>
<td class="d-admin-row__detail description">
<div class="d-admin-row__mobile-label">
{{i18n "admin.web_hooks.description_label"}}
</div>
{{webhook.description}}
</td>
<td class="d-admin-row__detail delivery-status">
<div class="d-admin-row__mobile-label">
{{i18n "admin.web_hooks.delivery_status.title"}}
</div>
<LinkTo @route="adminWebHooks.show" @model={{webhook}}> <LinkTo @route="adminWebHooks.show" @model={{webhook}}>
<WebhookStatus <WebhookStatus
@deliveryStatuses={{this.deliveryStatuses}} @deliveryStatuses={{this.deliveryStatuses}}
@ -34,28 +49,24 @@
/> />
</LinkTo> </LinkTo>
</td> </td>
<td class="payload-url"> <td class="d-admin-row__controls controls">
<LinkTo @route="adminWebHooks.edit" @model={{webhook}}> <div class="d-admin-row__controls-options">
{{webhook.payload_url}} <LinkTo
</LinkTo> @route="adminWebHooks.edit"
</td> @model={{webhook}}
<td class="description">{{webhook.description}}</td> class="btn btn-default no-text"
<td class="controls"> title={{i18n "admin.web_hooks.edit"}}
<LinkTo >
@route="adminWebHooks.edit" {{d-icon "far-pen-to-square"}}
@model={{webhook}} </LinkTo>
class="btn btn-default no-text"
title={{i18n "admin.web_hooks.edit"}}
>
{{d-icon "far-pen-to-square"}}
</LinkTo>
<DButton <DButton
@action={{fn this.destroyWebhook webhook}} @action={{fn this.destroyWebhook webhook}}
@icon="xmark" @icon="xmark"
@title="delete" @title="delete"
class="destroy btn-danger" class="destroy btn-danger"
/> />
</div>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

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

@ -12,10 +12,7 @@
</:button> </:button>
<:tooltip> <:tooltip>
{{#if @disabled}} {{#if @disabled}}
<DTooltip <DTooltip @icon="circle-info" @content={{i18n this.disallowedReason}} />
@icon="circle-info"
@content={{i18n "topic.create_disabled_category"}}
/>
{{/if}} {{/if}}
</:tooltip> </:tooltip>
</DButtonTooltip> </DButtonTooltip>

View File

@ -5,4 +5,12 @@ import { tagName } from "@ember-decorators/component";
export default class CreateTopicButton extends Component { export default class CreateTopicButton extends Component {
label = "topic.create"; label = "topic.create";
btnClass = "btn-default"; btnClass = "btn-default";
get disallowedReason() {
if (this.canCreateTopicOnTag === false) {
return "topic.create_disabled_tag";
} else if (this.disabled) {
return "topic.create_disabled_category";
}
}
} }

View File

@ -19,6 +19,7 @@ const ACTION_AS_STRING_DEPRECATION_ARGS = [
export default class DButton extends GlimmerComponentWithDeprecatedParentView { export default class DButton extends GlimmerComponentWithDeprecatedParentView {
@service router; @service router;
@service capabilities;
@notEmpty("args.icon") btnIcon; @notEmpty("args.icon") btnIcon;
@ -114,6 +115,7 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
_triggerAction(event) { _triggerAction(event) {
const { action: actionVal, route, routeModels } = this.args; const { action: actionVal, route, routeModels } = this.args;
const isIOS = this.capabilities?.isIOS;
if (actionVal || route) { if (actionVal || route) {
if (actionVal) { if (actionVal) {
@ -129,19 +131,35 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
); );
} }
} else if (typeof actionVal === "object" && actionVal.value) { } else if (typeof actionVal === "object" && actionVal.value) {
// Using `next()` to optimise INP if (isIOS) {
next(() => // Don't optimise INP in iOS
// it results in focus events not being triggered
forwardEvent forwardEvent
? actionVal.value(actionParam, event) ? actionVal.value(actionParam, event)
: actionVal.value(actionParam) : actionVal.value(actionParam);
); } else {
// Using `next()` to optimise INP
next(() =>
forwardEvent
? actionVal.value(actionParam, event)
: actionVal.value(actionParam)
);
}
} else if (typeof actionVal === "function") { } else if (typeof actionVal === "function") {
// Using `next()` to optimise INP if (isIOS) {
next(() => // Don't optimise INP in iOS
// it results in focus events not being triggered
forwardEvent forwardEvent
? actionVal(actionParam, event) ? actionVal(actionParam, event)
: actionVal(actionParam) : actionVal(actionParam);
); } else {
// Using `next()` to optimise INP
next(() =>
forwardEvent
? actionVal(actionParam, event)
: actionVal(actionParam)
);
}
} }
} else if (route) { } else if (route) {
if (routeModels) { if (routeModels) {

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

@ -46,6 +46,10 @@ export default class DSelect extends Component {
return this.args.value && this.args.value !== NO_VALUE_OPTION; return this.args.value && this.args.value !== NO_VALUE_OPTION;
} }
get includeNone() {
return this.args.includeNone ?? true;
}
<template> <template>
<select <select
value={{@value}} value={{@value}}
@ -53,13 +57,15 @@ export default class DSelect extends Component {
class="d-select" class="d-select"
{{on "input" this.handleInput}} {{on "input" this.handleInput}}
> >
<DSelectOption @value={{NO_VALUE_OPTION}}> {{#if this.includeNone}}
{{#if this.hasSelectedValue}} <DSelectOption @value={{NO_VALUE_OPTION}}>
{{i18n "none_placeholder"}} {{#if this.hasSelectedValue}}
{{else}} {{i18n "none_placeholder"}}
{{i18n "select_placeholder"}} {{else}}
{{/if}} {{i18n "select_placeholder"}}
</DSelectOption> {{/if}}
</DSelectOption>
{{/if}}
{{yield (hash Option=(component DSelectOption selected=@value))}} {{yield (hash Option=(component DSelectOption selected=@value))}}
</select> </select>

View File

@ -48,6 +48,13 @@
<label class="alt-placeholder" for="login-account-password"> <label class="alt-placeholder" for="login-account-password">
{{i18n "login.password"}} {{i18n "login.password"}}
</label> </label>
{{#if @loginPassword}}
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
tabindex="3"
/>
{{/if}}
<div class="login__password-links"> <div class="login__password-links">
<a <a
href href
@ -57,13 +64,6 @@
> >
{{i18n "forgot_password.action"}} {{i18n "forgot_password.action"}}
</a> </a>
{{#if @loginPassword}}
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
tabindex="3"
/>
{{/if}}
</div> </div>
<div class="caps-lock-warning {{unless this.capsLockOn 'hidden'}}"> <div class="caps-lock-warning {{unless this.capsLockOn 'hidden'}}">
{{d-icon "triangle-exclamation"}} {{d-icon "triangle-exclamation"}}

View File

@ -138,7 +138,10 @@
<label class="alt-placeholder" for="new-account-password"> <label class="alt-placeholder" for="new-account-password">
{{i18n "user.password.title"}} {{i18n "user.password.title"}}
</label> </label>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
/>
<div class="create-account__password-info"> <div class="create-account__password-info">
<div class="create-account__password-tip-validation"> <div class="create-account__password-tip-validation">
{{#if this.showPasswordValidation}} {{#if this.showPasswordValidation}}
@ -163,10 +166,6 @@
{{i18n "login.caps_lock_warning"}} {{i18n "login.caps_lock_warning"}}
</div> </div>
</div> </div>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
/>
</div> </div>
{{/if}} {{/if}}

View File

@ -3,10 +3,7 @@ import { tracked } from "@glimmer/tracking";
import { concat } from "@ember/helper"; import { concat } from "@ember/helper";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { eq } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import dIcon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class SignupProgressBar extends Component { export default class SignupProgressBar extends Component {
@service siteSettings; @service siteSettings;
@ -22,10 +19,6 @@ export default class SignupProgressBar extends Component {
} }
} }
stepText(step) {
return i18n(`create_account.progress_bar.${step}`);
}
get currentStepIndex() { get currentStepIndex() {
return this.steps.findIndex((step) => step === this.args.step); return this.steps.findIndex((step) => step === this.args.step);
} }
@ -57,19 +50,8 @@ export default class SignupProgressBar extends Component {
> >
<div class="signup-progress-bar__step"> <div class="signup-progress-bar__step">
<div class="signup-progress-bar__circle"> <div class="signup-progress-bar__circle">
{{#if this.site.desktopView}}
{{#if (eq (this.getStepState index) "completed")}}
{{dIcon "check"}}
{{/if}}
{{/if}}
</div> </div>
{{#unless (eq index this.lastStepIndex)}}
<span class="signup-progress-bar__line"></span>
{{/unless}}
</div> </div>
<span class="signup-progress-bar__step-text">
{{this.stepText step}}
</span>
</div> </div>
{{/each}} {{/each}}
</div> </div>

View File

@ -1,10 +1,10 @@
<DButton <DButton
@action={{@togglePasswordMask}} @action={{@togglePasswordMask}}
@label={{if @maskPassword "login.show_password" "login.hide_password"}} @icon={{if @maskPassword "far-eye" "far-eye-slash"}}
@title={{if @title={{if
@maskPassword @maskPassword
"login.show_password_title" "login.show_password_title"
"login.hide_password_title" "login.hide_password_title"
}} }}
class="btn-link toggle-password-mask" class="btn-transparent toggle-password-mask"
/> />

View File

@ -29,6 +29,7 @@ export default class PasswordResetController extends Controller.extend(
requiresApproval = false; requiresApproval = false;
redirected = false; redirected = false;
maskPassword = true; maskPassword = true;
passwordValidationVisible = false;
lockImageUrl = getURL("/images/lock.svg"); lockImageUrl = getURL("/images/lock.svg");
@ -65,6 +66,30 @@ export default class PasswordResetController extends Controller.extend(
return getURL(redirectTo || "/"); return getURL(redirectTo || "/");
} }
@discourseComputed(
"passwordValidation.ok",
"passwordValidation.reason",
"passwordValidationVisible"
)
showPasswordValidation(
passwordValidationOk,
passwordValidationReason,
passwordValidationVisible
) {
return (
passwordValidationOk ||
(passwordValidationReason && passwordValidationVisible)
);
}
@action
togglePasswordValidation() {
this.set(
"passwordValidationVisible",
Boolean(this.passwordValidation.reason)
);
}
@action @action
done(event) { done(event) {
if (wantsNewWindow(event)) { if (wantsNewWindow(event)) {

View File

@ -15,12 +15,17 @@ const SelectOption = <template>
export default class FKControlSelect extends Component { export default class FKControlSelect extends Component {
static controlType = "select"; static controlType = "select";
get includeNone() {
return this.args.field.validation !== "required";
}
<template> <template>
<DSelect <DSelect
class="form-kit__control-select" class="form-kit__control-select"
disabled={{@field.disabled}} disabled={{@field.disabled}}
@value={{@field.value}} @value={{@field.value}}
@onChange={{@field.set}} @onChange={{@field.set}}
@includeNone={{this.includeNone}}
...attributes ...attributes
> >
{{yield (hash Option=(component SelectOption selected=@field.value))}} {{yield (hash Option=(component SelectOption selected=@field.value))}}

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

@ -134,6 +134,11 @@
<label class="alt-placeholder" for="new-account-password"> <label class="alt-placeholder" for="new-account-password">
{{i18n "invites.password_label"}} {{i18n "invites.password_label"}}
</label> </label>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
<div class="create-account__password-info"> <div class="create-account__password-info">
<div class="create-account__password-tip-validation"> <div class="create-account__password-tip-validation">
<InputTip <InputTip
@ -148,11 +153,6 @@
{{i18n "login.caps_lock_warning"}} {{i18n "login.caps_lock_warning"}}
</div> </div>
</div> </div>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
</div> </div>
</div> </div>
{{/unless}} {{/unless}}

View File

@ -2,11 +2,7 @@
{{hide-application-sidebar}} {{hide-application-sidebar}}
{{hide-application-header-buttons "search" "login" "signup" "menu"}} {{hide-application-header-buttons "search" "login" "signup" "menu"}}
<div class="container password-reset clearfix"> <div class="container password-reset clearfix">
<div class="pull-left col-image"> <form class="change-password-form login-left-side">
<img src={{this.lockImageUrl}} class="password-reset-img" alt="" />
</div>
<div class="pull-left col-form">
{{#if this.successMessage}} {{#if this.successMessage}}
<p>{{this.successMessage}}</p> <p>{{this.successMessage}}</p>
@ -22,92 +18,95 @@
{{/unless}} {{/unless}}
{{/if}} {{/if}}
{{else}} {{else}}
<form class="change-password-form"> {{#if this.securityKeyOrSecondFactorRequired}}
{{#if this.securityKeyOrSecondFactorRequired}} <h2>{{i18n "user.change_password.title"}}</h2>
<h2>{{i18n "user.change_password.title"}}</h2> <p>
<p> {{i18n "user.change_password.verify_identity"}}
{{i18n "user.change_password.verify_identity"}} </p>
</p> {{#if this.errorMessage}}
{{#if this.errorMessage}} <div class="alert alert-error">{{this.errorMessage}}</div>
<div class="alert alert-error">{{this.errorMessage}}</div> <br />
<br /> {{/if}}
{{/if}}
{{#if this.displaySecurityKeyForm}} {{#if this.displaySecurityKeyForm}}
<SecurityKeyForm <SecurityKeyForm
@setSecondFactorMethod={{fn @setSecondFactorMethod={{fn (mut this.selectedSecondFactorMethod)}}
(mut this.selectedSecondFactorMethod) @backupEnabled={{this.backupEnabled}}
}} @totpEnabled={{this.secondFactorRequired}}
@backupEnabled={{this.backupEnabled}} @otherMethodAllowed={{this.otherMethodAllowed}}
@totpEnabled={{this.secondFactorRequired}} @action={{this.authenticateSecurityKey}}
@otherMethodAllowed={{this.otherMethodAllowed}} />
@action={{this.authenticateSecurityKey}}
/>
{{else}}
<SecondFactorForm
@secondFactorMethod={{this.selectedSecondFactorMethod}}
@secondFactorToken={{this.secondFactorToken}}
@backupEnabled={{this.backupEnabled}}
@totpEnabled={{this.secondFactorRequired}}
@isLogin={{false}}
>
<SecondFactorInput
{{on
"input"
(with-event-value (fn (mut this.secondFactorToken)))
}}
@secondFactorMethod={{this.selectedSecondFactorMethod}}
value={{this.secondFactorToken}}
id="second-factor"
/>
</SecondFactorForm>
{{/if}}
{{#unless this.displaySecurityKeyForm}}
<DButton
@action={{action "submit"}}
@label="submit"
type="submit"
class="btn-primary"
/>
{{/unless}}
{{else}} {{else}}
<h2>{{i18n "user.change_password.choose"}}</h2> <SecondFactorForm
{{#if this.errorMessage}} @secondFactorMethod={{this.selectedSecondFactorMethod}}
<div class="alert alert-error">{{this.errorMessage}}</div> @secondFactorToken={{this.secondFactorToken}}
<br /> @backupEnabled={{this.backupEnabled}}
{{/if}} @totpEnabled={{this.secondFactorRequired}}
@isLogin={{false}}
<div class="input"> >
<PasswordField <SecondFactorInput
@value={{this.accountPassword}} {{on
@capsLockOn={{this.capsLockOn}} "input"
type={{if this.maskPassword "password" "text"}} (with-event-value (fn (mut this.secondFactorToken)))
autofocus="autofocus" }}
autocomplete="new-password" @secondFactorMethod={{this.selectedSecondFactorMethod}}
id="new-account-password" value={{this.secondFactorToken}}
id="second-factor"
/> />
</SecondFactorForm>
{{/if}}
{{#unless this.displaySecurityKeyForm}}
<DButton
@action={{action "submit"}}
@label="submit"
type="submit"
class="btn-primary"
/>
{{/unless}}
{{else}}
<h2>{{i18n "user.change_password.choose_new"}}</h2>
{{#if this.errorMessage}}
<div class="alert alert-error">{{this.errorMessage}}</div>
<br />
{{/if}}
<div class="input">
<PasswordField
@value={{this.accountPassword}}
{{on "focusout" this.togglePasswordValidation}}
@capsLockOn={{this.capsLockOn}}
type={{if this.maskPassword "password" "text"}}
autofocus="autofocus"
autocomplete="new-password"
id="new-account-password"
/>
<div class="change-password__password-info">
<div class="change-password_tip-validation">
{{#if this.showPasswordValidation}}
<InputTip @validation={{this.passwordValidation}} />
{{/if}}
<div
class="caps-lock-warning {{unless this.capsLockOn 'hidden'}}"
>
{{d-icon "triangle-exclamation"}}
{{i18n "login.caps_lock_warning"}}
</div>
</div>
<TogglePasswordMask <TogglePasswordMask
@maskPassword={{this.maskPassword}} @maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}} @togglePasswordMask={{this.togglePasswordMask}}
/> />
<div class="caps-lock-warning {{unless this.capsLockOn 'hidden'}}">
{{d-icon "triangle-exclamation"}}
{{i18n "login.caps_lock_warning"}}
</div>
</div> </div>
</div>
<InputTip @validation={{this.passwordValidation}} /> <DButton
@action={{action "submit"}}
<DButton @label="user.change_password.set_password"
@action={{action "submit"}} type="submit"
@label="user.change_password.set_password" class="btn-primary"
type="submit" />
class="btn-primary" {{/if}}
/>
{{/if}}
</form>
{{/if}} {{/if}}
</div> </form>
</div> </div>

View File

@ -134,7 +134,10 @@
<label class="alt-placeholder" for="new-account-password"> <label class="alt-placeholder" for="new-account-password">
{{i18n "user.password.title"}} {{i18n "user.password.title"}}
</label> </label>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
/>
<div class="create-account__password-info"> <div class="create-account__password-info">
<div class="create-account__password-tip-validation"> <div class="create-account__password-tip-validation">
{{#if this.showPasswordValidation}} {{#if this.showPasswordValidation}}
@ -159,10 +162,6 @@
{{i18n "login.caps_lock_warning"}} {{i18n "login.caps_lock_warning"}}
</div> </div>
</div> </div>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
/>
</div> </div>
{{/if}} {{/if}}

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

@ -7,6 +7,10 @@ let userFound = false;
acceptance("Forgot password", function (needs) { acceptance("Forgot password", function (needs) {
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
needs.settings({
hide_email_address_taken: false,
});
server.post("/session/forgot_password", () => { server.post("/session/forgot_password", () => {
return helper.response({ return helper.response({
user_found: userFound, user_found: userFound,
@ -78,6 +82,10 @@ acceptance("Forgot password", function (needs) {
acceptance( acceptance(
"Forgot password - hide_email_address_taken enabled", "Forgot password - hide_email_address_taken enabled",
function (needs) { function (needs) {
needs.settings({
hide_email_address_taken: true,
});
needs.pretender((server, helper) => { needs.pretender((server, helper) => {
server.post("/session/forgot_password", () => { server.post("/session/forgot_password", () => {
return helper.response({}); return helper.response({});
@ -93,12 +101,12 @@ acceptance(
.dom(".forgot-password-reset") .dom(".forgot-password-reset")
.isDisabled("disables the button until the field is filled"); .isDisabled("disables the button until the field is filled");
await fillIn("#username-or-email", "someuser"); await fillIn("#username-or-email", "someuser@discourse.org");
await click(".forgot-password-reset"); await click(".forgot-password-reset");
assert.dom(".d-modal__body").hasHtml( assert.dom(".d-modal__body").hasHtml(
i18n("forgot_password.complete_username", { i18n("forgot_password.complete_email", {
username: "someuser", email: "someuser@discourse.org",
}), }),
"displays a success message" "displays a success message"
); );

View File

@ -1,4 +1,4 @@
import { click, fillIn, visit } from "@ember/test-helpers"; import { blur, click, fillIn, visit } from "@ember/test-helpers";
import { test } from "qunit"; import { test } from "qunit";
import sinon from "sinon"; import sinon from "sinon";
import PreloadStore from "discourse/lib/preload-store"; import PreloadStore from "discourse/lib/preload-store";
@ -71,6 +71,8 @@ acceptance("Password Reset", function (needs) {
assert.dom(".password-reset .tip.good").exists("input looks good"); assert.dom(".password-reset .tip.good").exists("input looks good");
await fillIn(".password-reset input", "123"); await fillIn(".password-reset input", "123");
await blur(".password-reset input");
assert.dom(".password-reset .tip.bad").exists("input is not valid"); assert.dom(".password-reset .tip.bad").exists("input is not valid");
assert.dom(".password-reset .tip.bad").includesHtml( assert.dom(".password-reset .tip.bad").includesHtml(
i18n("user.password.too_short", { i18n("user.password.too_short", {

View File

@ -4,7 +4,6 @@ import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import AdminConfigAreaCard from "admin/components/admin-config-area-card"; import AdminConfigAreaCard from "admin/components/admin-config-area-card";
module("Integration | Component | AdminConfigAreaCard", function (hooks) { module("Integration | Component | AdminConfigAreaCard", function (hooks) {
hooks.beforeEach(function () {});
setupRenderingTest(hooks); setupRenderingTest(hooks);
test("renders admin config area card without toggle button", async function (assert) { test("renders admin config area card without toggle button", async function (assert) {

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

@ -166,7 +166,7 @@ module("Integration | Component | CreateInvite", function (hooks) {
assert.deepEqual( assert.deepEqual(
formKit().field("expiresAfterDays").options(), formKit().field("expiresAfterDays").options(),
["__NONE__", "1", "3", "7", "30", "90", "999999"], ["1", "3", "7", "30", "90", "999999"],
"the value of invite_expiry_days is added to the dropdown" "the value of invite_expiry_days is added to the dropdown"
); );
@ -179,7 +179,7 @@ module("Integration | Component | CreateInvite", function (hooks) {
assert.deepEqual( assert.deepEqual(
formKit().field("expiresAfterDays").options(), formKit().field("expiresAfterDays").options(),
["__NONE__", "1", "7", "30", "90", "999999"], ["1", "7", "30", "90", "999999"],
"the value of invite_expiry_days is not added to the dropdown if it's already one of the options" "the value of invite_expiry_days is not added to the dropdown if it's already one of the options"
); );
}); });

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

@ -50,6 +50,16 @@ module("Integration | Component | d-select", function (hooks) {
}); });
}); });
test("required field", async function (assert) {
await render(<template>
<DSelect @includeNone={{false}} as |s|>
<s.Option @value="foo">The real foo</s.Option>
</DSelect>
</template>);
assert.dselect().hasNoOption(NO_VALUE_OPTION);
});
test("select attributes", async function (assert) { test("select attributes", async function (assert) {
await render(<template><DSelect class="test" /></template>); await render(<template><DSelect class="test" /></template>);

View File

@ -33,7 +33,7 @@ module("Integration | Component | webhook-status", function (hooks) {
assert.dom().hasText("Failed"); assert.dom().hasText("Failed");
}); });
test("iconName", async function (assert) { test("statusLabelClass", async function (assert) {
const webhook = new CoreFabricators(getOwner(this)).webhook(); const webhook = new CoreFabricators(getOwner(this)).webhook();
await render(<template> await render(<template>
<WebhookStatus <WebhookStatus
@ -42,30 +42,18 @@ module("Integration | Component | webhook-status", function (hooks) {
/> />
</template>); </template>);
assert.dom(".d-icon-far-circle").exists(); assert.dom(".status-label").hasClass("--inactive");
webhook.set("last_delivery_status", 2); webhook.set("last_delivery_status", 2);
await rerender(); await rerender();
assert.dom(".status-label").hasClass("--critical");
assert.dom(".d-icon-circle-xmark").exists(); webhook.set("last_delivery_status", 3);
});
test("iconClass", async function (assert) {
const webhook = new CoreFabricators(getOwner(this)).webhook();
await render(<template>
<WebhookStatus
@deliveryStatuses={{DELIVERY_STATUSES}}
@webhook={{webhook}}
/>
</template>);
assert.dom(".d-icon").hasClass("text-muted");
webhook.set("last_delivery_status", 2);
await rerender(); await rerender();
assert.dom(".status-label").hasClass("--success");
assert.dom(".d-icon").hasClass("text-danger"); webhook.set("last_delivery_status", 4);
await rerender();
assert.dom(".status-label").hasClass("--inactive");
}); });
}); });

View File

@ -3,7 +3,8 @@
$mobile-breakpoint: 700px; $mobile-breakpoint: 700px;
:root { :root {
--space-1: 0.25rem; --space-0: 0.125rem; //2px
--space-1: 0.25rem; //4px
--space-2: calc(0.25rem * 2); --space-2: calc(0.25rem * 2);
--space-3: calc(0.25rem * 3); --space-3: calc(0.25rem * 3);
--space-4: calc(0.25rem * 4); --space-4: calc(0.25rem * 4);

View File

@ -67,25 +67,27 @@
} }
} }
// Default
.status-label { .status-label {
--d-border-radius: var(--space-4); --d-border-radius: var(--space-4);
--status-icon-diameter: 8px;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
width: fit-content; width: fit-content;
background-color: var(--primary-low); background-color: var(--primary-low);
padding: var(--space-1) var(--space-2); padding: var(--space-0) var(--space-2);
border-radius: var(--d-border-radius); border-radius: var(--d-border-radius);
.status-label-indicator { .status-label-indicator {
display: inline-block; display: inline-block;
width: 6px; width: var(--status-icon-diameter);
height: 6px; height: var(--status-icon-diameter);
border-radius: 50%; border-radius: 50%;
background-color: var(--primary-high); background-color: var(--primary-high);
flex-shrink: 0; flex-shrink: 0;
margin-right: var(--space-1); margin-right: var(--space-1);
margin-top: 0.4rem; margin-top: 0.35rem;
} }
.status-label-text { .status-label-text {
@ -93,6 +95,45 @@
font-size: var(--font-down-1); font-size: var(--font-down-1);
} }
} }
// Success badge
.status-label.--success {
background-color: var(--success-low);
.status-label-indicator {
background-color: var(--success);
}
.status-label-text {
color: var(--success-hover);
}
}
// Critical badge
.status-label.--critical {
background-color: var(--danger-low);
.status-label-indicator {
background-color: var(--danger);
}
.status-label-text {
color: var(--danger-hover);
}
}
// Inactive badge
.status-label.--inactive {
background-color: var(--primary-low);
.status-label-indicator {
background-color: var(--primary-high);
}
.status-label-text {
color: var(--primary-high);
}
}
} }
.d-admin-row__overview { .d-admin-row__overview {

View File

@ -1,44 +1,24 @@
// Styles for admin/api // Styles for admin/api
table.web-hooks.grid { .d-admin-table.web-hooks {
td.delivery-status { .d-admin-row__overview.payload-url {
div {
display: flex;
align-items: center;
}
.d-icon {
margin-right: 0.25em;
}
}
td.payload-url {
word-wrap: break-word; word-wrap: break-word;
max-width: 55vw; max-width: 20vw;
}
td.controls {
display: flex;
button {
margin-left: 0.25em;
}
}
@media screen and (min-width: 550px) {
tbody {
tr {
grid-template-columns: 0.5fr repeat(2, 1fr) 0.5fr;
}
td.controls { @include breakpoint(medium) {
text-align: right; max-width: 70vw;
}
} }
} }
@include breakpoint(mobile-extra-large) {
tbody { .d-admin-row__detail.description {
tr { @include breakpoint(medium) {
grid-template-columns: 0.5fr 1fr; display: block;
}
} }
td.controls {
grid-row: 2; .d-admin-row__mobile-label {
@include breakpoint(medium) {
display: block;
}
} }
} }
} }

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

@ -11,7 +11,8 @@
body.login-page, body.login-page,
body.signup-page, body.signup-page,
body.invite-page { body.invite-page,
body.password-reset-page {
& ~ .powered-by-discourse, & ~ .powered-by-discourse,
.above-main-container-outlet { .above-main-container-outlet {
display: none; display: none;
@ -25,7 +26,8 @@ body.signup-page {
.login-fullpage, .login-fullpage,
.signup-fullpage, .signup-fullpage,
.invites-show { .invites-show,
.password-reset-page {
.signup-body, .signup-body,
.login-body { .login-body {
display: flex; display: flex;
@ -241,8 +243,6 @@ body.signup-page {
.caps-lock-warning { .caps-lock-warning {
color: var(--danger); color: var(--danger);
font-size: var(--font-down-1); font-size: var(--font-down-1);
font-weight: bold;
margin-top: 0.5em;
} }
.create-account__password-info { .create-account__password-info {

View File

@ -33,41 +33,39 @@ body.invite-page {
// the second button can wrap in some locales, and this helps alignment // the second button can wrap in some locales, and this helps alignment
} }
.password-reset {
.instructions {
label {
color: var(--primary-medium);
}
}
#new-account-password {
width: 15em;
}
.tip {
margin: 0 0 0.5em;
}
.toggle-password-mask {
margin-left: 0.25em;
}
}
.password-reset-page { .password-reset-page {
.caps-lock-warning {
display: inline;
}
.change-password-form { .change-password-form {
margin: 0 auto;
display: flex;
flex-direction: column;
width: 400px;
input {
padding: 0.75em 0.77em;
min-width: 250px;
margin-bottom: 0.25em;
width: 100%;
}
.input {
position: relative;
margin-bottom: 1em;
}
.tip { .tip {
display: block; display: block;
} }
} }
} }
.signup-fullpage .input-group input[type="password"] {
padding-right: 3em;
}
.toggle-password-mask { .toggle-password-mask {
align-self: start; position: absolute;
line-height: 1.4; // aligns with input description text right: 0;
padding: 0.75em 0.77em; // alligns with input padding
.ios-device & { .ios-device & {
// reset form-item-sizing mixin styles // reset form-item-sizing mixin styles
padding-top: 0; padding: 0.7em;
padding-bottom: 0;
font-size: var(--font-0); font-size: var(--font-0);
} }
} }

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

@ -1,15 +1,22 @@
$progress-bar-line-width: 2px; :root {
$progress-bar-circle-size: 1.2rem; --progress-bar-line-width: 1px;
$progress-bar-icon-size: 0.8rem; --progress-bar-circle-size: 0.5rem;
--progress-bar-icon-size: 0.25rem;
}
.signup-progress-bar { .signup-progress-bar {
width: 100%; width: auto;
display: flex; display: flex;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 1.2em; margin-bottom: 1.2em;
gap: 1rem;
.account-created &,
.activate-account & {
margin-inline: 0;
}
&__segment { &__segment {
width: 100%; width: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
@ -21,7 +28,7 @@ $progress-bar-icon-size: 0.8rem;
} }
&:last-child { &:last-child {
width: $progress-bar-circle-size; width: var(--progress-bar-circle-size);
.signup-progress-bar__circle { .signup-progress-bar__circle {
transform: translateX(-50%); transform: translateX(-50%);
z-index: 1; z-index: 1;
@ -33,66 +40,29 @@ $progress-bar-icon-size: 0.8rem;
display: flex; display: flex;
} }
&__step-text {
color: var(--primary-high);
white-space: nowrap;
width: fit-content;
transform: translateX(calc(calc($progress-bar-circle-size / 2) - 50%));
.signup-progress-bar__segment:first-child & {
transform: translateX(0%);
}
.signup-progress-bar__segment:last-child & {
transform: translateX(
calc(calc($progress-bar-circle-size + $progress-bar-line-width) - 100%)
);
}
.--active & {
font-weight: bold;
color: var(--primary);
}
}
&__line {
transform: translateY(
calc(calc($progress-bar-circle-size + $progress-bar-line-width) / 2)
);
height: $progress-bar-line-width;
width: 100%;
background-color: var(--primary-low-mid);
.--completed & {
background-color: var(--success);
}
}
&__circle { &__circle {
flex-shrink: 0; flex-shrink: 0;
font-size: $progress-bar-icon-size; font-size: var(--progress-bar-icon-size);
color: var(--secondary); color: var(--secondary);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: $progress-bar-circle-size; height: var(--progress-bar-circle-size);
width: $progress-bar-circle-size; width: var(--progress-bar-circle-size);
transform: none;
border-radius: 50%; border-radius: 50%;
border: $progress-bar-line-width solid var(--primary-low-mid); border: var(--progress-bar-line-width) solid var(--primary-low-mid);
background-color: var(--secondary); background-color: var(--secondary);
.--active & { .--active & {
background-color: var(--success);
border-color: var(--success); border-color: var(--success);
background: var(--success); box-shadow: 0 0 1px calc(var(--progress-bar-circle-size) / 2)
box-shadow: 0 0 1px 5px var(--success-low); var(--success-low);
} }
.--completed & { .--completed & {
background-color: var(--success); background-color: var(--success);
border-color: var(--success); border-color: var(--success);
} }
} }
&__line.--completed {
background-color: var(--success);
}
} }

View File

@ -6,9 +6,22 @@
margin-bottom: 0; margin-bottom: 0;
} }
.invited-by {
display: flex;
align-items: center;
gap: 0.5em;
margin: 1em 0;
font-size: var(--font-down-1);
}
.invited-by p {
margin: 0;
}
.user-info { .user-info {
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
margin: 0;
} }
.avatar { .avatar {
@ -55,3 +68,30 @@
} }
} }
} }
.invite-page {
background: var(--secondary);
}
.invites-show,
#simple-container .invite-error {
max-width: 500px;
padding: 2rem 3rem;
background: var(--secondary);
margin: 0 auto;
@media screen and (max-width: 700px) {
margin: 1em auto 1em auto;
padding: 1rem;
}
}
#simple-container .invite-error {
.error-info {
text-align: center;
}
.error-image {
text-align: center;
padding-bottom: 1em;
}
}

View File

@ -3,7 +3,6 @@
@import "compose"; @import "compose";
@import "discourse"; @import "discourse";
@import "header"; @import "header";
@import "invite-signup";
@import "latest-topic-list"; @import "latest-topic-list";
@import "login-signup-page"; @import "login-signup-page";
@import "menu-panel"; @import "menu-panel";

View File

@ -1,25 +0,0 @@
.invite-page {
background: var(--secondary);
}
.invites-show,
#simple-container .invite-error {
max-width: 500px;
padding: 2rem 3rem;
background: var(--secondary);
margin: 10vh auto 1em auto;
@media screen and (max-height: 700px) {
margin: 1em auto 1em auto;
}
}
#simple-container .invite-error {
.error-info {
text-align: center;
}
.error-image {
text-align: center;
padding-bottom: 1em;
}
}

View File

@ -2,6 +2,10 @@
box-sizing: border-box; box-sizing: border-box;
padding: 1em; padding: 1em;
.signup-progress-bar {
margin: 0;
}
.invitation-cta { .invitation-cta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -97,11 +97,15 @@ class PostMover
@first_post_number_moved = @first_post_number_moved =
posts.first.is_first_post? ? posts[1]&.post_number : posts.first.post_number posts.first.is_first_post? ? posts[1]&.post_number : posts.first.post_number
if @options[:freeze_original] # in this case we need to add the moderator post after the last copied post if @options[:freeze_original]
from_posts = @original_topic.ordered_posts.where("post_number > ?", posts.last.post_number) # in this case we need to add the moderator post after the last copied post
shift_post_numbers(from_posts) if !@full_move if @full_move
@first_post_number_moved = @original_topic.ordered_posts.last.post_number + 1
@first_post_number_moved = posts.last.post_number + 1 else
from_posts = @original_topic.ordered_posts.where("post_number > ?", posts.last.post_number)
shift_post_numbers(from_posts)
@first_post_number_moved = posts.last.post_number + 1
end
end end
move_each_post move_each_post
@ -478,7 +482,9 @@ class PostMover
def copy_shifted_post_timings_from_temp def copy_shifted_post_timings_from_temp
DB.exec <<~SQL DB.exec <<~SQL
INSERT INTO post_timings (topic_id, user_id, post_number, msecs) INSERT INTO post_timings (topic_id, user_id, post_number, msecs)
SELECT DISTINCT topic_id, user_id, post_number, msecs FROM temp_post_timings SELECT DISTINCT ON (topic_id, post_number, user_id) topic_id, user_id, post_number, msecs
FROM temp_post_timings
ORDER BY topic_id, post_number, user_id, msecs DESC
ON CONFLICT (topic_id, post_number, user_id) DO UPDATE ON CONFLICT (topic_id, post_number, user_id) DO UPDATE
SET msecs = GREATEST(post_timings.msecs, excluded.msecs) SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
SQL SQL

View File

@ -235,7 +235,11 @@ module Discourse
# Use discourse-fonts gem to symlink fonts and generate .scss file # Use discourse-fonts gem to symlink fonts and generate .scss file
fonts_path = File.join(config.root, "public/fonts") fonts_path = File.join(config.root, "public/fonts")
Discourse::Utils.atomic_ln_s(DiscourseFonts.path_for_fonts, fonts_path) if !File.exist?(fonts_path) || File.realpath(fonts_path) != DiscourseFonts.path_for_fonts
puts "Symlinking fonts from discourse-fonts gem"
File.delete(fonts_path) if File.exist?(fonts_path)
Discourse::Utils.atomic_ln_s(DiscourseFonts.path_for_fonts, fonts_path)
end
require "stylesheet/manager" require "stylesheet/manager"
require "svg_sprite" require "svg_sprite"

View File

@ -3275,6 +3275,7 @@ en:
other: "%{count} posts in topic" other: "%{count} posts in topic"
create: "New Topic" create: "New Topic"
create_disabled_category: "You're not allowed to create topics in this category" create_disabled_category: "You're not allowed to create topics in this category"
create_disabled_tag: "You're not allowed to create topics with this tag"
create_long: "Create a new Topic" create_long: "Create a new Topic"
open_draft: "Open Draft" open_draft: "Open Draft"
private_message: "Start a message" private_message: "Start a message"

View File

@ -621,7 +621,7 @@ login:
list_type: simple list_type: simple
hide_email_address_taken: hide_email_address_taken:
client: true client: true
default: false default: true
log_out_strict: false log_out_strict: false
pending_users_reminder_delay_minutes: pending_users_reminder_delay_minutes:
min: -1 min: -1

View File

@ -422,7 +422,7 @@ class TopicQuery
def list_new_in_category(category) def list_new_in_category(category)
create_list(:new_in_category, unordered: true, category: category.id) do |list| create_list(:new_in_category, unordered: true, category: category.id) do |list|
list.by_newest.first(25) list.by_newest.limit(25)
end end
end end

View File

@ -119,6 +119,8 @@ describe "Core extensions" do
describe "user custom fields" do describe "user custom fields" do
it "supports discourse_automation_ids" do it "supports discourse_automation_ids" do
SiteSetting.hide_email_address_taken = false
user = create_user user = create_user
automation_1.add_id_to_custom_field(user, DiscourseAutomation::AUTOMATION_IDS_CUSTOM_FIELD) automation_1.add_id_to_custom_field(user, DiscourseAutomation::AUTOMATION_IDS_CUSTOM_FIELD)

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

@ -3,14 +3,12 @@ import { hbs } from "ember-cli-htmlbars";
import { module, test } from "qunit"; import { module, test } from "qunit";
import sinon from "sinon"; import sinon from "sinon";
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 { HEADER_INDICATOR_PREFERENCE_ALL_NEW } from "discourse/plugins/chat/discourse/controllers/preferences-chat"; import { HEADER_INDICATOR_PREFERENCE_ALL_NEW } from "discourse/plugins/chat/discourse/controllers/preferences-chat";
module("Discourse Chat | Component | chat-header-icon", function (hooks) { module("Discourse Chat | Component | chat-header-icon", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(function () {});
test("full page - never separated sidebar mode", async function (assert) { test("full page - never separated sidebar mode", async function (assert) {
this.currentUser.user_option.chat_separate_sidebar_mode = "never"; this.currentUser.user_option.chat_separate_sidebar_mode = "never";
sinon sinon

View File

@ -5,6 +5,7 @@ RSpec.describe "invite only" do
describe "#create invite only" do describe "#create invite only" do
it "can create user via API" do it "can create user via API" do
SiteSetting.invite_only = true SiteSetting.invite_only = true
SiteSetting.hide_email_address_taken = false
Jobs.run_immediately! Jobs.run_immediately!
admin = Fabricate(:admin) admin = Fabricate(:admin)

View File

@ -5,6 +5,8 @@ RSpec.describe EmailUpdater do
let(:new_email) { "new.email@example.com" } let(:new_email) { "new.email@example.com" }
it "provides better error message when a staged user has the same email" do it "provides better error message when a staged user has the same email" do
SiteSetting.hide_email_address_taken = false
Fabricate(:user, staged: true, email: new_email) Fabricate(:user, staged: true, email: new_email)
user = Fabricate(:user, email: old_email) user = Fabricate(:user, email: old_email)

View File

@ -20,6 +20,30 @@ RSpec.describe TopicQuery do
fab!(:moderator) fab!(:moderator)
fab!(:admin) fab!(:admin)
before do
@plugin_instance = Plugin::Instance.new
@validator_blk =
lambda do |topics, options, query|
# this is notable, we do not send in a relation for suggested
# it would force us to completely rewrite SuggestedTopicsBuilder
expect(topics.is_a?(ActiveRecord::Relation)).to eq(true) if options[:filter] != :suggested
topics
end
DiscoursePluginRegistry.register_modifier(
@plugin_instance,
:topic_query_create_list_topics,
&@validator_blk
)
end
after do
DiscoursePluginRegistry.unregister_modifier(
@plugin_instance,
:topic_query_create_list_topics,
&@validator_blk
)
end
describe "secure category" do describe "secure category" do
it "filters categories out correctly" do it "filters categories out correctly" do
category = Fabricate(:category_with_definition) category = Fabricate(:category_with_definition)
@ -2228,22 +2252,28 @@ RSpec.describe TopicQuery do
fab!(:topic1) { Fabricate(:topic, created_at: 3.days.ago, bumped_at: 1.hour.ago) } fab!(:topic1) { Fabricate(:topic, created_at: 3.days.ago, bumped_at: 1.hour.ago) }
fab!(:topic2) { Fabricate(:topic, created_at: 2.days.ago, bumped_at: 3.hour.ago) } fab!(:topic2) { Fabricate(:topic, created_at: 2.days.ago, bumped_at: 3.hour.ago) }
after { DiscoursePluginRegistry.clear_modifiers! }
it "allows changing" do it "allows changing" do
original_topic_query = TopicQuery.new(user) original_topic_query = TopicQuery.new(user)
plugin_instance = Plugin::Instance.new
Plugin::Instance blk =
.new lambda do |topics, options, topic_query|
.register_modifier(:topic_query_create_list_topics) do |topics, options, topic_query|
expect(topic_query).to eq(topic_query) expect(topic_query).to eq(topic_query)
topic_query.options[:order] = "created" topic_query.options[:order] = "created"
topics topics
end end
DiscoursePluginRegistry.register_modifier(
plugin_instance,
:topic_query_create_list_topics,
&blk
)
expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic1, topic2].map(&:id)) expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic1, topic2].map(&:id))
DiscoursePluginRegistry.clear_modifiers! DiscoursePluginRegistry.unregister_modifier(
plugin_instance,
:topic_query_create_list_topics,
&blk
)
expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic2, topic1].map(&:id)) expect(original_topic_query.list_latest.topics.map(&:id)).to eq([topic2, topic1].map(&:id))
end end

View File

@ -117,6 +117,8 @@ RSpec.describe Invite do
end end
it "escapes the email_address when raising an existing user error" do it "escapes the email_address when raising an existing user error" do
SiteSetting.hide_email_address_taken = false
user.email = xss_email user.email = xss_email
user.save(validate: false) user.save(validate: false)

View File

@ -2898,7 +2898,7 @@ RSpec.describe PostMover do
).to eq(true) ).to eq(true)
end end
it "creates the moderator message in the correct position" do it "creates the moderator message in the correct position for partial move" do
PostMover.new( PostMover.new(
original_topic, original_topic,
Discourse.system_user, Discourse.system_user,
@ -2908,11 +2908,34 @@ RSpec.describe PostMover do
}, },
).to_topic(destination_topic.id) ).to_topic(destination_topic.id)
moderator_post = # Moderator post is right after the second_post since it was the last post moved
original_topic.reload.ordered_posts.find_by(post_number: second_post.post_number + 1) # the next post expect(
expect(moderator_post).to be_present original_topic.ordered_posts.find_by(
expect(moderator_post.post_type).to eq(Post.types[:small_action]) post_number: second_post.post_number + 1,
expect(moderator_post.action_code).to eq("split_topic") post_type: Post.types[:small_action],
action_code: "split_topic",
),
).to be_present
end
it "creates the moderator message in the correct position for full move" do
small_action = Fabricate(:small_action, topic: original_topic)
PostMover.new(
original_topic,
Discourse.system_user,
[op.id, first_post.id, second_post.id, third_post.id],
options: {
freeze_original: true,
},
).to_topic(destination_topic.id)
expect(
original_topic.ordered_posts.find_by(
post_number: small_action.post_number + 1,
post_type: Post.types[:small_action],
action_code: "split_topic",
),
).to be_present
end end
context "with `post_mover_create_moderator_post` modifier" do context "with `post_mover_create_moderator_post` modifier" do

View File

@ -656,6 +656,8 @@ RSpec.describe "users" do
end end
path "/session/forgot_password.json" do path "/session/forgot_password.json" do
SiteSetting.hide_email_address_taken = false
post "Send password reset email" do post "Send password reset email" do
tags "Users" tags "Users"
operationId "sendPasswordResetEmail" operationId "sendPasswordResetEmail"

View File

@ -16,6 +16,8 @@ RSpec.describe SessionController do
end end
end end
before { SiteSetting.hide_email_address_taken = false }
describe "#email_login_info" do describe "#email_login_info" do
let(:email_token) do let(:email_token) do
Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login]) Fabricate(:email_token, user: user, scope: EmailToken.scopes[:email_login])

View File

@ -19,6 +19,8 @@ RSpec.describe UsersController do
# late for fab! to work. # late for fab! to work.
let(:user_deferred) { Fabricate(:user, refresh_auto_groups: true) } let(:user_deferred) { Fabricate(:user, refresh_auto_groups: true) }
before { SiteSetting.hide_email_address_taken = false }
describe "#full account registration flow" do describe "#full account registration flow" do
it "will correctly handle honeypot and challenge" do it "will correctly handle honeypot and challenge" do
get "/session/hp.json" get "/session/hp.json"

View File

@ -207,12 +207,28 @@ RSpec.describe UsersEmailController do
context "when new email is different case of existing email" do context "when new email is different case of existing email" do
fab!(:other_user) { Fabricate(:user, email: "case.insensitive@gmail.com") } fab!(:other_user) { Fabricate(:user, email: "case.insensitive@gmail.com") }
it "raises an error" do context "when hiding taken e-mails" do
put "/u/#{user.username}/preferences/email.json", before { SiteSetting.hide_email_address_taken = true }
params: {
email: other_user.email.upcase, it "raises an error" do
} put "/u/#{user.username}/preferences/email.json",
expect(response).to_not be_successful params: {
email: other_user.email.upcase,
}
expect(response).to be_successful
end
end
context "when revealing taken e-mails" do
before { SiteSetting.hide_email_address_taken = false }
it "raises an error" do
put "/u/#{user.username}/preferences/email.json",
params: {
email: other_user.email.upcase,
}
expect(response).to_not be_successful
end
end end
end end

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

@ -51,6 +51,8 @@ describe "Changing email", type: :system do
end end
it "works when user has totp 2fa" do it "works when user has totp 2fa" do
SiteSetting.hide_email_address_taken = false
second_factor = Fabricate(:user_second_factor_totp, user: user) second_factor = Fabricate(:user_second_factor_totp, user: user)
sign_in user sign_in user

View File

@ -10,7 +10,10 @@ shared_examples "login scenarios" do |login_page_object|
fab!(:admin) { Fabricate(:admin, username: "admin", password: "supersecurepassword") } fab!(:admin) { Fabricate(:admin, username: "admin", password: "supersecurepassword") }
let(:user_menu) { PageObjects::Components::UserMenu.new } let(:user_menu) { PageObjects::Components::UserMenu.new }
before { Jobs.run_immediately! } before do
SiteSetting.hide_email_address_taken = false
Jobs.run_immediately!
end
def wait_for_email_link(user, type) def wait_for_email_link(user, type)
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 } wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }

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

View File

@ -221,7 +221,10 @@ shared_examples "signup scenarios" do |signup_page_object, login_page_object|
end end
context "when the email domain is blocked" do context "when the email domain is blocked" do
before { SiteSetting.blocked_email_domains = "example.com" } before do
SiteSetting.hide_email_address_taken = false
SiteSetting.blocked_email_domains = "example.com"
end
it "cannot signup" do it "cannot signup" do
signup_form signup_form