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

View File

@ -1,138 +1,25 @@
import { hash } from "@ember/helper";
import DButton from "discourse/components/d-button";
// TODO (martin) Delete this once we have removed references from plugins.
export const AdminPageActionButton = <template>
<DButton
class="admin-page-action-button btn-small"
...attributes
@action={{@action}}
@route={{@route}}
@routeModels={{@routeModels}}
@label={{@label}}
@title={{@title}}
@icon={{@icon}}
@isLoading={{@isLoading}}
/>
</template>;
import {
DangerActionListItem,
DangerButton,
DefaultActionListItem,
DefaultButton,
DPageActionButton,
DPageActionListItem,
PrimaryActionListItem,
PrimaryButton,
WrappedActionListItem,
WrappedButton,
} from "discourse/components/d-page-action-button";
// This is used for cases where there is another component,
// e.g. UppyBackupUploader, that is a button which cannot use
// PrimaryButton and so on directly. This should be used very rarely,
// most cases are covered by the other button types.
export const WrappedButton = <template>
<span class="admin-page-action-wrapped-button">{{yield}}</span>
</template>;
// No buttons here pass in an @icon by design. They are okay to
// use on dropdown list items, but our UI guidelines do not allow them
// on regular buttons.
export const PrimaryButton = <template>
<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>;
export { DangerActionListItem as DangerActionListItem };
export { DangerButton as DangerButton };
export { DefaultActionListItem as DefaultActionListItem };
export { DefaultButton as DefaultButton };
export { DPageActionButton as DPageActionButton };
export { DPageActionListItem as DPageActionListItem };
export { PrimaryActionListItem as PrimaryActionListItem };
export { PrimaryButton as PrimaryButton };
export { WrappedActionListItem as WrappedActionListItem };
export { WrappedButton as WrappedButton };

View File

