Browse Source

Refactored subscriber add/edit from from modal to modal + standalone view

Kailash Nadh 6 years ago
parent
commit
ac2234a838

+ 2 - 0
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 {
                                 <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 } } />} />

+ 1 - 1
frontend/my/src/Lists.js

@@ -62,7 +62,7 @@ class CreateFormDef extends React.PureComponent {
                 {" "}
                 { record.name }
                 <br />                
-                <span className="text-tiny text-grey">ID { record.id } &mdash; UUID { record.uuid }</span>
+                <span className="text-tiny text-grey">ID { record.id } / UUID { record.uuid }</span>
             </div>
         )
     }

+ 292 - 0
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 (
+            <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

+ 50 - 188
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 (
-             <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 } &mdash; 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 {
     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(
                     <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>
                 )
                 
@@ -350,7 +181,14 @@ class Subscribers extends React.PureComponent {
             width: "15%",
             render: (text, record) => {
                 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}
-                    formType={ this.state.formType }
-                    record={ this.state.record }
-                    lists={ this.props.data[cs.ModelLists] }
-                    list={ this.state.queryParams.list }
-                    fetchRecords={ this.fetchRecords }
-                    queryParams= { this.state.queryParams }
-                    onClose={ this.handleHideForm } />
+                { this.state.formType !== null &&
+                    <Modal visible={ true } width="750px"
+                        className="subscriber-modal"
+                        okText={ this.state.form === cs.FormCreate ? "Add" : "Save" }
+                        confirmLoading={ this.state.modalWaiting }
+                        onOk={(e) => {
+                            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 }}>            
+                        <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}

+ 8 - 1
frontend/my/src/constants.js

@@ -55,12 +55,16 @@ 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",
 
+    // Subscribers.
+    ViewSubscribers: "/subscribers",
     GetSubscribers: "/api/subscribers",
+    GetSubscriber: "/api/subscribers/:id",
     GetSubscribersByList: "/api/subscribers/lists/:listID",
     PreviewCampaign: "/api/campaigns/:id/preview",
     CreateSubscriber: "/api/subscribers",
@@ -71,11 +75,11 @@ export const Routes = {
     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",
@@ -88,10 +92,12 @@ export const Routes = {
     UpdateCampaignStatus: "/api/campaigns/:id/status",
     DeleteCampaign: "/api/campaigns/:id",
 
+    // Media.
     GetMedia: "/api/media",
     AddMedia: "/api/media",
     DeleteMedia: "/api/media/:id",
 
+    // Templates.
     GetTemplates: "/api/templates",
     PreviewTemplate: "/api/templates/:id/preview",
     PreviewNewTemplate: "/api/templates/preview",
@@ -100,6 +106,7 @@ export const Routes = {
     SetDefaultTemplate: "/api/templates/:id/default",
     DeleteTemplate: "/api/templates/:id",
 
+    // Import.
     UploadRouteImport: "/api/import/subscribers",
     GetRouteImportStats: "/api/import/subscribers",
     GetRouteImportLogs: "/api/import/subscribers/logs"