DEV: Introduce {{body-class}}, soft-deprecate <DSection /> (#23479)

`<DSection />` is now deprecated. Please use `{{body-class "foo-page" "bar"}}` and/or `<section></section>` instead.
This commit is contained in:
Jarek Radosz 2023-09-11 13:44:52 +02:00 committed by GitHub
parent 055d29d898
commit 87d0336f05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1196 additions and 1084 deletions

View File

@ -1,4 +1,4 @@
<DSection @class="award-badge">
<section class="award-badge">
<h1>{{i18n "admin.badges.mass_award.title"}}</h1>
<p>{{i18n "admin.badges.mass_award.description"}}</p>
@ -93,4 +93,4 @@
"admin.badges.mass_award.no_badge_selected"
}}</span>
{{/if}}
</DSection>
</section>

View File

@ -1,4 +1,4 @@
<DSection @class="current-badges">
<section class="current-badges">
<div class="badge-intro admin-intro">
<img
src={{this.badgeIntroEmoji}}
@ -22,4 +22,4 @@
</div>
</div>
</div>
</DSection>
</section>

View File

@ -1,4 +1,4 @@
<DSection @class="current-badge content-body">
<section class="current-badge content-body">
<div class="control-group current-badge__toggle-badge">
<DToggleSwitch
@state={{this.buffered.enabled}}
@ -287,7 +287,7 @@
{{/unless}}
</div>
</form>
</DSection>
</section>
{{#if this.grant_count}}
<div class="content-body current-badge-actions">

View File

@ -269,8 +269,8 @@
{{/if}}
{{#unless this.model.component}}
<DSection
@class="form-horizontal theme settings control-unit theme-settings__color-scheme"
<section
class="form-horizontal theme settings control-unit theme-settings__color-scheme"
>
<div class="row setting">
<div class="setting-label">
@ -305,11 +305,11 @@
{{/if}}
</div>
</div>
</DSection>
</section>
{{/unless}}
{{#if this.model.component}}
<DSection @class="form-horizontal theme settings control-unit">
<section class="form-horizontal theme settings control-unit">
<div class="row setting">
<ThemeSettingRelativesSelector
@setting={{this.relativesSelectorSettingsForComponent}}
@ -317,9 +317,9 @@
@class="theme-setting"
/>
</div>
</DSection>
</section>
{{else}}
<DSection @class="form-horizontal theme settings control-unit">
<section class="form-horizontal theme settings control-unit">
<div class="row setting">
<ThemeSettingRelativesSelector
@setting={{this.relativesSelectorSettingsForTheme}}
@ -327,7 +327,7 @@
@class="theme-setting"
/>
</div>
</DSection>
</section>
{{/if}}
{{#unless this.model.remote_theme.is_git}}
@ -421,7 +421,7 @@
<p><i>{{i18n
"admin.customize.theme.overriden_settings_explanation"
}}</i></p>
<DSection @class="form-horizontal theme settings control-unit">
<section class="form-horizontal theme settings control-unit">
{{#each this.settings as |setting|}}
<ThemeSettingEditor
@setting={{setting}}
@ -429,7 +429,7 @@
@class="theme-setting control-unit"
/>
{{/each}}
</DSection>
</section>
</div>
{{/if}}
@ -438,8 +438,8 @@
<div class="mini-title">{{i18n
"admin.customize.theme.theme_translations"
}}</div>
<DSection
@class="form-horizontal theme settings translations control-unit"
<section
class="form-horizontal theme settings translations control-unit"
>
{{#each this.translations as |translation|}}
<ThemeTranslation
@ -448,7 +448,7 @@
@class="theme-translation"
/>
{{/each}}
</DSection>
</section>
</div>
{{/if}}

View File

@ -1,5 +1,5 @@
{{#if this.filteredContent}}
<DSection @class="form-horizontal settings">
<section class="form-horizontal settings">
{{#each this.filteredContent as |setting|}}
<SiteSetting
@setting={{setting}}
@ -12,7 +12,7 @@
count=this.category.maxResults
}}</p>
{{/if}}
</DSection>
</section>
{{else}}
<br />
{{i18n "admin.site_settings.no_results"}}

View File

@ -1,3 +1 @@
<DSection>
{{outlet}}
</DSection>
<section>{{outlet}}</section>

View File

@ -1,9 +1,8 @@
<DSection
tabIndex="-1"
<section
tabindex="-1"
id={{this.elementId}}
aria-hidden={{not this.isVisible}}
class={{this.HTMLClassList}}
@scrollTop={{false}}
{{did-insert this.registerAppEventListeners}}
{{will-destroy this.deregisterAppEventListeners}}
>
@ -81,4 +80,4 @@
<div class="d-lightbox__focus-trap" tabindex="0"></div>
</div>
{{/if}}
</DSection>
</section>

View File

@ -0,0 +1,36 @@
import Component from "@glimmer/component";
import bodyClass from "discourse/helpers/body-class";
import { concat } from "@ember/helper";
import notEq from "truth-helpers/helpers/not-eq";
import deprecated from "discourse-common/lib/deprecated";
// Can add a body class from within a component
export default class DSection extends Component {
<template>
{{#if @pageClass}}
{{bodyClass (concat @pageClass "-page")}}
{{/if}}
{{#if @bodyClass}}
{{bodyClass @bodyClass}}
{{/if}}
{{#if (notEq @tagName "")}}
<section id={{@id}} class={{@class}} ...attributes>{{yield}}</section>
{{else}}
{{yield}}
{{/if}}
</template>
constructor() {
super(...arguments);
deprecated(
`<DSection> is deprecated. Use {{body-class "foo-page" "bar"}} and/or <section></section> instead.`,
{
since: "3.2.0.beta1",
dropFrom: "3.3.0.beta1",
id: "discourse.d-section",
}
);
}
}

View File

@ -1,47 +0,0 @@
import Component from "@ember/component";
import { scheduleOnce } from "@ember/runloop";
// Can add a body class from within a component
export default class extends Component {
tagName = null;
pageClass = null;
bodyClass = null;
currentClasses = new Set();
didReceiveAttrs() {
this._super(...arguments);
scheduleOnce("afterRender", this, this._updateClasses);
}
willDestroyElement() {
this._super(...arguments);
scheduleOnce("afterRender", this, this._removeClasses);
}
_updateClasses() {
if (this.isDestroying || this.isDestroyed) {
return;
}
const desiredClasses = new Set();
if (this.pageClass) {
desiredClasses.add(`${this.pageClass}-page`);
}
if (this.bodyClass) {
for (const bodyClass of this.bodyClass.split(" ")) {
desiredClasses.add(bodyClass);
}
}
document.body.classList.add(...desiredClasses);
const removeClasses = [...this.currentClasses].filter(
(c) => !desiredClasses.has(c)
);
document.body.classList.remove(...removeClasses);
this.currentClasses = desiredClasses;
}
_removeClasses() {
document.body.classList.remove(...this.currentClasses);
}
}

View File

@ -1,4 +1,6 @@
<DSection @pageClass="has-sidebar" @id="d-sidebar" @class="sidebar-container">
{{body-class "has-sidebar-page"}}
<section id="d-sidebar" class="sidebar-container">
{{#if this.showSwitchPanelButtonsOnTop}}
<Sidebar::SwitchPanelButtons @buttons={{this.switchPanelButtons}} />
{{/if}}
@ -21,4 +23,4 @@
{{/unless}}
<Sidebar::Footer />
</DSection>
</section>

View File

@ -0,0 +1,13 @@
import Helper from "@ember/component/helper";
import { inject as service } from "@ember/service";
export default class BodyClass extends Helper {
@service bodyClasses;
compute([...classes]) {
this.bodyClasses.registerClasses(
this,
classes.flatMap((c) => c?.split(" ")).filter(Boolean)
);
}
}

View File

@ -0,0 +1,39 @@
import Service from "@ember/service";
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
import { registerDestructor } from "@ember/destroyable";
@disableImplicitInjections
export default class BodyClassesService extends Service {
#helpers = new Map();
registerClasses(helper, classes) {
if (this.#helpers.has(helper)) {
const previousClasses = this.#helpers.get(helper);
this.#helpers.set(helper, classes);
this.removeUnusedClasses(previousClasses);
} else {
this.#helpers.set(helper, classes);
registerDestructor(helper, () => {
const previousClasses = this.#helpers.get(helper);
this.#helpers.delete(helper);
this.removeUnusedClasses(previousClasses);
});
}
for (const bodyClass of classes) {
document.body.classList.add(bodyClass);
}
}
removeUnusedClasses(classes) {
const remainingClasses = new Set([...this.#helpers.values()].flat());
for (const bodyClass of classes) {
if (!remainingClasses.has(bodyClass)) {
document.body.classList.remove(bodyClass);
}
}
}
}

View File

@ -1,4 +1,6 @@
<DSection @pageClass="about">
{{body-class "about-page"}}
<section>
<div class="container">
<div class="contents clearfix body-page">
@ -172,4 +174,4 @@
</div>
</div>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @pageClass="badges">
{{body-class "badges-page"}}
<section>
<div class="container badges">
<h1>{{i18n "badges.title"}}</h1>
@ -21,4 +23,4 @@
{{/each}}
</div>
</div>
</DSection>
</section>

View File

@ -1 +0,0 @@
<section>{{yield}}</section>

View File

@ -1,4 +1,6 @@
<DSection @bodyClass="static-{{this.model.path}}" @class="container">
{{body-class (concat "static-" this.model.path)}}
<section class="container">
<WatchRead>
<div class="contents clearfix body-page">
<PluginOutlet @name="above-static" />
@ -6,4 +8,4 @@
<PluginOutlet @name="below-static" />
</div>
</WatchRead>
</DSection>
</section>

View File

@ -2,7 +2,9 @@
{{hide-application-footer}}
{{/if}}
<DSection @pageClass="search" @class="search-container">
{{body-class "search-page"}}
<section class="search-container">
<ScrollTracker
@name="full-page-search"
@tag={{this.searchTerm}}
@ -280,4 +282,4 @@
</div>
{{/if}}
</div>
</DSection>
</section>

View File

@ -2,9 +2,11 @@
{{hide-application-footer}}
{{/if}}
{{body-class "groups-page"}}
<PluginOutlet @name="before-groups-index-container" @connectorTagName="div" />
<DSection @pageClass="groups" @class="container groups-index">
<section class="container groups-index">
<div class="groups-header">
{{#if this.currentUser.can_create_group}}
<DButton
@ -126,7 +128,6 @@
<p role="status">{{i18n "groups.index.empty"}}</p>
{{/if}}
</ConditionalLoadingSpinner>
</DSection>
</section>
<PluginOutlet @name="after-groups-index-container" @connectorTagName="div" />

View File

@ -1,4 +1,6 @@
<DSection @pageClass="groups-new">
{{body-class "groups-new-page"}}
<section>
<h1>{{i18n "admin.groups.new.title"}}</h1>
<hr />
@ -51,4 +53,4 @@
</LinkTo>
</div>
</form>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @pageClass="invite">
{{body-class "invite-page"}}
<section>
<div class="container invites-show clearfix">
<div class="login-welcome-header">
<h1 class="login-title">{{this.welcomeTitle}}</h1>
@ -231,4 +233,4 @@
</div>
</div>
</div>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @bodyClass="static-login" @class="container">
{{body-class "static-login"}}
<section class="container">
<div class="contents clearfix body-page">
<div class="login-welcome">
<PluginOutlet @name="above-login" @outletArgs={{hash model=this.model}} />
@ -29,4 +31,4 @@
</div>
</div>
</div>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @bodyClass="navigation-categories" @class="navigation-container">
{{body-class "navigation-categories"}}
<section class="navigation-container">
<DNavigation
@filterMode="categories"
@showCategoryAdmin={{this.showCategoryAdmin}}
@ -8,4 +10,4 @@
@hasDraft={{this.currentUser.has_topic_draft}}
@createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
/>
</DSection>
</section>

View File

@ -17,7 +17,7 @@
</span>
</section>
<DSection @class="navigation-container category-navigation">
<section class="navigation-container category-navigation">
<DNavigation
@category={{this.category}}
@filterMode={{this.filterMode}}
@ -37,4 +37,4 @@
@connectorTagName="div"
@outletArgs={{hash category=this.category}}
/>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @bodyClass="navigation-topics" @class="navigation-container">
{{body-class "navigation-topics"}}
<section class="navigation-container">
<DNavigation
@filterMode={{this.filterMode}}
@canCreateTopic={{this.canCreateTopic}}
@ -6,4 +8,4 @@
@createTopic={{fn this.composer.openNewTopic (hash preferDraft=true)}}
@skipCategoriesNavItem={{this.skipCategoriesNavItem}}
/>
</DSection>
</section>

View File

@ -1,4 +1,6 @@
<DSection @bodyClass="navigation-filter" @class="navigation-container">
{{body-class "navigation-filter"}}
<section class="navigation-container">
<div class="topic-query-filter">
<div class="topic-query-filter__input">
{{d-icon "filter" class="topic-query-filter__icon"}}
@ -41,4 +43,4 @@
</div>
{{/if}}
</div>
</DSection>
</section>

View File

@ -1,54 +1,53 @@
<DSection @pageClass="user-preferences" @tagName="">
<section class="user-content user-preferences solo-preference">
<form class="form-vertical">
{{#if this.success}}
<div class="alert alert-success">{{this.successMessage}}</div>
<LinkTo @route="preferences.account" class="success-back">
{{d-icon "arrow-left"}}
{{i18n "user.change_email.back_to_preferences"}}
</LinkTo>
{{body-class "user-preferences-page"}}
{{else}}
{{#if this.error}}
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
<div class="control-group">
<label class="control-label">
{{i18n
(if this.new "user.add_email.title" "user.change_email.title")
}}
</label>
<div class="controls">
<TextField
@value={{this.newEmail}}
@id="change-email"
@classNames="input-xxlarge"
@autofocus="autofocus"
/>
<div class="instructions">
{{#if this.taken}}
{{i18n "user.change_email.taken"}}
{{else}}
{{i18n "user.email.instructions"}}
{{/if}}
</div>
<InputTip @validation={{this.emailValidation}} />
</div>
</div>
<div class="controls save-button">
<DButton
@action={{action "saveEmail"}}
@disabled={{this.saveDisabled}}
@translatedLabel={{this.saveButtonText}}
type="submit"
class="btn-primary"
/>
<CancelLink
@route="preferences.account"
@args={{this.model.username}}
/>
</div>
<section class="user-content user-preferences solo-preference">
<form class="form-vertical">
{{#if this.success}}
<div class="alert alert-success">{{this.successMessage}}</div>
<LinkTo @route="preferences.account" class="success-back">
{{d-icon "arrow-left"}}
{{i18n "user.change_email.back_to_preferences"}}
</LinkTo>
{{else}}
{{#if this.error}}
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
</form>
</section>
</DSection>
<div class="control-group">
<label class="control-label">
{{i18n
(if this.new "user.add_email.title" "user.change_email.title")
}}
</label>
<div class="controls">
<TextField
@value={{this.newEmail}}
@id="change-email"
@classNames="input-xxlarge"
@autofocus="autofocus"
/>
<div class="instructions">
{{#if this.taken}}
{{i18n "user.change_email.taken"}}
{{else}}
{{i18n "user.email.instructions"}}
{{/if}}
</div>
<InputTip @validation={{this.emailValidation}} />
</div>
</div>
<div class="controls save-button">
<DButton
@action={{action "saveEmail"}}
@disabled={{this.saveDisabled}}
@translatedLabel={{this.saveButtonText}}
type="submit"
class="btn-primary"
/>
<CancelLink
@route="preferences.account"
@args={{this.model.username}}
/>
</div>
{{/if}}
</form>
</section>

View File

@ -1,213 +1,213 @@
<DSection @pageClass="user-preferences" @tagName="">
<section class="user-content user-preferences solo-preference second-factor">
<ConditionalLoadingSpinner @condition={{this.loading}}>
<form class="form-vertical">
{{#if this.showEnforcedNotice}}
<div class="alert alert-error">{{i18n
"user.second_factor.enforced_notice"
}}</div>
{{/if}}
{{body-class "user-preferences-page"}}
{{#if this.displayOAuthWarning}}
<div class="alert alert-warning">{{i18n
"user.second_factor.oauth_enabled_warning"
}}</div>
{{/if}}
<section class="user-content user-preferences solo-preference second-factor">
<ConditionalLoadingSpinner @condition={{this.loading}}>
<form class="form-vertical">
{{#if this.showEnforcedNotice}}
<div class="alert alert-error">{{i18n
"user.second_factor.enforced_notice"
}}</div>
{{/if}}
{{#if this.errorMessage}}
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
{{#if this.displayOAuthWarning}}
<div class="alert alert-warning">{{i18n
"user.second_factor.oauth_enabled_warning"
}}</div>
{{/if}}
{{#if this.loaded}}
<div class="control-group totp">
<div class="controls">
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
{{#each this.totps as |totp|}}
<div class="second-factor-item row">
<div class="details">
{{#if totp.name}}
{{totp.name}}
{{else}}
{{i18n "user.second_factor.totp.default_name"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<div class="actions">
<TokenBasedAuthDropdown
@totp={{totp}}
@editSecondFactor={{action "editSecondFactor"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
}}
/>
</div>
{{/if}}
</div>
{{/each}}
<DButton
@action={{action "createTotp"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.totp.add"
class="btn-default new-totp"
/>
</div>
</div>
{{#if this.errorMessage}}
<div class="alert alert-error">{{this.errorMessage}}</div>
{{/if}}
<div class="control-group security-key">
<div class="controls">
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
{{#each this.security_keys as |security_key|}}
<div class="second-factor-item row">
<div class="details">
{{#if security_key.name}}
{{security_key.name}}
{{else}}
{{i18n "user.second_factor.security_key.default_name"}}
{{/if}}
</div>
{{#if this.isCurrentUser}}
<div class="actions">
<SecurityKeyDropdown
@securityKey={{security_key}}
@editSecurityKey={{action "editSecurityKey"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
}}
/>
</div>
{{/if}}
</div>
{{/each}}
<DButton
@action={{action "createSecurityKey"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.security_key.add"
class="btn-default new-security-key"
/>
</div>
</div>
<div class="control-group pref-second-factor-backup">
<div class="controls pref-second-factor-backup">
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
{{#if this.loaded}}
<div class="control-group totp">
<div class="controls">
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
{{#each this.totps as |totp|}}
<div class="second-factor-item row">
{{#if this.model.second_factor_enabled}}
<div class="details">
{{#if this.model.second_factor_backup_enabled}}
{{html-safe
(i18n
"user.second_factor_backup.manage"
count=this.model.second_factor_remaining_backup_codes
)
}}
{{else}}
<DButton
@action={{action "editSecondFactorBackup"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor_backup.enable_long"
class="btn-default new-second-factor-backup"
/>
{{/if}}
</div>
{{#if
(and
this.model.second_factor_backup_enabled this.isCurrentUser
)
}}
<div class="actions">
<TwoFactorBackupDropdown
@secondFactorBackupEnabled={{this.model.second_factor_backup_enabled}}
@editSecondFactorBackup={{action
"editSecondFactorBackup"
}}
@disableSecondFactorBackup={{action
"disableSecondFactorBackup"
}}
/>
</div>
<div class="details">
{{#if totp.name}}
{{totp.name}}
{{else}}
{{i18n "user.second_factor.totp.default_name"}}
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
</div>
{{#if this.isCurrentUser}}
<div class="actions">
<TokenBasedAuthDropdown
@totp={{totp}}
@editSecondFactor={{action "editSecondFactor"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
}}
/>
</div>
{{/if}}
</div>
</div>
{{/each}}
<DButton
@action={{action "createTotp"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.totp.add"
class="btn-default new-totp"
/>
</div>
</div>
{{#if this.model.second_factor_enabled}}
{{#unless this.showEnforcedNotice}}
<div class="control-group pref-second-factor-disable-all">
<div class="controls -actions">
<DButton
@icon="ban"
@action={{action "disableAllSecondFactors"}}
@disabled={{this.loading}}
@label="user.second_factor.disable_all"
class="btn-danger"
/>
<CancelLink
@route="preferences.security"
@args={{this.model.username}}
/>
<div class="control-group security-key">
<div class="controls">
<h2>{{i18n "user.second_factor.security_key.title"}}</h2>
{{#each this.security_keys as |security_key|}}
<div class="second-factor-item row">
<div class="details">
{{#if security_key.name}}
{{security_key.name}}
{{else}}
{{i18n "user.second_factor.security_key.default_name"}}
{{/if}}
</div>
</div>
{{/unless}}
{{/if}}
{{else}}
<div class="control-group">
<label class="control-label">{{i18n "user.password.title"}}</label>
<div class="controls">
<div>
<TextField
@value={{this.password}}
@id="password"
@type="password"
@classNames="input-large"
@autofocus="autofocus"
/>
</div>
<div class="instructions">
{{i18n "user.second_factor.confirm_password_description"}}
{{#if this.isCurrentUser}}
<div class="actions">
<SecurityKeyDropdown
@securityKey={{security_key}}
@editSecurityKey={{action "editSecurityKey"}}
@disableSingleSecondFactor={{action
"disableSingleSecondFactor"
}}
/>
</div>
{{/if}}
</div>
{{/each}}
<DButton
@action={{action "createSecurityKey"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor.security_key.add"
class="btn-default new-security-key"
/>
</div>
</div>
<div class="control-group pref-second-factor-backup">
<div class="controls pref-second-factor-backup">
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
<div class="second-factor-item row">
{{#if this.model.second_factor_enabled}}
<div class="details">
{{#if this.model.second_factor_backup_enabled}}
{{html-safe
(i18n
"user.second_factor_backup.manage"
count=this.model.second_factor_remaining_backup_codes
)
}}
{{else}}
<DButton
@action={{action "editSecondFactorBackup"}}
@icon="plus"
@disabled={{this.loading}}
@label="user.second_factor_backup.enable_long"
class="btn-default new-second-factor-backup"
/>
{{/if}}
</div>
{{#if
(and
this.model.second_factor_backup_enabled this.isCurrentUser
)
}}
<div class="actions">
<TwoFactorBackupDropdown
@secondFactorBackupEnabled={{this.model.second_factor_backup_enabled}}
@editSecondFactorBackup={{action
"editSecondFactorBackup"
}}
@disableSecondFactorBackup={{action
"disableSecondFactorBackup"
}}
/>
</div>
{{/if}}
{{else}}
{{i18n "user.second_factor_backup.enable_prerequisites"}}
{{/if}}
</div>
</div>
</div>
<div class="control-group">
<div class="controls -actions">
<DButton
@action={{action "confirmPassword"}}
@disabled={{this.loading}}
@label="continue"
type="submit"
class="btn-primary"
/>
{{#unless this.showEnforcedNotice}}
{{#if this.model.second_factor_enabled}}
{{#unless this.showEnforcedNotice}}
<div class="control-group pref-second-factor-disable-all">
<div class="controls -actions">
<DButton
@icon="ban"
@action={{action "disableAllSecondFactors"}}
@disabled={{this.loading}}
@label="user.second_factor.disable_all"
class="btn-danger"
/>
<CancelLink
@route="preferences.security"
@args={{this.model.username}}
/>
{{/unless}}
</div>
</div>
<div class="controls" style="margin-top: 5px">
{{this.resetPasswordProgress}}
{{#unless this.resetPasswordLoading}}
<a
href
class="instructions"
{{on "click" this.resetPassword}}
>{{i18n "user.second_factor.forgot_password"}}</a>
{{/unless}}
{{/unless}}
{{/if}}
{{else}}
<div class="control-group">
<label class="control-label">{{i18n "user.password.title"}}</label>
<div class="controls">
<div>
<TextField
@value={{this.password}}
@id="password"
@type="password"
@classNames="input-large"
@autofocus="autofocus"
/>
</div>
<div class="instructions">
{{i18n "user.second_factor.confirm_password_description"}}
</div>
</div>
{{/if}}
</form>
</ConditionalLoadingSpinner>
</section>
</DSection>
</div>
<div class="control-group">
<div class="controls -actions">
<DButton
@action={{action "confirmPassword"}}
@disabled={{this.loading}}
@label="continue"
type="submit"
class="btn-primary"
/>
{{#unless this.showEnforcedNotice}}
<CancelLink
@route="preferences.security"
@args={{this.model.username}}
/>
{{/unless}}
</div>
<div class="controls" style="margin-top: 5px">
{{this.resetPasswordProgress}}
{{#unless this.resetPasswordLoading}}
<a
href
class="instructions"
{{on "click" this.resetPassword}}
>{{i18n "user.second_factor.forgot_password"}}</a>
{{/unless}}
</div>
</div>
{{/if}}
</form>
</ConditionalLoadingSpinner>
</section>

View File

@ -1,4 +1,4 @@
<DSection @pageClass="user-preferences" />
{{body-class "user-preferences-page"}}
<div class="user-navigation user-navigation-secondary">
<HorizontalOverflowNav @ariaLabel="User secondary - preferences">

View File

@ -1,7 +1,9 @@
<DSection @bodyClass="static-privacy" @class="container">
{{body-class "static-privacy"}}
<section class="container">
<div class="contents clearfix body-page">
<PluginOutlet @name="above-static" />
{{html-safe this.model.html}}
<PluginOutlet @name="below-static" />
</div>
</DSection>
</section>

View File

@ -2,200 +2,188 @@
{{hide-application-footer}}
{{/if}}
<DSection
@tagName=""
@pageClass="tags"
@bodyClass={{concat
"tag-"
this.tag.id
(if this.category.slug (concat " category-" this.category.slug))
""
(if this.additionalTags " tags-intersection")
}}
>
<div class="container">
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
</div>
{{body-class
"tags-page"
(concat "tag-" this.tag.id)
(if this.category.slug (concat "category-" this.category.slug))
(if this.additionalTags "tags-intersection")
}}
<span>
<PluginOutlet
@name="discovery-list-controls-above"
@connectorTagName="div"
/>
</span>
<div class="container">
<DiscourseBanner @user={{this.currentUser}} @banner={{this.site.banner}} />
</div>
<div class="list-controls">
<PluginOutlet
@name="discovery-navigation-bar-above"
@connectorTagName="div"
/>
<div class="container">
<section class="navigation-container tag-navigation">
<DNavigation
@filterMode={{this.filterMode}}
@canCreateTopic={{this.canCreateTopic}}
@hasDraft={{this.currentUser.has_topic_draft}}
@createTopic={{route-action "createTopic"}}
@category={{this.category}}
@editCategory={{route-action "editCategory" this.category}}
@tag={{this.tag}}
@noSubcategories={{this.noSubcategories}}
@tagNotification={{this.tagNotification}}
@additionalTags={{this.additionalTags}}
@showInfo={{this.showInfo}}
@canCreateTopicOnTag={{this.canCreateTopicOnTag}}
@createTopicDisabled={{this.createTopicDisabled}}
@changeTagNotificationLevel={{action "changeTagNotificationLevel"}}
@toggleInfo={{action "toggleInfo"}}
/>
<span>
<PluginOutlet @name="discovery-list-controls-above" @connectorTagName="div" />
</span>
<PluginOutlet
@name="tag-navigation"
@connectorTagName="div"
@outletArgs={{hash category=this.category tag=this.tag}}
/>
</section>
</div>
</div>
{{#if this.showInfo}}
<TagInfo
@tag={{this.tag}}
@list={{this.list}}
@deleteAction={{action "deleteTag"}}
/>
{{/if}}
<span>
<PluginOutlet
@name="discovery-list-container-top"
@connectorTagName="div"
@outletArgs={{hash category=this.category}}
/>
</span>
<TopicDismissButtons
@position="top"
@selectedTopics={{this.selected}}
@model={{this.model}}
@showResetNew={{this.showResetNew}}
@showDismissRead={{this.showDismissRead}}
@resetNew={{action "resetNew"}}
<div class="list-controls">
<PluginOutlet
@name="discovery-navigation-bar-above"
@connectorTagName="div"
/>
<div class="container">
<section class="navigation-container tag-navigation">
<DNavigation
@filterMode={{this.filterMode}}
@canCreateTopic={{this.canCreateTopic}}
@hasDraft={{this.currentUser.has_topic_draft}}
@createTopic={{route-action "createTopic"}}
@category={{this.category}}
@editCategory={{route-action "editCategory" this.category}}
@tag={{this.tag}}
@noSubcategories={{this.noSubcategories}}
@tagNotification={{this.tagNotification}}
@additionalTags={{this.additionalTags}}
@showInfo={{this.showInfo}}
@canCreateTopicOnTag={{this.canCreateTopicOnTag}}
@createTopicDisabled={{this.createTopicDisabled}}
@changeTagNotificationLevel={{action "changeTagNotificationLevel"}}
@toggleInfo={{action "toggleInfo"}}
/>
<span>
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
</span>
<PluginOutlet
@name="tag-navigation"
@connectorTagName="div"
@outletArgs={{hash category=this.category tag=this.tag}}
/>
</section>
</div>
</div>
<div class="container list-container">
<div class="row">
<div class="full-width">
<PluginOutlet @name="before-list-area" />
<div id="list-area">
{{#unless this.loading}}
<DiscoveryTopicsList
@model={{this.list}}
@refresh={{action "refresh"}}
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
>
{{#if this.top}}
<div class="top-lists">
<PeriodChooser
@period={{this.period}}
@action={{action "changePeriod"}}
@fullDay={{false}}
/>
{{#if this.showInfo}}
<TagInfo
@tag={{this.tag}}
@list={{this.list}}
@deleteAction={{action "deleteTag"}}
/>
{{/if}}
<span>
<PluginOutlet
@name="discovery-list-container-top"
@connectorTagName="div"
@outletArgs={{hash category=this.category}}
/>
</span>
<TopicDismissButtons
@position="top"
@selectedTopics={{this.selected}}
@model={{this.model}}
@showResetNew={{this.showResetNew}}
@showDismissRead={{this.showDismissRead}}
@resetNew={{action "resetNew"}}
/>
<span>
<PluginOutlet @name="discovery-above" @connectorTagName="div" />
</span>
<div class="container list-container">
<div class="row">
<div class="full-width">
<PluginOutlet @name="before-list-area" />
<div id="list-area">
{{#unless this.loading}}
<DiscoveryTopicsList
@model={{this.list}}
@refresh={{action "refresh"}}
@autoAddTopicsToBulkSelect={{this.autoAddTopicsToBulkSelect}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@addTopicsToBulkSelect={{action "addTopicsToBulkSelect"}}
>
{{#if this.top}}
<div class="top-lists">
<PeriodChooser
@period={{this.period}}
@action={{action "changePeriod"}}
@fullDay={{false}}
/>
</div>
{{else}}
{{#if this.topicTrackingState.hasIncoming}}
<div class="show-more {{if this.hasTopics 'has-topics'}}">
<a
tabindex="0"
href
{{on "click" this.showInserted}}
class="alert alert-info clickable"
>
<CountI18n
@key="topic_count_"
@suffix={{this.topicTrackingState.filter}}
@count={{this.topicTrackingState.incomingCount}}
/>
</a>
</div>
{{else}}
{{#if this.topicTrackingState.hasIncoming}}
<div class="show-more {{if this.hasTopics 'has-topics'}}">
<a
tabindex="0"
href
{{on "click" this.showInserted}}
class="alert alert-info clickable"
>
<CountI18n
@key="topic_count_"
@suffix={{this.topicTrackingState.filter}}
@count={{this.topicTrackingState.incomingCount}}
/>
</a>
</div>
{{/if}}
{{/if}}
{{#unless this.bulkSelectEnabled}}
{{#if
(and this.showTopicsAndRepliesToggle this.site.mobileView)
}}
<NewListHeaderControlsWrapper
@current={{this.subset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
@changeNewListSubset={{action "changeNewListSubset"}}
/>
{{/if}}
{{/unless}}
{{#if this.list.topics}}
<TopicList
@topics={{this.list.topics}}
@canBulkSelect={{this.canBulkSelect}}
@toggleBulkSelect={{action "toggleBulkSelect"}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@bulkSelectAction={{action "refresh"}}
@updateAutoAddTopicsToBulkSelect={{action
"updateAutoAddTopicsToBulkSelect"
}}
@selected={{this.selected}}
@category={{this.category}}
@showPosters={{true}}
@order={{this.order}}
@ascending={{this.ascending}}
@changeSort={{action "changeSort"}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{this.subset}}
@changeNewListSubset={{action "changeNewListSubset"}}
{{/if}}
{{#unless this.bulkSelectEnabled}}
{{#if (and this.showTopicsAndRepliesToggle this.site.mobileView)}}
<NewListHeaderControlsWrapper
@current={{this.subset}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
@changeNewListSubset={{action "changeNewListSubset"}}
/>
{{/if}}
</DiscoveryTopicsList>
<footer class="topic-list-bottom">
<TopicDismissButtons
@position="bottom"
@selectedTopics={{this.selected}}
@model={{this.model}}
@showResetNew={{this.showResetNew}}
@showDismissRead={{this.showDismissRead}}
@resetNew={{action "resetNew"}}
{{/unless}}
{{#if this.list.topics}}
<TopicList
@topics={{this.list.topics}}
@canBulkSelect={{this.canBulkSelect}}
@toggleBulkSelect={{action "toggleBulkSelect"}}
@bulkSelectEnabled={{this.bulkSelectEnabled}}
@bulkSelectAction={{action "refresh"}}
@updateAutoAddTopicsToBulkSelect={{action
"updateAutoAddTopicsToBulkSelect"
}}
@selected={{this.selected}}
@category={{this.category}}
@showPosters={{true}}
@order={{this.order}}
@ascending={{this.ascending}}
@changeSort={{action "changeSort"}}
@focusLastVisitedTopic={{true}}
@showTopicsAndRepliesToggle={{this.showTopicsAndRepliesToggle}}
@newListSubset={{this.subset}}
@changeNewListSubset={{action "changeNewListSubset"}}
@newRepliesCount={{this.newRepliesCount}}
@newTopicsCount={{this.newTopicsCount}}
/>
{{/if}}
</DiscoveryTopicsList>
{{#unless this.list.canLoadMore}}
<FooterMessage
@education={{this.footerEducation}}
@message={{this.footerMessage}}
>
{{html-safe
(i18n
"topic.browse_all_tags_or_latest" basePath=(base-path)
)
}}
</FooterMessage>
{{/unless}}
</footer>
{{/unless}}
<footer class="topic-list-bottom">
<TopicDismissButtons
@position="bottom"
@selectedTopics={{this.selected}}
@model={{this.model}}
@showResetNew={{this.showResetNew}}
@showDismissRead={{this.showDismissRead}}
@resetNew={{action "resetNew"}}
/>
<ConditionalLoadingSpinner @condition={{this.list.loadingMore}} />
</div>
{{#unless this.list.canLoadMore}}
<FooterMessage
@education={{this.footerEducation}}
@message={{this.footerMessage}}
>
{{html-safe
(i18n "topic.browse_all_tags_or_latest" basePath=(base-path))
}}
</FooterMessage>
{{/unless}}
</footer>
{{/unless}}
<ConditionalLoadingSpinner @condition={{this.list.loadingMore}} />
</div>
</div>
</div>
</div>
<span>
<PluginOutlet @name="discovery-below" @connectorTagName="div" />
</span>
</DSection>
<span>
<PluginOutlet @name="discovery-below" @connectorTagName="div" />
</span>

View File

@ -1,3 +1,2 @@
<DSection @pageClass="tags" @tagName="">
{{outlet}}
</DSection>
{{body-class "tags-page"}}
{{outlet}}

View File

@ -1,7 +1,9 @@
<DSection @bodyClass="static-tos" @class="container">
{{body-class "static-tos"}}
<section class="container">
<div class="contents clearfix body-page">
<PluginOutlet @name="above-static" />
{{html-safe this.model.html}}
<PluginOutlet @name="below-static" />
</div>
</DSection>
</section>

View File

@ -1,230 +1,226 @@
<DSection @pageClass="user-invites" @tagName="">
{{#if this.canInviteToForum}}
<LoadMore
@class="user-content"
@id="user-content"
@selector=".user-invite-list tr"
@action={{action "loadMore"}}
>
<DSection @class="user-additional-controls">
{{#if this.showSearch}}
<div class="user-invite-search">
<form><TextField
@value={{this.searchTerm}}
@placeholderKey="user.invited.search"
/></form>
</div>
{{body-class "user-invites-page"}}
{{#if this.canInviteToForum}}
<LoadMore
@class="user-content"
@id="user-content"
@selector=".user-invite-list tr"
@action={{action "loadMore"}}
>
<section class="user-additional-controls">
{{#if this.showSearch}}
<div class="user-invite-search">
<form><TextField
@value={{this.searchTerm}}
@placeholderKey="user.invited.search"
/></form>
</div>
{{/if}}
<section class="user-invite-buttons">
<DButton
@icon="plus"
@action={{this.createInvite}}
@label="user.invited.create"
class="btn-default"
/>
{{#if this.canBulkInvite}}
{{#if this.siteSettings.allow_bulk_invite}}
{{#unless this.site.mobileView}}
<DButton
@icon="upload"
@action={{this.createInviteCsv}}
@label="user.invited.bulk_invite.text"
class="btn-flat"
/>
{{/unless}}
{{/if}}
{{/if}}
<DSection @class="user-invite-buttons">
<DButton
@icon="plus"
@action={{this.createInvite}}
@label="user.invited.create"
class="btn-default"
/>
{{#if this.canBulkInvite}}
{{#if this.siteSettings.allow_bulk_invite}}
{{#unless this.site.mobileView}}
<DButton
@icon="upload"
@action={{this.createInviteCsv}}
@label="user.invited.bulk_invite.text"
class="btn-flat"
/>
{{/unless}}
{{/if}}
{{/if}}
{{#if this.showBulkActionButtons}}
{{#if this.inviteExpired}}
{{#if this.removedAll}}
<span class="removed-all">
{{i18n "user.invited.removed_all"}}
</span>
{{else}}
<DButton
@icon="times"
@action={{this.destroyAllExpired}}
@label="user.invited.remove_all"
/>
{{/if}}
{{/if}}
{{#if this.invitePending}}
{{#if this.reinvitedAll}}
<span class="reinvited-all">
<DButton
@icon="check"
@disabled={{true}}
@label="user.invited.reinvited_all"
/>
</span>
{{else if this.hasEmailInvites}}
<DButton
@icon="sync"
@action={{this.reinviteAll}}
@label="user.invited.reinvite_all"
class="btn-default"
/>
{{/if}}
{{/if}}
{{/if}}
</DSection>
</DSection>
<section>
{{#if this.model.invites}}
{{#if this.inviteRedeemed}}
<table class="table user-invite-list">
<thead>
<tr>
<th>{{i18n "user.invited.user"}}</th>
<th>{{i18n "user.invited.redeemed_at"}}</th>
{{#if this.model.can_see_invite_details}}
<th>{{i18n "user.last_seen"}}</th>
<th>{{i18n "user.invited.topics_entered"}}</th>
<th>{{i18n "user.invited.posts_read_count"}}</th>
<th>{{i18n "user.invited.time_read"}}</th>
<th>{{i18n "user.invited.days_visited"}}</th>
<th>{{i18n "user.invited.invited_via"}}</th>
{{/if}}
</tr>
</thead>
<tbody>
{{#each this.model.invites as |invite|}}
<tr>
<td>
<LinkTo @route="user" @model={{invite.user}}>{{avatar
invite.user
imageSize="tiny"
}}</LinkTo>
<LinkTo
@route="user"
@model={{invite.user}}
>{{invite.user.username}}</LinkTo>
</td>
<td>{{format-date invite.redeemed_at}}</td>
{{#if this.model.can_see_invite_details}}
<td>{{format-date invite.user.last_seen_at}}</td>
<td>{{number invite.user.topics_entered}}</td>
<td>{{number invite.user.posts_read_count}}</td>
<td>{{format-duration invite.user.time_read}}</td>
<td>
<span
title={{i18n "user.invited.days_visited"}}
>{{html-safe invite.user.days_visited}}</span>
/
<span
title={{i18n "user.invited.account_age_days"}}
>{{html-safe invite.user.days_since_created}}</span>
</td>
<td>{{html-safe invite.invite_source}}</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<table class="table user-invite-list">
<thead>
<tr>
<th>{{i18n "user.invited.invited_via"}}</th>
<th>{{i18n "user.invited.sent"}}</th>
<th>{{i18n "user.invited.expires_at"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.model.invites as |invite|}}
<tr>
<td class="invite-type">
<div class="label">{{i18n
"user.invited.invited_via"
}}</div>
{{#if invite.email}}
{{d-icon "envelope"}}
{{invite.email}}
{{else}}
{{d-icon "link"}}
{{i18n
"user.invited.invited_via_link"
key=invite.shortKey
count=invite.redemption_count
max=invite.max_redemptions_allowed
}}
{{/if}}
{{#each invite.groups as |g|}}
<p class="invite-extra"><a href="/g/{{g.name}}">{{d-icon
"users"
}}
{{g.name}}</a></p>
{{/each}}
{{#if invite.topic}}
<p class="invite-extra"><a
href={{invite.topic.url}}
>{{d-icon "file"}} {{invite.topic.title}}</a></p>
{{/if}}
</td>
<td class="invite-updated-at">
<div class="label">{{i18n "user.invited.sent"}}</div>
{{format-date invite.updated_at}}
</td>
<td class="invite-expires-at">
<div class="label">{{i18n
"user.invited.expires_at"
}}</div>
{{#if this.inviteExpired}}
{{raw-date invite.expires_at}}
{{else if invite.expired}}
{{i18n "user.invited.expired"}}
{{else}}
{{raw-date invite.expires_at}}
{{/if}}
</td>
{{#if invite.can_delete_invite}}
<td class="invite-actions">
<DButton
@icon="pencil-alt"
@action={{fn this.editInvite invite}}
@title="user.invited.edit"
class="btn-default"
/>
<DButton
@icon="trash-alt"
@action={{fn this.destroyInvite invite}}
@title={{if
invite.destroyed
"user.invited.removed"
"user.invited.remove"
}}
class="cancel"
/>
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
<ConditionalLoadingSpinner @condition={{this.invitesLoading}} />
{{else}}
<div class="user-invite-none">
{{#if this.canBulkInvite}}
{{html-safe (i18n "user.invited.bulk_invite.none")}}
{{#if this.showBulkActionButtons}}
{{#if this.inviteExpired}}
{{#if this.removedAll}}
<span class="removed-all">
{{i18n "user.invited.removed_all"}}
</span>
{{else}}
{{i18n "user.invited.none"}}
<DButton
@icon="times"
@action={{this.destroyAllExpired}}
@label="user.invited.remove_all"
/>
{{/if}}
</div>
{{/if}}
{{#if this.invitePending}}
{{#if this.reinvitedAll}}
<span class="reinvited-all">
<DButton
@icon="check"
@disabled={{true}}
@label="user.invited.reinvited_all"
/>
</span>
{{else if this.hasEmailInvites}}
<DButton
@icon="sync"
@action={{this.reinviteAll}}
@label="user.invited.reinvite_all"
class="btn-default"
/>
{{/if}}
{{/if}}
{{/if}}
</section>
</LoadMore>
{{else}}
<div class="alert alert-error invite-error">
{{this.model.error}}
</div>
{{/if}}
</DSection>
</section>
<section>
{{#if this.model.invites}}
{{#if this.inviteRedeemed}}
<table class="table user-invite-list">
<thead>
<tr>
<th>{{i18n "user.invited.user"}}</th>
<th>{{i18n "user.invited.redeemed_at"}}</th>
{{#if this.model.can_see_invite_details}}
<th>{{i18n "user.last_seen"}}</th>
<th>{{i18n "user.invited.topics_entered"}}</th>
<th>{{i18n "user.invited.posts_read_count"}}</th>
<th>{{i18n "user.invited.time_read"}}</th>
<th>{{i18n "user.invited.days_visited"}}</th>
<th>{{i18n "user.invited.invited_via"}}</th>
{{/if}}
</tr>
</thead>
<tbody>
{{#each this.model.invites as |invite|}}
<tr>
<td>
<LinkTo @route="user" @model={{invite.user}}>{{avatar
invite.user
imageSize="tiny"
}}</LinkTo>
<LinkTo
@route="user"
@model={{invite.user}}
>{{invite.user.username}}</LinkTo>
</td>
<td>{{format-date invite.redeemed_at}}</td>
{{#if this.model.can_see_invite_details}}
<td>{{format-date invite.user.last_seen_at}}</td>
<td>{{number invite.user.topics_entered}}</td>
<td>{{number invite.user.posts_read_count}}</td>
<td>{{format-duration invite.user.time_read}}</td>
<td>
<span
title={{i18n "user.invited.days_visited"}}
>{{html-safe invite.user.days_visited}}</span>
/
<span
title={{i18n "user.invited.account_age_days"}}
>{{html-safe invite.user.days_since_created}}</span>
</td>
<td>{{html-safe invite.invite_source}}</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<table class="table user-invite-list">
<thead>
<tr>
<th>{{i18n "user.invited.invited_via"}}</th>
<th>{{i18n "user.invited.sent"}}</th>
<th>{{i18n "user.invited.expires_at"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{#each this.model.invites as |invite|}}
<tr>
<td class="invite-type">
<div class="label">{{i18n "user.invited.invited_via"}}</div>
{{#if invite.email}}
{{d-icon "envelope"}}
{{invite.email}}
{{else}}
{{d-icon "link"}}
{{i18n
"user.invited.invited_via_link"
key=invite.shortKey
count=invite.redemption_count
max=invite.max_redemptions_allowed
}}
{{/if}}
{{#each invite.groups as |g|}}
<p class="invite-extra"><a href="/g/{{g.name}}">{{d-icon
"users"
}}
{{g.name}}</a></p>
{{/each}}
{{#if invite.topic}}
<p class="invite-extra"><a
href={{invite.topic.url}}
>{{d-icon "file"}} {{invite.topic.title}}</a></p>
{{/if}}
</td>
<td class="invite-updated-at">
<div class="label">{{i18n "user.invited.sent"}}</div>
{{format-date invite.updated_at}}
</td>
<td class="invite-expires-at">
<div class="label">{{i18n "user.invited.expires_at"}}</div>
{{#if this.inviteExpired}}
{{raw-date invite.expires_at}}
{{else if invite.expired}}
{{i18n "user.invited.expired"}}
{{else}}
{{raw-date invite.expires_at}}
{{/if}}
</td>
{{#if invite.can_delete_invite}}
<td class="invite-actions">
<DButton
@icon="pencil-alt"
@action={{fn this.editInvite invite}}
@title="user.invited.edit"
class="btn-default"
/>
<DButton
@icon="trash-alt"
@action={{fn this.destroyInvite invite}}
@title={{if
invite.destroyed
"user.invited.removed"
"user.invited.remove"
}}
class="cancel"
/>
</td>
{{/if}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
<ConditionalLoadingSpinner @condition={{this.invitesLoading}} />
{{else}}
<div class="user-invite-none">
{{#if this.canBulkInvite}}
{{html-safe (i18n "user.invited.bulk_invite.none")}}
{{else}}
{{i18n "user.invited.none"}}
{{/if}}
</div>
{{/if}}
</section>
</LoadMore>
{{else}}
<div class="alert alert-error invite-error">
{{this.model.error}}
</div>
{{/if}}

View File

@ -1,5 +1,5 @@
{{#if this.can_see_invite_details}}
<DSection @pageClass="user-invites" />
{{body-class "user-invites-page"}}
<div class="user-navigation user-navigation-secondary">
<HorizontalOverflowNav @ariaLabel="User secondary - invites">

View File

@ -9,7 +9,7 @@
{{if this.model.profile_hidden 'profile-hidden'}}
{{this.primaryGroup}}"
>
<DSection @class="user-main">
<section class="user-main">
<a href="#user-content" id="skip-link" class="skip-link__user-nav">
{{i18n "skip_user_nav"}}
</a>
@ -474,5 +474,5 @@
{{outlet}}
</div>
</div>
</DSection>
</section>
</div>

View File

@ -1,4 +1,4 @@
<DSection @pageClass="user-activity" />
{{body-class "user-activity-page"}}
<div class="user-navigation user-navigation-secondary">
<HorizontalOverflowNav @ariaLabel="User secondary - activity">

View File

@ -1,4 +1,6 @@
<DSection @pageClass="user-badges" @class="user-content" id="user-content">
{{body-class "user-badges-page"}}
<section class="user-content" id="user-content">
<p class="favorite-count">
{{i18n
"badges.favorite_count"
@ -25,4 +27,4 @@
@outletArgs={{hash user=this.user.model}}
/>
</div>
</DSection>
</section>

View File

@ -1,4 +1,4 @@
<DSection @pageClass="user-messages" />
{{body-class "user-messages-page"}}
<PluginOutlet
@name="user-messages-above-navigation"

View File

@ -2,7 +2,7 @@
{{hide-application-footer}}
{{/if}}
<DSection @pageClass="user-notifications" />
{{body-class "user-notifications-page"}}
<div class="user-navigation user-navigation-secondary">
<HorizontalOverflowNav @ariaLabel="User secondary - notifications">

View File

@ -1,306 +1,300 @@
<DSection @pageClass="user-summary" @tagName="">
<div class="user-content" id="user-content">
<PluginOutlet
@name="above-user-summary-stats"
@outletArgs={{hash model=this.model user=this.user}}
/>
{{#if this.model.can_see_summary_stats}}
<div class="top-section stats-section">
<h3 class="stats-title">{{i18n "user.summary.stats"}}</h3>
<ul>
<li class="stats-days-visited">
{{body-class "user-summary-page"}}
<div class="user-content" id="user-content">
<PluginOutlet
@name="above-user-summary-stats"
@outletArgs={{hash model=this.model user=this.user}}
/>
{{#if this.model.can_see_summary_stats}}
<div class="top-section stats-section">
<h3 class="stats-title">{{i18n "user.summary.stats"}}</h3>
<ul>
<li class="stats-days-visited">
<UserStat
@value={{this.model.days_visited}}
@label="user.summary.days_visited"
/>
</li>
<li class="stats-time-read">
<UserStat
@value={{this.timeRead}}
@label="user.summary.time_read"
@rawTitle={{i18n
"user.summary.time_read_title"
duration=this.timeReadMedium
}}
@type="string"
/>
</li>
{{#if this.showRecentTimeRead}}
<li class="stats-recent-read">
<UserStat
@value={{this.model.days_visited}}
@label="user.summary.days_visited"
/>
</li>
<li class="stats-time-read">
<UserStat
@value={{this.timeRead}}
@label="user.summary.time_read"
@value={{this.recentTimeRead}}
@label="user.summary.recent_time_read"
@rawTitle={{i18n
"user.summary.time_read_title"
duration=this.timeReadMedium
"user.summary.recent_time_read_title"
duration=this.recentTimeReadMedium
}}
@type="string"
/>
</li>
{{#if this.showRecentTimeRead}}
<li class="stats-recent-read">
<UserStat
@value={{this.recentTimeRead}}
@label="user.summary.recent_time_read"
@rawTitle={{i18n
"user.summary.recent_time_read_title"
duration=this.recentTimeReadMedium
}}
@type="string"
/>
</li>
{{/if}}
<li class="stats-topics-entered">
{{/if}}
<li class="stats-topics-entered">
<UserStat
@value={{this.model.topics_entered}}
@label="user.summary.topics_entered"
/>
</li>
<li class="stats-posts-read">
<UserStat
@value={{this.model.posts_read_count}}
@label="user.summary.posts_read"
/>
</li>
<li class="stats-likes-given linked-stat">
<LinkTo @route="userActivity.likesGiven">
<UserStat
@value={{this.model.topics_entered}}
@label="user.summary.topics_entered"
/>
</li>
<li class="stats-posts-read">
<UserStat
@value={{this.model.posts_read_count}}
@label="user.summary.posts_read"
/>
</li>
<li class="stats-likes-given linked-stat">
<LinkTo @route="userActivity.likesGiven">
<UserStat
@value={{this.model.likes_given}}
@icon="heart"
@label="user.summary.likes_given"
/>
</LinkTo>
</li>
<li class="stats-likes-received">
<UserStat
@value={{this.model.likes_received}}
@value={{this.model.likes_given}}
@icon="heart"
@label="user.summary.likes_received"
@label="user.summary.likes_given"
/>
</LinkTo>
</li>
<li class="stats-likes-received">
<UserStat
@value={{this.model.likes_received}}
@icon="heart"
@label="user.summary.likes_received"
/>
</li>
{{#if this.model.bookmark_count}}
<li class="stats-bookmark-count linked-stat">
<LinkTo @route="userActivity.bookmarks">
<UserStat
@value={{this.model.bookmark_count}}
@label="user.summary.bookmark_count"
/>
</LinkTo>
</li>
{{#if this.model.bookmark_count}}
<li class="stats-bookmark-count linked-stat">
<LinkTo @route="userActivity.bookmarks">
<UserStat
@value={{this.model.bookmark_count}}
@label="user.summary.bookmark_count"
/>
</LinkTo>
{{/if}}
<li class="stats-topic-count linked-stat">
<LinkTo @route="userActivity.topics">
<UserStat
@value={{this.model.topic_count}}
@label="user.summary.topic_count"
/>
</LinkTo>
</li>
<li class="stats-post-count linked-stat">
<LinkTo @route="userActivity.replies">
<UserStat
@value={{this.model.post_count}}
@label="user.summary.post_count"
/>
</LinkTo>
</li>
<PluginOutlet
@name="user-summary-stat"
@connectorTagName="li"
@outletArgs={{hash model=this.model user=this.user}}
/>
</ul>
</div>
{{/if}}
<PluginOutlet
@name="below-user-summary-stats"
@outletArgs={{hash model=this.model user=this.user}}
/>
<div class="top-section">
<UserSummarySection @title="top_replies" @class="replies-section pull-left">
<UserSummaryTopicsList
@type="replies"
@items={{this.model.replies}}
@user={{this.user}}
as |reply|
>
<UserSummaryTopic
@createdAt={{reply.createdAt}}
@topic={{reply.topic}}
@likes={{reply.like_count}}
@url={{reply.url}}
/>
</UserSummaryTopicsList>
</UserSummarySection>
<UserSummarySection @title="top_topics" @class="topics-section pull-right">
<UserSummaryTopicsList
@type="topics"
@items={{this.model.topics}}
@user={{this.user}}
as |topic|
>
<UserSummaryTopic
@createdAt={{topic.created_at}}
@topic={{topic}}
@likes={{topic.like_count}}
@url={{topic.url}}
/>
</UserSummaryTopicsList>
</UserSummarySection>
</div>
<div class="top-section">
<UserSummarySection @title="top_links" @class="links-section pull-left">
{{#if this.model.links.length}}
<ul>
{{#each this.model.links as |link|}}
<li>
{{! template-lint-disable link-rel-noopener }}
<a
class="domain"
href={{link.url}}
title={{link.title}}
rel="noopener {{unless
this.user.removeNoFollow
'nofollow ugc'
}}"
target="_blank"
>
{{shorten-url link.url}}
</a>
{{! template-lint-enable link-rel-noopener }}
<span
class="badge badge-notification clicks"
title={{i18n "topic_map.clicks" count=link.clicks}}
>
{{number link.clicks}}
</span>
<br />
<a href={{link.post_url}}>
{{html-safe link.topic.fancyTitle}}
</a>
</li>
{{/if}}
<li class="stats-topic-count linked-stat">
<LinkTo @route="userActivity.topics">
<UserStat
@value={{this.model.topic_count}}
@label="user.summary.topic_count"
/>
</LinkTo>
</li>
<li class="stats-post-count linked-stat">
<LinkTo @route="userActivity.replies">
<UserStat
@value={{this.model.post_count}}
@label="user.summary.post_count"
/>
</LinkTo>
</li>
{{/each}}
</ul>
{{else}}
<p>{{i18n "user.summary.no_links"}}</p>
{{/if}}
</UserSummarySection>
<UserSummarySection
@title="most_replied_to_users"
@class="summary-user-list replied-section pull-right"
>
<UserSummaryUsersList
@none="no_replies"
@users={{this.model.most_replied_to_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="reply" @countClass="replies" />
</UserSummaryUsersList>
</UserSummarySection>
</div>
<div class="top-section most-liked-section">
<UserSummarySection
@title="most_liked_by"
@class="summary-user-list liked-by-section pull-left"
>
<UserSummaryUsersList
@none="no_likes"
@users={{this.model.most_liked_by_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="heart" @countClass="likes" />
</UserSummaryUsersList>
</UserSummarySection>
<UserSummarySection
@title="most_liked_users"
@class="summary-user-list liked-section pull-right"
>
<UserSummaryUsersList
@none="no_likes"
@users={{this.model.most_liked_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="heart" @countClass="likes" />
</UserSummaryUsersList>
</UserSummarySection>
</div>
{{#if this.model.top_categories.length}}
<div class="top-section top-categories-section">
<UserSummarySection
@title="top_categories"
@class="summary-category-list pull-left"
>
<table>
<thead>
<th class="category-link"></th>
<th class="topic-count">{{i18n "user.summary.topics"}}</th>
<th class="reply-count">{{i18n "user.summary.replies"}}</th>
</thead>
<tbody>
{{#each this.model.top_categories as |category|}}
<tr>
<td class="category-link">
{{category-link
category
allowUncategorized="true"
hideParent=false
}}
</td>
<td class="topic-count">
<UserSummaryCategorySearch
@user={{this.user}}
@category={{category}}
@count={{category.topic_count}}
/>
</td>
<td class="reply-count">
<UserSummaryCategorySearch
@user={{this.user}}
@category={{category}}
@count={{category.post_count}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</UserSummarySection>
</div>
{{/if}}
{{#if this.siteSettings.enable_badges}}
<div class="top-section badges-section">
<h3 class="stats-title">{{i18n "user.summary.top_badges"}}</h3>
{{#if this.model.badges}}
<div class="badge-group-list">
{{#each this.model.badges as |badge|}}
<BadgeCard
@badge={{badge}}
@count={{badge.count}}
@username={{this.user.username_lower}}
/>
{{/each}}
<PluginOutlet
@name="user-summary-stat"
@connectorTagName="li"
@name="after-user-summary-badges"
@outletArgs={{hash model=this.model user=this.user}}
/>
</ul>
</div>
{{/if}}
</div>
{{else}}
<p>{{i18n "user.summary.no_badges"}}</p>
{{/if}}
<PluginOutlet
@name="below-user-summary-stats"
@outletArgs={{hash model=this.model user=this.user}}
/>
<div class="top-section">
<UserSummarySection
@title="top_replies"
@class="replies-section pull-left"
>
<UserSummaryTopicsList
@type="replies"
@items={{this.model.replies}}
@user={{this.user}}
as |reply|
>
<UserSummaryTopic
@createdAt={{reply.createdAt}}
@topic={{reply.topic}}
@likes={{reply.like_count}}
@url={{reply.url}}
/>
</UserSummaryTopicsList>
</UserSummarySection>
<UserSummarySection
@title="top_topics"
@class="topics-section pull-right"
>
<UserSummaryTopicsList
@type="topics"
@items={{this.model.topics}}
@user={{this.user}}
as |topic|
>
<UserSummaryTopic
@createdAt={{topic.created_at}}
@topic={{topic}}
@likes={{topic.like_count}}
@url={{topic.url}}
/>
</UserSummaryTopicsList>
</UserSummarySection>
{{#if this.moreBadges}}
<LinkTo @route="user.badges" @model={{this.user}} class="more">
{{i18n "user.summary.more_badges"}}
</LinkTo>
{{/if}}
</div>
<div class="top-section">
<UserSummarySection @title="top_links" @class="links-section pull-left">
{{#if this.model.links.length}}
<ul>
{{#each this.model.links as |link|}}
<li>
{{! template-lint-disable link-rel-noopener }}
<a
class="domain"
href={{link.url}}
title={{link.title}}
rel="noopener {{unless
this.user.removeNoFollow
'nofollow ugc'
}}"
target="_blank"
>
{{shorten-url link.url}}
</a>
{{! template-lint-enable link-rel-noopener }}
<span
class="badge badge-notification clicks"
title={{i18n "topic_map.clicks" count=link.clicks}}
>
{{number link.clicks}}
</span>
<br />
<a href={{link.post_url}}>
{{html-safe link.topic.fancyTitle}}
</a>
</li>
{{/each}}
</ul>
{{else}}
<p>{{i18n "user.summary.no_links"}}</p>
{{/if}}
</UserSummarySection>
<UserSummarySection
@title="most_replied_to_users"
@class="summary-user-list replied-section pull-right"
>
<UserSummaryUsersList
@none="no_replies"
@users={{this.model.most_replied_to_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="reply" @countClass="replies" />
</UserSummaryUsersList>
</UserSummarySection>
</div>
<div class="top-section most-liked-section">
<UserSummarySection
@title="most_liked_by"
@class="summary-user-list liked-by-section pull-left"
>
<UserSummaryUsersList
@none="no_likes"
@users={{this.model.most_liked_by_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="heart" @countClass="likes" />
</UserSummaryUsersList>
</UserSummarySection>
<UserSummarySection
@title="most_liked_users"
@class="summary-user-list liked-section pull-right"
>
<UserSummaryUsersList
@none="no_likes"
@users={{this.model.most_liked_users}}
as |user|
>
<UserSummaryUser @user={{user}} @icon="heart" @countClass="likes" />
</UserSummaryUsersList>
</UserSummarySection>
</div>
{{#if this.model.top_categories.length}}
<div class="top-section top-categories-section">
<UserSummarySection
@title="top_categories"
@class="summary-category-list pull-left"
>
<table>
<thead>
<th class="category-link"></th>
<th class="topic-count">{{i18n "user.summary.topics"}}</th>
<th class="reply-count">{{i18n "user.summary.replies"}}</th>
</thead>
<tbody>
{{#each this.model.top_categories as |category|}}
<tr>
<td class="category-link">
{{category-link
category
allowUncategorized="true"
hideParent=false
}}
</td>
<td class="topic-count">
<UserSummaryCategorySearch
@user={{this.user}}
@category={{category}}
@count={{category.topic_count}}
/>
</td>
<td class="reply-count">
<UserSummaryCategorySearch
@user={{this.user}}
@category={{category}}
@count={{category.post_count}}
/>
</td>
</tr>
{{/each}}
</tbody>
</table>
</UserSummarySection>
</div>
{{/if}}
{{#if this.siteSettings.enable_badges}}
<div class="top-section badges-section">
<h3 class="stats-title">{{i18n "user.summary.top_badges"}}</h3>
{{#if this.model.badges}}
<div class="badge-group-list">
{{#each this.model.badges as |badge|}}
<BadgeCard
@badge={{badge}}
@count={{badge.count}}
@username={{this.user.username_lower}}
/>
{{/each}}
<PluginOutlet
@name="after-user-summary-badges"
@outletArgs={{hash model=this.model user=this.user}}
/>
</div>
{{else}}
<p>{{i18n "user.summary.no_badges"}}</p>
{{/if}}
{{#if this.moreBadges}}
<LinkTo @route="user.badges" @model={{this.user}} class="more">
{{i18n "user.summary.more_badges"}}
</LinkTo>
{{/if}}
</div>
{{/if}}
</div>
</DSection>
{{/if}}
</div>

View File

@ -2,7 +2,8 @@
{{hide-application-footer}}
{{/if}}
<DSection @pageClass="users">
{{body-class "users-page"}}
<section>
<LoadMore
@selector=".directory-table .directory-table__cell"
@action={{action "loadMore"}}
@ -86,4 +87,4 @@
</div>
</div>
</LoadMore>
</DSection>
</section>

View File

@ -10,5 +10,6 @@ globalThis.deprecationWorkflow.config = {
matchId: "ember-this-fallback.this-property-fallback",
},
{ handler: "silence", matchId: "discourse.select-kit" },
{ handler: "silence", matchId: "discourse.d-section" },
],
};

View File

@ -1,28 +1,35 @@
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";
import { hbs } from "ember-cli-htmlbars";
import { test } from "qunit";
import { visit } from "@ember/test-helpers";
import { render } from "@ember/test-helpers";
import { withSilencedDeprecationsAsync } from "discourse-common/lib/deprecated";
import Component from "@ember/component";
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
acceptance("Plugin Outlet - Deprecated parentView", function (needs) {
needs.hooks.beforeEach(function () {
registerTemporaryModule(
"discourse/templates/connectors/user-profile-primary/hello",
hbs`<span class='hello-username'>{{this.parentView.parentView.class}}</span>`
);
});
module("Plugin Outlet - Deprecated parentView", function (hooks) {
setupRenderingTest(hooks);
test("Can access parentView", async function (assert) {
this.component = class AComponent extends Component {
layout = hbs`<PluginOutlet @name="an-outlet" @connectorTagName="div" />`;
};
registerTemporaryModule(
"discourse/templates/connectors/an-outlet/hello",
hbs`<span class="hello-username">{{this.parentView.parentView.constructor.name}}</span>`
);
test("Can access parentview", async function (assert) {
await withSilencedDeprecationsAsync(
"discourse.plugin-outlet-parent-view",
async () => {
await visit("/u/eviltrout");
assert.strictEqual(
query(".hello-username").innerText,
"user-main",
"it renders a value from parentView.parentView"
);
await render(hbs`<this.component />`);
assert
.dom(".hello-username")
.hasText(
"AComponent",
"it renders a value from parentView.parentView"
);
}
);
});

View File

@ -0,0 +1,19 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render } from "@ember/test-helpers";
import DSection from "discourse/components/d-section";
module("Integration | Component | d-section", function (hooks) {
setupRenderingTest(hooks);
test("can set classes on the body element", async function (assert) {
await render(<template>
<DSection @pageClass="test" @bodyClass="foo bar" class="special">
testing!
</DSection>
</template>);
assert.dom(".special").hasText("testing!");
assert.strictEqual(document.body.className, "test-page foo bar");
});
});

View File

@ -0,0 +1,42 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { render, settled } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
module("Integration | Helper | body-class", function (hooks) {
setupRenderingTest(hooks);
test("A single class", async function (assert) {
await render(hbs`{{body-class "foo"}}`);
assert.true(document.body.classList.contains("foo"));
});
test("Multiple classes", async function (assert) {
this.set("bar", "bar");
await render(hbs`{{body-class "baz" this.bar}}`);
assert.true(document.body.classList.contains("baz"));
assert.true(document.body.classList.contains("bar"));
});
test("Empty classes", async function (assert) {
const classesBefore = document.body.className;
await render(hbs`{{body-class (if false "not-really")}}`);
assert.strictEqual(document.body.className, classesBefore);
});
test("Dynamic classes", async function (assert) {
this.set("dynamic", "bar");
await render(hbs`{{body-class this.dynamic}}`);
assert.true(document.body.classList.contains("bar"), "has .bar");
this.set("dynamic", "baz");
await settled();
assert.true(document.body.classList.contains("baz"), "has .baz");
assert.false(
document.body.classList.contains("bar"),
"does not have .bar anymore"
);
});
});

View File

@ -14,7 +14,7 @@
</StyleguideExample>
<StyleguideExample @title=".user-navigation .nav-stacked" class="half-size">
<DSection @class="user-navigation">
<section class="user-navigation">
<MobileNav
@class="preferences-nav"
@desktopClass="preferences-list action-list nav-stacked"
@ -27,5 +27,5 @@
</li>
{{/each}}
</MobileNav>
</DSection>
</section>
</StyleguideExample>

View File

@ -1,7 +1,7 @@
<StyleguideExample @title="navigation">
<div class="list-controls">
<div class="container">
<DSection @class="navigation-container">
<section class="navigation-container">
<BreadCrumbs @categories={{@dummy.categories}} />
<NavigationBar @navItems={{@dummy.navItems}} @filterMode="latest" />
@ -9,7 +9,7 @@
<CategoriesAdminDropdown />
<CreateTopicButton @canCreateTopic={{true}} />
</div>
</DSection>
</section>
</div>
</div>
</StyleguideExample>

View File

@ -1,5 +1,5 @@
<StyleguideExample @title=".user-main .about.collapsed-info.no-background">
<DSection @class="user-main">
<section class="user-main">
<section class="collapsed-info about no-background">
<div class="profile-image"></div>
@ -41,11 +41,11 @@
<div style="clear: both"></div>
</div>
</section>
</DSection>
</section>
</StyleguideExample>
<StyleguideExample @title=".user-main .about.collapsed-info.has-background">
<DSection @class="user-main">
<section class="user-main">
<section
class="collapsed-info about has-background"
style={{@dummy.user.profileBackground}}
@ -89,11 +89,11 @@
<div style="clear: both"></div>
</div>
</section>
</DSection>
</section>
</StyleguideExample>
<StyleguideExample @title=".user-main .about.no-background">
<DSection @class="user-main">
<section class="user-main">
<section class="about no-background">
<div class="staff-counters">
@ -244,11 +244,11 @@
</dl>
</div>
</section>
</DSection>
</section>
</StyleguideExample>
<StyleguideExample @title=".user-main .about.has-background">
<DSection @class="user-main">
<section class="user-main">
<section
class="about has-background"
style={{@dummy.user.profileBackground}}
@ -399,5 +399,5 @@
</dl>
</div>
</section>
</DSection>
</section>
</StyleguideExample>