Reformat all JS to 'prettier' style
This commit is contained in:
parent
5b42e8659f
commit
917cb8aeed
|
@ -1,21 +1,22 @@
|
|||
const {injectBabelPlugin} = require("react-app-rewired");
|
||||
const rewireLess = require("react-app-rewire-less");
|
||||
const { injectBabelPlugin } = require("react-app-rewired")
|
||||
const rewireLess = require("react-app-rewire-less")
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
config = injectBabelPlugin(
|
||||
[
|
||||
"import", {
|
||||
libraryName: "antd",
|
||||
libraryDirectory: "es",
|
||||
style: true
|
||||
}
|
||||
], // change importing css to less
|
||||
config,
|
||||
);
|
||||
[
|
||||
"import",
|
||||
{
|
||||
libraryName: "antd",
|
||||
libraryDirectory: "es",
|
||||
style: true
|
||||
}
|
||||
], // change importing css to less
|
||||
config
|
||||
)
|
||||
config = rewireLess.withLoaderOptions({
|
||||
modifyVars: {
|
||||
"@font-family":
|
||||
'"IBM Plex Sans", "Helvetica Neueue", "Segoe UI", "sans-serif"',
|
||||
'"IBM Plex Sans", "Helvetica Neueue", "Segoe UI", "sans-serif"',
|
||||
"@font-size-base": "15px",
|
||||
"@primary-color": "#7f2aff",
|
||||
"@shadow-1-up": "0 -2px 3px @shadow-color",
|
||||
|
@ -24,7 +25,7 @@ module.exports = function override(config, env) {
|
|||
"@shadow-1-right": "2px 0 3px @shadow-color",
|
||||
"@shadow-2": "0 2px 6px @shadow-color"
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
})(config, env);
|
||||
return config;
|
||||
};
|
||||
javascriptEnabled: true
|
||||
})(config, env)
|
||||
return config
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-import": "^1.11.0",
|
||||
"less-plugin-npm-import": "^2.1.0"
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"less-plugin-npm-import": "^2.1.0",
|
||||
"prettier": "1.15.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from 'react'
|
||||
import Utils from './utils'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import React from "react"
|
||||
import Utils from "./utils"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Icon, notification } from "antd"
|
||||
import axios from 'axios'
|
||||
import qs from 'qs'
|
||||
import axios from "axios"
|
||||
import qs from "qs"
|
||||
|
||||
import logo from "./static/listmonk.svg"
|
||||
import Layout from './Layout'
|
||||
import * as cs from './constants'
|
||||
import Layout from "./Layout"
|
||||
import * as cs from "./constants"
|
||||
|
||||
/*
|
||||
App acts as a an "automagic" wrapper for all sub components. It is also the central
|
||||
|
@ -26,144 +26,166 @@ import * as cs from './constants'
|
|||
*/
|
||||
|
||||
class App extends React.PureComponent {
|
||||
models = [cs.ModelUsers,
|
||||
cs.ModelSubscribers,
|
||||
cs.ModelLists,
|
||||
cs.ModelCampaigns,
|
||||
cs.ModelTemplates]
|
||||
models = [
|
||||
cs.ModelUsers,
|
||||
cs.ModelSubscribers,
|
||||
cs.ModelLists,
|
||||
cs.ModelCampaigns,
|
||||
cs.ModelTemplates
|
||||
]
|
||||
|
||||
state = {
|
||||
// Initialize empty states.
|
||||
reqStates: this.models.reduce((map, obj) => (map[obj] = cs.StatePending, map), {}),
|
||||
data: this.models.reduce((map, obj) => (map[obj] = [], map), {}),
|
||||
modStates: {}
|
||||
state = {
|
||||
// Initialize empty states.
|
||||
reqStates: this.models.reduce(
|
||||
// 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: {}
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
axios.defaults.paramsSerializer = params => {
|
||||
return qs.stringify(params, { arrayFormat: "repeat" })
|
||||
}
|
||||
}
|
||||
|
||||
// modelRequest is an opinionated wrapper for model specific HTTP requests,
|
||||
// including setting model states.
|
||||
modelRequest = async (model, route, method, params) => {
|
||||
let url = replaceParams(route, params)
|
||||
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [model]: cs.StatePending }
|
||||
})
|
||||
try {
|
||||
let req = {
|
||||
method: method,
|
||||
url: url
|
||||
}
|
||||
|
||||
if (method === cs.MethodGet || method === cs.MethodDelete) {
|
||||
req.params = params ? params : {}
|
||||
} else {
|
||||
req.data = params ? params : {}
|
||||
}
|
||||
|
||||
let res = await axios(req)
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
|
||||
})
|
||||
|
||||
// If it's a GET call, set the response as the data state.
|
||||
if (method === cs.MethodGet) {
|
||||
this.setState({ data: { ...this.state.data, [model]: res.data.data } })
|
||||
}
|
||||
return res
|
||||
} catch (e) {
|
||||
// If it's a GET call, throw a global notification.
|
||||
if (method === cs.MethodGet) {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error fetching data",
|
||||
description: Utils.HttpError(e).message
|
||||
})
|
||||
}
|
||||
|
||||
// Set states and show the error on the layout.
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [model]: cs.StateDone }
|
||||
})
|
||||
throw Utils.HttpError(e)
|
||||
}
|
||||
}
|
||||
|
||||
// request is a wrapper for generic HTTP requests.
|
||||
request = async (url, method, params, headers) => {
|
||||
url = replaceParams(url, params)
|
||||
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [url]: cs.StatePending }
|
||||
})
|
||||
try {
|
||||
let req = {
|
||||
method: method,
|
||||
url: url,
|
||||
headers: headers ? headers : {}
|
||||
}
|
||||
|
||||
if (method === cs.MethodGet || method === cs.MethodDelete) {
|
||||
req.params = params ? params : {}
|
||||
} else {
|
||||
req.data = params ? params : {}
|
||||
}
|
||||
|
||||
let res = await axios(req)
|
||||
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
|
||||
})
|
||||
return res
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
reqStates: { ...this.state.reqStates, [url]: cs.StateDone }
|
||||
})
|
||||
throw Utils.HttpError(e)
|
||||
}
|
||||
}
|
||||
|
||||
pageTitle = title => {
|
||||
document.title = title
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!window.CONFIG) {
|
||||
return (
|
||||
<div className="broken">
|
||||
<p>
|
||||
<img src={logo} alt="listmonk logo" />
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<h1>
|
||||
<Icon type="warning" /> Something's not right
|
||||
</h1>
|
||||
<p>
|
||||
The app configuration could not be loaded. Please ensure that the
|
||||
app is running and then refresh this page.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
axios.defaults.paramsSerializer = params => {
|
||||
return qs.stringify(params, {arrayFormat: "repeat"});
|
||||
}
|
||||
}
|
||||
|
||||
// modelRequest is an opinionated wrapper for model specific HTTP requests,
|
||||
// including setting model states.
|
||||
modelRequest = async (model, route, method, params) => {
|
||||
let url = replaceParams(route, params)
|
||||
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StatePending } })
|
||||
try {
|
||||
let req = {
|
||||
method: method,
|
||||
url: url,
|
||||
}
|
||||
|
||||
if (method === cs.MethodGet || method === cs.MethodDelete) {
|
||||
req.params = params ? params : {}
|
||||
} else {
|
||||
req.data = params ? params : {}
|
||||
}
|
||||
|
||||
|
||||
let res = await axios(req)
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StateDone } })
|
||||
|
||||
// If it's a GET call, set the response as the data state.
|
||||
if (method === cs.MethodGet) {
|
||||
this.setState({ data: { ...this.state.data, [model]: res.data.data } })
|
||||
}
|
||||
|
||||
return res
|
||||
} catch (e) {
|
||||
// If it's a GET call, throw a global notification.
|
||||
if (method === cs.MethodGet) {
|
||||
notification["error"]({ placement: cs.MsgPosition,
|
||||
message: "Error fetching data",
|
||||
description: Utils.HttpError(e).message
|
||||
})
|
||||
}
|
||||
|
||||
// Set states and show the error on the layout.
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [model]: cs.StateDone } })
|
||||
throw Utils.HttpError(e)
|
||||
}
|
||||
}
|
||||
|
||||
// request is a wrapper for generic HTTP requests.
|
||||
request = async (url, method, params, headers) => {
|
||||
url = replaceParams(url, params)
|
||||
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StatePending } })
|
||||
try {
|
||||
let req = {
|
||||
method: method,
|
||||
url: url,
|
||||
headers: headers ? headers : {}
|
||||
}
|
||||
|
||||
if(method === cs.MethodGet || method === cs.MethodDelete) {
|
||||
req.params = params ? params : {}
|
||||
} else {
|
||||
req.data = params ? params : {}
|
||||
}
|
||||
|
||||
let res = await axios(req)
|
||||
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StateDone } })
|
||||
return res
|
||||
} catch (e) {
|
||||
this.setState({ reqStates: { ...this.state.reqStates, [url]: cs.StateDone } })
|
||||
throw Utils.HttpError(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pageTitle = (title) => {
|
||||
document.title = title
|
||||
}
|
||||
|
||||
render() {
|
||||
if(!window.CONFIG) {
|
||||
return(
|
||||
<div className="broken">
|
||||
<p>
|
||||
<img src={logo} alt="listmonk logo" />
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
<h1><Icon type="warning" /> Something's not right</h1>
|
||||
<p>The app configuration could not be loaded.
|
||||
Please ensure that the app is running and then refresh this page.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout
|
||||
modelRequest={ this.modelRequest }
|
||||
request={ this.request }
|
||||
reqStates={ this.state.reqStates }
|
||||
pageTitle={ this.pageTitle }
|
||||
config={ window.CONFIG }
|
||||
data={ this.state.data } />
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Layout
|
||||
modelRequest={this.modelRequest}
|
||||
request={this.request}
|
||||
reqStates={this.state.reqStates}
|
||||
pageTitle={this.pageTitle}
|
||||
config={window.CONFIG}
|
||||
data={this.state.data}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function replaceParams (route, params) {
|
||||
// Replace :params in the URL with params in the array.
|
||||
let uriParams = route.match(/:([a-z0-9\-_]+)/ig)
|
||||
if(uriParams && uriParams.length > 0) {
|
||||
uriParams.forEach((p) => {
|
||||
let pName = p.slice(1) // Lose the ":" prefix
|
||||
if(params && params.hasOwnProperty(pName)) {
|
||||
route = route.replace(p, params[pName])
|
||||
}
|
||||
})
|
||||
}
|
||||
function replaceParams(route, params) {
|
||||
// Replace :params in the URL with params in the array.
|
||||
let uriParams = route.match(/:([a-z0-9\-_]+)/gi)
|
||||
if (uriParams && uriParams.length > 0) {
|
||||
uriParams.forEach(p => {
|
||||
let pName = p.slice(1) // Lose the ":" prefix
|
||||
if (params && params.hasOwnProperty(pName)) {
|
||||
route = route.replace(p, params[pName])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return route
|
||||
return route
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,131 +1,190 @@
|
|||
import { Col, Row, notification, Card, Tooltip, Icon, Spin } from "antd"
|
||||
import React from "react";
|
||||
import { Chart, Axis, Geom, Tooltip as BizTooltip } from 'bizcharts';
|
||||
import { Col, Row, notification, Card, Spin } from "antd"
|
||||
import React from "react"
|
||||
import { Chart, Geom, Tooltip as BizTooltip } from "bizcharts"
|
||||
|
||||
import * as cs from "./constants"
|
||||
|
||||
class Dashboard extends React.PureComponent {
|
||||
state = {
|
||||
stats: null,
|
||||
loading: true
|
||||
}
|
||||
state = {
|
||||
stats: null,
|
||||
loading: true
|
||||
}
|
||||
|
||||
campaignTypes = ["running", "finished", "paused", "draft", "scheduled", "cancelled"]
|
||||
campaignTypes = [
|
||||
"running",
|
||||
"finished",
|
||||
"paused",
|
||||
"draft",
|
||||
"scheduled",
|
||||
"cancelled"
|
||||
]
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.pageTitle("Dashboard")
|
||||
this.props.request(cs.Routes.GetDashboarcStats, cs.MethodGet).then((resp) => {
|
||||
this.setState({ stats: resp.data.data, loading: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
}
|
||||
componentDidMount = () => {
|
||||
this.props.pageTitle("Dashboard")
|
||||
this.props
|
||||
.request(cs.Routes.GetDashboarcStats, cs.MethodGet)
|
||||
.then(resp => {
|
||||
this.setState({ stats: resp.data.data, loading: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
}
|
||||
|
||||
orZero(v) {
|
||||
return v ? v : 0
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className = "dashboard">
|
||||
<h1>Welcome</h1>
|
||||
<hr />
|
||||
<Spin spinning={ this.state.loading }>
|
||||
{ this.state.stats &&
|
||||
<div className="stats">
|
||||
<Row>
|
||||
<Col span={ 16 }>
|
||||
<Row gutter={ 24 }>
|
||||
<Col span={ 8 }>
|
||||
<Card title="Active subscribers" bordered={ false }>
|
||||
<h1 className="count">{ this.orZero(this.state.stats.subscribers.enabled) }</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={ 8 }>
|
||||
<Card title="Blacklisted subscribers" bordered={ false }>
|
||||
<h1 className="count">{ this.orZero(this.state.stats.subscribers.blacklisted) }</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={ 8 }>
|
||||
<Card title="Orphaned subscribers" bordered={ false }>
|
||||
<h1 className="count">{ this.orZero(this.state.stats.orphan_subscribers) }</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={ 6 } offset={ 2 }>
|
||||
<Row gutter={ 24 }>
|
||||
<Col span={ 12 }>
|
||||
<Card title="Public lists" bordered={ false }>
|
||||
<h1 className="count">{ this.orZero(this.state.stats.lists.public) }</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={ 12 }>
|
||||
<Card title="Private lists" bordered={ false }>
|
||||
<h1 className="count">{ this.orZero(this.state.stats.lists.private) }</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col span={ 16 }>
|
||||
<Row gutter={ 24 }>
|
||||
<Col span={ 12 }>
|
||||
<Card title="Campaign views (last 3 months)" bordered={ false }>
|
||||
<h1 className="count">
|
||||
{ this.state.stats.campaign_views.reduce((total, v) => total + v.count, 0) }
|
||||
{' '}
|
||||
views
|
||||
</h1>
|
||||
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.campaign_views } forceFit>
|
||||
<BizTooltip crosshairs={{ type : "y" }} />
|
||||
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
||||
<Geom type='point' position="date*count" size={ 0 } />
|
||||
</Chart>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={ 12 }>
|
||||
<Card title="Link clicks (last 3 months)" bordered={ false }>
|
||||
<h1 className="count">
|
||||
{ this.state.stats.link_clicks.reduce((total, v) => total + v.count, 0) }
|
||||
{' '}
|
||||
clicks
|
||||
</h1>
|
||||
<Chart height={ 220 } padding={ [0, 0, 0, 0] } data={ this.state.stats.link_clicks } forceFit>
|
||||
<BizTooltip crosshairs={{ type : "y" }} />
|
||||
<Geom type="area" position="date*count" size={ 0 } color="#7f2aff" />
|
||||
<Geom type='point' position="date*count" size={ 0 } />
|
||||
</Chart>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
orZero(v) {
|
||||
return v ? v : 0
|
||||
}
|
||||
|
||||
<Col span={ 6 } offset={ 2 }>
|
||||
<Card title="Campaigns" bordered={ false } className="campaign-counts">
|
||||
{ this.campaignTypes.map((key) =>
|
||||
<Row key={ `stats-campaigns-${ key }` }>
|
||||
<Col span={ 18 }><h1 className="name">{ key }</h1></Col>
|
||||
<Col span={ 6 }>
|
||||
<h1 className="count">
|
||||
{ this.state.stats.campaigns.hasOwnProperty(key) ?
|
||||
this.state.stats.campaigns[key] : 0 }
|
||||
</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</Spin>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<section className="dashboard">
|
||||
<h1>Welcome</h1>
|
||||
<hr />
|
||||
<Spin spinning={this.state.loading}>
|
||||
{this.state.stats && (
|
||||
<div className="stats">
|
||||
<Row>
|
||||
<Col span={16}>
|
||||
<Row gutter={24}>
|
||||
<Col span={8}>
|
||||
<Card title="Active subscribers" bordered={false}>
|
||||
<h1 className="count">
|
||||
{this.orZero(this.state.stats.subscribers.enabled)}
|
||||
</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Blacklisted subscribers" bordered={false}>
|
||||
<h1 className="count">
|
||||
{this.orZero(
|
||||
this.state.stats.subscribers.blacklisted
|
||||
)}
|
||||
</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card title="Orphaned subscribers" bordered={false}>
|
||||
<h1 className="count">
|
||||
{this.orZero(this.state.stats.orphan_subscribers)}
|
||||
</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={6} offset={2}>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card title="Public lists" bordered={false}>
|
||||
<h1 className="count">
|
||||
{this.orZero(this.state.stats.lists.public)}
|
||||
</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="Private lists" bordered={false}>
|
||||
<h1 className="count">
|
||||
{this.orZero(this.state.stats.lists.private)}
|
||||
</h1>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
<hr />
|
||||
<Row>
|
||||
<Col span={16}>
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title="Campaign views (last 3 months)"
|
||||
bordered={false}
|
||||
>
|
||||
<h1 className="count">
|
||||
{this.state.stats.campaign_views.reduce(
|
||||
(total, v) => total + v.count,
|
||||
0
|
||||
)}{" "}
|
||||
views
|
||||
</h1>
|
||||
<Chart
|
||||
height={220}
|
||||
padding={[0, 0, 0, 0]}
|
||||
data={this.state.stats.campaign_views}
|
||||
forceFit
|
||||
>
|
||||
<BizTooltip crosshairs={{ type: "y" }} />
|
||||
<Geom
|
||||
type="area"
|
||||
position="date*count"
|
||||
size={0}
|
||||
color="#7f2aff"
|
||||
/>
|
||||
<Geom type="point" position="date*count" size={0} />
|
||||
</Chart>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title="Link clicks (last 3 months)"
|
||||
bordered={false}
|
||||
>
|
||||
<h1 className="count">
|
||||
{this.state.stats.link_clicks.reduce(
|
||||
(total, v) => total + v.count,
|
||||
0
|
||||
)}{" "}
|
||||
clicks
|
||||
</h1>
|
||||
<Chart
|
||||
height={220}
|
||||
padding={[0, 0, 0, 0]}
|
||||
data={this.state.stats.link_clicks}
|
||||
forceFit
|
||||
>
|
||||
<BizTooltip crosshairs={{ type: "y" }} />
|
||||
<Geom
|
||||
type="area"
|
||||
position="date*count"
|
||||
size={0}
|
||||
color="#7f2aff"
|
||||
/>
|
||||
<Geom type="point" position="date*count" size={0} />
|
||||
</Chart>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
<Col span={6} offset={2}>
|
||||
<Card
|
||||
title="Campaigns"
|
||||
bordered={false}
|
||||
className="campaign-counts"
|
||||
>
|
||||
{this.campaignTypes.map(key => (
|
||||
<Row key={`stats-campaigns-${key}`}>
|
||||
<Col span={18}>
|
||||
<h1 className="name">{key}</h1>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<h1 className="count">
|
||||
{this.state.stats.campaigns.hasOwnProperty(key)
|
||||
? this.state.stats.campaigns[key]
|
||||
: 0}
|
||||
</h1>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
|
@ -1,363 +1,466 @@
|
|||
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"
|
||||
|
||||
const StatusNone = "none"
|
||||
const StatusNone = "none"
|
||||
const StatusImporting = "importing"
|
||||
const StatusStopping = "stopping"
|
||||
const StatusFinished = "finished"
|
||||
const StatusFailed = "failed"
|
||||
const StatusStopping = "stopping"
|
||||
const StatusFinished = "finished"
|
||||
const StatusFailed = "failed"
|
||||
|
||||
class TheFormDef extends React.PureComponent {
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
fileList: [],
|
||||
formLoading: false,
|
||||
mode: "subscribe"
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
fileList: [],
|
||||
formLoading: false,
|
||||
mode: "subscribe"
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Fetch lists.
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
var err = null,
|
||||
values = {}
|
||||
this.props.form.validateFields((e, v) => {
|
||||
err = e
|
||||
values = v
|
||||
})
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Fetch lists.
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
if (this.state.fileList.length < 1) {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: "Select a valid file to upload"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
var err = null, values = {}
|
||||
this.props.form.validateFields((e, v) => {
|
||||
err = e
|
||||
values = v
|
||||
this.setState({ formLoading: true })
|
||||
let params = new FormData()
|
||||
params.set("params", JSON.stringify(values))
|
||||
params.append("file", this.state.fileList[0])
|
||||
this.props
|
||||
.request(cs.Routes.UploadRouteImport, cs.MethodPost, params)
|
||||
.then(() => {
|
||||
notification["info"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "File uploaded",
|
||||
description: "Please wait while the import is running"
|
||||
})
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
if(this.state.fileList.length < 1) {
|
||||
notification["error"]({ placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: "Select a valid file to upload" })
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ formLoading: true })
|
||||
let params = new FormData()
|
||||
params.set("params", JSON.stringify(values))
|
||||
params.append("file", this.state.fileList[0])
|
||||
this.props.request(cs.Routes.UploadRouteImport, cs.MethodPost, params).then(() => {
|
||||
notification["info"]({ placement: cs.MsgPosition,
|
||||
message: "File uploaded",
|
||||
description: "Please wait while the import is running" })
|
||||
this.props.fetchimportState()
|
||||
this.setState({ formLoading: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
this.setState({ formLoading: false })
|
||||
this.props.fetchimportState()
|
||||
this.setState({ formLoading: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
this.setState({ formLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmBlur = e => {
|
||||
const value = e.target.value
|
||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||
}
|
||||
|
||||
onFileChange = f => {
|
||||
let fileList = [f]
|
||||
this.setState({ fileList })
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
}
|
||||
|
||||
handleConfirmBlur = (e) => {
|
||||
const value = e.target.value
|
||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||
const formItemTailLayout = {
|
||||
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
||||
}
|
||||
|
||||
onFileChange = (f) => {
|
||||
let fileList = [f]
|
||||
this.setState({ fileList })
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
}
|
||||
|
||||
const formItemTailLayout = {
|
||||
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={ this.state.formLoading }>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Mode">
|
||||
{getFieldDecorator("mode", { rules: [{ required: true }], initialValue: "subscribe" })(
|
||||
<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>
|
||||
)}
|
||||
</Form.Item>
|
||||
{ this.state.mode === "subscribe" &&
|
||||
<React.Fragment>
|
||||
<Form.Item {...formItemLayout} label="Lists" extra="Lists to subscribe to">
|
||||
{getFieldDecorator("lists", { rules: [{ required: true }] })(
|
||||
<Select mode="multiple">
|
||||
{[...this.props.lists].map((v, i) =>
|
||||
<Select.Option value={v["id"]} key={v["id"]}>{v["name"]}</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
}
|
||||
{ this.state.mode === "blacklist" &&
|
||||
<Form.Item {...formItemTailLayout}>
|
||||
<p className="ant-form-extra">
|
||||
All existing subscribers found in the import will be marked as 'blacklisted' and will be
|
||||
unsubscribed from their existing subscriptions. New subscribers will be imported and marked as 'blacklisted'.
|
||||
</p>
|
||||
</Form.Item>
|
||||
}
|
||||
<Form.Item {...formItemLayout} label="CSV column delimiter" extra="Default delimiter is comma">
|
||||
{getFieldDecorator("delim", {
|
||||
initialValue: ","
|
||||
})(<Input maxLength="1" style={{ maxWidth: 40 }} />)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="CSV or ZIP file">
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator("file", {
|
||||
valuePropName: "file",
|
||||
getValueFromEvent: this.normFile,
|
||||
rules: [{ required: true }]
|
||||
})(
|
||||
<Upload.Dragger name="files"
|
||||
multiple={ false }
|
||||
fileList={ this.state.fileList }
|
||||
beforeUpload={ this.onFileChange }
|
||||
accept=".zip,.csv">
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">Click or drag a CSV or ZIP file here</p>
|
||||
</Upload.Dragger>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
<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>
|
||||
<Button type="primary" htmlType="submit"><Icon type="upload" /> Upload</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Spin spinning={this.state.formLoading}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Mode">
|
||||
{getFieldDecorator("mode", {
|
||||
rules: [{ required: true }],
|
||||
initialValue: "subscribe"
|
||||
})(
|
||||
<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>
|
||||
)}
|
||||
</Form.Item>
|
||||
{this.state.mode === "subscribe" && (
|
||||
<React.Fragment>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="Lists"
|
||||
extra="Lists to subscribe to"
|
||||
>
|
||||
{getFieldDecorator("lists", { rules: [{ required: true }] })(
|
||||
<Select mode="multiple">
|
||||
{[...this.props.lists].map((v, i) => (
|
||||
<Select.Option value={v["id"]} key={v["id"]}>
|
||||
{v["name"]}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{this.state.mode === "blacklist" && (
|
||||
<Form.Item {...formItemTailLayout}>
|
||||
<p className="ant-form-extra">
|
||||
All existing subscribers found in the import will be marked as
|
||||
'blacklisted' and will be unsubscribed from their existing
|
||||
subscriptions. New subscribers will be imported and marked as
|
||||
'blacklisted'.
|
||||
</p>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="CSV column delimiter"
|
||||
extra="Default delimiter is comma"
|
||||
>
|
||||
{getFieldDecorator("delim", {
|
||||
initialValue: ","
|
||||
})(<Input maxLength="1" style={{ maxWidth: 40 }} />)}
|
||||
</Form.Item>
|
||||
<Form.Item {...formItemLayout} label="CSV or ZIP file">
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator("file", {
|
||||
valuePropName: "file",
|
||||
getValueFromEvent: this.normFile,
|
||||
rules: [{ required: true }]
|
||||
})(
|
||||
<Upload.Dragger
|
||||
name="files"
|
||||
multiple={false}
|
||||
fileList={this.state.fileList}
|
||||
beforeUpload={this.onFileChange}
|
||||
accept=".zip,.csv"
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
Click or drag a CSV or ZIP file here
|
||||
</p>
|
||||
</Upload.Dragger>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
<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>
|
||||
<Button type="primary" htmlType="submit">
|
||||
<Icon type="upload" /> Upload
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
const TheForm = Form.create()(TheFormDef)
|
||||
|
||||
class Importing extends React.PureComponent {
|
||||
state = {
|
||||
pollID: -1,
|
||||
logs: ""
|
||||
}
|
||||
state = {
|
||||
pollID: -1,
|
||||
logs: ""
|
||||
}
|
||||
|
||||
stopImport = () => {
|
||||
// Get the import status.
|
||||
this.props.request(cs.Routes.UploadRouteImport, cs.MethodDelete).then((r) => {
|
||||
this.props.fetchimportState()
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
stopImport = () => {
|
||||
// Get the import status.
|
||||
this.props
|
||||
.request(cs.Routes.UploadRouteImport, cs.MethodDelete)
|
||||
.then(r => {
|
||||
this.props.fetchimportState()
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Poll for stats until it's finished or failed.
|
||||
let pollID = window.setInterval(() => {
|
||||
this.props.fetchimportState()
|
||||
this.fetchLogs()
|
||||
if( this.props.importState.status === StatusFinished ||
|
||||
this.props.importState.status === StatusFailed ) {
|
||||
window.clearInterval(this.state.pollID)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
this.setState({ pollID: pollID })
|
||||
}
|
||||
componentWillUnmount() {
|
||||
componentDidMount() {
|
||||
// Poll for stats until it's finished or failed.
|
||||
let pollID = window.setInterval(() => {
|
||||
this.props.fetchimportState()
|
||||
this.fetchLogs()
|
||||
if (
|
||||
this.props.importState.status === StatusFinished ||
|
||||
this.props.importState.status === StatusFailed
|
||||
) {
|
||||
window.clearInterval(this.state.pollID)
|
||||
}
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
fetchLogs() {
|
||||
this.props.request(cs.Routes.GetRouteImportLogs, cs.MethodGet).then((r) => {
|
||||
this.setState({ logs: r.data.data })
|
||||
let t = document.querySelector("#log-textarea")
|
||||
t.scrollTop = t.scrollHeight;
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
this.setState({ pollID: pollID })
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.clearInterval(this.state.pollID)
|
||||
}
|
||||
|
||||
fetchLogs() {
|
||||
this.props
|
||||
.request(cs.Routes.GetRouteImportLogs, cs.MethodGet)
|
||||
.then(r => {
|
||||
this.setState({ logs: r.data.data })
|
||||
let t = document.querySelector("#log-textarea")
|
||||
t.scrollTop = t.scrollHeight
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let progressPercent = 0
|
||||
if (this.props.importState.status === StatusFinished) {
|
||||
progressPercent = 100
|
||||
} else {
|
||||
progressPercent = Math.floor(
|
||||
(this.props.importState.imported / this.props.importState.total) * 100
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
let progressPercent = 0
|
||||
if( this.props.importState.status === StatusFinished ) {
|
||||
progressPercent = 100
|
||||
} else {
|
||||
progressPercent = Math.floor(this.props.importState.imported / this.props.importState.total * 100)
|
||||
}
|
||||
return (
|
||||
<section className="content import">
|
||||
<h1>Importing — {this.props.importState.name}</h1>
|
||||
{this.props.importState.status === StatusImporting && (
|
||||
<p>
|
||||
Import is in progress. It is safe to navigate away from this page.
|
||||
</p>
|
||||
)}
|
||||
|
||||
return(
|
||||
<section className="content import">
|
||||
<h1>Importing — { this.props.importState.name }</h1>
|
||||
{ this.props.importState.status === StatusImporting &&
|
||||
<p>Import is in progress. It is safe to navigate away from this page.</p>
|
||||
}
|
||||
{this.props.importState.status !== StatusImporting && (
|
||||
<p>Import has finished.</p>
|
||||
)}
|
||||
|
||||
{ this.props.importState.status !== StatusImporting &&
|
||||
<p>Import has finished.</p>
|
||||
}
|
||||
<Row className="import-container">
|
||||
<Col span="10" offset="3">
|
||||
<div className="stats center">
|
||||
<div>
|
||||
<Progress type="line" percent={progressPercent} />
|
||||
</div>
|
||||
|
||||
<Row className="import-container">
|
||||
<Col span="10" offset="3">
|
||||
<div className="stats center">
|
||||
<div>
|
||||
<Progress type="line" percent={ progressPercent } />
|
||||
</div>
|
||||
<div>
|
||||
<h3>{this.props.importState.imported} records</h3>
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<h3>{ this.props.importState.imported } records</h3>
|
||||
<br />
|
||||
|
||||
{ this.props.importState.status === StatusImporting &&
|
||||
<Popconfirm title="Are you sure?" onConfirm={() => this.stopImport()}>
|
||||
<p><Icon type="loading" /></p>
|
||||
<Button type="primary">Stop import</Button>
|
||||
</Popconfirm>
|
||||
}
|
||||
{ this.props.importState.status === StatusStopping &&
|
||||
<div>
|
||||
<p><Icon type="loading" /></p>
|
||||
<h4>Stopping</h4>
|
||||
</div>
|
||||
}
|
||||
{ this.props.importState.status !== StatusImporting &&
|
||||
this.props.importState.status !== StatusStopping &&
|
||||
<div>
|
||||
{ this.props.importState.status !== StatusFinished &&
|
||||
<div>
|
||||
<Tag color="red">{ this.props.importState.status }</Tag>
|
||||
<br />
|
||||
</div>
|
||||
}
|
||||
|
||||
<br />
|
||||
<Button type="primary" onClick={() => this.stopImport()}>Done</Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.props.importState.status === StatusImporting && (
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
onConfirm={() => this.stopImport()}
|
||||
>
|
||||
<p>
|
||||
<Icon type="loading" />
|
||||
</p>
|
||||
<Button type="primary">Stop import</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{this.props.importState.status === StatusStopping && (
|
||||
<div>
|
||||
<p>
|
||||
<Icon type="loading" />
|
||||
</p>
|
||||
<h4>Stopping</h4>
|
||||
</div>
|
||||
)}
|
||||
{this.props.importState.status !== StatusImporting &&
|
||||
this.props.importState.status !== StatusStopping && (
|
||||
<div>
|
||||
{this.props.importState.status !== StatusFinished && (
|
||||
<div>
|
||||
<Tag color="red">{this.props.importState.status}</Tag>
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="logs">
|
||||
<h3>Import log</h3>
|
||||
<Spin spinning={ this.state.logs === "" }>
|
||||
<Input.TextArea placeholder="Import logs"
|
||||
id="log-textarea"
|
||||
rows={10}
|
||||
value={ this.state.logs }
|
||||
autosize={{ minRows: 2, maxRows: 10 }} />
|
||||
</Spin>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
<br />
|
||||
<Button type="primary" onClick={() => this.stopImport()}>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="logs">
|
||||
<h3>Import log</h3>
|
||||
<Spin spinning={this.state.logs === ""}>
|
||||
<Input.TextArea
|
||||
placeholder="Import logs"
|
||||
id="log-textarea"
|
||||
rows={10}
|
||||
value={this.state.logs}
|
||||
autosize={{ minRows: 2, maxRows: 10 }}
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Import extends React.PureComponent {
|
||||
state = {
|
||||
importState: { "status": "" }
|
||||
}
|
||||
state = {
|
||||
importState: { status: "" }
|
||||
}
|
||||
|
||||
fetchimportState = () => {
|
||||
// Get the import status.
|
||||
this.props.request(cs.Routes.GetRouteImportStats, cs.MethodGet).then((r) => {
|
||||
this.setState({ importState: r.data.data })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
fetchimportState = () => {
|
||||
// Get the import status.
|
||||
this.props
|
||||
.request(cs.Routes.GetRouteImportStats, cs.MethodGet)
|
||||
.then(r => {
|
||||
this.setState({ importState: r.data.data })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Import subscribers")
|
||||
this.fetchimportState()
|
||||
}
|
||||
render() {
|
||||
if (this.state.importState.status === "") {
|
||||
// Fetching the status.
|
||||
return (
|
||||
<section className="content center">
|
||||
<Spin />
|
||||
</section>
|
||||
)
|
||||
} else if (this.state.importState.status !== StatusNone) {
|
||||
// There's an import state
|
||||
return (
|
||||
<Importing
|
||||
{...this.props}
|
||||
importState={this.state.importState}
|
||||
fetchimportState={this.fetchimportState}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Import subscribers")
|
||||
this.fetchimportState()
|
||||
}
|
||||
render() {
|
||||
if( this.state.importState.status === "" ) {
|
||||
// Fetching the status.
|
||||
return (
|
||||
<section className="content center">
|
||||
<Spin />
|
||||
</section>
|
||||
)
|
||||
} else if ( this.state.importState.status !== StatusNone ) {
|
||||
// There's an import state
|
||||
return <Importing { ...this.props }
|
||||
importState={ this.state.importState }
|
||||
fetchimportState={ this.fetchimportState } />
|
||||
}
|
||||
return (
|
||||
<section className="content import">
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<h1>Import subscribers</h1>
|
||||
</Col>
|
||||
<Col span={2} />
|
||||
</Row>
|
||||
|
||||
return (
|
||||
<section className="content import">
|
||||
<Row>
|
||||
<Col span={22}><h1>Import subscribers</h1></Col>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
</Row>
|
||||
<TheForm
|
||||
{...this.props}
|
||||
fetchimportState={this.fetchimportState}
|
||||
lists={this.props.data[cs.ModelLists]}
|
||||
/>
|
||||
|
||||
<TheForm { ...this.props }
|
||||
fetchimportState={ this.fetchimportState }
|
||||
lists={ this.props.data[cs.ModelLists] }>
|
||||
</TheForm>
|
||||
<hr />
|
||||
<div className="help">
|
||||
<h2>Instructions</h2>
|
||||
<p>
|
||||
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
|
||||
with the exact column names. <code>attributes</code> (optional)
|
||||
should be a valid JSON string with double escaped quotes.
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<div className="help">
|
||||
<h2>Instructions</h2>
|
||||
<p>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 with the exact column names.
|
||||
{" "}
|
||||
<code>attributes</code> (optional) should be a valid JSON string with double escaped quotes.
|
||||
</p>
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>status,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>status,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
|
||||
<h3>Example raw CSV</h3>
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>status,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
<code className="csv-row">
|
||||
<span>user1@mail.com,</span>
|
||||
<span>"User One",</span>
|
||||
<span>enabled,</span>
|
||||
<span>{ '"{""age"": 32, ""city"": ""Bangalore""}"' }</span>
|
||||
</code>
|
||||
<code className="csv-row">
|
||||
<span>user2@mail.com,</span>
|
||||
<span>"User Two",</span>
|
||||
<span>blacklisted,</span>
|
||||
<span>{ '"{""age"": 25, ""occupation"": ""Time Traveller""}"' }</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
<h3>Example raw CSV</h3>
|
||||
<blockquote className="csv-example">
|
||||
<code className="csv-headers">
|
||||
<span>email,</span>
|
||||
<span>name,</span>
|
||||
<span>status,</span>
|
||||
<span>attributes</span>
|
||||
</code>
|
||||
<code className="csv-row">
|
||||
<span>user1@mail.com,</span>
|
||||
<span>"User One",</span>
|
||||
<span>enabled,</span>
|
||||
<span>{'"{""age"": 32, ""city"": ""Bangalore""}"'}</span>
|
||||
</code>
|
||||
<code className="csv-row">
|
||||
<span>user2@mail.com,</span>
|
||||
<span>"User Two",</span>
|
||||
<span>blacklisted,</span>
|
||||
<span>
|
||||
{'"{""age"": 25, ""occupation"": ""Time Traveller""}"'}
|
||||
</span>
|
||||
</code>
|
||||
</blockquote>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Import
|
||||
|
|
|
@ -12,105 +12,246 @@ import Subscribers from "./Subscribers"
|
|||
import Subscriber from "./Subscriber"
|
||||
import Templates from "./Templates"
|
||||
import Import from "./Import"
|
||||
import Test from "./Test"
|
||||
import Campaigns from "./Campaigns";
|
||||
import Campaign from "./Campaign";
|
||||
import Media from "./Media";
|
||||
|
||||
import Campaigns from "./Campaigns"
|
||||
import Campaign from "./Campaign"
|
||||
import Media from "./Media"
|
||||
|
||||
const { Content, Footer, Sider } = Layout
|
||||
const SubMenu = Menu.SubMenu
|
||||
const year = new Date().getUTCFullYear()
|
||||
|
||||
class Base extends React.Component {
|
||||
state = {
|
||||
basePath: "/" + window.location.pathname.split("/")[1],
|
||||
error: null,
|
||||
collapsed: false
|
||||
};
|
||||
state = {
|
||||
basePath: "/" + window.location.pathname.split("/")[1],
|
||||
error: null,
|
||||
collapsed: false
|
||||
}
|
||||
|
||||
onCollapse = (collapsed) => {
|
||||
this.setState({ collapsed })
|
||||
}
|
||||
onCollapse = collapsed => {
|
||||
this.setState({ collapsed })
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={this.state.collapsed}
|
||||
onCollapse={this.onCollapse}
|
||||
theme="light"
|
||||
>
|
||||
<div className="logo">
|
||||
<Link to="/"><img src={logo} alt="listmonk logo" /></Link>
|
||||
</div>
|
||||
render() {
|
||||
return (
|
||||
<Layout style={{ minHeight: "100vh" }}>
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={this.state.collapsed}
|
||||
onCollapse={this.onCollapse}
|
||||
theme="light"
|
||||
>
|
||||
<div className="logo">
|
||||
<Link to="/">
|
||||
<img src={logo} alt="listmonk logo" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Menu defaultSelectedKeys={["/"]}
|
||||
selectedKeys={[window.location.pathname]}
|
||||
defaultOpenKeys={[this.state.basePath]}
|
||||
mode="inline">
|
||||
<Menu
|
||||
defaultSelectedKeys={["/"]}
|
||||
selectedKeys={[window.location.pathname]}
|
||||
defaultOpenKeys={[this.state.basePath]}
|
||||
mode="inline"
|
||||
>
|
||||
<Menu.Item key="/">
|
||||
<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
|
||||
key="/subscribers"
|
||||
title={
|
||||
<span>
|
||||
<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>
|
||||
|
||||
<Menu.Item key="/"><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
|
||||
key="/subscribers"
|
||||
title={<span><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
|
||||
key="/campaigns"
|
||||
title={
|
||||
<span>
|
||||
<Icon type="rocket" />
|
||||
<span>Campaigns</span>
|
||||
</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
|
||||
key="/campaigns"
|
||||
title={<span><Icon type="rocket" /><span>Campaigns</span></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
|
||||
key="/settings"
|
||||
title={
|
||||
<span>
|
||||
<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>
|
||||
<Menu.Item key="11">
|
||||
<Icon type="logout" />
|
||||
<span>Logout</span>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
|
||||
<SubMenu
|
||||
key="/settings"
|
||||
title={<span><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>
|
||||
<Menu.Item key="11"><Icon type="logout" /><span>Logout</span></Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
<Content style={{ margin: "0 16px" }}>
|
||||
<div className="content-body">
|
||||
<div id="alert-container"></div>
|
||||
<Switch>
|
||||
<Route exact key="/" path="/" render={(props) => <Dashboard { ...{ ...this.props, route: props } } />} />
|
||||
<Route exact key="/lists" 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 } } />} />
|
||||
<Route exact key="/test" path="/test" render={(props) => <Test { ...{ ...this.props, route: props } } />} />
|
||||
</Switch>
|
||||
|
||||
</div>
|
||||
</Content>
|
||||
<Footer>
|
||||
<span className="text-small">
|
||||
<a href="https://listmonk.app" rel="noreferrer noopener" target="_blank">listmonk</a>
|
||||
{" "}
|
||||
© 2019 { year != 2019 ? " - " + year : "" }
|
||||
</span>
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
<Layout>
|
||||
<Content style={{ margin: "0 16px" }}>
|
||||
<div className="content-body">
|
||||
<div id="alert-container" />
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
key="/"
|
||||
path="/"
|
||||
render={props => (
|
||||
<Dashboard {...{ ...this.props, route: props }} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
key="/lists"
|
||||
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>
|
||||
</div>
|
||||
</Content>
|
||||
<Footer>
|
||||
<span className="text-small">
|
||||
<a
|
||||
href="https://listmonk.app"
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
listmonk
|
||||
</a>{" "}
|
||||
© 2019 {year !== 2019 ? " - " + year : ""}
|
||||
</span>
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Base
|
||||
|
|
|
@ -1,267 +1,376 @@
|
|||
import React from "react"
|
||||
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 * as cs from "./constants"
|
||||
|
||||
const tagColors = {
|
||||
"private": "orange",
|
||||
"public": "green"
|
||||
private: "orange",
|
||||
public: "green"
|
||||
}
|
||||
|
||||
class CreateFormDef extends React.PureComponent {
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
modalWaiting: false
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
modalWaiting: false
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ modalWaiting: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Create a new list.
|
||||
this.props
|
||||
.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a list.
|
||||
this.props
|
||||
.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
modalTitle(formType, record) {
|
||||
if (formType === cs.FormCreate) {
|
||||
return "Create a list"
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Tag
|
||||
color={
|
||||
tagColors.hasOwnProperty(record.type) ? tagColors[record.type] : ""
|
||||
}
|
||||
>
|
||||
{record.type}
|
||||
</Tag>{" "}
|
||||
{record.name}
|
||||
<br />
|
||||
<span className="text-tiny text-grey">
|
||||
ID {record.id} / UUID {record.uuid}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
this.setState({ modalWaiting: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Create a new list.
|
||||
this.props.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a list.
|
||||
this.props.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
}
|
||||
})
|
||||
render() {
|
||||
const { formType, record, onClose } = this.props
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
|
||||
}
|
||||
|
||||
modalTitle(formType, record) {
|
||||
if(formType === cs.FormCreate) {
|
||||
return "Create a list"
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tag color={ tagColors.hasOwnProperty(record.type) ? tagColors[record.type] : "" }>{ record.type }</Tag>
|
||||
{" "}
|
||||
{ record.name }
|
||||
<br />
|
||||
<span className="text-tiny text-grey">ID { record.id } / UUID { record.uuid }</span>
|
||||
</div>
|
||||
)
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
render() {
|
||||
const { formType, record, onClose } = this.props
|
||||
const { getFieldDecorator } = this.props.form
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={this.modalTitle(this.state.form, record)}
|
||||
okText={this.state.form === cs.FormCreate ? "Create" : "Save"}
|
||||
confirmLoading={this.state.modalWaiting}
|
||||
onCancel={onClose}
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<div id="modal-alert-container" />
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
|
||||
}
|
||||
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal visible={ true } title={ this.modalTitle(this.state.form, record) }
|
||||
okText={ this.state.form === cs.FormCreate ? "Create" : "Save" }
|
||||
confirmLoading={ this.state.modalWaiting }
|
||||
onCancel={ onClose }
|
||||
onOk={ this.handleSubmit }>
|
||||
|
||||
<div id="modal-alert-container"></div>
|
||||
|
||||
<Spin spinning={ this.props.reqStates[cs.ModelLists] === cs.StatePending }>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item {...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.Option value="private">Private</Select.Option>
|
||||
<Select.Option value="public">Public</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item {...formItemLayout} label="Tags" extra="Hit Enter after typing a word to add multiple tags">
|
||||
{getFieldDecorator("tags", { initialValue: record.tags })(
|
||||
<Select mode="tags"></Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
<Spin
|
||||
spinning={this.props.reqStates[cs.ModelLists] === cs.StatePending}
|
||||
>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...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.Option value="private">Private</Select.Option>
|
||||
<Select.Option value="public">Public</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="Tags"
|
||||
extra="Hit Enter after typing a word to add multiple tags"
|
||||
>
|
||||
{getFieldDecorator("tags", { initialValue: record.tags })(
|
||||
<Select mode="tags" />
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const CreateForm = Form.create()(CreateFormDef)
|
||||
|
||||
class Lists extends React.PureComponent {
|
||||
state = {
|
||||
formType: null,
|
||||
record: {}
|
||||
}
|
||||
state = {
|
||||
formType: null,
|
||||
record: {}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.columns = [{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
sorter: true,
|
||||
width: "40%",
|
||||
render: (text, record) => {
|
||||
const out = [];
|
||||
out.push(
|
||||
<div className="name" key={`name-${record.id}`}><Link to={ `/subscribers/lists/${record.id}` }>{ text }</Link></div>
|
||||
)
|
||||
this.columns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
sorter: true,
|
||||
width: "40%",
|
||||
render: (text, record) => {
|
||||
const out = []
|
||||
out.push(
|
||||
<div className="name" key={`name-${record.id}`}>
|
||||
<Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
if(record.tags.length > 0) {
|
||||
for (let i = 0; i < record.tags.length; i++) {
|
||||
out.push(<Tag key={`tag-${i}`}>{ record.tags[i] }</Tag>);
|
||||
}
|
||||
}
|
||||
if (record.tags.length > 0) {
|
||||
for (let i = 0; i < record.tags.length; i++) {
|
||||
out.push(<Tag key={`tag-${i}`}>{record.tags[i]}</Tag>)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "type",
|
||||
width: "10%",
|
||||
render: (type, _) => {
|
||||
let color = type === "private" ? "orange" : "green"
|
||||
return <Tag color={color}>{type}</Tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Subscribers",
|
||||
dataIndex: "subscriber_count",
|
||||
width: "15%",
|
||||
align: "center",
|
||||
render: (text, record) => {
|
||||
return(
|
||||
<div className="name" key={`name-${record.id}`}><Link to={ `/subscribers/lists/${record.id}` }>{ text }</Link></div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
dataIndex: "created_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Updated",
|
||||
dataIndex: "updated_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
dataIndex: "actions",
|
||||
width: "10%",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="actions">
|
||||
<Tooltip title="Send a campaign"><a role="button"><Icon type="rocket" /></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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}]
|
||||
}
|
||||
return out
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Type",
|
||||
dataIndex: "type",
|
||||
width: "10%",
|
||||
render: (type, _) => {
|
||||
let color = type === "private" ? "orange" : "green"
|
||||
return <Tag color={color}>{type}</Tag>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Subscribers",
|
||||
dataIndex: "subscriber_count",
|
||||
width: "15%",
|
||||
align: "center",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="name" key={`name-${record.id}`}>
|
||||
<Link to={`/subscribers/lists/${record.id}`}>{text}</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
dataIndex: "created_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Updated",
|
||||
dataIndex: "updated_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
dataIndex: "actions",
|
||||
width: "10%",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="actions">
|
||||
<Tooltip title="Send a campaign">
|
||||
<a role="button">
|
||||
<Icon type="rocket" />
|
||||
</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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Lists")
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Lists")
|
||||
this.fetchRecords()
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
}
|
||||
|
||||
deleteRecord = record => {
|
||||
this.props
|
||||
.modelRequest(cs.ModelLists, cs.Routes.DeleteList, cs.MethodDelete, {
|
||||
id: record.id
|
||||
})
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "List deleted",
|
||||
description: `"${record.name}" deleted`
|
||||
})
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
}
|
||||
handleHideForm = () => {
|
||||
this.setState({ formType: null })
|
||||
}
|
||||
|
||||
deleteRecord = (record) => {
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.DeleteList, cs.MethodDelete, { id: record.id })
|
||||
.then(() => {
|
||||
notification["success"]({ placement: cs.MsgPosition, message: "List deleted", description: `"${record.name}" deleted` })
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
handleShowCreateForm = () => {
|
||||
this.setState({ formType: cs.FormCreate, record: {} })
|
||||
}
|
||||
|
||||
handleHideForm = () => {
|
||||
this.setState({ formType: null })
|
||||
}
|
||||
handleShowEditForm = record => {
|
||||
this.setState({ formType: cs.FormEdit, record: record })
|
||||
}
|
||||
|
||||
handleShowCreateForm = () => {
|
||||
this.setState({ formType: cs.FormCreate, record: {} })
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<section className="content">
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<h1>Lists ({this.props.data[cs.ModelLists].length}) </h1>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
onClick={this.handleShowCreateForm}
|
||||
>
|
||||
Create list
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
handleShowEditForm = (record) => {
|
||||
this.setState({ formType: cs.FormEdit, record: record })
|
||||
}
|
||||
<Table
|
||||
className="lists"
|
||||
columns={this.columns}
|
||||
rowKey={record => record.uuid}
|
||||
dataSource={this.props.data[cs.ModelLists]}
|
||||
loading={this.props.reqStates[cs.ModelLists] !== cs.StateDone}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="content">
|
||||
<Row>
|
||||
<Col span={22}><h1>Lists ({this.props.data[cs.ModelLists].length}) </h1></Col>
|
||||
<Col span={2}>
|
||||
<Button type="primary" icon="plus" onClick={this.handleShowCreateForm}>Create list</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
<Table
|
||||
className="lists"
|
||||
columns={ this.columns }
|
||||
rowKey={ record => record.uuid }
|
||||
dataSource={ this.props.data[cs.ModelLists] }
|
||||
loading={ this.props.reqStates[cs.ModelLists] !== cs.StateDone }
|
||||
pagination={ false }
|
||||
/>
|
||||
|
||||
<CreateForm { ...this.props }
|
||||
formType={ this.state.formType }
|
||||
record={ this.state.record }
|
||||
onClose={ this.handleHideForm }
|
||||
fetchRecords = { this.fetchRecords }
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
<CreateForm
|
||||
{...this.props}
|
||||
formType={this.state.formType}
|
||||
record={this.state.record}
|
||||
onClose={this.handleHideForm}
|
||||
fetchRecords={this.fetchRecords}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Lists
|
||||
|
|
|
@ -1,132 +1,176 @@
|
|||
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"
|
||||
|
||||
class TheFormDef extends React.PureComponent {
|
||||
state = {
|
||||
confirmDirty: false
|
||||
}
|
||||
state = {
|
||||
confirmDirty: false
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Media")
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Media")
|
||||
this.fetchRecords()
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(cs.ModelMedia, cs.Routes.GetMedia, cs.MethodGet)
|
||||
}
|
||||
|
||||
handleDeleteRecord = record => {
|
||||
this.props
|
||||
.modelRequest(cs.ModelMedia, cs.Routes.DeleteMedia, cs.MethodDelete, {
|
||||
id: record.id
|
||||
})
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Image deleted",
|
||||
description: `"${record.filename}" deleted`
|
||||
})
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleInsertMedia = record => {
|
||||
// The insertMedia callback may be passed down by the invoker (Campaign)
|
||||
if (!this.props.insertMedia) {
|
||||
return false
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(cs.ModelMedia, cs.Routes.GetMedia, cs.MethodGet)
|
||||
this.props.insertMedia(record.uri)
|
||||
return false
|
||||
}
|
||||
|
||||
onFileChange = f => {
|
||||
if (
|
||||
f.file.error &&
|
||||
f.file.response &&
|
||||
f.file.response.hasOwnProperty("message")
|
||||
) {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error uploading file",
|
||||
description: f.file.response.message
|
||||
})
|
||||
} else if (f.file.status === "done") {
|
||||
this.fetchRecords()
|
||||
}
|
||||
|
||||
handleDeleteRecord = (record) => {
|
||||
this.props.modelRequest(cs.ModelMedia, cs.Routes.DeleteMedia, cs.MethodDelete, { id: record.id })
|
||||
.then(() => {
|
||||
notification["success"]({ placement: cs.MsgPosition, message: "Image deleted", description: `"${record.filename}" deleted` })
|
||||
return false
|
||||
}
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
})
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
}
|
||||
|
||||
handleInsertMedia = (record) => {
|
||||
// The insertMedia callback may be passed down by the invoker (Campaign)
|
||||
if(!this.props.insertMedia) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.props.insertMedia(record.uri)
|
||||
return false
|
||||
}
|
||||
return (
|
||||
<Spin spinning={false}>
|
||||
<Form>
|
||||
<Form.Item {...formItemLayout} label="Upload images">
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator("file", {
|
||||
valuePropName: "file",
|
||||
getValueFromEvent: this.normFile,
|
||||
rules: [{ required: true }]
|
||||
})(
|
||||
<Upload.Dragger
|
||||
name="file"
|
||||
action="/api/media"
|
||||
multiple={true}
|
||||
listType="picture"
|
||||
onChange={this.onFileChange}
|
||||
accept=".gif, .jpg, .jpeg, .png"
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">Click or drag file here</p>
|
||||
</Upload.Dragger>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
onFileChange = (f) => {
|
||||
if(f.file.error && f.file.response && f.file.response.hasOwnProperty("message")) {
|
||||
notification["error"]({ placement: cs.MsgPosition,
|
||||
message: "Error uploading file",
|
||||
description: f.file.response.message })
|
||||
} else if(f.file.status === "done") {
|
||||
this.fetchRecords()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
render() {
|
||||
const { getFieldDecorator } = this.props.form
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
}
|
||||
|
||||
return (
|
||||
<Spin spinning={false}>
|
||||
<Form>
|
||||
<Form.Item
|
||||
{...formItemLayout}
|
||||
label="Upload images">
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator("file", {
|
||||
valuePropName: "file",
|
||||
getValueFromEvent: this.normFile,
|
||||
rules: [{ required: true }]
|
||||
})(
|
||||
<Upload.Dragger
|
||||
name="file"
|
||||
action="/api/media"
|
||||
multiple={ true }
|
||||
listType="picture"
|
||||
onChange={ this.onFileChange }
|
||||
accept=".gif, .jpg, .jpeg, .png">
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox" />
|
||||
</p>
|
||||
<p className="ant-upload-text">Click or drag file here</p>
|
||||
</Upload.Dragger>
|
||||
)}
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<section className="gallery">
|
||||
{this.props.media && this.props.media.map((record, i) =>
|
||||
<div key={ i } className="image">
|
||||
<a onClick={ () => {
|
||||
this.handleInsertMedia(record);
|
||||
if( this.props.onCancel ) {
|
||||
this.props.onCancel();
|
||||
}
|
||||
} }><img alt={ record.filename } src={ record.thumb_uri } /></a>
|
||||
<div className="actions">
|
||||
<Tooltip title="View" placement="bottom"><a role="button" href={ record.uri } target="_blank"><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>
|
||||
</div>
|
||||
<div className="name" title={ record.filename }>{ record.filename }</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
<section className="gallery">
|
||||
{this.props.media &&
|
||||
this.props.media.map((record, i) => (
|
||||
<div key={i} className="image">
|
||||
<a
|
||||
onClick={() => {
|
||||
this.handleInsertMedia(record)
|
||||
if (this.props.onCancel) {
|
||||
this.props.onCancel()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img alt={record.filename} src={record.thumb_uri} />
|
||||
</a>
|
||||
<div className="actions">
|
||||
<Tooltip title="View" placement="bottom">
|
||||
<a role="button" href={record.uri} target="_blank">
|
||||
<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>
|
||||
</div>
|
||||
<div className="name" title={record.filename}>
|
||||
{record.filename}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
const TheForm = Form.create()(TheFormDef)
|
||||
|
||||
class Media extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<section className="content media">
|
||||
<Row>
|
||||
<Col span={22}><h1>Images</h1></Col>
|
||||
<Col span={2}>
|
||||
</Col>
|
||||
</Row>
|
||||
render() {
|
||||
return (
|
||||
<section className="content media">
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<h1>Images</h1>
|
||||
</Col>
|
||||
<Col span={2} />
|
||||
</Row>
|
||||
|
||||
<TheForm { ...this.props }
|
||||
media={ this.props.data[cs.ModelMedia] }>
|
||||
</TheForm>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
<TheForm {...this.props} media={this.props.data[cs.ModelMedia]} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Media
|
||||
|
|
|
@ -5,67 +5,71 @@ import * as cs from "./constants"
|
|||
import { Spin } from "antd"
|
||||
|
||||
class ModalPreview extends React.PureComponent {
|
||||
makeForm(body) {
|
||||
let form = document.createElement("form")
|
||||
form.method = cs.MethodPost
|
||||
form.action = this.props.previewURL
|
||||
form.target = "preview-iframe"
|
||||
makeForm(body) {
|
||||
let form = document.createElement("form")
|
||||
form.method = cs.MethodPost
|
||||
form.action = this.props.previewURL
|
||||
form.target = "preview-iframe"
|
||||
|
||||
let input = document.createElement("input")
|
||||
input.type = "hidden"
|
||||
input.name = "body"
|
||||
input.value = body
|
||||
form.appendChild(input)
|
||||
document.body.appendChild(form)
|
||||
form.submit()
|
||||
}
|
||||
let input = document.createElement("input")
|
||||
input.type = "hidden"
|
||||
input.name = "body"
|
||||
input.value = body
|
||||
form.appendChild(input)
|
||||
document.body.appendChild(form)
|
||||
form.submit()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Modal visible={ true } title={ this.props.title }
|
||||
className="preview-modal"
|
||||
width="90%"
|
||||
height={ 900 }
|
||||
onCancel={ this.props.onCancel }
|
||||
onOk={ this.props.onCancel }>
|
||||
<div className="preview-iframe-container">
|
||||
<Spin className="preview-iframe-spinner"></Spin>
|
||||
<iframe key="preview-iframe" onLoad={() => {
|
||||
// If state is used to manage the spinner, it causes
|
||||
// the iframe to re-render and reload everything.
|
||||
// Hack the spinner away from the DOM directly instead.
|
||||
let spin = document.querySelector(".preview-iframe-spinner")
|
||||
if(spin) {
|
||||
spin.parentNode.removeChild(spin)
|
||||
}
|
||||
// this.setState({ loading: false })
|
||||
}} title={ this.props.title ? this.props.title : "Preview" }
|
||||
name="preview-iframe"
|
||||
id="preview-iframe"
|
||||
className="preview-iframe"
|
||||
ref={(o) => {
|
||||
if(!o) {
|
||||
return
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
visible={true}
|
||||
title={this.props.title}
|
||||
className="preview-modal"
|
||||
width="90%"
|
||||
height={900}
|
||||
onCancel={this.props.onCancel}
|
||||
onOk={this.props.onCancel}
|
||||
>
|
||||
<div className="preview-iframe-container">
|
||||
<Spin className="preview-iframe-spinner" />
|
||||
<iframe
|
||||
key="preview-iframe"
|
||||
onLoad={() => {
|
||||
// If state is used to manage the spinner, it causes
|
||||
// the iframe to re-render and reload everything.
|
||||
// Hack the spinner away from the DOM directly instead.
|
||||
let spin = document.querySelector(".preview-iframe-spinner")
|
||||
if (spin) {
|
||||
spin.parentNode.removeChild(spin)
|
||||
}
|
||||
// this.setState({ loading: false })
|
||||
}}
|
||||
title={this.props.title ? this.props.title : "Preview"}
|
||||
name="preview-iframe"
|
||||
id="preview-iframe"
|
||||
className="preview-iframe"
|
||||
ref={o => {
|
||||
if (!o) {
|
||||
return
|
||||
}
|
||||
|
||||
// When the DOM reference for the iframe is ready,
|
||||
// see if there's a body to post with the form hack.
|
||||
if(this.props.body !== undefined
|
||||
&& this.props.body !== null) {
|
||||
this.makeForm(this.props.body)
|
||||
} else {
|
||||
if(this.props.previewURL) {
|
||||
o.src = this.props.previewURL
|
||||
}
|
||||
}
|
||||
}}
|
||||
src="about:blank">
|
||||
</iframe>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
// When the DOM reference for the iframe is ready,
|
||||
// see if there's a body to post with the form hack.
|
||||
if (this.props.body !== undefined && this.props.body !== null) {
|
||||
this.makeForm(this.props.body)
|
||||
} else {
|
||||
if (this.props.previewURL) {
|
||||
o.src = this.props.previewURL
|
||||
}
|
||||
}
|
||||
}}
|
||||
src="about:blank"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalPreview
|
||||
|
|
|
@ -1,292 +1,397 @@
|
|||
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"
|
||||
|
||||
const tagColors = {
|
||||
"enabled": "green",
|
||||
"blacklisted": "red"
|
||||
enabled: "green",
|
||||
blacklisted: "red"
|
||||
}
|
||||
const formItemLayoutModal = {
|
||||
labelCol: { xs: { span: 24 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 24 }, sm: { span: 18 } }
|
||||
labelCol: { xs: { span: 24 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 24 }, sm: { span: 18 } }
|
||||
}
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
|
||||
}
|
||||
const formItemTailLayout = {
|
||||
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
||||
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
||||
}
|
||||
|
||||
class CreateFormDef extends React.PureComponent {
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
loading: false
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
loading: false
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = (e, cb) => {
|
||||
e.preventDefault()
|
||||
if (!cb) {
|
||||
// Set a fake callback.
|
||||
cb = () => {}
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = (e, cb) => {
|
||||
e.preventDefault()
|
||||
if(!cb) {
|
||||
// Set a fake callback.
|
||||
cb = () => {}
|
||||
}
|
||||
var err = null,
|
||||
values = {}
|
||||
this.props.form.validateFields((e, v) => {
|
||||
err = e
|
||||
values = v
|
||||
})
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
var err = null, values = {}
|
||||
this.props.form.validateFields((e, v) => {
|
||||
err = e
|
||||
values = v
|
||||
let a = values["attribs"]
|
||||
values["attribs"] = {}
|
||||
if (a && a.length > 0) {
|
||||
try {
|
||||
values["attribs"] = JSON.parse(a)
|
||||
if (values["attribs"] instanceof Array) {
|
||||
notification["error"]({
|
||||
message: "Invalid JSON type",
|
||||
description: "Attributes should be a map {} and not an array []"
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
notification["error"]({
|
||||
message: "Invalid JSON in attributes",
|
||||
description: e.toString()
|
||||
})
|
||||
if(err) {
|
||||
return
|
||||
}
|
||||
|
||||
let a = values["attribs"]
|
||||
values["attribs"] = {}
|
||||
if(a && a.length > 0) {
|
||||
try {
|
||||
values["attribs"] = JSON.parse(a)
|
||||
if(values["attribs"] instanceof Array) {
|
||||
notification["error"]({ message: "Invalid JSON type",
|
||||
description: "Attributes should be a map {} and not an array []" })
|
||||
return
|
||||
}
|
||||
} catch(e) {
|
||||
notification["error"]({ message: "Invalid JSON in attributes", description: e.toString() })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ loading: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Add a subscriber.
|
||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.CreateSubscriber, cs.MethodPost, values).then(() => {
|
||||
notification["success"]({ message: "Subscriber added", description: `${values["email"]} added` })
|
||||
if(!this.props.isModal) {
|
||||
this.props.fetchRecord(this.props.record.id)
|
||||
}
|
||||
cb(true)
|
||||
this.setState({ loading: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
cb(false)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a subscriber.
|
||||
delete(values["keys"])
|
||||
delete(values["vals"])
|
||||
this.props.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) {
|
||||
this.props.fetchRecord(this.props.record.id)
|
||||
}
|
||||
cb(true)
|
||||
this.setState({ loading: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
cb(false)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteRecord = (record) => {
|
||||
this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscriber, cs.MethodDelete, { id: record.id })
|
||||
.then(() => {
|
||||
notification["success"]({ message: "Subscriber deleted", description: `${record.email} deleted` })
|
||||
|
||||
this.props.route.history.push({
|
||||
pathname: cs.Routes.ViewSubscribers,
|
||||
})
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { formType, record } = this.props;
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let subListIDs = []
|
||||
let subStatuses = {}
|
||||
if(this.props.record && this.props.record.lists) {
|
||||
subListIDs = this.props.record.lists.map((v) => { return v["id"] })
|
||||
subStatuses = this.props.record.lists.reduce((o, item) => ({ ...o, [item.id]: item.subscription_status}), {})
|
||||
} else if(this.props.list) {
|
||||
subListIDs = [ this.props.list.id ]
|
||||
}
|
||||
|
||||
const layout = this.props.isModal ? formItemLayoutModal : formItemLayout;
|
||||
return (
|
||||
<Spin spinning={ this.state.loading }>
|
||||
<Form onSubmit={ this.handleSubmit }>
|
||||
<Form.Item { ...layout } label="E-mail">
|
||||
{getFieldDecorator("email", {
|
||||
initialValue: record.email,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus pattern="(.+?)@(.+?)" maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item { ...layout } label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item { ...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.Option value="enabled">Enabled</Select.Option>
|
||||
<Select.Option value="blacklisted">Blacklisted</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item { ...layout } label="Lists" extra="Lists to subscribe to. Lists from which subscribers have unsubscribed themselves cannot be removed.">
|
||||
{getFieldDecorator("lists", { initialValue: subListIDs })(
|
||||
<Select mode="multiple">
|
||||
{[...this.props.lists].map((v, i) =>
|
||||
<Select.Option value={ v.id } key={ v.id } disabled={ subStatuses[v.id] === cs.SubscriptionStatusUnsubscribed }>
|
||||
<span>{ v.name }
|
||||
{ subStatuses[v.id] &&
|
||||
<sup className={ "subscription-status " + subStatuses[v.id] }> { subStatuses[v.id] }</sup>
|
||||
}
|
||||
</span>
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item { ...layout } label="Attributes" colon={ false }>
|
||||
<div>
|
||||
{getFieldDecorator("attribs", {
|
||||
initialValue: record.attribs ? JSON.stringify(record.attribs, null, 4) : ""
|
||||
})(
|
||||
<Input.TextArea
|
||||
placeholder="{}"
|
||||
rows={10}
|
||||
readOnly={false}
|
||||
autosize={{ minRows: 5, maxRows: 10 }} />
|
||||
)}
|
||||
</div>
|
||||
<p className="ant-form-extra">Attributes are defined as a JSON map, for example:
|
||||
{' {"age": 30, "color": "red", "is_user": true}'}. <a href="">More info</a>.</p>
|
||||
</Form.Item>
|
||||
{ !this.props.isModal &&
|
||||
<Form.Item { ...formItemTailLayout }>
|
||||
<Button type="primary" htmlType="submit" icon={ this.props.formType === cs.FormCreate ? "plus" : "save" }>
|
||||
{ this.props.formType === cs.FormCreate ? "Add" : "Save" }
|
||||
</Button>
|
||||
{" "}
|
||||
{ this.props.formType === cs.FormEdit &&
|
||||
<Popconfirm title="Are you sure?" onConfirm={() => {
|
||||
this.handleDeleteRecord(record)
|
||||
}}>
|
||||
<Button icon="delete">Delete</Button>
|
||||
</Popconfirm>
|
||||
}
|
||||
</Form.Item>
|
||||
}
|
||||
</Form>
|
||||
</Spin>
|
||||
this.setState({ loading: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Add a subscriber.
|
||||
this.props
|
||||
.modelRequest(
|
||||
cs.ModelSubscribers,
|
||||
cs.Routes.CreateSubscriber,
|
||||
cs.MethodPost,
|
||||
values
|
||||
)
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
message: "Subscriber added",
|
||||
description: `${values["email"]} added`
|
||||
})
|
||||
if (!this.props.isModal) {
|
||||
this.props.fetchRecord(this.props.record.id)
|
||||
}
|
||||
cb(true)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
cb(false)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a subscriber.
|
||||
delete values["keys"]
|
||||
delete values["vals"]
|
||||
this.props
|
||||
.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) {
|
||||
this.props.fetchRecord(this.props.record.id)
|
||||
}
|
||||
cb(true)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
cb(false)
|
||||
this.setState({ loading: false })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteRecord = record => {
|
||||
this.props
|
||||
.modelRequest(
|
||||
cs.ModelSubscribers,
|
||||
cs.Routes.DeleteSubscriber,
|
||||
cs.MethodDelete,
|
||||
{ id: record.id }
|
||||
)
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
message: "Subscriber deleted",
|
||||
description: `${record.email} deleted`
|
||||
})
|
||||
|
||||
this.props.route.history.push({
|
||||
pathname: cs.Routes.ViewSubscribers
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { formType, record } = this.props
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let subListIDs = []
|
||||
let subStatuses = {}
|
||||
if (this.props.record && this.props.record.lists) {
|
||||
subListIDs = this.props.record.lists.map(v => {
|
||||
return v["id"]
|
||||
})
|
||||
subStatuses = this.props.record.lists.reduce(
|
||||
(o, item) => ({ ...o, [item.id]: item.subscription_status }),
|
||||
{}
|
||||
)
|
||||
} else if (this.props.list) {
|
||||
subListIDs = [this.props.list.id]
|
||||
}
|
||||
|
||||
const layout = this.props.isModal ? formItemLayoutModal : formItemLayout
|
||||
return (
|
||||
<Spin spinning={this.state.loading}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...layout} label="E-mail">
|
||||
{getFieldDecorator("email", {
|
||||
initialValue: record.email,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus pattern="(.+?)@(.+?)" maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item {...layout} label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...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.Option value="enabled">Enabled</Select.Option>
|
||||
<Select.Option value="blacklisted">Blacklisted</Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...layout}
|
||||
label="Lists"
|
||||
extra="Lists to subscribe to. Lists from which subscribers have unsubscribed themselves cannot be removed."
|
||||
>
|
||||
{getFieldDecorator("lists", { initialValue: subListIDs })(
|
||||
<Select mode="multiple">
|
||||
{[...this.props.lists].map((v, i) => (
|
||||
<Select.Option
|
||||
value={v.id}
|
||||
key={v.id}
|
||||
disabled={
|
||||
subStatuses[v.id] === cs.SubscriptionStatusUnsubscribed
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{v.name}
|
||||
{subStatuses[v.id] && (
|
||||
<sup
|
||||
className={"subscription-status " + subStatuses[v.id]}
|
||||
>
|
||||
{" "}
|
||||
{subStatuses[v.id]}
|
||||
</sup>
|
||||
)}
|
||||
</span>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item {...layout} label="Attributes" colon={false}>
|
||||
<div>
|
||||
{getFieldDecorator("attribs", {
|
||||
initialValue: record.attribs
|
||||
? JSON.stringify(record.attribs, null, 4)
|
||||
: ""
|
||||
})(
|
||||
<Input.TextArea
|
||||
placeholder="{}"
|
||||
rows={10}
|
||||
readOnly={false}
|
||||
autosize={{ minRows: 5, maxRows: 10 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="ant-form-extra">
|
||||
Attributes are defined as a JSON map, for example:
|
||||
{' {"age": 30, "color": "red", "is_user": true}'}.{" "}
|
||||
<a href="">More info</a>.
|
||||
</p>
|
||||
</Form.Item>
|
||||
{!this.props.isModal && (
|
||||
<Form.Item {...formItemTailLayout}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={this.props.formType === cs.FormCreate ? "plus" : "save"}
|
||||
>
|
||||
{this.props.formType === cs.FormCreate ? "Add" : "Save"}
|
||||
</Button>{" "}
|
||||
{this.props.formType === cs.FormEdit && (
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
onConfirm={() => {
|
||||
this.handleDeleteRecord(record)
|
||||
}}
|
||||
>
|
||||
<Button icon="delete">Delete</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const CreateForm = Form.create()(CreateFormDef)
|
||||
|
||||
class Subscriber extends React.PureComponent {
|
||||
state = {
|
||||
loading: true,
|
||||
formRef: null,
|
||||
record: {},
|
||||
subID: this.props.route.match.params ? parseInt(this.props.route.match.params.subID, 10) : 0,
|
||||
state = {
|
||||
loading: true,
|
||||
formRef: null,
|
||||
record: {},
|
||||
subID: this.props.route.match.params
|
||||
? parseInt(this.props.route.match.params.subID, 10)
|
||||
: 0
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// When this component is invoked within a modal from the subscribers list page,
|
||||
// the necessary context is supplied and there's no need to fetch anything.
|
||||
if (!this.props.isModal) {
|
||||
// Fetch lists.
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
|
||||
// Fetch subscriber.
|
||||
this.fetchRecord(this.state.subID)
|
||||
} else {
|
||||
this.setState({ record: this.props.record, loading: false })
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// When this component is invoked within a modal from the subscribers list page,
|
||||
// the necessary context is supplied and there's no need to fetch anything.
|
||||
if(!this.props.isModal) {
|
||||
// Fetch lists.
|
||||
this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
|
||||
|
||||
// Fetch subscriber.
|
||||
this.fetchRecord(this.state.subID)
|
||||
} else {
|
||||
this.setState({ record: this.props.record, loading: false })
|
||||
}
|
||||
}
|
||||
|
||||
fetchRecord = (id) => {
|
||||
this.props.request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id }).then((r) => {
|
||||
this.setState({ record: r.data.data, loading: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
fetchRecord = id => {
|
||||
this.props
|
||||
.request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id })
|
||||
.then(r => {
|
||||
this.setState({ record: r.data.data, loading: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setFormRef = (r) => {
|
||||
this.setState({ formRef: r })
|
||||
}
|
||||
setFormRef = r => {
|
||||
this.setState({ formRef: r })
|
||||
}
|
||||
|
||||
submitForm = (e, cb) => {
|
||||
if(this.state.formRef) {
|
||||
this.state.formRef.handleSubmit(e, cb)
|
||||
}
|
||||
submitForm = (e, cb) => {
|
||||
if (this.state.formRef) {
|
||||
this.state.formRef.handleSubmit(e, cb)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="content">
|
||||
<header className="header">
|
||||
<Row>
|
||||
<Col span={ 20 }>
|
||||
{ !this.state.record.id &&
|
||||
<h1>Add subscriber</h1>
|
||||
}
|
||||
{ this.state.record.id &&
|
||||
<div>
|
||||
<h1>
|
||||
<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 })
|
||||
</h1>
|
||||
<span className="text-small text-grey">ID { this.state.record.id } / UUID { this.state.record.uuid }</span>
|
||||
</div>
|
||||
}
|
||||
</Col>
|
||||
<Col span={ 2 }>
|
||||
</Col>
|
||||
</Row>
|
||||
</header>
|
||||
render() {
|
||||
return (
|
||||
<section className="content">
|
||||
<header className="header">
|
||||
<Row>
|
||||
<Col span={20}>
|
||||
{!this.state.record.id && <h1>Add subscriber</h1>}
|
||||
{this.state.record.id && (
|
||||
<div>
|
||||
<Spin spinning={ this.state.loading }>
|
||||
<CreateForm
|
||||
{...this.props}
|
||||
formType={ this.props.formType ? this.props.formType : cs.FormEdit }
|
||||
record={ this.state.record }
|
||||
fetchRecord={ this.fetchRecord }
|
||||
lists={ this.props.data[cs.ModelLists] }
|
||||
wrappedComponentRef={ (r) => {
|
||||
if(!r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save the form's reference so that when this component
|
||||
// is used as a modal, the invoker of the model can submit
|
||||
// it via submitForm()
|
||||
this.setState({ formRef: r })
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
<h1>
|
||||
<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})
|
||||
</h1>
|
||||
<span className="text-small text-grey">
|
||||
ID {this.state.record.id} / UUID {this.state.record.uuid}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</Col>
|
||||
<Col span={2} />
|
||||
</Row>
|
||||
</header>
|
||||
<div>
|
||||
<Spin spinning={this.state.loading}>
|
||||
<CreateForm
|
||||
{...this.props}
|
||||
formType={this.props.formType ? this.props.formType : cs.FormEdit}
|
||||
record={this.state.record}
|
||||
fetchRecord={this.fetchRecord}
|
||||
lists={this.props.data[cs.ModelLists]}
|
||||
wrappedComponentRef={r => {
|
||||
if (!r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save the form's reference so that when this component
|
||||
// is used as a modal, the invoker of the model can submit
|
||||
// it via submitForm()
|
||||
this.setState({ formRef: r })
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Subscriber
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,287 +1,439 @@
|
|||
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 Utils from "./utils"
|
||||
import * as cs from "./constants"
|
||||
|
||||
class CreateFormDef extends React.PureComponent {
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
modalWaiting: false,
|
||||
previewName: "",
|
||||
previewBody: ""
|
||||
state = {
|
||||
confirmDirty: false,
|
||||
modalWaiting: false,
|
||||
previewName: "",
|
||||
previewBody: ""
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = e => {
|
||||
e.preventDefault()
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({ modalWaiting: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Create a new list.
|
||||
this.props
|
||||
.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a list.
|
||||
this.props
|
||||
.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmBlur = e => {
|
||||
const value = e.target.value
|
||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||
}
|
||||
|
||||
handlePreview = (name, body) => {
|
||||
this.setState({ previewName: name, previewBody: body })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { formType, record, onClose } = this.props
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
|
||||
}
|
||||
|
||||
// Handle create / edit form submission.
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
visible={true}
|
||||
title={formType === cs.FormCreate ? "Add template" : record.name}
|
||||
okText={this.state.form === cs.FormCreate ? "Add" : "Save"}
|
||||
width="90%"
|
||||
height={900}
|
||||
confirmLoading={this.state.modalWaiting}
|
||||
onCancel={onClose}
|
||||
onOk={this.handleSubmit}
|
||||
>
|
||||
<Spin
|
||||
spinning={
|
||||
this.props.reqStates[cs.ModelTemplates] === cs.StatePending
|
||||
}
|
||||
>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
|
||||
{getFieldDecorator("body", {
|
||||
initialValue: record.body ? record.body : "",
|
||||
rules: [{ required: true }]
|
||||
})(<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />)}
|
||||
</Form.Item>
|
||||
{this.props.form.getFieldValue("body") !== "" && (
|
||||
<Form.Item {...formItemLayout} colon={false} label=" ">
|
||||
<Button
|
||||
icon="search"
|
||||
onClick={() =>
|
||||
this.handlePreview(
|
||||
this.props.form.getFieldValue("name"),
|
||||
this.props.form.getFieldValue("body")
|
||||
)
|
||||
}
|
||||
>
|
||||
Preview
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Spin>
|
||||
<Row>
|
||||
<Col span="4" />
|
||||
<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>
|
||||
.
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
|
||||
this.setState({ modalWaiting: true })
|
||||
if (this.props.formType === cs.FormCreate) {
|
||||
// Create a new list.
|
||||
this.props.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
} else {
|
||||
// Edit a list.
|
||||
this.props.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.onClose()
|
||||
this.setState({ modalWaiting: false })
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
this.setState({ modalWaiting: false })
|
||||
})
|
||||
{this.state.previewBody && (
|
||||
<ModalPreview
|
||||
title={
|
||||
this.state.previewName
|
||||
? this.state.previewName
|
||||
: "Template preview"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleConfirmBlur = (e) => {
|
||||
const value = e.target.value
|
||||
this.setState({ confirmDirty: this.state.confirmDirty || !!value })
|
||||
}
|
||||
|
||||
handlePreview = (name, body) => {
|
||||
this.setState({ previewName: name, previewBody: body })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { formType, record, onClose } = this.props
|
||||
const { getFieldDecorator } = this.props.form
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
|
||||
wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
|
||||
}
|
||||
|
||||
if (formType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal visible={ true } title={ formType === cs.FormCreate ? "Add template" : record.name }
|
||||
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
|
||||
width="90%"
|
||||
height={ 900 }
|
||||
confirmLoading={ this.state.modalWaiting }
|
||||
onCancel={ onClose }
|
||||
onOk={ this.handleSubmit }>
|
||||
|
||||
<Spin spinning={ this.props.reqStates[cs.ModelTemplates] === cs.StatePending }>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item {...formItemLayout} label="Name">
|
||||
{getFieldDecorator("name", {
|
||||
initialValue: record.name,
|
||||
rules: [{ required: true }]
|
||||
})(<Input autoFocus maxLength="200" />)}
|
||||
</Form.Item>
|
||||
<Form.Item {...formItemLayout} name="body" label="Raw HTML">
|
||||
{getFieldDecorator("body", { initialValue: record.body ? record.body : "", rules: [{ required: true }] })(
|
||||
<Input.TextArea autosize={{ minRows: 10, maxRows: 30 }} />
|
||||
)}
|
||||
</Form.Item>
|
||||
{ this.props.form.getFieldValue("body") !== "" &&
|
||||
<Form.Item {...formItemLayout} colon={ false } label=" ">
|
||||
<Button icon="search" onClick={ () =>
|
||||
this.handlePreview(this.props.form.getFieldValue("name"), this.props.form.getFieldValue("body"))
|
||||
}>Preview</Button>
|
||||
</Form.Item>
|
||||
}
|
||||
</Form>
|
||||
</Spin>
|
||||
<Row>
|
||||
<Col span="4"></Col>
|
||||
<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>.
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal>
|
||||
|
||||
{ this.state.previewBody &&
|
||||
<ModalPreview
|
||||
title={ this.state.previewName ? this.state.previewName : "Template preview" }
|
||||
previewURL={ cs.Routes.PreviewNewTemplate }
|
||||
body={ this.state.previewBody }
|
||||
onCancel={() => {
|
||||
this.setState({ previewBody: null, previewName: null })
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
previewURL={cs.Routes.PreviewNewTemplate}
|
||||
body={this.state.previewBody}
|
||||
onCancel={() => {
|
||||
this.setState({ previewBody: null, previewName: null })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const CreateForm = Form.create()(CreateFormDef)
|
||||
|
||||
class Templates extends React.PureComponent {
|
||||
state = {
|
||||
formType: null,
|
||||
record: {},
|
||||
previewRecord: null
|
||||
}
|
||||
state = {
|
||||
formType: null,
|
||||
record: {},
|
||||
previewRecord: null
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.columns = [{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
sorter: true,
|
||||
width: "50%",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="name">
|
||||
<a role="button" onClick={() => this.handleShowEditForm(record)}>{ text }</a>
|
||||
{ record.is_default &&
|
||||
<div><Tag>Default</Tag></div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
dataIndex: "created_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Updated",
|
||||
dataIndex: "updated_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
dataIndex: "actions",
|
||||
width: "20%",
|
||||
className: "actions",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="actions">
|
||||
<Tooltip title="Preview template" onClick={() => this.handlePreview(record)}><a role="button"><Icon type="search" /></a></Tooltip>
|
||||
this.columns = [
|
||||
{
|
||||
title: "Name",
|
||||
dataIndex: "name",
|
||||
sorter: true,
|
||||
width: "50%",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="name">
|
||||
<a role="button" onClick={() => this.handleShowEditForm(record)}>
|
||||
{text}
|
||||
</a>
|
||||
{record.is_default && (
|
||||
<div>
|
||||
<Tag>Default</Tag>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Created",
|
||||
dataIndex: "created_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Updated",
|
||||
dataIndex: "updated_at",
|
||||
render: (date, _) => {
|
||||
return Utils.DateString(date)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "",
|
||||
dataIndex: "actions",
|
||||
width: "20%",
|
||||
className: "actions",
|
||||
render: (text, record) => {
|
||||
return (
|
||||
<div className="actions">
|
||||
<Tooltip
|
||||
title="Preview template"
|
||||
onClick={() => this.handlePreview(record)}
|
||||
>
|
||||
<a role="button">
|
||||
<Icon type="search" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
{ !record.is_default &&
|
||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleSetDefault(record)}>
|
||||
<Tooltip title="Set as default" placement="bottom"><a role="button"><Icon type="check" /></a></Tooltip>
|
||||
</Popconfirm>
|
||||
}
|
||||
{!record.is_default && (
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
onConfirm={() => this.handleSetDefault(record)}
|
||||
>
|
||||
<Tooltip title="Set as default" placement="bottom">
|
||||
<a role="button">
|
||||
<Icon type="check" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</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 &&
|
||||
<Popconfirm title="Are you sure?" onConfirm={() => this.handleDeleteRecord(record)}>
|
||||
<Tooltip title="Delete template" placement="bottom"><a role="button"><Icon type="delete" /></a></Tooltip>
|
||||
</Popconfirm>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}]
|
||||
}
|
||||
{record.id !== 1 && (
|
||||
<Popconfirm
|
||||
title="Are you sure?"
|
||||
onConfirm={() => this.handleDeleteRecord(record)}
|
||||
>
|
||||
<Tooltip title="Delete template" placement="bottom">
|
||||
<a role="button">
|
||||
<Icon type="delete" />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Templates")
|
||||
componentDidMount() {
|
||||
this.props.pageTitle("Templates")
|
||||
this.fetchRecords()
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(
|
||||
cs.ModelTemplates,
|
||||
cs.Routes.GetTemplates,
|
||||
cs.MethodGet
|
||||
)
|
||||
}
|
||||
|
||||
handleDeleteRecord = record => {
|
||||
this.props
|
||||
.modelRequest(
|
||||
cs.ModelTemplates,
|
||||
cs.Routes.DeleteTemplate,
|
||||
cs.MethodDelete,
|
||||
{ id: record.id }
|
||||
)
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Template deleted",
|
||||
description: `"${record.name}" deleted`
|
||||
})
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
|
||||
fetchRecords = () => {
|
||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.GetTemplates, cs.MethodGet)
|
||||
}
|
||||
handleSetDefault = record => {
|
||||
this.props
|
||||
.modelRequest(
|
||||
cs.ModelTemplates,
|
||||
cs.Routes.SetDefaultTemplate,
|
||||
cs.MethodPut,
|
||||
{ id: record.id }
|
||||
)
|
||||
.then(() => {
|
||||
notification["success"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Template updated",
|
||||
description: `"${record.name}" set as default`
|
||||
})
|
||||
|
||||
handleDeleteRecord = (record) => {
|
||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.DeleteTemplate, cs.MethodDelete, { id: record.id })
|
||||
.then(() => {
|
||||
notification["success"]({ placement: cs.MsgPosition, message: "Template deleted", description: `"${record.name}" deleted` })
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
})
|
||||
.catch(e => {
|
||||
notification["error"]({
|
||||
placement: cs.MsgPosition,
|
||||
message: "Error",
|
||||
description: e.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}).catch(e => {
|
||||
notification["error"]({ message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
handlePreview = record => {
|
||||
this.setState({ previewRecord: record })
|
||||
}
|
||||
|
||||
handleSetDefault = (record) => {
|
||||
this.props.modelRequest(cs.ModelTemplates, cs.Routes.SetDefaultTemplate, cs.MethodPut, { id: record.id })
|
||||
.then(() => {
|
||||
notification["success"]({ placement: cs.MsgPosition, message: "Template updated", description: `"${record.name}" set as default` })
|
||||
|
||||
// Reload the table.
|
||||
this.fetchRecords()
|
||||
}).catch(e => {
|
||||
notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
|
||||
})
|
||||
}
|
||||
hideForm = () => {
|
||||
this.setState({ formType: null })
|
||||
}
|
||||
|
||||
handlePreview = (record) => {
|
||||
this.setState({ previewRecord: record })
|
||||
}
|
||||
handleShowCreateForm = () => {
|
||||
this.setState({ formType: cs.FormCreate, record: {} })
|
||||
}
|
||||
|
||||
hideForm = () => {
|
||||
this.setState({ formType: null })
|
||||
}
|
||||
handleShowEditForm = record => {
|
||||
this.setState({ formType: cs.FormEdit, record: record })
|
||||
}
|
||||
|
||||
handleShowCreateForm = () => {
|
||||
this.setState({ formType: cs.FormCreate, record: {} })
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<section className="content templates">
|
||||
<Row>
|
||||
<Col span={22}>
|
||||
<h1>Templates ({this.props.data[cs.ModelTemplates].length}) </h1>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="plus"
|
||||
onClick={this.handleShowCreateForm}
|
||||
>
|
||||
Add template
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
|
||||
handleShowEditForm = (record) => {
|
||||
this.setState({ formType: cs.FormEdit, record: record })
|
||||
}
|
||||
<Table
|
||||
columns={this.columns}
|
||||
rowKey={record => record.id}
|
||||
dataSource={this.props.data[cs.ModelTemplates]}
|
||||
loading={this.props.reqStates[cs.ModelTemplates] !== cs.StateDone}
|
||||
pagination={false}
|
||||
/>
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="content templates">
|
||||
<Row>
|
||||
<Col span={22}><h1>Templates ({this.props.data[cs.ModelTemplates].length}) </h1></Col>
|
||||
<Col span={2}>
|
||||
<Button type="primary" icon="plus" onClick={ this.handleShowCreateForm }>Add template</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<CreateForm
|
||||
{...this.props}
|
||||
formType={this.state.formType}
|
||||
record={this.state.record}
|
||||
onClose={this.hideForm}
|
||||
fetchRecords={this.fetchRecords}
|
||||
/>
|
||||
|
||||
<Table
|
||||
columns={ this.columns }
|
||||
rowKey={ record => record.id }
|
||||
dataSource={ this.props.data[cs.ModelTemplates] }
|
||||
loading={ this.props.reqStates[cs.ModelTemplates] !== cs.StateDone }
|
||||
pagination={ false }
|
||||
/>
|
||||
|
||||
<CreateForm { ...this.props }
|
||||
formType={ this.state.formType }
|
||||
record={ this.state.record }
|
||||
onClose={ this.hideForm }
|
||||
fetchRecords = { this.fetchRecords }
|
||||
/>
|
||||
|
||||
{ this.state.previewRecord &&
|
||||
<ModalPreview
|
||||
title={ this.state.previewRecord.name }
|
||||
previewURL={ cs.Routes.PreviewTemplate.replace(":id", this.state.previewRecord.id) }
|
||||
onCancel={() => {
|
||||
this.setState({ previewRecord: null })
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
{this.state.previewRecord && (
|
||||
<ModalPreview
|
||||
title={this.state.previewRecord.name}
|
||||
previewURL={cs.Routes.PreviewTemplate.replace(
|
||||
":id",
|
||||
this.state.previewRecord.id
|
||||
)}
|
||||
onCancel={() => {
|
||||
this.setState({ previewRecord: null })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Templates
|
||||
|
|
|
@ -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;
|
|
@ -31,19 +31,19 @@ export const MsgPosition = "bottomRight"
|
|||
|
||||
// Model specific.
|
||||
export const CampaignStatusColors = {
|
||||
draft: "",
|
||||
scheduled: "purple",
|
||||
running: "blue",
|
||||
paused: "orange",
|
||||
finished: "green",
|
||||
cancelled: "red",
|
||||
draft: "",
|
||||
scheduled: "purple",
|
||||
running: "blue",
|
||||
paused: "orange",
|
||||
finished: "green",
|
||||
cancelled: "red"
|
||||
}
|
||||
|
||||
export const CampaignStatusDraft = "draft"
|
||||
export const CampaignStatusDraft = "draft"
|
||||
export const CampaignStatusScheduled = "scheduled"
|
||||
export const CampaignStatusRunning = "running"
|
||||
export const CampaignStatusPaused = "paused"
|
||||
export const CampaignStatusFinished = "finished"
|
||||
export const CampaignStatusRunning = "running"
|
||||
export const CampaignStatusPaused = "paused"
|
||||
export const CampaignStatusFinished = "finished"
|
||||
export const CampaignStatusCancelled = "cancelled"
|
||||
|
||||
export const SubscriptionStatusConfirmed = "confirmed"
|
||||
|
@ -52,62 +52,62 @@ export const SubscriptionStatusUnsubscribed = "unsubscribed"
|
|||
|
||||
// API routes.
|
||||
export const Routes = {
|
||||
GetDashboarcStats: "/api/dashboard/stats",
|
||||
GetUsers: "/api/users",
|
||||
|
||||
// Lists.
|
||||
GetLists: "/api/lists",
|
||||
CreateList: "/api/lists",
|
||||
UpdateList: "/api/lists/:id",
|
||||
DeleteList: "/api/lists/:id",
|
||||
GetDashboarcStats: "/api/dashboard/stats",
|
||||
GetUsers: "/api/users",
|
||||
|
||||
// Subscribers.
|
||||
ViewSubscribers: "/subscribers",
|
||||
GetSubscribers: "/api/subscribers",
|
||||
GetSubscriber: "/api/subscribers/:id",
|
||||
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
||||
PreviewCampaign: "/api/campaigns/:id/preview",
|
||||
CreateSubscriber: "/api/subscribers",
|
||||
UpdateSubscriber: "/api/subscribers/:id",
|
||||
DeleteSubscriber: "/api/subscribers/:id",
|
||||
DeleteSubscribers: "/api/subscribers",
|
||||
BlacklistSubscriber: "/api/subscribers/:id/blacklist",
|
||||
BlacklistSubscribers: "/api/subscribers/blacklist",
|
||||
AddSubscriberToLists: "/api/subscribers/lists/:id",
|
||||
AddSubscribersToLists: "/api/subscribers/lists",
|
||||
DeleteSubscribersByQuery: "/api/subscribers/query/delete",
|
||||
BlacklistSubscribersByQuery: "/api/subscribers/query/blacklist",
|
||||
AddSubscribersToListsByQuery: "/api/subscribers/query/lists",
|
||||
|
||||
// Campaigns.
|
||||
ViewCampaigns: "/campaigns",
|
||||
ViewCampaign: "/campaigns/:id",
|
||||
GetCampaignMessengers: "/api/campaigns/messengers",
|
||||
GetCampaigns: "/api/campaigns",
|
||||
GetCampaign: "/api/campaigns/:id",
|
||||
GetRunningCampaignStats: "/api/campaigns/running/stats",
|
||||
CreateCampaign: "/api/campaigns",
|
||||
TestCampaign: "/api/campaigns/:id/test",
|
||||
UpdateCampaign: "/api/campaigns/:id",
|
||||
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
||||
DeleteCampaign: "/api/campaigns/:id",
|
||||
// Lists.
|
||||
GetLists: "/api/lists",
|
||||
CreateList: "/api/lists",
|
||||
UpdateList: "/api/lists/:id",
|
||||
DeleteList: "/api/lists/:id",
|
||||
|
||||
// Media.
|
||||
GetMedia: "/api/media",
|
||||
AddMedia: "/api/media",
|
||||
DeleteMedia: "/api/media/:id",
|
||||
// Subscribers.
|
||||
ViewSubscribers: "/subscribers",
|
||||
GetSubscribers: "/api/subscribers",
|
||||
GetSubscriber: "/api/subscribers/:id",
|
||||
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
||||
PreviewCampaign: "/api/campaigns/:id/preview",
|
||||
CreateSubscriber: "/api/subscribers",
|
||||
UpdateSubscriber: "/api/subscribers/:id",
|
||||
DeleteSubscriber: "/api/subscribers/:id",
|
||||
DeleteSubscribers: "/api/subscribers",
|
||||
BlacklistSubscriber: "/api/subscribers/:id/blacklist",
|
||||
BlacklistSubscribers: "/api/subscribers/blacklist",
|
||||
AddSubscriberToLists: "/api/subscribers/lists/:id",
|
||||
AddSubscribersToLists: "/api/subscribers/lists",
|
||||
DeleteSubscribersByQuery: "/api/subscribers/query/delete",
|
||||
BlacklistSubscribersByQuery: "/api/subscribers/query/blacklist",
|
||||
AddSubscribersToListsByQuery: "/api/subscribers/query/lists",
|
||||
|
||||
// Templates.
|
||||
GetTemplates: "/api/templates",
|
||||
PreviewTemplate: "/api/templates/:id/preview",
|
||||
PreviewNewTemplate: "/api/templates/preview",
|
||||
CreateTemplate: "/api/templates",
|
||||
UpdateTemplate: "/api/templates/:id",
|
||||
SetDefaultTemplate: "/api/templates/:id/default",
|
||||
DeleteTemplate: "/api/templates/:id",
|
||||
// Campaigns.
|
||||
ViewCampaigns: "/campaigns",
|
||||
ViewCampaign: "/campaigns/:id",
|
||||
GetCampaignMessengers: "/api/campaigns/messengers",
|
||||
GetCampaigns: "/api/campaigns",
|
||||
GetCampaign: "/api/campaigns/:id",
|
||||
GetRunningCampaignStats: "/api/campaigns/running/stats",
|
||||
CreateCampaign: "/api/campaigns",
|
||||
TestCampaign: "/api/campaigns/:id/test",
|
||||
UpdateCampaign: "/api/campaigns/:id",
|
||||
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
||||
DeleteCampaign: "/api/campaigns/:id",
|
||||
|
||||
// Import.
|
||||
UploadRouteImport: "/api/import/subscribers",
|
||||
GetRouteImportStats: "/api/import/subscribers",
|
||||
GetRouteImportLogs: "/api/import/subscribers/logs"
|
||||
// Media.
|
||||
GetMedia: "/api/media",
|
||||
AddMedia: "/api/media",
|
||||
DeleteMedia: "/api/media/:id",
|
||||
|
||||
// Templates.
|
||||
GetTemplates: "/api/templates",
|
||||
PreviewTemplate: "/api/templates/:id/preview",
|
||||
PreviewNewTemplate: "/api/templates/preview",
|
||||
CreateTemplate: "/api/templates",
|
||||
UpdateTemplate: "/api/templates/:id",
|
||||
SetDefaultTemplate: "/api/templates/:id/default",
|
||||
DeleteTemplate: "/api/templates/:id",
|
||||
|
||||
// Import.
|
||||
UploadRouteImport: "/api/import/subscribers",
|
||||
GetRouteImportStats: "/api/import/subscribers",
|
||||
GetRouteImportLogs: "/api/import/subscribers/logs"
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import './index.css';
|
||||
import App from './App.js'
|
||||
import "./index.css"
|
||||
import App from "./App.js"
|
||||
|
||||
|
||||
ReactDOM.render((<App />), document.getElementById('root'))
|
||||
ReactDOM.render(<App />, document.getElementById("root"))
|
||||
|
|
|
@ -1,59 +1,82 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Alert } from 'antd';
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import { Alert } from "antd"
|
||||
|
||||
class Utils {
|
||||
static months = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]
|
||||
static days = [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ]
|
||||
static months = [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec"
|
||||
]
|
||||
static days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
|
||||
// Converts the ISO date format to a simpler form.
|
||||
static DateString = (stamp, showTime) => {
|
||||
if(!stamp) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let d = new Date(stamp)
|
||||
|
||||
let out = Utils.days[d.getDay()] + ", " + d.getDate() + " " + Utils.months[d.getMonth()] + " " + d.getFullYear()
|
||||
if(showTime) {
|
||||
out += " " + d.getHours() + ":" + d.getMinutes()
|
||||
}
|
||||
|
||||
return out
|
||||
// Converts the ISO date format to a simpler form.
|
||||
static DateString = (stamp, showTime) => {
|
||||
if (!stamp) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// HttpError takes an axios error and returns an error dict after some sanity checks.
|
||||
static HttpError = (err) => {
|
||||
if (!err.response) {
|
||||
return err
|
||||
}
|
||||
|
||||
if(!err.response.data || !err.response.data.message) {
|
||||
return {
|
||||
"message": err.message + " - " + err.response.request.responseURL,
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
let d = new Date(stamp)
|
||||
let out =
|
||||
Utils.days[d.getDay()] +
|
||||
", " +
|
||||
d.getDate() +
|
||||
" " +
|
||||
Utils.months[d.getMonth()] +
|
||||
" " +
|
||||
d.getFullYear()
|
||||
|
||||
return {
|
||||
"message": err.response.data.message,
|
||||
"data": err.response.data.data
|
||||
}
|
||||
if (showTime) {
|
||||
out += " " + d.getHours() + ":" + d.getMinutes()
|
||||
}
|
||||
|
||||
// Shows a flash message.
|
||||
static Alert = (msg, msgType) => {
|
||||
document.getElementById('alert-container').classList.add('visible')
|
||||
ReactDOM.render(<Alert message={ msg } type={ msgType } showIcon />,
|
||||
document.getElementById('alert-container'))
|
||||
return out
|
||||
}
|
||||
|
||||
// HttpError takes an axios error and returns an error dict after some sanity checks.
|
||||
static HttpError = err => {
|
||||
if (!err.response) {
|
||||
return err
|
||||
}
|
||||
static ModalAlert = (msg, msgType) => {
|
||||
document.getElementById('modal-alert-container').classList.add('visible')
|
||||
ReactDOM.render(<Alert message={ msg } type={ msgType } showIcon />,
|
||||
document.getElementById('modal-alert-container'))
|
||||
|
||||
if (!err.response.data || !err.response.data.message) {
|
||||
return {
|
||||
message: err.message + " - " + err.response.request.responseURL,
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
message: err.response.data.message,
|
||||
data: err.response.data.data
|
||||
}
|
||||
}
|
||||
|
||||
// Shows a flash message.
|
||||
static Alert = (msg, msgType) => {
|
||||
document.getElementById("alert-container").classList.add("visible")
|
||||
ReactDOM.render(
|
||||
<Alert message={msg} type={msgType} showIcon />,
|
||||
document.getElementById("alert-container")
|
||||
)
|
||||
}
|
||||
static ModalAlert = (msg, msgType) => {
|
||||
document.getElementById("modal-alert-container").classList.add("visible")
|
||||
ReactDOM.render(
|
||||
<Alert message={msg} type={msgType} showIcon />,
|
||||
document.getElementById("modal-alert-container")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Utils
|
||||
|
|
|
@ -5,9 +5,8 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/lib/pq"
|
||||
)
|
||||
|
||||
// Queries contains all prepared SQL queries.
|
||||
|
|
Loading…
Reference in New Issue