listmonk/frontend/src/Subscriber.js

418 lines
12 KiB
JavaScript
Raw Normal View History

import React from "react";
2019-03-09 08:46:47 +01:00
import {
Row,
Col,
Form,
Input,
Select,
Button,
Tag,
Tooltip,
Icon,
2019-03-09 08:46:47 +01:00
Spin,
Popconfirm,
notification
} from "antd";
import * as cs from "./constants";
const tagColors = {
2019-03-09 08:46:47 +01:00
enabled: "green",
blacklisted: "red"
};
const formItemLayoutModal = {
2019-03-09 08:46:47 +01:00
labelCol: { xs: { span: 24 }, sm: { span: 4 } },
wrapperCol: { xs: { span: 24 }, sm: { span: 18 } }
};
const formItemLayout = {
2019-03-09 08:46:47 +01:00
labelCol: { xs: { span: 16 }, sm: { span: 4 } },
wrapperCol: { xs: { span: 16 }, sm: { span: 10 } }
};
const formItemTailLayout = {
2019-03-09 08:46:47 +01:00
wrapperCol: { xs: { span: 24, offset: 0 }, sm: { span: 10, offset: 4 } }
};
class CreateFormDef extends React.PureComponent {
2019-03-09 08:46:47 +01:00
state = {
confirmDirty: false,
loading: false
};
2019-03-09 08:46:47 +01:00
// Handle create / edit form submission.
handleSubmit = (e, cb) => {
e.preventDefault();
2019-03-09 08:46:47 +01:00
if (!cb) {
// Set a fake callback.
cb = () => {};
2019-03-09 08:46:47 +01:00
}
2019-03-09 08:46:47 +01:00
var err = null,
values = {};
2019-03-09 08:46:47 +01:00
this.props.form.validateFields((e, v) => {
err = e;
values = v;
});
2019-03-09 08:46:47 +01:00
if (err) {
return;
2019-03-09 08:46:47 +01:00
}
let a = values["attribs"];
values["attribs"] = {};
2019-03-09 08:46:47 +01:00
if (a && a.length > 0) {
try {
values["attribs"] = JSON.parse(a);
2019-03-09 08:46:47 +01:00
if (values["attribs"] instanceof Array) {
notification["error"]({
message: "Invalid JSON type",
description: "Attributes should be a map {} and not an array []"
});
return;
}
2019-03-09 08:46:47 +01:00
} catch (e) {
notification["error"]({
message: "Invalid JSON in attributes",
description: e.toString()
});
return;
2019-03-09 08:46:47 +01:00
}
}
this.setState({ loading: true });
2019-03-09 08:46:47 +01:00
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`
});
2019-03-09 08:46:47 +01:00
if (!this.props.isModal) {
this.props.fetchRecord(this.props.record.id);
2019-03-09 08:46:47 +01:00
}
cb(true);
this.setState({ loading: false });
2019-03-09 08:46:47 +01:00
})
.catch(e => {
notification["error"]({ message: "Error", description: e.message });
cb(false);
this.setState({ loading: false });
});
2019-03-09 08:46:47 +01:00
} else {
// Edit a subscriber.
delete values["keys"];
delete values["vals"];
2019-03-09 08:46:47 +01:00
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`
});
2019-03-09 08:46:47 +01:00
if (!this.props.isModal) {
this.props.fetchRecord(this.props.record.id);
2019-03-09 08:46:47 +01:00
}
cb(true);
this.setState({ loading: false });
2019-03-09 08:46:47 +01:00
})
.catch(e => {
notification["error"]({ message: "Error", description: e.message });
cb(false);
this.setState({ loading: false });
});
}
};
2019-03-09 08:46:47 +01:00
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`
});
2019-03-09 08:46:47 +01:00
this.props.route.history.push({
pathname: cs.Routes.ViewSubscribers
});
2019-03-09 08:46:47 +01:00
})
.catch(e => {
notification["error"]({ message: "Error", description: e.message });
});
};
2019-03-09 08:46:47 +01:00
render() {
const { formType, record } = this.props;
const { getFieldDecorator } = this.props.form;
2019-03-09 08:46:47 +01:00
if (formType === null) {
return null;
2019-03-09 08:46:47 +01:00
}
let subListIDs = [];
let subStatuses = {};
2019-03-09 08:46:47 +01:00
if (this.props.record && this.props.record.lists) {
subListIDs = this.props.record.lists.map(v => {
return v["id"];
});
2019-03-09 08:46:47 +01:00
subStatuses = this.props.record.lists.reduce(
(o, item) => ({ ...o, [item.id]: item.subscription_status }),
{}
);
2019-03-09 08:46:47 +01:00
} else if (this.props.list) {
subListIDs = [this.props.list.id];
2019-03-09 08:46:47 +01:00
}
const layout = this.props.isModal ? formItemLayoutModal : formItemLayout;
2019-03-09 08:46:47 +01:00
return (
<Spin spinning={this.state.loading}>
<Form onSubmit={this.handleSubmit}>
<Form.Item {...layout} label="E-mail">
{getFieldDecorator("email", {
initialValue: record.email,
rules: [{ required: true }]
2019-03-28 13:34:27 +01:00
})(<Input autoFocus pattern="(.+?)@(.+?)" maxLength={200} />)}
2019-03-09 08:46:47 +01:00
</Form.Item>
<Form.Item {...layout} label="Name">
{getFieldDecorator("name", {
initialValue: record.name,
rules: [{ required: true }]
2019-03-28 13:34:27 +01:00
})(<Input maxLength={200} />)}
2019-03-09 08:46:47 +01:00
</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
}
2019-03-09 08:46:47 +01:00
>
<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="https://listmonk.app/docs/concepts"
rel="noopener noreferrer"
target="_blank"
>
More info
</a>
.
2019-03-09 08:46:47 +01:00
</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);
2019-03-09 08:46:47 +01:00
}}
>
<Button icon="delete">Delete</Button>
</Popconfirm>
)}
</Form.Item>
)}
</Form>
</Spin>
);
2019-03-09 08:46:47 +01:00
}
}
const CreateForm = Form.create()(CreateFormDef);
class Subscriber extends React.PureComponent {
2019-03-09 08:46:47 +01:00
state = {
loading: true,
formRef: null,
record: {},
subID: this.props.route.match.params
? parseInt(this.props.route.match.params.subID, 10)
: 0
};
2019-03-09 08:46:47 +01:00
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);
2019-03-09 08:46:47 +01:00
// Fetch subscriber.
this.fetchRecord(this.state.subID);
2019-03-09 08:46:47 +01:00
} else {
this.setState({ record: this.props.record, loading: false });
}
2019-03-09 08:46:47 +01:00
}
2019-03-09 08:46:47 +01:00
fetchRecord = id => {
this.props
.request(cs.Routes.GetSubscriber, cs.MethodGet, { id: id })
.then(r => {
this.setState({ record: r.data.data, loading: false });
2019-03-09 08:46:47 +01:00
})
.catch(e => {
notification["error"]({
placement: cs.MsgPosition,
message: "Error",
description: e.message
});
});
};
2019-03-09 08:46:47 +01:00
setFormRef = r => {
this.setState({ formRef: r });
};
2019-03-09 08:46:47 +01:00
submitForm = (e, cb) => {
if (this.state.formRef) {
this.state.formRef.handleSubmit(e, cb);
}
};
2019-03-09 08:46:47 +01:00
render() {
return (
<section className="content">
<header className="header">
<Row>
<Col span={22}>
2019-03-09 08:46:47 +01:00
{!this.state.record.id && <h1>Add subscriber</h1>}
{this.state.record.id && (
<div>
2019-03-09 08:46:47 +01:00
<h1>
<Tag className="subscriber-status"
2019-03-09 08:46:47 +01:00
color={
tagColors.hasOwnProperty(this.state.record.status)
? tagColors[this.state.record.status]
: ""
}
>
{this.state.record.status}
</Tag>{" "}
<span className="subscriber-name">
{this.state.record.name} ({this.state.record.email})
</span>
2019-03-09 08:46:47 +01:00
</h1>
<span className="text-small text-grey">
ID {this.state.record.id} / UUID {this.state.record.uuid}
</span>
</div>
2019-03-09 08:46:47 +01:00
)}
</Col>
<Col span={2} className="right subscriber-export">
<Tooltip title="Export data" placement="top">
<a
role="button"
href={"/api/subscribers/" + this.state.record.id + "/export"}
>
<Icon type="export" style={{ fontSize: "20px" }}/>
</a>
</Tooltip>
</Col>
2019-03-09 08:46:47 +01:00
</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].results}
2019-03-09 08:46:47 +01:00
wrappedComponentRef={r => {
if (!r) {
return;
2019-03-09 08:46:47 +01:00
}
// 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 });
2019-03-09 08:46:47 +01:00
}}
/>
</Spin>
</div>
</section>
);
2019-03-09 08:46:47 +01:00
}
}
export default Subscriber;