- Campaigns now have a `type` property (regular, opt-in) - Opt-in campaigns work for double opt-in lists and e-mail subscribers who haven't confirmed their subscriptions. - Lists UI shows a 'Send opt-in campaign' optin that automatically creates an opt-in campaign for the list with a default message body that can be tweaked before sending the campaign. - Primary usecase is to send opt-in campaigns to subscribers who are added via bulk import. This is a breaking change. Adds a new Postgres enum type `campaign_type` and a new column `type` to the campaigns table.
873 lines
26 KiB
JavaScript
873 lines
26 KiB
JavaScript
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 parseUrl from "querystring"
|
|
import ReactQuill from "react-quill"
|
|
import Delta from "quill-delta"
|
|
import "react-quill/dist/quill.snow.css"
|
|
|
|
const formItemLayout = {
|
|
labelCol: { xs: { span: 16 }, sm: { span: 10 }, md: { span: 4 } },
|
|
wrapperCol: { xs: { span: 16 }, sm: { span: 14 }, md: { span: 10 } }
|
|
}
|
|
|
|
class Editor extends React.PureComponent {
|
|
state = {
|
|
editor: null,
|
|
quill: null,
|
|
rawInput: null,
|
|
selContentType: cs.CampaignContentTypeRichtext,
|
|
contentType: cs.CampaignContentTypeRichtext,
|
|
body: ""
|
|
}
|
|
|
|
quillModules = {
|
|
toolbar: {
|
|
container: [
|
|
[{ header: [1, 2, 3, false] }],
|
|
["bold", "italic", "underline", "strike", "blockquote", "code"],
|
|
[{ color: [] }, { background: [] }, { size: [] }],
|
|
[
|
|
{ list: "ordered" },
|
|
{ list: "bullet" },
|
|
{ indent: "-1" },
|
|
{ indent: "+1" }
|
|
],
|
|
[
|
|
{ align: "" },
|
|
{ align: "center" },
|
|
{ align: "right" },
|
|
{ align: "justify" }
|
|
],
|
|
["link", "image"],
|
|
["clean", "font"]
|
|
],
|
|
handlers: {
|
|
image: () => {
|
|
this.props.toggleMedia()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
componentDidMount = () => {
|
|
// The editor component will only load once the individual campaign metadata
|
|
// has loaded, i.e., record.body is guaranteed to be available here.
|
|
if (this.props.record && this.props.record.id) {
|
|
this.setState({
|
|
body: this.props.record.body,
|
|
contentType: this.props.record.content_type,
|
|
selContentType: this.props.record.content_type
|
|
})
|
|
}
|
|
}
|
|
|
|
// Custom handler for inserting images from the media popup.
|
|
insertMedia = uri => {
|
|
const quill = this.state.quill.getEditor()
|
|
let range = quill.getSelection(true)
|
|
quill.updateContents(
|
|
new Delta()
|
|
.retain(range.index)
|
|
.delete(range.length)
|
|
.insert({ image: this.props.config.rootURL + uri }),
|
|
null
|
|
)
|
|
}
|
|
|
|
handleSelContentType = (_, e) => {
|
|
this.setState({ selContentType: e.props.value })
|
|
}
|
|
|
|
handleSwitchContentType = () => {
|
|
this.setState({ contentType: this.state.selContentType })
|
|
if (!this.state.quill || !this.state.quill.editor || !this.state.rawInput) {
|
|
return
|
|
}
|
|
|
|
// Switching from richtext to html.
|
|
let body = ""
|
|
if (this.state.selContentType === cs.CampaignContentTypeHTML) {
|
|
body = this.state.quill.editor.container.firstChild.innerHTML
|
|
// eslint-disable-next-line
|
|
this.state.rawInput.value = body
|
|
} else if (this.state.selContentType === cs.CampaignContentTypeRichtext) {
|
|
body = this.state.rawInput.value
|
|
this.state.quill.editor.clipboard.dangerouslyPasteHTML(body, "raw")
|
|
}
|
|
|
|
this.props.setContent(this.state.selContentType, body)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div>
|
|
<header className="header">
|
|
{!this.props.formDisabled && (
|
|
<Row>
|
|
<Col span={20}>
|
|
<div className="content-type">
|
|
<p>Content format</p>
|
|
<Select
|
|
name="content_type"
|
|
onChange={this.handleSelContentType}
|
|
style={{ minWidth: 200 }}
|
|
value={this.state.selContentType}
|
|
>
|
|
<Select.Option value={ cs.CampaignContentTypeRichtext }>Rich Text</Select.Option>
|
|
<Select.Option value={ cs.CampaignContentTypeHTML }>Raw HTML</Select.Option>
|
|
</Select>
|
|
{this.state.contentType !== this.state.selContentType && (
|
|
<div className="actions">
|
|
<Popconfirm
|
|
title="The content may lose its formatting. Are you sure?"
|
|
onConfirm={this.handleSwitchContentType}
|
|
>
|
|
<Button>
|
|
<Icon type="save" /> Switch format
|
|
</Button>
|
|
</Popconfirm>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Col>
|
|
<Col span={4} />
|
|
</Row>
|
|
)}
|
|
</header>
|
|
<ReactQuill
|
|
readOnly={this.props.formDisabled}
|
|
style={{
|
|
display: this.state.contentType === cs.CampaignContentTypeRichtext ? "block" : "none"
|
|
}}
|
|
modules={this.quillModules}
|
|
defaultValue={this.props.record.body}
|
|
ref={o => {
|
|
if (!o) {
|
|
return
|
|
}
|
|
|
|
this.setState({ quill: o })
|
|
document.querySelector(".ql-editor").focus()
|
|
}}
|
|
onChange={() => {
|
|
if (!this.state.quill) {
|
|
return
|
|
}
|
|
|
|
this.props.setContent(
|
|
this.state.contentType,
|
|
this.state.quill.editor.root.innerHTML
|
|
)
|
|
}}
|
|
/>
|
|
|
|
<Input.TextArea
|
|
readOnly={this.props.formDisabled}
|
|
placeholder="Your message here"
|
|
style={{
|
|
display: this.state.contentType === "html" ? "block" : "none"
|
|
}}
|
|
id="html-body"
|
|
rows={10}
|
|
autosize={{ minRows: 2, maxRows: 10 }}
|
|
defaultValue={this.props.record.body}
|
|
ref={o => {
|
|
if (!o) {
|
|
return
|
|
}
|
|
|
|
this.setState({ rawInput: o.textAreaRef })
|
|
}}
|
|
onChange={e => {
|
|
this.props.setContent(this.state.contentType, e.target.value)
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
class TheFormDef extends React.PureComponent {
|
|
state = {
|
|
editorVisible: false,
|
|
sendLater: false,
|
|
loading: false
|
|
}
|
|
|
|
componentWillReceiveProps(nextProps) {
|
|
// On initial load, toggle the send_later switch if the record
|
|
// has a "send_at" date.
|
|
if (nextProps.record.send_at === this.props.record.send_at) {
|
|
return
|
|
}
|
|
this.setState({
|
|
sendLater: nextProps.isSingle && nextProps.record.send_at !== null
|
|
})
|
|
}
|
|
|
|
validateEmail = (rule, value, callback) => {
|
|
if (!value.match(/(.+?)\s<(.+?)@(.+?)>/)) {
|
|
return callback("Format should be: Your Name <email@address.com>")
|
|
}
|
|
|
|
callback()
|
|
}
|
|
|
|
handleSendLater = e => {
|
|
this.setState({ sendLater: e })
|
|
}
|
|
|
|
// Handle create / edit form submission.
|
|
handleSubmit = cb => {
|
|
if (this.state.loading) {
|
|
return
|
|
}
|
|
|
|
if (!cb) {
|
|
// Set a fake callback.
|
|
cb = () => {}
|
|
}
|
|
|
|
this.props.form.validateFields((err, values) => {
|
|
if (err) {
|
|
return
|
|
}
|
|
|
|
if (!values.tags) {
|
|
values.tags = []
|
|
}
|
|
|
|
values.type = cs.CampaignTypeRegular
|
|
values.body = this.props.body
|
|
values.content_type = this.props.contentType
|
|
|
|
if (values.send_at) {
|
|
values.send_later = true
|
|
} else {
|
|
values.send_later = false
|
|
}
|
|
|
|
// Create a new campaign.
|
|
this.setState({ loading: true })
|
|
if (!this.props.isSingle) {
|
|
this.props
|
|
.modelRequest(
|
|
cs.ModelCampaigns,
|
|
cs.Routes.CreateCampaign,
|
|
cs.MethodPost,
|
|
values
|
|
)
|
|
.then(resp => {
|
|
notification["success"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Campaign created",
|
|
description: `"${values["name"]}" created`
|
|
})
|
|
|
|
this.props.route.history.push({
|
|
pathname: cs.Routes.ViewCampaign.replace(
|
|
":id",
|
|
resp.data.data.id
|
|
),
|
|
hash: "content-tab"
|
|
})
|
|
cb(true)
|
|
})
|
|
.catch(e => {
|
|
notification["error"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Error",
|
|
description: e.message
|
|
})
|
|
this.setState({ loading: false })
|
|
cb(false)
|
|
})
|
|
} else {
|
|
this.props
|
|
.modelRequest(
|
|
cs.ModelCampaigns,
|
|
cs.Routes.UpdateCampaign,
|
|
cs.MethodPut,
|
|
{
|
|
...values,
|
|
id: this.props.record.id
|
|
}
|
|
)
|
|
.then(resp => {
|
|
notification["success"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Campaign updated",
|
|
description: `"${values["name"]}" updated`
|
|
})
|
|
this.setState({ loading: false })
|
|
cb(true)
|
|
})
|
|
.catch(e => {
|
|
notification["error"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Error",
|
|
description: e.message
|
|
})
|
|
this.setState({ loading: false })
|
|
cb(false)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
handleTestCampaign = e => {
|
|
e.preventDefault()
|
|
this.props.form.validateFields((err, values) => {
|
|
if (err) {
|
|
return
|
|
}
|
|
|
|
if (!values.tags) {
|
|
values.tags = []
|
|
}
|
|
|
|
values.id = this.props.record.id
|
|
values.body = this.props.body
|
|
values.content_type = this.props.contentType
|
|
|
|
this.setState({ loading: true })
|
|
this.props
|
|
.request(cs.Routes.TestCampaign, cs.MethodPost, values)
|
|
.then(resp => {
|
|
this.setState({ loading: false })
|
|
notification["success"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Test sent",
|
|
description: `Test messages sent`
|
|
})
|
|
})
|
|
.catch(e => {
|
|
this.setState({ loading: false })
|
|
notification["error"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Error",
|
|
description: e.message
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
render() {
|
|
const { record } = this.props
|
|
const { getFieldDecorator } = this.props.form
|
|
|
|
let subLists = []
|
|
if (this.props.isSingle && record.lists) {
|
|
subLists = record.lists
|
|
.map(v => {
|
|
// Exclude deleted lists.
|
|
return v.id !== 0 ? v.id : null
|
|
})
|
|
.filter(v => v !== null)
|
|
} else if (this.props.route.location.search) {
|
|
// One or more list_id in the query params.
|
|
const p = parseUrl.parse(this.props.route.location.search.substring(1))
|
|
if (p.hasOwnProperty("list_id")) {
|
|
if(Array.isArray(p.list_id)) {
|
|
p.list_id.forEach(i => {
|
|
// eslint-disable-next-line radix
|
|
const id = parseInt(i)
|
|
if (id) {
|
|
subLists.push(id)
|
|
}
|
|
});
|
|
} else {
|
|
// eslint-disable-next-line radix
|
|
const id = parseInt(p.list_id)
|
|
if (id) {
|
|
subLists.push(id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.record) {
|
|
this.props.pageTitle(record.name + " / Campaigns")
|
|
} else {
|
|
this.props.pageTitle("New campaign")
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Spin spinning={this.state.loading}>
|
|
<Form onSubmit={this.handleSubmit}>
|
|
<Form.Item {...formItemLayout} label="Campaign name">
|
|
{getFieldDecorator("name", {
|
|
extra:
|
|
"This is internal and will not be visible to subscribers",
|
|
initialValue: record.name,
|
|
rules: [{ required: true }]
|
|
})(
|
|
<Input
|
|
disabled={this.props.formDisabled}
|
|
autoFocus
|
|
maxLength={200}
|
|
/>
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item {...formItemLayout} label="Subject">
|
|
{getFieldDecorator("subject", {
|
|
initialValue: record.subject,
|
|
rules: [{ required: true }]
|
|
})(<Input disabled={this.props.formDisabled} maxLength={500} />)}
|
|
</Form.Item>
|
|
<Form.Item {...formItemLayout} label="From address">
|
|
{getFieldDecorator("from_email", {
|
|
initialValue: record.from_email
|
|
? record.from_email
|
|
: this.props.config.fromEmail,
|
|
rules: [{ required: true }, { validator: this.validateEmail }]
|
|
})(
|
|
<Input
|
|
disabled={this.props.formDisabled}
|
|
placeholder="Company Name <email@company.com>"
|
|
maxLength={200}
|
|
/>
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item
|
|
{...formItemLayout}
|
|
label="Lists"
|
|
extra="Lists to subscribe to"
|
|
>
|
|
{getFieldDecorator("lists", {
|
|
initialValue:
|
|
subLists.length > 0
|
|
? subLists
|
|
: this.props.data[cs.ModelLists].hasOwnProperty(
|
|
"results"
|
|
) && this.props.data[cs.ModelLists].results.length === 1
|
|
? [this.props.data[cs.ModelLists].results[0].id]
|
|
: undefined,
|
|
rules: [{ required: true }]
|
|
})(
|
|
<Select disabled={this.props.formDisabled} mode="multiple">
|
|
{this.props.data[cs.ModelLists].hasOwnProperty("results") &&
|
|
[...this.props.data[cs.ModelLists].results].map((v) =>
|
|
(record.type !== cs.CampaignTypeOptin || v.optin === cs.ListOptinDouble) && (
|
|
<Select.Option value={v["id"]} key={v["id"]}>
|
|
{v["name"]}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item {...formItemLayout} label="Template" extra="Template">
|
|
{getFieldDecorator("template_id", {
|
|
initialValue: record.template_id
|
|
? record.template_id
|
|
: this.props.data[cs.ModelTemplates].length > 0
|
|
? this.props.data[cs.ModelTemplates].filter(
|
|
t => t.is_default
|
|
)[0].id
|
|
: undefined,
|
|
rules: [{ required: true }]
|
|
})(
|
|
<Select disabled={this.props.formDisabled}>
|
|
{this.props.data[cs.ModelTemplates].map((v, i) => (
|
|
<Select.Option value={v["id"]} key={v["id"]}>
|
|
{v["name"]}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item
|
|
{...formItemLayout}
|
|
label="Tags"
|
|
extra="Hit Enter after typing a word to add multiple tags"
|
|
>
|
|
{getFieldDecorator("tags", { initialValue: record.tags })(
|
|
<Select disabled={this.props.formDisabled} mode="tags" />
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item
|
|
{...formItemLayout}
|
|
label="Messenger"
|
|
style={{
|
|
display:
|
|
this.props.config.messengers.length === 1 ? "none" : "block"
|
|
}}
|
|
>
|
|
{getFieldDecorator("messenger", {
|
|
initialValue: record.messenger ? record.messenger : "email"
|
|
})(
|
|
<Radio.Group className="messengers">
|
|
{[...this.props.config.messengers].map((v, i) => (
|
|
<Radio disabled={this.props.formDisabled} value={v} key={v}>
|
|
{v}
|
|
</Radio>
|
|
))}
|
|
</Radio.Group>
|
|
)}
|
|
</Form.Item>
|
|
|
|
<hr />
|
|
<Form.Item {...formItemLayout} label="Send later?">
|
|
<Row>
|
|
<Col span={2}>
|
|
{getFieldDecorator("send_later")(
|
|
<Switch
|
|
disabled={this.props.formDisabled}
|
|
checked={this.state.sendLater}
|
|
onChange={this.handleSendLater}
|
|
/>
|
|
)}
|
|
</Col>
|
|
<Col xs={24} sm={2}>
|
|
{this.state.sendLater &&
|
|
getFieldDecorator("send_at", {
|
|
initialValue:
|
|
record && typeof record.send_at === "string"
|
|
? moment(record.send_at)
|
|
: moment(new Date())
|
|
.add(1, "days")
|
|
.startOf("day")
|
|
})(
|
|
<DatePicker
|
|
disabled={this.props.formDisabled}
|
|
showTime
|
|
format="YYYY-MM-DD HH:mm:ss"
|
|
placeholder="Select a date and time"
|
|
/>
|
|
)}
|
|
</Col>
|
|
</Row>
|
|
</Form.Item>
|
|
|
|
{this.props.isSingle && (
|
|
<div>
|
|
<hr />
|
|
<Form.Item
|
|
{...formItemLayout}
|
|
label="Send test messages"
|
|
extra="Hit Enter after typing an address to add multiple recipients. The addresses must belong to existing subscribers."
|
|
>
|
|
{getFieldDecorator("subscribers")(
|
|
<Select mode="tags" style={{ width: "100%" }} />
|
|
)}
|
|
</Form.Item>
|
|
<Form.Item {...formItemLayout} label=" " colon={false}>
|
|
<Button onClick={this.handleTestCampaign}>
|
|
<Icon type="mail" /> Send test
|
|
</Button>
|
|
</Form.Item>
|
|
</div>
|
|
)}
|
|
</Form>
|
|
</Spin>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
const TheForm = Form.create()(TheFormDef)
|
|
|
|
class Campaign extends React.PureComponent {
|
|
state = {
|
|
campaignID: this.props.route.match.params
|
|
? parseInt(this.props.route.match.params.campaignID, 10)
|
|
: 0,
|
|
record: {},
|
|
formRef: null,
|
|
contentType: cs.CampaignContentTypeRichtext,
|
|
previewRecord: null,
|
|
body: "",
|
|
currentTab: "form",
|
|
editor: null,
|
|
loading: true,
|
|
mediaVisible: false,
|
|
formDisabled: false
|
|
}
|
|
|
|
componentDidMount = () => {
|
|
// Fetch lists.
|
|
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
|
|
per_page: "all"
|
|
})
|
|
|
|
// Fetch templates.
|
|
this.props.modelRequest(
|
|
cs.ModelTemplates,
|
|
cs.Routes.GetTemplates,
|
|
cs.MethodGet
|
|
)
|
|
|
|
// Fetch campaign.
|
|
if (this.state.campaignID) {
|
|
this.fetchRecord(this.state.campaignID)
|
|
} else {
|
|
this.setState({ loading: false })
|
|
}
|
|
|
|
// Content tab?
|
|
if (document.location.hash === "#content-tab") {
|
|
this.setCurrentTab("content")
|
|
}
|
|
}
|
|
|
|
fetchRecord = id => {
|
|
this.props
|
|
.request(cs.Routes.GetCampaign, cs.MethodGet, { id: id })
|
|
.then(r => {
|
|
const record = r.data.data
|
|
this.setState({ record: record, loading: false })
|
|
|
|
// The form for non draft and scheduled campaigns should be locked.
|
|
if (
|
|
record.status !== cs.CampaignStatusDraft &&
|
|
record.status !== cs.CampaignStatusScheduled
|
|
) {
|
|
this.setState({ formDisabled: true })
|
|
}
|
|
})
|
|
.catch(e => {
|
|
notification["error"]({
|
|
placement: cs.MsgPosition,
|
|
message: "Error",
|
|
description: e.message
|
|
})
|
|
})
|
|
}
|
|
|
|
setContent = (contentType, body) => {
|
|
this.setState({ contentType: contentType, body: body })
|
|
}
|
|
|
|
toggleMedia = () => {
|
|
this.setState({ mediaVisible: !this.state.mediaVisible })
|
|
}
|
|
|
|
setCurrentTab = tab => {
|
|
this.setState({ currentTab: tab })
|
|
}
|
|
|
|
handlePreview = record => {
|
|
this.setState({ previewRecord: record })
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<section className="content campaign">
|
|
<Row>
|
|
<Col xs={24} sm={16}>
|
|
{!this.state.record.id && <h1>Create a campaign</h1>}
|
|
{this.state.record.id && (
|
|
<div>
|
|
<h1>
|
|
<Tag
|
|
color={cs.CampaignStatusColors[this.state.record.status]}
|
|
>
|
|
{this.state.record.status}
|
|
</Tag>
|
|
{this.state.record.type === cs.CampaignStatusOptin && (
|
|
<Tag className="campaign-type" color="geekblue">
|
|
{this.state.record.type}
|
|
</Tag>
|
|
)}
|
|
{this.state.record.name}
|
|
</h1>
|
|
<span className="text-tiny text-grey">
|
|
ID {this.state.record.id} — UUID{" "}
|
|
{this.state.record.uuid}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</Col>
|
|
<Col xs={24} sm={8} className="right header-action-break">
|
|
{!this.state.formDisabled && !this.state.loading && (
|
|
<div>
|
|
<Button
|
|
type="primary"
|
|
icon="save"
|
|
onClick={() => {
|
|
this.state.formRef.handleSubmit()
|
|
}}
|
|
>
|
|
{!this.state.record.id ? "Continue" : "Save changes"}
|
|
</Button>{" "}
|
|
{this.state.record.status === cs.CampaignStatusDraft &&
|
|
this.state.record.send_at && (
|
|
<Popconfirm
|
|
title="The campaign will start automatically at the scheduled date and time. Schedule now?"
|
|
onConfirm={() => {
|
|
this.state.formRef.handleSubmit(() => {
|
|
this.props.route.history.push({
|
|
pathname: cs.Routes.ViewCampaigns,
|
|
state: {
|
|
campaign: this.state.record,
|
|
campaignStatus: cs.CampaignStatusScheduled
|
|
}
|
|
})
|
|
})
|
|
}}
|
|
>
|
|
<Button icon="clock-circle" type="primary">
|
|
Schedule campaign
|
|
</Button>
|
|
</Popconfirm>
|
|
)}
|
|
{this.state.record.status === cs.CampaignStatusDraft &&
|
|
!this.state.record.send_at && (
|
|
<Popconfirm
|
|
title="Campaign properties cannot be changed once it starts. Save changes and start now?"
|
|
onConfirm={() => {
|
|
this.state.formRef.handleSubmit(() => {
|
|
this.props.route.history.push({
|
|
pathname: cs.Routes.ViewCampaigns,
|
|
state: {
|
|
campaign: this.state.record,
|
|
campaignStatus: cs.CampaignStatusRunning
|
|
}
|
|
})
|
|
})
|
|
}}
|
|
>
|
|
<Button icon="rocket" type="primary">
|
|
Start campaign
|
|
</Button>
|
|
</Popconfirm>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Col>
|
|
</Row>
|
|
<br />
|
|
|
|
<Tabs
|
|
type="card"
|
|
activeKey={this.state.currentTab}
|
|
onTabClick={t => {
|
|
this.setState({ currentTab: t })
|
|
}}
|
|
>
|
|
<Tabs.TabPane tab="Campaign" key="form">
|
|
<Spin spinning={this.state.loading}>
|
|
<TheForm
|
|
{...this.props}
|
|
wrappedComponentRef={r => {
|
|
if (!r) {
|
|
return
|
|
}
|
|
// Take the editor's reference and save it in the state
|
|
// so that it's insertMedia() function can be passed to <Media />
|
|
this.setState({ formRef: r })
|
|
}}
|
|
record={this.state.record}
|
|
isSingle={this.state.record.id ? true : false}
|
|
body={
|
|
this.state.body ? this.state.body : this.state.record.body
|
|
}
|
|
contentType={this.state.contentType}
|
|
formDisabled={this.state.formDisabled}
|
|
fetchRecord={this.fetchRecord}
|
|
setCurrentTab={this.setCurrentTab}
|
|
/>
|
|
</Spin>
|
|
</Tabs.TabPane>
|
|
<Tabs.TabPane
|
|
tab="Content"
|
|
disabled={this.state.record.id ? false : true}
|
|
key="content"
|
|
>
|
|
{this.state.record.id && (
|
|
<div>
|
|
<Editor
|
|
{...this.props}
|
|
ref={r => {
|
|
if (!r) {
|
|
return
|
|
}
|
|
// Take the editor's reference and save it in the state
|
|
// so that it's insertMedia() function can be passed to <Media />
|
|
this.setState({ editor: r })
|
|
}}
|
|
isSingle={this.state.record.id ? true : false}
|
|
record={this.state.record}
|
|
visible={this.state.editorVisible}
|
|
toggleMedia={this.toggleMedia}
|
|
setContent={this.setContent}
|
|
formDisabled={this.state.formDisabled}
|
|
/>
|
|
<div className="content-actions">
|
|
<p>
|
|
<Button
|
|
icon="search"
|
|
onClick={() => this.handlePreview(this.state.record)}
|
|
>
|
|
Preview
|
|
</Button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{!this.state.record.id && <Spin className="empty-spinner" />}
|
|
</Tabs.TabPane>
|
|
</Tabs>
|
|
|
|
<Modal
|
|
visible={this.state.mediaVisible}
|
|
width="900px"
|
|
title="Media"
|
|
okText={"Ok"}
|
|
onCancel={this.toggleMedia}
|
|
onOk={this.toggleMedia}
|
|
>
|
|
<Media
|
|
{...{
|
|
...this.props,
|
|
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>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default Campaign
|