From a1b5a39cfbe4df003de2795840aaec1e8bbc54fa Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Fri, 26 Oct 2018 11:18:17 +0530 Subject: [PATCH] Added preview component with preview support for campaigns and templates --- campaigns.go | 23 ++++--- frontend/my/src/Campaign.js | 21 ++++++- frontend/my/src/Campaigns.js | 35 +++++++---- frontend/my/src/ModalPreview.js | 54 ++++++++++++++++ frontend/my/src/Templates.js | 106 +++++++++++++++++++------------- frontend/my/src/constants.js | 2 + frontend/my/src/index.css | 7 ++- main.go | 2 + queries.go | 1 + queries.sql | 3 +- subscribers.go | 1 - templates.go | 30 +++++---- 12 files changed, 200 insertions(+), 85 deletions(-) create mode 100644 frontend/my/src/ModalPreview.js 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)) }