Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved post preview design #22113

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const Sidebar: React.FC<SidebarProps> = ({
}) => {
const visibilityCheckboxes = [
{
label: 'Logged out visitors',
label: 'Anonymous visitors',
onChange: (e:boolean) => {
toggleVisibility('visitors', e);
},
Expand Down
118 changes: 99 additions & 19 deletions ghost/admin/app/components/editor/modals/preview.hbs
Original file line number Diff line number Diff line change
@@ -1,29 +1,118 @@
<div class="flex flex-column h-100">
<div class="gh-post-preview-modal">
<header class="modal-header gh-post-preview-header" data-test-modal="preview-email">
<div class="left">
<button class="gh-btn-editor gh-editor-back-button" title="Close" type="button" {{on "click" @close}}>
<span>{{svg-jar "arrow-left"}} Editor</span>
</button>
<h2>Preview</h2>
</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>
<button type="button" class="gh-btn gh-post-preview-mode {{if (or (eq this.tab "browser") (eq this.tab "mobile")) "gh-btn-group-selected"}}" {{on "click" (fn this.changeTab "browser")}}><span>Web</span></button>
{{#if (and (not-eq this.settings.membersSignupAccess "none") (not-eq this.settings.editorDefaultEmailRecipients "disabled"))}}
{{#if (and @data.publishOptions.post.isPost (not @data.publishOptions.user.isContributor))}}
<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")}} data-test-button="email-preview"><span>{{svg-jar "email-unread"}}</span></button>
<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")}} data-test-button="email-preview"><span>Email</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 "facebook-logo"}}</span></button>
</div>
<div class="gh-contentfilter-divider"></div>
<div class="gh-contentfilter gh-btn-group">
<button type="button" class="gh-btn {{if (or (eq this.tab "browser") (eq this.tab "email")) "gh-btn-group-selected"}} gh-post-preview-mode" {{on "click" (fn this.changeTab "browser")}}><span>{{svg-jar "laptop"}}</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>
</div>
<div class="gh-contentfilter-divider"></div>
<span class="gh-select gh-web-preview-segment">
<PowerSelect
@selected={{hash value="free" label="Free member"}}
@options={{if (eq this.tab "email")
(array
(hash value="free" label="Free member")
(hash value="paid" label="Paid member")
)
(array
(hash value="anonymous" label="Anonymous")
(hash value="free" label="Free member")
(hash value="paid" label="Paid member")
)
}}
@onChange={{this.noop}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-preview-segment-trigger"
@dropdownClass="gh-preview-segment-dropdown"
as |option|
>
{{option.label}}
</PowerSelect>
</span>
</div>
<div class="right">
{{#if (eq this.tab "email")}}
<div class="gh-post-test-email-group">
<GhDropdownButton
@dropdownName="post-preview-test-email"
@classNames="gh-btn gh-post-preview-email-trigger"
data-test-button="post-preview-test-email"
>
<span>{{svg-jar "send-email"}}</span>
</GhDropdownButton>
<GhDropdown
@name="post-preview-test-email"
@classNames="dropdown-menu gh-post-preview-email-test-dropdown"
@onOpen={{this.focusInput}}
>
<div class="gh-post-preview-email-test">
<form class="form-group">
<label for="post-preview-email-input">Send test email</label>
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-input-x gh-post-preview-email-input"
placeholder="[email protected]"
aria-label="Email address to receive preview"
aria-invalid={{if this.sendPreviewEmailError "true"}}
aria-describedby={{if this.sendPreviewEmailError "sendError"}}
data-post-preview-email-input
autofocus="true"
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>
<p class="description">You'll receive this as a free member.</p>
<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-icon gh-btn-primary"
data-test-button="send-test-email"
/>
</form>
</div>

{{#if this.sendPreviewEmailError}}
<div class="gh-post-preview-email-error">
<span class="response" id="sendError">{{this.sendPreviewEmailError}}</span>
</div>
{{/if}}
</GhDropdown>
</div>
{{else}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify error handling in email preview.

The email preview form includes error handling but ensure:

  1. Error messages are properly displayed
  2. Network errors are handled gracefully
  3. Rate limiting is considered

🏁 Script executed:

#!/bin/bash
# Search for email-related error handling
rg -A 5 "sendPreviewEmailError|sendPreviewEmailTask" --type js

Length of output: 2959


Email Preview Error Handling Verification

  • Error Messages Displayed: The template in ghost/admin/app/components/editor/modals/preview.hbs properly renders the error state via the sendPreviewEmailError property.
  • Network Errors Handling: The sendPreviewEmailTask in email.js uses a try-catch block to capture errors and assigns the error message to sendPreviewEmailError, ensuring that network issues are communicated.
  • Rate Limiting: There is no explicit handling or differentiation for rate limiting (e.g. detecting HTTP 429 responses) in the task. If rate limiting is expected, consider adding specific logic to identify and manage such responses.

<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-btn gh-post-preview-url gh-tooltip-trigger tooltip-bottom no-shortcut">
<span>
{{#if this.copyPreviewUrl.isRunning}}
{{svg-jar "check-circle" class="check v-mid mr1 ml2"}} Copied
{{else}}
{{svg-jar "link"}}
{{/if}}
</span>
<GhTooltip @text="Copy preview link" />
</button>
<a href={{@post.previewUrl}} target="_blank" rel="noopener noreferrer" class="gh-btn gh-publish-preview-newtab gh-tooltip-trigger tooltip-bottom no-shortcut">
<span>{{svg-jar "external"}}</span>
<GhTooltip @text="Open in new tab" />
</a>
{{/if}}
<div class="gh-contentfilter-divider"></div>
<button
type="button"
class="gh-btn gh-btn-editor gh-editor-preview-trigger active"
{{on "click" @close}}
>
<span>Preview</span>
<span>Close</span>
</button>

{{#if @data.publishOptions.user.isContributor}}
Expand All @@ -36,14 +125,12 @@
{{else}}
<button
type="button"
class="gh-btn gh-btn-editor darkgrey gh-publish-trigger"
class="gh-btn gh-btn-primary"
{{on "click" @data.togglePreviewPublish}}
>
<span>Publish</span>
</button>
{{/if}}

<div class="settings-menu-toggle-spacer"></div>
</div>
</header>

Expand Down Expand Up @@ -74,12 +161,5 @@
/>
{{/if}}
{{/unless}}

{{#if (eq this.tab "social")}}
<Editor::Modals::Preview::Social
@post={{@data.publishOptions.post}}
@skipAnimation={{this.skipAnimation}}
/>
{{/if}}
{{/if}}
</div>
19 changes: 18 additions & 1 deletion ghost/admin/app/components/editor/modals/preview.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Component from '@glimmer/component';
import copyTextToClipboard from 'ghost-admin/utils/copy-text-to-clipboard';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency';
import {task, timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking';

export default class EditorPostPreviewModal extends Component {
Expand All @@ -16,6 +17,7 @@ export default class EditorPostPreviewModal extends Component {

@tracked tab = this.args.data.currentTab || 'browser';
@tracked isChangingTab = false;
@tracked previewEmailAddress = this.session.user.email;

constructor() {
super(...arguments);
Expand All @@ -33,6 +35,13 @@ export default class EditorPostPreviewModal extends Component {
this.args.data.changeTab?.(tab);
}

@action
focusInput() {
setTimeout(() => {
document.querySelector('[data-post-preview-email-input]')?.focus();
}, 100);
}

@task
*saveFirstTask() {
const {saveTask, publishOptions, hasDirtyAttributes} = this.args.data;
Expand All @@ -45,4 +54,12 @@ export default class EditorPostPreviewModal extends Component {
yield saveTask.perform();
}
}

@task
*copyPreviewUrl() {
copyTextToClipboard(this.args.post.previewUrl);
yield timeout(this.isTesting ? 50 : 3000);
}

noop() {}
}
25 changes: 0 additions & 25 deletions ghost/admin/app/components/editor/modals/preview/browser.hbs
Original file line number Diff line number Diff line change
@@ -1,29 +1,4 @@
<div class="gh-post-preview-container gh-post-preview-browser-container {{unless @skipAnimation "fade-in"}}">
<div class="gh-browserpreview-browser">
<div class="tabs">
<ul><li></li><li></li><li></li></ul>
<div>
{{#if (or @icon this.settings.icon)}}
<span class="favicon"><img src={{or @icon this.settings.icon}} alt="icon"></span>
{{else}}
<span class="favicon default">{{svg-jar "default-favicon"}}</span>
{{/if}}
<span class="db truncate w-90">{{@post.previewUrl}}</span>
<button type="button" {{on "click" (perform this.copyPreviewUrl)}} class="gh-post-preview-url">
<span class="green-d1">
{{#if this.copyPreviewUrl.isRunning}}
{{svg-jar "check-circle" class="check v-mid mr1 ml2"}} Copied
{{else}}
{{svg-jar "copy" class="w4 ml3 v-mid fill-darkgrey"}}
{{/if}}
</span>
</button>
<a href={{@post.previewUrl}} target="_blank" rel="noopener noreferrer" class="gh-publish-preview-newtab">
{{svg-jar "external"}}
</a>
</div>
</div>
</div>
<div class="gh-browserpreview-iframecontainer">
<iframe class="gh-pe-iframe" src={{@post.previewUrl}} title="Desktop browser post preview"></iframe>
</div>
Expand Down
82 changes: 10 additions & 72 deletions ghost/admin/app/components/editor/modals/preview/email.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@
<div class="gh-post-preview-email-header">
<div class="gh-post-preview-email-columns">
<div class="gh-post-preview-email-group">
<div class="gh-email-preview-newsletter-select" data-test-email-preview-newsletter-select-section>
<p>From:</p>
<form class="gh-email-preview-newsletter-select" data-test-email-preview-newsletter-select-section>
<label for="email-preview-newsletter-select">From</label>
{{#if (gt this.newslettersList.length 1)}}
<PowerSelect
@selected={{this.newsletter}}
@options={{this.newslettersList}}
@onChange={{this.setNewsletter}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-preview-newsletter-trigger"
@dropdownClass="gh-publish-newsletter-dropdown gh-preview-newsletter-dropdown"
@extra={{concat "<" (sender-email-address this.newsletter.senderEmail) ">"}}
@triggerClass="gh-preview-newsletter-trigger gh-input-x"
@dropdownClass="gh-dropdown-x gh-publish-newsletter-dropdown gh-preview-newsletter-dropdown"
@selectedItemComponent={{component "editor/modals/preview/selected-newsletter-label"}}
data-test-email-preview-newsletter-select
as |option|
Expand All @@ -24,80 +23,19 @@
<p class="gh-preview-newsletter-name">{{this.newsletter.name}} <span
class="gh-preview-email-address">&lt;{{sender-email-address this.newsletter.senderEmail}}&gt;</span></p>
{{/if}}
</div>
<div class="gh-email-preview-newsletter-select" data-test-email-preview-segment-select-section>
<p>To:</p>
{{#if this.paidMembersEnabled}}
<PowerSelect
@selected={{this.selectedSegment}}
@options={{this.segments}}
@onChange={{this.setSegment}}
@triggerComponent={{component "gh-power-select/trigger"}}
@triggerClass="gh-preview-newsletter-trigger"
@dropdownClass="gh-publish-newsletter-dropdown gh-preview-newsletter-dropdown"
data-test-email-preview-segment-select
as |option|
>
<span>{{option.name}}</span>
</PowerSelect>
{{else}}
<p class="gh-preview-newsletter-name">Jamie Larson <span class="gh-preview-email-address">&lt;[email protected]&gt;</span>
</p>
{{/if}}
</div>
</div>

<div class="gh-post-test-email-group">
<GhDropdownButton
@dropdownName="post-preview-test-email"
@classNames="gh-btn gh-btn-text gh-btn-icon gh-post-preview-email-trigger"
data-test-button="post-preview-test-email"
>
<span>Send test email {{svg-jar "arrow-down-small"}}</span>
</GhDropdownButton>

<GhDropdown
@name="post-preview-test-email"
@classNames="dropdown-menu dropdown-align-right gh-post-preview-email-test-dropdown"
>
<div class="gh-post-preview-email-test">
<Input
@value={{this.previewEmailAddress}}
class="gh-input gh-post-preview-email-input"
placeholder="[email protected]"
aria-label="Email address to receive preview"
aria-invalid={{if this.sendPreviewEmailError "true"}}
aria-describedby={{if this.sendPreviewEmailError "sendError"}}
data-post-preview-email-input
{{on-key "Enter" (perform this.sendPreviewEmailTask)}}
/>

<GhTaskButton
@task={{this.sendPreviewEmailTask}}
@buttonText="Send"
@successText="Sent"
@runningText="Sending..."
@class="gh-btn gh-btn-icon gh-btn-primary"
data-test-button="send-test-email"
/>
</div>

{{#if this.sendPreviewEmailError}}
<div class="gh-post-preview-email-error">
<span class="response" id="sendError">{{this.sendPreviewEmailError}}</span>
</div>
{{/if}}
</GhDropdown>
</form>
</div>
</div>

<div class="gh-email-preview-newsletter-select" data-test-email-preview-segment-select-section>
<p>Subject:</p>
<hr>

<form class="gh-email-preview-newsletter-select" data-test-email-preview-segment-select-section>
<label for="email-preview-newsletter-select">Subject</label>
<Editor::Modals::Preview::Email::EmailSubject
@post={{@post}}
@savePostTask={{@savePostTask}}
/>
</div>
</form>
</div>
<iframe class="gh-pe-iframe" {{did-insert this.renderEmailPreview}} title="Email preview"
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
<div class="gh-email-subject">
{{#if this.isEditing}}
<input
aria-label="Email subject"
type="text"
class="gh-input gh-preview-email-subject-input"
placeholder={{truncate @post.title 40}}
value={{this.subject}}
{{on "blur" this.setSubject}}
{{on-key "Enter" this.setSubject}}
{{autofocus}}
/>
{{else}}
<button class="gh-preview-email-subject" type="button" {{on "click" this.editSubject}}>
{{this.subject}} {{svg-jar "pen"}}
</button>
{{/if}}
<input
aria-label="Email subject"
type="text"
class="gh-input gh-input-x"
placeholder={{truncate @post.title 40}}
value={{this.subject}}
{{on "blur" this.setSubject}}
{{on-key "Enter" this.setSubject}}
/>

<div class="error">
<GhErrorMessage @errors={{@post.errors}} @property="emailSubject" />
Expand Down
Loading
Loading