2018-10-25 15:51:47 +02:00
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"
2018-10-26 07:48:17 +02:00
import ModalPreview from "./ModalPreview"
2018-10-25 15:51:47 +02:00
import moment from 'moment'
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 : 4 } } ,
wrapperCol : { xs : { span : 16 } , sm : { span : 10 } }
}
const formItemTailLayout = {
wrapperCol : { xs : { span : 24 , offset : 0 } , sm : { span : 10 , offset : 4 } }
}
class Editor extends React . PureComponent {
state = {
editor : null ,
quill : null ,
rawInput : null ,
selContentType : "richtext" ,
contentType : "richtext" ,
2018-10-29 10:50:49 +01:00
body : ""
2018-10-25 15:51:47 +02:00
}
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" , "gallery" ] ,
[ "clean" , "font" ]
] ,
handlers : {
"gallery" : ( ) => {
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 : 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 === "html" ) {
body = this . state . quill . editor . container . firstChild . innerHTML
this . state . rawInput . value = body
} else if ( this . state . selContentType === "richtext" ) {
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 = "richtext" > Rich Text < / S e l e c t . O p t i o n >
< Select . Option value = "html" > Raw HTML < / S e l e c t . O p t i o n >
< / S e l e c t >
{ 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
< / B u t t o n >
< / P o p c o n f i r m >
< / d i v > }
< / d i v >
< / C o l >
< Col span = { 4 } > < / C o l >
< / R o w >
}
< / h e a d e r >
< ReactQuill
readOnly = { this . props . formDisabled }
style = { { display : this . state . contentType === "richtext" ? "block" : "none" } }
modules = { this . quillModules }
defaultValue = { this . props . record . body }
ref = { ( o ) => {
2018-10-29 10:50:49 +01:00
if ( o ) {
this . setState ( { quill : o } )
}
2018-10-25 15:51:47 +02:00
} }
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 )
} }
/ >
< / d i v >
)
}
}
class TheFormDef extends React . PureComponent {
state = {
editorVisible : false ,
2018-10-29 10:50:49 +01:00
sendLater : false ,
loading : false
2018-10-25 15:51:47 +02:00
}
componentWillReceiveProps ( nextProps ) {
const has = nextProps . isSingle && nextProps . record . send _at !== null
if ( ! has ) {
return
}
if ( this . state . sendLater !== has ) {
this . setState ( { sendLater : has } )
}
}
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 = ( e ) => {
e . preventDefault ( )
this . props . form . validateFields ( ( err , values ) => {
if ( err ) {
return
}
2018-10-29 10:50:49 +01:00
2018-10-25 15:51:47 +02:00
if ( ! values . tags ) {
values . tags = [ ]
}
2018-10-29 10:50:49 +01:00
2018-10-25 15:51:47 +02:00
values . body = this . props . body
values . content _type = this . props . contentType
2018-10-29 10:50:49 +01:00
2018-10-25 15:51:47 +02:00
// Create a new campaign.
2018-10-29 10:50:49 +01:00
this . setState ( { loading : true } )
2018-10-25 15:51:47 +02:00
if ( ! this . props . isSingle ) {
this . props . modelRequest ( cs . ModelCampaigns , cs . Routes . CreateCampaign , cs . MethodPost , values ) . then ( ( resp ) => {
notification [ "success" ] ( { placement : "topRight" ,
message : "Campaign created" ,
description : ` " ${ values [ "name" ] } " created ` } )
this . props . route . history . push ( cs . Routes . ViewCampaign . replace ( ":id" , resp . data . data . id ) )
this . props . fetchRecord ( resp . data . data . id )
this . props . setCurrentTab ( "content" )
2018-10-29 10:50:49 +01:00
this . setState ( { loading : false } )
2018-10-25 15:51:47 +02:00
} ) . catch ( e => {
notification [ "error" ] ( { message : "Error" , description : e . message } )
2018-10-30 06:48:15 +01:00
this . setState ( { loading : false } )
2018-10-25 15:51:47 +02:00
} )
} else {
this . props . modelRequest ( cs . ModelCampaigns , cs . Routes . UpdateCampaign , cs . MethodPut , { ... values , id : this . props . record . id } ) . then ( ( resp ) => {
notification [ "success" ] ( { placement : "topRight" ,
2018-10-29 10:50:49 +01:00
message : "Campaign updated" ,
description : ` " ${ values [ "name" ] } " updated ` } )
2018-10-30 06:48:15 +01:00
this . setState ( { loading : false } )
2018-10-25 15:51:47 +02:00
} ) . catch ( e => {
notification [ "error" ] ( { message : "Error" , description : e . message } )
2018-10-29 10:50:49 +01:00
this . setState ( { loading : false } )
2018-10-25 15:51:47 +02:00
} )
}
} )
}
2018-10-29 10:50:49 +01:00
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 : "topRight" ,
message : "Test sent" ,
description : ` Test messages sent ` } )
} ) . catch ( e => {
this . setState ( { loading : false } )
notification [ "error" ] ( { message : "Error" , description : e . message } )
} )
} )
}
2018-10-25 15:51:47 +02:00
render ( ) {
const { record } = this . props ;
const { getFieldDecorator } = this . props . form
let subLists = [ ]
if ( this . props . isSingle && record . lists ) {
subLists = record . lists . map ( ( v ) => { return v . id !== 0 ? v . id : null } ) . filter ( v => v !== null )
}
return (
< div >
2018-10-29 10:50:49 +01:00
< 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" / > ) }
< / F o r m . I t e m >
< Form . Item { ... formItemLayout } label = "Subject" >
{ getFieldDecorator ( "subject" , {
initialValue : record . subject ,
rules : [ { required : true } ]
} ) ( < Input disabled = { this . props . formDisabled } maxLength = "500" / > ) }
< / F o r m . I t e m >
< Form . Item { ... formItemLayout } label = "From address" >
{ getFieldDecorator ( "from_email" , {
2018-11-02 19:03:00 +01:00
initialValue : record . from _email ? record . from _email : this . props . config . fromEmail ,
2018-10-29 10:50:49 +01:00
rules : [ { required : true } , { validator : this . validateEmail } ]
} ) ( < Input disabled = { this . props . formDisabled } placeholder = "Company Name <email@company.com>" maxLength = "200" / > ) }
< / F o r m . I t e m >
< Form . Item { ... formItemLayout } label = "Lists" extra = "Lists to subscribe to" >
{ getFieldDecorator ( "lists" , { initialValue : subLists , rules : [ { required : true } ] } ) (
< Select disabled = { this . props . formDisabled } mode = "multiple" >
{ [ ... this . props . data [ cs . ModelLists ] ] . map ( ( v , i ) =>
< Select . Option value = { v [ "id" ] } key = { v [ "id" ] } > { v [ "name" ] } < / S e l e c t . O p t i o n >
) }
< / S e l e c t >
2018-10-25 15:51:47 +02:00
) }
2018-10-29 10:50:49 +01:00
< / F o r m . I t e m >
< Form . Item { ... formItemLayout } label = "Template" extra = "Template" >
{ getFieldDecorator ( "template_id" , { initialValue : record . template _id , 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" ] } < / S e l e c t . O p t i o n >
) }
< / S e l e c t >
) }
< / F o r m . I t e m >
< 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" > < / S e l e c t >
2018-10-25 15:51:47 +02:00
) }
< / F o r m . I t e m >
2018-11-02 19:03:00 +01:00
< Form . Item { ... formItemLayout } label = "Messenger" style = { { display : this . props . config . messengers . length === 1 ? "none" : "block" } } >
2018-10-29 10:50:49 +01:00
{ getFieldDecorator ( "messenger" , { initialValue : record . messenger ? record . messenger : "email" } ) (
< Radio . Group className = "messengers" >
2018-11-02 19:03:00 +01:00
{ [ ... this . props . config . messengers ] . map ( ( v , i ) =>
2018-10-29 10:50:49 +01:00
< Radio disabled = { this . props . formDisabled } value = { v } key = { v } > { v } < / R a d i o >
) }
< / R a d i o . G r o u p >
) }
< / F o r m . I t e m >
2018-10-25 15:51:47 +02:00
< hr / >
2018-10-29 10:50:49 +01:00
< Form . Item { ... formItemLayout } label = "Send later?" >
< Row >
< Col span = { 2 } >
{ getFieldDecorator ( "send_later" , { defaultChecked : this . props . isSingle } ) (
< Switch disabled = { this . props . formDisabled }
checked = { this . state . sendLater }
onChange = { this . handleSendLater } / >
) }
< / C o l >
< Col span = { 12 } >
{ 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"
/ >
) }
< / C o l >
< / R o w >
2018-10-25 15:51:47 +02:00
< / F o r m . I t e m >
2018-10-29 10:50:49 +01:00
{ ! this . props . formDisabled &&
< Form . Item { ... formItemTailLayout } >
< Button htmlType = "submit" type = "primary" >
< Icon type = "save" / > { ! this . props . isSingle ? "Continue" : "Save changes" }
< / B u t t o n >
< / F o r m . I t e m >
}
{ 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%" } } > < / S e l e c t >
) }
< / F o r m . I t e m >
< Form . Item { ... formItemLayout } label = " " colon = { false } >
< Button onClick = { this . handleTestCampaign } > < Icon type = "mail" / > Send test < / B u t t o n >
< / F o r m . I t e m >
< / d i v >
}
< / F o r m >
< / S p i n >
2018-10-25 15:51:47 +02:00
< / d i v >
)
}
}
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 : { } ,
contentType : "richtext" ,
2018-10-26 07:48:17 +02:00
previewRecord : null ,
2018-10-25 15:51:47 +02:00
body : "" ,
currentTab : "form" ,
editor : null ,
loading : true ,
mediaVisible : false ,
formDisabled : false
}
componentDidMount = ( ) => {
// Fetch lists.
this . props . modelRequest ( cs . ModelLists , cs . Routes . GetLists , cs . MethodGet )
// Fetch templates.
this . props . modelRequest ( cs . ModelTemplates , cs . Routes . GetTemplates , cs . MethodGet )
// Fetch campaign.
if ( this . state . campaignID ) {
this . fetchRecord ( this . state . campaignID )
2018-11-02 19:03:00 +01:00
} else {
this . setState ( { loading : false } )
2018-10-25 15:51:47 +02:00
}
}
fetchRecord = ( id ) => {
2018-10-29 10:50:49 +01:00
this . props . request ( cs . Routes . GetCampaign , cs . MethodGet , { id : id } ) . then ( ( r ) => {
2018-10-25 15:51:47 +02:00
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" ] ( { 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 } )
}
2018-10-26 07:48:17 +02:00
handlePreview = ( record ) => {
this . setState ( { previewRecord : record } )
}
2018-10-25 15:51:47 +02:00
render ( ) {
return (
< section className = "content campaign" >
< Row >
< Col span = { 22 } >
{ ! this . state . record . id && < h1 > Create a campaign < / h 1 > }
{ this . state . record . id &&
< h1 >
< Tag color = { cs . CampaignStatusColors [ this . state . record . status ] } > { this . state . record . status } < / T a g >
{ this . state . record . name }
< / h 1 >
}
< / C o l >
< Col span = { 2 } >
< / C o l >
< / R o w >
< 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 }
record = { this . state . record }
isSingle = { this . state . record . id ? true : false }
2018-10-29 10:50:49 +01:00
body = { this . state . body ? this . state . body : this . state . record . body }
2018-10-25 15:51:47 +02:00
contentType = { this . state . contentType }
formDisabled = { this . state . formDisabled }
fetchRecord = { this . fetchRecord }
setCurrentTab = { this . setCurrentTab }
/ >
< / S p i n >
< / T a b s . T a b P a n e >
< Tabs . TabPane tab = "Content" disabled = { this . state . record . id ? false : true } key = "content" >
< Editor { ... this . props }
ref = { ( e ) => {
// 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 : e } )
} }
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" >
2018-10-26 07:48:17 +02:00
< p > < Button icon = "search" onClick = { ( ) => this . handlePreview ( this . state . record ) } > Preview < / B u t t o n > < / p >
2018-10-25 15:51:47 +02:00
< / d i v >
< / T a b s . T a b P a n e >
< / T a b s >
< 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 ,
2018-10-29 10:50:49 +01:00
onOk : this . toggleMedia } } / >
2018-10-25 15:51:47 +02:00
< / M o d a l >
2018-10-26 07:48:17 +02:00
{ 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 } )
} }
/ >
}
2018-10-25 15:51:47 +02:00
< / s e c t i o n >
)
}
}
export default Campaign