Switched to a minimal form when creating a newsletter (#2356)

no issue

The full edit newsletter form with all the settings, design options, and preview felt quite overwhelming when the only piece of data that's required to create a newsletter is the name.

- re-organised the newsletter modal components by renaming `modals/edit-newlsetter` to `modals/newsletters` to better represent the full suite of modals that are used in newsletter management
- added a `modals/newsletters/new` component containing a minimal form with name/description/opt-in-existing fields
- switched the `new-newsletter` route to open the new modal rather than the previous dual-purpose edit modal
- moved message about newsletter creation into the create modal and dropped the separate create confirmation modal
- dropped unnecessary unsaved-changes confirmation
- removed the now-unused opt-in-existing behaviour from the edit newsletter modal

Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
Kevin Ansfield 2022-04-27 10:36:43 +01:00 committed by GitHub
parent 26582592c7
commit 25f4694d06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 151 additions and 144 deletions

View File

@ -1,37 +0,0 @@
<div class="modal-content">
<header class="modal-header" data-test-modal="confirm-newsletter-create">
<h1>{{@data.newsletter.name}}</h1>
</header>
<button type="button" class="close" role="button" title="Close" {{on "click" (fn @close false)}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
<p>
{{#if @data.optInExisting}}
{{#let (members-count-fetcher query=(hash filter="newsletters.status:active")) as |countFetcher|}}
This newsletter will be available to <strong>all members</strong>. Your {{#if countFetcher.count}}<strong>{{countFetcher.count}}</strong>{{/if}} existing subscriber{{#if (gt countFetcher.count 1)}}s{{/if}} will also be opted-in to receive it. Sound good?
{{/let}}
{{else}}
The newsletter will be available to <strong>all new members</strong>. Existing members wont be subscribed, but may visit their account area to opt-in to future emails.
{{/if}}
</p>
</div>
<div class="modal-footer">
<button
type="button"
class="gh-btn"
{{on "click" (fn @close false)}}
>
<span>Back to edit</span>
</button>
<button
type="button"
class="gh-btn gh-btn-black"
{{on "click" (fn @close true)}}
{{on-key "Enter"}}
>
<span>Create newsletter</span>
</button>
</div>
</div>

View File

@ -13,13 +13,11 @@
</div>
{{#if (eq this.tab "settings")}}
<Modals::EditNewsletter::Settings
<Modals::Newsletters::Edit::Settings
@newsletter={{@data.newsletter}}
@optInExisting={{this.optInExisting}}
@setOptInExisting={{this.setOptInExisting}}
/>
{{else}}
<Modals::EditNewsletter::Design @newsletter={{@data.newsletter}} />
<Modals::Newsletters::Edit::Design @newsletter={{@data.newsletter}} />
{{/if}}
</div>
@ -39,7 +37,7 @@
data-test-button="save-newsletter"
/>
</div>
<Modals::EditNewsletter::Preview @newsletter={{@data.newsletter}} />
<Modals::Newsletters::Edit::Preview @newsletter={{@data.newsletter}} />
</div>
</div>
</div>

View File

@ -1,6 +1,5 @@
import Component from '@glimmer/component';
import ConfirmCreateModal from './edit-newsletter/confirm-create';
import ConfirmNewsletterEmailModal from './edit-newsletter/confirm-newsletter-email';
import ConfirmNewsletterEmailModal from './confirm-newsletter-email';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
@ -14,7 +13,6 @@ export default class EditNewsletterModal extends Component {
};
@tracked tab = 'settings';
@tracked optInExisting = this.args.data.newsletter.isNew;
willDestroy() {
super.willDestroy(...arguments);
@ -34,35 +32,14 @@ export default class EditNewsletterModal extends Component {
this.saveTask.perform();
}
@action
setOptInExisting(value) {
this.optInExisting = value;
}
@task
*saveTask() {
try {
yield this.args.data.newsletter.validate({});
const {optInExisting} = this;
if (this.args.data.newsletter.isNew) {
const shouldCreate = yield this.modals.open(ConfirmCreateModal, {
optInExisting,
newsletter: this.args.data.newsletter
});
if (!shouldCreate) {
// ensure task button returns to idle state
return 'canceled';
}
}
const newEmail = this.args.data.newsletter.senderEmail;
const result = yield this.args.data.newsletter.save({
adapterOptions: {optInExisting}
});
const result = yield this.args.data.newsletter.save();
if (result._meta?.sent_email_verification) {
yield this.modals.open(ConfirmNewsletterEmailModal, {

View File

@ -41,7 +41,7 @@
<GhErrorMessage @errors={{@newsletter.errors}} @property="senderName" />
</GhFormGroup>
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderEmail">
<GhFormGroup @classNames="vertical" @errors={{@newsletter.errors}} @hasValidated={{@newsletter.hasValidated}} @property="senderEmail">
<span class="flex items-center justify-between">
<label for="newsletter-sender-email" class="modal-fullsettings-title ml2">Newsletter email address</label>
<span class="tooltip-top-left" data-tooltip="Defaults to {{full-email-address "noreply"}} if empty">{{svg-jar "info" class="fill-darkgrey w4 h4"}}</span>
@ -88,23 +88,6 @@
</div>
</div>
</GhFormGroup>
{{#if @newsletter.isNew}}
<GhFormGroup>
<label for="opt-in-existing" class="modal-fullsettings-title">Opt-in existing subscribers</label>
<div class="for-switch small">
<div class="container">
<input
type="checkbox"
id="opt-in-existing"
checked={{@optInExisting}}
{{on "check" this.setOptInExisting}}
>
<button type="button" class="input-toggle-component" {{on "click" this.toggleOptInExisting}}></button>
</div>
</div>
</GhFormGroup>
{{/if}}
</div>
</fieldset>
</div>

View File

@ -0,0 +1,77 @@
<div class="modal-content">
<header class="modal-header">
<h1>Create newsletter</h1>
</header>
<button type="button" class="close" title="Close" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
<fieldset>
<GhFormGroup @errors={{@data.newsletter.errors}} @hasValidated={{@data.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={{@data.newsletter.name}}
placeholder="Weekly Roundup"
{{on "input" (fn this.onInput "name")}}
/>
<GhErrorMessage @errors={{@data.newsletter.errors}} @property="name" />
</GhFormGroup>
<GhFormGroup @errors={{@data.newsletter.errors}} @hasValidated={{@data.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")}}
>{{@data.newsletter.description}}</textarea>
<GhErrorMessage @errors={{@data.newsletter.errors}} @property="description" />
</GhFormGroup>
<GhFormGroup @classNames="flex justify-between items-start mb2">
<div class="mr3">
<label for="opt-in-existing" class="modal-fullsettings-title">Opt-in existing subscribers</label>
<p>
{{#if this.optInExisting}}
{{#let (members-count-fetcher query=(hash filter="newsletters.status:active")) as |countFetcher|}}
This newsletter will be available to <strong>all members</strong>. Your {{#if countFetcher.count}}<strong>{{countFetcher.count}}</strong>{{/if}} existing subscriber{{#if (gt countFetcher.count 1)}}s{{/if}} will also be opted-in to receive it.
{{/let}}
{{else}}
The newsletter will be available to <strong>all new members</strong>. Existing members wont be subscribed, but may visit their account area to opt-in to future emails.
{{/if}}
</p>
</div>
<div class="for-switch small">
<div class="container">
<input
type="checkbox"
id="opt-in-existing"
checked={{this.optInExisting}}
{{on "check" this.setOptInExisting}}
>
<button type="button" class="input-toggle-component" {{on "click" this.toggleOptInExisting}}></button>
</div>
</div>
</GhFormGroup>
</fieldset>
</div>
<div class="modal-footer">
<button class="gh-btn" type="button" {{on "click" @close}}>
<span>Cancel</span>
</button>
<GhTaskButton
@buttonText="Create"
@runningText="Creating"
@successText="Created"
@task={{this.saveTask}}
@idleClass="gh-btn-primary"
@class="gh-btn gh-btn-icon"
{{on-key "cmd+s" this.saveViaKeyboard priority=1}}
{{on-key "Enter" this.saveViaKeyboard priority=1}}
data-test-button="save-newsletter"
/>
</div>
</div>

View File

@ -0,0 +1,61 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class NewNewsletterModal extends Component {
@service modals;
@tracked optInExisting = this.args.data.newsletter.isNew;
willDestroy() {
super.willDestroy(...arguments);
this.args.data.newsletter.rollbackAttributes();
}
@action
onInput(property, event) {
this.args.data.newsletter[property] = event.target.value;
}
@action
saveViaKeyboard(event, responder) {
responder.stopPropagation();
event.preventDefault();
this.saveTask.perform();
}
@action
setOptInExisting(event) {
this.optInExisting = event.target.value;
}
@action
toggleOptInExisting() {
this.optInExisting = !this.optInExisting;
}
@task
*saveTask() {
try {
yield this.args.data.newsletter.validate({});
const result = yield this.args.data.newsletter.save({
adapterOptions: {optInExisting: this.optInExisting}
});
this.args.data.afterSave?.(result);
return result;
} catch (e) {
if (e === undefined) {
// ensure task button shows failed state
throw new Error('Validation failed');
}
throw e;
}
}
}

View File

@ -1,6 +1,6 @@
import Component from '@glimmer/component';
import ConfirmArchiveModal from '../../modals/edit-newsletter/confirm-archive';
import ConfirmUnarchiveModal from '../../modals/edit-newsletter/confirm-unarchive';
import ConfirmArchiveModal from '../../modals/newsletters/confirm-archive';
import ConfirmUnarchiveModal from '../../modals/newsletters/confirm-unarchive';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';

View File

@ -1,6 +1,6 @@
import AdminRoute from 'ghost-admin/routes/admin';
import ConfirmUnsavedChangesModal from '../../components/modals/confirm-unsaved-changes';
import VerifyNewsletterEmail from '../../components/modals/edit-newsletter/verify-newsletter-email';
import VerifyNewsletterEmail from '../../components/modals/newsletters/verify-newsletter-email';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';

View File

@ -1,6 +1,6 @@
import AdminRoute from 'ghost-admin/routes/admin';
import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes';
import EditNewsletterModal from '../../../components/modals/edit-newsletter';
import EditNewsletterModal from '../../../components/modals/newsletters/edit';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';

View File

@ -1,6 +1,5 @@
import AdminRoute from 'ghost-admin/routes/admin';
import ConfirmUnsavedChangesModal from '../../../components/modals/confirm-unsaved-changes';
import EditNewsletterModal from '../../../components/modals/edit-newsletter';
import NewNewsletterModal from '../../../components/modals/newsletters/new';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
@ -19,7 +18,7 @@ export default class NewNewsletterRoute extends AdminRoute {
setupController(controller, model) {
this.newsletterModal?.close();
this.newsletterModal = this.modals.open(EditNewsletterModal, {
this.newsletterModal = this.modals.open(NewNewsletterModal, {
newsletter: model,
afterSave: this.afterSave
}, {
@ -35,64 +34,13 @@ export default class NewNewsletterRoute extends AdminRoute {
deactivate() {
this.isLeaving = true;
this.newsletterModal?.close();
this.isLeaving = false;
this.newsletterModal = null;
this.confirmModal = null;
this.hasConfirmed = false;
}
@action
async willTransition(transition) {
if (this.hasConfirmed) {
return true;
}
transition.abort();
// wait for any existing confirm modal to be closed before allowing transition
if (this.confirmModal) {
return;
}
const shouldLeave = await this.confirmUnsavedChanges();
if (shouldLeave) {
this.hasConfirmed = true;
return transition.retry();
}
}
async confirmUnsavedChanges() {
const newsletter = this.newsletterModal?._data.newsletter;
if (newsletter && newsletter.hasDirtyAttributes && Object.keys(newsletter.changedAttributes()).length > 0) {
this.confirmModal = this.modals.open(ConfirmUnsavedChangesModal)
.then((discardChanges) => {
if (discardChanges === true) {
newsletter.rollbackAttributes();
}
return discardChanges;
}).finally(() => {
this.confirmModal = null;
});
return this.confirmModal;
}
return true;
}
@action
async beforeModalClose() {
const shouldLeave = await this.confirmUnsavedChanges();
if (shouldLeave && !this.isLeaving) {
if (!this.isLeaving) {
this.router.transitionTo('settings.members-email-labs');
return true;
}
return false;
}
}