From ac2234a838f68d7f82b0ea38851bccb86e8d5aaa Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Fri, 4 Jan 2019 18:27:34 +0530 Subject: [PATCH] Refactored subscriber add/edit from from modal to modal + standalone view --- frontend/my/src/Layout.js | 2 + frontend/my/src/Lists.js | 2 +- frontend/my/src/Subscriber.js | 292 +++++++++++++++++++++++++++++++++ frontend/my/src/Subscribers.js | 238 ++++++--------------------- frontend/my/src/constants.js | 9 +- 5 files changed, 353 insertions(+), 190 deletions(-) create mode 100644 frontend/my/src/Subscriber.js diff --git a/frontend/my/src/Layout.js b/frontend/my/src/Layout.js index dc3d7de..242dee2 100644 --- a/frontend/my/src/Layout.js +++ b/frontend/my/src/Layout.js @@ -9,6 +9,7 @@ import logo from "./static/listmonk.svg" import Dashboard from "./Dashboard" import Lists from "./Lists" import Subscribers from "./Subscribers" +import Subscriber from "./Subscriber" import Templates from "./Templates" import Import from "./Import" import Test from "./Test" @@ -87,6 +88,7 @@ class Base extends React.Component { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/my/src/Lists.js b/frontend/my/src/Lists.js index 162fb44..6dcef4f 100644 --- a/frontend/my/src/Lists.js +++ b/frontend/my/src/Lists.js @@ -62,7 +62,7 @@ class CreateFormDef extends React.PureComponent { {" "} { record.name }
- ID { record.id } — UUID { record.uuid } + ID { record.id } / UUID { record.uuid } ) } diff --git a/frontend/my/src/Subscriber.js b/frontend/my/src/Subscriber.js new file mode 100644 index 0000000..f025896 --- /dev/null +++ b/frontend/my/src/Subscriber.js @@ -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 ( + +
+ + {getFieldDecorator("email", { + initialValue: record.email, + rules: [{ required: true }] + })()} + + + {getFieldDecorator("name", { + initialValue: record.name, + rules: [{ required: true }] + })()} + + + {getFieldDecorator("status", { initialValue: record.status ? record.status : "enabled", rules: [{ required: true, message: "Type is required" }] })( + + )} + + + {getFieldDecorator("lists", { initialValue: subListIDs })( + + )} + + +
+ {getFieldDecorator("attribs", { + initialValue: record.attribs ? JSON.stringify(record.attribs, null, 4) : "" + })( + + )} +
+

Attributes are defined as a JSON map, for example: + {' {"age": 30, "color": "red", "is_user": true}'}. More info.

+
+ { !this.props.isModal && + + + {" "} + { this.props.formType === cs.FormEdit && + { + this.handleDeleteRecord(record) + }}> + + + } + + } +
+
+ ) + } +} + +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 ( +
+
+ + + { !this.state.record.id && +

Add subscriber

+ } + { this.state.record.id && +
+

+ { this.state.record.status } + {" "} + { this.state.record.name } ({ this.state.record.email }) +

+ ID { this.state.record.id } / UUID { this.state.record.uuid } +
+ } + + + +
+
+
+ + { + 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 }) + }} + /> + +
+
+ ) + } +} + +export default Subscriber diff --git a/frontend/my/src/Subscribers.js b/frontend/my/src/Subscribers.js index d95b51a..b5415c9 100644 --- a/frontend/my/src/Subscribers.js +++ b/frontend/my/src/Subscribers.js @@ -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 Utils from "./utils" +import Subscriber from "./Subscriber" import * as cs from "./constants" @@ -11,183 +12,6 @@ const tagColors = { "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 ( -
- { record.status } - {" "} - { record.name } ({ record.email }) -
- ID { record.id } — UUID { record.uuid } -
- ) - } - - 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 ( - - - - -
- - {getFieldDecorator("email", { - initialValue: record.email, - rules: [{ required: true }] - })()} - - - {getFieldDecorator("name", { - initialValue: record.name, - rules: [{ required: true }] - })()} - - - {getFieldDecorator("status", { initialValue: record.status ? record.status : "enabled", rules: [{ required: true, message: "Type is required" }] })( - - )} - - - {getFieldDecorator("lists", { initialValue: subListIDs })( - - )} - -
-

Attributes

-

Attributes can be defined as a JSON map, for example: - {'{"age": 30, "color": "red", "is_user": true}'}. More info.

- -
- {getFieldDecorator("attribs-json", { - initialValue: JSON.stringify(this.state.attribs, null, 4) - })( - )} -
-
-
-
-
- ) - } -} - - class ListsFormDef extends React.PureComponent { state = { modalWaiting: false @@ -273,7 +97,6 @@ class ListsFormDef extends React.PureComponent { } } -const CreateForm = Form.create()(CreateFormDef) const ListsForm = Form.create()(ListsFormDef) class Subscribers extends React.PureComponent { @@ -282,6 +105,7 @@ class Subscribers extends React.PureComponent { state = { formType: null, listsFormVisible: false, + modalForm: null, record: {}, queryParams: { page: 1, @@ -327,7 +151,14 @@ class Subscribers extends React.PureComponent { const out = []; out.push(
- { this.handleShowEditForm(record)}}>{text} + { + // Open the individual subscriber page on ctrl+click + // and the modal otherwise. + if(!e.ctrlKey) { + this.handleShowEditForm(record) + e.preventDefault() + } + }}>{ text }
) @@ -350,7 +181,14 @@ class Subscribers extends React.PureComponent { width: "15%", render: (text, record) => { return ( - this.handleShowEditForm(record)}>{text} + { + // Open the individual subscriber page on ctrl+click + // and the modal otherwise. + if(!e.ctrlKey) { + this.handleShowEditForm(record) + e.preventDefault() + } + }}>{ text } ) } }, @@ -718,14 +556,38 @@ class Subscribers extends React.PureComponent { }} /> - { this.state.formType !== null && + { this.state.formType !== null && + { + if(!this.state.modalForm) { + 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 }}> + { + if(!r) { + return + } + + this.setState({ modalForm: r }) + }}/> + } { this.state.listsFormVisible &&