0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Migrated <GhValidationStatusContainer> to {{validation-status}} modifier

no issue

- moved logic from `<GhValidationStatusContainer>` to a new `validation-status` modifier
  - removes a usage of the `ValidationState` mixin
  - migrated uses of the component to a mixin
  - paves the way for full removal of the `ValidationState` mixin in later refactors (mixins are deprecated)
- migrated `<GhFormGroup>` to a glimmer component
  - swapped the extend of `GhValidationStatusContainer` to usage of the `validation-status` modifier with a template-only component
  - updated all `<GhFormGroup>` to use the standard `class=""` instead of `@classNames=""` and `@class=""`
  - allows `data-test-*` attributes to be added to uses of `<FormGroup>` to help when complex components are grouped as a form input
This commit is contained in:
Kevin Ansfield 2022-12-08 17:04:20 +00:00
parent 34d99c92e0
commit 9fd87f565d
26 changed files with 233 additions and 194 deletions

View file

@ -6,7 +6,7 @@
<div class="modal-body {{if this.authenticationError 'error'}}">
<form id="login" class="login-form" method="post" novalidate="novalidate" {{on "submit" (perform this.reauthenticateTask)}}>
<GhValidationStatusContainer @class="password-wrap" @errors={{this.signup.errors}} @property="password" @hasValidated={{this.signup.hasValidated}}>
<div class="password-wrap" {{validation-status errors=this.signup.errors property="password" hasValidated=this.signup.hasValidated}}>
<input
type="password"
class="gh-input password"
@ -16,7 +16,7 @@
aria-label="Your password"
{{on "input" this.setPassword}}
/>
</GhValidationStatusContainer>
</div>
<div>
<GhTaskButton

View file

