Refactored subscriber add/edit from from modal to modal + standalone view
This commit is contained in:
parent
ab1a6bbed8
commit
ac2234a838
|
@ -9,6 +9,7 @@ import logo from "./static/listmonk.svg"
|
||||||
import Dashboard from "./Dashboard"
|
import Dashboard from "./Dashboard"
|
||||||
import Lists from "./Lists"
|
import Lists from "./Lists"
|
||||||
import Subscribers from "./Subscribers"
|
import Subscribers from "./Subscribers"
|
||||||
|
import Subscriber from "./Subscriber"
|
||||||
import Templates from "./Templates"
|
import Templates from "./Templates"
|
||||||
import Import from "./Import"
|
import Import from "./Import"
|
||||||
import Test from "./Test"
|
import Test from "./Test"
|
||||||
|
@ -87,6 +88,7 @@ class Base extends React.Component {
|
||||||
<Route exact key="/subscribers" path="/subscribers" render={(props) => <Subscribers { ...{ ...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/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/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" 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/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/media" path="/campaigns/media" render={(props) => <Media { ...{ ...this.props, route: props } } />} />
|
||||||
|
|
|
@ -62,7 +62,7 @@ class CreateFormDef extends React.PureComponent {
|
||||||
{" "}
|
{" "}
|
||||||
{ record.name }
|
{ record.name }
|
||||||
<br />
|
<br />
|
||||||
<span className="text-tiny text-grey">ID { record.id } — UUID { record.uuid }</span>
|
<span className="text-tiny text-grey">ID { record.id } / UUID { record.uuid }</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,292 @@
|
||||||
|
import React from "react"
|
||||||
|
import { Row, Col, Form, Input, Select, Button, Tag, Spin, Popconfirm, notification } from "antd"
|
||||||
|
|
||||||
|
import * as cs from "./constants"
|
||||||
|
|
||||||
|
const tagColors = {
|
||||||
|
"enabled": "green",
|
||||||
|
"blacklisted": "red"
|
||||||
|
}
|
||||||
|
const formItemLayoutModal = {
|
||||||
|
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 } }
|
||||||
|
}
|
||||||
|
const formItemTailLayout = {
|
||||||
|
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateFormDef extends React.PureComponent {
|
||||||
|
state = {
|
||||||
|
confirmDirty: false,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormRef = (r) => {
|
||||||
|
this.setState({ formRef: r })
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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
|
|
@ -3,6 +3,7 @@ import { Link } from "react-router-dom"
|
||||||
import { Row, Col, Modal, Form, Input, Select, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification, Radio } from "antd"
|
import { Row, Col, Modal, Form, Input, Select, Button, Table, Icon, Tooltip, Tag, Popconfirm, Spin, notification, Radio } from "antd"
|
||||||
|
|
||||||
import Utils from "./utils"
|
import Utils from "./utils"
|
||||||
|
import Subscriber from "./Subscriber"
|
||||||
import * as cs from "./constants"
|
import * as cs from "./constants"
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,183 +12,6 @@ const tagColors = {
|
||||||
"blacklisted": "red"
|
"blacklisted": "red"
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateFormDef extends React.PureComponent {
|
|
||||||
state = {
|
|
||||||
confirmDirty: false,
|
|
||||||
attribs: {},
|
|
||||||
modalWaiting: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setState({ attribs: this.props.record.attribs })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
values["attribs"] = {}
|
|
||||||
|
|
||||||
let a = this.props.form.getFieldValue("attribs-json")
|
|
||||||
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({ modalWaiting: 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` })
|
|
||||||
this.props.fetchRecords()
|
|
||||||
this.props.onClose()
|
|
||||||
}).catch(e => {
|
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
|
||||||
this.setState({ modalWaiting: 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(() => {
|
|
||||||
notification["success"]({ message: "Subscriber modified", description: `${values["email"]} modified` })
|
|
||||||
|
|
||||||
// Reload the table.
|
|
||||||
this.props.fetchRecords()
|
|
||||||
this.props.onClose()
|
|
||||||
}).catch(e => {
|
|
||||||
notification["error"]({ message: "Error", description: e.message })
|
|
||||||
this.setState({ modalWaiting: false })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modalTitle(formType, record) {
|
|
||||||
if(formType === cs.FormCreate) {
|
|
||||||
return "Add subscriber"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Tag color={ tagColors.hasOwnProperty(record.status) ? tagColors[record.status] : "" }>{ record.status }</Tag>
|
|
||||||
{" "}
|
|
||||||
{ record.name } ({ record.email })
|
|
||||||
<br />
|
|
||||||
<span className="text-tiny text-grey">ID { record.id } — UUID { record.uuid }</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal visible={ true } width="750px"
|
|
||||||
className="subscriber-modal"
|
|
||||||
title={ this.modalTitle(formType, record) }
|
|
||||||
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
|
|
||||||
confirmLoading={ this.state.modalWaiting }
|
|
||||||
onCancel={ onClose }
|
|
||||||
onOk={ this.handleSubmit }
|
|
||||||
okButtonProps={{ disabled: this.props.reqStates[cs.ModelSubscribers] === cs.StatePending }}>
|
|
||||||
|
|
||||||
<div id="modal-alert-container"></div>
|
|
||||||
<Spin spinning={ this.props.reqStates[cs.ModelSubscribers] === cs.StatePending }>
|
|
||||||
<Form onSubmit={ this.handleSubmit }>
|
|
||||||
<Form.Item {...formItemLayout} label="E-mail">
|
|
||||||
{getFieldDecorator("email", {
|
|
||||||
initialValue: record.email,
|
|
||||||
rules: [{ required: true }]
|
|
||||||
})(<Input autoFocus pattern="(.+?)@(.+?)" maxLength="200" />)}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item {...formItemLayout} label="Name">
|
|
||||||
{getFieldDecorator("name", {
|
|
||||||
initialValue: record.name,
|
|
||||||
rules: [{ required: true }]
|
|
||||||
})(<Input maxLength="200" />)}
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item {...formItemLayout} 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 {...formItemLayout} 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>
|
|
||||||
<section>
|
|
||||||
<h3>Attributes</h3>
|
|
||||||
<p className="ant-form-extra">Attributes can be defined as a JSON map, for example:
|
|
||||||
{'{"age": 30, "color": "red", "is_user": true}'}. <a href="">More info</a>.</p>
|
|
||||||
|
|
||||||
<div className="json-editor">
|
|
||||||
{getFieldDecorator("attribs-json", {
|
|
||||||
initialValue: JSON.stringify(this.state.attribs, null, 4)
|
|
||||||
})(
|
|
||||||
<Input.TextArea placeholder="{}"
|
|
||||||
rows={10}
|
|
||||||
readOnly={false}
|
|
||||||
autosize={{ minRows: 5, maxRows: 10 }} />)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Form>
|
|
||||||
</Spin>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ListsFormDef extends React.PureComponent {
|
class ListsFormDef extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
modalWaiting: false
|
modalWaiting: false
|
||||||
|
@ -273,7 +97,6 @@ class ListsFormDef extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateForm = Form.create()(CreateFormDef)
|
|
||||||
const ListsForm = Form.create()(ListsFormDef)
|
const ListsForm = Form.create()(ListsFormDef)
|
||||||
|
|
||||||
class Subscribers extends React.PureComponent {
|
class Subscribers extends React.PureComponent {
|
||||||
|
@ -282,6 +105,7 @@ class Subscribers extends React.PureComponent {
|
||||||
state = {
|
state = {
|
||||||
formType: null,
|
formType: null,
|
||||||
listsFormVisible: false,
|
listsFormVisible: false,
|
||||||
|
modalForm: null,
|
||||||
record: {},
|
record: {},
|
||||||
queryParams: {
|
queryParams: {
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -327,7 +151,14 @@ class Subscribers extends React.PureComponent {
|
||||||
const out = [];
|
const out = [];
|
||||||
out.push(
|
out.push(
|
||||||
<div key={`sub-email-${ record.id }`} className="sub-name">
|
<div key={`sub-email-${ record.id }`} className="sub-name">
|
||||||
<a role="button" onClick={() => { this.handleShowEditForm(record)}}>{text}</a>
|
<Link to={ `/subscribers/${record.id}` } onClick={(e) => {
|
||||||
|
// Open the individual subscriber page on ctrl+click
|
||||||
|
// and the modal otherwise.
|
||||||
|
if(!e.ctrlKey) {
|
||||||
|
this.handleShowEditForm(record)
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}>{ text }</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -350,7 +181,14 @@ class Subscribers extends React.PureComponent {
|
||||||
width: "15%",
|
width: "15%",
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return (
|
return (
|
||||||
<a role="button" onClick={() => this.handleShowEditForm(record)}>{text}</a>
|
<Link to={ `/subscribers/${record.id}` } onClick={(e) => {
|
||||||
|
// Open the individual subscriber page on ctrl+click
|
||||||
|
// and the modal otherwise.
|
||||||
|
if(!e.ctrlKey) {
|
||||||
|
this.handleShowEditForm(record)
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}>{ text }</Link>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -718,14 +556,38 @@ class Subscribers extends React.PureComponent {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ this.state.formType !== null && <CreateForm {...this.props}
|
{ this.state.formType !== null &&
|
||||||
formType={ this.state.formType }
|
<Modal visible={ true } width="750px"
|
||||||
record={ this.state.record }
|
className="subscriber-modal"
|
||||||
lists={ this.props.data[cs.ModelLists] }
|
okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
|
||||||
list={ this.state.queryParams.list }
|
confirmLoading={ this.state.modalWaiting }
|
||||||
fetchRecords={ this.fetchRecords }
|
onOk={(e) => {
|
||||||
queryParams= { this.state.queryParams }
|
if(!this.state.modalForm) {
|
||||||
onClose={ this.handleHideForm } />
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This submits the form embedded in the Subscriber component.
|
||||||
|
this.state.modalForm.submitForm(e, (ok) => {
|
||||||
|
if(ok) {
|
||||||
|
this.handleHideForm()
|
||||||
|
this.fetchRecords()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
onCancel={ this.handleHideForm }
|
||||||
|
okButtonProps={{ disabled: this.props.reqStates[cs.ModelSubscribers] === cs.StatePending }}>
|
||||||
|
<Subscriber {...this.props}
|
||||||
|
isModal={ true }
|
||||||
|
formType={ this.state.formType }
|
||||||
|
record={ this.state.record }
|
||||||
|
ref={ (r) => {
|
||||||
|
if(!r) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ modalForm: r })
|
||||||
|
}}/>
|
||||||
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
||||||
{ this.state.listsFormVisible && <ListsForm {...this.props}
|
{ this.state.listsFormVisible && <ListsForm {...this.props}
|
||||||
|
|
|
@ -55,12 +55,16 @@ export const Routes = {
|
||||||
GetDashboarcStats: "/api/dashboard/stats",
|
GetDashboarcStats: "/api/dashboard/stats",
|
||||||
GetUsers: "/api/users",
|
GetUsers: "/api/users",
|
||||||
|
|
||||||
|
// Lists.
|
||||||
GetLists: "/api/lists",
|
GetLists: "/api/lists",
|
||||||
CreateList: "/api/lists",
|
CreateList: "/api/lists",
|
||||||
UpdateList: "/api/lists/:id",
|
UpdateList: "/api/lists/:id",
|
||||||
DeleteList: "/api/lists/:id",
|
DeleteList: "/api/lists/:id",
|
||||||
|
|
||||||
|
// Subscribers.
|
||||||
|
ViewSubscribers: "/subscribers",
|
||||||
GetSubscribers: "/api/subscribers",
|
GetSubscribers: "/api/subscribers",
|
||||||
|
GetSubscriber: "/api/subscribers/:id",
|
||||||
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
GetSubscribersByList: "/api/subscribers/lists/:listID",
|
||||||
PreviewCampaign: "/api/campaigns/:id/preview",
|
PreviewCampaign: "/api/campaigns/:id/preview",
|
||||||
CreateSubscriber: "/api/subscribers",
|
CreateSubscriber: "/api/subscribers",
|
||||||
|
@ -71,11 +75,11 @@ export const Routes = {
|
||||||
BlacklistSubscribers: "/api/subscribers/blacklist",
|
BlacklistSubscribers: "/api/subscribers/blacklist",
|
||||||
AddSubscriberToLists: "/api/subscribers/lists/:id",
|
AddSubscriberToLists: "/api/subscribers/lists/:id",
|
||||||
AddSubscribersToLists: "/api/subscribers/lists",
|
AddSubscribersToLists: "/api/subscribers/lists",
|
||||||
|
|
||||||
DeleteSubscribersByQuery: "/api/subscribers/query/delete",
|
DeleteSubscribersByQuery: "/api/subscribers/query/delete",
|
||||||
BlacklistSubscribersByQuery: "/api/subscribers/query/blacklist",
|
BlacklistSubscribersByQuery: "/api/subscribers/query/blacklist",
|
||||||
AddSubscribersToListsByQuery: "/api/subscribers/query/lists",
|
AddSubscribersToListsByQuery: "/api/subscribers/query/lists",
|
||||||
|
|
||||||
|
// Campaigns.
|
||||||
ViewCampaigns: "/campaigns",
|
ViewCampaigns: "/campaigns",
|
||||||
ViewCampaign: "/campaigns/:id",
|
ViewCampaign: "/campaigns/:id",
|
||||||
GetCampaignMessengers: "/api/campaigns/messengers",
|
GetCampaignMessengers: "/api/campaigns/messengers",
|
||||||
|
@ -88,10 +92,12 @@ export const Routes = {
|
||||||
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
UpdateCampaignStatus: "/api/campaigns/:id/status",
|
||||||
DeleteCampaign: "/api/campaigns/:id",
|
DeleteCampaign: "/api/campaigns/:id",
|
||||||
|
|
||||||
|
// Media.
|
||||||
GetMedia: "/api/media",
|
GetMedia: "/api/media",
|
||||||
AddMedia: "/api/media",
|
AddMedia: "/api/media",
|
||||||
DeleteMedia: "/api/media/:id",
|
DeleteMedia: "/api/media/:id",
|
||||||
|
|
||||||
|
// Templates.
|
||||||
GetTemplates: "/api/templates",
|
GetTemplates: "/api/templates",
|
||||||
PreviewTemplate: "/api/templates/:id/preview",
|
PreviewTemplate: "/api/templates/:id/preview",
|
||||||
PreviewNewTemplate: "/api/templates/preview",
|
PreviewNewTemplate: "/api/templates/preview",
|
||||||
|
@ -100,6 +106,7 @@ export const Routes = {
|
||||||
SetDefaultTemplate: "/api/templates/:id/default",
|
SetDefaultTemplate: "/api/templates/:id/default",
|
||||||
DeleteTemplate: "/api/templates/:id",
|
DeleteTemplate: "/api/templates/:id",
|
||||||
|
|
||||||
|
// Import.
|
||||||
UploadRouteImport: "/api/import/subscribers",
|
UploadRouteImport: "/api/import/subscribers",
|
||||||
GetRouteImportStats: "/api/import/subscribers",
|
GetRouteImportStats: "/api/import/subscribers",
|
||||||
GetRouteImportLogs: "/api/import/subscribers/logs"
|
GetRouteImportLogs: "/api/import/subscribers/logs"
|
||||||
|
|
Loading…
Reference in New Issue