Reformat all JS to 'prettier' style
This commit is contained in:
parent
5b42e8659f
commit
917cb8aeed
|
@ -1,17 +1,18 @@
|
||||||
const {injectBabelPlugin} = require("react-app-rewired");
|
const { injectBabelPlugin } = require("react-app-rewired")
|
||||||
const rewireLess = require("react-app-rewire-less");
|
const rewireLess = require("react-app-rewire-less")
|
||||||
|
|
||||||
module.exports = function override(config, env) {
|
module.exports = function override(config, env) {
|
||||||
config = injectBabelPlugin(
|
config = injectBabelPlugin(
|
||||||
[
|
[
|
||||||
"import", {
|
"import",
|
||||||
|
{
|
||||||
libraryName: "antd",
|
libraryName: "antd",
|
||||||
libraryDirectory: "es",
|
libraryDirectory: "es",
|
||||||
style: true
|
style: true
|
||||||
}
|
}
|
||||||
], // change importing css to less
|
], // change importing css to less
|
||||||
config,
|
config
|
||||||
);
|
)
|
||||||
config = rewireLess.withLoaderOptions({
|
config = rewireLess.withLoaderOptions({
|
||||||
modifyVars: {
|
modifyVars: {
|
||||||
"@font-family":
|
"@font-family":
|
||||||
|
@ -24,7 +25,7 @@ module.exports = function override(config, env) {
|
||||||
"@shadow-1-right": "2px 0 3px @shadow-color",
|
"@shadow-1-right": "2px 0 3px @shadow-color",
|
||||||
"@shadow-2": "0 2px 6px @shadow-color"
|
"@shadow-2": "0 2px 6px @shadow-color"
|
||||||
},
|
},
|
||||||
javascriptEnabled: true,
|
javascriptEnabled: true
|
||||||
})(config, env);
|
})(config, env)
|
||||||
return config;
|
return config
|
||||||
};
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-plugin-import": "^1.11.0",
|
"babel-plugin-import": "^1.11.0",
|
||||||
"less-plugin-npm-import": "^2.1.0"
|
"eslint-plugin-prettier": "^3.0.1",
|
||||||
|
"less-plugin-npm-import": "^2.1.0",
|
||||||
|
"prettier": "1.15.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import Utils from './utils'
|
import Utils from "./utils"
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from "react-router-dom"
|
||||||
import { Icon, notification } from "antd"
|
import { Icon, notification } from "antd"
|
||||||
import axios from 'axios'
|
import axios from "axios"
|
||||||
import qs from 'qs'
|
import qs from "qs"
|
||||||
|
|
||||||
import logo from "./static/listmonk.svg"
|
import logo from "./static/listmonk.svg"
|
||||||
import Layout from './Layout'
|
import Layout from "./Layout"
|
||||||
import * as cs from './constants'
|
import * as cs from "./constants"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
App acts as a an "automagic" wrapper for all sub components. It is also the central
|
App acts as a an "automagic" wrapper for all sub components. It is also the central
|
||||||
|
@ -26,22 +26,29 @@ import * as cs from './constants'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class App extends React.PureComponent {
|
class App extends React.PureComponent {
|
||||||
models = [cs.ModelUsers,
|
models = [
|
||||||
|
cs.ModelUsers,
|
||||||
cs.ModelSubscribers,
|
cs.ModelSubscribers,
|
||||||
cs.ModelLists,
|
cs.ModelLists,
|
||||||
cs.ModelCampaigns,
|
cs.ModelCampaigns,
|
||||||
cs.ModelTemplates]
|
cs.ModelTemplates
|
||||||
|
]
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
// Initialize empty states.
|
// Initialize empty states.
|
||||||
reqStates: this.models.reduce((map, obj) => (map[obj] = cs.StatePending, map), {}),
|
reqStates: this.models.reduce(
|
||||||
data: this.models.reduce((map, obj) => (map[obj] = [], map), {}),
|
// eslint-disable-next-line
|
||||||
|
(map, obj) => ((map[obj] = cs.StatePending), map),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
data: this.models.reduce((map, obj) => ((map[obj] = []), map), {}),
|
||||||
modStates: {}
|
modStates: {}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
axios.defaults.paramsSerializer = params => {
|
axios.defaults.paramsSerializer = params => {
|
||||||
return qs.stringify(params, {arrayFormat: "repeat"});
|
return qs.stringify(params, { arrayFormat: "repeat" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,11 +57,13 @@ class App extends React.PureComponent {
|
||||||
modelRequest = async (model, route, method, params) => {
|
modelRequest = async (model, route, method, params) => {
|
||||||
let url = replaceParams(route, params)
|
let url = replaceParams(route, params)
|
||||||
|
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StatePending } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [model]: cs.StatePending }
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
let req = {
|
let req = {
|
||||||
method: method,
|
method: method,
|
||||||
url: url,
|
url: url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (method === cs.MethodGet || method === cs.MethodDelete) {
|
if (method === cs.MethodGet || method === cs.MethodDelete) {
|
||||||
|
@ -63,27 +72,30 @@ class App extends React.PureComponent {
|
||||||
req.data = params ? params : {}
|
req.data = params ? params : {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let res = await axios(req)
|
let res = await axios(req)
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StateDone } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
|
||||||
|
})
|
||||||
|
|
||||||
// If it's a GET call, set the response as the data state.
|
// If it's a GET call, set the response as the data state.
|
||||||
if (method === cs.MethodGet) {
|
if (method === cs.MethodGet) {
|
||||||
this.setState({ data: { ...this.state.data, [model]: res.data.data } })
|
this.setState({ data: { ...this.state.data, [model]: res.data.data } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If it's a GET call, throw a global notification.
|
// If it's a GET call, throw a global notification.
|
||||||
if (method === cs.MethodGet) {
|
if (method === cs.MethodGet) {
|
||||||
notification["error"]({ placement: cs.MsgPosition,
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Error fetching data",
|
message: "Error fetching data",
|
||||||
description: Utils.HttpError(e).message
|
description: Utils.HttpError(e).message
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set states and show the error on the layout.
|
// Set states and show the error on the layout.
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StateDone } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
|
||||||
|
})
|
||||||
throw Utils.HttpError(e)
|
throw Utils.HttpError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,7 +104,9 @@ class App extends React.PureComponent {
|
||||||
request = async (url, method, params, headers) => {
|
request = async (url, method, params, headers) => {
|
||||||
url = replaceParams(url, params)
|
url = replaceParams(url, params)
|
||||||
|
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StatePending } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [url]: cs.StatePending }
|
||||||
|
})
|
||||||
try {
|
try {
|
||||||
let req = {
|
let req = {
|
||||||
method: method,
|
method: method,
|
||||||
|
@ -108,16 +122,19 @@ class App extends React.PureComponent {
|
||||||
|
|
||||||
let res = await axios(req)
|
let res = await axios(req)
|
||||||
|
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StateDone } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
|
||||||
|
})
|
||||||
return res
|
return res
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StateDone } })
|
this.setState({
|
||||||
|
reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
|
||||||
|
})
|
||||||
throw Utils.HttpError(e)
|
throw Utils.HttpError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageTitle = title => {
|
||||||
pageTitle = (title) => {
|
|
||||||
document.title = title
|
document.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,9 +147,13 @@ class App extends React.PureComponent {
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h1><Icon type="warning" /> Something's not right</h1>
|
<h1>
|
||||||
<p>The app configuration could not be loaded.
|
<Icon type="warning" /> Something's not right
|
||||||
Please ensure that the app is running and then refresh this page.</p>
|
</h1>
|
||||||
|
<p>
|
||||||
|
The app configuration could not be loaded. Please ensure that the
|
||||||
|
app is running and then refresh this page.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -145,7 +166,8 @@ class App extends React.PureComponent {
|
||||||
reqStates={this.state.reqStates}
|
reqStates={this.state.reqStates}
|
||||||
pageTitle={this.pageTitle}
|
pageTitle={this.pageTitle}
|
||||||
config={window.CONFIG}
|
config={window.CONFIG}
|
||||||
data={ this.state.data } />
|
data={this.state.data}
|
||||||
|
/>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -153,9 +175,9 @@ class App extends React.PureComponent {
|
||||||
|
|
||||||
function replaceParams(route, params) {
|
function replaceParams(route, params) {
|
||||||
// Replace :params in the URL with params in the array.
|
// Replace :params in the URL with params in the array.
|
||||||
let uriParams = route.match(/:([a-z0-9\-_]+)/ig)
|
let uriParams = route.match(/:([a-z0-9\-_]+)/gi)
|
||||||
if (uriParams && uriParams.length > 0) {
|
if (uriParams && uriParams.length > 0) {
|
||||||
uriParams.forEach((p) => {
|
uriParams.forEach(p => {
|
||||||
let pName = p.slice(1) // Lose the ":" prefix
|
let pName = p.slice(1) // Lose the ":" prefix
|
||||||
if (params && params.hasOwnProperty(pName)) {
|
if (params && params.hasOwnProperty(pName)) {
|
||||||
route = route.replace(p, params[pName])
|
route = route.replace(p, params[pName])
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
import React from "react"
|
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 ModalPreview from "./ModalPreview"
|
||||||
|
|
||||||
import moment from 'moment'
|
import moment from "moment"
|
||||||
import ReactQuill from "react-quill"
|
import ReactQuill from "react-quill"
|
||||||
import Delta from "quill-delta"
|
import Delta from "quill-delta"
|
||||||
import "react-quill/dist/quill.snow.css"
|
import "react-quill/dist/quill.snow.css"
|
||||||
|
@ -14,10 +31,6 @@ const formItemLayout = {
|
||||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
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 {
|
class Editor extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
editor: null,
|
editor: null,
|
||||||
|
@ -31,16 +44,26 @@ class Editor extends React.PureComponent {
|
||||||
quillModules = {
|
quillModules = {
|
||||||
toolbar: {
|
toolbar: {
|
||||||
container: [
|
container: [
|
||||||
[{"header": [1, 2, 3, false] }],
|
[{ header: [1, 2, 3, false] }],
|
||||||
["bold", "italic", "underline", "strike", "blockquote", "code"],
|
["bold", "italic", "underline", "strike", "blockquote", "code"],
|
||||||
[{ "color": [] }, { "background": [] }, { 'size': [] }],
|
[{ color: [] }, { background: [] }, { size: [] }],
|
||||||
[{"list": "ordered"}, {"list": "bullet"}, {"indent": "-1"}, {"indent": "+1"}],
|
[
|
||||||
[{"align": ""}, { "align": "center" }, { "align": "right" }, { "align": "justify" }],
|
{ list: "ordered" },
|
||||||
|
{ list: "bullet" },
|
||||||
|
{ indent: "-1" },
|
||||||
|
{ indent: "+1" }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ align: "" },
|
||||||
|
{ align: "center" },
|
||||||
|
{ align: "right" },
|
||||||
|
{ align: "justify" }
|
||||||
|
],
|
||||||
["link", "image"],
|
["link", "image"],
|
||||||
["clean", "font"]
|
["clean", "font"]
|
||||||
],
|
],
|
||||||
handlers: {
|
handlers: {
|
||||||
"image": () => {
|
image: () => {
|
||||||
this.props.toggleMedia()
|
this.props.toggleMedia()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,14 +83,16 @@ class Editor extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom handler for inserting images from the media popup.
|
// Custom handler for inserting images from the media popup.
|
||||||
insertMedia = (uri) => {
|
insertMedia = uri => {
|
||||||
const quill = this.state.quill.getEditor()
|
const quill = this.state.quill.getEditor()
|
||||||
let range = quill.getSelection(true);
|
let range = quill.getSelection(true)
|
||||||
quill.updateContents(new Delta()
|
quill.updateContents(
|
||||||
|
new Delta()
|
||||||
.retain(range.index)
|
.retain(range.index)
|
||||||
.delete(range.length)
|
.delete(range.length)
|
||||||
.insert({ image: this.props.config.rootURL + uri })
|
.insert({ image: this.props.config.rootURL + uri }),
|
||||||
, null);
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelContentType = (_, e) => {
|
handleSelContentType = (_, e) => {
|
||||||
|
@ -84,6 +109,7 @@ class Editor extends React.PureComponent {
|
||||||
let body = ""
|
let body = ""
|
||||||
if (this.state.selContentType === "html") {
|
if (this.state.selContentType === "html") {
|
||||||
body = this.state.quill.editor.container.firstChild.innerHTML
|
body = this.state.quill.editor.container.firstChild.innerHTML
|
||||||
|
// eslint-disable-next-line
|
||||||
this.state.rawInput.value = body
|
this.state.rawInput.value = body
|
||||||
} else if (this.state.selContentType === "richtext") {
|
} else if (this.state.selContentType === "richtext") {
|
||||||
body = this.state.rawInput.value
|
body = this.state.rawInput.value
|
||||||
|
@ -97,37 +123,46 @@ class Editor extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<header className="header">
|
<header className="header">
|
||||||
{ !this.props.formDisabled &&
|
{!this.props.formDisabled && (
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={20}>
|
<Col span={20}>
|
||||||
<div className="content-type">
|
<div className="content-type">
|
||||||
<p>Content format</p>
|
<p>Content format</p>
|
||||||
<Select name="content_type" onChange={ this.handleSelContentType } style={{ minWidth: 200 }}
|
<Select
|
||||||
value={ this.state.selContentType }>
|
name="content_type"
|
||||||
|
onChange={this.handleSelContentType}
|
||||||
|
style={{ minWidth: 200 }}
|
||||||
|
value={this.state.selContentType}
|
||||||
|
>
|
||||||
<Select.Option value="richtext">Rich Text</Select.Option>
|
<Select.Option value="richtext">Rich Text</Select.Option>
|
||||||
<Select.Option value="html">Raw HTML</Select.Option>
|
<Select.Option value="html">Raw HTML</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
{ this.state.contentType !== this.state.selContentType &&
|
{this.state.contentType !== this.state.selContentType && (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Popconfirm title="The content may lose its formatting. Are you sure?"
|
<Popconfirm
|
||||||
onConfirm={ this.handleSwitchContentType }>
|
title="The content may lose its formatting. Are you sure?"
|
||||||
|
onConfirm={this.handleSwitchContentType}
|
||||||
|
>
|
||||||
<Button>
|
<Button>
|
||||||
<Icon type="save" /> Switch format
|
<Icon type="save" /> Switch format
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={ 4 }></Col>
|
<Col span={4} />
|
||||||
</Row>
|
</Row>
|
||||||
}
|
)}
|
||||||
</header>
|
</header>
|
||||||
<ReactQuill
|
<ReactQuill
|
||||||
readOnly={this.props.formDisabled}
|
readOnly={this.props.formDisabled}
|
||||||
style={{ display: this.state.contentType === "richtext" ? "block" : "none" }}
|
style={{
|
||||||
|
display: this.state.contentType === "richtext" ? "block" : "none"
|
||||||
|
}}
|
||||||
modules={this.quillModules}
|
modules={this.quillModules}
|
||||||
defaultValue={this.props.record.body}
|
defaultValue={this.props.record.body}
|
||||||
ref={ (o) => {
|
ref={o => {
|
||||||
if (!o) {
|
if (!o) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -140,26 +175,31 @@ class Editor extends React.PureComponent {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.setContent(this.state.contentType, this.state.quill.editor.root.innerHTML)
|
this.props.setContent(
|
||||||
|
this.state.contentType,
|
||||||
|
this.state.quill.editor.root.innerHTML
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
readOnly={this.props.formDisabled}
|
readOnly={this.props.formDisabled}
|
||||||
placeholder="Your message here"
|
placeholder="Your message here"
|
||||||
style={{ display: this.state.contentType === "html" ? "block" : "none" }}
|
style={{
|
||||||
|
display: this.state.contentType === "html" ? "block" : "none"
|
||||||
|
}}
|
||||||
id="html-body"
|
id="html-body"
|
||||||
rows={10}
|
rows={10}
|
||||||
autosize={{ minRows: 2, maxRows: 10 }}
|
autosize={{ minRows: 2, maxRows: 10 }}
|
||||||
defaultValue={this.props.record.body}
|
defaultValue={this.props.record.body}
|
||||||
ref={ (o) => {
|
ref={o => {
|
||||||
if (!o) {
|
if (!o) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ rawInput: o.textAreaRef })
|
this.setState({ rawInput: o.textAreaRef })
|
||||||
}}
|
}}
|
||||||
onChange={ (e) => {
|
onChange={e => {
|
||||||
this.props.setContent(this.state.contentType, e.target.value)
|
this.props.setContent(this.state.contentType, e.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -194,12 +234,12 @@ class TheFormDef extends React.PureComponent {
|
||||||
callback()
|
callback()
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSendLater = (e) => {
|
handleSendLater = e => {
|
||||||
this.setState({ sendLater: e })
|
this.setState({ sendLater: e })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
handleSubmit = (cb) => {
|
handleSubmit = cb => {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -224,30 +264,61 @@ class TheFormDef extends React.PureComponent {
|
||||||
// Create a new campaign.
|
// Create a new campaign.
|
||||||
this.setState({ loading: true })
|
this.setState({ loading: true })
|
||||||
if (!this.props.isSingle) {
|
if (!this.props.isSingle) {
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, values).then((resp) => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition,
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.CreateCampaign,
|
||||||
|
cs.MethodPost,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Campaign created",
|
message: "Campaign created",
|
||||||
description: `"${values["name"]}" created` })
|
description: `"${values["name"]}" created`
|
||||||
|
})
|
||||||
|
|
||||||
this.props.route.history.push({
|
this.props.route.history.push({
|
||||||
pathname: cs.Routes.ViewCampaign.replace(":id", resp.data.data.id),
|
pathname: cs.Routes.ViewCampaign.replace(
|
||||||
|
":id",
|
||||||
|
resp.data.data.id
|
||||||
|
),
|
||||||
hash: "content-tab"
|
hash: "content-tab"
|
||||||
})
|
})
|
||||||
cb(true)
|
cb(true)
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
cb(false)
|
cb(false)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaign, cs.MethodPut, { ...values, id: this.props.record.id }).then((resp) => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition,
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.UpdateCampaign,
|
||||||
|
cs.MethodPut,
|
||||||
|
{ ...values, id: this.props.record.id }
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Campaign updated",
|
message: "Campaign updated",
|
||||||
description: `"${values["name"]}" updated` })
|
description: `"${values["name"]}" updated`
|
||||||
|
})
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
cb(true)
|
cb(true)
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
cb(false)
|
cb(false)
|
||||||
})
|
})
|
||||||
|
@ -255,8 +326,7 @@ class TheFormDef extends React.PureComponent {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTestCampaign = e => {
|
||||||
handleTestCampaign = (e) => {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.props.form.validateFields((err, values) => {
|
this.props.form.validateFields((err, values) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -272,25 +342,38 @@ class TheFormDef extends React.PureComponent {
|
||||||
values.content_type = this.props.contentType
|
values.content_type = this.props.contentType
|
||||||
|
|
||||||
this.setState({ loading: true })
|
this.setState({ loading: true })
|
||||||
this.props.request(cs.Routes.TestCampaign, cs.MethodPost, values).then((resp) => {
|
this.props
|
||||||
|
.request(cs.Routes.TestCampaign, cs.MethodPost, values)
|
||||||
|
.then(resp => {
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
notification["success"]({ placement: cs.MsgPosition,
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Test sent",
|
message: "Test sent",
|
||||||
description: `Test messages sent` })
|
description: `Test messages sent`
|
||||||
}).catch(e => {
|
})
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { record } = this.props;
|
const { record } = this.props
|
||||||
const { getFieldDecorator } = this.props.form
|
const { getFieldDecorator } = this.props.form
|
||||||
|
|
||||||
let subLists = []
|
let subLists = []
|
||||||
if (this.props.isSingle && record.lists) {
|
if (this.props.isSingle && record.lists) {
|
||||||
subLists = record.lists.map((v) => { return v.id !== 0 ? v.id : null }).filter(v => v !== null)
|
subLists = record.lists
|
||||||
|
.map(v => {
|
||||||
|
return v.id !== 0 ? v.id : null
|
||||||
|
})
|
||||||
|
.filter(v => v !== null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.record) {
|
if (this.record) {
|
||||||
|
@ -305,10 +388,17 @@ class TheFormDef extends React.PureComponent {
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
<Form.Item {...formItemLayout} label="Campaign name">
|
<Form.Item {...formItemLayout} label="Campaign name">
|
||||||
{getFieldDecorator("name", {
|
{getFieldDecorator("name", {
|
||||||
extra: "This is internal and will not be visible to subscribers",
|
extra:
|
||||||
|
"This is internal and will not be visible to subscribers",
|
||||||
initialValue: record.name,
|
initialValue: record.name,
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(<Input disabled={ this.props.formDisabled }autoFocus maxLength="200" />)}
|
})(
|
||||||
|
<Input
|
||||||
|
disabled={this.props.formDisabled}
|
||||||
|
autoFocus
|
||||||
|
maxLength="200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Subject">
|
<Form.Item {...formItemLayout} label="Subject">
|
||||||
{getFieldDecorator("subject", {
|
{getFieldDecorator("subject", {
|
||||||
|
@ -318,45 +408,87 @@ class TheFormDef extends React.PureComponent {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="From address">
|
<Form.Item {...formItemLayout} label="From address">
|
||||||
{getFieldDecorator("from_email", {
|
{getFieldDecorator("from_email", {
|
||||||
initialValue: record.from_email ? record.from_email : this.props.config.fromEmail,
|
initialValue: record.from_email
|
||||||
|
? record.from_email
|
||||||
|
: this.props.config.fromEmail,
|
||||||
rules: [{ required: true }, { validator: this.validateEmail }]
|
rules: [{ required: true }, { validator: this.validateEmail }]
|
||||||
})(<Input disabled={ this.props.formDisabled } placeholder="Company Name <email@company.com>" maxLength="200" />)}
|
})(
|
||||||
|
<Input
|
||||||
|
disabled={this.props.formDisabled}
|
||||||
|
placeholder="Company Name <email@company.com>"
|
||||||
|
maxLength="200"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to">
|
<Form.Item
|
||||||
|
{...formItemLayout}
|
||||||
|
label="Lists"
|
||||||
|
extra="Lists to subscribe to"
|
||||||
|
>
|
||||||
{getFieldDecorator("lists", {
|
{getFieldDecorator("lists", {
|
||||||
initialValue: subLists.length > 0 ? subLists : (this.props.data[cs.ModelLists].length === 1 ? [this.props.data[cs.ModelLists][0].id] : undefined),
|
initialValue:
|
||||||
|
subLists.length > 0
|
||||||
|
? subLists
|
||||||
|
: this.props.data[cs.ModelLists].length === 1
|
||||||
|
? [this.props.data[cs.ModelLists][0].id]
|
||||||
|
: undefined,
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(
|
})(
|
||||||
<Select disabled={this.props.formDisabled} mode="multiple">
|
<Select disabled={this.props.formDisabled} mode="multiple">
|
||||||
{[...this.props.data[cs.ModelLists]].map((v, i) =>
|
{[...this.props.data[cs.ModelLists]].map((v, i) => (
|
||||||
<Select.Option value={ v["id"] } key={ v["id"] }>{ v["name"] }</Select.Option>
|
<Select.Option value={v["id"]} key={v["id"]}>
|
||||||
)}
|
{v["name"]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Template" extra="Template">
|
<Form.Item {...formItemLayout} label="Template" extra="Template">
|
||||||
{getFieldDecorator("template_id", {
|
{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),
|
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 }]
|
rules: [{ required: true }]
|
||||||
})(
|
})(
|
||||||
<Select disabled={this.props.formDisabled}>
|
<Select disabled={this.props.formDisabled}>
|
||||||
{this.props.data[cs.ModelTemplates].map((v, i) =>
|
{this.props.data[cs.ModelTemplates].map((v, i) => (
|
||||||
<Select.Option value={ v["id"] } key={ v["id"] }>{ v["name"] }</Select.Option>
|
<Select.Option value={v["id"]} key={v["id"]}>
|
||||||
)}
|
{v["name"]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Tags" extra="Hit Enter after typing a word to add multiple tags">
|
<Form.Item
|
||||||
|
{...formItemLayout}
|
||||||
|
label="Tags"
|
||||||
|
extra="Hit Enter after typing a word to add multiple tags"
|
||||||
|
>
|
||||||
{getFieldDecorator("tags", { initialValue: record.tags })(
|
{getFieldDecorator("tags", { initialValue: record.tags })(
|
||||||
<Select disabled={ this.props.formDisabled } mode="tags"></Select>
|
<Select disabled={this.props.formDisabled} mode="tags" />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Messenger" style={{ display: this.props.config.messengers.length === 1 ? "none" : "block" }}>
|
<Form.Item
|
||||||
{getFieldDecorator("messenger", { initialValue: record.messenger ? record.messenger : "email" })(
|
{...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">
|
<Radio.Group className="messengers">
|
||||||
{[...this.props.config.messengers].map((v, i) =>
|
{[...this.props.config.messengers].map((v, i) => (
|
||||||
<Radio disabled={ this.props.formDisabled } value={v} key={v}>{ v }</Radio>
|
<Radio disabled={this.props.formDisabled} value={v} key={v}>
|
||||||
)}
|
{v}
|
||||||
|
</Radio>
|
||||||
|
))}
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -365,15 +497,26 @@ class TheFormDef extends React.PureComponent {
|
||||||
<Form.Item {...formItemLayout} label="Send later?">
|
<Form.Item {...formItemLayout} label="Send later?">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
{getFieldDecorator("send_later", { defaultChecked: this.props.isSingle })(
|
{getFieldDecorator("send_later", {
|
||||||
<Switch disabled={ this.props.formDisabled }
|
defaultChecked: this.props.isSingle
|
||||||
|
})(
|
||||||
|
<Switch
|
||||||
|
disabled={this.props.formDisabled}
|
||||||
checked={this.state.sendLater}
|
checked={this.state.sendLater}
|
||||||
onChange={ this.handleSendLater } />
|
onChange={this.handleSendLater}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
{this.state.sendLater && getFieldDecorator("send_at",
|
{this.state.sendLater &&
|
||||||
{ initialValue: (record && typeof(record.send_at) === "string") ? moment(record.send_at) : moment(new Date()).add(1, "days").startOf("day") })(
|
getFieldDecorator("send_at", {
|
||||||
|
initialValue:
|
||||||
|
record && typeof record.send_at === "string"
|
||||||
|
? moment(record.send_at)
|
||||||
|
: moment(new Date())
|
||||||
|
.add(1, "days")
|
||||||
|
.startOf("day")
|
||||||
|
})(
|
||||||
<DatePicker
|
<DatePicker
|
||||||
disabled={this.props.formDisabled}
|
disabled={this.props.formDisabled}
|
||||||
showTime
|
showTime
|
||||||
|
@ -385,32 +528,38 @@ class TheFormDef extends React.PureComponent {
|
||||||
</Row>
|
</Row>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
{ this.props.isSingle &&
|
{this.props.isSingle && (
|
||||||
<div>
|
<div>
|
||||||
<hr />
|
<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.">
|
<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")(
|
{getFieldDecorator("subscribers")(
|
||||||
<Select mode="tags" style={{ width: "100%" }}></Select>
|
<Select mode="tags" style={{ width: "100%" }} />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label=" " colon={false}>
|
<Form.Item {...formItemLayout} label=" " colon={false}>
|
||||||
<Button onClick={ this.handleTestCampaign }><Icon type="mail" /> Send test</Button>
|
<Button onClick={this.handleTestCampaign}>
|
||||||
|
<Icon type="mail" /> Send test
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const TheForm = Form.create()(TheFormDef)
|
const TheForm = Form.create()(TheFormDef)
|
||||||
|
|
||||||
|
|
||||||
class Campaign extends React.PureComponent {
|
class Campaign extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
campaignID: this.props.route.match.params ? parseInt(this.props.route.match.params.campaignID, 10) : 0,
|
campaignID: this.props.route.match.params
|
||||||
|
? parseInt(this.props.route.match.params.campaignID, 10)
|
||||||
|
: 0,
|
||||||
record: {},
|
record: {},
|
||||||
formRef: null,
|
formRef: null,
|
||||||
contentType: "richtext",
|
contentType: "richtext",
|
||||||
|
@ -428,7 +577,11 @@ class Campaign extends React.PureComponent {
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||||
|
|
||||||
// Fetch templates.
|
// Fetch templates.
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.GetTemplates, cs.MethodGet)
|
this.props.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.GetTemplates,
|
||||||
|
cs.MethodGet
|
||||||
|
)
|
||||||
|
|
||||||
// Fetch campaign.
|
// Fetch campaign.
|
||||||
if (this.state.campaignID) {
|
if (this.state.campaignID) {
|
||||||
|
@ -443,18 +596,27 @@ class Campaign extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecord = (id) => {
|
fetchRecord = id => {
|
||||||
this.props.request(cs.Routes.GetCampaign, cs.MethodGet, { id: id }).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetCampaign, cs.MethodGet, { id: id })
|
||||||
|
.then(r => {
|
||||||
const record = r.data.data
|
const record = r.data.data
|
||||||
this.setState({ record: record, loading: false })
|
this.setState({ record: record, loading: false })
|
||||||
|
|
||||||
// The form for non draft and scheduled campaigns should be locked.
|
// The form for non draft and scheduled campaigns should be locked.
|
||||||
if(record.status !== cs.CampaignStatusDraft &&
|
if (
|
||||||
record.status !== cs.CampaignStatusScheduled) {
|
record.status !== cs.CampaignStatusDraft &&
|
||||||
|
record.status !== cs.CampaignStatusScheduled
|
||||||
|
) {
|
||||||
this.setState({ formDisabled: true })
|
this.setState({ formDisabled: true })
|
||||||
}
|
}
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,11 +628,11 @@ class Campaign extends React.PureComponent {
|
||||||
this.setState({ mediaVisible: !this.state.mediaVisible })
|
this.setState({ mediaVisible: !this.state.mediaVisible })
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentTab = (tab) => {
|
setCurrentTab = tab => {
|
||||||
this.setState({ currentTab: tab })
|
this.setState({ currentTab: tab })
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreview = (record) => {
|
handlePreview = record => {
|
||||||
this.setState({ previewRecord: record })
|
this.setState({ previewRecord: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,66 +642,95 @@ class Campaign extends React.PureComponent {
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
{!this.state.record.id && <h1>Create a campaign</h1>}
|
{!this.state.record.id && <h1>Create a campaign</h1>}
|
||||||
{ this.state.record.id &&
|
{this.state.record.id && (
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
<Tag color={ cs.CampaignStatusColors[this.state.record.status] }>{ this.state.record.status }</Tag>
|
<Tag
|
||||||
|
color={cs.CampaignStatusColors[this.state.record.status]}
|
||||||
|
>
|
||||||
|
{this.state.record.status}
|
||||||
|
</Tag>
|
||||||
{this.state.record.name}
|
{this.state.record.name}
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-tiny text-grey">ID { this.state.record.id } — UUID { this.state.record.uuid }</span>
|
<span className="text-tiny text-grey">
|
||||||
|
ID {this.state.record.id} — UUID{" "}
|
||||||
|
{this.state.record.uuid}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8} className="right">
|
<Col span={8} className="right">
|
||||||
{ !this.state.formDisabled && !this.state.loading &&
|
{!this.state.formDisabled && !this.state.loading && (
|
||||||
<div>
|
<div>
|
||||||
<Button type="primary" icon="save" onClick={() => {
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="save"
|
||||||
|
onClick={() => {
|
||||||
this.state.formRef.handleSubmit()
|
this.state.formRef.handleSubmit()
|
||||||
}}>{ !this.state.record.id ? "Continue" : "Save changes" }</Button>
|
}}
|
||||||
{" "}
|
>
|
||||||
|
{!this.state.record.id ? "Continue" : "Save changes"}
|
||||||
{ ( this.state.record.status === cs.CampaignStatusDraft && this.state.record.send_at) &&
|
</Button>{" "}
|
||||||
<Popconfirm title="The campaign will start automatically at the scheduled date and time. Schedule now?"
|
{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={() => {
|
onConfirm={() => {
|
||||||
this.state.formRef.handleSubmit(() => {
|
this.state.formRef.handleSubmit(() => {
|
||||||
this.props.route.history.push({
|
this.props.route.history.push({
|
||||||
pathname: cs.Routes.ViewCampaigns,
|
pathname: cs.Routes.ViewCampaigns,
|
||||||
state: { campaign: this.state.record, campaignStatus: cs.CampaignStatusScheduled }
|
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?"
|
}}
|
||||||
|
>
|
||||||
|
<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={() => {
|
onConfirm={() => {
|
||||||
this.state.formRef.handleSubmit(() => {
|
this.state.formRef.handleSubmit(() => {
|
||||||
this.props.route.history.push({
|
this.props.route.history.push({
|
||||||
pathname: cs.Routes.ViewCampaigns,
|
pathname: cs.Routes.ViewCampaigns,
|
||||||
state: { campaign: this.state.record, campaignStatus: cs.CampaignStatusRunning }
|
state: {
|
||||||
})
|
campaign: this.state.record,
|
||||||
})
|
campaignStatus: cs.CampaignStatusRunning
|
||||||
}}>
|
|
||||||
<Button icon="rocket" type="primary">Start campaign</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button icon="rocket" type="primary">
|
||||||
|
Start campaign
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Tabs type="card"
|
<Tabs
|
||||||
|
type="card"
|
||||||
activeKey={this.state.currentTab}
|
activeKey={this.state.currentTab}
|
||||||
onTabClick={ (t) => {
|
onTabClick={t => {
|
||||||
this.setState({ currentTab: t })
|
this.setState({ currentTab: t })
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Tabs.TabPane tab="Campaign" key="form">
|
<Tabs.TabPane tab="Campaign" key="form">
|
||||||
<Spin spinning={this.state.loading}>
|
<Spin spinning={this.state.loading}>
|
||||||
<TheForm { ...this.props }
|
<TheForm
|
||||||
wrappedComponentRef={ (r) => {
|
{...this.props}
|
||||||
|
wrappedComponentRef={r => {
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -549,7 +740,9 @@ class Campaign extends React.PureComponent {
|
||||||
}}
|
}}
|
||||||
record={this.state.record}
|
record={this.state.record}
|
||||||
isSingle={this.state.record.id ? true : false}
|
isSingle={this.state.record.id ? true : false}
|
||||||
body={ this.state.body ? this.state.body : this.state.record.body }
|
body={
|
||||||
|
this.state.body ? this.state.body : this.state.record.body
|
||||||
|
}
|
||||||
contentType={this.state.contentType}
|
contentType={this.state.contentType}
|
||||||
formDisabled={this.state.formDisabled}
|
formDisabled={this.state.formDisabled}
|
||||||
fetchRecord={this.fetchRecord}
|
fetchRecord={this.fetchRecord}
|
||||||
|
@ -557,11 +750,16 @@ class Campaign extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab="Content" disabled={ this.state.record.id ? false : true } key="content">
|
<Tabs.TabPane
|
||||||
{ this.state.record.id &&
|
tab="Content"
|
||||||
|
disabled={this.state.record.id ? false : true}
|
||||||
|
key="content"
|
||||||
|
>
|
||||||
|
{this.state.record.id && (
|
||||||
<div>
|
<div>
|
||||||
<Editor { ...this.props }
|
<Editor
|
||||||
ref={ (r) => {
|
{...this.props}
|
||||||
|
ref={r => {
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -578,38 +776,53 @@ class Campaign extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
<div className="content-actions">
|
<div className="content-actions">
|
||||||
<p>
|
<p>
|
||||||
<Button icon="search" onClick={() => this.handlePreview(this.state.record)}>Preview</Button>
|
<Button
|
||||||
|
icon="search"
|
||||||
|
onClick={() => this.handlePreview(this.state.record)}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{ !this.state.record.id &&
|
{!this.state.record.id && <Spin className="empty-spinner" />}
|
||||||
<Spin className="empty-spinner"></Spin>
|
|
||||||
}
|
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Modal visible={ this.state.mediaVisible } width="900px"
|
<Modal
|
||||||
|
visible={this.state.mediaVisible}
|
||||||
|
width="900px"
|
||||||
title="Media"
|
title="Media"
|
||||||
okText={"Ok"}
|
okText={"Ok"}
|
||||||
onCancel={this.toggleMedia}
|
onCancel={this.toggleMedia}
|
||||||
onOk={ this.toggleMedia }>
|
onOk={this.toggleMedia}
|
||||||
<Media { ...{ ...this.props,
|
>
|
||||||
insertMedia: this.state.editor ? this.state.editor.insertMedia : null,
|
<Media
|
||||||
|
{...{
|
||||||
|
...this.props,
|
||||||
|
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 &&
|
{this.state.previewRecord && (
|
||||||
<ModalPreview
|
<ModalPreview
|
||||||
title={this.state.previewRecord.name}
|
title={this.state.previewRecord.name}
|
||||||
body={this.state.body}
|
body={this.state.body}
|
||||||
previewURL={ cs.Routes.PreviewCampaign.replace(":id", this.state.previewRecord.id) }
|
previewURL={cs.Routes.PreviewCampaign.replace(
|
||||||
|
":id",
|
||||||
|
this.state.previewRecord.id
|
||||||
|
)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ previewRecord: null })
|
this.setState({ previewRecord: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,21 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { Row, Col, Button, Table, Icon, Tooltip, Tag, Popconfirm, Progress, Modal, Select, notification, Input } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
Icon,
|
||||||
|
Tooltip,
|
||||||
|
Tag,
|
||||||
|
Popconfirm,
|
||||||
|
Progress,
|
||||||
|
Modal,
|
||||||
|
notification,
|
||||||
|
Input
|
||||||
|
} from "antd"
|
||||||
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 ModalPreview from "./ModalPreview"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
@ -42,24 +55,26 @@ class Campaigns extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.columns = [{
|
this.columns = [
|
||||||
|
{
|
||||||
title: "Name",
|
title: "Name",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
width: "20%",
|
width: "20%",
|
||||||
vAlign: "top",
|
vAlign: "top",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const out = [];
|
const out = []
|
||||||
out.push(
|
out.push(
|
||||||
<div className="name" key={`name-${record.id}`}>
|
<div className="name" key={`name-${record.id}`}>
|
||||||
<Link to={ `/campaigns/${record.id}` }>{ text }</Link><br />
|
<Link to={`/campaigns/${record.id}`}>{text}</Link>
|
||||||
|
<br />
|
||||||
<span className="text-tiny">{record.subject}</span>
|
<span className="text-tiny">{record.subject}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (record.tags.length > 0) {
|
if (record.tags.length > 0) {
|
||||||
for (let i = 0; i < record.tags.length; i++) {
|
for (let i = 0; i < record.tags.length; i++) {
|
||||||
out.push(<Tag key={`tag-${i}`}>{ record.tags[i] }</Tag>);
|
out.push(<Tag key={`tag-${i}`}>{record.tags[i]}</Tag>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,13 +87,18 @@ class Campaigns extends React.PureComponent {
|
||||||
className: "status",
|
className: "status",
|
||||||
width: "10%",
|
width: "10%",
|
||||||
render: (status, record) => {
|
render: (status, record) => {
|
||||||
let color = cs.CampaignStatusColors.hasOwnProperty(status) ? cs.CampaignStatusColors[status] : ""
|
let color = cs.CampaignStatusColors.hasOwnProperty(status)
|
||||||
|
? cs.CampaignStatusColors[status]
|
||||||
|
: ""
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tag color={color}>{status}</Tag>
|
<Tag color={color}>{status}</Tag>
|
||||||
{record.send_at &&
|
{record.send_at && (
|
||||||
<span className="text-tiny date">Scheduled — { dayjs(record.send_at).format(cs.DateFormat) }</span>
|
<span className="text-tiny date">
|
||||||
}
|
Scheduled —{" "}
|
||||||
|
{dayjs(record.send_at).format(cs.DateFormat)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -91,9 +111,11 @@ class Campaigns extends React.PureComponent {
|
||||||
className: "lists",
|
className: "lists",
|
||||||
render: (lists, record) => {
|
render: (lists, record) => {
|
||||||
const out = []
|
const out = []
|
||||||
lists.forEach((l) => {
|
lists.forEach(l => {
|
||||||
out.push(
|
out.push(
|
||||||
<span className="name" key={`name-${l.id}`}><Link to={ `/subscribers/lists/${l.id}` }>{ l.name }</Link></span>
|
<span className="name" key={`name-${l.id}`}>
|
||||||
|
<Link to={`/subscribers/lists/${l.id}`}>{l.name}</Link>
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -105,7 +127,10 @@ class Campaigns extends React.PureComponent {
|
||||||
className: "stats",
|
className: "stats",
|
||||||
width: "30%",
|
width: "30%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
if(record.status !== cs.CampaignStatusDraft && record.status !== cs.CampaignStatusScheduled) {
|
if (
|
||||||
|
record.status !== cs.CampaignStatusDraft &&
|
||||||
|
record.status !== cs.CampaignStatusScheduled
|
||||||
|
) {
|
||||||
return this.renderStats(record)
|
return this.renderStats(record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,68 +143,145 @@ class Campaigns extends React.PureComponent {
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{ ( record.status === cs.CampaignStatusPaused ) &&
|
{record.status === cs.CampaignStatusPaused && (
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleUpdateStatus(record, cs.CampaignStatusRunning)}>
|
<Popconfirm
|
||||||
<Tooltip title="Resume campaign" placement="bottom"><a role="button"><Icon type="rocket" /></a></Tooltip>
|
title="Are you sure?"
|
||||||
</Popconfirm>
|
onConfirm={() =>
|
||||||
|
this.handleUpdateStatus(record, cs.CampaignStatusRunning)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title="Resume campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="rocket" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
{ ( record.status === cs.CampaignStatusRunning ) &&
|
{record.status === cs.CampaignStatusRunning && (
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleUpdateStatus(record, cs.CampaignStatusPaused)}>
|
<Popconfirm
|
||||||
<Tooltip title="Pause campaign" placement="bottom"><a role="button"><Icon type="pause-circle-o" /></a></Tooltip>
|
title="Are you sure?"
|
||||||
</Popconfirm>
|
onConfirm={() =>
|
||||||
|
this.handleUpdateStatus(record, cs.CampaignStatusPaused)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title="Pause campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="pause-circle-o" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Draft with send_at */}
|
{/* Draft with send_at */}
|
||||||
{ ( record.status === cs.CampaignStatusDraft && record.send_at) &&
|
{record.status === cs.CampaignStatusDraft && record.send_at && (
|
||||||
<Popconfirm title="The campaign will start automatically at the scheduled date and time. Schedule now?" onConfirm={() => this.handleUpdateStatus(record, cs.CampaignStatusScheduled) }>
|
<Popconfirm
|
||||||
<Tooltip title="Schedule campaign" placement="bottom"><a role="button"><Icon type="clock-circle" /></a></Tooltip>
|
title="The campaign will start automatically at the scheduled date and time. Schedule now?"
|
||||||
</Popconfirm>
|
onConfirm={() =>
|
||||||
|
this.handleUpdateStatus(record, cs.CampaignStatusScheduled)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title="Schedule campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="clock-circle" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
{ ( record.status === cs.CampaignStatusDraft && !record.send_at) &&
|
{record.status === cs.CampaignStatusDraft && !record.send_at && (
|
||||||
<Popconfirm title="Campaign properties cannot be changed once it starts. Start now?" onConfirm={() => this.handleUpdateStatus(record, cs.CampaignStatusRunning) }>
|
<Popconfirm
|
||||||
<Tooltip title="Start campaign" placement="bottom"><a role="button"><Icon type="rocket" /></a></Tooltip>
|
title="Campaign properties cannot be changed once it starts. Start now?"
|
||||||
</Popconfirm>
|
onConfirm={() =>
|
||||||
|
this.handleUpdateStatus(record, cs.CampaignStatusRunning)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title="Start campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="rocket" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
{ ( record.status === cs.CampaignStatusPaused || record.status === cs.CampaignStatusRunning) &&
|
{(record.status === cs.CampaignStatusPaused ||
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleUpdateStatus(record, cs.CampaignStatusCancelled)}>
|
record.status === cs.CampaignStatusRunning) && (
|
||||||
<Tooltip title="Cancel campaign" placement="bottom"><a role="button"><Icon type="close-circle-o" /></a></Tooltip>
|
<Popconfirm
|
||||||
</Popconfirm>
|
title="Are you sure?"
|
||||||
|
onConfirm={() =>
|
||||||
|
this.handleUpdateStatus(record, cs.CampaignStatusCancelled)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<Tooltip title="Cancel campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="close-circle-o" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
|
||||||
<Tooltip title="Preview campaign" placement="bottom">
|
<Tooltip title="Preview campaign" placement="bottom">
|
||||||
<a role="button" onClick={() => {
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
this.handlePreview(record)
|
this.handlePreview(record)
|
||||||
}}><Icon type="search" /></a>
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="search" />
|
||||||
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title="Clone campaign" placement="bottom">
|
<Tooltip title="Clone campaign" placement="bottom">
|
||||||
<a role="button" onClick={() => {
|
<a
|
||||||
let r = { ...record, lists: record.lists.map((i) => { return i.id }) }
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
let r = {
|
||||||
|
...record,
|
||||||
|
lists: record.lists.map(i => {
|
||||||
|
return i.id
|
||||||
|
})
|
||||||
|
}
|
||||||
this.handleToggleCloneForm(r)
|
this.handleToggleCloneForm(r)
|
||||||
}}><Icon type="copy" /></a>
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="copy" />
|
||||||
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{ ( record.status === cs.CampaignStatusDraft || record.status === cs.CampaignStatusScheduled ) &&
|
{(record.status === cs.CampaignStatusDraft ||
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleDeleteRecord(record)}>
|
record.status === cs.CampaignStatusScheduled) && (
|
||||||
<Tooltip title="Delete campaign" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.handleDeleteRecord(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete campaign" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
progressPercent(record) {
|
progressPercent(record) {
|
||||||
return Math.round(this.getStatsField("sent", record) / this.getStatsField("to_send", record) * 100, 2)
|
return Math.round(
|
||||||
|
(this.getStatsField("sent", record) /
|
||||||
|
this.getStatsField("to_send", record)) *
|
||||||
|
100,
|
||||||
|
2
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDone(record) {
|
isDone(record) {
|
||||||
return this.getStatsField("status", record) === cs.CampaignStatusFinished ||
|
return (
|
||||||
|
this.getStatsField("status", record) === cs.CampaignStatusFinished ||
|
||||||
this.getStatsField("status", record) === cs.CampaignStatusCancelled
|
this.getStatsField("status", record) === cs.CampaignStatusCancelled
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getStatsField returns a stats field value of a given record if it
|
// getStatsField returns a stats field value of a given record if it
|
||||||
|
@ -192,8 +294,10 @@ class Campaigns extends React.PureComponent {
|
||||||
return record[field]
|
return record[field]
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStats = (record) => {
|
renderStats = record => {
|
||||||
let color = cs.CampaignStatusColors.hasOwnProperty(record.status) ? cs.CampaignStatusColors[record.status] : ""
|
let color = cs.CampaignStatusColors.hasOwnProperty(record.status)
|
||||||
|
? cs.CampaignStatusColors[record.status]
|
||||||
|
: ""
|
||||||
const startedAt = this.getStatsField("started_at", record)
|
const startedAt = this.getStatsField("started_at", record)
|
||||||
const updatedAt = this.getStatsField("updated_at", record)
|
const updatedAt = this.getStatsField("updated_at", record)
|
||||||
const sent = this.getStatsField("sent", record)
|
const sent = this.getStatsField("sent", record)
|
||||||
|
@ -205,45 +309,87 @@ class Campaigns extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ !isDone &&
|
{!isDone && (
|
||||||
<Progress strokeColor={ color } status="active"
|
<Progress
|
||||||
type="line" percent={ this.progressPercent(record) } />
|
strokeColor={color}
|
||||||
}
|
status="active"
|
||||||
<Row><Col className="label" span={10}>Sent</Col><Col span={12}>
|
type="line"
|
||||||
{ sent >= toSend &&
|
percent={this.progressPercent(record)}
|
||||||
<span>{ toSend }</span>
|
/>
|
||||||
}
|
)}
|
||||||
{ sent < toSend &&
|
<Row>
|
||||||
<span>{ sent } / { toSend }</span>
|
<Col className="label" span={10}>
|
||||||
}
|
Sent
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
{sent >= toSend && <span>{toSend}</span>}
|
||||||
|
{sent < toSend && (
|
||||||
|
<span>
|
||||||
|
{sent} / {toSend}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
{ record.status === cs.CampaignStatusRunning &&
|
{record.status === cs.CampaignStatusRunning && (
|
||||||
<Icon type="loading" style={{ fontSize: 12 }} spin />
|
<Icon type="loading" style={{ fontSize: 12 }} spin />
|
||||||
}
|
)}
|
||||||
</Col></Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{ rate > 0 &&
|
{rate > 0 && (
|
||||||
<Row><Col className="label" span={10}>Rate</Col><Col span={12}>{ Math.round(rate, 2) } / min</Col></Row>
|
<Row>
|
||||||
}
|
<Col className="label" span={10}>
|
||||||
|
Rate
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>{Math.round(rate, 2)} / min</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
|
||||||
<Row><Col className="label" span={10}>Views</Col><Col span={12}>{ record.views }</Col></Row>
|
<Row>
|
||||||
<Row><Col className="label" span={10}>Clicks</Col><Col span={12}>{ record.clicks }</Col></Row>
|
<Col className="label" span={10}>
|
||||||
|
Views
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>{record.views}</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col className="label" span={10}>
|
||||||
|
Clicks
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>{record.clicks}</Col>
|
||||||
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
<Row><Col className="label" span={10}>Created</Col><Col span={12}>{ dayjs(record.created_at).format(cs.DateFormat) }</Col></Row>
|
<Row>
|
||||||
|
<Col className="label" span={10}>
|
||||||
|
Created
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>{dayjs(record.created_at).format(cs.DateFormat)}</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
{ startedAt &&
|
{startedAt && (
|
||||||
<Row><Col className="label" span={10}>Started</Col><Col span={12}>{ dayjs(startedAt).format(cs.DateFormat) }</Col></Row>
|
<Row>
|
||||||
}
|
<Col className="label" span={10}>
|
||||||
{ isDone &&
|
Started
|
||||||
<Row><Col className="label" span={10}>Ended</Col><Col span={12}>
|
</Col>
|
||||||
{ dayjs(updatedAt).format(cs.DateFormat) }
|
<Col span={12}>{dayjs(startedAt).format(cs.DateFormat)}</Col>
|
||||||
</Col></Row>
|
</Row>
|
||||||
}
|
)}
|
||||||
{ startedAt && updatedAt &&
|
{isDone && (
|
||||||
<Row><Col className="label" span={10}>Duration</Col><Col className="duration" span={12}>
|
<Row>
|
||||||
|
<Col className="label" span={10}>
|
||||||
|
Ended
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>{dayjs(updatedAt).format(cs.DateFormat)}</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{startedAt && updatedAt && (
|
||||||
|
<Row>
|
||||||
|
<Col className="label" span={10}>
|
||||||
|
Duration
|
||||||
|
</Col>
|
||||||
|
<Col className="duration" span={12}>
|
||||||
{dayjs(updatedAt).from(dayjs(startedAt), true)}
|
{dayjs(updatedAt).from(dayjs(startedAt), true)}
|
||||||
</Col></Row>
|
</Col>
|
||||||
}
|
</Row>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -268,7 +414,7 @@ class Campaigns extends React.PureComponent {
|
||||||
window.clearInterval(this.state.pollID)
|
window.clearInterval(this.state.pollID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecords = (params) => {
|
fetchRecords = params => {
|
||||||
let qParams = {
|
let qParams = {
|
||||||
page: this.state.queryParams.page,
|
page: this.state.queryParams.page,
|
||||||
per_page: this.state.queryParams.per_page
|
per_page: this.state.queryParams.per_page
|
||||||
|
@ -283,18 +429,25 @@ class Campaigns extends React.PureComponent {
|
||||||
qParams = { ...qParams, ...params }
|
qParams = { ...qParams, ...params }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.GetCampaigns, cs.MethodGet, qParams).then((r) => {
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.GetCampaigns,
|
||||||
|
cs.MethodGet,
|
||||||
|
qParams
|
||||||
|
)
|
||||||
|
.then(r => {
|
||||||
this.startStatsPoll()
|
this.startStatsPoll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
startStatsPoll = () => {
|
startStatsPoll = () => {
|
||||||
window.clearInterval(this.state.pollID)
|
window.clearInterval(this.state.pollID)
|
||||||
this.setState({ "stats": {} })
|
this.setState({ stats: {} })
|
||||||
|
|
||||||
// If there's at least one running campaign, start polling.
|
// If there's at least one running campaign, start polling.
|
||||||
let hasRunning = false
|
let hasRunning = false
|
||||||
this.props.data[cs.ModelCampaigns].forEach((c) => {
|
this.props.data[cs.ModelCampaigns].forEach(c => {
|
||||||
if (c.status === cs.CampaignStatusRunning) {
|
if (c.status === cs.CampaignStatusRunning) {
|
||||||
hasRunning = true
|
hasRunning = true
|
||||||
return
|
return
|
||||||
|
@ -307,7 +460,9 @@ class Campaigns extends React.PureComponent {
|
||||||
|
|
||||||
// Poll for campaign stats.
|
// Poll for campaign stats.
|
||||||
let pollID = window.setInterval(() => {
|
let pollID = window.setInterval(() => {
|
||||||
this.props.request(cs.Routes.GetRunningCampaignStats, cs.MethodGet).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetRunningCampaignStats, cs.MethodGet)
|
||||||
|
.then(r => {
|
||||||
// No more running campaigns.
|
// No more running campaigns.
|
||||||
if (r.data.data.length === 0) {
|
if (r.data.data.length === 0) {
|
||||||
window.clearInterval(this.state.pollID)
|
window.clearInterval(this.state.pollID)
|
||||||
|
@ -316,12 +471,13 @@ class Campaigns extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
let stats = {}
|
let stats = {}
|
||||||
r.data.data.forEach((s) => {
|
r.data.data.forEach(s => {
|
||||||
stats[s.id] = s
|
stats[s.id] = s
|
||||||
})
|
})
|
||||||
|
|
||||||
this.setState({ stats: stats })
|
this.setState({ stats: stats })
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
console.log(e.message)
|
console.log(e.message)
|
||||||
})
|
})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
@ -330,49 +486,99 @@ class Campaigns extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateStatus = (record, status) => {
|
handleUpdateStatus = (record, status) => {
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.UpdateCampaignStatus,
|
||||||
|
cs.MethodPut,
|
||||||
|
{ id: record.id, status: status }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: `Campaign ${status}`, description: `"${record.name}" ${status}` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: `Campaign ${status}`,
|
||||||
|
description: `"${record.name}" ${status}`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecord = (record) => {
|
handleDeleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.DeleteCampaign,
|
||||||
|
cs.MethodDelete,
|
||||||
|
{ id: record.id }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Campaign deleted", description: `"${record.name}" deleted` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Campaign deleted",
|
||||||
|
description: `"${record.name}" deleted`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleToggleCloneForm = (record) => {
|
handleToggleCloneForm = record => {
|
||||||
this.setState({ cloneModalVisible: !this.state.cloneModalVisible, record: record, cloneName: record.name })
|
this.setState({
|
||||||
|
cloneModalVisible: !this.state.cloneModalVisible,
|
||||||
|
record: record,
|
||||||
|
cloneName: record.name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCloneCampaign = (record) => {
|
handleCloneCampaign = record => {
|
||||||
this.setState({ modalWaiting: true })
|
this.setState({ modalWaiting: true })
|
||||||
this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record).then((resp) => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition,
|
.modelRequest(
|
||||||
|
cs.ModelCampaigns,
|
||||||
|
cs.Routes.CreateCampaign,
|
||||||
|
cs.MethodPost,
|
||||||
|
record
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Campaign created",
|
message: "Campaign created",
|
||||||
description: `${record.name} created` })
|
description: `${record.name} created`
|
||||||
|
})
|
||||||
|
|
||||||
this.setState({ record: null, modalWaiting: false })
|
this.setState({ record: null, modalWaiting: false })
|
||||||
this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id))
|
this.props.route.history.push(
|
||||||
}).catch(e => {
|
cs.Routes.ViewCampaign.replace(":id", resp.data.data.id)
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreview = (record) => {
|
handlePreview = record => {
|
||||||
this.setState({ previewRecord: record })
|
this.setState({ previewRecord: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,9 +591,15 @@ class Campaigns extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<section className="content campaigns">
|
<section className="content campaigns">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={ 22 }><h1>Campaigns</h1></Col>
|
<Col span={22}>
|
||||||
|
<h1>Campaigns</h1>
|
||||||
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Link to="/campaigns/new"><Button type="primary" icon="plus" role="link">New campaign</Button></Link>
|
<Link to="/campaigns/new">
|
||||||
|
<Button type="primary" icon="plus" role="link">
|
||||||
|
New campaign
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
|
@ -401,28 +613,45 @@ class Campaigns extends React.PureComponent {
|
||||||
pagination={pagination}
|
pagination={pagination}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ this.state.previewRecord &&
|
{this.state.previewRecord && (
|
||||||
<ModalPreview
|
<ModalPreview
|
||||||
title={this.state.previewRecord.name}
|
title={this.state.previewRecord.name}
|
||||||
previewURL={ cs.Routes.PreviewCampaign.replace(":id", this.state.previewRecord.id) }
|
previewURL={cs.Routes.PreviewCampaign.replace(
|
||||||
|
":id",
|
||||||
|
this.state.previewRecord.id
|
||||||
|
)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ previewRecord: null })
|
this.setState({ previewRecord: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{ this.state.cloneModalVisible && this.state.record &&
|
{this.state.cloneModalVisible && this.state.record && (
|
||||||
<Modal visible={ this.state.record !== null } width="500px"
|
<Modal
|
||||||
|
visible={this.state.record !== null}
|
||||||
|
width="500px"
|
||||||
className="clone-campaign-modal"
|
className="clone-campaign-modal"
|
||||||
title={"Clone " + this.state.record.name}
|
title={"Clone " + this.state.record.name}
|
||||||
okText="Clone"
|
okText="Clone"
|
||||||
confirmLoading={this.state.modalWaiting}
|
confirmLoading={this.state.modalWaiting}
|
||||||
onCancel={this.handleToggleCloneForm}
|
onCancel={this.handleToggleCloneForm}
|
||||||
onOk={() => { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }}>
|
onOk={() => {
|
||||||
<Input autoFocus defaultValue={ this.state.record.name } style={{ width: "100%" }} onChange={(e) => {
|
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 })
|
this.setState({ cloneName: e.target.value })
|
||||||
}} />
|
}}
|
||||||
</Modal> }
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Col, Row, notification, Card, Tooltip, Icon, Spin } from "antd"
|
import { Col, Row, notification, Card, Spin } from "antd"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
import { Chart, Axis, Geom, Tooltip as BizTooltip } from 'bizcharts';
|
import { Chart, Geom, Tooltip as BizTooltip } from "bizcharts"
|
||||||
|
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
|
@ -10,13 +10,23 @@ class Dashboard extends React.PureComponent {
|
||||||
loading: true
|
loading: true
|
||||||
}
|
}
|
||||||
|
|
||||||
campaignTypes = ["running", "finished", "paused", "draft", "scheduled", "cancelled"]
|
campaignTypes = [
|
||||||
|
"running",
|
||||||
|
"finished",
|
||||||
|
"paused",
|
||||||
|
"draft",
|
||||||
|
"scheduled",
|
||||||
|
"cancelled"
|
||||||
|
]
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
this.props.pageTitle("Dashboard")
|
this.props.pageTitle("Dashboard")
|
||||||
this.props.request(cs.Routes.GetDashboarcStats, cs.MethodGet).then((resp) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetDashboarcStats, cs.MethodGet)
|
||||||
|
.then(resp => {
|
||||||
this.setState({ stats: resp.data.data, loading: false })
|
this.setState({ stats: resp.data.data, loading: false })
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
})
|
})
|
||||||
|
@ -32,24 +42,32 @@ class Dashboard extends React.PureComponent {
|
||||||
<h1>Welcome</h1>
|
<h1>Welcome</h1>
|
||||||
<hr />
|
<hr />
|
||||||
<Spin spinning={this.state.loading}>
|
<Spin spinning={this.state.loading}>
|
||||||
{ this.state.stats &&
|
{this.state.stats && (
|
||||||
<div className="stats">
|
<div className="stats">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card title="Active subscribers" bordered={false}>
|
<Card title="Active subscribers" bordered={false}>
|
||||||
<h1 className="count">{ this.orZero(this.state.stats.subscribers.enabled) }</h1>
|
<h1 className="count">
|
||||||
|
{this.orZero(this.state.stats.subscribers.enabled)}
|
||||||
|
</h1>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card title="Blacklisted subscribers" bordered={false}>
|
<Card title="Blacklisted subscribers" bordered={false}>
|
||||||
<h1 className="count">{ this.orZero(this.state.stats.subscribers.blacklisted) }</h1>
|
<h1 className="count">
|
||||||
|
{this.orZero(
|
||||||
|
this.state.stats.subscribers.blacklisted
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
<Card title="Orphaned subscribers" bordered={false}>
|
<Card title="Orphaned subscribers" bordered={false}>
|
||||||
<h1 className="count">{ this.orZero(this.state.stats.orphan_subscribers) }</h1>
|
<h1 className="count">
|
||||||
|
{this.orZero(this.state.stats.orphan_subscribers)}
|
||||||
|
</h1>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -58,12 +76,16 @@ class Dashboard extends React.PureComponent {
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title="Public lists" bordered={false}>
|
<Card title="Public lists" bordered={false}>
|
||||||
<h1 className="count">{ this.orZero(this.state.stats.lists.public) }</h1>
|
<h1 className="count">
|
||||||
|
{this.orZero(this.state.stats.lists.public)}
|
||||||
|
</h1>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title="Private lists" bordered={false}>
|
<Card title="Private lists" bordered={false}>
|
||||||
<h1 className="count">{ this.orZero(this.state.stats.lists.private) }</h1>
|
<h1 className="count">
|
||||||
|
{this.orZero(this.state.stats.lists.private)}
|
||||||
|
</h1>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -74,30 +96,60 @@ class Dashboard extends React.PureComponent {
|
||||||
<Col span={16}>
|
<Col span={16}>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title="Campaign views (last 3 months)" bordered={ false }>
|
<Card
|
||||||
|
title="Campaign views (last 3 months)"
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
<h1 className="count">
|
<h1 className="count">
|
||||||
{ this.state.stats.campaign_views.reduce((total, v) => total + v.count, 0) }
|
{this.state.stats.campaign_views.reduce(
|
||||||
{' '}
|
(total, v) => total + v.count,
|
||||||
|
0
|
||||||
|
)}{" "}
|
||||||
views
|
views
|
||||||
</h1>
|
</h1>
|
||||||
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.campaign_views } forceFit>
|
<Chart
|
||||||
|
height={220}
|
||||||
|
padding={[0, 0, 0, 0]}
|
||||||
|
data={this.state.stats.campaign_views}
|
||||||
|
forceFit
|
||||||
|
>
|
||||||
<BizTooltip crosshairs={{ type: "y" }} />
|
<BizTooltip crosshairs={{ type: "y" }} />
|
||||||
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
<Geom
|
||||||
<Geom type='point' position="date*count" size={ 0 } />
|
type="area"
|
||||||
|
position="date*count"
|
||||||
|
size={0}
|
||||||
|
color="#7f2aff"
|
||||||
|
/>
|
||||||
|
<Geom type="point" position="date*count" size={0} />
|
||||||
</Chart>
|
</Chart>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title="Link clicks (last 3 months)" bordered={ false }>
|
<Card
|
||||||
|
title="Link clicks (last 3 months)"
|
||||||
|
bordered={false}
|
||||||
|
>
|
||||||
<h1 className="count">
|
<h1 className="count">
|
||||||
{ this.state.stats.link_clicks.reduce((total, v) => total + v.count, 0) }
|
{this.state.stats.link_clicks.reduce(
|
||||||
{' '}
|
(total, v) => total + v.count,
|
||||||
|
0
|
||||||
|
)}{" "}
|
||||||
clicks
|
clicks
|
||||||
</h1>
|
</h1>
|
||||||
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.link_clicks } forceFit>
|
<Chart
|
||||||
|
height={220}
|
||||||
|
padding={[0, 0, 0, 0]}
|
||||||
|
data={this.state.stats.link_clicks}
|
||||||
|
forceFit
|
||||||
|
>
|
||||||
<BizTooltip crosshairs={{ type: "y" }} />
|
<BizTooltip crosshairs={{ type: "y" }} />
|
||||||
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
<Geom
|
||||||
<Geom type='point' position="date*count" size={ 0 } />
|
type="area"
|
||||||
|
position="date*count"
|
||||||
|
size={0}
|
||||||
|
color="#7f2aff"
|
||||||
|
/>
|
||||||
|
<Geom type="point" position="date*count" size={0} />
|
||||||
</Chart>
|
</Chart>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -105,27 +157,34 @@ class Dashboard extends React.PureComponent {
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={6} offset={2}>
|
<Col span={6} offset={2}>
|
||||||
<Card title="Campaigns" bordered={ false } className="campaign-counts">
|
<Card
|
||||||
{ this.campaignTypes.map((key) =>
|
title="Campaigns"
|
||||||
|
bordered={false}
|
||||||
|
className="campaign-counts"
|
||||||
|
>
|
||||||
|
{this.campaignTypes.map(key => (
|
||||||
<Row key={`stats-campaigns-${key}`}>
|
<Row key={`stats-campaigns-${key}`}>
|
||||||
<Col span={ 18 }><h1 className="name">{ key }</h1></Col>
|
<Col span={18}>
|
||||||
|
<h1 className="name">{key}</h1>
|
||||||
|
</Col>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<h1 className="count">
|
<h1 className="count">
|
||||||
{ this.state.stats.campaigns.hasOwnProperty(key) ?
|
{this.state.stats.campaigns.hasOwnProperty(key)
|
||||||
this.state.stats.campaigns[key] : 0 }
|
? this.state.stats.campaigns[key]
|
||||||
|
: 0}
|
||||||
</h1>
|
</h1>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</section>
|
</section>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
ReactDOM.render(<App />, div);
|
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
|
||||||
});
|
|
|
@ -1,5 +1,20 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Row, Col, Form, Select, Input, Upload, Button, Radio, Icon, Spin, Progress, Popconfirm, Tag, notification } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Form,
|
||||||
|
Select,
|
||||||
|
Input,
|
||||||
|
Upload,
|
||||||
|
Button,
|
||||||
|
Radio,
|
||||||
|
Icon,
|
||||||
|
Spin,
|
||||||
|
Progress,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
notification
|
||||||
|
} from "antd"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
const StatusNone = "none"
|
const StatusNone = "none"
|
||||||
|
@ -22,9 +37,10 @@ class TheFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
handleSubmit = (e) => {
|
handleSubmit = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
var err = null, values = {}
|
var err = null,
|
||||||
|
values = {}
|
||||||
this.props.form.validateFields((e, v) => {
|
this.props.form.validateFields((e, v) => {
|
||||||
err = e
|
err = e
|
||||||
values = v
|
values = v
|
||||||
|
@ -34,9 +50,11 @@ class TheFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.fileList.length < 1) {
|
if (this.state.fileList.length < 1) {
|
||||||
notification["error"]({ placement: cs.MsgPosition,
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Error",
|
message: "Error",
|
||||||
description: "Select a valid file to upload" })
|
description: "Select a valid file to upload"
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,24 +62,33 @@ class TheFormDef extends React.PureComponent {
|
||||||
let params = new FormData()
|
let params = new FormData()
|
||||||
params.set("params", JSON.stringify(values))
|
params.set("params", JSON.stringify(values))
|
||||||
params.append("file", this.state.fileList[0])
|
params.append("file", this.state.fileList[0])
|
||||||
this.props.request(cs.Routes.UploadRouteImport, cs.MethodPost, params).then(() => {
|
this.props
|
||||||
notification["info"]({ placement: cs.MsgPosition,
|
.request(cs.Routes.UploadRouteImport, cs.MethodPost, params)
|
||||||
|
.then(() => {
|
||||||
|
notification["info"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "File uploaded",
|
message: "File uploaded",
|
||||||
description: "Please wait while the import is running" })
|
description: "Please wait while the import is running"
|
||||||
|
})
|
||||||
this.props.fetchimportState()
|
this.props.fetchimportState()
|
||||||
this.setState({ formLoading: false })
|
this.setState({ formLoading: false })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ formLoading: false })
|
this.setState({ formLoading: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmBlur = (e) => {
|
handleConfirmBlur = e => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileChange = (f) => {
|
onFileChange = f => {
|
||||||
let fileList = [f]
|
let fileList = [f]
|
||||||
this.setState({ fileList })
|
this.setState({ fileList })
|
||||||
return false
|
return false
|
||||||
|
@ -83,64 +110,95 @@ class TheFormDef extends React.PureComponent {
|
||||||
<Spin spinning={this.state.formLoading}>
|
<Spin spinning={this.state.formLoading}>
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
<Form.Item {...formItemLayout} label="Mode">
|
<Form.Item {...formItemLayout} label="Mode">
|
||||||
{getFieldDecorator("mode", { rules: [{ required: true }], initialValue: "subscribe" })(
|
{getFieldDecorator("mode", {
|
||||||
<Radio.Group className="mode" onChange={(e) => { this.setState({ mode: e.target.value }) }}>
|
rules: [{ required: true }],
|
||||||
<Radio disabled={ this.props.formDisabled } value="subscribe">Subscribe</Radio>
|
initialValue: "subscribe"
|
||||||
<Radio disabled={ this.props.formDisabled } value="blacklist">Blacklist</Radio>
|
})(
|
||||||
|
<Radio.Group
|
||||||
|
className="mode"
|
||||||
|
onChange={e => {
|
||||||
|
this.setState({ mode: e.target.value })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Radio disabled={this.props.formDisabled} value="subscribe">
|
||||||
|
Subscribe
|
||||||
|
</Radio>
|
||||||
|
<Radio disabled={this.props.formDisabled} value="blacklist">
|
||||||
|
Blacklist
|
||||||
|
</Radio>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{ this.state.mode === "subscribe" &&
|
{this.state.mode === "subscribe" && (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to">
|
<Form.Item
|
||||||
|
{...formItemLayout}
|
||||||
|
label="Lists"
|
||||||
|
extra="Lists to subscribe to"
|
||||||
|
>
|
||||||
{getFieldDecorator("lists", { rules: [{ required: true }] })(
|
{getFieldDecorator("lists", { rules: [{ required: true }] })(
|
||||||
<Select mode="multiple">
|
<Select mode="multiple">
|
||||||
{[...this.props.lists].map((v, i) =>
|
{[...this.props.lists].map((v, i) => (
|
||||||
<Select.Option value={v["id"]} key={v["id"]}>{v["name"]}</Select.Option>
|
<Select.Option value={v["id"]} key={v["id"]}>
|
||||||
)}
|
{v["name"]}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
)}
|
||||||
{ this.state.mode === "blacklist" &&
|
{this.state.mode === "blacklist" && (
|
||||||
<Form.Item {...formItemTailLayout}>
|
<Form.Item {...formItemTailLayout}>
|
||||||
<p className="ant-form-extra">
|
<p className="ant-form-extra">
|
||||||
All existing subscribers found in the import will be marked as 'blacklisted' and will be
|
All existing subscribers found in the import will be marked as
|
||||||
unsubscribed from their existing subscriptions. New subscribers will be imported and marked as 'blacklisted'.
|
'blacklisted' and will be unsubscribed from their existing
|
||||||
|
subscriptions. New subscribers will be imported and marked as
|
||||||
|
'blacklisted'.
|
||||||
</p>
|
</p>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
)}
|
||||||
<Form.Item {...formItemLayout} label="CSV column delimiter" extra="Default delimiter is comma">
|
<Form.Item
|
||||||
|
{...formItemLayout}
|
||||||
|
label="CSV column delimiter"
|
||||||
|
extra="Default delimiter is comma"
|
||||||
|
>
|
||||||
{getFieldDecorator("delim", {
|
{getFieldDecorator("delim", {
|
||||||
initialValue: ","
|
initialValue: ","
|
||||||
})(<Input maxLength="1" style={{ maxWidth: 40 }} />)}
|
})(<Input maxLength="1" style={{ maxWidth: 40 }} />)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item {...formItemLayout} label="CSV or ZIP file">
|
||||||
{...formItemLayout}
|
|
||||||
label="CSV or ZIP file">
|
|
||||||
<div className="dropbox">
|
<div className="dropbox">
|
||||||
{getFieldDecorator("file", {
|
{getFieldDecorator("file", {
|
||||||
valuePropName: "file",
|
valuePropName: "file",
|
||||||
getValueFromEvent: this.normFile,
|
getValueFromEvent: this.normFile,
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(
|
})(
|
||||||
<Upload.Dragger name="files"
|
<Upload.Dragger
|
||||||
|
name="files"
|
||||||
multiple={false}
|
multiple={false}
|
||||||
fileList={this.state.fileList}
|
fileList={this.state.fileList}
|
||||||
beforeUpload={this.onFileChange}
|
beforeUpload={this.onFileChange}
|
||||||
accept=".zip,.csv">
|
accept=".zip,.csv"
|
||||||
|
>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<Icon type="inbox" />
|
<Icon type="inbox" />
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">Click or drag a CSV or ZIP file here</p>
|
<p className="ant-upload-text">
|
||||||
|
Click or drag a CSV or ZIP file here
|
||||||
|
</p>
|
||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemTailLayout}>
|
<Form.Item {...formItemTailLayout}>
|
||||||
<p className="ant-form-extra">For existing subscribers, the names and attributes will be overwritten with the values in the CSV.</p>
|
<p className="ant-form-extra">
|
||||||
<Button type="primary" htmlType="submit"><Icon type="upload" /> Upload</Button>
|
For existing subscribers, the names and attributes will be
|
||||||
|
overwritten with the values in the CSV.
|
||||||
|
</p>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
<Icon type="upload" /> Upload
|
||||||
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
@ -157,10 +215,17 @@ class Importing extends React.PureComponent {
|
||||||
|
|
||||||
stopImport = () => {
|
stopImport = () => {
|
||||||
// Get the import status.
|
// Get the import status.
|
||||||
this.props.request(cs.Routes.UploadRouteImport, cs.MethodDelete).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.UploadRouteImport, cs.MethodDelete)
|
||||||
|
.then(r => {
|
||||||
this.props.fetchimportState()
|
this.props.fetchimportState()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,8 +234,10 @@ class Importing extends React.PureComponent {
|
||||||
let pollID = window.setInterval(() => {
|
let pollID = window.setInterval(() => {
|
||||||
this.props.fetchimportState()
|
this.props.fetchimportState()
|
||||||
this.fetchLogs()
|
this.fetchLogs()
|
||||||
if( this.props.importState.status === StatusFinished ||
|
if (
|
||||||
this.props.importState.status === StatusFailed ) {
|
this.props.importState.status === StatusFinished ||
|
||||||
|
this.props.importState.status === StatusFailed
|
||||||
|
) {
|
||||||
window.clearInterval(this.state.pollID)
|
window.clearInterval(this.state.pollID)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
@ -182,12 +249,19 @@ class Importing extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchLogs() {
|
fetchLogs() {
|
||||||
this.props.request(cs.Routes.GetRouteImportLogs, cs.MethodGet).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetRouteImportLogs, cs.MethodGet)
|
||||||
|
.then(r => {
|
||||||
this.setState({ logs: r.data.data })
|
this.setState({ logs: r.data.data })
|
||||||
let t = document.querySelector("#log-textarea")
|
let t = document.querySelector("#log-textarea")
|
||||||
t.scrollTop = t.scrollHeight;
|
t.scrollTop = t.scrollHeight
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,19 +270,23 @@ class Importing extends React.PureComponent {
|
||||||
if (this.props.importState.status === StatusFinished) {
|
if (this.props.importState.status === StatusFinished) {
|
||||||
progressPercent = 100
|
progressPercent = 100
|
||||||
} else {
|
} else {
|
||||||
progressPercent = Math.floor(this.props.importState.imported / this.props.importState.total * 100)
|
progressPercent = Math.floor(
|
||||||
|
(this.props.importState.imported / this.props.importState.total) * 100
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="content import">
|
<section className="content import">
|
||||||
<h1>Importing — {this.props.importState.name}</h1>
|
<h1>Importing — {this.props.importState.name}</h1>
|
||||||
{ this.props.importState.status === StatusImporting &&
|
{this.props.importState.status === StatusImporting && (
|
||||||
<p>Import is in progress. It is safe to navigate away from this page.</p>
|
<p>
|
||||||
}
|
Import is in progress. It is safe to navigate away from this page.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{ this.props.importState.status !== StatusImporting &&
|
{this.props.importState.status !== StatusImporting && (
|
||||||
<p>Import has finished.</p>
|
<p>Import has finished.</p>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Row className="import-container">
|
<Row className="import-container">
|
||||||
<Col span="10" offset="3">
|
<Col span="10" offset="3">
|
||||||
|
@ -221,43 +299,54 @@ class Importing extends React.PureComponent {
|
||||||
<h3>{this.props.importState.imported} records</h3>
|
<h3>{this.props.importState.imported} records</h3>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
{ this.props.importState.status === StatusImporting &&
|
{this.props.importState.status === StatusImporting && (
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.stopImport()}>
|
<Popconfirm
|
||||||
<p><Icon type="loading" /></p>
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.stopImport()}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
<Icon type="loading" />
|
||||||
|
</p>
|
||||||
<Button type="primary">Stop import</Button>
|
<Button type="primary">Stop import</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
}
|
)}
|
||||||
{ this.props.importState.status === StatusStopping &&
|
{this.props.importState.status === StatusStopping && (
|
||||||
<div>
|
<div>
|
||||||
<p><Icon type="loading" /></p>
|
<p>
|
||||||
|
<Icon type="loading" />
|
||||||
|
</p>
|
||||||
<h4>Stopping</h4>
|
<h4>Stopping</h4>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{this.props.importState.status !== StatusImporting &&
|
{this.props.importState.status !== StatusImporting &&
|
||||||
this.props.importState.status !== StatusStopping &&
|
this.props.importState.status !== StatusStopping && (
|
||||||
<div>
|
<div>
|
||||||
{ this.props.importState.status !== StatusFinished &&
|
{this.props.importState.status !== StatusFinished && (
|
||||||
<div>
|
<div>
|
||||||
<Tag color="red">{this.props.importState.status}</Tag>
|
<Tag color="red">{this.props.importState.status}</Tag>
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
<Button type="primary" onClick={() => this.stopImport()}>Done</Button>
|
<Button type="primary" onClick={() => this.stopImport()}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="logs">
|
<div className="logs">
|
||||||
<h3>Import log</h3>
|
<h3>Import log</h3>
|
||||||
<Spin spinning={this.state.logs === ""}>
|
<Spin spinning={this.state.logs === ""}>
|
||||||
<Input.TextArea placeholder="Import logs"
|
<Input.TextArea
|
||||||
|
placeholder="Import logs"
|
||||||
id="log-textarea"
|
id="log-textarea"
|
||||||
rows={10}
|
rows={10}
|
||||||
value={this.state.logs}
|
value={this.state.logs}
|
||||||
autosize={{ minRows: 2, maxRows: 10 }} />
|
autosize={{ minRows: 2, maxRows: 10 }}
|
||||||
|
/>
|
||||||
</Spin>
|
</Spin>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -269,15 +358,22 @@ class Importing extends React.PureComponent {
|
||||||
|
|
||||||
class Import extends React.PureComponent {
|
class Import extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
importState: { "status": "" }
|
importState: { status: "" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchimportState = () => {
|
fetchimportState = () => {
|
||||||
// Get the import status.
|
// Get the import status.
|
||||||
this.props.request(cs.Routes.GetRouteImportStats, cs.MethodGet).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetRouteImportStats, cs.MethodGet)
|
||||||
|
.then(r => {
|
||||||
this.setState({ importState: r.data.data })
|
this.setState({ importState: r.data.data })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,33 +391,38 @@ class Import extends React.PureComponent {
|
||||||
)
|
)
|
||||||
} else if (this.state.importState.status !== StatusNone) {
|
} else if (this.state.importState.status !== StatusNone) {
|
||||||
// There's an import state
|
// There's an import state
|
||||||
return <Importing { ...this.props }
|
return (
|
||||||
|
<Importing
|
||||||
|
{...this.props}
|
||||||
importState={this.state.importState}
|
importState={this.state.importState}
|
||||||
fetchimportState={ this.fetchimportState } />
|
fetchimportState={this.fetchimportState}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="content import">
|
<section className="content import">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={22}><h1>Import subscribers</h1></Col>
|
<Col span={22}>
|
||||||
<Col span={2}>
|
<h1>Import subscribers</h1>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={2} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<TheForm { ...this.props }
|
<TheForm
|
||||||
|
{...this.props}
|
||||||
fetchimportState={this.fetchimportState}
|
fetchimportState={this.fetchimportState}
|
||||||
lists={ this.props.data[cs.ModelLists] }>
|
lists={this.props.data[cs.ModelLists]}
|
||||||
</TheForm>
|
/>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<div className="help">
|
<div className="help">
|
||||||
<h2>Instructions</h2>
|
<h2>Instructions</h2>
|
||||||
<p>Upload a CSV file or a ZIP file with a single CSV file in it
|
<p>
|
||||||
to bulk import subscribers.
|
Upload a CSV file or a ZIP file with a single CSV file in it to bulk
|
||||||
{" "}
|
import subscribers. The CSV file should have the following headers
|
||||||
The CSV file should have the following headers with the exact column names.
|
with the exact column names. <code>attributes</code> (optional)
|
||||||
{" "}
|
should be a valid JSON string with double escaped quotes.
|
||||||
<code>attributes</code> (optional) should be a valid JSON string with double escaped quotes.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<blockquote className="csv-example">
|
<blockquote className="csv-example">
|
||||||
|
@ -351,7 +452,9 @@ class Import extends React.PureComponent {
|
||||||
<span>user2@mail.com,</span>
|
<span>user2@mail.com,</span>
|
||||||
<span>"User Two",</span>
|
<span>"User Two",</span>
|
||||||
<span>blacklisted,</span>
|
<span>blacklisted,</span>
|
||||||
<span>{ '"{""age"": 25, ""occupation"": ""Time Traveller""}"' }</span>
|
<span>
|
||||||
|
{'"{""age"": 25, ""occupation"": ""Time Traveller""}"'}
|
||||||
|
</span>
|
||||||
</code>
|
</code>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,11 +12,9 @@ import Subscribers from "./Subscribers"
|
||||||
import Subscriber from "./Subscriber"
|
import Subscriber from "./Subscriber"
|
||||||
import Templates from "./Templates"
|
import Templates from "./Templates"
|
||||||
import Import from "./Import"
|
import Import from "./Import"
|
||||||
import Test from "./Test"
|
import Campaigns from "./Campaigns"
|
||||||
import Campaigns from "./Campaigns";
|
import Campaign from "./Campaign"
|
||||||
import Campaign from "./Campaign";
|
import Media from "./Media"
|
||||||
import Media from "./Media";
|
|
||||||
|
|
||||||
|
|
||||||
const { Content, Footer, Sider } = Layout
|
const { Content, Footer, Sider } = Layout
|
||||||
const SubMenu = Menu.SubMenu
|
const SubMenu = Menu.SubMenu
|
||||||
|
@ -27,9 +25,9 @@ class Base extends React.Component {
|
||||||
basePath: "/" + window.location.pathname.split("/")[1],
|
basePath: "/" + window.location.pathname.split("/")[1],
|
||||||
error: null,
|
error: null,
|
||||||
collapsed: false
|
collapsed: false
|
||||||
};
|
}
|
||||||
|
|
||||||
onCollapse = (collapsed) => {
|
onCollapse = collapsed => {
|
||||||
this.setState({ collapsed })
|
this.setState({ collapsed })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,68 +41,211 @@ class Base extends React.Component {
|
||||||
theme="light"
|
theme="light"
|
||||||
>
|
>
|
||||||
<div className="logo">
|
<div className="logo">
|
||||||
<Link to="/"><img src={logo} alt="listmonk logo" /></Link>
|
<Link to="/">
|
||||||
|
<img src={logo} alt="listmonk logo" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Menu defaultSelectedKeys={["/"]}
|
<Menu
|
||||||
|
defaultSelectedKeys={["/"]}
|
||||||
selectedKeys={[window.location.pathname]}
|
selectedKeys={[window.location.pathname]}
|
||||||
defaultOpenKeys={[this.state.basePath]}
|
defaultOpenKeys={[this.state.basePath]}
|
||||||
mode="inline">
|
mode="inline"
|
||||||
|
>
|
||||||
<Menu.Item key="/"><Link to="/"><Icon type="dashboard" /><span>Dashboard</span></Link></Menu.Item>
|
<Menu.Item key="/">
|
||||||
<Menu.Item key="/lists"><Link to="/lists"><Icon type="bars" /><span>Lists</span></Link></Menu.Item>
|
<Link to="/">
|
||||||
|
<Icon type="dashboard" />
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/lists">
|
||||||
|
<Link to="/lists">
|
||||||
|
<Icon type="bars" />
|
||||||
|
<span>Lists</span>
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
<SubMenu
|
<SubMenu
|
||||||
key="/subscribers"
|
key="/subscribers"
|
||||||
title={<span><Icon type="team" /><span>Subscribers</span></span>}>
|
title={
|
||||||
<Menu.Item key="/subscribers"><Link to="/subscribers"><Icon type="team" /> All subscribers</Link></Menu.Item>
|
<span>
|
||||||
<Menu.Item key="/subscribers/import"><Link to="/subscribers/import"><Icon type="upload" /> Import</Link></Menu.Item>
|
<Icon type="team" />
|
||||||
|
<span>Subscribers</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu.Item key="/subscribers">
|
||||||
|
<Link to="/subscribers">
|
||||||
|
<Icon type="team" /> All subscribers
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/subscribers/import">
|
||||||
|
<Link to="/subscribers/import">
|
||||||
|
<Icon type="upload" /> Import
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
|
||||||
<SubMenu
|
<SubMenu
|
||||||
key="/campaigns"
|
key="/campaigns"
|
||||||
title={<span><Icon type="rocket" /><span>Campaigns</span></span>}>
|
title={
|
||||||
<Menu.Item key="/campaigns"><Link to="/campaigns"><Icon type="rocket" /> All campaigns</Link></Menu.Item>
|
<span>
|
||||||
<Menu.Item key="/campaigns/new"><Link to="/campaigns/new"><Icon type="plus" /> Create new</Link></Menu.Item>
|
<Icon type="rocket" />
|
||||||
<Menu.Item key="/campaigns/media"><Link to="/campaigns/media"><Icon type="picture" /> Media</Link></Menu.Item>
|
<span>Campaigns</span>
|
||||||
<Menu.Item key="/campaigns/templates"><Link to="/campaigns/templates"><Icon type="code-o" /> Templates</Link></Menu.Item>
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu.Item key="/campaigns">
|
||||||
|
<Link to="/campaigns">
|
||||||
|
<Icon type="rocket" /> All campaigns
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/campaigns/new">
|
||||||
|
<Link to="/campaigns/new">
|
||||||
|
<Icon type="plus" /> Create new
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/campaigns/media">
|
||||||
|
<Link to="/campaigns/media">
|
||||||
|
<Icon type="picture" /> Media
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="/campaigns/templates">
|
||||||
|
<Link to="/campaigns/templates">
|
||||||
|
<Icon type="code-o" /> Templates
|
||||||
|
</Link>
|
||||||
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
|
|
||||||
<SubMenu
|
<SubMenu
|
||||||
key="/settings"
|
key="/settings"
|
||||||
title={<span><Icon type="setting" /><span>Settings</span></span>}>
|
title={
|
||||||
<Menu.Item key="9"><Icon type="user" /> Users</Menu.Item>
|
<span>
|
||||||
<Menu.Item key="10"><Icon type="setting" />Settings</Menu.Item>
|
<Icon type="setting" />
|
||||||
|
<span>Settings</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Menu.Item key="9">
|
||||||
|
<Icon type="user" /> Users
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item key="10">
|
||||||
|
<Icon type="setting" />
|
||||||
|
Settings
|
||||||
|
</Menu.Item>
|
||||||
</SubMenu>
|
</SubMenu>
|
||||||
<Menu.Item key="11"><Icon type="logout" /><span>Logout</span></Menu.Item>
|
<Menu.Item key="11">
|
||||||
|
<Icon type="logout" />
|
||||||
|
<span>Logout</span>
|
||||||
|
</Menu.Item>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Content style={{ margin: "0 16px" }}>
|
<Content style={{ margin: "0 16px" }}>
|
||||||
<div className="content-body">
|
<div className="content-body">
|
||||||
<div id="alert-container"></div>
|
<div id="alert-container" />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact key="/" path="/" render={(props) => <Dashboard { ...{ ...this.props, route: props } } />} />
|
<Route
|
||||||
<Route exact key="/lists" path="/lists" render={(props) => <Lists { ...{ ...this.props, route: props } } />} />
|
exact
|
||||||
<Route exact key="/subscribers" path="/subscribers" render={(props) => <Subscribers { ...{ ...this.props, route: props } } />} />
|
key="/"
|
||||||
<Route exact key="/subscribers/lists/:listID" path="/subscribers/lists/:listID" render={(props) => <Subscribers { ...{ ...this.props, route: props } } />} />
|
path="/"
|
||||||
<Route exact key="/subscribers/import" path="/subscribers/import" render={(props) => <Import { ...{ ...this.props, route: props } } />} />
|
render={props => (
|
||||||
<Route exact key="/subscribers/:subID" path="/subscribers/:subID" render={(props) => <Subscriber { ...{ ...this.props, route: props } } />} />
|
<Dashboard {...{ ...this.props, route: props }} />
|
||||||
<Route exact key="/campaigns" path="/campaigns" render={(props) => <Campaigns { ...{ ...this.props, route: props } } />} />
|
)}
|
||||||
<Route exact key="/campaigns/new" path="/campaigns/new" render={(props) => <Campaign { ...{ ...this.props, route: props } } />} />
|
/>
|
||||||
<Route exact key="/campaigns/media" path="/campaigns/media" render={(props) => <Media { ...{ ...this.props, route: props } } />} />
|
<Route
|
||||||
<Route exact key="/campaigns/templates" path="/campaigns/templates" render={(props) => <Templates { ...{ ...this.props, route: props } } />} />
|
exact
|
||||||
<Route exact key="/campaigns/:campaignID" path="/campaigns/:campaignID" render={(props) => <Campaign { ...{ ...this.props, route: props } } />} />
|
key="/lists"
|
||||||
<Route exact key="/test" path="/test" render={(props) => <Test { ...{ ...this.props, route: props } } />} />
|
path="/lists"
|
||||||
|
render={props => (
|
||||||
|
<Lists {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/subscribers"
|
||||||
|
path="/subscribers"
|
||||||
|
render={props => (
|
||||||
|
<Subscribers {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/subscribers/lists/:listID"
|
||||||
|
path="/subscribers/lists/:listID"
|
||||||
|
render={props => (
|
||||||
|
<Subscribers {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/subscribers/import"
|
||||||
|
path="/subscribers/import"
|
||||||
|
render={props => (
|
||||||
|
<Import {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/subscribers/:subID"
|
||||||
|
path="/subscribers/:subID"
|
||||||
|
render={props => (
|
||||||
|
<Subscriber {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/campaigns"
|
||||||
|
path="/campaigns"
|
||||||
|
render={props => (
|
||||||
|
<Campaigns {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/campaigns/new"
|
||||||
|
path="/campaigns/new"
|
||||||
|
render={props => (
|
||||||
|
<Campaign {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/campaigns/media"
|
||||||
|
path="/campaigns/media"
|
||||||
|
render={props => (
|
||||||
|
<Media {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/campaigns/templates"
|
||||||
|
path="/campaigns/templates"
|
||||||
|
render={props => (
|
||||||
|
<Templates {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
key="/campaigns/:campaignID"
|
||||||
|
path="/campaigns/:campaignID"
|
||||||
|
render={props => (
|
||||||
|
<Campaign {...{ ...this.props, route: props }} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
<Footer>
|
<Footer>
|
||||||
<span className="text-small">
|
<span className="text-small">
|
||||||
<a href="https://listmonk.app" rel="noreferrer noopener" target="_blank">listmonk</a>
|
<a
|
||||||
{" "}
|
href="https://listmonk.app"
|
||||||
© 2019 { year != 2019 ? " - " + year : "" }
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
listmonk
|
||||||
|
</a>{" "}
|
||||||
|
© 2019 {year !== 2019 ? " - " + year : ""}
|
||||||
</span>
|
</span>
|
||||||
</Footer>
|
</Footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { Row, Col, Modal, Form, Input, Select, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
Icon,
|
||||||
|
Tooltip,
|
||||||
|
Tag,
|
||||||
|
Popconfirm,
|
||||||
|
Spin,
|
||||||
|
notification
|
||||||
|
} from "antd"
|
||||||
|
|
||||||
import Utils from "./utils"
|
import Utils from "./utils"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
const tagColors = {
|
const tagColors = {
|
||||||
"private": "orange",
|
private: "orange",
|
||||||
"public": "green"
|
public: "green"
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateFormDef extends React.PureComponent {
|
class CreateFormDef extends React.PureComponent {
|
||||||
|
@ -17,7 +32,7 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
handleSubmit = (e) => {
|
handleSubmit = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.props.form.validateFields((err, values) => {
|
this.props.form.validateFields((err, values) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -27,24 +42,50 @@ class CreateFormDef extends React.PureComponent {
|
||||||
this.setState({ modalWaiting: true })
|
this.setState({ modalWaiting: true })
|
||||||
if (this.props.formType === cs.FormCreate) {
|
if (this.props.formType === cs.FormCreate) {
|
||||||
// Create a new list.
|
// Create a new list.
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.CreateList, cs.MethodPost, values).then(() => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "List created", description: `"${values["name"]}" created` })
|
.modelRequest(
|
||||||
|
cs.ModelLists,
|
||||||
|
cs.Routes.CreateList,
|
||||||
|
cs.MethodPost,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "List created",
|
||||||
|
description: `"${values["name"]}" created`
|
||||||
|
})
|
||||||
this.props.fetchRecords()
|
this.props.fetchRecords()
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Edit a list.
|
// Edit a list.
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.UpdateList, cs.MethodPut, { ...values, id: this.props.record.id }).then(() => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "List modified", description: `"${values["name"]}" modified` })
|
.modelRequest(cs.ModelLists, cs.Routes.UpdateList, cs.MethodPut, {
|
||||||
|
...values,
|
||||||
|
id: this.props.record.id
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "List modified",
|
||||||
|
description: `"${values["name"]}" modified`
|
||||||
|
})
|
||||||
this.props.fetchRecords()
|
this.props.fetchRecords()
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -58,11 +99,18 @@ class CreateFormDef extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tag color={ tagColors.hasOwnProperty(record.type) ? tagColors[record.type] : "" }>{ record.type }</Tag>
|
<Tag
|
||||||
{" "}
|
color={
|
||||||
|
tagColors.hasOwnProperty(record.type) ? tagColors[record.type] : ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{record.type}
|
||||||
|
</Tag>{" "}
|
||||||
{record.name}
|
{record.name}
|
||||||
<br />
|
<br />
|
||||||
<span className="text-tiny text-grey">ID { record.id } / UUID { record.uuid }</span>
|
<span className="text-tiny text-grey">
|
||||||
|
ID {record.id} / UUID {record.uuid}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,15 +129,19 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={ true } title={ this.modalTitle(this.state.form, record) }
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
title={this.modalTitle(this.state.form, record)}
|
||||||
okText={this.state.form === cs.FormCreate ? "Create" : "Save"}
|
okText={this.state.form === cs.FormCreate ? "Create" : "Save"}
|
||||||
confirmLoading={this.state.modalWaiting}
|
confirmLoading={this.state.modalWaiting}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onOk={ this.handleSubmit }>
|
onOk={this.handleSubmit}
|
||||||
|
>
|
||||||
|
<div id="modal-alert-container" />
|
||||||
|
|
||||||
<div id="modal-alert-container"></div>
|
<Spin
|
||||||
|
spinning={this.props.reqStates[cs.ModelLists] === cs.StatePending}
|
||||||
<Spin spinning={ this.props.reqStates[cs.ModelLists] === cs.StatePending }>
|
>
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
<Form.Item {...formItemLayout} label="Name">
|
<Form.Item {...formItemLayout} label="Name">
|
||||||
{getFieldDecorator("name", {
|
{getFieldDecorator("name", {
|
||||||
|
@ -97,17 +149,29 @@ class CreateFormDef extends React.PureComponent {
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(<Input autoFocus maxLength="200" />)}
|
})(<Input autoFocus maxLength="200" />)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} name="type" label="Type" extra="Public lists are open to the world to subscribe">
|
<Form.Item
|
||||||
{getFieldDecorator("type", { initialValue: record.type ? record.type : "private", rules: [{ required: true }] })(
|
{...formItemLayout}
|
||||||
|
name="type"
|
||||||
|
label="Type"
|
||||||
|
extra="Public lists are open to the world to subscribe"
|
||||||
|
>
|
||||||
|
{getFieldDecorator("type", {
|
||||||
|
initialValue: record.type ? record.type : "private",
|
||||||
|
rules: [{ required: true }]
|
||||||
|
})(
|
||||||
<Select style={{ maxWidth: 120 }}>
|
<Select style={{ maxWidth: 120 }}>
|
||||||
<Select.Option value="private">Private</Select.Option>
|
<Select.Option value="private">Private</Select.Option>
|
||||||
<Select.Option value="public">Public</Select.Option>
|
<Select.Option value="public">Public</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Tags" extra="Hit Enter after typing a word to add multiple tags">
|
<Form.Item
|
||||||
|
{...formItemLayout}
|
||||||
|
label="Tags"
|
||||||
|
extra="Hit Enter after typing a word to add multiple tags"
|
||||||
|
>
|
||||||
{getFieldDecorator("tags", { initialValue: record.tags })(
|
{getFieldDecorator("tags", { initialValue: record.tags })(
|
||||||
<Select mode="tags"></Select>
|
<Select mode="tags" />
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -128,20 +192,23 @@ class Lists extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.columns = [{
|
this.columns = [
|
||||||
|
{
|
||||||
title: "Name",
|
title: "Name",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
width: "40%",
|
width: "40%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const out = [];
|
const out = []
|
||||||
out.push(
|
out.push(
|
||||||
<div className="name" key={`name-${record.id}`}><Link to={ `/subscribers/lists/${record.id}` }>{ text }</Link></div>
|
<div className="name" key={`name-${record.id}`}>
|
||||||
|
<Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (record.tags.length > 0) {
|
if (record.tags.length > 0) {
|
||||||
for (let i = 0; i < record.tags.length; i++) {
|
for (let i = 0; i < record.tags.length; i++) {
|
||||||
out.push(<Tag key={`tag-${i}`}>{ record.tags[i] }</Tag>);
|
out.push(<Tag key={`tag-${i}`}>{record.tags[i]}</Tag>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +231,9 @@ class Lists extends React.PureComponent {
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="name" key={`name-${record.id}`}><Link to={ `/subscribers/lists/${record.id}` }>{ text }</Link></div>
|
<div className="name" key={`name-${record.id}`}>
|
||||||
|
<Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -189,15 +258,34 @@ class Lists extends React.PureComponent {
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Tooltip title="Send a campaign"><a role="button"><Icon type="rocket" /></a></Tooltip>
|
<Tooltip title="Send a campaign">
|
||||||
<Tooltip title="Edit list"><a role="button" onClick={() => this.handleShowEditForm(record)}><Icon type="edit" /></a></Tooltip>
|
<a role="button">
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.deleteRecord(record)}>
|
<Icon type="rocket" />
|
||||||
<Tooltip title="Delete list" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Edit list">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => this.handleShowEditForm(record)}
|
||||||
|
>
|
||||||
|
<Icon type="edit" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.deleteRecord(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete list" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -209,15 +297,27 @@ class Lists extends React.PureComponent {
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRecord = (record) => {
|
deleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.DeleteList, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(cs.ModelLists, cs.Routes.DeleteList, cs.MethodDelete, {
|
||||||
|
id: record.id
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "List deleted", description: `"${record.name}" deleted` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "List deleted",
|
||||||
|
description: `"${record.name}" deleted`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,7 +329,7 @@ class Lists extends React.PureComponent {
|
||||||
this.setState({ formType: cs.FormCreate, record: {} })
|
this.setState({ formType: cs.FormCreate, record: {} })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowEditForm = (record) => {
|
handleShowEditForm = record => {
|
||||||
this.setState({ formType: cs.FormEdit, record: record })
|
this.setState({ formType: cs.FormEdit, record: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -237,9 +337,17 @@ class Lists extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<section className="content">
|
<section className="content">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={22}><h1>Lists ({this.props.data[cs.ModelLists].length}) </h1></Col>
|
<Col span={22}>
|
||||||
|
<h1>Lists ({this.props.data[cs.ModelLists].length}) </h1>
|
||||||
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button type="primary" icon="plus" onClick={this.handleShowCreateForm}>Create list</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="plus"
|
||||||
|
onClick={this.handleShowCreateForm}
|
||||||
|
>
|
||||||
|
Create list
|
||||||
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
|
@ -253,7 +361,8 @@ class Lists extends React.PureComponent {
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateForm { ...this.props }
|
<CreateForm
|
||||||
|
{...this.props}
|
||||||
formType={this.state.formType}
|
formType={this.state.formType}
|
||||||
record={this.state.record}
|
record={this.state.record}
|
||||||
onClose={this.handleHideForm}
|
onClose={this.handleHideForm}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Row, Col, Form, Upload, Icon, Spin, Popconfirm, Tooltip, notification } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Form,
|
||||||
|
Upload,
|
||||||
|
Icon,
|
||||||
|
Spin,
|
||||||
|
Popconfirm,
|
||||||
|
Tooltip,
|
||||||
|
notification
|
||||||
|
} from "antd"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
class TheFormDef extends React.PureComponent {
|
class TheFormDef extends React.PureComponent {
|
||||||
|
@ -16,19 +26,31 @@ class TheFormDef extends React.PureComponent {
|
||||||
this.props.modelRequest(cs.ModelMedia, cs.Routes.GetMedia, cs.MethodGet)
|
this.props.modelRequest(cs.ModelMedia, cs.Routes.GetMedia, cs.MethodGet)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecord = (record) => {
|
handleDeleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelMedia, cs.Routes.DeleteMedia, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(cs.ModelMedia, cs.Routes.DeleteMedia, cs.MethodDelete, {
|
||||||
|
id: record.id
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Image deleted", description: `"${record.filename}" deleted` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Image deleted",
|
||||||
|
description: `"${record.filename}" deleted`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInsertMedia = (record) => {
|
handleInsertMedia = record => {
|
||||||
// The insertMedia callback may be passed down by the invoker (Campaign)
|
// The insertMedia callback may be passed down by the invoker (Campaign)
|
||||||
if (!this.props.insertMedia) {
|
if (!this.props.insertMedia) {
|
||||||
return false
|
return false
|
||||||
|
@ -38,11 +60,17 @@ class TheFormDef extends React.PureComponent {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
onFileChange = (f) => {
|
onFileChange = f => {
|
||||||
if(f.file.error && f.file.response && f.file.response.hasOwnProperty("message")) {
|
if (
|
||||||
notification["error"]({ placement: cs.MsgPosition,
|
f.file.error &&
|
||||||
|
f.file.response &&
|
||||||
|
f.file.response.hasOwnProperty("message")
|
||||||
|
) {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
message: "Error uploading file",
|
message: "Error uploading file",
|
||||||
description: f.file.response.message })
|
description: f.file.response.message
|
||||||
|
})
|
||||||
} else if (f.file.status === "done") {
|
} else if (f.file.status === "done") {
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}
|
}
|
||||||
|
@ -60,9 +88,7 @@ class TheFormDef extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Spin spinning={false}>
|
<Spin spinning={false}>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Item
|
<Form.Item {...formItemLayout} label="Upload images">
|
||||||
{...formItemLayout}
|
|
||||||
label="Upload images">
|
|
||||||
<div className="dropbox">
|
<div className="dropbox">
|
||||||
{getFieldDecorator("file", {
|
{getFieldDecorator("file", {
|
||||||
valuePropName: "file",
|
valuePropName: "file",
|
||||||
|
@ -75,7 +101,8 @@ class TheFormDef extends React.PureComponent {
|
||||||
multiple={true}
|
multiple={true}
|
||||||
listType="picture"
|
listType="picture"
|
||||||
onChange={this.onFileChange}
|
onChange={this.onFileChange}
|
||||||
accept=".gif, .jpg, .jpeg, .png">
|
accept=".gif, .jpg, .jpeg, .png"
|
||||||
|
>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<Icon type="inbox" />
|
<Icon type="inbox" />
|
||||||
</p>
|
</p>
|
||||||
|
@ -87,23 +114,41 @@ class TheFormDef extends React.PureComponent {
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<section className="gallery">
|
<section className="gallery">
|
||||||
{this.props.media && this.props.media.map((record, i) =>
|
{this.props.media &&
|
||||||
|
this.props.media.map((record, i) => (
|
||||||
<div key={i} className="image">
|
<div key={i} className="image">
|
||||||
<a onClick={ () => {
|
<a
|
||||||
this.handleInsertMedia(record);
|
onClick={() => {
|
||||||
|
this.handleInsertMedia(record)
|
||||||
if (this.props.onCancel) {
|
if (this.props.onCancel) {
|
||||||
this.props.onCancel();
|
this.props.onCancel()
|
||||||
}
|
}
|
||||||
} }><img alt={ record.filename } src={ record.thumb_uri } /></a>
|
}}
|
||||||
|
>
|
||||||
|
<img alt={record.filename} src={record.thumb_uri} />
|
||||||
|
</a>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Tooltip title="View" placement="bottom"><a role="button" href={ record.uri } target="_blank"><Icon type="login" /></a></Tooltip>
|
<Tooltip title="View" placement="bottom">
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleDeleteRecord(record)}>
|
<a role="button" href={record.uri} target="_blank">
|
||||||
<Tooltip title="Delete" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
<Icon type="login" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.handleDeleteRecord(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
<div className="name" title={ record.filename }>{ record.filename }</div>
|
<div className="name" title={record.filename}>
|
||||||
|
{record.filename}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
))}
|
||||||
</section>
|
</section>
|
||||||
</Spin>
|
</Spin>
|
||||||
)
|
)
|
||||||
|
@ -116,14 +161,13 @@ class Media extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<section className="content media">
|
<section className="content media">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={22}><h1>Images</h1></Col>
|
<Col span={22}>
|
||||||
<Col span={2}>
|
<h1>Images</h1>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={2} />
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<TheForm { ...this.props }
|
<TheForm {...this.props} media={this.props.data[cs.ModelMedia]} />
|
||||||
media={ this.props.data[cs.ModelMedia] }>
|
|
||||||
</TheForm>
|
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,15 +22,20 @@ class ModalPreview extends React.PureComponent {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Modal visible={ true } title={ this.props.title }
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
title={this.props.title}
|
||||||
className="preview-modal"
|
className="preview-modal"
|
||||||
width="90%"
|
width="90%"
|
||||||
height={900}
|
height={900}
|
||||||
onCancel={this.props.onCancel}
|
onCancel={this.props.onCancel}
|
||||||
onOk={ this.props.onCancel }>
|
onOk={this.props.onCancel}
|
||||||
|
>
|
||||||
<div className="preview-iframe-container">
|
<div className="preview-iframe-container">
|
||||||
<Spin className="preview-iframe-spinner"></Spin>
|
<Spin className="preview-iframe-spinner" />
|
||||||
<iframe key="preview-iframe" onLoad={() => {
|
<iframe
|
||||||
|
key="preview-iframe"
|
||||||
|
onLoad={() => {
|
||||||
// If state is used to manage the spinner, it causes
|
// If state is used to manage the spinner, it causes
|
||||||
// the iframe to re-render and reload everything.
|
// the iframe to re-render and reload everything.
|
||||||
// Hack the spinner away from the DOM directly instead.
|
// Hack the spinner away from the DOM directly instead.
|
||||||
|
@ -39,19 +44,19 @@ class ModalPreview extends React.PureComponent {
|
||||||
spin.parentNode.removeChild(spin)
|
spin.parentNode.removeChild(spin)
|
||||||
}
|
}
|
||||||
// this.setState({ loading: false })
|
// this.setState({ loading: false })
|
||||||
}} title={ this.props.title ? this.props.title : "Preview" }
|
}}
|
||||||
|
title={this.props.title ? this.props.title : "Preview"}
|
||||||
name="preview-iframe"
|
name="preview-iframe"
|
||||||
id="preview-iframe"
|
id="preview-iframe"
|
||||||
className="preview-iframe"
|
className="preview-iframe"
|
||||||
ref={(o) => {
|
ref={o => {
|
||||||
if (!o) {
|
if (!o) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the DOM reference for the iframe is ready,
|
// When the DOM reference for the iframe is ready,
|
||||||
// see if there's a body to post with the form hack.
|
// see if there's a body to post with the form hack.
|
||||||
if(this.props.body !== undefined
|
if (this.props.body !== undefined && this.props.body !== null) {
|
||||||
&& this.props.body !== null) {
|
|
||||||
this.makeForm(this.props.body)
|
this.makeForm(this.props.body)
|
||||||
} else {
|
} else {
|
||||||
if (this.props.previewURL) {
|
if (this.props.previewURL) {
|
||||||
|
@ -59,11 +64,10 @@ class ModalPreview extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
src="about:blank">
|
src="about:blank"
|
||||||
</iframe>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Row, Col, Form, Input, Select, Button, Tag, Spin, Popconfirm, notification } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Button,
|
||||||
|
Tag,
|
||||||
|
Spin,
|
||||||
|
Popconfirm,
|
||||||
|
notification
|
||||||
|
} from "antd"
|
||||||
|
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
const tagColors = {
|
const tagColors = {
|
||||||
"enabled": "green",
|
enabled: "green",
|
||||||
"blacklisted": "red"
|
blacklisted: "red"
|
||||||
}
|
}
|
||||||
const formItemLayoutModal = {
|
const formItemLayoutModal = {
|
||||||
labelCol: { xs: { span: 24 }, sm: { span: 4 } },
|
labelCol: { xs: { span: 24 }, sm: { span: 4 } },
|
||||||
|
@ -33,7 +44,8 @@ class CreateFormDef extends React.PureComponent {
|
||||||
cb = () => {}
|
cb = () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
var err = null, values = {}
|
var err = null,
|
||||||
|
values = {}
|
||||||
this.props.form.validateFields((e, v) => {
|
this.props.form.validateFields((e, v) => {
|
||||||
err = e
|
err = e
|
||||||
values = v
|
values = v
|
||||||
|
@ -48,12 +60,17 @@ class CreateFormDef extends React.PureComponent {
|
||||||
try {
|
try {
|
||||||
values["attribs"] = JSON.parse(a)
|
values["attribs"] = JSON.parse(a)
|
||||||
if (values["attribs"] instanceof Array) {
|
if (values["attribs"] instanceof Array) {
|
||||||
notification["error"]({ message: "Invalid JSON type",
|
notification["error"]({
|
||||||
description: "Attributes should be a map {} and not an array []" })
|
message: "Invalid JSON type",
|
||||||
|
description: "Attributes should be a map {} and not an array []"
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notification["error"]({ message: "Invalid JSON in attributes", description: e.toString() })
|
notification["error"]({
|
||||||
|
message: "Invalid JSON in attributes",
|
||||||
|
description: e.toString()
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,30 +78,52 @@ class CreateFormDef extends React.PureComponent {
|
||||||
this.setState({ loading: true })
|
this.setState({ loading: true })
|
||||||
if (this.props.formType === cs.FormCreate) {
|
if (this.props.formType === cs.FormCreate) {
|
||||||
// Add a subscriber.
|
// Add a subscriber.
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.CreateSubscriber, cs.MethodPost, values).then(() => {
|
this.props
|
||||||
notification["success"]({ message: "Subscriber added", description: `${values["email"]} added` })
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.CreateSubscriber,
|
||||||
|
cs.MethodPost,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
message: "Subscriber added",
|
||||||
|
description: `${values["email"]} added`
|
||||||
|
})
|
||||||
if (!this.props.isModal) {
|
if (!this.props.isModal) {
|
||||||
this.props.fetchRecord(this.props.record.id)
|
this.props.fetchRecord(this.props.record.id)
|
||||||
}
|
}
|
||||||
cb(true)
|
cb(true)
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
cb(false)
|
cb(false)
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Edit a subscriber.
|
// Edit a subscriber.
|
||||||
delete(values["keys"])
|
delete values["keys"]
|
||||||
delete(values["vals"])
|
delete values["vals"]
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.UpdateSubscriber, cs.MethodPut, { ...values, id: this.props.record.id }).then((resp) => {
|
this.props
|
||||||
notification["success"]({ message: "Subscriber modified", description: `${values["email"]} modified` })
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.UpdateSubscriber,
|
||||||
|
cs.MethodPut,
|
||||||
|
{ ...values, id: this.props.record.id }
|
||||||
|
)
|
||||||
|
.then(resp => {
|
||||||
|
notification["success"]({
|
||||||
|
message: "Subscriber modified",
|
||||||
|
description: `${values["email"]} modified`
|
||||||
|
})
|
||||||
if (!this.props.isModal) {
|
if (!this.props.isModal) {
|
||||||
this.props.fetchRecord(this.props.record.id)
|
this.props.fetchRecord(this.props.record.id)
|
||||||
}
|
}
|
||||||
cb(true)
|
cb(true)
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
cb(false)
|
cb(false)
|
||||||
this.setState({ loading: false })
|
this.setState({ loading: false })
|
||||||
|
@ -92,21 +131,31 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecord = (record) => {
|
handleDeleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscriber, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.DeleteSubscriber,
|
||||||
|
cs.MethodDelete,
|
||||||
|
{ id: record.id }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber deleted", description: `${record.email} deleted` })
|
notification["success"]({
|
||||||
|
message: "Subscriber deleted",
|
||||||
|
description: `${record.email} deleted`
|
||||||
|
})
|
||||||
|
|
||||||
this.props.route.history.push({
|
this.props.route.history.push({
|
||||||
pathname: cs.Routes.ViewSubscribers,
|
pathname: cs.Routes.ViewSubscribers
|
||||||
})
|
})
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { formType, record } = this.props;
|
const { formType, record } = this.props
|
||||||
const { getFieldDecorator } = this.props.form
|
const { getFieldDecorator } = this.props.form
|
||||||
|
|
||||||
if (formType === null) {
|
if (formType === null) {
|
||||||
|
@ -116,13 +165,18 @@ class CreateFormDef extends React.PureComponent {
|
||||||
let subListIDs = []
|
let subListIDs = []
|
||||||
let subStatuses = {}
|
let subStatuses = {}
|
||||||
if (this.props.record && this.props.record.lists) {
|
if (this.props.record && this.props.record.lists) {
|
||||||
subListIDs = this.props.record.lists.map((v) => { return v["id"] })
|
subListIDs = this.props.record.lists.map(v => {
|
||||||
subStatuses = this.props.record.lists.reduce((o, item) => ({ ...o, [item.id]: item.subscription_status}), {})
|
return v["id"]
|
||||||
|
})
|
||||||
|
subStatuses = this.props.record.lists.reduce(
|
||||||
|
(o, item) => ({ ...o, [item.id]: item.subscription_status }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
} else if (this.props.list) {
|
} else if (this.props.list) {
|
||||||
subListIDs = [this.props.list.id]
|
subListIDs = [this.props.list.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
const layout = this.props.isModal ? formItemLayoutModal : formItemLayout;
|
const layout = this.props.isModal ? formItemLayoutModal : formItemLayout
|
||||||
return (
|
return (
|
||||||
<Spin spinning={this.state.loading}>
|
<Spin spinning={this.state.loading}>
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
|
@ -138,59 +192,95 @@ class CreateFormDef extends React.PureComponent {
|
||||||
rules: [{ required: true }]
|
rules: [{ required: true }]
|
||||||
})(<Input maxLength="200" />)}
|
})(<Input maxLength="200" />)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item { ...layout } name="status" label="Status" extra="Blacklisted users will not receive any e-mails ever">
|
<Form.Item
|
||||||
{getFieldDecorator("status", { initialValue: record.status ? record.status : "enabled", rules: [{ required: true, message: "Type is required" }] })(
|
{...layout}
|
||||||
|
name="status"
|
||||||
|
label="Status"
|
||||||
|
extra="Blacklisted users will not receive any e-mails ever"
|
||||||
|
>
|
||||||
|
{getFieldDecorator("status", {
|
||||||
|
initialValue: record.status ? record.status : "enabled",
|
||||||
|
rules: [{ required: true, message: "Type is required" }]
|
||||||
|
})(
|
||||||
<Select style={{ maxWidth: 120 }}>
|
<Select style={{ maxWidth: 120 }}>
|
||||||
<Select.Option value="enabled">Enabled</Select.Option>
|
<Select.Option value="enabled">Enabled</Select.Option>
|
||||||
<Select.Option value="blacklisted">Blacklisted</Select.Option>
|
<Select.Option value="blacklisted">Blacklisted</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item { ...layout } label="Lists" extra="Lists to subscribe to. Lists from which subscribers have unsubscribed themselves cannot be removed.">
|
<Form.Item
|
||||||
|
{...layout}
|
||||||
|
label="Lists"
|
||||||
|
extra="Lists to subscribe to. Lists from which subscribers have unsubscribed themselves cannot be removed."
|
||||||
|
>
|
||||||
{getFieldDecorator("lists", { initialValue: subListIDs })(
|
{getFieldDecorator("lists", { initialValue: subListIDs })(
|
||||||
<Select mode="multiple">
|
<Select mode="multiple">
|
||||||
{[...this.props.lists].map((v, i) =>
|
{[...this.props.lists].map((v, i) => (
|
||||||
<Select.Option value={ v.id } key={ v.id } disabled={ subStatuses[v.id] === cs.SubscriptionStatusUnsubscribed }>
|
<Select.Option
|
||||||
<span>{ v.name }
|
value={v.id}
|
||||||
{ subStatuses[v.id] &&
|
key={v.id}
|
||||||
<sup className={ "subscription-status " + subStatuses[v.id] }> { subStatuses[v.id] }</sup>
|
disabled={
|
||||||
|
subStatuses[v.id] === cs.SubscriptionStatusUnsubscribed
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{v.name}
|
||||||
|
{subStatuses[v.id] && (
|
||||||
|
<sup
|
||||||
|
className={"subscription-status " + subStatuses[v.id]}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{subStatuses[v.id]}
|
||||||
|
</sup>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
)}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...layout} label="Attributes" colon={false}>
|
<Form.Item {...layout} label="Attributes" colon={false}>
|
||||||
<div>
|
<div>
|
||||||
{getFieldDecorator("attribs", {
|
{getFieldDecorator("attribs", {
|
||||||
initialValue: record.attribs ? JSON.stringify(record.attribs, null, 4) : ""
|
initialValue: record.attribs
|
||||||
|
? JSON.stringify(record.attribs, null, 4)
|
||||||
|
: ""
|
||||||
})(
|
})(
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
placeholder="{}"
|
placeholder="{}"
|
||||||
rows={10}
|
rows={10}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
autosize={{ minRows: 5, maxRows: 10 }} />
|
autosize={{ minRows: 5, maxRows: 10 }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="ant-form-extra">Attributes are defined as a JSON map, for example:
|
<p className="ant-form-extra">
|
||||||
{' {"age": 30, "color": "red", "is_user": true}'}. <a href="">More info</a>.</p>
|
Attributes are defined as a JSON map, for example:
|
||||||
|
{' {"age": 30, "color": "red", "is_user": true}'}.{" "}
|
||||||
|
<a href="">More info</a>.
|
||||||
|
</p>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{ !this.props.isModal &&
|
{!this.props.isModal && (
|
||||||
<Form.Item {...formItemTailLayout}>
|
<Form.Item {...formItemTailLayout}>
|
||||||
<Button type="primary" htmlType="submit" icon={ this.props.formType === cs.FormCreate ? "plus" : "save" }>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
icon={this.props.formType === cs.FormCreate ? "plus" : "save"}
|
||||||
|
>
|
||||||
{this.props.formType === cs.FormCreate ? "Add" : "Save"}
|
{this.props.formType === cs.FormCreate ? "Add" : "Save"}
|
||||||
</Button>
|
</Button>{" "}
|
||||||
{" "}
|
{this.props.formType === cs.FormEdit && (
|
||||||
{ this.props.formType === cs.FormEdit &&
|
<Popconfirm
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => {
|
title="Are you sure?"
|
||||||
|
onConfirm={() => {
|
||||||
this.handleDeleteRecord(record)
|
this.handleDeleteRecord(record)
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Button icon="delete">Delete</Button>
|
<Button icon="delete">Delete</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Spin>
|
</Spin>
|
||||||
)
|
)
|
||||||
|
@ -204,7 +294,9 @@ class Subscriber extends React.PureComponent {
|
||||||
loading: true,
|
loading: true,
|
||||||
formRef: null,
|
formRef: null,
|
||||||
record: {},
|
record: {},
|
||||||
subID: this.props.route.match.params ? parseInt(this.props.route.match.params.subID, 10) : 0,
|
subID: this.props.route.match.params
|
||||||
|
? parseInt(this.props.route.match.params.subID, 10)
|
||||||
|
: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -221,15 +313,22 @@ class Subscriber extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecord = (id) => {
|
fetchRecord = id => {
|
||||||
this.props.request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id }).then((r) => {
|
this.props
|
||||||
|
.request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id })
|
||||||
|
.then(r => {
|
||||||
this.setState({ record: r.data.data, loading: false })
|
this.setState({ record: r.data.data, loading: false })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormRef = (r) => {
|
setFormRef = r => {
|
||||||
this.setState({ formRef: r })
|
this.setState({ formRef: r })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,22 +344,28 @@ class Subscriber extends React.PureComponent {
|
||||||
<header className="header">
|
<header className="header">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={20}>
|
<Col span={20}>
|
||||||
{ !this.state.record.id &&
|
{!this.state.record.id && <h1>Add subscriber</h1>}
|
||||||
<h1>Add subscriber</h1>
|
{this.state.record.id && (
|
||||||
}
|
|
||||||
{ this.state.record.id &&
|
|
||||||
<div>
|
<div>
|
||||||
<h1>
|
<h1>
|
||||||
<Tag color={ tagColors.hasOwnProperty(this.state.record.status) ? tagColors[this.state.record.status] : "" }>{ this.state.record.status }</Tag>
|
<Tag
|
||||||
{" "}
|
color={
|
||||||
|
tagColors.hasOwnProperty(this.state.record.status)
|
||||||
|
? tagColors[this.state.record.status]
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{this.state.record.status}
|
||||||
|
</Tag>{" "}
|
||||||
{this.state.record.name} ({this.state.record.email})
|
{this.state.record.name} ({this.state.record.email})
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-small text-grey">ID { this.state.record.id } / UUID { this.state.record.uuid }</span>
|
<span className="text-small text-grey">
|
||||||
|
ID {this.state.record.id} / UUID {this.state.record.uuid}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Col>
|
|
||||||
<Col span={ 2 }>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col span={2} />
|
||||||
</Row>
|
</Row>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
|
@ -271,7 +376,7 @@ class Subscriber extends React.PureComponent {
|
||||||
record={this.state.record}
|
record={this.state.record}
|
||||||
fetchRecord={this.fetchRecord}
|
fetchRecord={this.fetchRecord}
|
||||||
lists={this.props.data[cs.ModelLists]}
|
lists={this.props.data[cs.ModelLists]}
|
||||||
wrappedComponentRef={ (r) => {
|
wrappedComponentRef={r => {
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,29 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { Row, Col, Modal, Form, Input, Select, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification, Radio } from "antd"
|
import {
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
Icon,
|
||||||
|
Tooltip,
|
||||||
|
Tag,
|
||||||
|
Popconfirm,
|
||||||
|
notification,
|
||||||
|
Radio
|
||||||
|
} from "antd"
|
||||||
|
|
||||||
import Utils from "./utils"
|
import Utils from "./utils"
|
||||||
import Subscriber from "./Subscriber"
|
import Subscriber from "./Subscriber"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
|
|
||||||
const tagColors = {
|
const tagColors = {
|
||||||
"enabled": "green",
|
enabled: "green",
|
||||||
"blacklisted": "red"
|
blacklisted: "red"
|
||||||
}
|
}
|
||||||
|
|
||||||
class ListsFormDef extends React.PureComponent {
|
class ListsFormDef extends React.PureComponent {
|
||||||
|
@ -18,10 +32,11 @@ class ListsFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
handleSubmit = (e) => {
|
handleSubmit = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
var err = null, values = {}
|
var err = null,
|
||||||
|
values = {}
|
||||||
this.props.form.validateFields((e, v) => {
|
this.props.form.validateFields((e, v) => {
|
||||||
err = e
|
err = e
|
||||||
values = v
|
values = v
|
||||||
|
@ -38,15 +53,25 @@ class ListsFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ modalWaiting: true })
|
this.setState({ modalWaiting: true })
|
||||||
this.props.request(!this.props.allRowsSelected ? cs.Routes.AddSubscribersToLists : cs.Routes.AddSubscribersToListsByQuery,
|
this.props
|
||||||
cs.MethodPut, values).then(() => {
|
.request(
|
||||||
notification["success"]({ message: "Lists changed",
|
!this.props.allRowsSelected
|
||||||
description: `Lists changed for selected subscribers` })
|
? cs.Routes.AddSubscribersToLists
|
||||||
|
: cs.Routes.AddSubscribersToListsByQuery,
|
||||||
|
cs.MethodPut,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
message: "Lists changed",
|
||||||
|
description: `Lists changed for selected subscribers`
|
||||||
|
})
|
||||||
this.props.clearSelectedRows()
|
this.props.clearSelectedRows()
|
||||||
this.props.fetchRecords()
|
this.props.fetchRecords()
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
|
@ -60,13 +85,16 @@ class ListsFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal visible={ true } width="750px"
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
width="750px"
|
||||||
className="subscriber-lists-modal"
|
className="subscriber-lists-modal"
|
||||||
title="Manage lists"
|
title="Manage lists"
|
||||||
okText="Ok"
|
okText="Ok"
|
||||||
confirmLoading={this.state.modalWaiting}
|
confirmLoading={this.state.modalWaiting}
|
||||||
onCancel={this.props.onClose}
|
onCancel={this.props.onClose}
|
||||||
onOk={ this.handleSubmit }>
|
onOk={this.handleSubmit}
|
||||||
|
>
|
||||||
<Form onSubmit={this.handleSubmit}>
|
<Form onSubmit={this.handleSubmit}>
|
||||||
<Form.Item {...formItemLayout} label="Action">
|
<Form.Item {...formItemLayout} label="Action">
|
||||||
{getFieldDecorator("action", {
|
{getFieldDecorator("action", {
|
||||||
|
@ -81,13 +109,15 @@ class ListsFormDef extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item {...formItemLayout} label="Lists">
|
<Form.Item {...formItemLayout} label="Lists">
|
||||||
{getFieldDecorator("target_list_ids", { rules:[{ required: true }] })(
|
{getFieldDecorator("target_list_ids", {
|
||||||
|
rules: [{ required: true }]
|
||||||
|
})(
|
||||||
<Select mode="multiple">
|
<Select mode="multiple">
|
||||||
{[...this.props.lists].map((v, i) =>
|
{[...this.props.lists].map((v, i) => (
|
||||||
<Select.Option value={v.id} key={v.id}>
|
<Select.Option value={v.id} key={v.id}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
)}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -111,7 +141,9 @@ class Subscribers extends React.PureComponent {
|
||||||
page: 1,
|
page: 1,
|
||||||
total: 0,
|
total: 0,
|
||||||
perPage: this.defaultPerPage,
|
perPage: this.defaultPerPage,
|
||||||
listID: this.props.route.match.params.listID ? parseInt(this.props.route.match.params.listID, 10) : 0,
|
listID: this.props.route.match.params.listID
|
||||||
|
? parseInt(this.props.route.match.params.listID, 10)
|
||||||
|
: 0,
|
||||||
list: null,
|
list: null,
|
||||||
query: null,
|
query: null,
|
||||||
targetLists: []
|
targetLists: []
|
||||||
|
@ -142,32 +174,53 @@ class Subscribers extends React.PureComponent {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
// Table layout.
|
// Table layout.
|
||||||
this.columns = [{
|
this.columns = [
|
||||||
|
{
|
||||||
title: "E-mail",
|
title: "E-mail",
|
||||||
dataIndex: "email",
|
dataIndex: "email",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
width: "25%",
|
width: "25%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const out = [];
|
const out = []
|
||||||
out.push(
|
out.push(
|
||||||
<div key={`sub-email-${record.id}`} className="sub-name">
|
<div key={`sub-email-${record.id}`} className="sub-name">
|
||||||
<Link to={ `/subscribers/${record.id}` } onClick={(e) => {
|
<Link
|
||||||
|
to={`/subscribers/${record.id}`}
|
||||||
|
onClick={e => {
|
||||||
// Open the individual subscriber page on ctrl+click
|
// Open the individual subscriber page on ctrl+click
|
||||||
// and the modal otherwise.
|
// and the modal otherwise.
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey) {
|
||||||
this.handleShowEditForm(record)
|
this.handleShowEditForm(record)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}}>{ text }</Link>
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (record.lists.length > 0) {
|
if (record.lists.length > 0) {
|
||||||
for (let i = 0; i < record.lists.length; i++) {
|
for (let i = 0; i < record.lists.length; i++) {
|
||||||
out.push(<Tag className="list" key={`sub-${ record.id }-list-${ record.lists[i].id }`}>
|
out.push(
|
||||||
<Link to={ `/subscribers/lists/${ record.lists[i].id }` }>{ record.lists[i].name }</Link>
|
<Tag
|
||||||
<sup className={ "subscription-status " + record.lists[i].subscription_status }> { record.lists[i].subscription_status }</sup>
|
className="list"
|
||||||
</Tag>)
|
key={`sub-${record.id}-list-${record.lists[i].id}`}
|
||||||
|
>
|
||||||
|
<Link to={`/subscribers/lists/${record.lists[i].id}`}>
|
||||||
|
{record.lists[i].name}
|
||||||
|
</Link>
|
||||||
|
<sup
|
||||||
|
className={
|
||||||
|
"subscription-status " +
|
||||||
|
record.lists[i].subscription_status
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{record.lists[i].subscription_status}
|
||||||
|
</sup>
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,14 +234,19 @@ class Subscribers extends React.PureComponent {
|
||||||
width: "15%",
|
width: "15%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<Link to={ `/subscribers/${record.id}` } onClick={(e) => {
|
<Link
|
||||||
|
to={`/subscribers/${record.id}`}
|
||||||
|
onClick={e => {
|
||||||
// Open the individual subscriber page on ctrl+click
|
// Open the individual subscriber page on ctrl+click
|
||||||
// and the modal otherwise.
|
// and the modal otherwise.
|
||||||
if (!e.ctrlKey) {
|
if (!e.ctrlKey) {
|
||||||
this.handleShowEditForm(record)
|
this.handleShowEditForm(record)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
}}>{ text }</Link>
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -197,7 +255,13 @@ class Subscribers extends React.PureComponent {
|
||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
width: "5%",
|
width: "5%",
|
||||||
render: (status, _) => {
|
render: (status, _) => {
|
||||||
return <Tag color={ tagColors.hasOwnProperty(status) ? tagColors[status] : "" }>{ status }</Tag>
|
return (
|
||||||
|
<Tag
|
||||||
|
color={tagColors.hasOwnProperty(status) ? tagColors[status] : ""}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -206,7 +270,19 @@ class Subscribers extends React.PureComponent {
|
||||||
width: "10%",
|
width: "10%",
|
||||||
align: "center",
|
align: "center",
|
||||||
render: (lists, _) => {
|
render: (lists, _) => {
|
||||||
return <span>{ lists.reduce((def, item) => def + (item.subscription_status !== cs.SubscriptionStatusUnsubscribed ? 1 : 0), 0) }</span>
|
return (
|
||||||
|
<span>
|
||||||
|
{lists.reduce(
|
||||||
|
(def, item) =>
|
||||||
|
def +
|
||||||
|
(item.subscription_status !==
|
||||||
|
cs.SubscriptionStatusUnsubscribed
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -233,9 +309,23 @@ class Subscribers extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{/* <Tooltip title="Send an e-mail"><a role="button"><Icon type="rocket" /></a></Tooltip> */}
|
{/* <Tooltip title="Send an e-mail"><a role="button"><Icon type="rocket" /></a></Tooltip> */}
|
||||||
<Tooltip title="Edit subscriber"><a role="button" onClick={() => this.handleShowEditForm(record)}><Icon type="edit" /></a></Tooltip>
|
<Tooltip title="Edit subscriber">
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleDeleteRecord(record)}>
|
<a
|
||||||
<Tooltip title="Delete subscriber" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
role="button"
|
||||||
|
onClick={() => this.handleShowEditForm(record)}
|
||||||
|
>
|
||||||
|
<Icon type="edit" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.handleDeleteRecord(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete subscriber" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -246,12 +336,16 @@ class Subscribers extends React.PureComponent {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Load lists on boot.
|
// Load lists on boot.
|
||||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet).then(() => {
|
this.props
|
||||||
|
.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||||
|
.then(() => {
|
||||||
// If this is an individual list's view, pick up that list.
|
// If this is an individual list's view, pick up that list.
|
||||||
if (this.state.queryParams.listID) {
|
if (this.state.queryParams.listID) {
|
||||||
this.props.data[cs.ModelLists].forEach((l) => {
|
this.props.data[cs.ModelLists].forEach(l => {
|
||||||
if (l.id === this.state.queryParams.listID) {
|
if (l.id === this.state.queryParams.listID) {
|
||||||
this.setState({ queryParams: { ...this.state.queryParams, list: l }})
|
this.setState({
|
||||||
|
queryParams: { ...this.state.queryParams, list: l }
|
||||||
|
})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -261,7 +355,7 @@ class Subscribers extends React.PureComponent {
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecords = (params) => {
|
fetchRecords = params => {
|
||||||
let qParams = {
|
let qParams = {
|
||||||
page: this.state.queryParams.page,
|
page: this.state.queryParams.page,
|
||||||
per_page: this.state.queryParams.per_page,
|
per_page: this.state.queryParams.per_page,
|
||||||
|
@ -278,76 +372,128 @@ class Subscribers extends React.PureComponent {
|
||||||
qParams = { ...qParams, ...params }
|
qParams = { ...qParams, ...params }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.GetSubscribers, cs.MethodGet, qParams).then(() => {
|
this.props
|
||||||
this.setState({ queryParams: {
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.GetSubscribers,
|
||||||
|
cs.MethodGet,
|
||||||
|
qParams
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({
|
||||||
|
queryParams: {
|
||||||
...this.state.queryParams,
|
...this.state.queryParams,
|
||||||
total: this.props.data[cs.ModelSubscribers].total,
|
total: this.props.data[cs.ModelSubscribers].total,
|
||||||
perPage: this.props.data[cs.ModelSubscribers].per_page,
|
perPage: this.props.data[cs.ModelSubscribers].per_page,
|
||||||
page: this.props.data[cs.ModelSubscribers].page,
|
page: this.props.data[cs.ModelSubscribers].page,
|
||||||
query: this.props.data[cs.ModelSubscribers].query,
|
query: this.props.data[cs.ModelSubscribers].query
|
||||||
}})
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecord = (record) => {
|
handleDeleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscriber, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.DeleteSubscriber,
|
||||||
|
cs.MethodDelete,
|
||||||
|
{ id: record.id }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber deleted", description: `${record.email} deleted` })
|
notification["success"]({
|
||||||
|
message: "Subscriber deleted",
|
||||||
|
description: `${record.email} deleted`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecords = (records) => {
|
handleDeleteRecords = records => {
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscribers, cs.MethodDelete, { id: records.map(r => r.id) })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.DeleteSubscribers,
|
||||||
|
cs.MethodDelete,
|
||||||
|
{ id: records.map(r => r.id) }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber(s) deleted", description: "Selected subscribers deleted" })
|
notification["success"]({
|
||||||
|
message: "Subscriber(s) deleted",
|
||||||
|
description: "Selected subscribers deleted"
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlacklistSubscribers = (records) => {
|
handleBlacklistSubscribers = records => {
|
||||||
this.props.request(cs.Routes.BlacklistSubscribers, cs.MethodPut, { ids: records.map(r => r.id) })
|
this.props
|
||||||
|
.request(cs.Routes.BlacklistSubscribers, cs.MethodPut, {
|
||||||
|
ids: records.map(r => r.id)
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber(s) blacklisted", description: "Selected subscribers blacklisted" })
|
notification["success"]({
|
||||||
|
message: "Subscriber(s) blacklisted",
|
||||||
|
description: "Selected subscribers blacklisted"
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arbitrary query based calls.
|
// Arbitrary query based calls.
|
||||||
handleDeleteRecordsByQuery = (listIDs, query) => {
|
handleDeleteRecordsByQuery = (listIDs, query) => {
|
||||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscribersByQuery, cs.MethodPost,
|
this.props
|
||||||
{ list_ids: listIDs, query: query })
|
.modelRequest(
|
||||||
|
cs.ModelSubscribers,
|
||||||
|
cs.Routes.DeleteSubscribersByQuery,
|
||||||
|
cs.MethodPost,
|
||||||
|
{ list_ids: listIDs, query: query }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber(s) deleted", description: "Selected subscribers have been deleted" })
|
notification["success"]({
|
||||||
|
message: "Subscriber(s) deleted",
|
||||||
|
description: "Selected subscribers have been deleted"
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBlacklistSubscribersByQuery = (listIDs, query) => {
|
handleBlacklistSubscribersByQuery = (listIDs, query) => {
|
||||||
this.props.request(cs.Routes.BlacklistSubscribersByQuery, cs.MethodPut,
|
this.props
|
||||||
{ list_ids: listIDs, query: query })
|
.request(cs.Routes.BlacklistSubscribersByQuery, cs.MethodPut, {
|
||||||
|
list_ids: listIDs,
|
||||||
|
query: query
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ message: "Subscriber(s) blacklisted", description: "Selected subscribers have been blacklisted" })
|
notification["success"]({
|
||||||
|
message: "Subscriber(s) blacklisted",
|
||||||
|
description: "Selected subscribers have been blacklisted"
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -359,10 +505,16 @@ class Subscribers extends React.PureComponent {
|
||||||
target_lists: targetLists
|
target_lists: targetLists
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.request(cs.Routes.QuerySubscribersIntoLists, cs.MethodPost, params).then((res) => {
|
this.props
|
||||||
notification["success"]({ message: "Subscriber(s) added", description: `${ res.data.data.count } added` })
|
.request(cs.Routes.QuerySubscribersIntoLists, cs.MethodPost, params)
|
||||||
|
.then(res => {
|
||||||
|
notification["success"]({
|
||||||
|
message: "Subscriber(s) added",
|
||||||
|
description: `${res.data.data.count} added`
|
||||||
|
})
|
||||||
this.handleToggleListModal()
|
this.handleToggleListModal()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -375,7 +527,7 @@ class Subscribers extends React.PureComponent {
|
||||||
this.setState({ formType: cs.FormCreate, attribs: [], record: {} })
|
this.setState({ formType: cs.FormCreate, attribs: [], record: {} })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowEditForm = (record) => {
|
handleShowEditForm = record => {
|
||||||
this.setState({ formType: cs.FormEdit, record: record })
|
this.setState({ formType: cs.FormEdit, record: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,7 +535,7 @@ class Subscribers extends React.PureComponent {
|
||||||
this.setState({ listsFormVisible: !this.state.listsFormVisible })
|
this.setState({ listsFormVisible: !this.state.listsFormVisible })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSearch = (q) => {
|
handleSearch = q => {
|
||||||
q = q.trim().toLowerCase()
|
q = q.trim().toLowerCase()
|
||||||
if (q === "") {
|
if (q === "") {
|
||||||
this.fetchRecords({ query: null })
|
this.fetchRecords({ query: null })
|
||||||
|
@ -400,8 +552,10 @@ class Subscribers extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectAllRows = () => {
|
handleSelectAllRows = () => {
|
||||||
this.setState({ allRowsSelected: true,
|
this.setState({
|
||||||
selectedRows: this.props.data[cs.ModelSubscribers].results })
|
allRowsSelected: true,
|
||||||
|
selectedRows: this.props.data[cs.ModelSubscribers].results
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSelectedRows = (_, records) => {
|
clearSelectedRows = (_, records) => {
|
||||||
|
@ -435,14 +589,22 @@ class Subscribers extends React.PureComponent {
|
||||||
<Col span={20}>
|
<Col span={20}>
|
||||||
<h1>
|
<h1>
|
||||||
Subscribers
|
Subscribers
|
||||||
{ this.props.data[cs.ModelSubscribers].total > 0 &&
|
{this.props.data[cs.ModelSubscribers].total > 0 && (
|
||||||
<span> ({ this.props.data[cs.ModelSubscribers].total })</span> }
|
<span> ({this.props.data[cs.ModelSubscribers].total})</span>
|
||||||
{ this.state.queryParams.list &&
|
)}
|
||||||
<span> » { this.state.queryParams.list.name }</span> }
|
{this.state.queryParams.list && (
|
||||||
|
<span> » {this.state.queryParams.list.name}</span>
|
||||||
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button type="primary" icon="plus" onClick={ this.handleShowCreateForm }>Add subscriber</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="plus"
|
||||||
|
onClick={this.handleShowCreateForm}
|
||||||
|
>
|
||||||
|
Add subscriber
|
||||||
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</header>
|
</header>
|
||||||
|
@ -457,29 +619,40 @@ class Subscribers extends React.PureComponent {
|
||||||
name="name"
|
name="name"
|
||||||
placeholder="Name or e-mail"
|
placeholder="Name or e-mail"
|
||||||
enterButton
|
enterButton
|
||||||
onSearch={ this.handleSearch } />
|
onSearch={this.handleSearch}
|
||||||
{" "}
|
/>{" "}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={8} offset={1}>
|
<Col span={8} offset={1}>
|
||||||
<label> </label><br />
|
<label> </label>
|
||||||
|
<br />
|
||||||
<a role="button" onClick={this.handleToggleQueryForm}>
|
<a role="button" onClick={this.handleToggleQueryForm}>
|
||||||
<Icon type="setting" /> Advanced</a>
|
<Icon type="setting" /> Advanced
|
||||||
|
</a>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
{ this.state.queryFormVisible &&
|
{this.state.queryFormVisible && (
|
||||||
<div className="advanced-query">
|
<div className="advanced-query">
|
||||||
<p>
|
<p>
|
||||||
<label>Advanced query</label>
|
<label>Advanced query</label>
|
||||||
<Input.TextArea placeholder="subscribers.name LIKE '%user%' or subscribers.status='blacklisted'"
|
<Input.TextArea
|
||||||
|
placeholder="subscribers.name LIKE '%user%' or subscribers.status='blacklisted'"
|
||||||
id="subscriber-query"
|
id="subscriber-query"
|
||||||
rows={10}
|
rows={10}
|
||||||
onChange={(e) => {
|
onChange={e => {
|
||||||
this.setState({ queryParams: { ...this.state.queryParams, query: e.target.value } })
|
this.setState({
|
||||||
|
queryParams: {
|
||||||
|
...this.state.queryParams,
|
||||||
|
query: e.target.value
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
value={this.state.queryParams.query}
|
value={this.state.queryParams.query}
|
||||||
autosize={{ minRows: 2, maxRows: 10 }} />
|
autosize={{ minRows: 2, maxRows: 10 }}
|
||||||
|
/>
|
||||||
<span className="text-tiny text-small">
|
<span className="text-tiny text-small">
|
||||||
Write a partial SQL expression to query the subscribers based on their primary information or attributes. Learn more.
|
Write a partial SQL expression to query the subscribers
|
||||||
|
based on their primary information or attributes. Learn
|
||||||
|
more.
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -487,58 +660,101 @@ class Subscribers extends React.PureComponent {
|
||||||
disabled={this.state.queryParams.query === ""}
|
disabled={this.state.queryParams.query === ""}
|
||||||
type="primary"
|
type="primary"
|
||||||
icon="search"
|
icon="search"
|
||||||
onClick={ () => { this.fetchRecords() } }>Query</Button>
|
onClick={() => {
|
||||||
{" "}
|
this.fetchRecords()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Query
|
||||||
|
</Button>{" "}
|
||||||
<Button
|
<Button
|
||||||
disabled={this.state.queryParams.query === ""}
|
disabled={this.state.queryParams.query === ""}
|
||||||
icon="refresh"
|
icon="refresh"
|
||||||
onClick={ () => { this.fetchRecords({ query: null }) } }>Reset</Button>
|
onClick={() => {
|
||||||
|
this.fetchRecords({ query: null })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={14}>
|
<Col span={14}>
|
||||||
{ this.state.selectedRows.length > 0 &&
|
{this.state.selectedRows.length > 0 && (
|
||||||
<nav className="table-options">
|
<nav className="table-options">
|
||||||
<p>
|
<p>
|
||||||
<strong>{ this.state.allRowsSelected ? this.state.queryParams.total : this.state.selectedRows.length }</strong>
|
<strong>
|
||||||
{" "} subscriber(s) selected
|
{this.state.allRowsSelected
|
||||||
{ !this.state.allRowsSelected && this.state.queryParams.total > this.state.queryParams.perPage &&
|
? this.state.queryParams.total
|
||||||
<span> — <a role="button" onClick={ this.handleSelectAllRows }>
|
: this.state.selectedRows.length}
|
||||||
Select all { this.state.queryParams.total }?</a>
|
</strong>{" "}
|
||||||
|
subscriber(s) selected
|
||||||
|
{!this.state.allRowsSelected &&
|
||||||
|
this.state.queryParams.total >
|
||||||
|
this.state.queryParams.perPage && (
|
||||||
|
<span>
|
||||||
|
{" "}
|
||||||
|
—{" "}
|
||||||
|
<a role="button" onClick={this.handleSelectAllRows}>
|
||||||
|
Select all {this.state.queryParams.total}?
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a role="button" onClick={this.handleToggleListsForm}>
|
<a role="button" onClick={this.handleToggleListsForm}>
|
||||||
<Icon type="bars" /> Manage lists
|
<Icon type="bars" /> Manage lists
|
||||||
</a>
|
</a>
|
||||||
<a role="button"><Icon type="rocket" /> Send campaign</a>
|
<a role="button">
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => {
|
<Icon type="rocket" /> Send campaign
|
||||||
|
</a>
|
||||||
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => {
|
||||||
if (this.state.allRowsSelected) {
|
if (this.state.allRowsSelected) {
|
||||||
this.handleDeleteRecordsByQuery(this.state.queryParams.listID ? [this.state.queryParams.listID] : [], this.state.queryParams.query)
|
this.handleDeleteRecordsByQuery(
|
||||||
|
this.state.queryParams.listID
|
||||||
|
? [this.state.queryParams.listID]
|
||||||
|
: [],
|
||||||
|
this.state.queryParams.query
|
||||||
|
)
|
||||||
this.clearSelectedRows()
|
this.clearSelectedRows()
|
||||||
} else {
|
} else {
|
||||||
this.handleDeleteRecords(this.state.selectedRows)
|
this.handleDeleteRecords(this.state.selectedRows)
|
||||||
this.clearSelectedRows()
|
this.clearSelectedRows()
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
<a role="button"><Icon type="delete" /> Delete</a>
|
>
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" /> Delete
|
||||||
|
</a>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => {
|
<Popconfirm
|
||||||
|
title="Are you sure?"
|
||||||
|
onConfirm={() => {
|
||||||
if (this.state.allRowsSelected) {
|
if (this.state.allRowsSelected) {
|
||||||
this.handleBlacklistSubscribersByQuery(this.state.queryParams.listID ? [this.state.queryParams.listID] : [], this.state.queryParams.query)
|
this.handleBlacklistSubscribersByQuery(
|
||||||
|
this.state.queryParams.listID
|
||||||
|
? [this.state.queryParams.listID]
|
||||||
|
: [],
|
||||||
|
this.state.queryParams.query
|
||||||
|
)
|
||||||
this.clearSelectedRows()
|
this.clearSelectedRows()
|
||||||
} else {
|
} else {
|
||||||
this.handleBlacklistSubscribers(this.state.selectedRows)
|
this.handleBlacklistSubscribers(
|
||||||
|
this.state.selectedRows
|
||||||
|
)
|
||||||
this.clearSelectedRows()
|
this.clearSelectedRows()
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
<a role="button"><Icon type="close" /> Blacklist</a>
|
>
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="close" /> Blacklist
|
||||||
|
</a>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</p>
|
</p>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
@ -556,18 +772,20 @@ class Subscribers extends React.PureComponent {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ this.state.formType !== null &&
|
{this.state.formType !== null && (
|
||||||
<Modal visible={ true } width="750px"
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
width="750px"
|
||||||
className="subscriber-modal"
|
className="subscriber-modal"
|
||||||
okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
|
okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
|
||||||
confirmLoading={this.state.modalWaiting}
|
confirmLoading={this.state.modalWaiting}
|
||||||
onOk={(e) => {
|
onOk={e => {
|
||||||
if (!this.state.modalForm) {
|
if (!this.state.modalForm) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This submits the form embedded in the Subscriber component.
|
// This submits the form embedded in the Subscriber component.
|
||||||
this.state.modalForm.submitForm(e, (ok) => {
|
this.state.modalForm.submitForm(e, ok => {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
this.handleHideForm()
|
this.handleHideForm()
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
|
@ -575,31 +793,44 @@ class Subscribers extends React.PureComponent {
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onCancel={this.handleHideForm}
|
onCancel={this.handleHideForm}
|
||||||
okButtonProps={{ disabled: this.props.reqStates[cs.ModelSubscribers] === cs.StatePending }}>
|
okButtonProps={{
|
||||||
<Subscriber {...this.props}
|
disabled:
|
||||||
|
this.props.reqStates[cs.ModelSubscribers] === cs.StatePending
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Subscriber
|
||||||
|
{...this.props}
|
||||||
isModal={true}
|
isModal={true}
|
||||||
formType={this.state.formType}
|
formType={this.state.formType}
|
||||||
record={this.state.record}
|
record={this.state.record}
|
||||||
ref={ (r) => {
|
ref={r => {
|
||||||
if (!r) {
|
if (!r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ modalForm: r })
|
this.setState({ modalForm: r })
|
||||||
}}/>
|
}}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{ this.state.listsFormVisible && <ListsForm {...this.props}
|
{this.state.listsFormVisible && (
|
||||||
|
<ListsForm
|
||||||
|
{...this.props}
|
||||||
lists={this.props.data[cs.ModelLists]}
|
lists={this.props.data[cs.ModelLists]}
|
||||||
allRowsSelected={this.state.allRowsSelected}
|
allRowsSelected={this.state.allRowsSelected}
|
||||||
selectedRows={this.state.selectedRows}
|
selectedRows={this.state.selectedRows}
|
||||||
selectedLists={ this.state.queryParams.listID ? [this.state.queryParams.listID] : []}
|
selectedLists={
|
||||||
|
this.state.queryParams.listID
|
||||||
|
? [this.state.queryParams.listID]
|
||||||
|
: []
|
||||||
|
}
|
||||||
clearSelectedRows={this.clearSelectedRows}
|
clearSelectedRows={this.clearSelectedRows}
|
||||||
query={this.state.queryParams.query}
|
query={this.state.queryParams.query}
|
||||||
fetchRecords={this.fetchRecords}
|
fetchRecords={this.fetchRecords}
|
||||||
onClose={ this.handleToggleListsForm } />
|
onClose={this.handleToggleListsForm}
|
||||||
}
|
/>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
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 ModalPreview from "./ModalPreview"
|
||||||
import Utils from "./utils"
|
import Utils from "./utils"
|
||||||
|
@ -14,7 +28,7 @@ class CreateFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle create / edit form submission.
|
// Handle create / edit form submission.
|
||||||
handleSubmit = (e) => {
|
handleSubmit = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.props.form.validateFields((err, values) => {
|
this.props.form.validateFields((err, values) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -24,31 +38,63 @@ class CreateFormDef extends React.PureComponent {
|
||||||
this.setState({ modalWaiting: true })
|
this.setState({ modalWaiting: true })
|
||||||
if (this.props.formType === cs.FormCreate) {
|
if (this.props.formType === cs.FormCreate) {
|
||||||
// Create a new list.
|
// Create a new list.
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.CreateTemplate, cs.MethodPost, values).then(() => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Template added", description: `"${values["name"]}" added` })
|
.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.CreateTemplate,
|
||||||
|
cs.MethodPost,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Template added",
|
||||||
|
description: `"${values["name"]}" added`
|
||||||
|
})
|
||||||
this.props.fetchRecords()
|
this.props.fetchRecords()
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Edit a list.
|
// Edit a list.
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.UpdateTemplate, cs.MethodPut, { ...values, id: this.props.record.id }).then(() => {
|
this.props
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Template updated", description: `"${values["name"]}" modified` })
|
.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.UpdateTemplate,
|
||||||
|
cs.MethodPut,
|
||||||
|
{ ...values, id: this.props.record.id }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Template updated",
|
||||||
|
description: `"${values["name"]}" modified`
|
||||||
|
})
|
||||||
this.props.fetchRecords()
|
this.props.fetchRecords()
|
||||||
this.props.onClose()
|
this.props.onClose()
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
this.setState({ modalWaiting: false })
|
this.setState({ modalWaiting: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmBlur = (e) => {
|
handleConfirmBlur = e => {
|
||||||
const value = e.target.value
|
const value = e.target.value
|
||||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||||
}
|
}
|
||||||
|
@ -72,15 +118,21 @@ class CreateFormDef extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
|
<Modal
|
||||||
|
visible={true}
|
||||||
|
title={formType === cs.FormCreate ? "Add template" : record.name}
|
||||||
okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
|
okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
|
||||||
width="90%"
|
width="90%"
|
||||||
height={900}
|
height={900}
|
||||||
confirmLoading={this.state.modalWaiting}
|
confirmLoading={this.state.modalWaiting}
|
||||||
onCancel={onClose}
|
onCancel={onClose}
|
||||||
onOk={ this.handleSubmit }>
|
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", {
|
||||||
|
@ -89,37 +141,60 @@ class CreateFormDef extends React.PureComponent {
|
||||||
})(<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", {
|
||||||
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />
|
initialValue: record.body ? record.body : "",
|
||||||
)}
|
rules: [{ required: true }]
|
||||||
|
})(<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{ this.props.form.getFieldValue("body") !== "" &&
|
{this.props.form.getFieldValue("body") !== "" && (
|
||||||
<Form.Item {...formItemLayout} colon={false} label=" ">
|
<Form.Item {...formItemLayout} colon={false} label=" ">
|
||||||
<Button icon="search" onClick={ () =>
|
<Button
|
||||||
this.handlePreview(this.props.form.getFieldValue("name"), this.props.form.getFieldValue("body"))
|
icon="search"
|
||||||
}>Preview</Button>
|
onClick={() =>
|
||||||
</Form.Item>
|
this.handlePreview(
|
||||||
|
this.props.form.getFieldValue("name"),
|
||||||
|
this.props.form.getFieldValue("body")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Spin>
|
</Spin>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span="4"></Col>
|
<Col span="4" />
|
||||||
<Col span="18" className="text-grey text-small">
|
<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>.
|
The placeholder{" "}
|
||||||
|
<code>
|
||||||
|
{"{"}
|
||||||
|
{"{"} template "content" . {"}"}
|
||||||
|
{"}"}
|
||||||
|
</code>{" "}
|
||||||
|
should appear in the template.{" "}
|
||||||
|
<a href="" target="_blank">
|
||||||
|
Read more on templating
|
||||||
|
</a>
|
||||||
|
.
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{ this.state.previewBody &&
|
{this.state.previewBody && (
|
||||||
<ModalPreview
|
<ModalPreview
|
||||||
title={ this.state.previewName ? this.state.previewName : "Template preview" }
|
title={
|
||||||
|
this.state.previewName
|
||||||
|
? this.state.previewName
|
||||||
|
: "Template preview"
|
||||||
|
}
|
||||||
previewURL={cs.Routes.PreviewNewTemplate}
|
previewURL={cs.Routes.PreviewNewTemplate}
|
||||||
body={this.state.previewBody}
|
body={this.state.previewBody}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ previewBody: null, previewName: null })
|
this.setState({ previewBody: null, previewName: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -137,7 +212,8 @@ class Templates extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.columns = [{
|
this.columns = [
|
||||||
|
{
|
||||||
title: "Name",
|
title: "Name",
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
@ -145,9 +221,14 @@ class Templates extends React.PureComponent {
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="name">
|
<div className="name">
|
||||||
<a role="button" onClick={() => this.handleShowEditForm(record)}>{ text }</a>
|
<a role="button" onClick={() => this.handleShowEditForm(record)}>
|
||||||
{ record.is_default &&
|
{text}
|
||||||
<div><Tag>Default</Tag></div>}
|
</a>
|
||||||
|
{record.is_default && (
|
||||||
|
<div>
|
||||||
|
<Tag>Default</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -174,25 +255,54 @@ class Templates extends React.PureComponent {
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<Tooltip title="Preview template" onClick={() => this.handlePreview(record)}><a role="button"><Icon type="search" /></a></Tooltip>
|
<Tooltip
|
||||||
|
title="Preview template"
|
||||||
|
onClick={() => this.handlePreview(record)}
|
||||||
|
>
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="search" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{ !record.is_default &&
|
{!record.is_default && (
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleSetDefault(record)}>
|
<Popconfirm
|
||||||
<Tooltip title="Set as default" placement="bottom"><a role="button"><Icon type="check" /></a></Tooltip>
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.handleSetDefault(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Set as default" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="check" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<Tooltip title="Edit template"><a role="button" onClick={() => this.handleShowEditForm(record)}><Icon type="edit" /></a></Tooltip>
|
<Tooltip title="Edit template">
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => this.handleShowEditForm(record)}
|
||||||
|
>
|
||||||
|
<Icon type="edit" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
{ record.id !== 1 &&
|
{record.id !== 1 && (
|
||||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleDeleteRecord(record)}>
|
<Popconfirm
|
||||||
<Tooltip title="Delete template" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
title="Are you sure?"
|
||||||
|
onConfirm={() => this.handleDeleteRecord(record)}
|
||||||
|
>
|
||||||
|
<Tooltip title="Delete template" placement="bottom">
|
||||||
|
<a role="button">
|
||||||
|
<Icon type="delete" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -201,34 +311,64 @@ class Templates extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchRecords = () => {
|
fetchRecords = () => {
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.GetTemplates, cs.MethodGet)
|
this.props.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.GetTemplates,
|
||||||
|
cs.MethodGet
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteRecord = (record) => {
|
handleDeleteRecord = record => {
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.DeleteTemplate, cs.MethodDelete, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.DeleteTemplate,
|
||||||
|
cs.MethodDelete,
|
||||||
|
{ id: record.id }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Template deleted", description: `"${record.name}" deleted` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Template deleted",
|
||||||
|
description: `"${record.name}" deleted`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
|
.catch(e => {
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
notification["error"]({ message: "Error", description: e.message })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSetDefault = (record) => {
|
handleSetDefault = record => {
|
||||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.SetDefaultTemplate, cs.MethodPut, { id: record.id })
|
this.props
|
||||||
|
.modelRequest(
|
||||||
|
cs.ModelTemplates,
|
||||||
|
cs.Routes.SetDefaultTemplate,
|
||||||
|
cs.MethodPut,
|
||||||
|
{ id: record.id }
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
notification["success"]({ placement: cs.MsgPosition, message: "Template updated", description: `"${record.name}" set as default` })
|
notification["success"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Template updated",
|
||||||
|
description: `"${record.name}" set as default`
|
||||||
|
})
|
||||||
|
|
||||||
// Reload the table.
|
// Reload the table.
|
||||||
this.fetchRecords()
|
this.fetchRecords()
|
||||||
}).catch(e => {
|
})
|
||||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
.catch(e => {
|
||||||
|
notification["error"]({
|
||||||
|
placement: cs.MsgPosition,
|
||||||
|
message: "Error",
|
||||||
|
description: e.message
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePreview = (record) => {
|
handlePreview = record => {
|
||||||
this.setState({ previewRecord: record })
|
this.setState({ previewRecord: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +380,7 @@ class Templates extends React.PureComponent {
|
||||||
this.setState({ formType: cs.FormCreate, record: {} })
|
this.setState({ formType: cs.FormCreate, record: {} })
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShowEditForm = (record) => {
|
handleShowEditForm = record => {
|
||||||
this.setState({ formType: cs.FormEdit, record: record })
|
this.setState({ formType: cs.FormEdit, record: record })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,9 +388,17 @@ class Templates extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<section className="content templates">
|
<section className="content templates">
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={22}><h1>Templates ({this.props.data[cs.ModelTemplates].length}) </h1></Col>
|
<Col span={22}>
|
||||||
|
<h1>Templates ({this.props.data[cs.ModelTemplates].length}) </h1>
|
||||||
|
</Col>
|
||||||
<Col span={2}>
|
<Col span={2}>
|
||||||
<Button type="primary" icon="plus" onClick={ this.handleShowCreateForm }>Add template</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon="plus"
|
||||||
|
onClick={this.handleShowCreateForm}
|
||||||
|
>
|
||||||
|
Add template
|
||||||
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<br />
|
<br />
|
||||||
|
@ -263,22 +411,26 @@ class Templates extends React.PureComponent {
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CreateForm { ...this.props }
|
<CreateForm
|
||||||
|
{...this.props}
|
||||||
formType={this.state.formType}
|
formType={this.state.formType}
|
||||||
record={this.state.record}
|
record={this.state.record}
|
||||||
onClose={this.hideForm}
|
onClose={this.hideForm}
|
||||||
fetchRecords={this.fetchRecords}
|
fetchRecords={this.fetchRecords}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ this.state.previewRecord &&
|
{this.state.previewRecord && (
|
||||||
<ModalPreview
|
<ModalPreview
|
||||||
title={this.state.previewRecord.name}
|
title={this.state.previewRecord.name}
|
||||||
previewURL={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }
|
previewURL={cs.Routes.PreviewTemplate.replace(
|
||||||
|
":id",
|
||||||
|
this.state.previewRecord.id
|
||||||
|
)}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
this.setState({ previewRecord: null })
|
this.setState({ previewRecord: null })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import ReactQuill from "react-quill"
|
|
||||||
import "react-quill/dist/quill.snow.css"
|
|
||||||
|
|
||||||
const 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": function() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class QuillEditor extends React.Component {
|
|
||||||
componentDidMount() {
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ReactQuill
|
|
||||||
modules={ quillModules }
|
|
||||||
value="<h2>Welcome</h2>"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QuillEditor;
|
|
|
@ -36,7 +36,7 @@ export const CampaignStatusColors = {
|
||||||
running: "blue",
|
running: "blue",
|
||||||
paused: "orange",
|
paused: "orange",
|
||||||
finished: "green",
|
finished: "green",
|
||||||
cancelled: "red",
|
cancelled: "red"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CampaignStatusDraft = "draft"
|
export const CampaignStatusDraft = "draft"
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React from 'react';
|
import React from "react"
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom"
|
||||||
|
|
||||||
import './index.css';
|
import "./index.css"
|
||||||
import App from './App.js'
|
import App from "./App.js"
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById("root"))
|
||||||
ReactDOM.render((<App />), document.getElementById('root'))
|
|
||||||
|
|
|
@ -1,11 +1,23 @@
|
||||||
import React from 'react'
|
import React from "react"
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom"
|
||||||
|
|
||||||
import { Alert } from 'antd';
|
|
||||||
|
|
||||||
|
import { Alert } from "antd"
|
||||||
|
|
||||||
class Utils {
|
class Utils {
|
||||||
static months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
|
static months = [
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec"
|
||||||
|
]
|
||||||
static days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
static days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||||
|
|
||||||
// Converts the ISO date format to a simpler form.
|
// Converts the ISO date format to a simpler form.
|
||||||
|
@ -15,8 +27,15 @@ class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
let d = new Date(stamp)
|
let d = new Date(stamp)
|
||||||
|
let out =
|
||||||
|
Utils.days[d.getDay()] +
|
||||||
|
", " +
|
||||||
|
d.getDate() +
|
||||||
|
" " +
|
||||||
|
Utils.months[d.getMonth()] +
|
||||||
|
" " +
|
||||||
|
d.getFullYear()
|
||||||
|
|
||||||
let out = Utils.days[d.getDay()] + ", " + d.getDate() + " " + Utils.months[d.getMonth()] + " " + d.getFullYear()
|
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
out += " " + d.getHours() + ":" + d.getMinutes()
|
out += " " + d.getHours() + ":" + d.getMinutes()
|
||||||
}
|
}
|
||||||
|
@ -25,34 +44,38 @@ class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HttpError takes an axios error and returns an error dict after some sanity checks.
|
// HttpError takes an axios error and returns an error dict after some sanity checks.
|
||||||
static HttpError = (err) => {
|
static HttpError = err => {
|
||||||
if (!err.response) {
|
if (!err.response) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!err.response.data || !err.response.data.message) {
|
if (!err.response.data || !err.response.data.message) {
|
||||||
return {
|
return {
|
||||||
"message": err.message + " - " + err.response.request.responseURL,
|
message: err.message + " - " + err.response.request.responseURL,
|
||||||
"data": {}
|
data: {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"message": err.response.data.message,
|
message: err.response.data.message,
|
||||||
"data": err.response.data.data
|
data: err.response.data.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shows a flash message.
|
// Shows a flash message.
|
||||||
static Alert = (msg, msgType) => {
|
static Alert = (msg, msgType) => {
|
||||||
document.getElementById('alert-container').classList.add('visible')
|
document.getElementById("alert-container").classList.add("visible")
|
||||||
ReactDOM.render(<Alert message={ msg } type={ msgType } showIcon />,
|
ReactDOM.render(
|
||||||
document.getElementById('alert-container'))
|
<Alert message={msg} type={msgType} showIcon />,
|
||||||
|
document.getElementById("alert-container")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
static ModalAlert = (msg, msgType) => {
|
static ModalAlert = (msg, msgType) => {
|
||||||
document.getElementById('modal-alert-container').classList.add('visible')
|
document.getElementById("modal-alert-container").classList.add("visible")
|
||||||
ReactDOM.render(<Alert message={ msg } type={ msgType } showIcon />,
|
ReactDOM.render(
|
||||||
document.getElementById('modal-alert-container'))
|
<Alert message={msg} type={msgType} showIcon />,
|
||||||
|
document.getElementById("modal-alert-container")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,8 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/lib/pq"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Queries contains all prepared SQL queries.
|
// Queries contains all prepared SQL queries.
|
||||||
|
|
Loading…
Reference in New Issue