@ -7,12 +7,9 @@
<div class="gh-blognav-line {{unless this.name "placeholder"}}">
{{svg-jar "check-2"}}
<GhValidationStatusContainer
@tagName="span"
@class="gh-blognav-label"
@errors={{this.benefitItem.errors}}
@property="name"
@hasValidated={{this.benefitItem.hasValidated}}
<span
class="gh-blognav-label"
{{validation-status errors=this.benefitItem.errors property="name" hasValidated=this.benefitItem.hasValidated}}
>
<GhTrimFocusInput
@shouldFocus={{this.benefitItem.last}}
@ -28,7 +25,7 @@
@errors={{this.benefitItem.errors}}
@property="name"
data-test-error="benefit-label" />
</GhValidationStatusContainer>
</span>
</div>
{{#if this.benefitItem.isNew}}

View file

@ -0,0 +1,7 @@
<div
class="form-group"
{{validation-status errors=@errors property=@property hasValidated=@hasValidated}}
...attributes
>
{{yield}}
</div>

View file

@ -1,7 +0,0 @@
import ValidationStatusContainer from 'ghost-admin/components/gh-validation-status-container';
import classic from 'ember-classic-decorator';
import {classNames} from '@ember-decorators/component';
@classic
@classNames('form-group')
export default class GhFormGroup extends ValidationStatusContainer {}

View file

@ -6,7 +6,7 @@
<div class="gh-main-section-content grey">
<div>
<div class="gh-cp-member-email-name">
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="name" @classNames="max-width">
<GhFormGroup class="max-width" @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="name">
<label for="member-name">Name</label>
<GhTextInput
@id="member-name"
@ -20,7 +20,7 @@
<GhErrorMessage @errors={{this.member.errors}} @property="name" />
</GhFormGroup>
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="email" @classNames="max-width">
<GhFormGroup class="max-width" @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="email">
<label for="member-email">Email</label>
<GhTextInput
@value={{this.scratchMember.email}}
@ -37,7 +37,7 @@
</GhFormGroup>
</div>
<GhFormGroup @classNames="gh-member-labels">
<GhFormGroup class="gh-member-labels">
<label for="label-input">Labels</label>
<GhMemberLabelInput
@onChange={{this.setLabels}}
@ -49,7 +49,7 @@
/>
</GhFormGroup>
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="note" @classNames="mb0 gh-member-note">
<GhFormGroup @errors={{this.member.errors}} @hasValidated={{this.member.hasValidated}} @property="note" class="mb0 gh-member-note">
<label for="member-note">Note <span class="midgrey-d1 fw4">(not visible to member)</span></label>
<GhTextarea
@id="member-note"
@ -65,7 +65,7 @@
{{gh-count-down-characters this.scratchMember.note 500}}</p>
</GhFormGroup>
{{#if this.canShowSingleNewsletter}}
<GhFormGroup @classNames="gh-members-subscribed-checkbox mb0">
<GhFormGroup class="gh-members-subscribed-checkbox mb0">
<div class="flex justify-between items-center">
<div>
<h4 class="gh-setting-title m">Subscribed to newsletter</h4>

View file

@ -80,7 +80,7 @@
</GhFormGroup>
{{#if (eq this.post.visibility "tiers")}}
<GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="tiers" @class="nt3">
<GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="tiers" class="nt3">
<GhPostSettingsMenu::VisibilitySegmentSelect
@tiers={{this.post.tiers}}
@onChange={{action "setVisibility"}}
@ -109,7 +109,7 @@
</GhFormGroup>
{{#unless this.session.user.isAuthorOrContributor}}
<GhFormGroup @class="for-select" @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="authors" data-test-input="authors">
<GhFormGroup class="for-select" @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="authors" data-test-input="authors">
<label for="author-list">Authors</label>
<GhPsmAuthorsInput @selectedAuthors={{this.post.authors}} @updateAuthors={{action "changeAuthors"}} @triggerId="author-list" />
<GhErrorMessage @errors={{this.post.errors}} @property="authors" data-test-error="authors" />

View file

@ -1,28 +0,0 @@
import Component from '@ember/component';
import ValidationStateMixin from 'ghost-admin/mixins/validation-state';
import classic from 'ember-classic-decorator';
import {classNameBindings} from '@ember-decorators/component';
import {computed} from '@ember/object';
/**
* Handles the CSS necessary to show a specific property state. When passed a
* DS.Errors object and a property name, if the DS.Errors object has errors for
* the specified property, it will change the CSS to reflect the error state
* @param {DS.Errors} errors The DS.Errors object
* @param {string} property Name of the property
*/
@classic
@classNameBindings('errorClass')
export default class GhValidationStatusContainer extends Component.extend(ValidationStateMixin) {
@computed('property', 'hasError', 'hasValidated.[]')
get errorClass() {
let hasValidated = this.hasValidated;
let property = this.property;
if (hasValidated && hasValidated.includes(property)) {
return this.hasError ? 'error' : 'success';
} else {
return '';
}
}
}

View file

@ -19,7 +19,7 @@
{{#unless this.membersUtils.isStripeEnabled}}
<button class="gh-btn gh-btn-link {{unless this.session.user.isAdmin "disabled"}}" type="button" {{on "click" (action "openStripeConnect")}}>Connect to Stripe</button>
{{/unless}}
<GhFormGroup @classNames="gh-members-subscribed-checkbox gh-portal-setting-first mb0">
<GhFormGroup class="gh-members-subscribed-checkbox gh-portal-setting-first mb0">
<div class="flex justify-between items-center">
<div class="mr3">
<h4 class="gh-portal-setting-title">Display name in signup form</h4>
@ -153,7 +153,7 @@
</button>
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded" onclick={{action "switchPreviewPage" "signup"}}>
<GhFormGroup @classNames="gh-members-subscribed-checkbox gh-portal-setting-first mb0 b--whitegrey">
<GhFormGroup class="gh-members-subscribed-checkbox gh-portal-setting-first mb0 b--whitegrey">
<div class="flex justify-between items-center">
<h4 class="gh-portal-setting-title">Show Portal button</h4>
<div class="for-switch small">
@ -175,7 +175,7 @@
</GhFormGroup>
{{#if this.settings.portalButton}}
<div class="mt5">
<GhFormGroup @classNames="space-l">
<GhFormGroup class="space-l">
<h4 class="gh-portal-setting-title mb1">Portal button style</h4>
<span
class="gh-select mt2"
@ -195,7 +195,7 @@
</span>
</GhFormGroup>
{{#if this.showIconSetting}}
<GhFormGroup @classNames="space-l">
<GhFormGroup class="space-l">
<h4 class="gh-portal-setting-title">Icon</h4>
<GhUploader
@extensions={{this.iconExtensions}}
@ -251,7 +251,7 @@
{{/if}}
</div>
{{#if this.showButtonTextSetting}}
<GhFormGroup @classNames="space-l">
<GhFormGroup class="space-l">
<h4 class="gh-portal-setting-title">Signup button text</h4>
<div class="flex items-center mt2">
@ -275,7 +275,7 @@
</button>
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded" onclick={{action "switchPreviewPage" "accountHome"}}>
<GhFormGroup @classNames="space-l mt5" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="members_support_address">
<GhFormGroup class="space-l mt5" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="members_support_address">
<h4 class="gh-portal-setting-title">Support email address</h4>
<div class="mt2">
<GhTextInput

View file

@ -11,7 +11,7 @@
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded">
<div class="gh-stack">
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<GhUploader
@extensions={{this.imageExtensions}}
@paramsHash={{hash purpose="image"}}
@ -58,7 +58,7 @@
</GhFormGroup>
{{#if this.settings.icon}}
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<label class="modal-fullsettings-title {{unless this.settings.icon "disabled"}}">Publication icon</label>
<div class="for-switch small {{unless this.settings.icon "disabled"}}">
<label class="switch" for="show-header">
@ -76,7 +76,7 @@
</GhFormGroup>
{{/if}}
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<label class="modal-fullsettings-title">Publication title</label>
<div class="for-switch small">
<label class="switch" for="show-title" data-test-toggle="showHeaderTitle">
@ -91,7 +91,7 @@
</label>
</div>
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<label class="modal-fullsettings-title">Newsletter name</label>
<div class="for-switch small">
<label class="switch" for="show-header-name" data-test-toggle="showHeaderName">
@ -119,7 +119,7 @@
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded">
<div class="gh-stack">
<GhFormGroup @classNames="gh-stack-item">
<GhFormGroup class="gh-stack-item">
<label class="modal-fullsettings-title">Newsletter title style</label>
<div class="gh-email-design-typography-wrapper header">
<div class="modal-fullsettings-radiogroup gh-email-design-typography" data-test-input="titleFontCategory">
@ -134,7 +134,7 @@
</div>
</div>
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item">
<GhFormGroup class="gh-stack-item">
<label class="modal-fullsettings-title">Body style</label>
<div class="gh-email-design-typography-wrapper">
<div class="modal-fullsettings-radiogroup gh-email-design-typography" data-test-input="bodyFontCategory">
@ -145,7 +145,7 @@
</div>
</div>
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<label class="modal-fullsettings-title">Feature image</label>
<div class="for-switch small">
<label class="switch" for="show-feature-image">
@ -175,7 +175,7 @@
<div class="gh-stack">
{{#if (feature "audienceFeedback")}}
<GhFormGroup @classNames="gh-stack-item gh-setting gh-setting-extra">
<GhFormGroup class="gh-stack-item gh-setting gh-setting-extra">
<label for="capture-feedback" class="modal-fullsettings-title" data-test-toggle="feedbackEnabled">Ask your readers for feedback</label>
<div class="for-switch small">
<div class="container">
@ -191,7 +191,7 @@
</GhFormGroup>
{{/if}}
<GhFormGroup @classNames="gh-stack-item">
<GhFormGroup class="gh-stack-item">
<label class="modal-fullsettings-title">Email footer</label>
<KoenigBasicHtmlInput
@name="footer"

View file

@ -10,7 +10,7 @@
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded">
<div class="gh-stack">
<GhFormGroup @classNames="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="name">
<GhFormGroup class="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="name">
<label for="newsletter-title" class="modal-fullsettings-title">Name</label>
<input
id="newsletter-title"
@ -23,7 +23,7 @@
<GhErrorMessage @errors={{@newsletter.errors}} @property="name" />
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="description">
<GhFormGroup class="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="description">
<label for="newsletter-description" class="modal-fullsettings-title">Description</label>
<textarea
id="newsletter-description"
@ -45,7 +45,7 @@
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded">
<div class="gh-stack">
<GhFormGroup @classNames="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderName">
<GhFormGroup class="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderName">
<label for="newsletter-sender-name" class="modal-fullsettings-title">Sender name</label>
<input
id="newsletter-sender-name"
@ -58,7 +58,7 @@
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderName" />
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderEmail">
<GhFormGroup class="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderEmail">
<span class="flex items-center justify-between">
<label for="newsletter-sender-email" class="modal-fullsettings-title ml2">Sender email address</label>
<span class="tooltip-top-left" data-tooltip="Defaults to {{full-email-address "noreply"}} if empty">{{svg-jar "info" class="w4 h4"}}</span>
@ -74,7 +74,7 @@
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderEmail" />
</GhFormGroup>
<GhFormGroup @classNames="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderReplyTo">
<GhFormGroup class="gh-stack-item" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderReplyTo">
<label for="newsletter-reply-to" class="modal-fullsettings-title">Reply-to email</label>
<Inputs::Select
id="newsletter-reply-to"
@ -100,7 +100,7 @@
{{#liquid-if isOpen}}
<div class="modal-fullsettings-tab-expanded">
<div class="gh-stack">
<GhFormGroup @classNames="gh-stack-item gh-setting">
<GhFormGroup class="gh-stack-item gh-setting">
<label for="subscribe-on-signup" class="modal-fullsettings-title" data-test-toggle="subscribeOnSignup">Subscribe new members on signup</label>
<div class="for-switch small">
<div class="container">

View file

@ -29,7 +29,7 @@
<GhErrorMessage @errors={{@data.newsletter.errors}} @property="description" />
</GhFormGroup>
<GhFormGroup @classNames="flex justify-between items-start mb2">
<GhFormGroup class="flex justify-between items-start mb2">
<div class="mr3">
<label for="opt-in-existing" class="modal-fullsettings-title">Opt-in existing subscribers</label>
<p>

View file

@ -3,7 +3,7 @@
<label class="gh-setting-title" for="site-description">Site description</label>
<div class="gh-setting-desc mb3">Used in your theme, meta data and search results</div>
<div class="gh-setting-action" data-test-setting="description">
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="description" @class="description-container">
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="description" class="description-container">
<input
id="site-description"
type="text"

View file

@ -6,13 +6,7 @@
{{/unless}}
<div class="gh-blognav-line">
<GhValidationStatusContainer
@tagName="span"
@class="gh-blognav-label"
@errors={{this.navItem.errors}}
@property="label"
@hasValidated={{this.navItem.hasValidated}}
>
<span class="gh-blognav-label" {{validation-status errors=this.navItem.errors property="label" hasValidated=this.navItem.hasValidated}}>
<GhTrimFocusInput
@shouldFocus={{this.navItem.last}}
@placeholder="Label"
@ -23,14 +17,8 @@
<GhErrorMessage
@errors={{this.navItem.errors}}
@property="label" data-test-error="label" />
</GhValidationStatusContainer>
<GhValidationStatusContainer
@tagName="span"
@class="gh-blognav-url"
@errors={{this.navItem.errors}}
@property="url"
@hasValidated={{this.navItem.hasValidated}}
>
</span>
<span class="gh-blognav-url" {{validation-status errors=this.navItem.errors property="url" hasValidated=this.navItem.hasValidated}}>
<Settings::Navigation::NavItemUrlInput
@baseUrl={{this.baseUrl}}
@isNew={{this.navItem.isNew}}
@ -40,7 +28,7 @@
<GhErrorMessage
@errors={{this.navItem.errors}}
@property="url" data-test-error="url" />
</GhValidationStatusContainer>
</span>
</div>
{{#if this.navItem.isNew}}

View file

@ -88,7 +88,7 @@
<div class="flex flex-column flex">
<GhFormGroup>
<div class="flex items-center">
<GhFormGroup @class="gh-mailgun-region no-margin">
<GhFormGroup class="gh-mailgun-region no-margin">
<label class="fw6 f8">Mailgun region</label>
<div class="mt1">
<PowerSelect
@ -104,7 +104,7 @@
</PowerSelect>
</div>
</GhFormGroup>
<GhFormGroup @class="no-margin">
<GhFormGroup class="no-margin">
<label class="fw6 f8" for="mailgun-domain">Mailgun domain</label>
<input
id="mailgun-domain"

View file

@ -76,7 +76,7 @@
<GhErrorMessage @errors={{@tag.errors}} @property="slug" />
</GhFormGroup>
<GhFormGroup @class="no-margin" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="description">
<GhFormGroup class="no-margin" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="description">
<label for="tag-description">Description</label>
<textarea
id="tag-description"
@ -91,7 +91,7 @@
<p>Maximum: <b>500</b> characters. Youve used {{gh-count-down-characters @tag.description 500}}</p>
</GhFormGroup>
</div>
<GhFormGroup @class="gh-tag-image-uploader no-margin" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="featureImage">
<GhFormGroup class="gh-tag-image-uploader no-margin" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="featureImage">
<label for="tag-image">Tag image</label>
<GhImageUploaderWithPreview
@image={{@tag.featureImage}}
@ -197,7 +197,7 @@
<div class="gh-setting-content-extended">
<div class="gh-twitter-settings">
<div class="gh-twitter-settings-left flex-basis-1-2-m flex-basis-2-3-l">
<GhFormGroup @class="gh-tag-image-uploader" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="twitterImage">
<GhFormGroup class="gh-tag-image-uploader" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="twitterImage">
<label for="twitter-image">Twitter image</label>
<GhImageUploaderWithPreview
@image={{@tag.twitterImage}}
@ -294,7 +294,7 @@
<div class="gh-setting-content-extended">
<div class="gh-og-settings">
<div class="gh-og-settings-left flex-basis-1-2-m flex-basis-2-3-l">
<GhFormGroup @class="gh-tag-image-uploader" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="ogImage">
<GhFormGroup class="gh-tag-image-uploader" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="ogImage">
<label for="og-image">Facebook image</label>
<GhImageUploaderWithPreview
@image={{@tag.ogImage}}
@ -390,7 +390,7 @@
<div class="gh-expandable-content">
{{#liquid-if this.codeInjectionOpen}}
<div class="gh-main-section">
<GhFormGroup @class="gh-main-section-block settings-code" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="codeinjectionHead">
<GhFormGroup class="gh-main-section-block settings-code" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="codeinjectionHead">
<label for="codeinjection-head" class="gh-tag-setting-codeheader">Tag header <code class="fw4 ml1">\{{ghost_head}}</code></label>
<GhCmEditor
@value={{@tag.codeinjectionHead}}
@ -404,7 +404,7 @@
<GhErrorMessage @errors={{@tag.errors}} @property="codeinjectionHead"/>
</GhFormGroup>
<GhFormGroup @class="gh-main-section-block settings-code" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="codeinjectionFoot">
<GhFormGroup class="gh-main-section-block settings-code" @errors={{@tag.errors}} @hasValidated={{@tag.hasValidated}} @property="codeinjectionFoot">
<label for="codeinjection-foot"class="gh-tag-setting-codeheader">Tag footer <code class="fw4 ml1">\{{ghost_foot}}</code></label>
<GhCmEditor @value={{@tag.codeinjectionFoot}}
@id="tag-setting-codeinjection-foot"

View file

@ -6,6 +6,11 @@ import {observer} from '@ember/object';
import {on} from '@ember/object/evented';
import {run} from '@ember/runloop';
/**
* Adds `success` or `error` classes to the element based on the passed
* in `DS.Errors` object, the `property` to inspect, and an array of
* validated property names in `hasValidated`
*/
export default Mixin.create({
errors: null,

View file

@ -0,0 +1,46 @@
import Modifier from 'ember-modifier';
import {isEmpty} from '@ember/utils';
const errorClass = 'error';
const successClass = 'success';
export default class ValidationStatusModifier extends Modifier {
modify(element, positional, {errors, property, hasValidated}) {
const validationClass = this.errorClass(errors, property, hasValidated);
element.classList.remove(errorClass);
element.classList.remove(successClass);
if (validationClass) {
element.classList.add(validationClass);
}
}
errorClass(errors, property, hasValidated) {
const hasError = this.hasError(errors, property, hasValidated);
if (hasValidated && hasValidated.includes(property)) {
return hasError ? errorClass : successClass;
} else {
return '';
}
}
hasError(errors, property, hasValidated) {
// if we aren't looking at a specific property we always want an error class
if (!property && errors && !errors.get('isEmpty')) {
return true;
}
// If we haven't yet validated this field, there is no validation class needed
if (!hasValidated || !hasValidated.includes(property)) {
return false;
}
if (errors && !isEmpty(errors.errorsFor(property))) {
return true;
}
return false;
}
}

View file

@ -271,7 +271,7 @@
<GhErrorMessage @errors={{this.offer.errors}} @property="displayDescription" />
</span>
</GhFormGroup>
<GhFormGroup @errors={{this.errors}} @property="url" @class="gh-offer-url" class="no-margin">
<GhFormGroup @errors={{this.errors}} @property="url" class="gh-offer-url no-margin">
<label for="url" class="fw6">URL</label>
<div class="gh-input-group">
<GhTextInput

View file

@ -42,7 +42,7 @@
<p>The name of your site</p>
</GhFormGroup>
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="description" @class="description-container">
<GhFormGroup @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="description" class="description-container">
<GhTextInput
@value={{readonly this.settings.description}}
@input={{action (mut this.settings.description) value="target.value"}}
@ -419,7 +419,7 @@
A private RSS feed is available at
<a href="{{this.privateRSSUrl}}" target="_blank" rel="noopener noreferrer">{{this.privateRSSUrl}}</a>
</span>
<GhFormGroup @class="no-margin pt2" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="password">
<GhFormGroup class="no-margin pt2" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="password">
<GhTextInput
@value={{readonly this.settings.password}}
@name="general[password]"

View file

@ -64,11 +64,13 @@
</figure>
</div>
<div class="flex-auto">
<GhValidationStatusContainer
@class="flex flex-column w-100 mr3"
@errors={{this.integration.errors}}
@hasValidated={{this.integration.hasValidated}}
@property="name"
<div
class="flex flex-column w-100 mr3"
{{validation-status
errors=this.integration.errors
hasValidated=this.integration.hasValidated
property="name"
}}
>
<label for="integration_name">Name</label>
<GhTextInput
@ -81,13 +83,15 @@
data-test-input="name"
/>
<GhErrorMessage @errors={{this.integration.errors}} @property="name" data-test-error="name" class="ma0" />
</GhValidationStatusContainer>
</div>
<GhValidationStatusContainer
@class="flex flex-column w-100 mr3"
@errors={{this.integration.errors}}
@hasValidated={{this.integration.hasValidated}}
@property="description"
<div
class="flex flex-column w-100 mr3"
{{validation-status
errors=this.integration.errors
hasValidated=this.integration.hasValidated
property="description"
}}
>
<label for="integration_description" class="mt3">Description</label>
<GhTextInput
@ -100,7 +104,7 @@
data-test-input="description"
/>
<GhErrorMessage @errors={{this.integration.errors}} @property="description" data-test-error="description" class="ma0" />
</GhValidationStatusContainer>
</div>
<table class="app-custom-api-table list" style="table-layout: fixed">
<tbody>

View file

@ -62,7 +62,7 @@
<div class="gh-setting-title">Google Analytics Tracking ID</div>
<div class="gh-setting-desc">Tracks AMP traffic in Google Analytics, find your ID <a href="https://ghost.org/help/how-to-find-your-google-analytics-tracking-id/">here</a></div>
<div class="gh-setting-content-extended">
<GhFormGroup @class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="ampGtagId">
<GhFormGroup class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="ampGtagId">
<GhTextInput
@placeholder="UA-XXXXXXX-X"
@name="amp_gtag_id"

View file

@ -62,7 +62,7 @@
<div class="gh-setting-title">FirstPromoter Account ID</div>
<div class="gh-setting-desc"> Affiliate and referral tracking, find your ID <a href="https://ghost.org/help/firstpromoter-id/">here</a></div>
<div class="gh-setting-content-extended">
<GhFormGroup @class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="firstpromoterId">
<GhFormGroup class="no-margin" @errors={{this.settings.errors}} @hasValidated={{this.settings.hasValidated}} @property="firstpromoterId">
<GhTextInput
@placeholder="XXXXXXXX"
@name="firstpromoter_id"

View file

@ -99,7 +99,7 @@
<div class="pa5">
<fieldset class="user-details-bottom">
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="name" @class="first-form-group">
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="name" class="first-form-group">
<label for="user-name">Full name</label>
<input
type="text"
@ -226,7 +226,7 @@
<p>URL of your personal Twitter profile</p>
</GhFormGroup>
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="bio" @class="bio-container">
<GhFormGroup @errors={{this.user.errors}} @hasValidated={{this.user.hasValidated}} @property="bio" class="bio-container">
<label for="user-bio">Bio</label>
<textarea
id="user-bio"

View file

@ -0,0 +1,100 @@
import DS from 'ember-data'; // eslint-disable-line
import EmberObject from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import {expect} from 'chai';
import {find, render} from '@ember/test-helpers';
import {settled} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
const {Errors} = DS;
describe('Integration: Component: gh-form-group', function () {
setupRenderingTest();
beforeEach(function () {
let testObject = EmberObject.create();
testObject.name = 'Test';
testObject.hasValidated = [];
testObject.errors = Errors.create();
this.set('testObject', testObject);
});
// NOTE: primarily testing the validation-status modifier
it('has no success/error class by default', async function () {
await render(hbs`
<GhFormGroup
@property="test"
@errors={{this.testObject.errors}}
@hasValidated={{this.testObject.hasValidated}}
>Testing</GhFormGroup>
`);
expect(find('.form-group')).to.exist;
expect(find('.form-group')).to.not.have.class('success');
expect(find('.form-group')).to.not.have.class('error');
});
it('has success class when valid', async function () {
await render(hbs`
<GhFormGroup
@property="name"
@errors={{this.testObject.errors}}
@hasValidated={{this.testObject.hasValidated}}
>Testing</GhFormGroup>
`);
this.testObject.hasValidated.pushObject('name'); // pushObject vs push because this is an EmberArray and we're testing tracking
await settled();
expect(find('.form-group')).to.have.class('success');
expect(find('.form-group')).to.not.have.class('error');
});
it('has error class when invalid', async function () {
await render(hbs`
<GhFormGroup
@property="name"
@errors={{this.testObject.errors}}
@hasValidated={{this.testObject.hasValidated}}
>Testing</GhFormGroup>
`);
this.testObject.hasValidated.pushObject('name'); // pushObject vs push because this is an EmberArray and we're testing tracking
this.testObject.errors.add('name', 'has error');
await settled();
expect(find('.form-group')).to.have.class('error');
expect(find('.form-group')).to.not.have.class('success');
});
it('still renders if hasValidated is undefined', async function () {
delete this.testObject.hasValidated;
await render(hbs`
<GhFormGroup
@property="name"
@errors={{this.testObject.errors}}
@hasValidated={{this.testObject.hasValidated}}
>Testing</GhFormGroup>
`);
expect(find('.form-group')).to.exist;
});
it('passes element attributes through', async function () {
await render(hbs`
<GhFormGroup
class="custom"
@property="name"
@errors={{this.testObject.errors}}
@hasValidated={{this.testObject.hasValidated}}
data-test-exists="true"
>Testing</GhFormGroup>
`);
expect(find('.form-group')).to.have.class('custom');
expect(find('[data-test-exists="true"]')).to.exist;
});
});

View file

@ -1,73 +0,0 @@
// TODO: remove usage of Ember Data's private `Errors` class when refactoring validations
// eslint-disable-next-line
import DS from 'ember-data';
import EmberObject from '@ember/object';
import hbs from 'htmlbars-inline-precompile';
import {describe, it} from 'mocha';
import {expect} from 'chai';
import {find, render} from '@ember/test-helpers';
import {setupRenderingTest} from 'ember-mocha';
const {Errors} = DS;
describe('Integration: Component: gh-validation-status-container', function () {
setupRenderingTest();
beforeEach(function () {
let testObject = EmberObject.create();
testObject.set('name', 'Test');
testObject.set('hasValidated', []);
testObject.set('errors', Errors.create());
this.set('testObject', testObject);
});
it('has no success/error class by default', async function () {
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.not.have.class('success');
expect(find('.gh-test')).to.not.have.class('error');
});
it('has success class when valid', async function () {
this.get('testObject.hasValidated').push('name');
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.have.class('success');
expect(find('.gh-test')).to.not.have.class('error');
});
it('has error class when invalid', async function () {
this.get('testObject.hasValidated').push('name');
this.get('testObject.errors').add('name', 'has error');
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
expect(find('.gh-test')).to.exist;
expect(find('.gh-test')).to.not.have.class('success');
expect(find('.gh-test')).to.have.class('error');
});
it('still renders if hasValidated is undefined', async function () {
this.set('testObject.hasValidated', undefined);
await render(hbs`
{{#gh-validation-status-container class="gh-test" property="name" errors=testObject.errors hasValidated=testObject.hasValidated}}
{{/gh-validation-status-container}}
`);
expect(find('.gh-test')).to.exist;
});
});

View file

@ -18,10 +18,10 @@ describe('Integration: Component: settings/navigation/nav-item', function () {
await render(hbs`<Settings::Navigation::NavItem @navItem={{this.navItem}} @baseUrl={{this.baseUrl}} />`);
let item = find('.gh-blognav-item');
expect(item.querySelector('.gh-blognav-grab')).to.exist;
expect(item.querySelector('.gh-blognav-label')).to.exist;
expect(item.querySelector('.gh-blognav-url')).to.exist;
expect(item.querySelector('.gh-blognav-delete')).to.exist;
expect(item.querySelector('.gh-blognav-grab'), 'grab').to.exist;
expect(item.querySelector('.gh-blognav-label'), 'label').to.exist;
expect(item.querySelector('.gh-blognav-url'), 'url').to.exist;
expect(item.querySelector('.gh-blognav-delete'), 'delete').to.exist;
// doesn't show any errors
expect(find('.gh-blognav-item--error')).to.not.exist;