Ghost/ghost/admin/app/validators/post.js
Daniël van der Winden 0412decaec
Excerpt form error fix (#20730)
fixes
https://linear.app/tryghost/issue/DES-435/excerpt-in-post-settings-has-an-inconsistent-error-state

The excerpt form field seemed to not be properly handling errors.
However, it was a case of the error styling being overruled by the
regular styling, causing the red border to only show upon `:focus` when
there is an error in the excerpt.

I've rewritten the logic to be slightly less obfuscated and added some
CSS to circumvent the issue.
2024-08-12 09:35:21 +00:00

214 lines
7.4 KiB
JavaScript

import BaseValidator from './base';
import moment from 'moment-timezone';
import validator from 'validator';
import {isBlank, isEmpty, isPresent} from '@ember/utils';
export default BaseValidator.create({
properties: [
'title',
'authors',
'customExcerpt',
'canonicalUrl',
'codeinjectionHead',
'codeinjectionFoot',
'metaTitle',
'metaDescription',
'ogtitle',
'ogDescription',
'twitterTitle',
'twitterDescription',
'publishedAtBlogTime',
'publishedAtBlogDate',
'emailSubject',
'featureImageAlt'
],
title(model) {
if (isBlank(model.title)) {
model.errors.add('title', 'You must specify a title for the post.');
this.invalidate();
}
if (!validator.isLength(model.title || '', 0, 255)) {
model.errors.add('title', 'Title cannot be longer than 255 characters.');
this.invalidate();
}
},
authors(model) {
if (isEmpty(model.authors)) {
model.errors.add('authors', 'At least one author is required.');
this.invalidate();
}
},
canonicalUrl(model) {
let validatorOptions = {require_protocol: true};
let urlRegex = new RegExp(/^(\/|[a-zA-Z0-9-]+:)/);
let url = model.canonicalUrl;
if (isBlank(url)) {
return;
}
if (url.match(/\s/) || (!validator.isURL(url, validatorOptions) && !url.match(urlRegex))) {
model.errors.add('canonicalUrl', 'Please enter a valid URL');
this.invalidate();
} else if (!validator.isLength(model.canonicalUrl, 0, 2000)) {
model.errors.add('canonicalUrl', 'Canonical URL is too long, max 2000 chars');
this.invalidate();
}
},
customExcerpt(model) {
if (!validator.isLength(model.customExcerpt || '', 0, 300)) {
const errorMessage = 'Excerpt cannot be longer than 300 characters.';
model.errors.add('customExcerpt', errorMessage);
this.invalidate();
} else {
model.errors.remove('customExcerpt');
}
},
visibility(model) {
if (isBlank(model.visibility) && !model.isNew) {
model.errors.add('visibility', 'Please select at least one tier');
this.invalidate();
}
},
tiers(model) {
if (model.visibility === 'tiers' && !model.isNew && isEmpty(model.tiers)) {
model.errors.add('tiers', 'Please select at least one tier');
this.invalidate();
}
},
codeinjectionFoot(model) {
if (!validator.isLength(model.codeinjectionFoot || '', 0, 65535)) {
model.errors.add('codeinjectionFoot', 'Footer code cannot be longer than 65535 characters.');
this.invalidate();
}
},
codeinjectionHead(model) {
if (!validator.isLength(model.codeinjectionHead || '', 0, 65535)) {
model.errors.add('codeinjectionHead', 'Header code cannot be longer than 65535 characters.');
this.invalidate();
}
},
metaTitle(model) {
if (!validator.isLength(model.metaTitle || '', 0, 300)) {
model.errors.add('metaTitle', 'Meta Title cannot be longer than 300 characters.');
this.invalidate();
}
},
metaDescription(model) {
if (!validator.isLength(model.metaDescription || '', 0, 500)) {
model.errors.add('metaDescription', 'Meta Description cannot be longer than 500 characters.');
this.invalidate();
}
},
ogTitle(model) {
if (!validator.isLength(model.ogTitle || '', 0, 300)) {
model.errors.add('ogTitle', 'Facebook Title cannot be longer than 300 characters.');
this.invalidate();
}
},
ogDescription(model) {
if (!validator.isLength(model.ogDescription || '', 0, 500)) {
model.errors.add('ogDescription', 'Facebook Description cannot be longer than 500 characters.');
this.invalidate();
}
},
twitterTitle(model) {
if (!validator.isLength(model.twitterTitle || '', 0, 300)) {
model.errors.add('twitterTitle', 'Twitter Title cannot be longer than 300 characters.');
this.invalidate();
}
},
twitterDescription(model) {
if (!validator.isLength(model.twitterDescription || '', 0, 500)) {
model.errors.add('twitterDescription', 'Twitter Description cannot be longer than 500 characters.');
this.invalidate();
}
},
emailSubject(model) {
if (!validator.isLength(model.emailSubject || '', 0, 300)) {
model.errors.add('emailSubject', 'Email Subject cannot be longer than 300 characters.');
this.invalidate();
}
},
// for posts which haven't been published before and where the blog date/time
// is blank we should ignore the validation
_shouldValidatePublishedAtBlog(model) {
return isPresent(model.publishedAtUTC)
|| isPresent(model.publishedAtBlogDate)
|| isPresent(model.publishedAtBlogTime);
},
// convenience method as .validate({property: 'x'}) doesn't accept multiple properties
publishedAtBlog(model) {
this.publishedAtBlogTime(model);
this.publishedAtBlogDate(model);
},
publishedAtBlogTime(model) {
let timeRegex = /^(([0-1]?[0-9])|([2][0-3])):([0-5][0-9])$/;
if (!timeRegex.test(model.publishedAtBlogTime) && this._shouldValidatePublishedAtBlog(model)) {
model.errors.add('publishedAtBlogTime', 'Must be in format: "15:00"');
this.invalidate();
}
},
publishedAtBlogDate(model) {
let publishedAtBlogDate = model.publishedAtBlogDate;
let publishedAtBlogTime = model.publishedAtBlogTime;
if (!this._shouldValidatePublishedAtBlog(model)) {
return;
}
// we have a time string but no date string
if (isBlank(publishedAtBlogDate) && !isBlank(publishedAtBlogTime)) {
model.errors.add('publishedAtBlogDate', 'Can\'t be blank');
return this.invalidate();
}
// don't validate the date if the time format is incorrect
if (isEmpty(model.errors.errorsFor('publishedAtBlogTime'))) {
let status = model.status;
let now = moment();
let publishedAtBlogTZ = model.publishedAtBlogTZ;
let isInFuture = publishedAtBlogTZ.isSameOrAfter(now);
// draft/published must be in past
if ((status === 'draft' || status === 'published') && publishedAtBlogTZ.isSameOrAfter(now)) {
model.errors.add('publishedAtBlogDate', 'Please choose a past date and time.');
this.invalidate();
// scheduled must be in the future when first scheduling
} else if ((model.changedAttributes().status || model.changedAttributes().publishedAtUTC) && status === 'scheduled' && !isInFuture) {
model.errors.add('publishedAtBlogDate', 'Please choose a future date and time.');
this.invalidate();
}
}
},
featureImageAlt(model) {
if (!validator.isLength(model.featureImageAlt || '', 0, 125)) {
model.errors.add('featureImageAlt', 'Feature image alt text cannot be longer than 125 characters.');
this.invalidate();
}
}
});