Added preview component with preview support for campaigns and templates
This commit is contained in:
parent
2121c250ff
commit
a1b5a39cfb
12 changed files with 200 additions and 85 deletions
23
campaigns.go
23
campaigns.go
|
@ -89,26 +89,26 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
camps models.Campaigns
|
body = c.FormValue("body")
|
||||||
|
|
||||||
|
camp models.Campaign
|
||||||
)
|
)
|
||||||
|
|
||||||
if id < 1 {
|
if id < 1 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
|
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 != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
||||||
|
}
|
||||||
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
|
||||||
} else if len(camps) == 0 {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var sub models.Subscriber
|
||||||
camp = camps[0]
|
|
||||||
sub models.Subscriber
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get a random subscriber from the campaign.
|
// Get a random subscriber from the campaign.
|
||||||
if err := app.Queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil {
|
if err := app.Queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
|
@ -126,7 +126,10 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile the template.
|
// 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 {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { Modal, Tabs, Row, Col, Form, Switch, Select, Radio, Tag, Input, Button, Icon, Spin, DatePicker, Popconfirm, notification } from "antd"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
import Media from "./Media"
|
import Media from "./Media"
|
||||||
|
import ModalPreview from "./ModalPreview"
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import ReactQuill from "react-quill"
|
import ReactQuill from "react-quill"
|
||||||
|
@ -352,6 +353,7 @@ class Campaign extends React.PureComponent {
|
||||||
record: {},
|
record: {},
|
||||||
contentType: "richtext",
|
contentType: "richtext",
|
||||||
messengers: [],
|
messengers: [],
|
||||||
|
previewRecord: null,
|
||||||
body: "",
|
body: "",
|
||||||
currentTab: "form",
|
currentTab: "form",
|
||||||
editor: null,
|
editor: null,
|
||||||
|
@ -405,6 +407,10 @@ class Campaign extends React.PureComponent {
|
||||||
this.setState({ currentTab: tab })
|
this.setState({ currentTab: tab })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePreview = (record) => {
|
||||||
|
this.setState({ previewRecord: record })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<section className="content campaign">
|
<section className="content campaign">
|
||||||
|
@ -457,7 +463,7 @@ class Campaign extends React.PureComponent {
|
||||||
formDisabled={ this.state.formDisabled }
|
formDisabled={ this.state.formDisabled }
|
||||||
/>
|
/>
|
||||||
<div className="content-actions">
|
<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>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -471,8 +477,19 @@ class Campaign extends React.PureComponent {
|
||||||
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
|
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
|
||||||
onCancel: this.toggleMedia,
|
onCancel: this.toggleMedia,
|
||||||
onOk: this.toggleMedia
|
onOk: this.toggleMedia
|
||||||
} } />
|
}} />
|
||||||
</Modal>
|
</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>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Row, Col, Button, Table, Icon, Tooltip, Tag, Popconfirm, Progress, Moda
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
|
|
||||||
|
import ModalPreview from "./ModalPreview"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
class Campaigns extends React.PureComponent {
|
class Campaigns extends React.PureComponent {
|
||||||
|
@ -15,6 +16,7 @@ class Campaigns extends React.PureComponent {
|
||||||
queryParams: "",
|
queryParams: "",
|
||||||
stats: {},
|
stats: {},
|
||||||
record: null,
|
record: null,
|
||||||
|
previewRecord: null,
|
||||||
cloneName: "",
|
cloneName: "",
|
||||||
modalWaiting: false
|
modalWaiting: false
|
||||||
}
|
}
|
||||||
|
@ -110,10 +112,16 @@ class Campaigns extends React.PureComponent {
|
||||||
title: "",
|
title: "",
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
className: "actions",
|
className: "actions",
|
||||||
width: "10%",
|
width: "20%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="actions">
|
<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">
|
<Tooltip title="Clone campaign" placement="bottom">
|
||||||
<a role="button" onClick={() => {
|
<a role="button" onClick={() => {
|
||||||
let r = { ...record, lists: record.lists.map((i) => { return i.id }) }
|
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() {
|
render() {
|
||||||
const pagination = {
|
const pagination = {
|
||||||
...this.paginationOptions,
|
...this.paginationOptions,
|
||||||
|
@ -377,18 +389,15 @@ class Campaigns extends React.PureComponent {
|
||||||
pagination={ pagination }
|
pagination={ pagination }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ this.state.record &&
|
{ this.state.previewRecord &&
|
||||||
<Modal visible={ this.state.record } width="500px"
|
<ModalPreview
|
||||||
className="clone-campaign-modal"
|
title={ this.state.previewRecord.name }
|
||||||
title={ "Clone " + this.state.record.name}
|
previewURL={ cs.Routes.PreviewCampaign.replace(":id", this.state.previewRecord.id) }
|
||||||
okText="Clone"
|
onCancel={() => {
|
||||||
confirmLoading={ this.state.modalWaiting }
|
this.setState({ previewRecord: null })
|
||||||
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> }
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
54
frontend/my/src/ModalPreview.js
Normal file
54
frontend/my/src/ModalPreview.js
Normal 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
|
|
@ -1,13 +1,16 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Row, Col, Modal, Form, Input, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification } from "antd"
|
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 Utils from "./utils"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
class CreateFormDef extends React.PureComponent {
|
class CreateFormDef extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
confirmDirty: false,
|
confirmDirty: false,
|
||||||
modalWaiting: false
|
modalWaiting: false,
|
||||||
|
previewName: "",
|
||||||
|
previewBody: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
|
@ -50,6 +53,10 @@ class CreateFormDef extends React.PureComponent {
|
||||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handlePreview = (name, body) => {
|
||||||
|
this.setState({ previewName: name, previewBody: body })
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { formType, record, onClose } = this.props
|
const { formType, record, onClose } = this.props
|
||||||
const { getFieldDecorator } = this.props.form
|
const { getFieldDecorator } = this.props.form
|
||||||
|
@ -64,37 +71,54 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
|
<div>
|
||||||
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
|
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
|
||||||
width="90%"
|
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
|
||||||
height={ 900 }
|
width="90%"
|
||||||
confirmLoading={ this.state.modalWaiting }
|
height={ 900 }
|
||||||
onCancel={ onClose }
|
confirmLoading={ this.state.modalWaiting }
|
||||||
onOk={ this.handleSubmit }>
|
onCancel={ onClose }
|
||||||
|
onOk={ this.handleSubmit }>
|
||||||
|
|
||||||
<Spin spinning={ this.props.reqStates[cs.ModelTemplates] === cs.StatePending }>
|
<Spin spinning={ this.props.reqStates[cs.ModelTemplates] === cs.StatePending }>
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
<Form.Item {...formItemLayout} label="Name">
|
<Form.Item {...formItemLayout} label="Name">
|
||||||
{getFieldDecorator("name", {
|
{getFieldDecorator("name", {
|
||||||
initialValue: record.name,
|
initialValue: record.name,
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(<Input autoFocus maxLength="200" />)}
|
})(<Input autoFocus maxLength="200" />)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
|
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
|
||||||
{getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
|
{getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
|
||||||
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }}>
|
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />
|
||||||
</Input.TextArea>
|
)}
|
||||||
)}
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item {...formItemLayout} colon={ false } label=" ">
|
||||||
</Form>
|
<Button icon="search" onClick={ () =>
|
||||||
</Spin>
|
this.handlePreview(this.props.form.getFieldValue("name"), this.props.form.getFieldValue("body"))
|
||||||
<Row>
|
}>Preview</Button>
|
||||||
<Col span="4"></Col>
|
</Form.Item>
|
||||||
<Col span="18" className="text-grey text-small">
|
</Form>
|
||||||
The placeholder <code>{'{'}{'{'} template "content" . {'}'}{'}'}</code> should appear in the template. <a href="" target="_blank">Read more on templating</a>.
|
</Spin>
|
||||||
</Col>
|
<Row>
|
||||||
</Row>
|
<Col span="4"></Col>
|
||||||
</Modal>
|
<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 }
|
fetchRecords = { this.fetchRecords }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal visible={ this.state.previewRecord !== null } title={ this.state.previewRecord ? this.state.previewRecord.name : "" }
|
{ this.state.previewRecord &&
|
||||||
className="template-preview-modal"
|
<ModalPreview
|
||||||
width="90%"
|
title={ this.state.previewRecord.name }
|
||||||
height={ 900 }
|
previewURL={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }
|
||||||
onOk={ () => { this.setState({ previewRecord: null }) } }>
|
onCancel={() => {
|
||||||
{ this.state.previewRecord !== null &&
|
this.setState({ previewRecord: null })
|
||||||
<iframe title="Template preview"
|
}}
|
||||||
className="template-preview"
|
/>
|
||||||
src={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }>
|
}
|
||||||
</iframe> }
|
|
||||||
</Modal>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ export const Routes = {
|
||||||
|
|
||||||
GetSubscribers: "/api/subscribers",
|
GetSubscribers: "/api/subscribers",
|
||||||
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
||||||
|
PreviewCampaign: "/api/campaigns/:id/preview",
|
||||||
CreateSubscriber: "/api/subscribers",
|
CreateSubscriber: "/api/subscribers",
|
||||||
UpdateSubscriber: "/api/subscribers/:id",
|
UpdateSubscriber: "/api/subscribers/:id",
|
||||||
DeleteSubscriber: "/api/subscribers/:id",
|
DeleteSubscriber: "/api/subscribers/:id",
|
||||||
|
@ -81,6 +82,7 @@ export const Routes = {
|
||||||
|
|
||||||
GetTemplates: "/api/templates",
|
GetTemplates: "/api/templates",
|
||||||
PreviewTemplate: "/api/templates/:id/preview",
|
PreviewTemplate: "/api/templates/:id/preview",
|
||||||
|
PreviewNewTemplate: "/api/templates/preview",
|
||||||
CreateTemplate: "/api/templates",
|
CreateTemplate: "/api/templates",
|
||||||
UpdateTemplate: "/api/templates/:id",
|
UpdateTemplate: "/api/templates/:id",
|
||||||
SetDefaultTemplate: "/api/templates/:id/default",
|
SetDefaultTemplate: "/api/templates/:id/default",
|
||||||
|
|
|
@ -253,12 +253,15 @@ td.actions {
|
||||||
.templates .template-body {
|
.templates .template-body {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
.template-preview {
|
.preview-iframe-container {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.preview-iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
}
|
}
|
||||||
.template-preview-modal .ant-modal-footer button:first-child {
|
.preview-modal .ant-modal-footer button:first-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
2
main.go
2
main.go
|
@ -103,6 +103,7 @@ func registerHandlers(e *echo.Echo) {
|
||||||
e.GET("/api/campaigns/messengers", handleGetCampaignMessengers)
|
e.GET("/api/campaigns/messengers", handleGetCampaignMessengers)
|
||||||
e.GET("/api/campaigns/:id", handleGetCampaigns)
|
e.GET("/api/campaigns/:id", handleGetCampaigns)
|
||||||
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
|
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
|
||||||
|
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
|
||||||
e.POST("/api/campaigns", handleCreateCampaign)
|
e.POST("/api/campaigns", handleCreateCampaign)
|
||||||
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
||||||
e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
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", handleGetTemplates)
|
||||||
e.GET("/api/templates/:id", handleGetTemplates)
|
e.GET("/api/templates/:id", handleGetTemplates)
|
||||||
e.GET("/api/templates/:id/preview", handlePreviewTemplate)
|
e.GET("/api/templates/:id/preview", handlePreviewTemplate)
|
||||||
|
e.POST("/api/templates/preview", handlePreviewTemplate)
|
||||||
e.POST("/api/templates", handleCreateTemplate)
|
e.POST("/api/templates", handleCreateTemplate)
|
||||||
e.PUT("/api/templates/:id", handleUpdateTemplate)
|
e.PUT("/api/templates/:id", handleUpdateTemplate)
|
||||||
e.PUT("/api/templates/:id/default", handleTemplateSetDefault)
|
e.PUT("/api/templates/:id/default", handleTemplateSetDefault)
|
||||||
|
|
|
@ -27,6 +27,7 @@ type Queries struct {
|
||||||
|
|
||||||
CreateCampaign *sqlx.Stmt `query:"create-campaign"`
|
CreateCampaign *sqlx.Stmt `query:"create-campaign"`
|
||||||
GetCampaigns *sqlx.Stmt `query:"get-campaigns"`
|
GetCampaigns *sqlx.Stmt `query:"get-campaigns"`
|
||||||
|
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
||||||
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
||||||
NextCampaigns *sqlx.Stmt `query:"next-campaigns"`
|
NextCampaigns *sqlx.Stmt `query:"next-campaigns"`
|
||||||
NextCampaignSubscribers *sqlx.Stmt `query:"next-campaign-subscribers"`
|
NextCampaignSubscribers *sqlx.Stmt `query:"next-campaign-subscribers"`
|
||||||
|
|
|
@ -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
|
SELECT campaigns.*, COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body
|
||||||
FROM campaigns
|
FROM campaigns
|
||||||
LEFT JOIN templates ON (templates.id = campaigns.template_id)
|
LEFT JOIN templates ON (templates.id = campaigns.template_id)
|
||||||
WHERE (status='running' OR (status='scheduled' AND campaigns.send_at >= NOW()))
|
WHERE campaigns.id = $1;
|
||||||
AND NOT(campaigns.id = ANY($1::INT[]))
|
|
||||||
|
|
||||||
-- name: get-campaign-stats
|
-- name: get-campaign-stats
|
||||||
SELECT id, status, to_send, sent, started_at, updated_at
|
SELECT id, status, to_send, sent, started_at, updated_at
|
||||||
|
|
|
@ -49,7 +49,6 @@ func handleGetSubscriber(c echo.Context) error {
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch one list.
|
|
||||||
if id < 1 {
|
if id < 1 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.")
|
||||||
}
|
}
|
||||||
|
|
30
templates.go
30
templates.go
|
@ -66,26 +66,30 @@ func handlePreviewTemplate(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
app = c.Get("app").(*App)
|
app = c.Get("app").(*App)
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
tpls []models.Template
|
body = c.FormValue("body")
|
||||||
|
|
||||||
|
tpls []models.Template
|
||||||
)
|
)
|
||||||
|
|
||||||
if id < 1 {
|
if body == "" {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
|
if id < 1 {
|
||||||
}
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
|
||||||
|
}
|
||||||
|
|
||||||
err := app.Queries.GetTemplates.Select(&tpls, id, false)
|
err := app.Queries.GetTemplates.Select(&tpls, id, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tpls) == 0 {
|
if len(tpls) == 0 {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Template not found.")
|
return echo.NewHTTPError(http.StatusBadRequest, "Template not found.")
|
||||||
|
}
|
||||||
|
body = tpls[0].Body
|
||||||
}
|
}
|
||||||
t := tpls[0]
|
|
||||||
|
|
||||||
// Compile the template.
|
// Compile the template.
|
||||||
tpl, err := runner.CompileMessageTemplate(t.Body, dummyTpl)
|
tpl, err := runner.CompileMessageTemplate(body, dummyTpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue