mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 20:03:12 +03:00
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:
parent
7521d69405
commit
77484210ee
@ -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') {
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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');
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
});
|
@ -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? Here’s 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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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> <{{full-email-address (or this.newsletter.senderEmail "noreply")}}>
|
||||
</p>
|
||||
<p><span class="dark">To:</span> Jamie Larson <jamie@example.com></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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 you’d 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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -66,7 +66,6 @@ export default class FeatureService extends Service {
|
||||
@feature('improvedOnboarding') improvedOnboarding;
|
||||
@feature('membersTableStatus') membersTableStatus;
|
||||
@feature('selectablePortalLinks') selectablePortalLinks;
|
||||
@feature('publishingFlow') publishingFlow;
|
||||
|
||||
_user = null;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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}}
|
||||
|
Loading…
Reference in New Issue
Block a user