@ -1,10 +1,10 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import DBreadcrumbsItem from "discourse/components/d-breadcrumbs-item";
import DPageHeader from "discourse/components/d-page-header";
import NavItem from "discourse/components/nav-item";
import { headerActionComponentForPlugin } from "discourse/lib/admin-plugin-header-actions";
import { i18n } from "discourse-i18n";
import AdminPageHeader from "./admin-page-header";
import AdminPluginConfigArea from "./admin-plugin-config-area";
export default class AdminPluginConfigPage extends Component {
@ -41,14 +41,14 @@ export default class AdminPluginConfigPage extends Component {
<template>
<div class="admin-plugin-config-page">
<AdminPageHeader
@titleLabelTranslated={{@plugin.nameTitleized}}
@descriptionLabelTranslated={{@plugin.about}}
<DPageHeader
@titleLabel={{@plugin.nameTitleized}}
@descriptionLabel={{@plugin.about}}
@learnMoreUrl={{@plugin.linkUrl}}
@headerActionComponent={{this.headerActionComponent}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/plugins"
@label={{i18n "admin.plugins.title"}}
@ -75,7 +75,7 @@ export default class AdminPluginConfigPage extends Component {
{{/each}}
{{/if}}
</:tabs>
</AdminPageHeader>
</DPageHeader>
<div class="admin-plugin-config-page__content">
<div class={{this.mainAreaClasses}}>

View File

@ -1,14 +1,14 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
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 {
DangerButton,
DefaultButton,
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 {
get title() {

View File

@ -1,10 +1,8 @@
import Component from "@glimmer/component";
import icon from "discourse-common/helpers/d-icon";
import { i18n } from "discourse-i18n";
export default class WebhookStatus extends Component {
iconNames = ["far-circle", "circle-xmark", "circle", "circle"];
iconClasses = ["text-muted", "text-danger", "text-successful", "text-muted"];
statusClasses = ["--inactive", "--critical", "--success", "--inactive"];
get 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}`);
}
get iconName() {
return this.iconNames[this.status.id - 1];
}
get iconClass() {
return this.iconClasses[this.status.id - 1];
get statusClass() {
return this.statusClasses[this.status.id - 1];
}
<template>
{{icon this.iconName class=this.iconClass}}
{{this.deliveryStatus}}
<div role="status" class="status-label {{this.statusClass}}">
<div class="status-label-indicator">
</div>
<div class="status-label-text">
{{this.deliveryStatus}}
</div>
</div>
</template>
}

View File

@ -1,10 +1,11 @@
<div class="badges">
<AdminPageHeader
@titleLabel="admin.badges.title"
@descriptionLabel="admin.badges.page_description"
<DPageHeader
@titleLabel={{i18n "admin.badges.title"}}
@descriptionLabel={{i18n "admin.badges.page_description"}}
@learnMoreUrl="https://meta.discourse.org/t/understanding-and-using-badges/32540"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/badges"
@label={{i18n "admin.badges.title"}}
@ -35,7 +36,7 @@
class="edit-groupings-btn"
/>
</:actions>
</AdminPageHeader>
</DPageHeader>
<div class="admin-container">
<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.Wrapped as |wrapped|>
{{#if this.localBackupStorage}}
@ -15,7 +15,7 @@
{{/if}}
</actions.Wrapped>
</:actions>
</AdminPageSubheader>
</DPageSubheader>
{{#if this.status.restoreDisabled}}
<div class="backup-message alert alert-info">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,4 +5,12 @@ import { tagName } from "@ember-decorators/component";
export default class CreateTopicButton extends Component {
label = "topic.create";
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 {
@service router;
@service capabilities;
@notEmpty("args.icon") btnIcon;
@ -114,6 +115,7 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
_triggerAction(event) {
const { action: actionVal, route, routeModels } = this.args;
const isIOS = this.capabilities?.isIOS;
if (actionVal || route) {
if (actionVal) {
@ -129,19 +131,35 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
);
}
} else if (typeof actionVal === "object" && actionVal.value) {
// Using `next()` to optimise INP
next(() =>
if (isIOS) {
// Don't optimise INP in iOS
// it results in focus events not being triggered
forwardEvent
? 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") {
// Using `next()` to optimise INP
next(() =>
if (isIOS) {
// Don't optimise INP in iOS
// it results in focus events not being triggered
forwardEvent
? actionVal(actionParam, event)
: actionVal(actionParam)
);
: actionVal(actionParam);
} else {
// Using `next()` to optimise INP
next(() =>
forwardEvent
? actionVal(actionParam, event)
: actionVal(actionParam)
);
}
}
} else if (route) {
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;
}
get includeNone() {
return this.args.includeNone ?? true;
}
<template>
<select
value={{@value}}
@ -53,13 +57,15 @@ export default class DSelect extends Component {
class="d-select"
{{on "input" this.handleInput}}
>
<DSelectOption @value={{NO_VALUE_OPTION}}>
{{#if this.hasSelectedValue}}
{{i18n "none_placeholder"}}
{{else}}
{{i18n "select_placeholder"}}
{{/if}}
</DSelectOption>
{{#if this.includeNone}}
<DSelectOption @value={{NO_VALUE_OPTION}}>
{{#if this.hasSelectedValue}}
{{i18n "none_placeholder"}}
{{else}}
{{i18n "select_placeholder"}}
{{/if}}
</DSelectOption>
{{/if}}
{{yield (hash Option=(component DSelectOption selected=@value))}}
</select>

View File

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

View File

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

View File

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

View File

@ -1,10 +1,10 @@
<DButton
@action={{@togglePasswordMask}}
@label={{if @maskPassword "login.show_password" "login.hide_password"}}
@icon={{if @maskPassword "far-eye" "far-eye-slash"}}
@title={{if
@maskPassword
"login.show_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;
redirected = false;
maskPassword = true;
passwordValidationVisible = false;
lockImageUrl = getURL("/images/lock.svg");
@ -65,6 +66,30 @@ export default class PasswordResetController extends Controller.extend(
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
done(event) {
if (wantsNewWindow(event)) {

View File

@ -15,12 +15,17 @@ const SelectOption = <template>
export default class FKControlSelect extends Component {
static controlType = "select";
get includeNone() {
return this.args.field.validation !== "required";
}
<template>
<DSelect
class="form-kit__control-select"
disabled={{@field.disabled}}
@value={{@field.value}}
@onChange={{@field.set}}
@includeNone={{this.includeNone}}
...attributes
>
{{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.
*
* 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">
{{i18n "invites.password_label"}}
</label>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
<div class="create-account__password-info">
<div class="create-account__password-tip-validation">
<InputTip
@ -148,11 +153,6 @@
{{i18n "login.caps_lock_warning"}}
</div>
</div>
<TogglePasswordMask
@maskPassword={{this.maskPassword}}
@togglePasswordMask={{this.togglePasswordMask}}
@parentController="invites-show"
/>
</div>
</div>
{{/unless}}

View File

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

View File

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

View File

@ -71,28 +71,28 @@ acceptance("Admin - Users List", function (needs) {
await visit("/admin/users/list/active");
assert.dom(".admin-page-subheader__title").hasText(activeTitle);
assert.dom(".d-page-subheader__title").hasText(activeTitle);
assert
.dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser);
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
.dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser);
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
.dom(".users-list .user:nth-child(1) .username")
.includesText(suspectUser);
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
.dom(".users-list .user:nth-child(1) .username")
.includesText(activeUser);

View File

@ -7,6 +7,10 @@ let userFound = false;
acceptance("Forgot password", function (needs) {
needs.pretender((server, helper) => {
needs.settings({
hide_email_address_taken: false,
});
server.post("/session/forgot_password", () => {
return helper.response({
user_found: userFound,
@ -78,6 +82,10 @@ acceptance("Forgot password", function (needs) {
acceptance(
"Forgot password - hide_email_address_taken enabled",
function (needs) {
needs.settings({
hide_email_address_taken: true,
});
needs.pretender((server, helper) => {
server.post("/session/forgot_password", () => {
return helper.response({});
@ -93,12 +101,12 @@ acceptance(
.dom(".forgot-password-reset")
.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");
assert.dom(".d-modal__body").hasHtml(
i18n("forgot_password.complete_username", {
username: "someuser",
i18n("forgot_password.complete_email", {
email: "someuser@discourse.org",
}),
"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 sinon from "sinon";
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");
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").includesHtml(
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";
module("Integration | Component | AdminConfigAreaCard", function (hooks) {
hooks.beforeEach(function () {});
setupRenderingTest(hooks);
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(
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"
);
@ -179,7 +179,7 @@ module("Integration | Component | CreateInvite", function (hooks) {
assert.deepEqual(
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"
);
});

View File

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

View File

@ -33,7 +33,7 @@ module("Integration | Component | webhook-status", function (hooks) {
assert.dom().hasText("Failed");
});
test("iconName", async function (assert) {
test("statusLabelClass", async function (assert) {
const webhook = new CoreFabricators(getOwner(this)).webhook();
await render(<template>
<WebhookStatus
@ -42,30 +42,18 @@ module("Integration | Component | webhook-status", function (hooks) {
/>
</template>);
assert.dom(".d-icon-far-circle").exists();
assert.dom(".status-label").hasClass("--inactive");
webhook.set("last_delivery_status", 2);
await rerender();
assert.dom(".status-label").hasClass("--critical");
assert.dom(".d-icon-circle-xmark").exists();
});
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);
webhook.set("last_delivery_status", 3);
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;
:root {
--space-1: 0.25rem;
--space-0: 0.125rem; //2px
--space-1: 0.25rem; //4px
--space-2: calc(0.25rem * 2);
--space-3: calc(0.25rem * 3);
--space-4: calc(0.25rem * 4);

View File

@ -67,25 +67,27 @@
}
}
// Default
.status-label {
--d-border-radius: var(--space-4);
--status-icon-diameter: 8px;
display: flex;
flex-wrap: nowrap;
width: fit-content;
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);
.status-label-indicator {
display: inline-block;
width: 6px;
height: 6px;
width: var(--status-icon-diameter);
height: var(--status-icon-diameter);
border-radius: 50%;
background-color: var(--primary-high);
flex-shrink: 0;
margin-right: var(--space-1);
margin-top: 0.4rem;
margin-top: 0.35rem;
}
.status-label-text {
@ -93,6 +95,45 @@
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 {

View File

@ -1,44 +1,24 @@
// Styles for admin/api
table.web-hooks.grid {
td.delivery-status {
div {
display: flex;
align-items: center;
}
.d-icon {
margin-right: 0.25em;
}
}
td.payload-url {
.d-admin-table.web-hooks {
.d-admin-row__overview.payload-url {
word-wrap: break-word;
max-width: 55vw;
}
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;
}
max-width: 20vw;
td.controls {
text-align: right;
}
@include breakpoint(medium) {
max-width: 70vw;
}
}
@include breakpoint(mobile-extra-large) {
tbody {
tr {
grid-template-columns: 0.5fr 1fr;
}
.d-admin-row__detail.description {
@include breakpoint(medium) {
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;
max-width: 70%;
&.admin-page-action-button {
&.d-page-action-button {
margin-top: 0;
@media (max-width: $mobile-breakpoint) {

View File

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

View File

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

View File

@ -11,7 +11,8 @@
body.login-page,
body.signup-page,
body.invite-page {
body.invite-page,
body.password-reset-page {
& ~ .powered-by-discourse,
.above-main-container-outlet {
display: none;
@ -25,7 +26,8 @@ body.signup-page {
.login-fullpage,
.signup-fullpage,
.invites-show {
.invites-show,
.password-reset-page {
.signup-body,
.login-body {
display: flex;
@ -241,8 +243,6 @@ body.signup-page {
.caps-lock-warning {
color: var(--danger);
font-size: var(--font-down-1);
font-weight: bold;
margin-top: 0.5em;
}
.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
}
.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 {
.caps-lock-warning {
display: inline;
}
.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 {
display: block;
}
}
}
.signup-fullpage .input-group input[type="password"] {
padding-right: 3em;
}
.toggle-password-mask {
align-self: start;
line-height: 1.4; // aligns with input description text
position: absolute;
right: 0;
padding: 0.75em 0.77em; // alligns with input padding
.ios-device & {
// reset form-item-sizing mixin styles
padding-top: 0;
padding-bottom: 0;
padding: 0.7em;
font-size: var(--font-0);
}
}

View File

@ -1,6 +1,7 @@
@import "badges";
@import "banner";
@import "d-breadcrumbs";
@import "d-page-header";
@import "d-stat-tiles";
@import "bookmark-list";
@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;
$progress-bar-circle-size: 1.2rem;
$progress-bar-icon-size: 0.8rem;
:root {
--progress-bar-line-width: 1px;
--progress-bar-circle-size: 0.5rem;
--progress-bar-icon-size: 0.25rem;
}
.signup-progress-bar {
width: 100%;
width: auto;
display: flex;
box-sizing: border-box;
margin-bottom: 1.2em;
gap: 1rem;
.account-created &,
.activate-account & {
margin-inline: 0;
}
&__segment {
width: 100%;
width: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
@ -21,7 +28,7 @@ $progress-bar-icon-size: 0.8rem;
}
&:last-child {
width: $progress-bar-circle-size;
width: var(--progress-bar-circle-size);
.signup-progress-bar__circle {
transform: translateX(-50%);
z-index: 1;
@ -33,66 +40,29 @@ $progress-bar-icon-size: 0.8rem;
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 {
flex-shrink: 0;
font-size: $progress-bar-icon-size;
font-size: var(--progress-bar-icon-size);
color: var(--secondary);
display: flex;
justify-content: center;
align-items: center;
height: $progress-bar-circle-size;
width: $progress-bar-circle-size;
height: var(--progress-bar-circle-size);
width: var(--progress-bar-circle-size);
transform: none;
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);
.--active & {
background-color: var(--success);
border-color: var(--success);
background: var(--success);
box-shadow: 0 0 1px 5px var(--success-low);
box-shadow: 0 0 1px calc(var(--progress-bar-circle-size) / 2)
var(--success-low);
}
.--completed & {
background-color: var(--success);
border-color: var(--success);
}
}
&__line.--completed {
background-color: var(--success);
}
}

View File

@ -6,9 +6,22 @@
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 {
align-items: center;
gap: 0.5em;
margin: 0;
}
.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 "discourse";
@import "header";
@import "invite-signup";
@import "latest-topic-list";
@import "login-signup-page";
@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;
padding: 1em;
.signup-progress-bar {
margin: 0;
}
.invitation-cta {
display: flex;
flex-direction: column;

View File

@ -97,11 +97,15 @@ class PostMover
@first_post_number_moved =
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
from_posts = @original_topic.ordered_posts.where("post_number > ?", posts.last.post_number)
shift_post_numbers(from_posts) if !@full_move
@first_post_number_moved = posts.last.post_number + 1
if @options[:freeze_original]
# in this case we need to add the moderator post after the last copied post
if @full_move
@first_post_number_moved = @original_topic.ordered_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
move_each_post
@ -478,7 +482,9 @@ class PostMover
def copy_shifted_post_timings_from_temp
DB.exec <<~SQL
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
SET msecs = GREATEST(post_timings.msecs, excluded.msecs)
SQL

View File

@ -235,7 +235,11 @@ module Discourse
# Use discourse-fonts gem to symlink fonts and generate .scss file
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 "svg_sprite"

View File

@ -3275,6 +3275,7 @@ en:
other: "%{count} posts in topic"
create: "New Topic"
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"
open_draft: "Open Draft"
private_message: "Start a message"

View File

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

View File

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

View File

@ -119,6 +119,8 @@ describe "Core extensions" do
describe "user custom fields" do
it "supports discourse_automation_ids" do
SiteSetting.hide_email_address_taken = false
user = create_user
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">
<AdminPageSubheader
@titleLabel="chat.incoming_webhooks.title"
@descriptionLabel="chat.incoming_webhooks.instructions"
<DPageSubheader
@titleLabel={{i18n "chat.incoming_webhooks.title"}}
@descriptionLabel={{i18n "chat.incoming_webhooks.instructions"}}
>
<:actions as |actions|>
<actions.Primary
@ -18,7 +18,7 @@
class="admin-incoming-webhooks-new"
/>
</:actions>
</AdminPageSubheader>
</DPageSubheader>
<div class="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(: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
chat_system_bootstrap(current_user)
@ -16,11 +16,11 @@ describe "Admin Chat Incoming Webhooks", type: :system do
it "can create incoming webhooks" do
admin_incoming_webhooks_page.visit
expect(admin_header).to be_visible
expect(d_page_header).to be_visible
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("description").fill_in("Some test content")

View File

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

View File

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

View File

@ -5,6 +5,8 @@ RSpec.describe EmailUpdater do
let(:new_email) { "new.email@example.com" }
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)
user = Fabricate(:user, email: old_email)

View File

@ -20,6 +20,30 @@ RSpec.describe TopicQuery do
fab!(:moderator)
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
it "filters categories out correctly" do
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!(:topic2) { Fabricate(:topic, created_at: 2.days.ago, bumped_at: 3.hour.ago) }
after { DiscoursePluginRegistry.clear_modifiers! }
it "allows changing" do
original_topic_query = TopicQuery.new(user)
Plugin::Instance
.new
.register_modifier(:topic_query_create_list_topics) do |topics, options, topic_query|
plugin_instance = Plugin::Instance.new
blk =
lambda do |topics, options, topic_query|
expect(topic_query).to eq(topic_query)
topic_query.options[:order] = "created"
topics
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))
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))
end

View File

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

View File

@ -2898,7 +2898,7 @@ RSpec.describe PostMover do
).to eq(true)
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(
original_topic,
Discourse.system_user,
@ -2908,11 +2908,34 @@ RSpec.describe PostMover do
},
).to_topic(destination_topic.id)
moderator_post =
original_topic.reload.ordered_posts.find_by(post_number: second_post.post_number + 1) # the next post
expect(moderator_post).to be_present
expect(moderator_post.post_type).to eq(Post.types[:small_action])
expect(moderator_post.action_code).to eq("split_topic")
# Moderator post is right after the second_post since it was the last post moved
expect(
original_topic.ordered_posts.find_by(
post_number: second_post.post_number + 1,
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
context "with `post_mover_create_moderator_post` modifier" do

View File

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

View File

@ -16,6 +16,8 @@ RSpec.describe SessionController do
end
end
before { SiteSetting.hide_email_address_taken = false }
describe "#email_login_info" do
let(:email_token) do
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.
let(:user_deferred) { Fabricate(:user, refresh_auto_groups: true) }
before { SiteSetting.hide_email_address_taken = false }
describe "#full account registration flow" do
it "will correctly handle honeypot and challenge" do
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
fab!(:other_user) { Fabricate(:user, email: "case.insensitive@gmail.com") }
it "raises an error" do
put "/u/#{user.username}/preferences/email.json",
params: {
email: other_user.email.upcase,
}
expect(response).to_not be_successful
context "when hiding taken e-mails" do
before { SiteSetting.hide_email_address_taken = true }
it "raises an error" do
put "/u/#{user.username}/preferences/email.json",
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

View File

@ -8,7 +8,7 @@ describe "Admin Flags Page", type: :system do
let(:admin_flags_page) { PageObjects::Pages::AdminFlags.new }
let(:admin_flag_form_page) { PageObjects::Pages::AdminFlagForm.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
sign_in(admin)
@ -27,7 +27,7 @@ describe "Admin Flags Page", type: :system do
)
admin_flags_page.visit
expect(admin_header).to be_visible
expect(d_page_header).to be_visible
admin_flags_page.toggle("spam")
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
admin_flags_page.click_add_flag
expect(admin_header).to be_hidden
expect(d_page_header).to be_hidden
admin_flag_form_page
.fill_in_name("Vulgar")
@ -115,7 +114,7 @@ describe "Admin Flags Page", type: :system do
# update
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
expect(admin_flags_page).to have_flags(
@ -158,7 +157,7 @@ describe "Admin Flags Page", type: :system do
it "has settings tab" do
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")],
)

View File

@ -6,11 +6,11 @@ describe "Admin User Fields", type: :system do
before { sign_in(current_user) }
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
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")
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.click_add_field
expect(admin_header).to be_hidden
expect(page_header).to be_hidden
form = page.find(".user-field")
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")
expect(admin_header).to be_hidden
expect(page_header).to be_hidden
form.find(".btn-primary").click

View File

@ -51,6 +51,8 @@ describe "Changing email", type: :system do
end
it "works when user has totp 2fa" do
SiteSetting.hide_email_address_taken = false
second_factor = Fabricate(:user_second_factor_totp, user: 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") }
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)
wait_for(timeout: 5) { ActionMailer::Base.deliveries.count != 0 }

View File

@ -2,17 +2,18 @@
module PageObjects
module Components
# TODO (martin) Delete this after plugins have been updated to use DPageHeader
class AdminHeader < PageObjects::Pages::Base
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
def visible?
has_css?(".admin-page-header")
has_css?(".d-page-header")
end
def hidden?
has_no_css?(".admin-page-header")
has_no_css?(".d-page-header")
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
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

View File

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

View File

@ -221,7 +221,10 @@ shared_examples "signup scenarios" do |signup_page_object, login_page_object|
end
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
signup_form