mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
Wired up newsletter management with real newsletter model and API
refs https://github.com/TryGhost/Team/issues/1441 - switched "leave settings" confirmation modal on members email settings screen over to modern modal pattern - removed unused `showEmailDesignSettings` property and `closeEmailDesignSettings()` action on `<Settings::MembersEmailLabs>` component - the used property and action live on the controller, looks like it was a copy/paste hangover when functionality was moved to a component - added newsletter model - includes design-related attributes which are not yet supported by the API but are due to be added - includes `default` attribute but there is no setting for it, due to be removed from the API but it's needed for save not to error for now - set up basic mirage model and endpoints - added validation for main settings to match API validation - added `EditNewsletter` modal - separate tabs for general newsletter settings and design-related settings - used for both creating and editing newsletters - added `/settings/members-email/newsletters/new` and `/settings/members-email/newsletters/:id` routes - both display the `EditNewsletter` modal on top of the members-email settings screen with the appropriate newsletter model - updated `<Settings::MembersEmailLabs::NewsletterManagement>` component to work with real newsletter model instances and the new add/edit routes - removed now-unused `newsletter` service that was providing mocked data for earlier design iteration
This commit is contained in:
parent
4be3c3b53f
commit
6e0be9e175
43
ghost/admin/app/components/modals/edit-newsletter.hbs
Normal file
43
ghost/admin/app/components/modals/edit-newsletter.hbs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body modal-fullsettings">
|
||||||
|
<div class="flex items-center justify-between w-100 modal-fullsettings-topbar">
|
||||||
|
<h2 class="modal-fullsettings-heading">
|
||||||
|
{{if @data.newsletter.isNew "Add" "Edit"}} newsletter
|
||||||
|
</h2>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button class="gh-btn mr3" type="button" {{on "click" @close}}>
|
||||||
|
<span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<GhTaskButton
|
||||||
|
@buttonText="Save and close"
|
||||||
|
@successText="Saved"
|
||||||
|
@task={{this.saveTask}}
|
||||||
|
@idleClass="gh-btn-primary"
|
||||||
|
@class="gh-btn gh-btn-icon"
|
||||||
|
{{on-key "cmd+s" this.saveViaKeyboard priority=1}}
|
||||||
|
data-test-button="save-newsletter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-fullsettings-body">
|
||||||
|
<div class="modal-fullsettings-sidebar with-footer">
|
||||||
|
<div class="gh-btn-group">
|
||||||
|
<button type="button" class="gh-btn gh-btn-icon {{if (eq this.tab "settings") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "settings")}}><span>Settings</span></button>
|
||||||
|
<button type="button" class="gh-btn gh-btn-icon {{if (eq this.tab "design") "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "design")}}><span>Design</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if (eq this.tab "settings")}}
|
||||||
|
<Modals::EditNewsletter::Settings @newsletter={{@data.newsletter}} />
|
||||||
|
{{else}}
|
||||||
|
<Modals::EditNewsletter::Design @newsletter={{@data.newsletter}} />
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-fullsettings-main">
|
||||||
|
<Modals::EditNewsletter::Preview @newsletter={{@data.newsletter}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
48
ghost/admin/app/components/modals/edit-newsletter.js
Normal file
48
ghost/admin/app/components/modals/edit-newsletter.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
import {tracked} from '@glimmer/tracking';
|
||||||
|
|
||||||
|
export default class EditNewsletterModal extends Component {
|
||||||
|
static modalOptions = {
|
||||||
|
className: 'fullscreen-modal-full-overlay fullscreen-modal-portal-settings'
|
||||||
|
};
|
||||||
|
|
||||||
|
@tracked tab = 'settings';
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
super.willDestroy(...arguments);
|
||||||
|
this.args.data.newsletter.rollbackAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
changeTab(tab) {
|
||||||
|
this.tab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
saveViaKeyboard(event, responder) {
|
||||||
|
responder.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.saveTask.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
@task
|
||||||
|
*saveTask() {
|
||||||
|
try {
|
||||||
|
const result = yield this.args.data.newsletter.save();
|
||||||
|
|
||||||
|
this.args.data.afterSave?.(result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
if (e === undefined) {
|
||||||
|
// validation error
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
166
ghost/admin/app/components/modals/edit-newsletter/design.hbs
Normal file
166
ghost/admin/app/components/modals/edit-newsletter/design.hbs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
<div>
|
||||||
|
<fieldset class="modal-fullsettings-form">
|
||||||
|
<div class="modal-fullsettings-section first">
|
||||||
|
<GhFormGroup @classNames="vertical">
|
||||||
|
<GhUploader
|
||||||
|
@extensions={{this.imageExtensions}}
|
||||||
|
@paramsHash={{hash purpose="image"}}
|
||||||
|
@onComplete={{fn this.imageUploaded "headerImage"}}
|
||||||
|
as |uploader|
|
||||||
|
>
|
||||||
|
<div class="modal-fullsettings-uploader">
|
||||||
|
<div class="gh-header-img-desc">
|
||||||
|
<h4 class="modal-fullsettings-title">Header image</h4>
|
||||||
|
<p>Optional, recommended size 1200x600</p>
|
||||||
|
</div>
|
||||||
|
{{#if uploader.isUploading}}
|
||||||
|
<div class="gh-header-img-container">
|
||||||
|
<div class="gh-loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
{{else if this.headerImage}}
|
||||||
|
<div class="gh-header-img">
|
||||||
|
<img
|
||||||
|
class="gh-header-img-thumbnail"
|
||||||
|
src={{@newsletter.headerImage}}
|
||||||
|
alt=""
|
||||||
|
role="presentation"
|
||||||
|
data-test-img="header"
|
||||||
|
>
|
||||||
|
<button type="button" class="gh-btn gh-header-img-deleteicon" {{on "click" (fn this.changeSetting "headerImage" null)}}>
|
||||||
|
<span> {{svg-jar "trash" class="w5 h5"}} </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<button type="button" class="gh-btn gh-header-img-uploadicon" {{on "click" uploader.triggerFileDialog}} data-test-image-upload-btn="header-image">
|
||||||
|
<span>{{svg-jar "upload-fill" class="w5 h5"}}</span>
|
||||||
|
</button>
|
||||||
|
<div style="display:none">
|
||||||
|
<GhFileInput
|
||||||
|
@multiple={{false}}
|
||||||
|
@action={{uploader.setFiles}}
|
||||||
|
@accept={{uploader.imageMimeTypes}}
|
||||||
|
@onInsert={{uploader.registerFileInput}}
|
||||||
|
data-test-file-input="icon" />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</GhUploader>
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup data-tooltip={{unless this.settings.icon "A publication icon must be set in Branding settings."}}>
|
||||||
|
<h4 class="modal-fullsettings-title {{unless this.settings.icon "disabled"}}">Publication icon</h4>
|
||||||
|
<div class="for-switch small {{unless this.settings.icon "disabled"}}">
|
||||||
|
<label class="switch" for="show-header">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{and @newsletter.showHeaderIcon this.settings.icon}}
|
||||||
|
id="show-header"
|
||||||
|
name="show-header"
|
||||||
|
disabled={{not this.settings.icon}}
|
||||||
|
{{on "click" (fn this.toggleSetting "showHeaderIcon")}}
|
||||||
|
>
|
||||||
|
<span class="input-toggle-component"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
<GhFormGroup>
|
||||||
|
<h4 class="modal-fullsettings-title">Publication title</h4>
|
||||||
|
<div class="for-switch small">
|
||||||
|
<label class="switch" for="show-title">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{@newsletter.showHeaderTitle}}
|
||||||
|
id="show-title"
|
||||||
|
name="show-title"
|
||||||
|
{{on "click" (fn this.toggleSetting "showHeaderTitle")}}
|
||||||
|
>
|
||||||
|
<span class="input-toggle-component"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
<div class="modal-fullsettings-section divider-top">
|
||||||
|
<GhFormGroup>
|
||||||
|
<h4 class="modal-fullsettings-title gh-email-design-alignment">Header style</h4>
|
||||||
|
<div class="gh-email-design-typography-wrapper header">
|
||||||
|
<div class="modal-fullsettings-radiogroup gh-email-design-typography">
|
||||||
|
<GhFontSelector
|
||||||
|
@selected={{@newsletter.titleFontCategory}}
|
||||||
|
@onChange={{fn this.changeSetting "titleFontCategory"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="gh-btn-group icons">
|
||||||
|
<button type="button" class="gh-btn gh-btn-icon {{if (eq @newsletter.titleAlignment "left") "gh-btn-group-selected"}}" {{on "click" (fn this.changeSetting "titleAlignment" "left")}}><span>{{svg-jar "align-left"}}</span></button>
|
||||||
|
<button type="button" class="gh-btn gh-btn-icon {{if (eq @newsletter.titleAlignment "center") "gh-btn-group-selected"}}" {{on "click" (fn this.changeSetting "titleAlignment" "center")}}><span>{{svg-jar "align-center"}}</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
<GhFormGroup>
|
||||||
|
<h4 class="modal-fullsettings-title">Feature image</h4>
|
||||||
|
<div class="for-switch small">
|
||||||
|
<label class="switch" for="show-feature-image">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{@newsletter.showFeatureImage}}
|
||||||
|
id="show-feature-image"
|
||||||
|
name="show-feature-image"
|
||||||
|
{{on "click" (fn this.toggleSetting "showFeatureImage")}}
|
||||||
|
>
|
||||||
|
<span class="input-toggle-component"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
<div class="modal-fullsettings-section">
|
||||||
|
<GhFormGroup>
|
||||||
|
<h4 class="modal-fullsettings-title">Body style</h4>
|
||||||
|
<div class="gh-email-design-typography-wrapper">
|
||||||
|
<div class="modal-fullsettings-radiogroup gh-email-design-typography">
|
||||||
|
<GhFontSelector
|
||||||
|
@selected={{@newsletter.bodyFontCategory}}
|
||||||
|
@onChange={{fn this.changeSetting "bodyFontCategory"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
<div class="modal-fullsettings-section divider-top">
|
||||||
|
<GhFormGroup @classNames="vertical">
|
||||||
|
<h4 class="modal-fullsettings-title">Email footer</h4>
|
||||||
|
<KoenigBasicHtmlInput
|
||||||
|
@name="footer"
|
||||||
|
@html={{@newsletter.footerContent}}
|
||||||
|
@class="miw-100 form-text gh-members-emailsettings-footer-input"
|
||||||
|
@onChange={{fn this.changeSetting "footerContent"}}
|
||||||
|
/>
|
||||||
|
<p>Any extra information or legal text</p>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="modal-fullsettings-section gh-members-emailsettings-footer">
|
||||||
|
<GhFormGroup>
|
||||||
|
<div class="gh-members-emailsettings-promotelabel">
|
||||||
|
<span>{{svg-jar "heart"}}</span>
|
||||||
|
<div>
|
||||||
|
<h4 class="modal-fullsettings-title">Promote independent publishing</h4>
|
||||||
|
<p>Show you’re a part of the indie publishing movement with a small badge in the footer</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="for-switch small">
|
||||||
|
<label
|
||||||
|
class="switch"
|
||||||
|
for="promote-ghost"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={{@newsletter.showBadge}}
|
||||||
|
id="promote-ghost"
|
||||||
|
name="promote-ghost"
|
||||||
|
{{on "click" (fn this.toggleSetting "showBadge")}}
|
||||||
|
>
|
||||||
|
<span class="input-toggle-component"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
27
ghost/admin/app/components/modals/edit-newsletter/design.js
Normal file
27
ghost/admin/app/components/modals/edit-newsletter/design.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import {IMAGE_EXTENSIONS} from 'ghost-admin/components/gh-image-uploader';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class EditNewsletterDesignForm extends Component {
|
||||||
|
@service settings;
|
||||||
|
|
||||||
|
imageExtensions = IMAGE_EXTENSIONS;
|
||||||
|
|
||||||
|
@action
|
||||||
|
imageUploaded(property, images) {
|
||||||
|
if (images[0]) {
|
||||||
|
this.args.newsletter[property] = images[0].url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
changeSetting(property, value) {
|
||||||
|
this.args.newsletter[property] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleSetting(property, event) {
|
||||||
|
this.args.newsletter[property] = event.target.checked;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
<div class="modal-fullsettings-preview-container gh-members-emailpreview">
|
||||||
|
<div class="gh-members-emailpreview-container">
|
||||||
|
<div class="gh-members-emailpreview-faux">
|
||||||
|
<p>
|
||||||
|
<span class="strong">{{or @newsletter.senderName this.config.blogTitle}}</span> <{{full-email-address (or @newsletter.senderEmail this.settings.membersFromAddress)}}>
|
||||||
|
</p>
|
||||||
|
<p><span class="dark">To:</span> Jamie Larson <jamie@example.com></p>
|
||||||
|
</div>
|
||||||
|
<div class="gh-members-emailpreview-contents">
|
||||||
|
{{#if @newsletter.headerImage}}
|
||||||
|
<div class="gh-members-emailpreview-header-image">
|
||||||
|
<img src={{@newsletter.headerImage}} alt="" role="presentation">
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.showHeader}}
|
||||||
|
<div class={{if @newsletter.showHeaderTitle "gh-members-emailpreview-header bordered" "gh-members-emailpreview-header"}}>
|
||||||
|
{{#if (and this.settings.icon @newsletter.showHeaderIcon)}}
|
||||||
|
<img src={{this.settings.icon}} alt="" role="presentation" />
|
||||||
|
{{/if}}
|
||||||
|
{{#if @newsletter.showHeaderTitle}}
|
||||||
|
<h4>{{or @newsletter.name this.config.blogTitle}}</h4>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="gh-members-emailpreview-title {{if (eq @newsletter.titleAlignment "left") "gh-members-emailpreview-title-left"}}">
|
||||||
|
<h2 class="{{if (eq @newsletter.titleFontCategory "serif") "serif"}}">Your email newsletter</h2>
|
||||||
|
<p>
|
||||||
|
<span>By {{or this.session.user.name this.session.user.email}} – {{moment-format (moment-site-tz) "D MMM YYYY"}} – </span> <a href="javascript:">View online →</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{#if @newsletter.showFeatureImage}}
|
||||||
|
<div class="gh-members-emailpreview-featureimage" style={{this.featureImageStyle}}></div>
|
||||||
|
<div class="gh-members-emailpreview-featureimage-caption">Feature image caption</div>
|
||||||
|
{{/if}}
|
||||||
|
<div class="gh-members-emailpreview-content {{if (eq @newsletter.bodyFontCategory "sans_serif") "sans-serif"}}">
|
||||||
|
<p>This is what your content will look like when you send one of your posts as an email newsletter to your subscribers.</p>
|
||||||
|
<p>Over there on the left 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>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>
|
||||||
|
</div>
|
||||||
|
<div class="gh-members-emailpreview-footer">
|
||||||
|
<div class="gh-members-emailpreview-footercontent">
|
||||||
|
{{html-safe @newsletter.footerContent}}
|
||||||
|
</div>
|
||||||
|
<div class="gh-members-emailpreview-footersite">
|
||||||
|
<span>{{this.config.blogTitle}} © {{moment-format (moment-site-tz) "YYYY"}} – </span> <a href="javascript:">Unsubscribe</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gh-members-emailpreview-badge {{unless @newsletter.showBadge "hide"}}">
|
||||||
|
<a href="javascript:"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0C18.6274 0 24 5.37258 24 12ZM11.8326 2.33879C6.37785 2.95189 3.95901 5.20797 3.41126 9.74699C3.34896 10.2632 3.22642 10.7805 3.10443 11.2954C2.93277 12.02 2.76221 12.74 2.76221 13.4458C2.76221 17.9885 6.5856 21.556 11.1283 21.556C12.8959 21.556 14.4433 20.8144 15.8756 20.048C19.0536 18.3478 22.0328 16.2597 22.0328 12.5411C22.0328 9.91512 20.1051 7.56932 18.466 5.5747C18.3834 5.47416 18.3015 5.37451 18.2206 5.27577C17.3866 4.25742 14.4333 2.04643 11.8326 2.33879Z" fill="black"/>
|
||||||
|
</svg> <span>Powered by Ghost</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
26
ghost/admin/app/components/modals/edit-newsletter/preview.js
Normal file
26
ghost/admin/app/components/modals/edit-newsletter/preview.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import {htmlSafe} from '@ember/template';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class EditNewsletterPreview extends Component {
|
||||||
|
@service config;
|
||||||
|
@service ghostPaths;
|
||||||
|
@service session;
|
||||||
|
@service settings;
|
||||||
|
|
||||||
|
get showHeader() {
|
||||||
|
return (this.args.newsletter.showHeaderIcon && this.settings.get('icon'))
|
||||||
|
|| this.args.newsletter.showHeaderTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
get featureImageUrl() {
|
||||||
|
// keep path separate so asset rewriting correctly picks it up
|
||||||
|
const imagePath = '/img/user-cover.png';
|
||||||
|
const fullPath = this.ghostPaths.assetRoot.replace(/\/$/, '') + imagePath;
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
get featureImageStyle() {
|
||||||
|
return htmlSafe(`background-image: url(${this.featureImageUrl})`);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
<div>
|
||||||
|
<fieldset class="modal-fullsettings-form">
|
||||||
|
<div class="modal-fullsettings-section first">
|
||||||
|
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="name">
|
||||||
|
<label for="newsletter-title" class="modal-fullsettings-title">Name</label>
|
||||||
|
<input
|
||||||
|
id="newsletter-title"
|
||||||
|
type="text"
|
||||||
|
class="gh-input miw-100 form-text"
|
||||||
|
value={{@newsletter.name}}
|
||||||
|
placeholder={{this.config.blogTitle}}
|
||||||
|
{{on "input" (fn this.onInput "name")}}
|
||||||
|
/>
|
||||||
|
<GhErrorMessage @errors={{@newsletter.errors}} @property="name" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="description">
|
||||||
|
<label for="newsletter-description" class="modal-fullsettings-title">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="newsletter-description"
|
||||||
|
class="gh-input miw-100 form-text"
|
||||||
|
{{on "input" (fn this.onInput "description")}}
|
||||||
|
>{{@newsletter.description}}</textarea>
|
||||||
|
<GhErrorMessage @errors={{@newsletter.errors}} @property="description" />
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-fullsettings-section divider-top">
|
||||||
|
<GhFormGroup @classNames="vertical" @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"
|
||||||
|
type="text"
|
||||||
|
class="gh-input miw-100 form-text"
|
||||||
|
value={{@newsletter.senderName}}
|
||||||
|
placeholder={{this.config.blogTitle}}
|
||||||
|
{{on "input" (fn this.onInput "senderName")}}
|
||||||
|
/>
|
||||||
|
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderName" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderEmail">
|
||||||
|
<label for="newsletter-sender-email" class="modal-fullsettings-title">Newsletter email address</label>
|
||||||
|
<input
|
||||||
|
id="newsletter-sender-email"
|
||||||
|
type="text"
|
||||||
|
class="gh-input miw-100 form-text"
|
||||||
|
value={{@newsletter.senderEmail}}
|
||||||
|
placeholder={{full-email-address this.settings.membersFromAddress}}
|
||||||
|
{{on "input" (fn this.onInput "senderEmail")}}
|
||||||
|
/>
|
||||||
|
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderEmail" />
|
||||||
|
</GhFormGroup>
|
||||||
|
|
||||||
|
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderReplyTo">
|
||||||
|
<label for="newsletter-reply-to" class="modal-fullsettings-title">Reply-to address</label>
|
||||||
|
<input
|
||||||
|
id="newsletter-reply-to"
|
||||||
|
type="text"
|
||||||
|
class="gh-input miw-100 form-text"
|
||||||
|
value={{@newsletter.senderReplyTo}}
|
||||||
|
placeholder={{full-email-address this.settings.membersFromAddress}}
|
||||||
|
{{on "input" (fn this.onInput "senderReplyTo")}}
|
||||||
|
/>
|
||||||
|
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderReplyTo" />
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-fullsettings-section divider-top">
|
||||||
|
<GhFormGroup>
|
||||||
|
<label for="subscribe-on-signup" class="modal-fullsettings-title">Subscribe new members on signup</label>
|
||||||
|
<div class="for-switch small">
|
||||||
|
<div class="container">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="subscribe-on-signup"
|
||||||
|
checked={{@newsletter.subscribeOnSignup}}
|
||||||
|
{{on "change" (fn this.onChange "subscribeOnSignup")}}
|
||||||
|
>
|
||||||
|
<button type="button" class="input-toggle-component" {{on "click" (fn this.toggleProperty "subscribeOnSignup")}}></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GhFormGroup>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
@ -0,0 +1,23 @@
|
|||||||
|
import Component from '@glimmer/component';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class EditNewsletterSettingsForm extends Component {
|
||||||
|
@service config;
|
||||||
|
@service settings;
|
||||||
|
|
||||||
|
@action
|
||||||
|
onChange(property, event) {
|
||||||
|
this.args.newsletter[property] = event.target.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleProperty(property) {
|
||||||
|
this.args.newsletter[property] = !this.args.newsletter[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onInput(property, event) {
|
||||||
|
this.args.newsletter[property] = event.target.value;
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{#if this.emailNewsletterEnabled}}
|
{{#if this.emailNewsletterEnabled}}
|
||||||
<Settings::MembersEmailLabs::NewsletterManagement @toggleEmailDesignSettings={{@toggleEmailDesignSettings}} />
|
<Settings::MembersEmailLabs::NewsletterManagement />
|
||||||
|
|
||||||
<section class="gh-main-section">
|
<section class="gh-main-section">
|
||||||
<h4 class="gh-main-section-header small bn">General settings</h4>
|
<h4 class="gh-main-section-header small bn">General settings</h4>
|
||||||
|
@ -20,7 +20,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
@tracked recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
@tracked recipientsSelectValue = this._getDerivedRecipientsSelectValue();
|
||||||
|
|
||||||
@tracked showFromAddressConfirmation = false;
|
@tracked showFromAddressConfirmation = false;
|
||||||
@tracked showEmailDesignSettings = false;
|
|
||||||
|
|
||||||
mailgunRegions = [US, EU];
|
mailgunRegions = [US, EU];
|
||||||
|
|
||||||
@ -78,11 +77,6 @@ export default class MembersEmailLabs extends Component {
|
|||||||
this.showFromAddressConfirmation = !this.showFromAddressConfirmation;
|
this.showFromAddressConfirmation = !this.showFromAddressConfirmation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
closeEmailDesignSettings() {
|
|
||||||
this.showEmailDesignSettings = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setMailgunDomain(event) {
|
setMailgunDomain(event) {
|
||||||
this.settings.set('mailgunDomain', event.target.value);
|
this.settings.set('mailgunDomain', event.target.value);
|
||||||
|
@ -25,8 +25,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<section class="gh-expandable">
|
<section class="gh-expandable">
|
||||||
<div class="gh-expandable-block">
|
<div class="gh-expandable-block">
|
||||||
{{#each this.newsletters.newsletters as |newsletter|}}
|
{{#if this.loadNewslettersTask.isRunning}}
|
||||||
{{#if (eq newsletter.status "active")}}
|
<div class="gh-main-content">... loading</div>
|
||||||
|
{{else}}
|
||||||
|
{{#each this.activeNewsletters as |newsletter|}}
|
||||||
<div class="gh-main-content-card gh-newsletter-card {{if this.hasMultiple "multiple"}}">
|
<div class="gh-main-content-card gh-newsletter-card {{if this.hasMultiple "multiple"}}">
|
||||||
{{svg-jar "grab" class="grab-newsletter"}}
|
{{svg-jar "grab" class="grab-newsletter"}}
|
||||||
<div class="gh-newsletter-card-block title-block">
|
<div class="gh-newsletter-card-block title-block">
|
||||||
@ -65,25 +67,25 @@
|
|||||||
@classNames="gh-newsletter-actions-menu dropdown-menu dropdown-triangle-top-right"
|
@classNames="gh-newsletter-actions-menu dropdown-menu dropdown-triangle-top-right"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<button class="mr2" type="button" {{on "click" @toggleEmailDesignSettings}}>
|
<LinkTo @route="settings.members-email-labs.edit-newsletter" @model={{newsletter.id}} class="mr2"><span>Edit</span></LinkTo>
|
||||||
<span>Edit</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button class="mr2" type="button" {{on "click" (fn this.archiveNewsletter newsletter.id)}}>
|
<button class="mr2" type="button" {{on "click" (perform this.archiveNewsletterTask newsletter)}}>
|
||||||
<span>Archive</span>
|
<span>Archive</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</GhDropdown>
|
</GhDropdown>
|
||||||
</span>
|
</span>
|
||||||
{{else}}
|
{{else}}
|
||||||
<button class="gh-btn gh-btn-green" type="button" {{on "click" @toggleEmailDesignSettings}}><span>Customize →</span></button>
|
<LinkTo @route="settings.members-email-labs.edit-newsletter" @model={{newsletter.id}} class="gh-btn gh-btn-green"><span>Customize →</span></LinkTo>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{else}}
|
||||||
|
<div class="gh-main-content">No newsletters found</div>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<button type="button" class="gh-add-newsletter" {{on "click" this.addNewsletter}} {{on "click" @toggleEmailDesignSettings}}>{{svg-jar "add-stroke"}}Add newsletter</button>
|
<LinkTo @route="settings.members-email-labs.new-newsletter" class="gh-add-newsletter">{{svg-jar "add-stroke"}}Add newsletter</LinkTo>
|
||||||
</div>
|
</div>
|
@ -1,30 +1,37 @@
|
|||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import {action} from '@ember/object';
|
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
import {task} from 'ember-concurrency';
|
||||||
|
|
||||||
export default class NewsletterManagementComponent extends Component {
|
export default class NewsletterManagementComponent extends Component {
|
||||||
@service newsletters;
|
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
|
newsletters = this.store.peekAll('newsletter');
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.loadNewslettersTask.perform();
|
||||||
|
}
|
||||||
|
|
||||||
get activeNewsletters() {
|
get activeNewsletters() {
|
||||||
return this.newsletters.newsletters.filter(n => n.status === 'active');
|
return this.newsletters.filter(n => n.status === 'active');
|
||||||
}
|
}
|
||||||
|
|
||||||
get archivedNewsletters() {
|
get archivedNewsletters() {
|
||||||
return this.newsletters.newsletters.filter(n => n.status === 'archived');
|
return this.newsletters.filter(n => n.status === 'archived');
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasMultiple() {
|
get hasMultiple() {
|
||||||
return this.activeNewsletters.length > 1;
|
return this.activeNewsletters.length > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@task
|
||||||
addNewsletter() {
|
*archiveNewsletterTask(newsletter) {
|
||||||
this.newsletters.add();
|
newsletter.status = 'archived';
|
||||||
|
return yield newsletter.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@task
|
||||||
archiveNewsletter(id) {
|
*loadNewslettersTask() {
|
||||||
this.newsletters.archive(id);
|
return yield this.store.findAll('newsletter');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,48 +9,16 @@ export default class MembersEmailLabsController extends Controller {
|
|||||||
@service session;
|
@service session;
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
queryParams = ['showEmailDesignSettings'];
|
|
||||||
|
|
||||||
// from/supportAddress are set here so that they can be reset to saved values on save
|
// from/supportAddress are set here so that they can be reset to saved values on save
|
||||||
// to avoid it looking like they've been saved when they have a separate update process
|
// to avoid it looking like they've been saved when they have a separate update process
|
||||||
@tracked fromAddress = '';
|
@tracked fromAddress = '';
|
||||||
@tracked supportAddress = '';
|
@tracked supportAddress = '';
|
||||||
|
|
||||||
@tracked showEmailDesignSettings = false;
|
|
||||||
@tracked showLeaveSettingsModal = false;
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setEmailAddress(property, email) {
|
setEmailAddress(property, email) {
|
||||||
this[property] = email;
|
this[property] = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
toggleEmailDesignSettings() {
|
|
||||||
this.showEmailDesignSettings = !this.showEmailDesignSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveRoute(transition) {
|
|
||||||
if (this.settings.get('hasDirtyAttributes')) {
|
|
||||||
transition.abort();
|
|
||||||
this.leaveSettingsTransition = transition;
|
|
||||||
this.showLeaveSettingsModal = true;
|
|
||||||
}
|
|
||||||
this.showEmailDesignSettings = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async confirmLeave() {
|
|
||||||
this.settings.rollbackAttributes();
|
|
||||||
this.showLeaveSettingsModal = false;
|
|
||||||
this.leaveSettingsTransition.retry();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
cancelLeave() {
|
|
||||||
this.showLeaveSettingsModal = false;
|
|
||||||
this.leaveSettingsTransition = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
parseEmailAddress(address) {
|
parseEmailAddress(address) {
|
||||||
const emailAddress = address || 'noreply';
|
const emailAddress = address || 'noreply';
|
||||||
// Adds default domain as site domain
|
// Adds default domain as site domain
|
||||||
|
@ -4,7 +4,7 @@ import {inject as service} from '@ember/service';
|
|||||||
export default class FullEmailAddressHelper extends Helper {
|
export default class FullEmailAddressHelper extends Helper {
|
||||||
@service config;
|
@service config;
|
||||||
|
|
||||||
compute([email]) {
|
compute([email = '']) {
|
||||||
if (email.indexOf('@') > -1) {
|
if (email.indexOf('@') > -1) {
|
||||||
return email;
|
return email;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import MemberValidator from 'ghost-admin/validators/member';
|
|||||||
import Mixin from '@ember/object/mixin';
|
import Mixin from '@ember/object/mixin';
|
||||||
import Model from '@ember-data/model';
|
import Model from '@ember-data/model';
|
||||||
import NavItemValidator from 'ghost-admin/validators/nav-item';
|
import NavItemValidator from 'ghost-admin/validators/nav-item';
|
||||||
|
import NewsletterValidator from 'ghost-admin/validators/newsletter';
|
||||||
import OfferValidator from 'ghost-admin/validators/offer';
|
import OfferValidator from 'ghost-admin/validators/offer';
|
||||||
import PostValidator from 'ghost-admin/validators/post';
|
import PostValidator from 'ghost-admin/validators/post';
|
||||||
import ProductBenefitItemValidator from 'ghost-admin/validators/product-benefit-item';
|
import ProductBenefitItemValidator from 'ghost-admin/validators/product-benefit-item';
|
||||||
@ -75,7 +76,8 @@ export default Mixin.create({
|
|||||||
label: LabelValidator,
|
label: LabelValidator,
|
||||||
snippet: SnippetValidator,
|
snippet: SnippetValidator,
|
||||||
product: ProductValidator,
|
product: ProductValidator,
|
||||||
offer: OfferValidator
|
offer: OfferValidator,
|
||||||
|
newsletter: NewsletterValidator
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
32
ghost/admin/app/models/newsletter.js
Normal file
32
ghost/admin/app/models/newsletter.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Model, {attr} from '@ember-data/model';
|
||||||
|
import ValidationEngine from '../mixins/validation-engine';
|
||||||
|
|
||||||
|
export default class Newsletter extends Model.extend(ValidationEngine) {
|
||||||
|
validationType = 'newsletter';
|
||||||
|
|
||||||
|
@attr name;
|
||||||
|
@attr description;
|
||||||
|
|
||||||
|
@attr senderName;
|
||||||
|
@attr senderEmail;
|
||||||
|
@attr senderReplyTo;
|
||||||
|
|
||||||
|
@attr({defaultValue: 'active'}) status;
|
||||||
|
@attr({defaultValue: ''}) recipientFilter;
|
||||||
|
@attr({defaultValue: false}) subscribeOnSignup;
|
||||||
|
@attr({defaultValue: 0}) sortOrder;
|
||||||
|
|
||||||
|
// Design-related properties - TODO: not currently supported in API
|
||||||
|
@attr headerImage;
|
||||||
|
@attr({defaultValue: true}) showHeaderIcon;
|
||||||
|
@attr({defaultValue: true}) showHeaderTitle;
|
||||||
|
@attr({defaultValue: 'sans_serif'}) titleFontCategory;
|
||||||
|
@attr({defaultValue: 'center'}) titleAlignment;
|
||||||
|
@attr({defaultValue: true}) showFeatureImage;
|
||||||
|
@attr({defaultValue: 'sans_serif'}) bodyFontCategory;
|
||||||
|
@attr() footerContent;
|
||||||
|
@attr({defaultValue: true}) showBadge;
|
||||||
|
|
||||||
|
// TODO: delete attr, incorrectly needed for save to complete in API
|
||||||
|
@attr({defaultValue: false}) default;
|
||||||
|
}
|
@ -43,9 +43,13 @@ Router.map(function () {
|
|||||||
this.route('settings.general', {path: '/settings/general'});
|
this.route('settings.general', {path: '/settings/general'});
|
||||||
this.route('settings.membership', {path: '/settings/members'});
|
this.route('settings.membership', {path: '/settings/members'});
|
||||||
this.route('settings.members-email', {path: '/settings/members-email'});
|
this.route('settings.members-email', {path: '/settings/members-email'});
|
||||||
this.route('settings.members-email-labs', {path: '/settings/members-email-labs'});
|
|
||||||
this.route('settings.code-injection', {path: '/settings/code-injection'});
|
this.route('settings.code-injection', {path: '/settings/code-injection'});
|
||||||
|
|
||||||
|
this.route('settings.members-email-labs', {path: '/settings/members-email-labs'}, function () {
|
||||||
|
this.route('new-newsletter', {path: '/newsletters/new'});
|
||||||
|
this.route('edit-newsletter', {path: '/newsletters/:newsletter_id'});
|
||||||
|
});
|
||||||
|
|
||||||
this.route('settings.design', {path: '/settings/design'}, function () {
|
this.route('settings.design', {path: '/settings/design'}, function () {
|
||||||
this.route('change-theme', function () {
|
this.route('change-theme', function () {
|
||||||
this.route('view', {path: ':theme_name'});
|
this.route('view', {path: ':theme_name'});
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import AdminRoute from 'ghost-admin/routes/admin';
|
import AdminRoute from 'ghost-admin/routes/admin';
|
||||||
|
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
|
||||||
import {action} from '@ember/object';
|
import {action} from '@ember/object';
|
||||||
import {inject as service} from '@ember/service';
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
export default class MembersEmailLabsRoute extends AdminRoute {
|
export default class MembersEmailLabsRoute extends AdminRoute {
|
||||||
@service feature;
|
@service feature;
|
||||||
|
@service modals;
|
||||||
@service notifications;
|
@service notifications;
|
||||||
@service settings;
|
@service settings;
|
||||||
|
|
||||||
|
confirmModal = null;
|
||||||
|
hasConfirmed = false;
|
||||||
|
|
||||||
beforeModel(transition) {
|
beforeModel(transition) {
|
||||||
super.beforeModel(...arguments);
|
super.beforeModel(...arguments);
|
||||||
|
|
||||||
@ -32,7 +37,44 @@ export default class MembersEmailLabsRoute extends AdminRoute {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
willTransition(transition) {
|
willTransition(transition) {
|
||||||
return this.controller.leaveRoute(transition);
|
if (this.hasConfirmed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// always abort when not confirmed because Ember's router doesn't automatically wait on promises
|
||||||
|
transition.abort();
|
||||||
|
|
||||||
|
this.confirmUnsavedChanges().then((shouldLeave) => {
|
||||||
|
if (shouldLeave) {
|
||||||
|
this.hasConfirmed = true;
|
||||||
|
return transition.retry();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.confirmModal = null;
|
||||||
|
this.hasConfirmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmUnsavedChanges() {
|
||||||
|
if (!this.settings.get('hasDirtyAttributes')) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.confirmModal) {
|
||||||
|
this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal)
|
||||||
|
.then((discardChanges) => {
|
||||||
|
if (discardChanges === true) {
|
||||||
|
this.settings.rollbackAttributes();
|
||||||
|
}
|
||||||
|
return discardChanges;
|
||||||
|
}).finally(() => {
|
||||||
|
this.confirmModal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.confirmModal;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildRouteInfoMetadata() {
|
buildRouteInfoMetadata() {
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import AdminRoute from 'ghost-admin/routes/admin';
|
||||||
|
import EditNewsletterModal from 'ghost-admin/components/modals/edit-newsletter';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class EditNewsletterRoute extends AdminRoute {
|
||||||
|
@service modals;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
newsletterModal = null;
|
||||||
|
|
||||||
|
model(params) {
|
||||||
|
return this.store.find('newsletter', params.newsletter_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this.newsletterModal?.close();
|
||||||
|
|
||||||
|
this.newsletterModal = this.modals.open(EditNewsletterModal, {
|
||||||
|
newsletter: model,
|
||||||
|
afterSave: this.afterSave
|
||||||
|
}, {
|
||||||
|
beforeClose: this.beforeModalClose
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.isLeaving = true;
|
||||||
|
this.newsletterModal?.close();
|
||||||
|
|
||||||
|
this.isLeaving = false;
|
||||||
|
this.newsletterModal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
afterSave() {
|
||||||
|
this.router.transitionTo('settings.members-email-labs');
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
beforeModalClose() {
|
||||||
|
if (this.newsletterModal && !this.isLeaving) {
|
||||||
|
this.router.transitionTo('settings.members-email-labs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import AdminRoute from 'ghost-admin/routes/admin';
|
||||||
|
import EditNewsletterModal from 'ghost-admin/components/modals/edit-newsletter';
|
||||||
|
import {action} from '@ember/object';
|
||||||
|
import {inject as service} from '@ember/service';
|
||||||
|
|
||||||
|
export default class NewNewsletterRoute extends AdminRoute {
|
||||||
|
@service modals;
|
||||||
|
@service router;
|
||||||
|
@service store;
|
||||||
|
|
||||||
|
newsletterModal = null;
|
||||||
|
|
||||||
|
model() {
|
||||||
|
return this.store.createRecord('newsletter');
|
||||||
|
}
|
||||||
|
|
||||||
|
setupController(controller, model) {
|
||||||
|
this.newsletterModal?.close();
|
||||||
|
|
||||||
|
this.newsletterModal = this.modals.open(EditNewsletterModal, {
|
||||||
|
newsletter: model,
|
||||||
|
afterSave: this.afterSave
|
||||||
|
}, {
|
||||||
|
beforeClose: this.beforeModalClose
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deactivate() {
|
||||||
|
this.isLeaving = true;
|
||||||
|
this.newsletterModal?.close();
|
||||||
|
|
||||||
|
this.isLeaving = false;
|
||||||
|
this.newsletterModal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
afterSave() {
|
||||||
|
this.router.transitionTo('settings.members-email-labs');
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
beforeModalClose() {
|
||||||
|
if (this.newsletterModal && !this.isLeaving) {
|
||||||
|
this.router.transitionTo('settings.members-email-labs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,94 +0,0 @@
|
|||||||
import Service, {inject as service} from '@ember/service';
|
|
||||||
import {TrackedArray} from 'tracked-built-ins';
|
|
||||||
import {set} from '@ember/object';
|
|
||||||
import {tracked} from '@glimmer/tracking';
|
|
||||||
|
|
||||||
class Newsletter {
|
|
||||||
@tracked name;
|
|
||||||
@tracked description;
|
|
||||||
@tracked sender_name;
|
|
||||||
@tracked sender_email;
|
|
||||||
@tracked sender_reply_to;
|
|
||||||
@tracked default;
|
|
||||||
@tracked status;
|
|
||||||
@tracked recipient_filter;
|
|
||||||
@tracked subscribe_on_signup;
|
|
||||||
@tracked sort_order;
|
|
||||||
|
|
||||||
constructor(obj) {
|
|
||||||
Object.assign(this, obj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let counter = 0;
|
|
||||||
function getFakeNewsletter() {
|
|
||||||
counter += 1;
|
|
||||||
return new Newsletter({
|
|
||||||
id: Math.floor(Math.random() * 1e9),
|
|
||||||
name: 'Daily roundup ' + counter,
|
|
||||||
description: 'Daily news delivered to your inbox every morning.',
|
|
||||||
sender_name: 'Test',
|
|
||||||
sender_email: 'test@example.com',
|
|
||||||
sender_reply_to: 'test@example.com',
|
|
||||||
default: false,
|
|
||||||
status: 'active',
|
|
||||||
recipient_filter: '',
|
|
||||||
subscribe_on_signup: true,
|
|
||||||
sort_order: counter,
|
|
||||||
members: {
|
|
||||||
total: Math.floor(Math.random() * 100)
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
total: Math.floor(Math.random() * 100)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class NewslettersService extends Service {
|
|
||||||
@service config;
|
|
||||||
@service settings;
|
|
||||||
@service feature;
|
|
||||||
@service store;
|
|
||||||
|
|
||||||
newsletters = new TrackedArray([
|
|
||||||
new Newsletter({
|
|
||||||
id: '123',
|
|
||||||
name: 'Daily roundup',
|
|
||||||
description: 'Daily news delivered to your inbox every morning.',
|
|
||||||
sender_name: 'Test',
|
|
||||||
sender_email: 'test@example.com',
|
|
||||||
sender_reply_to: 'test@example.com',
|
|
||||||
default: true,
|
|
||||||
status: 'active',
|
|
||||||
recipient_filter: '',
|
|
||||||
subscribe_on_signup: true,
|
|
||||||
sort_order: 0,
|
|
||||||
members: {
|
|
||||||
total: 19
|
|
||||||
},
|
|
||||||
posts: {
|
|
||||||
total: 17
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
|
|
||||||
add() {
|
|
||||||
this.newsletters.push(getFakeNewsletter());
|
|
||||||
this.newsletters.sort((a,b) => a.sort_order - b.sort_order);
|
|
||||||
return this.newsletters;
|
|
||||||
}
|
|
||||||
|
|
||||||
archive(newsletterId) {
|
|
||||||
const newsletter = this.newsletters.find(n => n.id === newsletterId);
|
|
||||||
if (newsletter) {
|
|
||||||
set(newsletter, 'status', 'archived');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unArchive(newsletterId) {
|
|
||||||
const newsletter = this.newsletters.find(n => n.id === newsletterId);
|
|
||||||
if (newsletter) {
|
|
||||||
set(newsletter, 'status', 'active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,25 +24,7 @@
|
|||||||
@fromAddress={{this.fromAddress}}
|
@fromAddress={{this.fromAddress}}
|
||||||
@supportAddress={{this.supportAddress}}
|
@supportAddress={{this.supportAddress}}
|
||||||
@setEmailAddress={{this.setEmailAddress}}
|
@setEmailAddress={{this.setEmailAddress}}
|
||||||
@toggleEmailDesignSettings={{this.toggleEmailDesignSettings}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{#if this.showLeaveSettingsModal}}
|
|
||||||
<GhFullscreenModal
|
|
||||||
@modal="leave-settings"
|
|
||||||
@confirm={{this.confirmLeave}}
|
|
||||||
@close={{this.cancelLeave}}
|
|
||||||
@modifier="action wide"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{{#if this.showEmailDesignSettings}}
|
|
||||||
<GhFullscreenModal @modifier="full-overlay portal-settings">
|
|
||||||
<ModalEmailDesignSettings
|
|
||||||
@closeModal={{this.toggleEmailDesignSettings}}
|
|
||||||
/>
|
|
||||||
</GhFullscreenModal>
|
|
||||||
{{/if}}
|
|
69
ghost/admin/app/validators/newsletter.js
Normal file
69
ghost/admin/app/validators/newsletter.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import BaseValidator from './base';
|
||||||
|
import validator from 'validator';
|
||||||
|
import {isBlank} from '@ember/utils';
|
||||||
|
|
||||||
|
export default BaseValidator.create({
|
||||||
|
properties: ['name', 'senderName', 'senderEmail', 'senderReplyTo'],
|
||||||
|
|
||||||
|
name(model) {
|
||||||
|
if (isBlank(model.name)) {
|
||||||
|
model.errors.add('name', 'Please enter a name.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(model.name || '', 0, 191)) {
|
||||||
|
model.errors.add('name', 'Name cannot be longer than 191 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
model.hasValidated.addObject('name');
|
||||||
|
},
|
||||||
|
|
||||||
|
senderName(model) {
|
||||||
|
if (isBlank(model.senderName)) {
|
||||||
|
model.errors.add('senderName', 'Please enter a sender name.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(model.senderName || '', 0, 191)) {
|
||||||
|
model.errors.add('senderName', 'Sender name cannot be longer than 191 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
model.hasValidated.addObject('senderName');
|
||||||
|
},
|
||||||
|
|
||||||
|
senderEmail(model) {
|
||||||
|
if (isBlank(model.senderEmail)) {
|
||||||
|
model.errors.add('senderEmail', 'Please enter a newsletter email address.');
|
||||||
|
this.invalidate();
|
||||||
|
} else if (!validator.isEmail(model.senderEmail)) {
|
||||||
|
model.errors.add('senderEmail', 'Invalid email.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(model.senderEmail || '', 0, 191)) {
|
||||||
|
model.errors.add('senderEmail', 'Sender email cannot be longer than 191 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
model.hasValidated.addObject('senderEmail');
|
||||||
|
},
|
||||||
|
|
||||||
|
senderReplyTo(model) {
|
||||||
|
if (isBlank(model.senderReplyTo)) {
|
||||||
|
model.errors.add('senderReplyTo', 'Please enter a reply-to email address.');
|
||||||
|
this.invalidate();
|
||||||
|
} else if (!validator.isEmail(model.senderReplyTo)) {
|
||||||
|
model.errors.add('senderReplyTo', 'Invalid email.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(model.senderReplyTo || '', 0, 191)) {
|
||||||
|
model.errors.add('senderReplyTo', 'Reply-to email cannot be longer than 191 characters.');
|
||||||
|
this.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
model.hasValidated.addObject('senderReplyTo');
|
||||||
|
}
|
||||||
|
});
|
@ -8,6 +8,7 @@ import mockIntegrations from './config/integrations';
|
|||||||
import mockInvites from './config/invites';
|
import mockInvites from './config/invites';
|
||||||
import mockLabels from './config/labels';
|
import mockLabels from './config/labels';
|
||||||
import mockMembers from './config/members';
|
import mockMembers from './config/members';
|
||||||
|
import mockNewsletters from './config/newsletters';
|
||||||
import mockOffers from './config/offers';
|
import mockOffers from './config/offers';
|
||||||
import mockPages from './config/pages';
|
import mockPages from './config/pages';
|
||||||
import mockPosts from './config/posts';
|
import mockPosts from './config/posts';
|
||||||
@ -38,7 +39,7 @@ export default function () {
|
|||||||
// this.put('/posts/:id/', versionMismatchResponse);
|
// this.put('/posts/:id/', versionMismatchResponse);
|
||||||
// mockTags(this);
|
// mockTags(this);
|
||||||
// this.loadFixtures('settings');
|
// this.loadFixtures('settings');
|
||||||
mockSnippets(this);
|
mockNewsletters(this);
|
||||||
|
|
||||||
// keep this line, it allows all other API requests to hit the real server
|
// keep this line, it allows all other API requests to hit the real server
|
||||||
this.passthrough();
|
this.passthrough();
|
||||||
@ -78,6 +79,8 @@ export function testConfig() {
|
|||||||
mockWebhooks(this);
|
mockWebhooks(this);
|
||||||
mockProducts(this);
|
mockProducts(this);
|
||||||
mockOffers(this);
|
mockOffers(this);
|
||||||
|
mockSnippets(this);
|
||||||
|
mockNewsletters(this);
|
||||||
|
|
||||||
/* Notifications -------------------------------------------------------- */
|
/* Notifications -------------------------------------------------------- */
|
||||||
|
|
||||||
|
8
ghost/admin/mirage/config/newsletters.js
Normal file
8
ghost/admin/mirage/config/newsletters.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import {paginatedResponse} from '../utils';
|
||||||
|
|
||||||
|
export default function mockNewsletters(server) {
|
||||||
|
server.post('/newsletters/');
|
||||||
|
server.get('/newsletters/', paginatedResponse('newsletters'));
|
||||||
|
server.get('/newsletters/:id/');
|
||||||
|
server.put('/newsletters/:id/');
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user