Cleaned up old publish menu code

no issue

- the new publishing flow is GA making the old publish menu unused
- removed all related code
This commit is contained in:
Kevin Ansfield 2022-05-16 18:07:45 +01:00
parent 7521d69405
commit 77484210ee
28 changed files with 6 additions and 2459 deletions

View File

@ -10,9 +10,7 @@ export default class Post extends ApplicationAdapter {
const newsletter = snapshot.adapterOptions.newsletter;
parsedUrl.searchParams.append('newsletter', newsletter);
// TODO: cleanup sendEmailWhenPublished when removing publishingFlow flag
let emailSegment = snapshot?.adapterOptions?.emailSegment
|| snapshot?.adapterOptions?.sendEmailWhenPublished;
let emailSegment = snapshot?.adapterOptions?.emailSegment;
if (emailSegment) {
if (emailSegment === 'status:free,status:-free') {

View File

@ -1,17 +0,0 @@
<span class="gh-publishmenu-select" ...attributes>
<PowerSelect
@options={{this.availablePublishActions}}
@renderInPlace={{true}}
@selected={{this.distributionValue}}
@onChange={{this.setDistributionAction}}
@searchEnabled={{false}}
@id="publish-action"
@name="publish-action"
@triggerComponent="gh-power-select/trigger"
@triggerClass="ember-power-select-inline"
@dropdownClass="gh-publishmenu-select-dropdown"
as |availablePublishAction|
>
{{availablePublishAction.name}}
</PowerSelect>
</span>

View File

@ -1,24 +0,0 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
export default class GhDistributionActionSelect extends Component {
availablePublishActions = [{
value: 'publish_send',
name: 'publish & send'
}, {
value: 'publish',
name: 'publish'
}, {
value: 'send',
name: 'send'
}];
get distributionValue() {
return this.availablePublishActions.findBy('value', this.args.distributionAction);
}
@action
setDistributionAction(newAction) {
this.args.setDistributionAction(newAction.value);
}
}

View File

@ -1,94 +0,0 @@
<div ...attributes>
<header class="gh-publishmenu-heading" data-test-publishmenu-header>Ready to
{{#if @canSendEmail}}
<GhDistributionActionSelect
@distributionAction={{@distributionAction}}
@setDistributionAction={{@setDistributionAction}}
data-test-distribution-action-select
/>
{{else}}
publish
{{/if}}
this {{@post.displayName}}?
</header>
<section class="gh-publishmenu-content {{if this.disableEmailOption "no-border"}}">
<div class="gh-publishmenu-section">
<div class="gh-publishmenu-radio {{if (eq @saveType "publish") "active"}}" {{on "click" (fn this.setSaveType "publish")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">{{#if @emailOnly}}Send email now{{else}}Set it live now{{/if}}</div>
<div class="gh-publishmenu-radio-desc">{{#if @emailOnly}}Deliver this immediately{{else}}Publish this {{@post.displayName}} immediately{{/if}}</div>
</div>
</div>
<div class="gh-publishmenu-radio {{if (eq @saveType "schedule") "active"}}" {{on "click" (fn this.setSaveType "schedule")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-scheduled-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Schedule it for later</div>
<GhDateTimePicker
@date={{@post.publishedAtBlogDate}}
@time={{@post.publishedAtBlogTime}}
@setDate={{this.setDate}}
@setTime={{this.setTime}}
@onDateError={{this.dateInputDidError}}
@onTimeError={{this.timeInputDidError}}
@setTypedDateError={{@setTypedDateError}}
@errors={{@post.errors}}
@dateErrorProperty="publishedAtBlogDate"
@timeErrorProperty="publishedAtBlogTime"
@minDate={{this._minDate}}
@isActive={{eq @saveType "schedule"}}
/>
<div class="gh-publishmenu-radio-desc">{{#if @emailOnly}}Send email at a specific time{{else}}Set automatic future publish date{{/if}}</div>
</div>
</div>
</div>
{{#if this.showEmailSection}}
<div class="gh-publishmenu-section {{if @isSendingEmailLimited "no-border"}}" {{did-insert (perform this.countTotalMembersTask)}}>
<div class="gh-publishmenu-email">
{{#if @isSendingEmailLimited}}
<p class="gh-box gh-box-alert">{{html-safe @sendingEmailLimitError}}</p>
{{else}}
<div class="gh-publishmenu-email-label">
<label class="gh-publishmenu-radio-label mb3">Send by email to</label>
{{#if this.disableEmailOption}}
<p class="gh-box gh-content-box">
<LinkTo @route="members">
Add members
</LinkTo>
to start sending newsletters!
</p>
{{else}}
<div class="form-group">
{{#if (and (feature "multipleNewsletters") (gt @availableNewsletters.length 1))}}
<div class="mb3">
<PowerSelect
@selected={{@selectedNewsletter}}
@options={{@availableNewsletters}}
@onChange={{@selectNewsletter}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-publishmenu-newsletter-trigger"
@dropdownClass="gh-publishmenu-newsletter-dropdown"
@renderInPlace={{true}}
as |newsletter|
>
{{newsletter.name}}
</PowerSelect>
</div>
{{/if}}
<GhMembersRecipientSelect
@filter={{@recipientsFilter}}
@newsletter={{@selectedNewsletter}}
@onChange={{@setSendEmailWhenPublished}}
/>
</div>
{{/if}}
</div>
{{/if}}
</div>
</div>
{{/if}}
</section>
</div>

View File

@ -1,123 +0,0 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {isEmpty} from '@ember/utils';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
export default class GhPublishMenuDraftComponent extends Component {
@service config;
@service feature;
@service session;
@service settings;
@service store;
@tracked totalMemberCount = null;
// used to set minDate in datepicker
_minDate = null;
_publishedAtBlogTZ = null;
get disableEmailOption() {
// TODO: remove owner or admin check when editors can count members
return this.session.user.isAdmin && (this.totalMemberCount === 0);
}
get showEmailSection() {
return this.args.canSendEmail && this.args.distributionAction !== 'publish';
}
constructor() {
super(...arguments);
this.args.post.set('publishedAtBlogTZ', this.args.post.publishedAtUTC);
this._updateDatesForSaveType(this.args.saveType);
}
@action
setSaveType(type) {
if (this.args.saveType !== type) {
this._updateDatesForSaveType(type);
this.args.setSaveType(type);
this.args.post.validate();
}
}
@action
setDistributionAction(type) {
this.args.setDistributionAction(type);
}
@action
setDate(date) {
let post = this.args.post;
let dateString = moment(date).format('YYYY-MM-DD');
post.set('publishedAtBlogDate', dateString);
return post.validate();
}
@action
setTime(time) {
let post = this.args.post;
post.set('publishedAtBlogTime', time);
return post.validate();
}
// the date-time-picker component has it's own error handling for
// invalid date and times but in this case we want the values to make it
// to the model to make that invalid
@action
dateInputDidError(date) {
this.setDate(date);
}
@action
timeInputDidError(time) {
this.setTime(time);
}
@task
*countTotalMembersTask() {
const user = yield this.session.user;
if (user.isAdmin) {
const result = yield this.store.query('member', {limit: 1, filter: 'newsletters.status:active'});
this.totalMemberCount = result.meta.pagination.total;
}
}
_updateDatesForSaveType(type) {
let hasDateError = !isEmpty(this.args.post.errors.errorsFor('publishedAtBlogDate'));
let hasTimeError = !isEmpty(this.args.post.errors.errorsFor('publishedAtBlogTime'));
let minDate = this._getMinDate();
this._minDate = minDate;
// when publish: switch to now to avoid validation errors
// when schedule: switch to last valid or new minimum scheduled date
if (type === 'publish') {
if (!hasDateError && !hasTimeError) {
this._publishedAtBlogTZ = this.args.post.publishedAtBlogTZ;
} else {
this._publishedAtBlogTZ = this.args.post.publishedAtUTC;
}
this.args.post.set('publishedAtBlogTZ', this.args.post.publishedAtUTC);
} else {
if (!this._publishedAtBlogTZ || moment(this._publishedAtBlogTZ).isBefore(minDate)) {
this.args.post.set('publishedAtBlogTZ', minDate);
} else {
this.args.post.set('publishedAtBlogTZ', this._publishedAtBlogTZ);
}
}
}
// API only accepts dates at least 2 mins in the future, default the
// scheduled date 5 mins in the future to avoid immediate validation errors
_getMinDate() {
return moment.utc().add(5, 'minutes');
}
}

View File

@ -1,19 +0,0 @@
<div data-test-publishmenu-published="true" ...attributes>
<header class="gh-publishmenu-heading">Update {{@post.displayName}} status</header>
<section class="gh-publishmenu-content gh-publishmenu-section">
<div class="gh-publishmenu-radio {{if (eq @saveType "draft") "active"}}" {{on "click" (fn @setSaveType "draft")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-unpublished-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Unpublished</div>
<div class="gh-publishmenu-radio-desc">Revert this {{@post.displayName}} to a private draft</div>
</div>
</div>
<div class="gh-publishmenu-radio {{if (eq @saveType "publish") "active"}}" {{on "click" (fn @setSaveType "publish")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-published-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Published</div>
<div class="gh-publishmenu-radio-desc">Display this {{@post.displayName}} publicly</div>
</div>
</div>
</section>
</div>

View File

@ -1,72 +0,0 @@
<div data-test-publishmenu-scheduled="true" ...attributes>
<header class="gh-publishmenu-heading" data-test-publishmenu-header>Will be {{if @emailOnly "sent" "published"}} in {{this.timeToPublished}}</header>
<div class="gh-publishmenu-content">
<section class="gh-publishmenu-section">
<div class="gh-publishmenu-radio {{if (eq @saveType "draft") "active"}}" {{on "click" (fn this.setSaveType "draft")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-draft-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Revert to draft</div>
<div class="gh-publishmenu-radio-desc">Do not publish</div>
</div>
</div>
<div class="gh-publishmenu-radio {{if (eq @saveType "schedule") "active"}}" {{on "click" (fn this.setSaveType "schedule")}}>
<div class="gh-publishmenu-radio-button" data-test-publishmenu-scheduled-option></div>
<div class="gh-publishmenu-radio-content">
<div class="gh-publishmenu-radio-label">Schedule for later</div>
<GhDateTimePicker
@date={{@post.publishedAtBlogDate}}
@time={{@post.publishedAtBlogTime}}
@setDate={{this.setDate}}
@setTime={{this.setTime}}
@setTypedDateError={{@setTypedDateError}}
@errors={{@post.errors}}
@dateErrorProperty="publishedAtBlogDate"
@timeErrorProperty="publishedAtBlogTime"
@minDate={{this.minDate}}
@isActive={{eq @saveType "schedule"}}
/>
<div class="gh-publishmenu-radio-desc">Set automatic future publish date</div>
</div>
</div>
</section>
{{#if @canSendEmail}}
<section class="gh-publishmenu-section">
<div class="gh-publishmenu-email">
{{#if @isSendingEmailLimited}}
<p>{{html-safe @sendingEmailLimitError}}</p>
{{else}}
<div class="gh-publishmenu-email-label pe-none">
<label class="gh-publishmenu-radio-label mb3 midgrey">Send by email to</label>
{{#if (and (feature "multipleNewsletters") (gt @availableNewsletters.length 1))}}
<div class="mb3">
<PowerSelect
@selected={{this.selectedNewsletter}}
@options={{@availableNewsletters}}
@onChange={{noop}}
@triggerComponent="gh-power-select/trigger"
@triggerClass="gh-publishmenu-newsletter-trigger disabled"
@dropdownClass="gh-publishmenu-newsletter-dropdown"
@renderInPlace={{true}}
@disabled={{true}}
as |newsletter|
>
{{newsletter.name}}
</PowerSelect>
</div>
{{/if}}
<div class="form-group">
<GhMembersRecipientSelect
@filter={{@recipientsFilter}}
@newsletter={{this.selectedNewsletter}}
@disabled={{true}}
/>
</div>
</div>
{{/if}}
</div>
</section>
{{/if}}
</div>
</div>

View File

@ -1,74 +0,0 @@
import Component from '@glimmer/component';
import moment from 'moment';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class GhPublishmenuScheduledComponent extends Component {
@service clock;
@service session;
@service feature;
@service settings;
@service config;
// used to set minDate in datepicker
@tracked minDate = null;
get timeToPublished() {
let publishedAtUTC = this.args.post.publishedAtUTC;
if (!publishedAtUTC) {
return null;
}
this.clock.get('second');
return publishedAtUTC.toNow(true);
}
get selectedNewsletter() {
return this.args.availableNewsletters.find(n => n.id === this.args.post.newsletter?.id);
}
constructor() {
super(...arguments);
this.minDate = new Date();
}
@action
setSaveType(type) {
if (this.args.saveType !== type) {
this.minDate = new Date();
this.args.setSaveType(type);
// when draft switch to now to avoid validation errors
// when schedule switch back to saved date to avoid unnecessary re-scheduling
if (type === 'draft') {
this.args.post.set('publishedAtBlogTZ', new Date());
} else {
this.args.post.set('publishedAtBlogTZ', this.args.post.publishedAtUTC);
}
this.args.post.validate();
}
}
@action
setDate(date) {
let post = this.args.post;
let dateString = moment(date).format('YYYY-MM-DD');
post.set('publishedAtBlogDate', dateString);
return post.validate();
}
@action
setTime(time) {
let post = this.args.post;
if (!this.args.isClosing) {
post.set('publishedAtBlogTime', time);
return post.validate();
}
}
}

View File

@ -1,78 +0,0 @@
{{#if (eq this.displayState "sent")}}
<div class="gh-btn gh-btn-text sent">Sent</div>
{{else}}
<GhBasicDropdown @verticalPosition="below" @horizontalPosition="right" @onOpen={{action "open"}} @onClose={{action "close"}} as |dd|>
<dd.Trigger class="gh-btn gh-btn-editor {{if (or (eq this.displayState "published") (eq this.displayState "scheduled") (eq this.uiContext "preview")) "green"}} gh-publishmenu-trigger">
<span data-test-publishmenu-trigger>{{this.triggerText}} {{svg-jar "arrow-down"}}</span>
</dd.Trigger>
<dd.Content class="gh-publishmenu-dropdown">
{{#if (eq this.displayState "published")}}
<GhPublishmenuPublished
@post={{this.post}}
@saveType={{this.saveType}}
@setSaveType={{action "setSaveType"}}
data-test-publishmenu-published="true" />
{{else if (eq this.displayState "scheduled")}}
<GhPublishmenuScheduled
@post={{this.post}}
@saveType={{this.saveType}}
@emailOnly={{this.emailOnly}}
@isClosing={{this.isClosing}}
@canSendEmail={{this.canSendEmail}}
@recipientsFilter={{this.sendEmailWhenPublished}}
@setSaveType={{action "setSaveType"}}
@setTypedDateError={{action (mut this.typedDateError)}}
@isSendingEmailLimited={{this.isSendingEmailLimited}}
@sendingEmailLimitError={{this.sendingEmailLimitError}}
@availableNewsletters={{this.availableNewsletters}}
data-test-publishmenu-scheduled="true" />
{{else}}
<GhPublishmenuDraft
@post={{this.post}}
@saveType={{this.saveType}}
@setSaveType={{action "setSaveType"}}
@setTypedDateError={{action (mut this.typedDateError)}}
@canSendEmail={{this.canSendEmail}}
@emailOnly={{this.emailOnly}}
@recipientsFilter={{this.sendEmailWhenPublished}}
@setSendEmailWhenPublished={{action "setSendEmailWhenPublished"}}
@isSendingEmailLimited={{this.isSendingEmailLimited}}
@sendingEmailLimitError={{this.sendingEmailLimitError}}
@distributionAction={{this.distributionAction}}
@setDistributionAction={{action "setDistributionAction"}}
@availableNewsletters={{this.availableNewsletters}}
@selectedNewsletter={{this.selectedNewsletter}}
@selectNewsletter={{this.selectNewsletter}}
data-test-publishmenu-draft="true" />
{{/if}}
{{!--
save button needs to be outside of menu components so it doesn't lose state
or cancel the task when the post status updates and switches components
--}}
<footer class="gh-publishmenu-footer">
<button class="gh-btn gh-btn-outline" data-test-publishmenu-cancel type="button" {{on "click" (action dd.actions.close)}}>
<span>Cancel</span>
</button>
<GhTaskButton
@buttonText={{this.buttonText}}
@task={{this.save}}
@taskArgs={{hash dropdown=dd}}
@successText={{this.successText}}
@runningText={{this.runningText}}
@class="gh-btn gh-btn-black gh-publishmenu-button gh-btn-icon"
data-test-publishmenu-save="true"
/>
</footer>
</dd.Content>
</GhBasicDropdown>
{{/if}}
{{!--
Workaround to have an always-shown element to attach key handlers to.
TODO: Move onto main element once converted to a glimmer component
--}}
<div class="hidden" {{on-key "cmd+shift+p" (action "publishFromShortcut")}}></div>

View File

@ -1,537 +0,0 @@
import Component from '@ember/component';
import ConfirmPublishModal from './modals/editor/confirm-publish';
import EmailFailedError from 'ghost-admin/errors/email-failed-error';
import {action, computed} from '@ember/object';
import {bind, schedule} from '@ember/runloop';
import {or, reads} from '@ember/object/computed';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
const CONFIRM_EMAIL_POLL_LENGTH = 1000;
const CONFIRM_EMAIL_MAX_POLL_LENGTH = 15 * 1000;
export default Component.extend({
clock: service(),
config: service(),
feature: service(),
limit: service(),
modals: service(),
session: service(),
settings: service(),
store: service(),
classNames: 'gh-publishmenu',
displayState: 'draft',
saveType: 'publish',
post: null,
postStatus: 'draft',
distributionAction: 'publish_send',
runningText: null,
saveTask: null,
sendEmailWhenPublished: null,
typedDateError: null,
isSendingEmailLimited: false,
sendingEmailLimitError: '',
selectedNewsletter: null,
_publishedAtBlogTZ: null,
_previousStatus: null,
isClosing: null,
onClose() {},
forcePublishedMenu: reads('post.pastScheduledTime'),
hasEmailPermission: or('session.user.isOwnerOnly', 'session.user.isAdminOnly', 'session.user.isEditor'),
emailOnly: computed.equal('distributionAction', 'send'),
canSendEmail: computed('hasEmailPermission', 'post.{isPost,email}', 'settings.{editorDefaultEmailRecipients,membersSignupAccess,mailgunIsConfigured}', 'config.mailgunIsConfigured', function () {
let isDisabled = this.settings.get('editorDefaultEmailRecipients') === 'disabled' || this.settings.get('membersSignupAccess') === 'none';
let mailgunIsConfigured = this.settings.get('mailgunIsConfigured') || this.config.get('mailgunIsConfigured');
let isPost = this.post.isPost;
let hasSentEmail = !!this.post.email;
return this.hasEmailPermission &&
!isDisabled &&
mailgunIsConfigured &&
isPost &&
!hasSentEmail;
}),
postState: computed('post.{isPublished,isScheduled}', 'forcePublishedMenu', function () {
if (this.forcePublishedMenu || this.get('post.isPublished')) {
return 'published';
} else if (this.get('post.isScheduled')) {
return 'scheduled';
} else {
return 'draft';
}
}),
triggerText: computed('postState', function () {
let state = this.postState;
if (state === 'published') {
return 'Update';
} else if (state === 'scheduled') {
return 'Scheduled';
} else {
return 'Publish';
}
}),
_runningText: computed('postState', 'saveType', function () {
let saveType = this.saveType;
let postState = this.postState;
let runningText;
if (postState === 'draft') {
runningText = saveType === 'publish' ? 'Publishing' : 'Scheduling';
}
if (postState === 'published') {
runningText = saveType === 'publish' ? 'Updating' : 'Unpublishing';
}
if (postState === 'scheduled') {
runningText = saveType === 'schedule' ? 'Rescheduling' : 'Unscheduling';
}
return runningText || 'Publishing';
}),
buttonText: computed('postState', 'saveType', 'distributionAction', 'sendEmailWhenPublished', function () {
let saveType = this.saveType;
let postState = this.postState;
let distributionAction = this.distributionAction;
let buttonText;
if (postState === 'draft') {
switch (distributionAction) {
case 'publish_send':
if (saveType === 'publish') {
buttonText = 'Publish';
if (this.canSendEmail && this.sendEmailWhenPublished && this.sendEmailWhenPublished !== 'none') {
buttonText = `${buttonText} & send`;
}
} else {
buttonText = 'Schedule';
}
break;
case 'publish':
buttonText = (saveType === 'publish') ? 'Publish' : 'Schedule';
break;
case 'send':
buttonText = saveType === 'publish' ? 'Send' : 'Schedule';
break;
}
}
if (postState === 'published') {
buttonText = saveType === 'publish' ? 'Update' : 'Unpublish';
}
if (postState === 'scheduled') {
buttonText = saveType === 'schedule' ? 'Reschedule' : 'Unschedule';
}
return buttonText || 'Publish';
}),
successText: computed('_previousStatus', 'postState', function () {
let postState = this.postState;
let previousStatus = this._previousStatus;
let buttonText;
if (previousStatus === 'draft') {
buttonText = postState === 'published' ? 'Published' : 'Scheduled';
}
if (previousStatus === 'published') {
buttonText = postState === 'draft' ? 'Unpublished' : 'Updated';
}
if (previousStatus === 'scheduled') {
buttonText = postState === 'draft' ? 'Unscheduled' : 'Rescheduled';
}
return buttonText;
}),
defaultEmailRecipients: computed('settings.{editorDefaultEmailRecipients,editorDefaultEmailRecipientsFilter}', 'post.visibility', function () {
const defaultEmailRecipients = this.settings.get('editorDefaultEmailRecipients');
if (defaultEmailRecipients === 'disabled') {
return null;
}
if (defaultEmailRecipients === 'visibility') {
if (this.post.visibility === 'public') {
return 'status:free,status:-free';
}
if (this.post.visibility === 'members') {
return 'status:free,status:-free';
}
if (this.post.visibility === 'paid') {
return 'status:-free';
}
if (this.post.visibility === 'tiers') {
return this.post.visibilitySegment;
}
return this.post.visibility;
}
return this.settings.get('editorDefaultEmailRecipientsFilter');
}),
didReceiveAttrs() {
this._super(...arguments);
// update the displayState based on the post status but only after a
// save has finished to avoid swapping the menu prematurely and triggering
// calls to `setSaveType` due to the component re-rendering
// TODO: we should have a better way of dealing with this where we don't
// rely on the side-effect of component rendering calling setSaveType
let postStatus = this.postStatus;
if (postStatus !== this._postStatus) {
if (this.get('saveTask.isRunning')) {
this.get('saveTask.last').then(() => {
this.set('displayState', postStatus);
this.updateSaveTypeForPostStatus(postStatus);
});
} else {
this.set('displayState', postStatus);
this.updateSaveTypeForPostStatus(postStatus);
}
}
this._postStatus = this.postStatus;
this.setDefaultSendEmailWhenPublished();
this.checkIsSendingEmailLimitedTask.perform();
const defaultEmailRecipients = this.get('defaultEmailRecipients');
if (this.post.status === 'scheduled' && this.post.emailOnly) {
this.set('distributionAction', 'send');
}
if (this.post.isPage || !defaultEmailRecipients) {
this.set('distributionAction', 'publish');
}
},
didInsertElement() {
this._super(...arguments);
this.fetchNewslettersTask.perform();
},
actions: {
setSaveType(saveType) {
let post = this.post;
this.set('saveType', saveType);
if (saveType === 'draft') {
post.set('statusScratch', 'draft');
} else if (saveType === 'schedule') {
post.set('statusScratch', 'scheduled');
} else if (saveType === 'publish') {
post.set('statusScratch', 'published');
}
},
setSendEmailWhenPublished(sendEmailWhenPublished) {
this.set('sendEmailWhenPublished', sendEmailWhenPublished);
},
setDistributionAction(distributionAction) {
this.set('distributionAction', distributionAction);
if (distributionAction === 'publish') {
this.set('sendEmailWhenPublished', 'none');
} else {
this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
}
},
open() {
this._cachePublishedAtBlogTZ();
this.set('isClosing', false);
this.get('post.errors').clear();
this.setDefaultSendEmailWhenPublished();
if (this.onOpen) {
this.onOpen();
}
},
close(dropdown, e) {
// don't close the menu if the datepicker popup or confirm modal is clicked
if (e) {
let onDatepicker = !!e.target.closest('.ember-power-datepicker-content');
let onModal = !!e.target.closest('.fullscreen-modal-container');
if (onDatepicker || onModal) {
return false;
}
}
if (!this._skipDropdownCloseCleanup) {
this._cleanup();
}
this._skipDropdownCloseCleanup = false;
this.onClose();
this.set('isClosing', true);
return true;
},
publishFromShortcut() {
// trigger blur for inputs and textareas to trigger any actions
// before attempting to save so we're saving after the result
if (document.activeElement?.matches('input[type="text"], textarea')) {
// trigger focusout so that it bubbles
const focusout = new Event('focusout');
document.activeElement.dispatchEvent(focusout);
// make sure blur event is triggered too
document.activeElement.blur();
}
// wait for actions to be triggered by the focusout/blur before saving
schedule('actions', this, function () {
this.send('setSaveType', 'publish');
this.save.perform();
});
}
},
get availableNewsletters() {
return this.store.peekAll('newsletter').filter(n => n.status === 'active');
},
updateSaveTypeForPostStatus(status) {
if (status === 'draft' || status === 'published') {
this.set('saveType', 'publish');
}
if (status === 'scheduled') {
this.set('saveType', 'schedule');
}
},
setDefaultSendEmailWhenPublished() {
if (this.isSendingEmailLimited) {
this.set('sendEmailWhenPublished', false);
} else if (this.postStatus === 'draft' && this.canSendEmail) {
// Set default newsletter recipients
this.set('sendEmailWhenPublished', this.defaultEmailRecipients);
} else {
this.set('sendEmailWhenPublished', this.post.emailSegment);
}
},
checkIsSendingEmailLimitedTask: task(function* () {
try {
yield this.reloadSettingsTask.perform();
if (this.limit.limiter && this.limit.limiter.isLimited('emails')) {
yield this.limit.limiter.errorIfWouldGoOverLimit('emails');
} else if (this.settings.get('emailVerificationRequired')) {
this.set('isSendingEmailLimited', true);
this.set('sendingEmailLimitError', 'Email sending is temporarily disabled because your account is currently in review. You should have an email about this from us already, but you can also reach us any time at support@ghost.org.');
this.set('sendEmailWhenPublished', 'none');
return;
}
this.set('isSendingEmailLimited', false);
this.set('sendingEmailLimitError', null);
} catch (error) {
this.set('isSendingEmailLimited', true);
this.set('sendingEmailLimitError', error.message);
this.set('sendEmailWhenPublished', 'none');
}
}),
reloadSettingsTask: task(function* () {
yield this.settings.reload();
}),
save: task(function* (options = {}) {
const {post, saveType} = this;
// don't allow save if an invalid schedule date is present
if (this.typedDateError) {
return false;
}
// validate publishedAtBlog to avoid an alert when saving for already displayed errors
// important to do this before opening email confirmation modal too
try {
yield post.validate({property: 'publishedAtBlog'});
} catch (error) {
// re-throw if we don't have a validation error
if (error) {
throw error;
}
return false;
}
const isPublishOnly = this.distributionAction === 'publish'
|| this.sendEmailWhenPublished === 'none'
|| this.post.displayName === 'page'
|| this.post.email;
// open publish confirmation if post will be published/scheduled and emailed
if (!isPublishOnly && post.status === 'draft' && (saveType === 'publish' || saveType === 'schedule')) {
if (options.dropdown) {
this._skipDropdownCloseCleanup = true;
options.dropdown.actions.close();
}
return yield this.modals.open(ConfirmPublishModal, {
post: this.post,
emailOnly: this.emailOnly,
sendEmailWhenPublished: this.sendEmailWhenPublished,
newsletter: this.selectedNewsletter,
isScheduled: saveType === 'schedule',
confirm: this.saveWithConfirmedPublish.perform,
retryEmailSend: this.retryEmailSendTask.perform
}, {
beforeClose: bind(this, this._cleanup)
});
}
return yield this._saveTask.perform(options);
}),
saveWithConfirmedPublish: task(function* () {
return yield this._saveTask.perform();
}),
retryEmailSendTask: task(function* () {
if (!this.post.email) {
return;
}
let email = yield this.post.email.retry();
let pollTimeout = 0;
if (email && email.status !== 'submitted') {
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
pollTimeout += CONFIRM_EMAIL_POLL_LENGTH;
email = yield email.reload();
if (email.status === 'submitted') {
break;
}
if (email.status === 'failed') {
throw new EmailFailedError(email.error);
}
}
}
return email;
}),
selectNewsletter: action(function (newsletter) {
this.set('selectedNewsletter', newsletter);
}),
fetchNewslettersTask: task(function* () {
const newsletters = yield this.store.query('newsletter', {
filter: 'status:active',
order: 'sort_order ASC'
});
const defaultNewsletter = newsletters.toArray()[0];
this.defaultNewsletter = defaultNewsletter;
this.set('selectedNewsletter', defaultNewsletter);
}),
_saveTask: task(function* () {
let {
post,
emailOnly,
sendEmailWhenPublished,
saveType
} = this;
// runningText needs to be declared before the other states change during the
// save action.
this.set('runningText', this._runningText);
this.set('_previousStatus', this.get('post.status'));
this.setSaveType(saveType);
try {
// will show alert for non-date related failed validations
post = yield this.saveTask.perform({sendEmailWhenPublished, newsletter: this.selectedNewsletter?.slug, emailOnly});
this._cachePublishedAtBlogTZ();
if (sendEmailWhenPublished && sendEmailWhenPublished !== 'none') {
let pollTimeout = 0;
if (post.email && post.email.status !== 'submitted') {
while (pollTimeout < CONFIRM_EMAIL_MAX_POLL_LENGTH) {
yield timeout(CONFIRM_EMAIL_POLL_LENGTH);
pollTimeout += CONFIRM_EMAIL_POLL_LENGTH;
post = yield post.reload();
if (post.email.status === 'submitted') {
break;
}
if (post.email.status === 'failed') {
throw new EmailFailedError(post.email.error);
}
}
}
}
this._cleanup();
return post;
} catch (error) {
// re-throw if we don't have a validation error
if (error) {
throw error;
}
}
}),
_cachePublishedAtBlogTZ() {
this._publishedAtBlogTZ = this.get('post.publishedAtBlogTZ');
},
_cleanup() {
this.set('selectedNewsletter', this.defaultNewsletter);
if (this.post.isScheduled && this.post.emailOnly) {
this.set('distributionAction', 'send');
} else if (this.post.isPage || !this.defaultEmailRecipients) {
this.set('distributionAction', 'publish');
} else {
this.set('distributionAction', 'publish_send');
}
this.updateSaveTypeForPostStatus(this.post.status);
// when closing the menu we reset the publishedAtBlogTZ date so that the
// unsaved changes made to the scheduled date aren't reflected in the PSM
this.post.set('publishedAtBlogTZ', this._publishedAtBlogTZ);
this.post.set('statusScratch', null);
this.post.validate();
}
});

View File

@ -1,121 +0,0 @@
<div class="modal-content" data-test-modal="editor/confirm-publish">
{{#unless this.errorMessage}}
<header class="modal-header" data-test-state="confirm-publish">
<h1>Ready to go? Heres what happens next</h1>
</header>
<button class="close" title="Close" type="button" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body" {{did-insert this.countRecipientsTask.perform}}>
{{#if (eq @data.post.displayName 'page')}}
<p>
Your page will be published {{if @data.isScheduled "at the scheduled time" "immediately"}}. Sound good?
</p>
{{else if this.isPublishOnly}}
<p>
Your post will be published {{if @data.isScheduled "at the scheduled time" "immediately"}}
and won't be sent as an email. Sound good?
</p>
{{else}}
{{#if this.countRecipientsTask.isRunning}}
<div class="flex flex-column items-center">
<div class="gh-loading-spinner"></div>
</div>
{{else}}
{{#if this.isEmailOnlyWithNoMembers}}
<p>
You're trying to {{if @data.isScheduled "schedule" "send"}} a post
as an email newsletter with <strong>0 members</strong> selected.
Choose a segment of your audience and try again!
</p>
{{else if (eq this.memberCount 0)}}
<p>
Your post will be published {{if @data.isScheduled "at the scheduled time" "immediately"}}
and won't be sent as an email. Sound good?
</p>
{{else}}
<p>
Your post will be delivered to <strong>{{this.memberCountString}}</strong>
{{#if @data.emailOnly}}
but it will <strong>not</strong>
{{else}}
and will
{{/if}}
be published on your site{{#if @data.isScheduled}} at the scheduled time{{/if}}. Sound good?
</p>
{{/if}}
{{/if}}
{{/if}}
</div>
<div class="modal-footer">
{{#if this.isEmailOnlyWithNoMembers}}
<button type="button" class="gh-btn" {{on "click" @close}} data-test-button="cancel-email-with-no-members">
<span>Close</span>
</button>
{{else}}
<button class="gh-btn" data-test-button="cancel-publish-and-email" type="button" {{on "click" @close}}>
<span>Cancel</span>
</button>
{{#if @data.isScheduled}}
<GhTaskButton
@disabled={{this.countRecipientsTask.isRunning}}
@buttonText="Schedule"
@runningText="Scheduling..."
@task={{this.confirmAndCheckErrorTask}}
@class="gh-btn gh-btn-black gh-btn-icon"
data-test-button="confirm-schedule"
/>
{{else}}
<GhTaskButton
@disabled={{this.countRecipientsTask.isRunning}}
@buttonText={{this.publishAndSendButtonText}}
@runningText={{if @data.emailOnly "Sending..." "Publishing..."}}
@task={{this.confirmAndCheckErrorTask}}
@class="gh-btn gh-btn-black gh-btn-icon"
data-test-button="confirm-publish"
/>
{{/if}}
{{/if}}
</div>
{{else}}
<header class="modal-header" data-test-state="failed-send">
<h1>Failed to send email</h1>
</header>
<button class="close" title="Close" type="button" {{on "click" @close}}>{{svg-jar "close"}}<span class="hidden">Close</span></button>
<div class="modal-body">
<p>Your post has been published but the email failed to send. Please verify your email settings if the error persists.</p>
<p class="mb0">
<button type="button" class="gh-btn gh-btn-text regular" {{on "click" this.toggleErrorDetails}} data-test-toggle-error>
{{#if this.errorDetailsOpen}}
{{svg-jar "arrow-down" class="nudge-top--2 w2 h2 fill-darkgrey mr1"}}
{{else}}
{{svg-jar "arrow-right" class="nudge-top--1 w2 h2 fill-darkgrey mr1"}}
{{/if}}
<span>Error details</span>
</button>
</p>
{{#liquid-if this.errorDetailsOpen}}
<p class="error gh-box gh-box-error mt3 mb3">
{{svg-jar "warning"}}
{{this.errorMessage}}
</p>
{{/liquid-if}}
</div>
<div class="modal-footer">
<button class="gh-btn" data-test-button="cancel-publish-and-email" type="button" {{on "click" @close}}>
<span>Close</span>
</button>
<GhTaskButton
@buttonText="Retry email"
@runningText="Sending..."
@task={{this.retryEmailTask}}
@class="gh-btn gh-btn-red gh-btn-icon"
data-test-button="retry-email"
/>
</div>
{{/unless}}
</div>

View File

@ -1,112 +0,0 @@
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 ConfirmPublishModal extends Component {
@service membersCountCache;
@service session;
@service store;
@tracked errorMessage = null;
@tracked errorDetailsOpen = false;
@tracked memberCount = null;
@tracked memberCountString = null;
get isEmailOnlyWithNoMembers() {
return this.isEmailOnly && this.memberCount === 0;
}
get publishAndSendButtonText() {
if (this.isEmailOnly) {
return 'Send';
}
if (this.isPublishOnly || this.memberCount === 0) {
return 'Publish';
}
return 'Publish and Send';
}
constructor() {
super(...arguments);
// set static up-front so it doesn't change when post is saved and email is created
this.isPublishOnly = this.args.data.sendEmailWhenPublished === 'none'
|| this.args.data.post.displayName === 'page'
|| this.args.data.post.email;
this.isEmailOnly = this.args.data.emailOnly;
}
@action
confirm() {
if (this.errorMessage) {
return this.retryEmailTask.perform();
} else {
if (!this.countRecipientsTask.isRunning) {
return this.confirmAndCheckErrorTask.perform();
}
}
}
@action
toggleErrorDetails() {
this.errorDetailsOpen = !this.errorDetailsOpen;
}
@task
*countRecipientsTask() {
const {sendEmailWhenPublished,newsletter} = this.args.data;
const filter = `${newsletter.recipientFilter}+(${sendEmailWhenPublished})`;
this.memberCount = sendEmailWhenPublished ? (yield this.membersCountCache.count(filter)) : 0;
this.memberCountString = sendEmailWhenPublished ? (yield this.membersCountCache.countString(filter, {newsletter})) : '0 members';
}
@task
*confirmAndCheckErrorTask() {
try {
yield this.args.data.confirm();
this.args.close();
return true;
} catch (e) {
// switch to "failed" state if email fails
if (e && e.name === 'EmailFailedError') {
this.errorMessage = e.message;
return false;
}
// close modal and continue with normal error handling if it was
// a non-email-related error
this.args.close();
if (e) {
throw e;
}
}
}
@task
*retryEmailTask() {
try {
yield this.args.data.retryEmailSend();
this.args.close();
return true;
} catch (e) {
// update "failed" state if email fails again
if (e && e.name === 'EmailFailedError') {
this.errorMessage = e.message;
return;
}
// TODO: test a non-email failure - maybe this needs to go through
// the notifications service
if (e) {
throw e;
}
}
}
}

View File

@ -1,43 +0,0 @@
<div class="modal-content">
<div class="flex flex-column h-100">
<header class="modal-header gh-post-preview-header" data-test-modal="preview-email">
<div>
<button class="gh-editor-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "arrow-left"}} Back</span>
</button>
</div>
<div class="gh-post-preview-btn-group">
<div class="gh-contentfilter gh-btn-group">
<button type="button" class="gh-btn {{if (eq this.tab "browser") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "browser")}}><span>{{svg-jar "desktop"}}</span></button>
<button type="button" class="gh-btn {{if (eq this.tab "mobile") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "mobile")}}><span>{{svg-jar "mobile-phone"}}</span></button>
{{#if (and (not-eq this.settings.membersSignupAccess "none") (not-eq this.settings.editorDefaultEmailRecipients "disabled"))}}
{{#if @data.post.isPost}}
<button type="button" class="gh-btn {{if (eq this.tab "email") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "email")}}><span>{{svg-jar "email-unread"}}</span></button>
{{/if}}
{{/if}}
<button type="button" class="gh-btn {{if (eq this.tab "social") "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "social")}}><span>{{svg-jar "twitter"}}</span></button>
</div>
</div>
</header>
{{#if this.saveFirstTask.isRunning}}
<GhLoadingSpinner />
{{else}}
{{#if (eq this.tab "browser")}}
<Modals::PostPreview::Browser @post={{@data.post}} />
{{/if}}
{{#if (and (eq this.tab "mobile"))}}
<Modals::PostPreview::Mobile @post={{@data.post}} />
{{/if}}
{{#if (and (eq this.tab "email") @data.post.isPost)}}
<Modals::PostPreview::Email @post={{@data.post}} />
{{/if}}
{{#if (eq this.tab "social")}}
<Modals::PostPreview::Social @post={{@data.post}} />
{{/if}}
{{/if}}
</div>
</div>

View File

@ -1,40 +0,0 @@
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 PostPreviewModal extends Component {
@service settings;
@service session;
static modalOptions = {
className: 'fullscreen-modal-full-overlay fullscreen-modal-email-preview',
focusTrapOptions: null // not ideal but date inputs aren't focusable otherwise
};
@tracked tab = 'browser';
constructor() {
super(...arguments);
this.saveFirstTask.perform();
}
@action
changeTab(tab) {
this.tab = tab;
}
@task
*saveFirstTask() {
const {saveTask, post, hasDirtyAttributes} = this.args.data;
if (saveTask.isRunning) {
return yield saveTask.last;
}
if (post.isDraft && hasDirtyAttributes) {
yield saveTask.perform();
}
}
}

View File

@ -1,23 +0,0 @@
<div class="gh-post-preview-browser-container">
<iframe class="gh-pe-iframe" src={{@post.previewUrl}}></iframe>
</div>
<div class="gh-post-preview-browser-footer">
<span class="mr3 nowrap fw6 f8 darkgrey">Share preview privately</span>
<div class="gh-post-preview-url-container truncate">
<span class="db truncate w-90">{{@post.previewUrl}}</span>
</div>
<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-btn gh-btn-green gh-btn-icon gh-post-preview-copy-url-trigger">
<span>
{{#if this.copyPreviewUrl.isRunning}}
Copied!
{{else}}
copy
{{/if}}
</span>
</button>
<div>
<a href={{@post.previewUrl}} target="_blank" rel="noopener noreferrer" class="gh-btn gh-btn-editor gh-btn-icon gh-btn-icon-right gh-btn-external">
<span>Open in new tab {{svg-jar "external"}}</span>
</a>
</div>
</div>

View File

@ -1,11 +0,0 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {task, timeout} from 'ember-concurrency';
export default class ModalPostPreviewBrowserComponent extends Component {
@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}
}

View File

@ -1,39 +0,0 @@
<div class="gh-post-preview-email-container">
<div class="gh-post-preview-email-mockup">
<div class="gh-pe-emailclient-sender">
<p>
<span class="strong">{{or this.newsletter.senderName this.settings.title}}</span> &lt;{{full-email-address (or this.newsletter.senderEmail "noreply")}}&gt;
</p>
<p><span class="dark">To:</span> Jamie Larson &lt;jamie@example.com&gt;</p>
</div>
<iframe class="gh-pe-iframe" {{did-insert this.renderEmailPreview}} sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
</div>
</div>
<div class="gh-post-preview-email-footer">
<div class="gh-btn-group mr3">
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:free")}}><span>Free member</span></button>
<button type="button" class="gh-btn {{if (eq this.memberSegment "status:-free") "gh-btn-group-selected"}}" {{on "click" (fn this.changeMemberSegment "status:-free")}}><span>Paid member</span></button>
</div>
<div class="gh-post-preview-email-input {{if this.sendPreviewEmailError "error"}}">
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-post-preview-email-input"
placeholder="you@yoursite.com"
aria-invalid={{if this.sendPreviewEmailError "true"}}
aria-describedby={{if this.sendPreviewEmailError "sendError"}}
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>
{{#if this.sendPreviewEmailError}}
<div class="error fixed nowrap f8 lh-heading"><span class="response" id="sendError">{{this.sendPreviewEmailError}}</span></div>
{{/if}}
</div>
<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send test email"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-green gh-btn-icon gh-post-preview-email-trigger"
/>
</div>

View File

@ -1,161 +0,0 @@
import Component from '@glimmer/component';
import validator from 'validator';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {inject as service} from '@ember/service';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';
const INJECTED_CSS = `
html::-webkit-scrollbar {
display: none;
width: 0;
background: transparent
}
html {
scrollbar-width: none;
}
`;
// TODO: remove duplication with <ModalPostEmailPreview>
export default class ModalPostPreviewEmailComponent extends Component {
@service ajax;
@service config;
@service feature;
@service ghostPaths;
@service session;
@service settings;
@service store;
@tracked html = '';
@tracked subject = '';
@tracked memberSegment = 'status:free';
@tracked previewEmailAddress = this.session.user.email;
@tracked sendPreviewEmailError = '';
@tracked newsletter = null;
get mailgunIsEnabled() {
return this.config.get('mailgunIsConfigured') ||
!!(this.settings.get('mailgunApiKey') && this.settings.get('mailgunDomain') && this.settings.get('mailgunBaseUrl'));
}
@action
async renderEmailPreview(iframe) {
this._previewIframe = iframe;
await this._fetchEmailData();
// avoid timing issues when _fetchEmailData didn't perform any async ops
await timeout(100);
if (iframe) {
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(this.html);
iframe.contentWindow.document.close();
}
}
@action
changeMemberSegment(segment) {
this.memberSegment = segment;
if (this._previewIframe) {
this.renderEmailPreview(this._previewIframe);
}
}
@task({drop: true})
*sendPreviewEmailTask() {
try {
const resourceId = this.args.post.id;
const testEmail = this.previewEmailAddress.trim();
if (!validator.isEmail(testEmail)) {
this.sendPreviewEmailError = 'Please enter a valid email';
return false;
}
if (!this.mailgunIsEnabled) {
this.sendPreviewEmailError = 'Please verify your email settings';
return false;
}
this.sendPreviewEmailError = '';
const url = this.ghostPaths.url.api('/email_previews/posts', resourceId);
const data = {emails: [testEmail], memberSegment: this.memberSegment};
const options = {
data,
dataType: 'json'
};
yield this.ajax.post(url, options);
return true;
} catch (error) {
if (error) {
let message = 'Email could not be sent, verify mail settings';
// grab custom error message if present
if (
error.payload && error.payload.errors
&& error.payload.errors[0] && error.payload.errors[0].message) {
message = htmlSafe(error.payload.errors[0].message);
}
this.sendPreviewEmailError = message;
throw error;
}
}
}
async _fetchEmailData() {
let {html, subject, memberSegment} = this;
let {post} = this.args;
// Fetch newsletter
if (!this.newsletter && post.newsletter) {
this.newsletter = post.newsletter;
}
if (!this.newsletter) {
const newsletters = (await this.store.query('newsletter', {filter: 'status:active', limit: 1})).toArray();
const defaultNewsletter = newsletters[0];
this.newsletter = defaultNewsletter;
}
if (html && subject && memberSegment === this._lastMemberSegment) {
return {html, subject};
}
this._lastMemberSegment = memberSegment;
// model is an email
if (post.html && post.subject) {
html = post.html;
subject = post.subject;
// model is a post with an existing email
} else if (post.email) {
html = post.email.html;
subject = post.email.subject;
// model is a post, fetch email preview
} else {
let url = new URL(this.ghostPaths.url.api('/email_previews/posts', post.id), window.location.href);
url.searchParams.set('memberSegment', this.memberSegment);
let response = await this.ajax.request(url.href);
let [emailPreview] = response.email_previews;
html = emailPreview.html;
subject = emailPreview.subject;
}
// inject extra CSS into the html for disabling links and scrollbars etc
let domParser = new DOMParser();
let htmlDoc = domParser.parseFromString(html, 'text/html');
let stylesheet = htmlDoc.querySelector('style');
let originalCss = stylesheet.innerHTML;
stylesheet.innerHTML = `${originalCss}\n\n${INJECTED_CSS}`;
const doctype = new XMLSerializer().serializeToString(htmlDoc.doctype);
html = doctype + htmlDoc.documentElement.outerHTML;
this.html = html;
this.subject = subject;
}
}

View File

@ -1,27 +0,0 @@
<div class="modal-body modal-preview-email-content gh-pe-mobile-container h-auto overflow-auto">
<div class="gh-pe-mobile-bezel">
<div class="gh-pe-mobile-screen">
<iframe class="gh-post-preview-iframe" src={{@post.previewUrl}}></iframe>
</div>
</div>
</div>
<div class="gh-post-preview-browser-footer">
<span class="mr3 nowrap fw6 f8 darkgrey">Share preview privately</span>
<div class="gh-post-preview-url-container truncate">
<span class="db truncate w-90">{{@post.previewUrl}}</span>
</div>
<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-btn gh-btn-green gh-btn-icon gh-post-preview-copy-url-trigger">
<span>
{{#if this.copyPreviewUrl.isRunning}}
Copied!
{{else}}
copy
{{/if}}
</span>
</button>
<div>
<a href={{@post.previewUrl}} target="_blank" rel="noopener noreferrer" class="gh-btn gh-btn-editor gh-btn-icon gh-btn-icon-right gh-btn-external">
<span>Open in new tab {{svg-jar "external"}}</span>
</a>
</div>
</div>

View File

@ -1,11 +0,0 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {task, timeout} from 'ember-concurrency';
export default class ModalPostPreviewBrowserComponent extends Component {
@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}
}

View File

@ -1,258 +0,0 @@
<div class="gh-post-preview-social-container">
<p class="mb4">This is how your content will look when shared, you can click on any elements youd like to edit.</p>
<div class="flex flex-column">
<div class="flex gh-social-container-responsive">
<div class="gh-social-og-container">
<div class="flex ma3 mb2">
<span>{{svg-jar "social-facebook" class="social-icon"}}</span>
<div>
<div class="gh-social-og-title">{{or this.settings.metaTitle this.settings.title}}</div>
<div class="gh-social-og-time">12 hrs</div>
</div>
</div>
<div class="flex flex-column ma3 mt2">
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-60" />
</div>
<div
class="gh-social-og-preview"
{{on "mouseenter" (action (mut this.facebookHovered) true)}}
{{on "mouseleave" (action (mut this.facebookHovered) false)}}
>
{{#if (and this.facebookHovered (not this.facebookImage))}}
{{!-- only shown on hover when there's no image or fallback --}}
<button class="gh-social-og-preview-img-add" type="button" {{on "click" (fn this.triggerFileDialog "facebook")}}>+ Add image</button>
{{/if}}
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.setFacebookImage}}
as |uploader|
>
{{#each uploader.errors as |error|}}
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
{{/each}}
{{#if (or this.facebookImage uploader.isUploading)}}
<div class="gh-social-og-preview-image relative" style={{background-image-style this.facebookImage}}>
<div class="flex h-100 items-center justify-center">
{{#if (or this.facebookHovered uploader.isUploading)}}
{{#if uploader.isUploading}}
{{uploader.progressBar}}
{{else}}
<button type="button" class="gh-btn gh-btn-white" {{on "click" (fn this.triggerFileDialog "facebook")}}><span>{{if @post.ogImage "Change" "Upload"}} image</span></button>
{{/if}}
{{/if}}
{{#if (and this.facebookHovered @post.ogImage)}}
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Facebook image" {{on "click" this.clearFacebookImage}}>
<span>{{svg-jar "trash"}}</span>
<span class="hidden">Remove custom Facebook image</span>
</button>
{{/if}}
</div>
</div>
{{/if}}
<div style="display:none">
<GhFileInput id="facebookFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
<div class="gh-social-og-preview-bookmark">
{{!-- Ensures description is hidden if title exceeds one line --}}
<div class="gh-social-og-preview-content {{if this.editingFacebookTitle 'edit-mode'}} {{if this.editingFacebookDescription 'edit-mode'}}">
<div class="gh-social-og-preview-meta">
{{this.config.blogDomain}}
</div>
{{#if this.editingFacebookTitle}}
<input
type="text"
class="gh-input"
placeholder={{this.facebookTitle}}
value={{@post.ogTitle}}
maxlength="300"
{{on "blur" this.setFacebookTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "ogTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
/>
{{else}}
<div class="gh-social-og-preview-title editable pointer" {{on "click" this.editFacebookTitle}}>
{{truncate this.facebookTitle}}
</div>
{{/if}}
{{#if this.editingFacebookDescription}}
<textarea
class="gh-input"
maxlength="500"
placeholder={{truncate this.facebookDescription 160}}
{{on "blur" this.setFacebookDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "ogDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.ogDescription}}</textarea>
{{else}}
<div class="gh-social-og-preview-desc editable pointer" {{on "click" this.editFacebookDescription}}>
{{truncate this.facebookDescription}}
</div>
{{/if}}
</div>
</div>
</div>
<div class="gh-social-og-reactions">
<span class="gh-social-og-likes">{{svg-jar "facebook-like" class="z-999"}}{{svg-jar "facebook-heart" class="nl1"}}182</span>
<span class="gh-social-og-comments">7 comments</span>
<span class="gh-social-og-comments ml2">2 shares</span>
</div>
</div>
<div class="gh-social-twitter-container">
<div class="flex ma4">
<span>{{svg-jar "social-twitter" class="social-icon"}}</span>
<div>
<span class="gh-social-og-title">{{or this.settings.metaTitle this.settings.title}}</span>
<span class="gh-social-og-time">12 hrs</span>
<div class="flex flex-column mt2 mb3">
<span class="gh-social-og-desc w-100 mb2" />
<span class="gh-social-og-desc w-60" />
</div>
<div class="gh-social-twitter-post-preview"
{{on "mouseenter" (action (mut this.twitterHovered) true)}}
{{on "mouseleave" (action (mut this.twitterHovered) false)}}
>
{{#if (and this.twitterHovered (not this.twitterImage))}}
{{!-- only shown on hover when there's no image or fallback --}}
<button class="gh-social-twitter-preview-img-add" type="button" {{on "click" (fn this.triggerFileDialog "twitter")}}>+ Add image</button>
{{/if}}
<GhUploader
@extensions={{this.imageExtensions}}
@onComplete={{this.setTwitterImage}}
as |uploader|
>
{{#each uploader.errors as |error|}}
<div class="error pa2"><span class="response">{{or error.context error.message}}</span></div>
{{/each}}
{{#if (or this.twitterImage uploader.isUploading)}}
<div class="gh-social-twitter-preview-image relative" style={{background-image-style this.twitterImage}}>
<div class="flex h-100 items-center justify-center">
{{#if (or this.twitterHovered uploader.isUploading)}}
{{#if uploader.isUploading}}
{{uploader.progressBar}}
{{else}}
<button type="button" class="gh-btn gh-btn-white" {{on "click" (fn this.triggerFileDialog "twitter")}}><span>{{if @post.twitterImage "Change" "Upload"}} image</span></button>
{{/if}}
{{/if}}
{{#if (and this.twitterHovered @post.twitterImage)}}
<button type="button" class="gh-btn gh-btn-black gh-btn-icon gh-social-preview-img-delete" title="Remove custom Twitter image" {{on "click" this.clearTwitterImage}}>
<span>{{svg-jar "trash"}}</span>
<span class="hidden">Remove custom Twitter image</span>
</button>
{{/if}}
</div>
</div>
{{/if}}
<div style="display:none">
<GhFileInput id="twitterFileInput" @multiple={{false}} @action={{uploader.setFiles}} @accept={{this.imageMimeTypes}} />
</div>
</GhUploader>
<div class="gh-social-twitter-preview-content">
{{#if this.editingTwitterTitle}}
<input
type="text"
class="gh-input"
placeholder={{this.twitterTitle}}
value={{@post.twitterTitle}}
maxlength="300"
{{on "blur" this.setTwitterTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "twitterTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
/>
{{else}}
<div class="gh-social-twitter-preview-title editable pointer" {{on "click" this.editTwitterTitle}}>{{this.twitterTitle}}</div>
{{/if}}
{{#if this.editingTwitterDescription}}
<textarea
class="gh-input"
maxlength="500"
placeholder={{truncate this.twitterDescription 160}}
{{on "blur" this.setTwitterDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "twitterDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.twitterDescription}}</textarea>
{{else}}
<div class="gh-social-twitter-preview-desc editable pointer" {{on "click" this.editTwitterDescription}}>{{truncate this.twitterDescription}}</div>
{{/if}}
<div class="gh-social-twitter-preview-meta">
{{svg-jar "twitter-link"}}
{{this.config.blogDomain}}
</div>
</div>
</div>
<div class="gh-social-twitter-reactions">
<div class="flex items-center">{{svg-jar "twitter-comment"}}2</div>
<div class="flex items-center">{{svg-jar "twitter-retweet"}}11</div>
<div class="flex items-center">{{svg-jar "twitter-like"}}32</div>
<div class="flex items-center">{{svg-jar "twitter-share"}}</div>
</div>
</div>
</div>
</div>
</div>
<div class="gh-seo-preview-container">
{{svg-jar "google"}}
<div class="gh-seo-preview">
<div class="gh-seo-search-bar mb12">{{svg-jar "google-search"}}</div>
<div class="gh-seo-preview-link">{{this.serpURL}}</div>
{{#if this.editingMetaTitle}}
<input
type="text"
class="gh-input"
placeholder={{this.serpTitle}}
value={{@post.metaTitle}}
maxlength="300"
{{on "blur" this.setMetaTitle}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "metaTitle")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>
{{else}}
<div class="gh-seo-preview-title editable pointer" {{on "click" this.editMetaTitle}}>
{{this.serpTitle}}
</div>
{{/if}}
{{#if this.editingMetaDescription}}
<textarea
class="gh-input"
placeholder={{this.serpDescription}}
maxlength="500"
{{on "blur" this.setMetaDescription}}
{{on-key "Enter" this.blurElement}}
{{on-key "Escape" (fn this.cancelEdit "metaDescription")}}
{{autofocus}}
data-prevent-escape-close-modal="true"
>{{@post.metaDescription}}</textarea>
{{else}}
<div class="gh-seo-preview-desc editable pointer" {{on "click" this.editMetaDescription}}>
{{moment-format (now) "DD MMM YYYY"}}{{truncate this.serpDescription 149}}
</div>
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -1,209 +0,0 @@
import Component from '@glimmer/component';
import {
IMAGE_EXTENSIONS,
IMAGE_MIME_TYPES
} from 'ghost-admin/components/gh-image-uploader';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {tracked} from '@glimmer/tracking';
export default class ModalPostPreviewSocialComponent extends Component {
@service config;
@service settings;
@service ghostPaths;
@tracked editingFacebookTitle = false;
@tracked editingFacebookDescription = false;
@tracked editingTwitterTitle = false;
@tracked editingTwitterDescription = false;
@tracked editingMetaTitle = false;
@tracked editingMetaDescription = false;
imageExtensions = IMAGE_EXTENSIONS;
imageMimeTypes = IMAGE_MIME_TYPES;
get _fallbackDescription() {
return this.args.post.customExcerpt ||
this.serpDescription ||
this.settings.get('description');
}
@action
blurElement(event) {
if (!event.shiftKey) {
event.preventDefault();
event.target.blur();
}
}
@action
triggerFileDialog(name) {
const input = document.querySelector(`#${name}FileInput input`);
if (input) {
input.click();
}
}
// SERP
get serpTitle() {
return this.args.post.metaTitle || this.args.post.title || '(Untitled)';
}
get serpURL() {
const urlParts = [];
if (this.args.post.canonicalUrl) {
const canonicalUrl = new URL(this.args.post.canonicalUrl);
urlParts.push(canonicalUrl.host);
urlParts.push(...canonicalUrl.pathname.split('/').reject(p => !p));
} else {
const blogUrl = new URL(this.config.get('blogUrl'));
urlParts.push(blogUrl.host);
urlParts.push(...blogUrl.pathname.split('/').reject(p => !p));
urlParts.push(this.args.post.slug);
}
return urlParts.join(' > ');
}
get serpDescription() {
return this.args.post.metaDescription || this.args.post.excerpt;
}
@action
editMetaTitle() {
this.editingMetaTitle = true;
}
@action
setMetaTitle(event) {
const title = event.target.value;
this.args.post.metaTitle = title.trim();
this.args.post.save();
this.editingMetaTitle = false;
}
@action
editMetaDescription() {
this.editingMetaDescription = true;
}
@action
setMetaDescription(event) {
const description = event.target.value;
this.args.post.metaDescription = description.trim();
this.args.post.save();
this.editingMetaDescription = false;
}
// Facebook
get facebookTitle() {
return this.args.post.ogTitle || this.serpTitle;
}
get facebookDescription() {
return this.args.post.ogDescription || this._fallbackDescription;
}
get facebookImage() {
return this.args.post.ogImage || this.args.post.featureImage || this.settings.get('ogImage') || this.settings.get('coverImage');
}
@action
editFacebookTitle() {
this.editingFacebookTitle = true;
}
@action
cancelEdit(property, event) {
event.preventDefault();
event.target.value = this.args.post[property];
event.target.blur();
}
@action
setFacebookTitle(event) {
const title = event.target.value;
this.args.post.ogTitle = title.trim();
this.args.post.save();
this.editingFacebookTitle = false;
}
@action
editFacebookDescription() {
this.editingFacebookDescription = true;
}
@action
setFacebookDescription() {
const description = event.target.value;
this.args.post.ogDescription = description.trim();
this.args.post.save();
this.editingFacebookDescription = false;
}
@action
setFacebookImage([image]) {
this.args.post.ogImage = image.url;
this.args.post.save();
}
@action
clearFacebookImage() {
this.args.post.ogImage = null;
this.args.post.save();
}
// Twitter
get twitterTitle() {
return this.args.post.twitterTitle || this.serpTitle;
}
get twitterDescription() {
return this.args.post.twitterDescription || this._fallbackDescription;
}
get twitterImage() {
return this.args.post.twitterImage || this.args.post.featureImage || this.settings.get('twitterImage') || this.settings.get('coverImage');
}
@action
editTwitterTitle() {
this.editingTwitterTitle = true;
}
@action
setTwitterTitle(event) {
const title = event.target.value;
this.args.post.twitterTitle = title.trim();
this.args.post.save();
this.editingTwitterTitle = false;
}
@action
editTwitterDescription() {
this.editingTwitterDescription = true;
}
@action
setTwitterDescription() {
const description = event.target.value;
this.args.post.twitterDescription = description.trim();
this.args.post.save();
this.editingTwitterDescription = false;
}
@action
setTwitterImage([image]) {
this.args.post.twitterImage = image.url;
this.args.post.save();
}
@action
clearTwitterImage() {
this.args.post.twitterImage = null;
this.args.post.save();
}
}

View File

@ -2,7 +2,6 @@ import ConfirmEditorLeaveModal from '../components/modals/editor/confirm-leave';
import Controller, {inject as controller} from '@ember/controller';
import DeletePostModal from '../components/modals/delete-post';
import PostModel from 'ghost-admin/models/post';
import PostPreviewModal from '../components/modals/post-preview';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import classic from 'ember-classic-decorator';
import config from 'ghost-admin/config/environment';
@ -109,7 +108,6 @@ export default class EditorController extends Controller {
shouldFocusTitle = false;
showReAuthenticateModal = false;
showPostPreviewModal = false;
showUpgradeModal = false;
showDeleteSnippetModal = false;
showSettingsMenu = false;
@ -260,27 +258,6 @@ export default class EditorController extends Controller {
}
}
@action
openPostPreview(keyboardEvent) {
keyboardEvent?.preventDefault();
if (this.post.isDraft) {
this.openPostPreviewModal();
} else {
window.open(this.post.previewUrl, '_blank', 'noopener');
}
}
@action
openPostPreviewModal() {
this.modals.open(PostPreviewModal, {
post: this.post,
saveTask: this.saveTask,
hasDirtyAttributes: this.hasDirtyAttributes,
setEditorSaveType: this.setSaveType
});
}
@action
toggleReAuthenticateModal() {
if (this.showReAuthenticateModal) {
@ -485,16 +462,6 @@ export default class EditorController extends Controller {
status = 'draft';
}
}
// let the adapter know it should use the `?email_recipient_filter` QP when saving
let isPublishing = status === 'published' && !this.post.isPublished;
let isScheduling = status === 'scheduled' && !this.post.isScheduled;
if (options.sendEmailWhenPublished && (isPublishing || isScheduling)) {
options.adapterOptions = Object.assign({}, options.adapterOptions, {
sendEmailWhenPublished: options.sendEmailWhenPublished,
newsletter: options.newsletter
});
}
}
// set manually here instead of in beforeSaveTask because the
@ -509,7 +476,6 @@ export default class EditorController extends Controller {
post.set('statusScratch', null);
if (!options.silent) {
this.set('showPostPreviewModal', false);
this._showSaveNotification(prevStatus, post.get('status'), isNew ? true : false);
}
@ -940,7 +906,6 @@ export default class EditorController extends Controller {
this.set('post', null);
this.set('hasDirtyAttributes', false);
this.set('shouldFocusTitle', false);
this.set('showPostPreviewModal', false);
this.set('showSettingsMenu', false);
this.set('wordCount', null);

View File

@ -66,7 +66,6 @@ export default class FeatureService extends Service {
@feature('improvedOnboarding') improvedOnboarding;
@feature('membersTableStatus') membersTableStatus;
@feature('selectablePortalLinks') selectablePortalLinks;
@feature('publishingFlow') publishingFlow;
_user = null;

View File

@ -406,10 +406,6 @@ input:focus,
color: var(--darkgrey);
}
.gh-publishmenu-dropdown {
background: var(--whitegrey);
}
.gh-publish-setting {
border-bottom: 1px solid var(--lightgrey-l1);
}
@ -837,7 +833,7 @@ input:focus,
background: var(--white) !important;
}
.gh-btn-editor.active,
.gh-btn-editor.active,
.gh-btn-editor:hover {
background: var(--lightgrey) !important;
}

View File

@ -20,189 +20,6 @@
outline: 0;
}
.gh-publishmenu {
position: relative;
z-index: 1000;
display: inherit;
margin-right: 8px;
}
@media (max-width: 500px) {
.gh-publishmenu {
margin-right: 0;
}
}
.gh-publishmenu .sent {
display: block;
height: 34px;
margin-top: -2px;
color: var(--darkgrey);
font-size: 1.35rem;
font-weight: 500;
line-height: 34px;
letter-spacing: .2px;
}
.gh-publishmenu-dropdown {
position: absolute;
top: 100%;
right: 0;
margin: 5px 0 20px 0;
padding: 0px;
width: 344px;
background-color: #fff;
background-clip: padding-box;
border-radius: 4px;
list-style: none;
text-align: left;
text-transform: none;
font-size: 1.4rem;
font-weight: normal;
will-change: transform, opacity;
z-index: 99999; /* needs to sit on top of preview modal */
/* box-shadow: 0 0 0 1px rgba(99,114,130,0.06), 0 8px 16px rgba(27,39,51,0.08); */
box-shadow: var(--box-shadow-m);
}
.gh-publishmenu-dropdown.ember-basic-dropdown--transitioning-in {
animation: fade-in-scale 0.2s;
animation-fill-mode: forwards;
}
.gh-publishmenu-dropdown.ember-basic-dropdown--transitioning-out {
animation: fade-out 0.5s;
animation-fill-mode: forwards;
}
.gh-publishmenu-heading {
margin: 0 0 15px 0;
padding: 20px 20px 0;
font-size: 1.7rem;
font-weight: 400;
line-height: 1.25em;
}
.gh-publishmenu-select {
display: inline-block;
}
.gh-publishmenu-select .ember-power-select-inline {
padding-right: 3px;
color: var(--black);
font-size: 1.7rem;
font-weight: 500;
line-height: 1.25em;
}
.gh-publishmenu-select .ember-power-select-inline svg {
width: 9px !important;
height: 5.6px !important;
margin: 0 !important;
}
.gh-publishmenu-select .ember-power-select-inline svg path {
stroke: var(--black) !important;
stroke-width: 4;
}
.gh-publishmenu-select-dropdown {
width: unset !important;
min-width: min-content !important;
margin-top: 4px;
border-top: 1px solid var(--input-border-color) !important;
font-size: 1.4rem;
white-space: nowrap;
border-radius: 3px !important;
}
.gh-publishmenu-content:not(.gh-publishmenu-content.no-border) {
border-bottom: var(--whitegrey) 1px solid;
}
.gh-publishmenu-footer {
margin: 15px 0 0 0;
padding: 0 20px 20px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.gh-publishmenu-button {
float: right;
margin-left: 8px;
}
.gh-publishmenu-radio {
display: flex;
margin: 20px 0;
}
.gh-publishmenu-section {
padding: 0 20px;
border-top: var(--whitegrey) 1px solid;
}
.gh-publishmenu-section.no-border {
border-top: 0;
}
.gh-publishmenu-radio-button {
flex-shrink: 0;
position: relative;
width: 15px;
height: 15px;
border: color-mod(var(--whitegrey) l(-10%)) 1px solid;
border-radius: 100%;
background: #fff;
}
.gh-publishmenu-radio-content {
display: flex;
flex-direction: column;
margin: 0 0 0 15px;
width: 100%;
}
.gh-publishmenu-radio-label {
display: block;
font-size: 1.4rem;
line-height: 1.2em;
font-weight: 500;
}
.gh-publishmenu-radio-desc {
font-size: 1.3rem;
line-height: 1.4em;
font-weight: 300;
color: var(--midgrey-l1);
margin-top: 2px;
}
.gh-publishmenu-radio-label:hover,
.gh-publishmenu-radio-button:hover {
cursor: pointer;
}
.gh-publishmenu-radio.active .gh-publishmenu-radio-button {
border-color: var(--black);
background: var(--black);
}
.gh-publishmenu-radio.active .gh-publishmenu-radio-button:before {
display: block;
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 7px;
height: 7px;
background: var(--white);
border-radius: 100%;
box-shadow: rgba(0,0,0,0.25) 0 1px 3px;
}
.gh-date-time-picker {
position: relative;
display: flex;
@ -290,47 +107,6 @@
color: var(--red);
}
.gh-publishmenu-email {
margin: 15px 0;
justify-content: space-between;
align-items: center;
}
.gh-publishmenu-email .gh-box {
padding: 12px 16px;
font-size: 1.3rem;
line-height: 1.5em;
}
.gh-publishmenu-email .select-members {
margin-bottom: .2rem;
}
.gh-publishmenu-email .segment-totals {
color: var(--midgrey-l1);
font-size: 1.3rem;
font-weight: 300;
}
.for-checkbox .gh-publishmenu-email-checkbox {
margin-right: 0;
margin-top: -2px;
background: var(--white);
}
.gh-publishmenu-email-label.disabled {
pointer-events: none;
}
.gh-publishmenu-content .for-switch.pe-none {
opacity: 0.6;
}
.gh-publishmenu-email-info {
margin: 15px 0;
color: var(--midgrey);
}
.gh-publish-send-to {
display: flex;
}
@ -412,30 +188,6 @@
font-size: 1.3rem;
}
.gh-publishmenu-checkbox-disabled {
color: var(--midlightgrey);
opacity: 0.6;
pointer-events: none;
}
.gh-publishmenu-checkbox-disabled p {
color: var(--midgrey) !important;
}
.gh-publishmenu-newsletter-trigger {
padding: 7px 10px;
}
.gh-publishmenu-newsletter-trigger.disabled span {
opacity: .5;
}
.gh-publishmenu-newsletter-trigger svg {
position: absolute;
top: 50%;
right: 10px;
}
.gh-publish-newsletter-dropdown {
z-index: 99999;
}

View File

@ -813,63 +813,6 @@
height: 6px;
}
.gh-email-publishmenu-preview .gh-main-section-header.small {
padding-top: 4px;
}
.gh-email-publishmenu-preview .gh-publishmenu-dropdown-container {
height: calc(346px * .9);
}
.gh-email-publishmenu-preview .gh-publishmenu-dropdown {
position: relative;
top: inherit;
right: inherit;
margin: 0;
box-shadow:
0 7px 7px rgba(0, 0, 0, 0.04),
0 3px 2.2px -5px rgba(0, 0, 0, 0.011),
0 6px 5.3px -5px rgba(0, 0, 0, 0.016),
0 10.5px 10px -5px rgba(0, 0, 0, 0.02),
0 18px 17.9px -5px rgba(0, 0, 0, 0.024),
0 33.8px 33.4px -5px rgba(0, 0, 0, 0.029),
0 80px 80px -5px rgba(0, 0, 0, 0.04)
;
transform: scale(.9);
transform-origin: top left;
pointer-events: none;
}
.gh-publishmenu-heading-dropdown {
font-weight: 500;
color: var(--black);
}
.gh-publishmenu-heading-dropdown svg {
width: auto;
height: 9px;
margin-right: 4px;
}
.gh-publishmenu-heading-dropdown svg path {
stroke: var(--black);
stroke-width: 2px;
}
.gh-email-publishmenu-preview .gh-publishmenu-radio .gh-publishmenu-radio-label:after {
content: "";
display: block;
width: 80%;
height: 12px;
margin-top: 8px;
background: var(--whitegrey-d1);
border-radius: 6px;
}
.gh-email-publishmenu-preview .gh-publishmenu-radio.active .gh-publishmenu-radio-label:after {
width: 100%;
}
.member-segments .select-members {
margin-bottom: .4rem;
}
@ -966,9 +909,9 @@ body:not([data-user-is-dragging]) .gh-newsletter-card-draggable:hover .grab-news
visibility: visible;
opacity: 1;
/*
To make sure the grab handler also fades out correctly and only change visibility after the animation,
we need to update the animation curve for visibility to step-start at the start of the animation
/*
To make sure the grab handler also fades out correctly and only change visibility after the animation,
we need to update the animation curve for visibility to step-start at the start of the animation
*/
transition: visibility 200ms step-start, opacity 200ms ease-in-out;
}
@ -1115,7 +1058,7 @@ body:not([data-user-is-dragging]) .gh-newsletter-card-draggable:hover .grab-news
.gh-newsletters-labs .gh-members-emailpreview-faux .strong {
color: var(--darkgrey);
font-size: 1.4rem;
font-weight: 600;
font-weight: 600;
}
.gh-newsletters-labs .gh-members-emailpreview-faux p {

View File

@ -291,19 +291,6 @@
</div>
</div>
</div>
<div class="gh-expandable-block">
<div class="gh-expandable-header">
<div>
<h4 class="gh-expandable-title">Publishing flow</h4>
<p class="gh-expandable-description">
Revised workflow experience when publishing/scheduling/sending
</p>
</div>
<div class="for-switch">
<GhFeatureFlag @flag="publishingFlow" />
</div>
</div>
</div>
</div>
</div>
{{/if}}