Merge branch 'main' into main
This commit is contained in:
commit
5ea5c35932
@ -39,7 +39,7 @@
|
||||
"dependencies": {
|
||||
"@codemirror/lang-html": "6.4.9",
|
||||
"@tryghost/color-utils": "0.2.2",
|
||||
"@tryghost/kg-unsplash-selector": "0.1.17",
|
||||
"@tryghost/kg-unsplash-selector": "0.2.0",
|
||||
"@tryghost/limit-service": "1.2.14",
|
||||
"@tryghost/nql": "0.12.3",
|
||||
"@tryghost/timezone-data": "0.4.3",
|
||||
|
@ -63,14 +63,22 @@ const features = [{
|
||||
title: 'Internal Linking (private beta)',
|
||||
description: 'Adds internal URL search to editor link inputs',
|
||||
flag: 'internalLinking'
|
||||
},{
|
||||
title: 'Internal Linking @-links (internal alpha)',
|
||||
description: 'Adds internal URL search when typing @ in the editor',
|
||||
flag: 'internalLinkingAtLinks'
|
||||
},{
|
||||
title: 'ActivityPub',
|
||||
description: '(Highly) Experimental support for ActivityPub.',
|
||||
flag: 'ActivityPub'
|
||||
},{
|
||||
title: 'Subhead',
|
||||
description: 'Using custom excerpts as subheads in editor and newsletter',
|
||||
flag: 'subhead'
|
||||
title: 'Subtitle in editor',
|
||||
description: 'Using custom excerpt as subtitle in editor',
|
||||
flag: 'editorSubtitle'
|
||||
},{
|
||||
title: 'Subtitle in newsletter',
|
||||
description: 'Showing subtitle in newsletter',
|
||||
flag: 'newsletterSubtitle'
|
||||
}];
|
||||
|
||||
const AlphaFeatures: React.FC = () => {
|
||||
|
@ -103,7 +103,7 @@ const Sidebar: React.FC<{
|
||||
const {mutateAsync: uploadImage} = useUploadImage();
|
||||
const [selectedTab, setSelectedTab] = useState('generalSettings');
|
||||
const hasEmailCustomization = useFeatureFlag('emailCustomization');
|
||||
const hasSubhead = useFeatureFlag('subhead');
|
||||
const hasNewsletterSubtitle = useFeatureFlag('newsletterSubtitle');
|
||||
const {localSettings} = useSettingGroup();
|
||||
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
|
||||
const handleError = useHandleError();
|
||||
@ -417,6 +417,22 @@ const Sidebar: React.FC<{
|
||||
value={newsletter.title_color}
|
||||
onChange={color => updateNewsletter({title_color: color})}
|
||||
/>}
|
||||
<ToggleGroup gap='lg'>
|
||||
{(hasNewsletterSubtitle && newsletter.show_post_title_section) &&
|
||||
<Toggle
|
||||
checked={newsletter.show_subhead}
|
||||
direction="rtl"
|
||||
label='Subtitle'
|
||||
onChange={e => updateNewsletter({show_subhead: e.target.checked})}
|
||||
/>
|
||||
}
|
||||
<Toggle
|
||||
checked={newsletter.show_feature_image}
|
||||
direction="rtl"
|
||||
label='Feature image'
|
||||
onChange={e => updateNewsletter({show_feature_image: e.target.checked})}
|
||||
/>
|
||||
</ToggleGroup>
|
||||
<Select
|
||||
options={fontOptions}
|
||||
selectedOption={fontOptions.find(option => option.value === newsletter.body_font_category)}
|
||||
@ -424,20 +440,6 @@ const Sidebar: React.FC<{
|
||||
title='Body style'
|
||||
onSelect={option => updateNewsletter({body_font_category: option?.value})}
|
||||
/>
|
||||
{hasSubhead &&
|
||||
<Toggle
|
||||
checked={newsletter.show_subhead}
|
||||
direction="rtl"
|
||||
label='Subhead'
|
||||
onChange={e => updateNewsletter({show_subhead: e.target.checked})}
|
||||
/>
|
||||
}
|
||||
<Toggle
|
||||
checked={newsletter.show_feature_image}
|
||||
direction="rtl"
|
||||
label='Feature image'
|
||||
onChange={e => updateNewsletter({show_feature_image: e.target.checked})}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
<Form className='mt-6' gap='sm' margins='lg' title='Footer'>
|
||||
|
@ -77,7 +77,7 @@ const NewsletterPreviewContent: React.FC<{
|
||||
const showHeader = headerIcon || headerTitle;
|
||||
const {config} = useGlobalData();
|
||||
const hasNewEmailAddresses = useFeatureFlag('newEmailAddresses');
|
||||
const hasSubhead = useFeatureFlag('subhead');
|
||||
const hasNewsletterSubtitle = useFeatureFlag('newsletterSubtitle');
|
||||
|
||||
const currentDate = new Date().toLocaleDateString('default', {
|
||||
year: 'numeric',
|
||||
@ -115,11 +115,11 @@ const NewsletterPreviewContent: React.FC<{
|
||||
<div className="border border-transparent px-16" style={{borderColor}}>
|
||||
{headerImage && (
|
||||
<div>
|
||||
<img alt="" className="mt-6 block" src={headerImage} />
|
||||
<img alt="" className="mb-4 mt-6 block" src={headerImage} />
|
||||
</div>
|
||||
)}
|
||||
{showHeader && (
|
||||
<div className="border-b border-grey-200 py-8" style={{borderColor: secondaryBorderColor}}>
|
||||
<div className="py-3" style={{borderColor: secondaryBorderColor}}>
|
||||
{headerIcon && <img alt="" className="mx-auto mb-2 h-10 w-10" role="presentation" src={headerIcon} />}
|
||||
{headerTitle && <h4 className="mb-1 text-center text-[1.6rem] font-bold uppercase leading-tight tracking-tight text-grey-900" style={{color: textColor}}>{headerTitle}</h4>}
|
||||
{headerSubtitle && <h5 className="mb-1 text-center text-md font-normal text-grey-700" style={{color: secondaryTextColor}}>{headerSubtitle}</h5>}
|
||||
@ -134,8 +134,8 @@ const NewsletterPreviewContent: React.FC<{
|
||||
)} style={{color: titleColor}}>
|
||||
Your email newsletter
|
||||
</h2>
|
||||
{(hasSubhead && showSubhead) && (
|
||||
<p className="mb-4 text-[1.6rem] leading-[1.7] text-black">A subhead that highlights the key points of your newsletter.</p>
|
||||
{(hasNewsletterSubtitle && showSubhead) && (
|
||||
<p className="mb-5 text-pretty text-[1.6rem] leading-[1.7] text-black">A subtitle can help highlight key points and engage your readers.</p>
|
||||
)}
|
||||
<div className={clsx(
|
||||
'flex w-full justify-between text-center text-md leading-none text-grey-700',
|
||||
@ -153,14 +153,21 @@ const NewsletterPreviewContent: React.FC<{
|
||||
{/* Feature image */}
|
||||
{showFeatureImage && (
|
||||
<>
|
||||
<div className="h-[300px] w-full max-w-[600px] bg-grey-200 bg-cover bg-no-repeat">
|
||||
<div className={clsx(
|
||||
'h-[300px] w-full max-w-[600px] bg-cover bg-no-repeat',
|
||||
showPostTitleSection ? '' : 'pt-6'
|
||||
)}>
|
||||
<img alt="Feature" className='min-h-full min-w-full shrink-0' src={CoverImage} />
|
||||
</div>
|
||||
<div className="mt-1 w-full max-w-[600px] pb-8 text-center text-[1.3rem] text-grey-700" style={{color: secondaryTextColor}}>Feature image caption</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={clsx('max-w-[600px] border-b border-grey-200 pb-5 text-[1.6rem] leading-[1.7] text-black', bodyFontCategory === 'serif' && 'font-serif')} style={{borderColor: secondaryBorderColor}}>
|
||||
<div className={clsx(
|
||||
'max-w-[600px] border-b border-grey-200 pb-5 text-[1.6rem] leading-[1.7] text-black',
|
||||
bodyFontCategory === 'serif' && 'font-serif',
|
||||
(showFeatureImage || showPostTitleSection) ? '' : 'pt-8'
|
||||
)} style={{borderColor: secondaryBorderColor}}>
|
||||
<p className="mb-5" style={{color: textColor}}>This is what your content will look like when you send one of your posts as an email newsletter to your subscribers.</p>
|
||||
<p className="mb-5" style={{color: textColor}}>Over there on the right you'll see some settings that allow you to customize the look and feel of this template to make it perfectly suited to your brand. Email templates are exceptionally finnicky to make, but we've spent a long time optimising this one to make it work beautifully across devices, email clients and content types.</p>
|
||||
<p className="mb-5" style={{color: textColor}}>So, you can trust that every email you send with Ghost will look great and work well. Just like the rest of your site.</p>
|
||||
|
@ -261,13 +261,14 @@ export function getFreeProduct({
|
||||
}
|
||||
|
||||
export function getBenefits({numOfBenefits}) {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.floor(Math.random() * 100);
|
||||
|
||||
const beenfits = [
|
||||
getBenefitData({name: `Limited early adopter pricing #${random}`}),
|
||||
getBenefitData({name: `Latest gear reviews #${random}`}),
|
||||
getBenefitData({name: `Weekly email newsletter #${random}`}),
|
||||
getBenefitData({name: `Listen to my podcast #${random}`})
|
||||
getBenefitData({name: `Limited early adopter pricing #${random}-${timestamp}`}),
|
||||
getBenefitData({name: `Latest gear reviews #${random}-${timestamp}`}),
|
||||
getBenefitData({name: `Weekly email newsletter #${random}-${timestamp}`}),
|
||||
getBenefitData({name: `Listen to my podcast #$${random}-${timestamp}`})
|
||||
];
|
||||
return beenfits.slice(0, numOfBenefits);
|
||||
}
|
||||
|
@ -57,10 +57,10 @@
|
||||
data-test-editor-title-input={{true}}
|
||||
/>
|
||||
|
||||
{{#if (feature 'subhead')}}
|
||||
{{#if (feature 'editorSubtitle')}}
|
||||
<GhTextarea
|
||||
@class="gh-editor-subhead"
|
||||
@placeholder="Add a subhead..."
|
||||
@class="gh-editor-subtitle"
|
||||
@placeholder="Add a post excerpt..."
|
||||
@shouldFocus={{false}}
|
||||
@tabindex="1"
|
||||
@autoExpand=".gh-koenig-editor"
|
||||
|
@ -117,10 +117,10 @@ export default class GhKoenigEditorReactComponent extends Component {
|
||||
// - Enter, creating an empty paragraph when editor is not empty
|
||||
@action
|
||||
onTitleKeydown(event) {
|
||||
if (this.feature.get('subhead')) {
|
||||
if (this.feature.get('editorSubtitle')) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
const subheadElement = document.querySelector('.gh-editor-subhead');
|
||||
const subheadElement = document.querySelector('.gh-editor-subtitle');
|
||||
if (subheadElement) {
|
||||
subheadElement.focus();
|
||||
}
|
||||
@ -150,7 +150,7 @@ export default class GhKoenigEditorReactComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Subheader ("excerpt") Actions -------------------------------------------
|
||||
// Subhead ("excerpt") Actions -------------------------------------------
|
||||
|
||||
@action
|
||||
updateExcerpt(event) {
|
||||
|
@ -93,7 +93,7 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#unless (feature 'subhead')}}
|
||||
{{#unless (feature 'editorSubtitle')}}
|
||||
<GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="customExcerpt">
|
||||
<label for="custom-excerpt">Excerpt</label>
|
||||
<GhTextarea
|
||||
|
@ -374,10 +374,18 @@ export default class KoenigLexicalEditor extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// only published posts/pages have URLs
|
||||
// only published posts/pages and staff with posts have URLs
|
||||
const filteredResults = [];
|
||||
results.forEach((group) => {
|
||||
const items = (group.groupName === 'Posts' || group.groupName === 'Pages') ? group.options.filter(i => i.status === 'published') : group.options;
|
||||
let items = group.options;
|
||||
|
||||
if (group.groupName === 'Posts' || group.groupName === 'Pages') {
|
||||
items = items.filter(i => i.status === 'published');
|
||||
}
|
||||
|
||||
if (group.groupName === 'Staff') {
|
||||
items = items.filter(i => !/\/404\//.test(i.url));
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
@ -417,7 +425,8 @@ export default class KoenigLexicalEditor extends Component {
|
||||
feature: {
|
||||
collectionsCard: this.feature.collectionsCard,
|
||||
collections: this.feature.collections,
|
||||
internalLinking: this.feature.internalLinking
|
||||
internalLinking: this.feature.internalLinking,
|
||||
internalLinkingAtLinks: this.feature.internalLinkingAtLinks
|
||||
},
|
||||
deprecated: {
|
||||
headerV1: true // if false, shows header v1 in the menu
|
||||
|
@ -83,7 +83,9 @@ export default class FeatureService extends Service {
|
||||
@feature('onboardingChecklist') onboardingChecklist;
|
||||
@feature('ActivityPub') ActivityPub;
|
||||
@feature('internalLinking') internalLinking;
|
||||
@feature('subhead') subhead;
|
||||
@feature('internalLinkingAtLinks') internalLinkingAtLinks;
|
||||
@feature('editorSubtitle') editorSubtitle;
|
||||
@feature('newsletterSubtitle') newsletterSubtitle;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
@ -744,6 +744,11 @@ input:focus,
|
||||
|
||||
/* Editor */
|
||||
|
||||
.gh-editor-title,
|
||||
.gh-editor-subtitle {
|
||||
background: var(--white);
|
||||
}
|
||||
|
||||
.gh-editor-title::placeholder {
|
||||
color: var(--midlightgrey-d2);
|
||||
}
|
||||
|
@ -788,7 +788,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
.gh-editor-subhead {
|
||||
.gh-editor-subtitle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: unset;
|
||||
@ -806,7 +806,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.gh-editor-subhead:focus {
|
||||
.gh-editor-subtitle:focus {
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
@ -45,9 +45,9 @@
|
||||
"@tryghost/color-utils": "0.2.2",
|
||||
"@tryghost/ember-promise-modals": "2.0.1",
|
||||
"@tryghost/helpers": "1.1.90",
|
||||
"@tryghost/kg-clean-basic-html": "4.0.9",
|
||||
"@tryghost/kg-clean-basic-html": "4.1.0",
|
||||
"@tryghost/kg-converters": "1.0.4",
|
||||
"@tryghost/koenig-lexical": "1.1.16",
|
||||
"@tryghost/koenig-lexical": "1.2.2",
|
||||
"@tryghost/limit-service": "1.2.14",
|
||||
"@tryghost/members-csv": "0.0.0",
|
||||
"@tryghost/nql": "0.12.3",
|
||||
|
@ -52,7 +52,9 @@ const ALPHA_FEATURES = [
|
||||
'importMemberTier',
|
||||
'lexicalIndicators',
|
||||
'adminXDemo',
|
||||
'subhead'
|
||||
'editorSubtitle',
|
||||
'newsletterSubtitle',
|
||||
'internalLinkingAtLinks'
|
||||
];
|
||||
|
||||
module.exports.GA_KEYS = [...GA_FEATURES];
|
||||
|
@ -102,9 +102,9 @@
|
||||
"@tryghost/kg-converters": "1.0.4",
|
||||
"@tryghost/kg-default-atoms": "5.0.3",
|
||||
"@tryghost/kg-default-cards": "10.0.5",
|
||||
"@tryghost/kg-default-nodes": "1.0.16",
|
||||
"@tryghost/kg-html-to-lexical": "1.0.17",
|
||||
"@tryghost/kg-lexical-html-renderer": "1.0.16",
|
||||
"@tryghost/kg-default-nodes": "1.1.0",
|
||||
"@tryghost/kg-html-to-lexical": "1.1.1",
|
||||
"@tryghost/kg-lexical-html-renderer": "1.1.1",
|
||||
"@tryghost/kg-mobiledoc-html-renderer": "7.0.4",
|
||||
"@tryghost/limit-service": "1.2.14",
|
||||
"@tryghost/link-redirects": "0.0.0",
|
||||
@ -207,7 +207,7 @@
|
||||
"moment": "2.24.0",
|
||||
"moment-timezone": "0.5.23",
|
||||
"multer": "1.4.4",
|
||||
"mysql2": "3.9.8",
|
||||
"mysql2": "3.9.9",
|
||||
"nconf": "0.12.1",
|
||||
"node-jose": "2.2.0",
|
||||
"path-match": "1.2.4",
|
||||
@ -225,7 +225,7 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sentry/profiling-node": "7.116.0",
|
||||
"@tryghost/html-to-mobiledoc": "3.0.10",
|
||||
"@tryghost/html-to-mobiledoc": "3.1.0",
|
||||
"sqlite3": "5.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -169,7 +169,11 @@ describe('Can send cards via email', function () {
|
||||
'extended-text', // not a card
|
||||
'extended-quote', // not a card
|
||||
'extended-heading', // not a card
|
||||
'tk' // shouldn't be present in published posts / emails
|
||||
// not a card and shouldn't be present in published posts / emails
|
||||
'tk',
|
||||
'at-link',
|
||||
'at-link-search',
|
||||
'zwnj'
|
||||
];
|
||||
|
||||
const cardsInDefaultNodes = DEFAULT_NODES.map((node) => {
|
||||
|
@ -6,9 +6,16 @@ let tiers = {};
|
||||
let models = {};
|
||||
const crypto = require('crypto');
|
||||
|
||||
// @ts-check
|
||||
/**
|
||||
* @typedef {"customers" | "checkout" | "subscriptions" | "coupons" | "payment_methods" | "prices" | "setup_intents" | "products"} resource
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Stripe Mocker mimics an in memory version of the Stripe API. We can use it to quickly create new subscriptions and get a close to real world test environment with working webhooks etc.
|
||||
* If you create a new subscription, it will automatically send the customer.subscription.created webhook. So you can test what happens.
|
||||
*
|
||||
* If you create a new subscription, it will by default send the checkout.session.completed webhook.
|
||||
* If you update a subscription, it will by default send the customer.subscription.updated webhook.
|
||||
*/
|
||||
class StripeMocker {
|
||||
customers = [];
|
||||
@ -49,10 +56,19 @@ class StripeMocker {
|
||||
models = require('../../core/server/models');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random ID.
|
||||
* @returns {string} The generated random ID.
|
||||
*/
|
||||
#generateRandomId() {
|
||||
return crypto.randomBytes(8).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a mock Stripe customer.
|
||||
* @param {object} overrides - Optional overrides for the customer object.
|
||||
* @returns {object} - The created Stripe customer object.
|
||||
*/
|
||||
createCustomer(overrides = {}) {
|
||||
const customerId = `cus_${this.#generateRandomId()}`;
|
||||
const stripeCustomer = {
|
||||
@ -71,10 +87,10 @@ class StripeMocker {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} tierSlug
|
||||
* Fetches a mock Stripe price for a given product/tier (by slug).
|
||||
* @param {string} tierSlug
|
||||
* @param {'month' | 'year'} cadence
|
||||
* @returns
|
||||
* @returns {Promise<object>} - The fetched Stripe price object.
|
||||
*/
|
||||
async getPriceForTier(tierSlug, cadence) {
|
||||
const product = await models.Product.findOne({slug: tierSlug});
|
||||
@ -89,13 +105,13 @@ class StripeMocker {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a Ghost tier (product).
|
||||
* @param {object} data
|
||||
* @param {string} [data.name]
|
||||
* @param {string} data.currency
|
||||
* @param {number} data.monthly_price
|
||||
* @param {number} data.yearly_price
|
||||
* @returns
|
||||
* @returns {Promise<object>} - The created product object.
|
||||
*/
|
||||
async createTier({name, currency, monthly_price, yearly_price}) {
|
||||
const result = await tiers.api.add({
|
||||
@ -110,6 +126,15 @@ class StripeMocker {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a trial subscription.
|
||||
*
|
||||
* @param {object} options - The options for creating the trial subscription.
|
||||
* @param {string} options.customer - The customer ID.
|
||||
* @param {string} options.price - The price ID.
|
||||
* @param {object} [options.overrides] - Optional overrides for the trial subscription.
|
||||
* @returns {Promise<object>} - The created trial subscription object.
|
||||
*/
|
||||
async createTrialSubscription({customer, price, ...overrides}) {
|
||||
return await this.createSubscription({
|
||||
customer,
|
||||
@ -143,6 +168,17 @@ class StripeMocker {
|
||||
await DomainEvents.allSettled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a subscription for a customer.
|
||||
*
|
||||
* @param {object} data - The data for creating the subscription.
|
||||
* @param {object} data.customer - The customer object.
|
||||
* @param {object} data.price - The price object.
|
||||
* @param {object} [data.overrides] - Optional overrides for the subscription object.
|
||||
* @param {object} options - The options for creating the subscription.
|
||||
* @param {object} [options.sendWebhook=true] - Whether to send a webhook or not.
|
||||
* @returns {Promise<Object>} The created subscription object.
|
||||
*/
|
||||
async createSubscription({customer, price, ...overrides}, options = {sendWebhook: true}) {
|
||||
const subscriptionId = `sub_${this.#generateRandomId()}`;
|
||||
|
||||
@ -188,6 +224,14 @@ class StripeMocker {
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves requested data from the in-memory storage similar to a HTTP request.
|
||||
*
|
||||
* @param {Array} arr - The array to search in.
|
||||
* @param {string} id - The id to search for.
|
||||
* @returns {Array} - An array containing the HTTP status code and the setup intent object.
|
||||
* If the setup intent is not found, returns [404].
|
||||
*/
|
||||
#getData(arr, id) {
|
||||
const setupIntent = arr.find(c => c.id === id);
|
||||
if (!setupIntent) {
|
||||
@ -196,6 +240,15 @@ class StripeMocker {
|
||||
return [200, setupIntent];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creation and updating of in memory resources.
|
||||
*
|
||||
* @param {Array} arr - The array to store the processed data.
|
||||
* @param {string} id - The ID of the resource.
|
||||
* @param {string} body - The body of the POST request.
|
||||
* @param {resource} resource - The type of resource.
|
||||
* @returns {Array} - An array containing the HTTP status code and the processed data.
|
||||
*/
|
||||
#postData(arr, id, body, resource) {
|
||||
const qs = require('qs');
|
||||
let decoded = qs.parse(body, {
|
||||
@ -309,6 +362,9 @@ class StripeMocker {
|
||||
return [200, subscription];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the registered nock interceptors.
|
||||
*/
|
||||
remove() {
|
||||
for (const interceptor of this.nockInterceptors) {
|
||||
nock.removeInterceptor(interceptor);
|
||||
@ -316,6 +372,10 @@ class StripeMocker {
|
||||
this.nockInterceptors = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs the Stripe API.
|
||||
* This will intercept all requests to the Stripe API and return the mocked data.
|
||||
*/
|
||||
stub() {
|
||||
this.remove();
|
||||
|
||||
@ -425,10 +485,13 @@ class StripeMocker {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a webhook event to the Stripe webhook controller.
|
||||
*
|
||||
* @param {any} event - The webhook event to send.
|
||||
* @returns {Promise<void>} - A promise that resolves when the webhook event is handled.
|
||||
*/
|
||||
async sendWebhook(event) {
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
const webhookController = stripeService.webhookController;
|
||||
await webhookController.handleEvent(event);
|
||||
}
|
||||
|
@ -1079,7 +1079,7 @@ class EmailRenderer {
|
||||
classes: {
|
||||
title: 'post-title' + (newsletter.get('title_font_category') === 'serif' ? ` post-title-serif` : ``) + (newsletter.get('title_alignment') === 'left' ? ` post-title-left` : ``),
|
||||
titleLink: 'post-title-link' + (newsletter.get('title_alignment') === 'left' ? ` post-title-link-left` : ``),
|
||||
subhead: 'post-subhead' + (newsletter.get('title_alignment') === 'left' ? ` post-subhead-left` : ``),
|
||||
subtitle: 'post-subtitle' + (newsletter.get('title_alignment') === 'left' ? ` post-subtitle-left` : ``),
|
||||
meta: 'post-meta' + (newsletter.get('title_alignment') === 'left' ? ` post-meta-left` : ` post-meta-center`),
|
||||
body: newsletter.get('body_font_category') === 'sans_serif' ? `post-content-sans-serif` : `post-content`
|
||||
},
|
||||
|
@ -308,6 +308,7 @@ figure blockquote p {
|
||||
|
||||
.header-image {
|
||||
padding-top: 24px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
@ -376,12 +377,12 @@ figure blockquote p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.post-subhead-wrapper {
|
||||
.post-subtitle-wrapper {
|
||||
width: 100%;
|
||||
max-width: 600px !important;
|
||||
}
|
||||
|
||||
.post-subhead {
|
||||
.post-subtitle {
|
||||
margin: 0;
|
||||
padding-bottom: 20px;
|
||||
color: #15212A;
|
||||
@ -390,7 +391,7 @@ figure blockquote p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-subhead-left {
|
||||
.post-subtitle-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@ -434,15 +435,17 @@ figure blockquote p {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.feature-image-row:first-child > td {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.feature-image {
|
||||
padding-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feature-image-row:first-child > td,
|
||||
.header-image-row + .feature-image-row > td,
|
||||
.site-info-row + .feature-image-row > td {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.feature-image-with-caption {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
@ -609,7 +612,7 @@ figure blockquote p {
|
||||
}
|
||||
|
||||
.post-content-row:first-child > td, .header-image-row + .post-content-row > td {
|
||||
padding-top: 52px;
|
||||
padding-top: 32px;
|
||||
}
|
||||
|
||||
.post-content a,
|
||||
@ -1300,7 +1303,7 @@ a[data-flickr-embed] img {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
table.body .post-subhead {
|
||||
table.body .post-subtitle {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if (or showHeaderIcon showHeaderTitle showHeaderName) }}
|
||||
<tr>
|
||||
<tr class="site-info-row">
|
||||
<td class="site-info" width="100%" align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
{{#if (and showHeaderIcon site.iconUrl) }}
|
||||
@ -79,11 +79,11 @@
|
||||
<a href="{{post.url}}" class="{{classes.titleLink}}">{{post.title}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{#hasFeature 'subhead'}}
|
||||
{{#hasFeature 'newsletterSubtitle'}}
|
||||
{{#if newsletter.showSubhead}}
|
||||
<tr>
|
||||
<td class="post-subhead-wrapper" style="width: 100%">
|
||||
<p class="{{classes.subhead}}">Jonathan Haidt wrote a best-selling book about teens and social media. Not everyone buys its thesis.</p>
|
||||
<td class="post-subtitle-wrapper" style="width: 100%">
|
||||
<p class="{{classes.subtitle}}">Jonathan Haidt wrote a best-selling book about teens and social media. Not everyone buys its thesis.</p>
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
@ -1995,7 +1995,7 @@ describe('Email renderer', function () {
|
||||
title: 'post-title post-title-serif post-title-left',
|
||||
titleLink: 'post-title-link post-title-link-left',
|
||||
meta: 'post-meta post-meta-left',
|
||||
subhead: 'post-subhead post-subhead-left',
|
||||
subtitle: 'post-subtitle post-subtitle-left',
|
||||
body: 'post-content-sans-serif'
|
||||
});
|
||||
});
|
||||
|
107
yarn.lock
107
yarn.lock
@ -6967,12 +6967,12 @@
|
||||
dependencies:
|
||||
lodash-es "^4.17.11"
|
||||
|
||||
"@tryghost/html-to-mobiledoc@3.0.10":
|
||||
version "3.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/html-to-mobiledoc/-/html-to-mobiledoc-3.0.10.tgz#981a50a9bcdbe14d30beaf6a132921238fb9fa57"
|
||||
integrity sha512-ThK2wFyH/6J01clhumR8BFILwDpbY3Epr0GYMP3RMhw2/hk1mf5xj0y9acdgFACBoTYzclOcqUt67CZY0/Lt3Q==
|
||||
"@tryghost/html-to-mobiledoc@3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/html-to-mobiledoc/-/html-to-mobiledoc-3.1.0.tgz#64cb333c8f0b857f53edf289867a39d6113ccefb"
|
||||
integrity sha512-CL1IAL3mRrWPs2LcDcPptN2O2w9EY0mb/ofkOycQYaHoPPt0JgPOPeJdmIwRfddzGkEK1WA1yWnqKa4zSyIGRw==
|
||||
dependencies:
|
||||
"@tryghost/kg-parser-plugins" "4.0.9"
|
||||
"@tryghost/kg-parser-plugins" "4.1.0"
|
||||
"@tryghost/mobiledoc-kit" "^0.12.4-ghost.1"
|
||||
jsdom "^24.0.0"
|
||||
|
||||
@ -7014,10 +7014,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-card-factory/-/kg-card-factory-5.0.4.tgz#b2de98eaf01edbd5629fb1f4b06eca3a5f95d0ad"
|
||||
integrity sha512-KcNM4QJONSSOJeQlv9no5wFx+uV2mESX3bYBL2y3c0DqB26NlMaUx0QIAFSbCSinUlCvRFOwEEBQyaACtCOvzQ==
|
||||
|
||||
"@tryghost/kg-clean-basic-html@4.0.9":
|
||||
version "4.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-clean-basic-html/-/kg-clean-basic-html-4.0.9.tgz#6ac4119a7f2fb1000d00a1559a6fe39e61f14848"
|
||||
integrity sha512-VwVpFwlf/mVghbCQksVSuQRnixunJZMRvHb1Mi73MuQdff9RQSvpaADWxShb4Z3lnJTtF65E63OUP8rPeEYwYA==
|
||||
"@tryghost/kg-clean-basic-html@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-clean-basic-html/-/kg-clean-basic-html-4.1.0.tgz#f4979d99e65a9202e3d3376f90ff08de8ac56578"
|
||||
integrity sha512-exXUpTA1z0zyk3F3ZXcT7oy8b3acbpuHWMqiowdb2HI0/6LTGaBdmm866fhBKedgB59lIWRt511djUnUIILh+A==
|
||||
|
||||
"@tryghost/kg-converters@1.0.4":
|
||||
version "1.0.4"
|
||||
@ -7044,16 +7044,16 @@
|
||||
lodash "^4.17.21"
|
||||
luxon "^3.0.0"
|
||||
|
||||
"@tryghost/kg-default-nodes@1.0.16":
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-nodes/-/kg-default-nodes-1.0.16.tgz#b9d3fafb549ee2f858d09a15b6b962a1b2e7a68d"
|
||||
integrity sha512-obohpcnvNFXwFmtLViEHwcEA/tFZQm2CdzOT5vUaheq9LX/RXQN7/FVFQKhJ/Djv4ixHOufCp598X+Z3Ffnrqw==
|
||||
"@tryghost/kg-default-nodes@1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-nodes/-/kg-default-nodes-1.1.0.tgz#e8902b5b8f7ac77614110c17fc0f0e1aa8daec13"
|
||||
integrity sha512-Mx+c7q85SzDVZM/WFg+KKHOspOng0lG83i+YKaayAE0lU4yGzcFb1qV+TRIgn//4bMkMmjeKt8/0kp1GTfV0Ng==
|
||||
dependencies:
|
||||
"@lexical/clipboard" "0.13.1"
|
||||
"@lexical/rich-text" "0.13.1"
|
||||
"@lexical/selection" "0.13.1"
|
||||
"@lexical/utils" "0.13.1"
|
||||
"@tryghost/kg-clean-basic-html" "4.0.9"
|
||||
"@tryghost/kg-clean-basic-html" "4.1.0"
|
||||
"@tryghost/kg-markdown-html-renderer" "7.0.5"
|
||||
html-minifier "^4.0.0"
|
||||
jsdom "^24.0.0"
|
||||
@ -7061,21 +7061,21 @@
|
||||
lodash "^4.17.21"
|
||||
luxon "^3.3.0"
|
||||
|
||||
"@tryghost/kg-default-transforms@1.0.17":
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-transforms/-/kg-default-transforms-1.0.17.tgz#7383428b91b2cd389f655a876a5ed8e3c5c73679"
|
||||
integrity sha512-l7Z6Hv81c+OTCtpzkMmdBoFZoF5rdc2Sqws635F6+GaTVzxPsxR9lf6lG7jnTmtDM/YKCgPFQiGu+CBOaDUNPw==
|
||||
"@tryghost/kg-default-transforms@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-default-transforms/-/kg-default-transforms-1.1.1.tgz#219e515738e7a6aa0c534091b30000e93000491c"
|
||||
integrity sha512-r+++zB7XLAmkKPCMHHmrL0Qbg1NyRRHSaRslKYdC3Z5+Z16GTQfFzUAIAAY5A16Livj/hNbO8XlQrKNS3hV1tQ==
|
||||
dependencies:
|
||||
"@lexical/list" "0.13.1"
|
||||
"@lexical/rich-text" "0.13.1"
|
||||
"@lexical/utils" "0.13.1"
|
||||
"@tryghost/kg-default-nodes" "1.0.16"
|
||||
"@tryghost/kg-default-nodes" "1.1.0"
|
||||
lexical "0.13.1"
|
||||
|
||||
"@tryghost/kg-html-to-lexical@1.0.17":
|
||||
version "1.0.17"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-html-to-lexical/-/kg-html-to-lexical-1.0.17.tgz#b89c68ba6120601f01454a439356c5232c1e4e2c"
|
||||
integrity sha512-8SPXl02D0tmiUCo5tQ4IG511HixNlfdbeQCs7HAM+6gFUmkPCCByBtxeVQ2KkiUBcfe33/+D+JoGgZTvrBD16A==
|
||||
"@tryghost/kg-html-to-lexical@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-html-to-lexical/-/kg-html-to-lexical-1.1.1.tgz#412ef0be6feb97092bb22764e5578533085fd139"
|
||||
integrity sha512-KXFJeRShAy4BoL+hIpUijXkPM0V8GxaHzcH81vuKgcSrGk3MG21tC3tD4YxZh5uc8jy9H7/oO9ZxzN0ATDMvVg==
|
||||
dependencies:
|
||||
"@lexical/clipboard" "0.13.1"
|
||||
"@lexical/headless" "0.13.1"
|
||||
@ -7083,15 +7083,15 @@
|
||||
"@lexical/link" "0.13.1"
|
||||
"@lexical/list" "0.13.1"
|
||||
"@lexical/rich-text" "0.13.1"
|
||||
"@tryghost/kg-default-nodes" "1.0.16"
|
||||
"@tryghost/kg-default-transforms" "1.0.17"
|
||||
"@tryghost/kg-default-nodes" "1.1.0"
|
||||
"@tryghost/kg-default-transforms" "1.1.1"
|
||||
jsdom "^24.0.0"
|
||||
lexical "0.13.1"
|
||||
|
||||
"@tryghost/kg-lexical-html-renderer@1.0.16":
|
||||
version "1.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-lexical-html-renderer/-/kg-lexical-html-renderer-1.0.16.tgz#166697108b0000f6055f2f6e4e3619cbec633266"
|
||||
integrity sha512-Wkk295SmA+vyj0CguJmCmMqZsLblUGBdEE+RsfJCmUr1VzwHXmORbO7wfKOTn4arBL2D37rk7rj+8K2OCuicog==
|
||||
"@tryghost/kg-lexical-html-renderer@1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-lexical-html-renderer/-/kg-lexical-html-renderer-1.1.1.tgz#4a489cdc0268a15546be421e1b673eecb2616049"
|
||||
integrity sha512-W41r93zfI17Ff0QxJOru2VxE6l5ICAwd2xVzX0OUOC6PxAJ2e5RrjXzzHjdEjB9eTYIteKH5m+488zMCR4SOVw==
|
||||
dependencies:
|
||||
"@lexical/clipboard" "0.13.1"
|
||||
"@lexical/code" "0.13.1"
|
||||
@ -7099,10 +7099,11 @@
|
||||
"@lexical/link" "0.13.1"
|
||||
"@lexical/list" "0.13.1"
|
||||
"@lexical/rich-text" "0.13.1"
|
||||
"@tryghost/kg-default-nodes" "1.0.16"
|
||||
"@tryghost/kg-default-nodes" "1.1.0"
|
||||
"@tryghost/kg-default-transforms" "1.1.1"
|
||||
jsdom "^24.0.0"
|
||||
lexical "0.13.1"
|
||||
prettier "^3.0.0"
|
||||
prettier "3.2.5"
|
||||
|
||||
"@tryghost/kg-markdown-html-renderer@7.0.5":
|
||||
version "7.0.5"
|
||||
@ -7128,17 +7129,17 @@
|
||||
mobiledoc-dom-renderer "^0.7.0"
|
||||
simple-dom "^1.4.0"
|
||||
|
||||
"@tryghost/kg-parser-plugins@4.0.9":
|
||||
version "4.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-parser-plugins/-/kg-parser-plugins-4.0.9.tgz#66784a9df28b5cf105ceeff2fdf9245db09362b6"
|
||||
integrity sha512-3GiP9HsJT4dSdg/3ww7cpkgdZ2MNxKkmpZPOPLwixQeUbWJaR1a+1a6X0C1Woc8djd/pHLaBVQWcTDtpB/NaUA==
|
||||
"@tryghost/kg-parser-plugins@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-parser-plugins/-/kg-parser-plugins-4.1.0.tgz#60f6492d1b0d71558c51f7b0c45d905ae5aa8b0a"
|
||||
integrity sha512-sHwOc+9ObNBFccgbSxnbdgygLTPLI0PpjUwGsVJ7NAPQBEISHsQzHMYpWTCGatgdB+B+KmwTtBeGcvn3DIXagw==
|
||||
dependencies:
|
||||
"@tryghost/kg-clean-basic-html" "4.0.9"
|
||||
"@tryghost/kg-clean-basic-html" "4.1.0"
|
||||
|
||||
"@tryghost/kg-unsplash-selector@0.1.17":
|
||||
version "0.1.17"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-unsplash-selector/-/kg-unsplash-selector-0.1.17.tgz#6caa4ae0afbd3989981cbc17e7c909d243cadfc6"
|
||||
integrity sha512-8cw4qtkczWBrAo7PpZ/kT78yzpm8B+cEwWW6McWkTzZRs3AuBUWWyCVi6plb1c+RhY110FoFkyMLBJnGeM+D3g==
|
||||
"@tryghost/kg-unsplash-selector@0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/kg-unsplash-selector/-/kg-unsplash-selector-0.2.0.tgz#e811612fd66f20182a1d7f06e48a86bf94896ce6"
|
||||
integrity sha512-oVMUiKFwOk5bUwNmW2DrICfs9lRtqHIOrV2hnJF8cBrhKLPnTSDrYTrrz04FTkq1ApISJ0y9pLZA6eepZpzTUQ==
|
||||
|
||||
"@tryghost/kg-utils@1.0.26":
|
||||
version "1.0.26"
|
||||
@ -7147,10 +7148,10 @@
|
||||
dependencies:
|
||||
semver "^7.3.5"
|
||||
|
||||
"@tryghost/koenig-lexical@1.1.16":
|
||||
version "1.1.16"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/koenig-lexical/-/koenig-lexical-1.1.16.tgz#cdaed36dc97bde8f53a08880fa5cee171641fd93"
|
||||
integrity sha512-u2dqnTA7AbA8GUPZVVVToSW91eqV0G42pbJeGTxmlzK91OIKa4fbrMO4Vp0HD3cMhCHYml9kWa976HowGS/NjQ==
|
||||
"@tryghost/koenig-lexical@1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/koenig-lexical/-/koenig-lexical-1.2.2.tgz#39cd50959de7b5ff8e0d2dbd8688ff83c81f57e1"
|
||||
integrity sha512-1bFveIMMNg3Q6UPWGjTvg6m430N+4ZAMTWs2IfuR3iIVBPp687T41vgsiMQySfAepm/WKRAltpIh4kNYQOy1Ng==
|
||||
|
||||
"@tryghost/limit-service@1.2.14":
|
||||
version "1.2.14"
|
||||
@ -22801,10 +22802,10 @@ mysql2@3.9.7:
|
||||
seq-queue "^0.0.5"
|
||||
sqlstring "^2.3.2"
|
||||
|
||||
mysql2@3.9.8:
|
||||
version "3.9.8"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.8.tgz#fe8a0f975f2c495ed76ca988ddc5505801dc49ce"
|
||||
integrity sha512-+5JKNjPuks1FNMoy9TYpl77f+5frbTklz7eb3XDwbpsERRLEeXiW2PDEkakYF50UuKU2qwfGnyXpKYvukv8mGA==
|
||||
mysql2@3.9.9:
|
||||
version "3.9.9"
|
||||
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.9.9.tgz#262582b612a2dea540ef0d581b362d55958cf272"
|
||||
integrity sha512-Qtb2RUxwWMFkWXqF7Rd/7ySkupbQnNY7O0zQuQYgPcuJZ06M36JG3HIDEh/pEeq7LImcvA6O3lOVQ9XQK+HEZg==
|
||||
dependencies:
|
||||
denque "^2.1.0"
|
||||
generate-function "^2.3.1"
|
||||
@ -25302,16 +25303,16 @@ pretender@3.4.7, pretender@^3.4.7:
|
||||
fake-xml-http-request "^2.1.2"
|
||||
route-recognizer "^0.3.3"
|
||||
|
||||
prettier@3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
|
||||
integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==
|
||||
|
||||
prettier@^2.8.0:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
prettier@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
|
||||
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
|
Loading…
Reference in New Issue
Block a user