Added preview component with preview support for campaigns and templates

This commit is contained in:
Kailash Nadh 2018-10-26 11:18:17 +05:30
parent 2121c250ff
commit a1b5a39cfb
12 changed files with 200 additions and 85 deletions

View File

@ -89,26 +89,26 @@ func handlePreviewCampaign(c echo.Context) error {
var (
app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id"))
camps models.Campaigns
body = c.FormValue("body")
camp models.Campaign
)
if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
}
err := app.Queries.GetCampaigns.Select(&camps, id, "", 0, 1)
err := app.Queries.GetCampaignForPreview.Get(&camp, id)
if err != nil {
if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
}
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
} else if len(camps) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
}
var (
camp = camps[0]
sub models.Subscriber
)
var sub models.Subscriber
// Get a random subscriber from the campaign.
if err := app.Queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil {
if err == sql.ErrNoRows {
@ -126,7 +126,10 @@ func handlePreviewCampaign(c echo.Context) error {
}
// Compile the template.
tpl, err := runner.CompileMessageTemplate(`{{ template "content" . }}`, camp.Body)
if body == "" {
body = camp.Body
}
tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, body)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
}

View File

@ -2,6 +2,7 @@ import React from "react"
import { Modal, Tabs, Row, Col, Form, Switch, Select, Radio, Tag, Input, Button, Icon, Spin, DatePicker, Popconfirm, notification } from "antd"
import * as cs from "./constants"
import Media from "./Media"
import ModalPreview from "./ModalPreview"
import moment from 'moment'
import ReactQuill from "react-quill"
@ -352,6 +353,7 @@ class Campaign extends React.PureComponent {
record: {},
contentType: "richtext",
messengers: [],
previewRecord: null,
body: "",
currentTab: "form",
editor: null,
@ -405,6 +407,10 @@ class Campaign extends React.PureComponent {
this.setState({ currentTab: tab })
}
handlePreview = (record) => {
this.setState({ previewRecord: record })
}
render() {
return (
<section className="content campaign">
@ -457,7 +463,7 @@ class Campaign extends React.PureComponent {
formDisabled={ this.state.formDisabled }
/>
<div className="content-actions">
<p><Button icon="search">Preview</Button></p>
<p><Button icon="search" onClick={() => this.handlePreview(this.state.record)}>Preview</Button></p>
</div>
</Tabs.TabPane>
</Tabs>
@ -471,8 +477,19 @@ class Campaign extends React.PureComponent {
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
onCancel: this.toggleMedia,
onOk: this.toggleMedia
} } />
}} />
</Modal>
{ this.state.previewRecord &&
<ModalPreview
title={ this.state.previewRecord.name }
body={ this.state.body }
previewURL={ cs.Routes.PreviewCampaign.replace(":id", this.state.previewRecord.id) }
onCancel={() => {
this.setState({ previewRecord: null })
}}
/>
}
</section>
)
}

View File

