Campaign.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. import React from "react"
  2. import {
  3. Modal,
  4. Tabs,
  5. Row,
  6. Col,
  7. Form,
  8. Switch,
  9. Select,
  10. Radio,
  11. Tag,
  12. Input,
  13. Button,
  14. Icon,
  15. Spin,
  16. DatePicker,
  17. Popconfirm,
  18. notification
  19. } from "antd"
  20. import * as cs from "./constants"
  21. import Media from "./Media"
  22. import ModalPreview from "./ModalPreview"
  23. import moment from "moment"
  24. import parseUrl from "querystring"
  25. import ReactQuill from "react-quill"
  26. import Delta from "quill-delta"
  27. import "react-quill/dist/quill.snow.css"
  28. const formItemLayout = {
  29. labelCol: { xs: { span: 16 }, sm: { span: 10 }, md: { span: 4 } },
  30. wrapperCol: { xs: { span: 16 }, sm: { span: 14 }, md: { span: 10 } }
  31. }
  32. class Editor extends React.PureComponent {
  33. state = {
  34. editor: null,
  35. quill: null,
  36. rawInput: null,
  37. selContentType: cs.CampaignContentTypeRichtext,
  38. contentType: cs.CampaignContentTypeRichtext,
  39. body: ""
  40. }
  41. quillModules = {
  42. toolbar: {
  43. container: [
  44. [{ header: [1, 2, 3, false] }],
  45. ["bold", "italic", "underline", "strike", "blockquote", "code"],
  46. [{ color: [] }, { background: [] }, { size: [] }],
  47. [
  48. { list: "ordered" },
  49. { list: "bullet" },
  50. { indent: "-1" },
  51. { indent: "+1" }
  52. ],
  53. [
  54. { align: "" },
  55. { align: "center" },
  56. { align: "right" },
  57. { align: "justify" }
  58. ],
  59. ["link", "image"],
  60. ["clean", "font"]
  61. ],
  62. handlers: {
  63. image: () => {
  64. this.props.toggleMedia()
  65. }
  66. }
  67. }
  68. }
  69. componentDidMount = () => {
  70. // The editor component will only load once the individual campaign metadata
  71. // has loaded, i.e., record.body is guaranteed to be available here.
  72. if (this.props.record && this.props.record.id) {
  73. this.setState({
  74. body: this.props.record.body,
  75. contentType: this.props.record.content_type,
  76. selContentType: this.props.record.content_type
  77. })
  78. }
  79. }
  80. // Custom handler for inserting images from the media popup.
  81. insertMedia = uri => {
  82. const quill = this.state.quill.getEditor()
  83. let range = quill.getSelection(true)
  84. quill.updateContents(
  85. new Delta()
  86. .retain(range.index)
  87. .delete(range.length)
  88. .insert({ image: uri }),
  89. null
  90. )
  91. }
  92. handleSelContentType = (_, e) => {
  93. this.setState({ selContentType: e.props.value })
  94. }
  95. handleSwitchContentType = () => {
  96. this.setState({ contentType: this.state.selContentType })
  97. if (!this.state.quill || !this.state.quill.editor || !this.state.rawInput) {
  98. return
  99. }
  100. // Switching from richtext to html.
  101. let body = ""
  102. if (this.state.selContentType === cs.CampaignContentTypeHTML) {
  103. body = this.state.quill.editor.container.firstChild.innerHTML
  104. // eslint-disable-next-line
  105. this.state.rawInput.value = body
  106. } else if (this.state.selContentType === cs.CampaignContentTypeRichtext) {
  107. body = this.state.rawInput.value
  108. this.state.quill.editor.clipboard.dangerouslyPasteHTML(body, "raw")
  109. }
  110. this.props.setContent(this.state.selContentType, body)
  111. }
  112. render() {
  113. return (
  114. <div>
  115. <header className="header">
  116. {!this.props.formDisabled && (
  117. <Row>
  118. <Col span={20}>
  119. <div className="content-type">
  120. <p>Content format</p>
  121. <Select
  122. name="content_type"
  123. onChange={this.handleSelContentType}
  124. style={{ minWidth: 200 }}
  125. value={this.state.selContentType}
  126. >
  127. <Select.Option value={ cs.CampaignContentTypeRichtext }>Rich Text</Select.Option>
  128. <Select.Option value={ cs.CampaignContentTypeHTML }>Raw HTML</Select.Option>
  129. </Select>
  130. {this.state.contentType !== this.state.selContentType && (
  131. <div className="actions">
  132. <Popconfirm
  133. title="The content may lose its formatting. Are you sure?"
  134. onConfirm={this.handleSwitchContentType}
  135. >
  136. <Button>
  137. <Icon type="save" /> Switch format
  138. </Button>
  139. </Popconfirm>
  140. </div>
  141. )}
  142. </div>
  143. </Col>
  144. <Col span={4} />
  145. </Row>
  146. )}
  147. </header>
  148. <ReactQuill
  149. readOnly={this.props.formDisabled}
  150. style={{
  151. display: this.state.contentType === cs.CampaignContentTypeRichtext ? "block" : "none"
  152. }}
  153. modules={this.quillModules}
  154. defaultValue={this.props.record.body}
  155. ref={o => {
  156. if (!o) {
  157. return
  158. }
  159. this.setState({ quill: o })
  160. document.querySelector(".ql-editor").focus()
  161. }}
  162. onChange={() => {
  163. if (!this.state.quill) {
  164. return
  165. }
  166. this.props.setContent(
  167. this.state.contentType,
  168. this.state.quill.editor.root.innerHTML
  169. )
  170. }}
  171. />
  172. <Input.TextArea
  173. readOnly={this.props.formDisabled}
  174. placeholder="Your message here"
  175. style={{
  176. display: this.state.contentType === "html" ? "block" : "none"
  177. }}
  178. id="html-body"
  179. rows={10}
  180. autosize={{ minRows: 2, maxRows: 10 }}
  181. defaultValue={this.props.record.body}
  182. ref={o => {
  183. if (!o) {
  184. return
  185. }
  186. this.setState({ rawInput: o.textAreaRef })
  187. }}
  188. onChange={e => {
  189. this.props.setContent(this.state.contentType, e.target.value)
  190. }}
  191. />
  192. </div>
  193. )
  194. }
  195. }
  196. class TheFormDef extends React.PureComponent {
  197. state = {
  198. editorVisible: false,
  199. sendLater: false,
  200. loading: false
  201. }
  202. componentWillReceiveProps(nextProps) {
  203. // On initial load, toggle the send_later switch if the record
  204. // has a "send_at" date.
  205. if (nextProps.record.send_at === this.props.record.send_at) {
  206. return
  207. }
  208. this.setState({
  209. sendLater: nextProps.isSingle && nextProps.record.send_at !== null
  210. })
  211. }
  212. validateEmail = (rule, value, callback) => {
  213. if (!value.match(/(.+?)\s<(.+?)@(.+?)>/)) {
  214. return callback("Format should be: Your Name <email@address.com>")
  215. }
  216. callback()
  217. }
  218. handleSendLater = e => {
  219. this.setState({ sendLater: e })
  220. }
  221. // Handle create / edit form submission.
  222. handleSubmit = cb => {
  223. if (this.state.loading) {
  224. return
  225. }
  226. if (!cb) {
  227. // Set a fake callback.
  228. cb = () => {}
  229. }
  230. this.props.form.validateFields((err, values) => {
  231. if (err) {
  232. return
  233. }
  234. if (!values.tags) {
  235. values.tags = []
  236. }
  237. values.type = cs.CampaignTypeRegular
  238. values.body = this.props.body
  239. values.content_type = this.props.contentType
  240. if (values.send_at) {
  241. values.send_later = true
  242. } else {
  243. values.send_later = false
  244. }
  245. // Create a new campaign.
  246. this.setState({ loading: true })
  247. if (!this.props.isSingle) {
  248. this.props
  249. .modelRequest(
  250. cs.ModelCampaigns,
  251. cs.Routes.CreateCampaign,
  252. cs.MethodPost,
  253. values
  254. )
  255. .then(resp => {
  256. notification["success"]({
  257. placement: cs.MsgPosition,
  258. message: "Campaign created",
  259. description: `"${values["name"]}" created`
  260. })
  261. this.props.route.history.push({
  262. pathname: cs.Routes.ViewCampaign.replace(
  263. ":id",
  264. resp.data.data.id
  265. ),
  266. hash: "content-tab"
  267. })
  268. cb(true)
  269. })
  270. .catch(e => {
  271. notification["error"]({
  272. placement: cs.MsgPosition,
  273. message: "Error",
  274. description: e.message
  275. })
  276. this.setState({ loading: false })
  277. cb(false)
  278. })
  279. } else {
  280. this.props
  281. .modelRequest(
  282. cs.ModelCampaigns,
  283. cs.Routes.UpdateCampaign,
  284. cs.MethodPut,
  285. {
  286. ...values,
  287. id: this.props.record.id
  288. }
  289. )
  290. .then(resp => {
  291. notification["success"]({
  292. placement: cs.MsgPosition,
  293. message: "Campaign updated",
  294. description: `"${values["name"]}" updated`
  295. })
  296. this.setState({ loading: false })
  297. this.props.setRecord(resp.data.data)
  298. cb(true)
  299. })
  300. .catch(e => {
  301. notification["error"]({
  302. placement: cs.MsgPosition,
  303. message: "Error",
  304. description: e.message
  305. })
  306. this.setState({ loading: false })
  307. cb(false)
  308. })
  309. }
  310. })
  311. }
  312. handleTestCampaign = e => {
  313. e.preventDefault()
  314. this.props.form.validateFields((err, values) => {
  315. if (err) {
  316. return
  317. }
  318. if (!values.tags) {
  319. values.tags = []
  320. }
  321. values.id = this.props.record.id
  322. values.body = this.props.body
  323. values.content_type = this.props.contentType
  324. this.setState({ loading: true })
  325. this.props
  326. .request(cs.Routes.TestCampaign, cs.MethodPost, values)
  327. .then(resp => {
  328. this.setState({ loading: false })
  329. notification["success"]({
  330. placement: cs.MsgPosition,
  331. message: "Test sent",
  332. description: `Test messages sent`
  333. })
  334. })
  335. .catch(e => {
  336. this.setState({ loading: false })
  337. notification["error"]({
  338. placement: cs.MsgPosition,
  339. message: "Error",
  340. description: e.message
  341. })
  342. })
  343. })
  344. }
  345. render() {
  346. const { record } = this.props
  347. const { getFieldDecorator } = this.props.form
  348. let subLists = []
  349. if (this.props.isSingle && record.lists) {
  350. subLists = record.lists
  351. .map(v => {
  352. // Exclude deleted lists.
  353. return v.id !== 0 ? v.id : null
  354. })
  355. .filter(v => v !== null)
  356. } else if (this.props.route.location.search) {
  357. // One or more list_id in the query params.
  358. const p = parseUrl.parse(this.props.route.location.search.substring(1))
  359. if (p.hasOwnProperty("list_id")) {
  360. if(Array.isArray(p.list_id)) {
  361. p.list_id.forEach(i => {
  362. // eslint-disable-next-line radix
  363. const id = parseInt(i)
  364. if (id) {
  365. subLists.push(id)
  366. }
  367. });
  368. } else {
  369. // eslint-disable-next-line radix
  370. const id = parseInt(p.list_id)
  371. if (id) {
  372. subLists.push(id)
  373. }
  374. }
  375. }
  376. }
  377. if (this.record) {
  378. this.props.pageTitle(record.name + " / Campaigns")
  379. } else {
  380. this.props.pageTitle("New campaign")
  381. }
  382. return (
  383. <div>
  384. <Spin spinning={this.state.loading}>
  385. <Form onSubmit={this.handleSubmit}>
  386. <Form.Item {...formItemLayout} label="Campaign name">
  387. {getFieldDecorator("name", {
  388. extra:
  389. "This is internal and will not be visible to subscribers",
  390. initialValue: record.name,
  391. rules: [{ required: true }]
  392. })(
  393. <Input
  394. disabled={this.props.formDisabled}
  395. autoFocus
  396. maxLength={200}
  397. />
  398. )}
  399. </Form.Item>
  400. <Form.Item {...formItemLayout} label="Subject">
  401. {getFieldDecorator("subject", {
  402. initialValue: record.subject,
  403. rules: [{ required: true }]
  404. })(<Input disabled={this.props.formDisabled} maxLength={500} />)}
  405. </Form.Item>
  406. <Form.Item {...formItemLayout} label="From address">
  407. {getFieldDecorator("from_email", {
  408. initialValue: record.from_email
  409. ? record.from_email
  410. : this.props.config.fromEmail,
  411. rules: [{ required: true }, { validator: this.validateEmail }]
  412. })(
  413. <Input
  414. disabled={this.props.formDisabled}
  415. placeholder="Company Name <email@company.com>"
  416. maxLength={200}
  417. />
  418. )}
  419. </Form.Item>
  420. <Form.Item
  421. {...formItemLayout}
  422. label="Lists"
  423. extra="Lists to subscribe to"
  424. >
  425. {getFieldDecorator("lists", {
  426. initialValue:
  427. subLists.length > 0
  428. ? subLists
  429. : this.props.data[cs.ModelLists].hasOwnProperty(
  430. "results"
  431. ) && this.props.data[cs.ModelLists].results.length === 1
  432. ? [this.props.data[cs.ModelLists].results[0].id]
  433. : undefined,
  434. rules: [{ required: true }]
  435. })(
  436. <Select disabled={this.props.formDisabled} mode="multiple">
  437. {this.props.data[cs.ModelLists].hasOwnProperty("results") &&
  438. [...this.props.data[cs.ModelLists].results].map((v) =>
  439. (record.type !== cs.CampaignTypeOptin || v.optin === cs.ListOptinDouble) && (
  440. <Select.Option value={v["id"]} key={v["id"]}>
  441. {v["name"]}
  442. </Select.Option>
  443. ))}
  444. </Select>
  445. )}
  446. </Form.Item>
  447. <Form.Item {...formItemLayout} label="Template" extra="Template">
  448. {getFieldDecorator("template_id", {
  449. initialValue: record.template_id
  450. ? record.template_id
  451. : this.props.data[cs.ModelTemplates].length > 0
  452. ? this.props.data[cs.ModelTemplates].filter(
  453. t => t.is_default
  454. )[0].id
  455. : undefined,
  456. rules: [{ required: true }]
  457. })(
  458. <Select disabled={this.props.formDisabled}>
  459. {this.props.data[cs.ModelTemplates].map((v, i) => (
  460. <Select.Option value={v["id"]} key={v["id"]}>
  461. {v["name"]}
  462. </Select.Option>
  463. ))}
  464. </Select>
  465. )}
  466. </Form.Item>
  467. <Form.Item
  468. {...formItemLayout}
  469. label="Tags"
  470. extra="Hit Enter after typing a word to add multiple tags"
  471. >
  472. {getFieldDecorator("tags", { initialValue: record.tags })(
  473. <Select disabled={this.props.formDisabled} mode="tags" />
  474. )}
  475. </Form.Item>
  476. <Form.Item
  477. {...formItemLayout}
  478. label="Messenger"
  479. style={{
  480. display:
  481. this.props.config.messengers.length === 1 ? "none" : "block"
  482. }}
  483. >
  484. {getFieldDecorator("messenger", {
  485. initialValue: record.messenger ? record.messenger : "email"
  486. })(
  487. <Radio.Group className="messengers">
  488. {[...this.props.config.messengers].map((v, i) => (
  489. <Radio disabled={this.props.formDisabled} value={v} key={v}>
  490. {v}
  491. </Radio>
  492. ))}
  493. </Radio.Group>
  494. )}
  495. </Form.Item>
  496. <hr />
  497. <Form.Item {...formItemLayout} label="Send later?">
  498. <Row>
  499. <Col lg={4}>
  500. {getFieldDecorator("send_later")(
  501. <Switch
  502. disabled={this.props.formDisabled}
  503. checked={this.state.sendLater}
  504. onChange={this.handleSendLater}
  505. />
  506. )}
  507. </Col>
  508. <Col lg={20}>
  509. {this.state.sendLater &&
  510. getFieldDecorator("send_at", {
  511. initialValue:
  512. record && typeof record.send_at === "string"
  513. ? moment(record.send_at)
  514. : moment(new Date())
  515. .add(1, "days")
  516. .startOf("day")
  517. })(
  518. <DatePicker
  519. disabled={this.props.formDisabled}
  520. showTime
  521. format="YYYY-MM-DD HH:mm:ss"
  522. placeholder="Select a date and time"
  523. />
  524. )}
  525. </Col>
  526. </Row>
  527. </Form.Item>
  528. {this.props.isSingle && (
  529. <div>
  530. <hr />
  531. <Form.Item
  532. {...formItemLayout}
  533. label="Send test messages"
  534. extra="Hit Enter after typing an address to add multiple recipients. The addresses must belong to existing subscribers."
  535. >
  536. {getFieldDecorator("subscribers")(
  537. <Select mode="tags" style={{ width: "100%" }} />
  538. )}
  539. </Form.Item>
  540. <Form.Item {...formItemLayout} label="&nbsp;" colon={false}>
  541. <Button onClick={this.handleTestCampaign}>
  542. <Icon type="mail" /> Send test
  543. </Button>
  544. </Form.Item>
  545. </div>
  546. )}
  547. </Form>
  548. </Spin>
  549. </div>
  550. )
  551. }
  552. }
  553. const TheForm = Form.create()(TheFormDef)
  554. class Campaign extends React.PureComponent {
  555. state = {
  556. campaignID: this.props.route.match.params
  557. ? parseInt(this.props.route.match.params.campaignID, 10)
  558. : 0,
  559. record: {},
  560. formRef: null,
  561. contentType: cs.CampaignContentTypeRichtext,
  562. previewRecord: null,
  563. body: "",
  564. currentTab: "form",
  565. editor: null,
  566. loading: true,
  567. mediaVisible: false,
  568. formDisabled: false
  569. }
  570. componentDidMount = () => {
  571. // Fetch lists.
  572. this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet, {
  573. per_page: "all"
  574. })
  575. // Fetch templates.
  576. this.props.modelRequest(
  577. cs.ModelTemplates,
  578. cs.Routes.GetTemplates,
  579. cs.MethodGet
  580. )
  581. // Fetch campaign.
  582. if (this.state.campaignID) {
  583. this.fetchRecord(this.state.campaignID)
  584. } else {
  585. this.setState({ loading: false })
  586. }
  587. // Content tab?
  588. if (document.location.hash === "#content-tab") {
  589. this.setCurrentTab("content")
  590. }
  591. }
  592. setRecord = r => {
  593. this.setState({ record: r })
  594. }
  595. fetchRecord = id => {
  596. this.props
  597. .request(cs.Routes.GetCampaign, cs.MethodGet, { id: id })
  598. .then(r => {
  599. const record = r.data.data
  600. this.setState({ loading: false })
  601. this.setRecord(record)
  602. // The form for non draft and scheduled campaigns should be locked.
  603. if (
  604. record.status !== cs.CampaignStatusDraft &&
  605. record.status !== cs.CampaignStatusScheduled
  606. ) {
  607. this.setState({ formDisabled: true })
  608. }
  609. })
  610. .catch(e => {
  611. notification["error"]({
  612. placement: cs.MsgPosition,
  613. message: "Error",
  614. description: e.message
  615. })
  616. })
  617. }
  618. setContent = (contentType, body) => {
  619. this.setState({ contentType: contentType, body: body })
  620. }
  621. toggleMedia = () => {
  622. this.setState({ mediaVisible: !this.state.mediaVisible })
  623. }
  624. setCurrentTab = tab => {
  625. this.setState({ currentTab: tab })
  626. }
  627. handlePreview = record => {
  628. this.setState({ previewRecord: record })
  629. }
  630. render() {
  631. return (
  632. <section className="content campaign">
  633. <Row gutter={[2, 16]}>
  634. <Col span={24} md={12}>
  635. {!this.state.record.id && <h1>Create a campaign</h1>}
  636. {this.state.record.id && (
  637. <div>
  638. <h1>
  639. <Tag
  640. color={cs.CampaignStatusColors[this.state.record.status]}
  641. >
  642. {this.state.record.status}
  643. </Tag>
  644. {this.state.record.type === cs.CampaignStatusOptin && (
  645. <Tag className="campaign-type" color="geekblue">
  646. {this.state.record.type}
  647. </Tag>
  648. )}
  649. {this.state.record.name}
  650. </h1>
  651. <span className="text-tiny text-grey">
  652. ID {this.state.record.id} &mdash; UUID{" "}
  653. {this.state.record.uuid}
  654. </span>
  655. </div>
  656. )}
  657. </Col>
  658. <Col span={24} md={12} className="right header-action-break">
  659. {!this.state.formDisabled && !this.state.loading && (
  660. <div>
  661. <Button
  662. type="primary"
  663. icon="save"
  664. onClick={() => {
  665. this.state.formRef.handleSubmit()
  666. }}
  667. >
  668. {!this.state.record.id ? "Continue" : "Save changes"}
  669. </Button>{" "}
  670. {this.state.record.status === cs.CampaignStatusDraft &&
  671. this.state.record.send_at && (
  672. <Popconfirm
  673. title="The campaign will start automatically at the scheduled date and time. Schedule now?"
  674. onConfirm={() => {
  675. this.state.formRef.handleSubmit(() => {
  676. this.props.route.history.push({
  677. pathname: cs.Routes.ViewCampaigns,
  678. state: {
  679. campaign: this.state.record,
  680. campaignStatus: cs.CampaignStatusScheduled
  681. }
  682. })
  683. })
  684. }}
  685. >
  686. <Button icon="clock-circle" type="primary">
  687. Schedule campaign
  688. </Button>
  689. </Popconfirm>
  690. )}
  691. {this.state.record.status === cs.CampaignStatusDraft &&
  692. !this.state.record.send_at && (
  693. <Popconfirm
  694. title="Campaign properties cannot be changed once it starts. Save changes and start now?"
  695. onConfirm={() => {
  696. this.state.formRef.handleSubmit(() => {
  697. this.props.route.history.push({
  698. pathname: cs.Routes.ViewCampaigns,
  699. state: {
  700. campaign: this.state.record,
  701. campaignStatus: cs.CampaignStatusRunning
  702. }
  703. })
  704. })
  705. }}
  706. >
  707. <Button icon="rocket" type="primary">
  708. Start campaign
  709. </Button>
  710. </Popconfirm>
  711. )}
  712. </div>
  713. )}
  714. </Col>
  715. </Row>
  716. <br />
  717. <Tabs
  718. type="card"
  719. activeKey={this.state.currentTab}
  720. onTabClick={t => {
  721. this.setState({ currentTab: t })
  722. }}
  723. >
  724. <Tabs.TabPane tab="Campaign" key="form">
  725. <Spin spinning={this.state.loading}>
  726. <TheForm
  727. {...this.props}
  728. wrappedComponentRef={r => {
  729. if (!r) {
  730. return
  731. }
  732. // Take the editor's reference and save it in the state
  733. // so that it's insertMedia() function can be passed to <Media />
  734. this.setState({ formRef: r })
  735. }}
  736. record={this.state.record}
  737. setRecord={this.setRecord}
  738. isSingle={this.state.record.id ? true : false}
  739. body={
  740. this.state.body ? this.state.body : this.state.record.body
  741. }
  742. contentType={this.state.contentType}
  743. formDisabled={this.state.formDisabled}
  744. fetchRecord={this.fetchRecord}
  745. setCurrentTab={this.setCurrentTab}
  746. />
  747. </Spin>
  748. </Tabs.TabPane>
  749. <Tabs.TabPane
  750. tab="Content"
  751. disabled={this.state.record.id ? false : true}
  752. key="content"
  753. >
  754. {this.state.record.id && (
  755. <div>
  756. <Editor
  757. {...this.props}
  758. ref={r => {
  759. if (!r) {
  760. return
  761. }
  762. // Take the editor's reference and save it in the state
  763. // so that it's insertMedia() function can be passed to <Media />
  764. this.setState({ editor: r })
  765. }}
  766. isSingle={this.state.record.id ? true : false}
  767. record={this.state.record}
  768. visible={this.state.editorVisible}
  769. toggleMedia={this.toggleMedia}
  770. setContent={this.setContent}
  771. formDisabled={this.state.formDisabled}
  772. />
  773. <div className="content-actions">
  774. <p>
  775. <Button
  776. icon="search"
  777. onClick={() => this.handlePreview(this.state.record)}
  778. >
  779. Preview
  780. </Button>
  781. </p>
  782. </div>
  783. </div>
  784. )}
  785. {!this.state.record.id && <Spin className="empty-spinner" />}
  786. </Tabs.TabPane>
  787. </Tabs>
  788. <Modal
  789. visible={this.state.mediaVisible}
  790. width="900px"
  791. title="Media"
  792. okText={"Ok"}
  793. onCancel={this.toggleMedia}
  794. onOk={this.toggleMedia}
  795. >
  796. <Media
  797. {...{
  798. ...this.props,
  799. insertMedia: this.state.editor
  800. ? this.state.editor.insertMedia
  801. : null,
  802. onCancel: this.toggleMedia,
  803. onOk: this.toggleMedia
  804. }}
  805. />
  806. </Modal>
  807. {this.state.previewRecord && (
  808. <ModalPreview
  809. title={this.state.previewRecord.name}
  810. body={this.state.body}
  811. previewURL={cs.Routes.PreviewCampaign.replace(
  812. ":id",
  813. this.state.previewRecord.id
  814. )}
  815. onCancel={() => {
  816. this.setState({ previewRecord: null })
  817. }}
  818. />
  819. )}
  820. </section>
  821. )
  822. }
  823. }
  824. export default Campaign