Reformat all JS to 'prettier' style

This commit is contained in:
Kailash Nadh 2019-03-09 13:16:47 +05:30
parent 5b42e8659f
commit 917cb8aeed
20 changed files with 4639 additions and 3253 deletions

View File

@ -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
}; }

View File

@ -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"
} }
} }

View File

@ -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])

View File

@ -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="&nbsp;" colon={false}> <Form.Item {...formItemLayout} label="&nbsp;" 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 } &mdash; UUID { this.state.record.uuid }</span> <span className="text-tiny text-grey">
ID {this.state.record.id} &mdash; 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>
) )
} }

View File

@ -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 &mdash; { dayjs(record.send_at).format(cs.DateFormat) }</span> <span className="text-tiny date">
} Scheduled &mdash;{" "}
{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>
)}
&nbsp; &nbsp;
{ 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>
) )
} }

View File

@ -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

View File

@ -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);
});

View File

@ -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 &mdash; {this.props.importState.name}</h1> <h1>Importing &mdash; {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>

View File

@ -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"
&copy; 2019 { year != 2019 ? " - " + year : "" } rel="noreferrer noopener"
target="_blank"
>
listmonk
</a>{" "}
&copy; 2019 {year !== 2019 ? " - " + year : ""}
</span> </span>
</Footer> </Footer>
</Layout> </Layout>

View File

@ -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}

View File

@ -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>
) )
} }

View File

@ -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>
) )
} }
} }

View File

@ -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
} }

View File

@ -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> &raquo; { this.state.queryParams.list.name }</span> } {this.state.queryParams.list && (
<span> &raquo; {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>&nbsp;</label><br /> <label>&nbsp;</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> &mdash; <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>
{" "}
&mdash;{" "}
<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>
) )
} }

View File

@ -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="&nbsp;"> <Form.Item {...formItemLayout} colon={false} label="&nbsp;">
<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>
) )
} }

View File

@ -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;

View File

@ -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"

View File

@ -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'))

View File

@ -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")
)
} }
} }

View File

@ -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.