@ -4,6 +4,7 @@ import { Row, Col, Button, Table, Icon, Tooltip, Tag, Popconfirm, Progress, Moda
import dayjs from "dayjs"
import relativeTime from 'dayjs/plugin/relativeTime'
import ModalPreview from "./ModalPreview"
import * as cs from "./constants"
class Campaigns extends React.PureComponent {
@ -15,6 +16,7 @@ class Campaigns extends React.PureComponent {
queryParams: "",
stats: {},
record: null,
previewRecord: null,
cloneName: "",
modalWaiting: false
}
@ -110,10 +112,16 @@ class Campaigns extends React.PureComponent {
title: "",
dataIndex: "actions",
className: "actions",
width: "10%",
width: "20%",
render: (text, record) => {
return (
<div className="actions">
<Tooltip title="Preview campaign" placement="bottom">
<a role="button" onClick={() => {
this.handlePreview(record)
}}><Icon type="search" /></a>
</Tooltip>
<Tooltip title="Clone campaign" placement="bottom">
<a role="button" onClick={() => {
let r = { ...record, lists: record.lists.map((i) => { return i.id }) }
@ -352,6 +360,10 @@ class Campaigns extends React.PureComponent {
})
}
handlePreview = (record) => {
this.setState({ previewRecord: record })
}
render() {
const pagination = {
...this.paginationOptions,
@ -377,18 +389,15 @@ class Campaigns extends React.PureComponent {
pagination={ pagination }
/>
{ this.state.record &&
<Modal visible={ this.state.record } width="500px"
className="clone-campaign-modal"
title={ "Clone " + this.state.record.name}
okText="Clone"
confirmLoading={ this.state.modalWaiting }
onCancel={ this.handleToggleCloneForm }
onOk={() => { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }}>
<Input autoFocus defaultValue={ this.state.record.name } style={{ width: "100%" }} onChange={(e) => {
this.setState({ cloneName: e.target.value })
}} />
</Modal> }
{ this.state.previewRecord &&
<ModalPreview
title={ this.state.previewRecord.name }
previewURL={ cs.Routes.PreviewCampaign.replace(":id", this.state.previewRecord.id) }
onCancel={() => {
this.setState({ previewRecord: null })
}}
/>
}
</section>
)
}

View File

@ -0,0 +1,54 @@
import React from "react"
import { Modal } from "antd"
import * as cs from "./constants"
class ModalPreview extends React.PureComponent {
makeForm(body) {
let form = document.createElement("form")
form.method = cs.MethodPost
form.action = this.props.previewURL
form.target = "preview-iframe"
let input = document.createElement("input")
input.type = "hidden"
input.name = "body"
input.value = body
form.appendChild(input)
document.body.appendChild(form)
form.submit()
}
render () {
return (
<Modal visible={ true } title={ this.props.title }
className="preview-modal"
width="90%"
height={ 900 }
onCancel={ this.props.onCancel }
onOk={ this.props.onCancel }>
<div className="preview-iframe-container">
<iframe title={ this.props.title ? this.props.title : "Preview" }
name="preview-iframe"
id="preview-iframe"
className="preview-iframe"
ref={(o) => {
if(o) {
// When the DOM reference for the iframe is ready,
// see if there's a body to post with the form hack.
if(this.props.body !== undefined
&& this.props.body !== null) {
this.makeForm(this.props.body)
}
}
}}
src={ this.props.previewURL ? this.props.previewURL : "about:blank" }>
</iframe>
</div>
</Modal>
)
}
}
export default ModalPreview

View File

@ -1,13 +1,16 @@
import React from "react"
import { Row, Col, Modal, Form, Input, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification } from "antd"
import ModalPreview from "./ModalPreview"
import Utils from "./utils"
import * as cs from "./constants"
class CreateFormDef extends React.PureComponent {
state = {
confirmDirty: false,
modalWaiting: false
modalWaiting: false,
previewName: "",
previewBody: ""
}
// Handle create / edit form submission.
@ -50,6 +53,10 @@ class CreateFormDef extends React.PureComponent {
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
}
handlePreview = (name, body) => {
this.setState({ previewName: name, previewBody: body })
}
render() {
const { formType, record, onClose } = this.props
const { getFieldDecorator } = this.props.form
@ -64,37 +71,54 @@ class CreateFormDef extends React.PureComponent {
}
return (
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
width="90%"
height={ 900 }
confirmLoading={ this.state.modalWaiting }
onCancel={ onClose }
onOk={ this.handleSubmit }>
<div>
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
width="90%"
height={ 900 }
confirmLoading={ this.state.modalWaiting }
onCancel={ onClose }
onOk={ this.handleSubmit }>
<Spin spinning={ this.props.reqStates[cs.ModelTemplates] === cs.StatePending }>
<Form onSubmit={this.handleSubmit}>
<Form.Item {...formItemLayout} label="Name">
{getFieldDecorator("name", {
initialValue: record.name,
rules: [{ required: true }]
})(<Input autoFocus maxLength="200" />)}
</Form.Item>
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
{getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }}>
</Input.TextArea>
)}
</Form.Item>
</Form>
</Spin>
<Row>
<Col span="4"></Col>
<Col span="18" className="text-grey text-small">
The placeholder <code>{'{'}{'{'} template "content" . {'}'}{'}'}</code> should appear in the template. <a href="" target="_blank">Read more on templating</a>.
</Col>
</Row>
</Modal>
<Spin spinning={ this.props.reqStates[cs.ModelTemplates] === cs.StatePending }>
<Form onSubmit={this.handleSubmit}>
<Form.Item {...formItemLayout} label="Name">
{getFieldDecorator("name", {
initialValue: record.name,
rules: [{ required: true }]
})(<Input autoFocus maxLength="200" />)}
</Form.Item>
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
{getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />
)}
</Form.Item>
<Form.Item {...formItemLayout} colon={ false } label="&nbsp;">
<Button icon="search" onClick={ () =>
this.handlePreview(this.props.form.getFieldValue("name"), this.props.form.getFieldValue("body"))
}>Preview</Button>
</Form.Item>
</Form>
</Spin>
<Row>
<Col span="4"></Col>
<Col span="18" className="text-grey text-small">
The placeholder <code>{'{'}{'{'} template "content" . {'}'}{'}'}</code> should appear in the template. <a href="" target="_blank">Read more on templating</a>.
</Col>
</Row>
</Modal>
{ this.state.previewBody &&
<ModalPreview
title={ this.state.previewName ? this.state.previewName : "Template preview" }
previewURL={ cs.Routes.PreviewTemplate }
body={ this.state.previewBody }
onCancel={() => {
this.setState({ previewBody: null, previewName: null })
}}
/>
}
</div>
)
}
}
@ -243,17 +267,15 @@ class Templates extends React.PureComponent {
fetchRecords = { this.fetchRecords }
/>
<Modal visible={ this.state.previewRecord !== null } title={ this.state.previewRecord ? this.state.previewRecord.name : "" }
className="template-preview-modal"
width="90%"
height={ 900 }
onOk={ () => { this.setState({ previewRecord: null }) } }>
{ this.state.previewRecord !== null &&
<iframe title="Template preview"
className="template-preview"
src={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }>
</iframe> }
</Modal>
{ this.state.previewRecord &&
<ModalPreview
title={ this.state.previewRecord.name }
previewURL={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }
onCancel={() => {
this.setState({ previewRecord: null })
}}
/>
}
</section>
)
}

View File

@ -60,6 +60,7 @@ export const Routes = {
GetSubscribers: "/api/subscribers",
GetSubscribersByList: "/api/subscribers/lists/:listID",
PreviewCampaign: "/api/campaigns/:id/preview",
CreateSubscriber: "/api/subscribers",
UpdateSubscriber: "/api/subscribers/:id",
DeleteSubscriber: "/api/subscribers/:id",
@ -81,6 +82,7 @@ export const Routes = {
GetTemplates: "/api/templates",
PreviewTemplate: "/api/templates/:id/preview",
PreviewNewTemplate: "/api/templates/preview",
CreateTemplate: "/api/templates",
UpdateTemplate: "/api/templates/:id",
SetDefaultTemplate: "/api/templates/:id/default",

View File

@ -253,12 +253,15 @@ td.actions {
.templates .template-body {
margin-top: 30px;
}
.template-preview {
.preview-iframe-container {
min-height: 500px;
}
.preview-iframe {
border: 0;
width: 100%;
height: 100%;
min-height: 500px;
}
.template-preview-modal .ant-modal-footer button:first-child {
.preview-modal .ant-modal-footer button:first-child {
display: none;
}

View File

@ -103,6 +103,7 @@ func registerHandlers(e *echo.Echo) {
e.GET("/api/campaigns/messengers", handleGetCampaignMessengers)
e.GET("/api/campaigns/:id", handleGetCampaigns)
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
e.POST("/api/campaigns", handleCreateCampaign)
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
@ -115,6 +116,7 @@ func registerHandlers(e *echo.Echo) {
e.GET("/api/templates", handleGetTemplates)
e.GET("/api/templates/:id", handleGetTemplates)
e.GET("/api/templates/:id/preview", handlePreviewTemplate)
e.POST("/api/templates/preview", handlePreviewTemplate)
e.POST("/api/templates", handleCreateTemplate)
e.PUT("/api/templates/:id", handleUpdateTemplate)
e.PUT("/api/templates/:id/default", handleTemplateSetDefault)

View File

@ -27,6 +27,7 @@ type Queries struct {
CreateCampaign *sqlx.Stmt `query:"create-campaign"`
GetCampaigns *sqlx.Stmt `query:"get-campaigns"`
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
NextCampaigns *sqlx.Stmt `query:"next-campaigns"`
NextCampaignSubscribers *sqlx.Stmt `query:"next-campaign-subscribers"`

View File

@ -161,8 +161,7 @@ ORDER BY created_at DESC OFFSET $3 LIMIT $4;
SELECT campaigns.*, COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
FROM campaigns
LEFT JOIN templates ON (templates.id = campaigns.template_id)
WHERE (status='running' OR (status='scheduled' AND campaigns.send_at >= NOW()))
AND NOT(campaigns.id = ANY($1::INT[]))
WHERE campaigns.id = $1;
-- name: get-campaign-stats
SELECT id, status, to_send, sent, started_at, updated_at

View File

@ -49,7 +49,6 @@ func handleGetSubscriber(c echo.Context) error {
id, _ = strconv.Atoi(c.Param("id"))
)
// Fetch one list.
if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.")
}

View File

@ -66,26 +66,30 @@ func handlePreviewTemplate(c echo.Context) error {
var (
app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id"))
tpls []models.Template
body = c.FormValue("body")
tpls []models.Template
)
if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
}
if body == "" {
if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
}
err := app.Queries.GetTemplates.Select(&tpls, id, false)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
}
err := app.Queries.GetTemplates.Select(&tpls, id, false)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
}
if len(tpls) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Template not found.")
if len(tpls) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Template not found.")
}
body = tpls[0].Body
}
t := tpls[0]
// Compile the template.
tpl, err := runner.CompileMessageTemplate(t.Body, dummyTpl)
tpl, err := runner.CompileMessageTemplate(body, dummyTpl)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
}