Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/admin-x-framework/src/api/automated-emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type AutomatedEmail = {
sender_name: string | null;
sender_email: string | null;
sender_reply_to: string | null;
email_template_id: string | null;
created_at: string;
updated_at: string | null;
}
Expand Down
52 changes: 52 additions & 0 deletions apps/admin-x-framework/src/api/email-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
import {updateQueryCache} from '../utils/api/update-queries';

export type EmailTemplate = {
id: string;
name: string;
slug: string;
header_image: string | null;
show_publication_title: boolean;
show_badge: boolean;
footer_content: string | null;
background_color: string;
title_font_category: string;
title_font_weight: string;
body_font_category: string;
header_background_color: string;
title_alignment: string;
post_title_color: string | null;
section_title_color: string | null;
button_color: string | null;
button_style: string;
button_corners: string;
link_color: string | null;
link_style: string;
image_corners: string;
divider_color: string | null;
created_at: string;
updated_at: string | null;
}

export interface EmailTemplatesResponseType {
meta?: Meta;
email_templates: EmailTemplate[];
}

const dataType = 'EmailTemplatesResponseType';

export const useBrowseEmailTemplates = createQuery<EmailTemplatesResponseType>({
dataType,
path: '/email_templates/'
});

export const useEditEmailTemplate = createMutation<EmailTemplatesResponseType, EmailTemplate>({
method: 'PUT',
path: emailTemplate => `/email_templates/${emailTemplate.id}/`,
body: emailTemplate => ({email_templates: [emailTemplate]}),
updateQueries: {
dataType,
emberUpdateType: 'createOrUpdate',
update: updateQueryCache('email_templates')
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => {
sender_name: null,
sender_email: null,
sender_reply_to: null,
email_template_id: null,
created_at: '',
updated_at: null
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import {
Textarea
} from '@tryghost/shade';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useCallback, useState} from 'react';
import {useBrowseEmailTemplates, useEditEmailTemplate} from '@tryghost/admin-x-framework/api/email-templates';
import {useCallback, useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/global-data-provider';

interface GeneralSettings {
Expand Down Expand Up @@ -178,6 +179,10 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
const {siteData, settings: globalSettings, config} = useGlobalData();
const [siteTitle, defaultEmailAddress, supportEmailAddress] = getSettingValues<string>(globalSettings, ['title', 'default_email_address', 'support_email_address']);

const {data: emailTemplatesData} = useBrowseEmailTemplates();
const {mutateAsync: editEmailTemplate} = useEditEmailTemplate();
const template = emailTemplatesData?.email_templates?.[0];

const [designSettings, setDesignSettings] = useState<EmailDesignSettings>({...DEFAULT_EMAIL_DESIGN});
const [generalSettings, setGeneralSettings] = useState<GeneralSettings>({
senderName: siteTitle || '',
Expand All @@ -188,6 +193,35 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
emailFooter: ''
});

useEffect(() => {
if (template) {
setDesignSettings({
background_color: template.background_color,
title_font_category: template.title_font_category,
title_font_weight: template.title_font_weight,
body_font_category: template.body_font_category,
header_background_color: template.header_background_color,
post_title_color: template.post_title_color,
title_alignment: template.title_alignment,
section_title_color: template.section_title_color,
button_color: template.button_color,
button_style: template.button_style,
button_corners: template.button_corners,
link_color: template.link_color,
link_style: template.link_style,
image_corners: template.image_corners,
divider_color: template.divider_color
});
setGeneralSettings(prev => ({
...prev,
headerImage: template.header_image || '',
showPublicationTitle: template.show_publication_title,
showBadge: template.show_badge,
emailFooter: template.footer_content || ''
}));
}
}, [template]);

const handleDesignChange = useCallback((updates: Partial<EmailDesignSettings>) => {
setDesignSettings(prev => ({...prev, ...updates}));
}, []);
Expand All @@ -196,10 +230,19 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => {
setGeneralSettings(prev => ({...prev, ...updates}));
}, []);

const handleSave = useCallback(() => {
// TODO: persist to backend when design columns are added
const handleSave = useCallback(async () => {
if (template) {
await editEmailTemplate({
...template,
...designSettings,
header_image: generalSettings.headerImage || null,
show_publication_title: generalSettings.showPublicationTitle,
show_badge: generalSettings.showBadge,
footer_content: generalSettings.emailFooter || null
});
}
modal.remove();
}, [modal]);
}, [modal, template, editEmailTemplate, designSettings, generalSettings]);

const handleClose = useCallback(() => {
modal.remove();
Expand Down
4 changes: 2 additions & 2 deletions ghost/admin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghost-admin",
"version": "6.22.1",
"version": "6.23.0-rc.0",
"description": "Ember.js admin client for Ghost",
"author": "Ghost Foundation",
"homepage": "http://ghost.org",
Expand Down Expand Up @@ -228,4 +228,4 @@
}
}
}
}
}
83 changes: 83 additions & 0 deletions ghost/core/core/server/api/endpoints/email-templates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const models = require('../../models');

const messages = {
emailTemplateNotFound: 'Email template not found.'
};

/** @type {import('@tryghost/api-framework').Controller} */
const controller = {
docName: 'email_templates',

browse: {
headers: {
cacheInvalidate: false
},
options: [
'filter',
'fields',
'limit',
'order',
'page'
],
permissions: true,
query(frame) {
return models.EmailTemplate.findPage(frame.options);
}
},

read: {
headers: {
cacheInvalidate: false
},
options: [
'filter',
'fields'
],
data: [
'id'
],
permissions: true,
async query(frame) {
const model = await models.EmailTemplate.findOne(frame.data, frame.options);
if (!model) {
throw new errors.NotFoundError({
message: tpl(messages.emailTemplateNotFound)
});
}

return model;
}
},

edit: {
headers: {
cacheInvalidate: false
},
options: [
'id'
],
validation: {
options: {
id: {
required: true
}
}
},
permissions: true,
async query(frame) {
const data = frame.data.email_templates[0];
const model = await models.EmailTemplate.edit(data, frame.options);
if (!model) {
throw new errors.NotFoundError({
message: tpl(messages.emailTemplateNotFound)
});
}

return model;
}
}
};

module.exports = controller;
4 changes: 4 additions & 0 deletions ghost/core/core/server/api/endpoints/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ module.exports = {
return apiFramework.pipeline(require('./automated-emails'), localUtils);
},

get emailTemplates() {
return apiFramework.pipeline(require('./email-templates'), localUtils);
},

get membersStripeConnect() {
return apiFramework.pipeline(require('./members-stripe-connect'), localUtils);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const {addTable} = require('../../utils');

module.exports = addTable('email_templates', {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
name: {type: 'string', maxlength: 191, nullable: false, unique: true},
slug: {type: 'string', maxlength: 191, nullable: false, unique: true},
header_image: {type: 'string', maxlength: 2000, nullable: true},
show_publication_title: {type: 'boolean', nullable: false, defaultTo: true},
show_badge: {type: 'boolean', nullable: false, defaultTo: true},
footer_content: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
background_color: {type: 'string', maxlength: 50, nullable: false, defaultTo: '#ffffff'},
title_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif'},
title_font_weight: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'bold'},
body_font_category: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'sans_serif'},
header_background_color: {type: 'string', maxlength: 50, nullable: false, defaultTo: '#ffffff'},
title_alignment: {type: 'string', maxlength: 191, nullable: false, defaultTo: 'center'},
post_title_color: {type: 'string', maxlength: 50, nullable: true},
section_title_color: {type: 'string', maxlength: 50, nullable: true},
button_color: {type: 'string', maxlength: 50, nullable: true, defaultTo: 'accent'},
button_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'fill'},
button_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'rounded'},
link_color: {type: 'string', maxlength: 50, nullable: true, defaultTo: 'accent'},
link_style: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'underline'},
image_corners: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'square'},
divider_color: {type: 'string', maxlength: 50, nullable: true},
created_at: {type: 'dateTime', nullable: false},
updated_at: {type: 'dateTime', nullable: true},
'@@INDEXES@@': [
['slug']
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('automated_emails', 'email_template_id', {
type: 'string',
maxlength: 24,
nullable: true,
references: 'email_templates.id'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const {combineTransactionalMigrations, addPermissionWithRoles} = require('../../utils');

module.exports = combineTransactionalMigrations(
addPermissionWithRoles({
name: 'Browse email templates',
action: 'browse',
object: 'email_template'
}, [
'Administrator',
'Admin Integration'
]),
addPermissionWithRoles({
name: 'Read email templates',
action: 'read',
object: 'email_template'
}, [
'Administrator',
'Admin Integration'
]),
addPermissionWithRoles({
name: 'Edit email templates',
action: 'edit',
object: 'email_template'
}, [
'Administrator',
'Admin Integration'
])
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const logging = require('@tryghost/logging');
const {default: ObjectID} = require('bson-objectid');
const {createTransactionalMigration} = require('../../utils');

const DEFAULT_TEMPLATE_SLUG = 'default';

module.exports = createTransactionalMigration(
async function up(knex) {
const existing = await knex('email_templates')
.where({slug: DEFAULT_TEMPLATE_SLUG})
.first();

if (existing) {
logging.warn('Default email template already exists, skipping');
return;
}

const templateId = (new ObjectID()).toHexString();

logging.info('Creating default email template');
await knex('email_templates').insert({
id: templateId,
name: 'Default',
slug: DEFAULT_TEMPLATE_SLUG,
created_at: knex.raw('current_timestamp')
});

logging.info('Linking existing automated emails to default template');
await knex('automated_emails')
.whereNull('email_template_id')
.update({email_template_id: templateId});
},
async function down(knex) {
logging.info('Unlinking automated emails from default template');
await knex('automated_emails')
.update({email_template_id: null});

logging.info('Deleting default email template');
await knex('email_templates')
.where({slug: DEFAULT_TEMPLATE_SLUG})
.del();
}
);
Loading
Loading