diff --git a/campaigns.go b/campaigns.go
index 20d7b82..cd17ea7 100644
--- a/campaigns.go
+++ b/campaigns.go
@@ -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))
}
diff --git a/frontend/my/src/Campaign.js b/frontend/my/src/Campaign.js
index c190fda..e43ef9c 100644
--- a/frontend/my/src/Campaign.js
+++ b/frontend/my/src/Campaign.js
@@ -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 (
@@ -457,7 +463,7 @@ class Campaign extends React.PureComponent {
formDisabled={ this.state.formDisabled }
/>
-
+
@@ -471,8 +477,19 @@ class Campaign extends React.PureComponent {
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
onCancel: this.toggleMedia,
onOk: this.toggleMedia
- } } />
+ }} />
+
+ { this.state.previewRecord &&
+ {
+ this.setState({ previewRecord: null })
+ }}
+ />
+ }
)
}
diff --git a/frontend/my/src/Campaigns.js b/frontend/my/src/Campaigns.js
index 5040b40..8bd2b5c 100644
--- a/frontend/my/src/Campaigns.js
+++ b/frontend/my/src/Campaigns.js
@@ -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 (
+
+ {
+ this.handlePreview(record)
+ }}>
+
+
{
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 &&
- { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }}>
- {
- this.setState({ cloneName: e.target.value })
- }} />
- }
+ { this.state.previewRecord &&
+ {
+ this.setState({ previewRecord: null })
+ }}
+ />
+ }
)
}
diff --git a/frontend/my/src/ModalPreview.js b/frontend/my/src/ModalPreview.js
new file mode 100644
index 0000000..39051e8
--- /dev/null
+++ b/frontend/my/src/ModalPreview.js
@@ -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 (
+
+
+
+
+
+
+
+ )
+ }
+}
+
+export default ModalPreview
diff --git a/frontend/my/src/Templates.js b/frontend/my/src/Templates.js
index b236c05..b1cec48 100644
--- a/frontend/my/src/Templates.js
+++ b/frontend/my/src/Templates.js
@@ -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 (
-
+
+
-
-
- {getFieldDecorator("name", {
- initialValue: record.name,
- rules: [{ required: true }]
- })()}
-
-
- {getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
-
-
- )}
-
-
-
-
-
-
- The placeholder {'{'}{'{'} template "content" . {'}'}{'}'}
should appear in the template. Read more on templating.
-
-
-
+
+
+ {getFieldDecorator("name", {
+ initialValue: record.name,
+ rules: [{ required: true }]
+ })()}
+
+
+ {getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
+
+ )}
+
+
+
+
+
+
+
+
+
+ The placeholder {'{'}{'{'} template "content" . {'}'}{'}'}
should appear in the template. Read more on templating.
+
+
+
+
+ { this.state.previewBody &&
+
{
+ this.setState({ previewBody: null, previewName: null })
+ }}
+ />
+ }
+
)
}
}
@@ -243,17 +267,15 @@ class Templates extends React.PureComponent {
fetchRecords = { this.fetchRecords }
/>
- { this.setState({ previewRecord: null }) } }>
- { this.state.previewRecord !== null &&
- }
-
+ { this.state.previewRecord &&
+ {
+ this.setState({ previewRecord: null })
+ }}
+ />
+ }
)
}
diff --git a/frontend/my/src/constants.js b/frontend/my/src/constants.js
index 0ec313a..272aa2a 100644
--- a/frontend/my/src/constants.js
+++ b/frontend/my/src/constants.js
@@ -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",
diff --git a/frontend/my/src/index.css b/frontend/my/src/index.css
index 33a78dc..53feac9 100644
--- a/frontend/my/src/index.css
+++ b/frontend/my/src/index.css
@@ -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;
}
\ No newline at end of file
diff --git a/main.go b/main.go
index e869ec4..d720fe6 100644
--- a/main.go
+++ b/main.go
@@ -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)
diff --git a/queries.go b/queries.go
index 4b57dac..c10481b 100644
--- a/queries.go
+++ b/queries.go
@@ -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"`
diff --git a/queries.sql b/queries.sql
index 7298593..24b2615 100644
--- a/queries.sql
+++ b/queries.sql
@@ -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
diff --git a/subscribers.go b/subscribers.go
index b631cf8..4923cac 100644
--- a/subscribers.go
+++ b/subscribers.go
@@ -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.")
}
diff --git a/templates.go b/templates.go
index 8aca942..9eae4b1 100644
--- a/templates.go
+++ b/templates.go
@@ -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))
}