- { !isDone &&
-
- }
-
Sent
- { sent >= toSend &&
- { toSend }
- }
- { sent < toSend &&
- { sent } / { toSend }
- }
-
- { record.status === cs.CampaignStatusRunning &&
-
- }
- |
-
- { rate > 0 &&
-
Rate { Math.round(rate, 2) } / min|
- }
-
-
Views { record.views }|
-
Clicks { record.clicks }|
-
-
Created { dayjs(record.created_at).format(cs.DateFormat) }|
-
- { startedAt &&
-
Started { dayjs(startedAt).format(cs.DateFormat) }|
- }
- { isDone &&
-
Ended
- { dayjs(updatedAt).format(cs.DateFormat) }
- |
- }
- { startedAt && updatedAt &&
-
Duration
- { dayjs(updatedAt).from(dayjs(startedAt), true) }
- |
- }
+ this.columns = [
+ {
+ title: "Name",
+ dataIndex: "name",
+ sorter: true,
+ width: "20%",
+ vAlign: "top",
+ render: (text, record) => {
+ const out = []
+ out.push(
+
+ {text}
+
+ {record.subject}
- )
- }
+ )
- componentDidMount() {
- this.props.pageTitle("Campaigns")
- dayjs.extend(relativeTime)
- this.fetchRecords()
-
- // Did we land here to start a campaign?
- let loc = this.props.route.location
- let state = loc.state
- if(state && state.hasOwnProperty("campaign")) {
- this.handleUpdateStatus(state.campaign, state.campaignStatus)
- delete state.campaign
- delete state.campaignStatus
- this.props.route.history.replace({ ...loc, state })
- }
- }
-
- componentWillUnmount() {
- window.clearInterval(this.state.pollID)
- }
-
- fetchRecords = (params) => {
- let qParams = {
- page: this.state.queryParams.page,
- per_page: this.state.queryParams.per_page
- }
-
- // The records are for a specific list.
- if(this.state.queryParams.listID) {
- qParams.listID = this.state.queryParams.listID
- }
-
- if(params) {
- qParams = { ...qParams, ...params }
- }
-
- this.props.modelRequest(cs.ModelCampaigns, cs.Routes.GetCampaigns, cs.MethodGet, qParams).then((r) => {
- this.startStatsPoll()
- })
- }
-
- startStatsPoll = () => {
- window.clearInterval(this.state.pollID)
- this.setState({ "stats": {} })
-
- // If there's at least one running campaign, start polling.
- let hasRunning = false
- this.props.data[cs.ModelCampaigns].forEach((c) => {
- if(c.status === cs.CampaignStatusRunning) {
- hasRunning = true
- return
+ if (record.tags.length > 0) {
+ for (let i = 0; i < record.tags.length; i++) {
+ out.push(
{record.tags[i]} )
}
- })
+ }
- if(!hasRunning) {
+ return out
+ }
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ className: "status",
+ width: "10%",
+ render: (status, record) => {
+ let color = cs.CampaignStatusColors.hasOwnProperty(status)
+ ? cs.CampaignStatusColors[status]
+ : ""
+ return (
+
+ {status}
+ {record.send_at && (
+
+ Scheduled —{" "}
+ {dayjs(record.send_at).format(cs.DateFormat)}
+
+ )}
+
+ )
+ }
+ },
+ {
+ title: "Lists",
+ dataIndex: "lists",
+ width: "25%",
+ align: "left",
+ className: "lists",
+ render: (lists, record) => {
+ const out = []
+ lists.forEach(l => {
+ out.push(
+
+ {l.name}
+
+ )
+ })
+
+ return out
+ }
+ },
+ {
+ title: "Stats",
+ className: "stats",
+ width: "30%",
+ render: (text, record) => {
+ if (
+ record.status !== cs.CampaignStatusDraft &&
+ record.status !== cs.CampaignStatusScheduled
+ ) {
+ return this.renderStats(record)
+ }
+ }
+ },
+ {
+ title: "",
+ dataIndex: "actions",
+ className: "actions",
+ width: "15%",
+ render: (text, record) => {
+ return (
+
+ {record.status === cs.CampaignStatusPaused && (
+
+ this.handleUpdateStatus(record, cs.CampaignStatusRunning)
+ }
+ >
+
+
+
+
+
+
+ )}
+
+ {record.status === cs.CampaignStatusRunning && (
+
+ this.handleUpdateStatus(record, cs.CampaignStatusPaused)
+ }
+ >
+
+
+
+
+
+
+ )}
+
+ {/* Draft with send_at */}
+ {record.status === cs.CampaignStatusDraft && record.send_at && (
+
+ this.handleUpdateStatus(record, cs.CampaignStatusScheduled)
+ }
+ >
+
+
+
+
+
+
+ )}
+
+ {record.status === cs.CampaignStatusDraft && !record.send_at && (
+
+ this.handleUpdateStatus(record, cs.CampaignStatusRunning)
+ }
+ >
+
+
+
+
+
+
+ )}
+
+ {(record.status === cs.CampaignStatusPaused ||
+ record.status === cs.CampaignStatusRunning) && (
+
+ this.handleUpdateStatus(record, cs.CampaignStatusCancelled)
+ }
+ >
+
+
+
+
+
+
+ )}
+
+
+ {
+ this.handlePreview(record)
+ }}
+ >
+
+
+
+
+
+ {
+ let r = {
+ ...record,
+ lists: record.lists.map(i => {
+ return i.id
+ })
+ }
+ this.handleToggleCloneForm(r)
+ }}
+ >
+
+
+
+
+ {(record.status === cs.CampaignStatusDraft ||
+ record.status === cs.CampaignStatusScheduled) && (
+
this.handleDeleteRecord(record)}
+ >
+
+
+
+
+
+
+ )}
+
+ )
+ }
+ }
+ ]
+ }
+
+ progressPercent(record) {
+ return Math.round(
+ (this.getStatsField("sent", record) /
+ this.getStatsField("to_send", record)) *
+ 100,
+ 2
+ )
+ }
+
+ isDone(record) {
+ return (
+ this.getStatsField("status", record) === cs.CampaignStatusFinished ||
+ this.getStatsField("status", record) === cs.CampaignStatusCancelled
+ )
+ }
+
+ // getStatsField returns a stats field value of a given record if it
+ // exists in the stats state, or the value from the record itself.
+ getStatsField = (field, record) => {
+ if (this.state.stats.hasOwnProperty(record.id)) {
+ return this.state.stats[record.id][field]
+ }
+
+ return record[field]
+ }
+
+ renderStats = record => {
+ let color = cs.CampaignStatusColors.hasOwnProperty(record.status)
+ ? cs.CampaignStatusColors[record.status]
+ : ""
+ const startedAt = this.getStatsField("started_at", record)
+ const updatedAt = this.getStatsField("updated_at", record)
+ const sent = this.getStatsField("sent", record)
+ const toSend = this.getStatsField("to_send", record)
+ const isDone = this.isDone(record)
+
+ const r = this.getStatsField("rate", record)
+ const rate = r ? r : 0
+
+ return (
+
+ {!isDone && (
+
+ )}
+
+
+ Sent
+
+
+ {sent >= toSend && {toSend} }
+ {sent < toSend && (
+
+ {sent} / {toSend}
+
+ )}
+
+ {record.status === cs.CampaignStatusRunning && (
+
+ )}
+
+
+
+ {rate > 0 && (
+
+
+ Rate
+
+ {Math.round(rate, 2)} / min
+
+ )}
+
+
+
+ Views
+
+ {record.views}
+
+
+
+ Clicks
+
+ {record.clicks}
+
+
+
+
+ Created
+
+ {dayjs(record.created_at).format(cs.DateFormat)}
+
+
+ {startedAt && (
+
+
+ Started
+
+ {dayjs(startedAt).format(cs.DateFormat)}
+
+ )}
+ {isDone && (
+
+
+ Ended
+
+ {dayjs(updatedAt).format(cs.DateFormat)}
+
+ )}
+ {startedAt && updatedAt && (
+
+
+ Duration
+
+
+ {dayjs(updatedAt).from(dayjs(startedAt), true)}
+
+
+ )}
+
+ )
+ }
+
+ componentDidMount() {
+ this.props.pageTitle("Campaigns")
+ dayjs.extend(relativeTime)
+ this.fetchRecords()
+
+ // Did we land here to start a campaign?
+ let loc = this.props.route.location
+ let state = loc.state
+ if (state && state.hasOwnProperty("campaign")) {
+ this.handleUpdateStatus(state.campaign, state.campaignStatus)
+ delete state.campaign
+ delete state.campaignStatus
+ this.props.route.history.replace({ ...loc, state })
+ }
+ }
+
+ componentWillUnmount() {
+ window.clearInterval(this.state.pollID)
+ }
+
+ fetchRecords = params => {
+ let qParams = {
+ page: this.state.queryParams.page,
+ per_page: this.state.queryParams.per_page
+ }
+
+ // The records are for a specific list.
+ if (this.state.queryParams.listID) {
+ qParams.listID = this.state.queryParams.listID
+ }
+
+ if (params) {
+ qParams = { ...qParams, ...params }
+ }
+
+ this.props
+ .modelRequest(
+ cs.ModelCampaigns,
+ cs.Routes.GetCampaigns,
+ cs.MethodGet,
+ qParams
+ )
+ .then(r => {
+ this.startStatsPoll()
+ })
+ }
+
+ startStatsPoll = () => {
+ window.clearInterval(this.state.pollID)
+ this.setState({ stats: {} })
+
+ // If there's at least one running campaign, start polling.
+ let hasRunning = false
+ this.props.data[cs.ModelCampaigns].forEach(c => {
+ if (c.status === cs.CampaignStatusRunning) {
+ hasRunning = true
+ return
+ }
+ })
+
+ if (!hasRunning) {
+ return
+ }
+
+ // Poll for campaign stats.
+ let pollID = window.setInterval(() => {
+ this.props
+ .request(cs.Routes.GetRunningCampaignStats, cs.MethodGet)
+ .then(r => {
+ // No more running campaigns.
+ if (r.data.data.length === 0) {
+ window.clearInterval(this.state.pollID)
+ this.fetchRecords()
return
- }
+ }
- // Poll for campaign stats.
- let pollID = window.setInterval(() => {
- this.props.request(cs.Routes.GetRunningCampaignStats, cs.MethodGet).then((r) => {
- // No more running campaigns.
- if(r.data.data.length === 0) {
- window.clearInterval(this.state.pollID)
- this.fetchRecords()
- return
- }
+ let stats = {}
+ r.data.data.forEach(s => {
+ stats[s.id] = s
+ })
- let stats = {}
- r.data.data.forEach((s) => {
- stats[s.id] = s
- })
-
- this.setState({ stats: stats })
- }).catch(e => {
- console.log(e.message)
- })
- }, 3000)
-
- this.setState({ pollID: pollID })
- }
-
- handleUpdateStatus = (record, status) => {
- this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaignStatus, cs.MethodPut, { id: record.id, status: status })
- .then(() => {
- notification["success"]({ placement: cs.MsgPosition, message: `Campaign ${status}`, description: `"${record.name}" ${status}` })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
- })
- }
-
- handleDeleteRecord = (record) => {
- this.props.modelRequest(cs.ModelCampaigns, cs.Routes.DeleteCampaign, cs.MethodDelete, { id: record.id })
- .then(() => {
- notification["success"]({ placement: cs.MsgPosition, message: "Campaign deleted", description: `"${record.name}" deleted` })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
- })
- }
-
- handleToggleCloneForm = (record) => {
- this.setState({ cloneModalVisible: !this.state.cloneModalVisible, record: record, cloneName: record.name })
- }
-
- handleCloneCampaign = (record) => {
- this.setState({ modalWaiting: true })
- this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, record).then((resp) => {
- notification["success"]({ placement: cs.MsgPosition,
- message: "Campaign created",
- description: `${record.name} created` })
-
- this.setState({ record: null, modalWaiting: false })
- this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id))
- }).catch(e => {
- notification["error"]({ placement: cs.MsgPosition, message: "Error", description: e.message })
- this.setState({ modalWaiting: false })
+ this.setState({ stats: stats })
})
- }
+ .catch(e => {
+ console.log(e.message)
+ })
+ }, 3000)
- handlePreview = (record) => {
- this.setState({ previewRecord: record })
- }
+ this.setState({ pollID: pollID })
+ }
- render() {
- const pagination = {
- ...this.paginationOptions,
- ...this.state.queryParams
- }
-
- return (
-
-
- Campaigns
-
- New campaign
-
-
-
+ handleUpdateStatus = (record, status) => {
+ this.props
+ .modelRequest(
+ cs.ModelCampaigns,
+ cs.Routes.UpdateCampaignStatus,
+ cs.MethodPut,
+ { id: record.id, status: status }
+ )
+ .then(() => {
+ notification["success"]({
+ placement: cs.MsgPosition,
+ message: `Campaign ${status}`,
+ description: `"${record.name}" ${status}`
+ })
- record.uuid }
- dataSource={ this.props.data[cs.ModelCampaigns] }
- loading={ this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone }
- pagination={ pagination }
- />
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({
+ placement: cs.MsgPosition,
+ message: "Error",
+ description: e.message
+ })
+ })
+ }
- { this.state.previewRecord &&
- {
- this.setState({ previewRecord: null })
- }}
- />
- }
+ handleDeleteRecord = record => {
+ this.props
+ .modelRequest(
+ cs.ModelCampaigns,
+ cs.Routes.DeleteCampaign,
+ cs.MethodDelete,
+ { id: record.id }
+ )
+ .then(() => {
+ notification["success"]({
+ placement: cs.MsgPosition,
+ message: "Campaign deleted",
+ description: `"${record.name}" deleted`
+ })
- { this.state.cloneModalVisible && this.state.record &&
- { this.handleCloneCampaign({ ...this.state.record, name: this.state.cloneName }) }}>
- {
- this.setState({ cloneName: e.target.value })
- }} />
- }
-
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({
+ placement: cs.MsgPosition,
+ message: "Error",
+ description: e.message
+ })
+ })
+ }
+
+ handleToggleCloneForm = record => {
+ this.setState({
+ cloneModalVisible: !this.state.cloneModalVisible,
+ record: record,
+ cloneName: record.name
+ })
+ }
+
+ handleCloneCampaign = record => {
+ this.setState({ modalWaiting: true })
+ this.props
+ .modelRequest(
+ cs.ModelCampaigns,
+ cs.Routes.CreateCampaign,
+ cs.MethodPost,
+ record
+ )
+ .then(resp => {
+ notification["success"]({
+ placement: cs.MsgPosition,
+ message: "Campaign created",
+ description: `${record.name} created`
+ })
+
+ this.setState({ record: null, modalWaiting: false })
+ this.props.route.history.push(
+ cs.Routes.ViewCampaign.replace(":id", resp.data.data.id)
)
+ })
+ .catch(e => {
+ notification["error"]({
+ placement: cs.MsgPosition,
+ message: "Error",
+ description: e.message
+ })
+ this.setState({ modalWaiting: false })
+ })
+ }
+
+ handlePreview = record => {
+ this.setState({ previewRecord: record })
+ }
+
+ render() {
+ const pagination = {
+ ...this.paginationOptions,
+ ...this.state.queryParams
}
+
+ return (
+
+
+
+ Campaigns
+
+
+
+
+ New campaign
+
+
+
+
+
+
+ record.uuid}
+ dataSource={this.props.data[cs.ModelCampaigns]}
+ loading={this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone}
+ pagination={pagination}
+ />
+
+ {this.state.previewRecord && (
+ {
+ this.setState({ previewRecord: null })
+ }}
+ />
+ )}
+
+ {this.state.cloneModalVisible && this.state.record && (
+ {
+ this.handleCloneCampaign({
+ ...this.state.record,
+ name: this.state.cloneName
+ })
+ }}
+ >
+ {
+ this.setState({ cloneName: e.target.value })
+ }}
+ />
+
+ )}
+
+ )
+ }
}
export default Campaigns
diff --git a/frontend/my/src/Dashboard.js b/frontend/my/src/Dashboard.js
index 20c6e68..b406f4a 100644
--- a/frontend/my/src/Dashboard.js
+++ b/frontend/my/src/Dashboard.js
@@ -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 (
-
- Welcome
-
-
- { this.state.stats &&
-
-
-
-
-
-
- { this.orZero(this.state.stats.subscribers.enabled) }
-
-
-
-
- { this.orZero(this.state.stats.subscribers.blacklisted) }
-
-
-
-
- { this.orZero(this.state.stats.orphan_subscribers) }
-
-
-
-
-
-
-
-
- { this.orZero(this.state.stats.lists.public) }
-
-
-
-
- { this.orZero(this.state.stats.lists.private) }
-
-
-
-
-
-
-
-
-
-
-
-
- { this.state.stats.campaign_views.reduce((total, v) => total + v.count, 0) }
- {' '}
- views
-
-
-
-
-
-
-
-
-
-
-
- { this.state.stats.link_clicks.reduce((total, v) => total + v.count, 0) }
- {' '}
- clicks
-
-
-
-
-
-
-
-
-
-
+ orZero(v) {
+ return v ? v : 0
+ }
-
-
- { this.campaignTypes.map((key) =>
-
- { key }
-
-
- { this.state.stats.campaigns.hasOwnProperty(key) ?
- this.state.stats.campaigns[key] : 0 }
-
-
-
- )}
-
-
-
-
- }
-
-
- );
- }
+ render() {
+ return (
+
+ Welcome
+
+
+ {this.state.stats && (
+
+
+
+
+
+
+
+ {this.orZero(this.state.stats.subscribers.enabled)}
+
+
+
+
+
+
+ {this.orZero(
+ this.state.stats.subscribers.blacklisted
+ )}
+
+
+
+
+
+
+ {this.orZero(this.state.stats.orphan_subscribers)}
+
+
+
+
+
+
+
+
+
+
+ {this.orZero(this.state.stats.lists.public)}
+
+
+
+
+
+
+ {this.orZero(this.state.stats.lists.private)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.stats.campaign_views.reduce(
+ (total, v) => total + v.count,
+ 0
+ )}{" "}
+ views
+
+
+
+
+
+
+
+
+
+
+
+ {this.state.stats.link_clicks.reduce(
+ (total, v) => total + v.count,
+ 0
+ )}{" "}
+ clicks
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.campaignTypes.map(key => (
+
+
+ {key}
+
+
+
+ {this.state.stats.campaigns.hasOwnProperty(key)
+ ? this.state.stats.campaigns[key]
+ : 0}
+
+
+
+ ))}
+
+
+
+
+ )}
+
+
+ )
+ }
}
-export default Dashboard;
+export default Dashboard
diff --git a/frontend/my/src/Dashboard.test.js b/frontend/my/src/Dashboard.test.js
deleted file mode 100644
index a754b20..0000000
--- a/frontend/my/src/Dashboard.test.js
+++ /dev/null
@@ -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( , div);
- ReactDOM.unmountComponentAtNode(div);
-});
diff --git a/frontend/my/src/Import.js b/frontend/my/src/Import.js
index 792da4f..f627837 100644
--- a/frontend/my/src/Import.js
+++ b/frontend/my/src/Import.js
@@ -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 (
-
-
- {getFieldDecorator("mode", { rules: [{ required: true }], initialValue: "subscribe" })(
- { this.setState({ mode: e.target.value }) }}>
- Subscribe
- Blacklist
-
- )}
-
- { this.state.mode === "subscribe" &&
-
-
- {getFieldDecorator("lists", { rules: [{ required: true }] })(
-
- {[...this.props.lists].map((v, i) =>
- {v["name"]}
- )}
-
- )}
-
-
- }
- { this.state.mode === "blacklist" &&
-
-
- 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'.
-
-
- }
-
- {getFieldDecorator("delim", {
- initialValue: ","
- })( )}
-
-
-
- {getFieldDecorator("file", {
- valuePropName: "file",
- getValueFromEvent: this.normFile,
- rules: [{ required: true }]
- })(
-
-
-
-
- Click or drag a CSV or ZIP file here
-
- )}
-
-
-
- For existing subscribers, the names and attributes will be overwritten with the values in the CSV.
- Upload
-
-
-
- )
- }
+ return (
+
+
+ {getFieldDecorator("mode", {
+ rules: [{ required: true }],
+ initialValue: "subscribe"
+ })(
+ {
+ this.setState({ mode: e.target.value })
+ }}
+ >
+
+ Subscribe
+
+
+ Blacklist
+
+
+ )}
+
+ {this.state.mode === "subscribe" && (
+
+
+ {getFieldDecorator("lists", { rules: [{ required: true }] })(
+
+ {[...this.props.lists].map((v, i) => (
+
+ {v["name"]}
+
+ ))}
+
+ )}
+
+
+ )}
+ {this.state.mode === "blacklist" && (
+
+
+ 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'.
+
+
+ )}
+
+ {getFieldDecorator("delim", {
+ initialValue: ","
+ })( )}
+
+
+
+ {getFieldDecorator("file", {
+ valuePropName: "file",
+ getValueFromEvent: this.normFile,
+ rules: [{ required: true }]
+ })(
+
+
+
+
+
+ Click or drag a CSV or ZIP file here
+
+
+ )}
+
+
+
+
+ For existing subscribers, the names and attributes will be
+ overwritten with the values in the CSV.
+
+
+ Upload
+
+
+
+
+ )
+ }
}
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 (
+
+ Importing — {this.props.importState.name}
+ {this.props.importState.status === StatusImporting && (
+
+ Import is in progress. It is safe to navigate away from this page.
+
+ )}
- return(
-
- Importing — { this.props.importState.name }
- { this.props.importState.status === StatusImporting &&
- Import is in progress. It is safe to navigate away from this page.
- }
+ {this.props.importState.status !== StatusImporting && (
+ Import has finished.
+ )}
- { this.props.importState.status !== StatusImporting &&
- Import has finished.
- }
+
+
+
+
-
-
-
-
+
+
{this.props.importState.imported} records
+
-
-
{ this.props.importState.imported } records
-
-
- { this.props.importState.status === StatusImporting &&
-
this.stopImport()}>
-
- Stop import
-
- }
- { this.props.importState.status === StatusStopping &&
-
- }
- { this.props.importState.status !== StatusImporting &&
- this.props.importState.status !== StatusStopping &&
-
- { this.props.importState.status !== StatusFinished &&
-
- { this.props.importState.status }
-
-
- }
-
-
-
this.stopImport()}>Done
-
- }
-
+ {this.props.importState.status === StatusImporting && (
+
this.stopImport()}
+ >
+
+
+
+ Stop import
+
+ )}
+ {this.props.importState.status === StatusStopping && (
+
+ )}
+ {this.props.importState.status !== StatusImporting &&
+ this.props.importState.status !== StatusStopping && (
+
+ {this.props.importState.status !== StatusFinished && (
+
+ {this.props.importState.status}
+
+ )}
-
-
Import log
-
-
-
-
-
-
-
- )
- }
+
+
this.stopImport()}>
+ Done
+
+
+ )}
+
+
+
+
+
Import log
+
+
+
+
+
+
+
+ )
+ }
}
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 (
+
+ )
+ } else if (this.state.importState.status !== StatusNone) {
+ // There's an import state
+ return (
+
+ )
}
- componentDidMount() {
- this.props.pageTitle("Import subscribers")
- this.fetchimportState()
- }
- render() {
- if( this.state.importState.status === "" ) {
- // Fetching the status.
- return (
-
- )
- } else if ( this.state.importState.status !== StatusNone ) {
- // There's an import state
- return
- }
+ return (
+
+
+
+ Import subscribers
+
+
+
- return (
-
-
- Import subscribers
-
-
-
+
-
-
+
+
+
Instructions
+
+ 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. attributes
(optional)
+ should be a valid JSON string with double escaped quotes.
+
-
-
-
Instructions
-
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.
- {" "}
- attributes
(optional) should be a valid JSON string with double escaped quotes.
-
+
+
+ email,
+ name,
+ status,
+ attributes
+
+
-
-
- email,
- name,
- status,
- attributes
-
-
-
-
Example raw CSV
-
-
- email,
- name,
- status,
- attributes
-
-
- user1@mail.com,
- "User One",
- enabled,
- { '"{""age"": 32, ""city"": ""Bangalore""}"' }
-
-
- user2@mail.com,
- "User Two",
- blacklisted,
- { '"{""age"": 25, ""occupation"": ""Time Traveller""}"' }
-
-
-
-
- )
- }
+
Example raw CSV
+
+
+ email,
+ name,
+ status,
+ attributes
+
+
+ user1@mail.com,
+ "User One",
+ enabled,
+ {'"{""age"": 32, ""city"": ""Bangalore""}"'}
+
+
+ user2@mail.com,
+ "User Two",
+ blacklisted,
+
+ {'"{""age"": 25, ""occupation"": ""Time Traveller""}"'}
+
+
+
+
+
+ )
+ }
}
export default Import
diff --git a/frontend/my/src/Layout.js b/frontend/my/src/Layout.js
index 66ae2c4..6a23c28 100644
--- a/frontend/my/src/Layout.js
+++ b/frontend/my/src/Layout.js
@@ -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 (
-
-
-
-
-
+ render() {
+ return (
+
+
+
+
+
+
+
-
+
+
+
+
+ Dashboard
+
+
+
+
+
+ Lists
+
+
+
+
+ Subscribers
+
+ }
+ >
+
+
+ All subscribers
+
+
+
+
+ Import
+
+
+
- Dashboard
- Lists
- Subscribers }>
- All subscribers
- Import
-
+
+
+ Campaigns
+
+ }
+ >
+
+
+ All campaigns
+
+
+
+
+ Create new
+
+
+
+
+ Media
+
+
+
+
+ Templates
+
+
+
- Campaigns }>
- All campaigns
- Create new
- Media
- Templates
-
+
+
+ Settings
+
+ }
+ >
+
+ Users
+
+
+
+ Settings
+
+
+
+
+ Logout
+
+
+
- Settings }>
- Users
- Settings
-
- Logout
-
-
-
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
-
-
-
- listmonk
- {" "}
- © 2019 { year != 2019 ? " - " + year : "" }
-
-
-
-
- )
- }
+
+
+
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+
+
+
+ listmonk
+ {" "}
+ © 2019 {year !== 2019 ? " - " + year : ""}
+
+
+
+
+ )
+ }
}
export default Base
diff --git a/frontend/my/src/Lists.js b/frontend/my/src/Lists.js
index 6dcef4f..b48dcf0 100644
--- a/frontend/my/src/Lists.js
+++ b/frontend/my/src/Lists.js
@@ -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 (
+
+
+ {record.type}
+ {" "}
+ {record.name}
+
+
+ ID {record.id} / UUID {record.uuid}
+
+
+ )
+ }
- 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 (
-
- { record.type }
- {" "}
- { record.name }
-
- ID { record.id } / UUID { record.uuid }
-
- )
+ if (formType === null) {
+ return null
}
- render() {
- const { formType, record, onClose } = this.props
- const { getFieldDecorator } = this.props.form
+ return (
+
+
- const formItemLayout = {
- labelCol: { xs: { span: 16 }, sm: { span: 4 } },
- wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
- }
-
- if (formType === null) {
- return null
- }
-
- return (
-
-
-
-
-
-
- {getFieldDecorator("name", {
- initialValue: record.name,
- rules: [{ required: true }]
- })( )}
-
-
- {getFieldDecorator("type", { initialValue: record.type ? record.type : "private", rules: [{ required: true }] })(
-
- Private
- Public
-
- )}
-
-
- {getFieldDecorator("tags", { initialValue: record.tags })(
-
- )}
-
-
-
-
- )
- }
+
+
+ {getFieldDecorator("name", {
+ initialValue: record.name,
+ rules: [{ required: true }]
+ })( )}
+
+
+ {getFieldDecorator("type", {
+ initialValue: record.type ? record.type : "private",
+ rules: [{ required: true }]
+ })(
+
+ Private
+ Public
+
+ )}
+
+
+ {getFieldDecorator("tags", { initialValue: record.tags })(
+
+ )}
+
+
+
+
+ )
+ }
}
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(
- { text }
- )
+ this.columns = [
+ {
+ title: "Name",
+ dataIndex: "name",
+ sorter: true,
+ width: "40%",
+ render: (text, record) => {
+ const out = []
+ out.push(
+
+ {text}
+
+ )
- if(record.tags.length > 0) {
- for (let i = 0; i < record.tags.length; i++) {
- out.push({ record.tags[i] } );
- }
- }
+ if (record.tags.length > 0) {
+ for (let i = 0; i < record.tags.length; i++) {
+ out.push({record.tags[i]} )
+ }
+ }
- return out
- }
- },
- {
- title: "Type",
- dataIndex: "type",
- width: "10%",
- render: (type, _) => {
- let color = type === "private" ? "orange" : "green"
- return {type}
- }
- },
- {
- title: "Subscribers",
- dataIndex: "subscriber_count",
- width: "15%",
- align: "center",
- render: (text, record) => {
- return(
- { text }
- )
- }
- },
- {
- 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 (
-
- )
- }
- }]
- }
+ return out
+ }
+ },
+ {
+ title: "Type",
+ dataIndex: "type",
+ width: "10%",
+ render: (type, _) => {
+ let color = type === "private" ? "orange" : "green"
+ return {type}
+ }
+ },
+ {
+ title: "Subscribers",
+ dataIndex: "subscriber_count",
+ width: "15%",
+ align: "center",
+ render: (text, record) => {
+ return (
+
+ {text}
+
+ )
+ }
+ },
+ {
+ 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 (
+
+ )
+ }
+ }
+ ]
+ }
- 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 (
+
+
+
+ Lists ({this.props.data[cs.ModelLists].length})
+
+
+
+ Create list
+
+
+
+
- handleShowEditForm = (record) => {
- this.setState({ formType: cs.FormEdit, record: record })
- }
+ record.uuid}
+ dataSource={this.props.data[cs.ModelLists]}
+ loading={this.props.reqStates[cs.ModelLists] !== cs.StateDone}
+ pagination={false}
+ />
- render() {
- return (
-
-
- Lists ({this.props.data[cs.ModelLists].length})
-
- Create list
-
-
-
-
- record.uuid }
- dataSource={ this.props.data[cs.ModelLists] }
- loading={ this.props.reqStates[cs.ModelLists] !== cs.StateDone }
- pagination={ false }
- />
-
-
-
- )
- }
+
+
+ )
+ }
}
export default Lists
diff --git a/frontend/my/src/Media.js b/frontend/my/src/Media.js
index 089ea67..c42d7ad 100644
--- a/frontend/my/src/Media.js
+++ b/frontend/my/src/Media.js
@@ -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 (
+
+
+
+ {getFieldDecorator("file", {
+ valuePropName: "file",
+ getValueFromEvent: this.normFile,
+ rules: [{ required: true }]
+ })(
+
+
+
+
+ Click or drag file here
+
+ )}
+
+
+
- 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 (
-
-
-
- {getFieldDecorator("file", {
- valuePropName: "file",
- getValueFromEvent: this.normFile,
- rules: [{ required: true }]
- })(
-
-
-
-
- Click or drag file here
-
- )}
-
-
-
-
-
- {this.props.media && this.props.media.map((record, i) =>
-
- )}
-
-
- )
- }
+
+ {this.props.media &&
+ this.props.media.map((record, i) => (
+
+ ))}
+
+
+ )
+ }
}
const TheForm = Form.create()(TheFormDef)
class Media extends React.PureComponent {
- render() {
- return (
-
-
- Images
-
-
-
+ render() {
+ return (
+
- )
- }
+
+
+ )
+ }
}
export default Media
diff --git a/frontend/my/src/ModalPreview.js b/frontend/my/src/ModalPreview.js
index c92a0f3..d67b386 100644
--- a/frontend/my/src/ModalPreview.js
+++ b/frontend/my/src/ModalPreview.js
@@ -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 (
-
-
+
+ )
+ }
}
export default ModalPreview
diff --git a/frontend/my/src/Subscriber.js b/frontend/my/src/Subscriber.js
index f025896..a8ec567 100644
--- a/frontend/my/src/Subscriber.js
+++ b/frontend/my/src/Subscriber.js
@@ -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 (
-
-
- {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" }] })(
-
- Enabled
- Blacklisted
-
- )}
-
-
- {getFieldDecorator("lists", { initialValue: subListIDs })(
-
- {[...this.props.lists].map((v, i) =>
-
- { v.name }
- { subStatuses[v.id] &&
- { subStatuses[v.id] }
- }
-
-
- )}
-
- )}
-
-
-
- {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.FormCreate ? "Add" : "Save" }
-
- {" "}
- { this.props.formType === cs.FormEdit &&
- {
- this.handleDeleteRecord(record)
- }}>
- Delete
-
- }
-
- }
-
-
+ 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" }]
+ })(
+
+ Enabled
+ Blacklisted
+
+ )}
+
+
+ {getFieldDecorator("lists", { initialValue: subListIDs })(
+
+ {[...this.props.lists].map((v, i) => (
+
+
+ {v.name}
+ {subStatuses[v.id] && (
+
+ {" "}
+ {subStatuses[v.id]}
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ {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.FormCreate ? "Add" : "Save"}
+ {" "}
+ {this.props.formType === cs.FormEdit && (
+ {
+ this.handleDeleteRecord(record)
+ }}
+ >
+ Delete
+
+ )}
+
+ )}
+
+
+ )
+ }
}
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 (
-
-
+ render() {
+ return (
+
- )
- }
+ )}
+
+
+
+
+
+
+ {
+ 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 b5415c9..adb12b8 100644
--- a/frontend/my/src/Subscribers.js
+++ b/frontend/my/src/Subscribers.js
@@ -1,608 +1,839 @@
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, Radio } from "antd"
+import {
+ Row,
+ Col,
+ Modal,
+ Form,
+ Input,
+ Select,
+ Button,
+ Table,
+ Icon,
+ Tooltip,
+ Tag,
+ Popconfirm,
+ notification,
+ Radio
+} from "antd"
import Utils from "./utils"
import Subscriber from "./Subscriber"
import * as cs from "./constants"
-
const tagColors = {
- "enabled": "green",
- "blacklisted": "red"
+ enabled: "green",
+ blacklisted: "red"
}
class ListsFormDef extends React.PureComponent {
- state = {
- modalWaiting: false
+ state = {
+ modalWaiting: false
+ }
+
+ // 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
}
- // Handle create / edit form submission.
- handleSubmit = (e) => {
- e.preventDefault()
+ if (this.props.allRowsSelected) {
+ values["list_ids"] = this.props.listIDs
+ values["query"] = this.props.query
+ } else {
+ values["ids"] = this.props.selectedRows.map(r => r.id)
+ }
- var err = null, values = {}
- this.props.form.validateFields((e, v) => {
- err = e
- values = v
+ this.setState({ modalWaiting: true })
+ this.props
+ .request(
+ !this.props.allRowsSelected
+ ? cs.Routes.AddSubscribersToLists
+ : cs.Routes.AddSubscribersToListsByQuery,
+ cs.MethodPut,
+ values
+ )
+ .then(() => {
+ notification["success"]({
+ message: "Lists changed",
+ description: `Lists changed for selected subscribers`
})
- if(err) {
- return
- }
+ this.props.clearSelectedRows()
+ this.props.fetchRecords()
+ this.setState({ modalWaiting: false })
+ this.props.onClose()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ this.setState({ modalWaiting: false })
+ })
+ }
- if(this.props.allRowsSelected) {
- values["list_ids"] = this.props.listIDs
- values["query"] = this.props.query
- } else {
- values["ids"] = this.props.selectedRows.map(r => r.id)
- }
-
- this.setState({ modalWaiting: true })
- this.props.request(!this.props.allRowsSelected ? cs.Routes.AddSubscribersToLists : cs.Routes.AddSubscribersToListsByQuery,
- cs.MethodPut, values).then(() => {
- notification["success"]({ message: "Lists changed",
- description: `Lists changed for selected subscribers` })
- this.props.clearSelectedRows()
- this.props.fetchRecords()
- this.setState({ modalWaiting: false })
- this.props.onClose()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- this.setState({ modalWaiting: false })
- })
+ render() {
+ const { getFieldDecorator } = this.props.form
+ const formItemLayout = {
+ labelCol: { xs: { span: 16 }, sm: { span: 4 } },
+ wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
}
- render() {
- const { getFieldDecorator } = this.props.form
- const formItemLayout = {
- labelCol: { xs: { span: 16 }, sm: { span: 4 } },
- wrapperCol: { xs: { span: 16 }, sm: { span: 18 } }
- }
-
- return (
-
-
- {getFieldDecorator("action", {
- initialValue: "add",
- rules: [{ required: true }]
- })(
-
- Add
- Remove
- Mark as unsubscribed
-
- )}
-
-
- {getFieldDecorator("target_list_ids", { rules:[{ required: true }] })(
-
- {[...this.props.lists].map((v, i) =>
-
- { v.name }
-
- )}
-
- )}
-
-
-
- )
- }
+ return (
+
+
+ {getFieldDecorator("action", {
+ initialValue: "add",
+ rules: [{ required: true }]
+ })(
+
+ Add
+ Remove
+ Mark as unsubscribed
+
+ )}
+
+
+ {getFieldDecorator("target_list_ids", {
+ rules: [{ required: true }]
+ })(
+
+ {[...this.props.lists].map((v, i) => (
+
+ {v.name}
+
+ ))}
+
+ )}
+
+
+
+ )
+ }
}
const ListsForm = Form.create()(ListsFormDef)
class Subscribers extends React.PureComponent {
- defaultPerPage = 20
+ defaultPerPage = 20
- state = {
- formType: null,
- listsFormVisible: false,
- modalForm: null,
- record: {},
- queryParams: {
- page: 1,
- total: 0,
- perPage: this.defaultPerPage,
- listID: this.props.route.match.params.listID ? parseInt(this.props.route.match.params.listID, 10) : 0,
- list: null,
- query: null,
- targetLists: []
- },
- listModalVisible: false,
- allRowsSelected: false,
- selectedRows: []
+ state = {
+ formType: null,
+ listsFormVisible: false,
+ modalForm: null,
+ record: {},
+ queryParams: {
+ page: 1,
+ total: 0,
+ perPage: this.defaultPerPage,
+ listID: this.props.route.match.params.listID
+ ? parseInt(this.props.route.match.params.listID, 10)
+ : 0,
+ list: null,
+ query: null,
+ targetLists: []
+ },
+ listModalVisible: false,
+ allRowsSelected: false,
+ selectedRows: []
+ }
+
+ // Pagination config.
+ paginationOptions = {
+ hideOnSinglePage: true,
+ showSizeChanger: true,
+ showQuickJumper: true,
+ defaultPageSize: this.defaultPerPage,
+ pageSizeOptions: ["20", "50", "70", "100"],
+ position: "both",
+ showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`,
+ onChange: (page, perPage) => {
+ this.fetchRecords({ page: page, per_page: perPage })
+ },
+ onShowSizeChange: (page, perPage) => {
+ this.fetchRecords({ page: page, per_page: perPage })
}
+ }
- // Pagination config.
- paginationOptions = {
- hideOnSinglePage: true,
- showSizeChanger: true,
- showQuickJumper: true,
- defaultPageSize: this.defaultPerPage,
- pageSizeOptions: ["20", "50", "70", "100"],
- position: "both",
- showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`,
- onChange: (page, perPage) => {
- this.fetchRecords({ page: page, per_page: perPage })
- },
- onShowSizeChange: (page, perPage) => {
- this.fetchRecords({ page: page, per_page: perPage })
- }
- }
+ constructor(props) {
+ super(props)
- constructor(props) {
- super(props)
+ // Table layout.
+ this.columns = [
+ {
+ title: "E-mail",
+ dataIndex: "email",
+ sorter: true,
+ width: "25%",
+ render: (text, record) => {
+ const out = []
+ out.push(
+
+ {
+ // Open the individual subscriber page on ctrl+click
+ // and the modal otherwise.
+ if (!e.ctrlKey) {
+ this.handleShowEditForm(record)
+ e.preventDefault()
+ }
+ }}
+ >
+ {text}
+
+
+ )
- // Table layout.
- this.columns = [{
- title: "E-mail",
- dataIndex: "email",
- sorter: true,
- width: "25%",
- render: (text, record) => {
- const out = [];
- out.push(
-
- {
- // Open the individual subscriber page on ctrl+click
- // and the modal otherwise.
- if(!e.ctrlKey) {
- this.handleShowEditForm(record)
- e.preventDefault()
- }
- }}>{ text }
-
- )
-
- if(record.lists.length > 0) {
- for (let i = 0; i < record.lists.length; i++) {
- out.push(
- { record.lists[i].name }
- { record.lists[i].subscription_status }
- )
+ if (record.lists.length > 0) {
+ for (let i = 0; i < record.lists.length; i++) {
+ out.push(
+
+
+ {record.lists[i].name}
+
+
+ {" "}
+ {record.lists[i].subscription_status}
+
+
+ )
+ }
+ }
+
+ return out
+ }
+ },
+ {
+ title: "Name",
+ dataIndex: "name",
+ sorter: true,
+ width: "15%",
+ render: (text, record) => {
+ return (
+ {
+ // Open the individual subscriber page on ctrl+click
+ // and the modal otherwise.
+ if (!e.ctrlKey) {
+ this.handleShowEditForm(record)
+ e.preventDefault()
}
-
- return out
- }
- },
- {
- title: "Name",
- dataIndex: "name",
- sorter: true,
- width: "15%",
- render: (text, record) => {
- return (
- {
- // Open the individual subscriber page on ctrl+click
- // and the modal otherwise.
- if(!e.ctrlKey) {
- this.handleShowEditForm(record)
- e.preventDefault()
- }
- }}>{ text }
- )
- }
- },
- {
- title: "Status",
- dataIndex: "status",
- width: "5%",
- render: (status, _) => {
- return { status }
- }
- },
- {
- title: "Lists",
- dataIndex: "lists",
- width: "10%",
- align: "center",
- render: (lists, _) => {
- return { lists.reduce((def, item) => def + (item.subscription_status !== cs.SubscriptionStatusUnsubscribed ? 1 : 0), 0) }
- }
- },
- {
- title: "Created",
- width: "10%",
- dataIndex: "created_at",
- render: (date, _) => {
- return Utils.DateString(date)
- }
- },
- {
- title: "Updated",
- width: "10%",
- dataIndex: "updated_at",
- render: (date, _) => {
- return Utils.DateString(date)
- }
- },
- {
- title: "",
- dataIndex: "actions",
- width: "10%",
- render: (text, record) => {
- return (
-
- )
- }
+ }}
+ >
+ {text}
+
+ )
}
- ]
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ width: "5%",
+ render: (status, _) => {
+ return (
+
+ {status}
+
+ )
+ }
+ },
+ {
+ title: "Lists",
+ dataIndex: "lists",
+ width: "10%",
+ align: "center",
+ render: (lists, _) => {
+ return (
+
+ {lists.reduce(
+ (def, item) =>
+ def +
+ (item.subscription_status !==
+ cs.SubscriptionStatusUnsubscribed
+ ? 1
+ : 0),
+ 0
+ )}
+
+ )
+ }
+ },
+ {
+ title: "Created",
+ width: "10%",
+ dataIndex: "created_at",
+ render: (date, _) => {
+ return Utils.DateString(date)
+ }
+ },
+ {
+ title: "Updated",
+ width: "10%",
+ dataIndex: "updated_at",
+ render: (date, _) => {
+ return Utils.DateString(date)
+ }
+ },
+ {
+ title: "",
+ dataIndex: "actions",
+ width: "10%",
+ render: (text, record) => {
+ return (
+
+ )
+ }
+ }
+ ]
+ }
+
+ componentDidMount() {
+ // Load lists on boot.
+ this.props
+ .modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet)
+ .then(() => {
+ // If this is an individual list's view, pick up that list.
+ if (this.state.queryParams.listID) {
+ this.props.data[cs.ModelLists].forEach(l => {
+ if (l.id === this.state.queryParams.listID) {
+ this.setState({
+ queryParams: { ...this.state.queryParams, list: l }
+ })
+ return false
+ }
+ })
+ }
+ })
+
+ this.fetchRecords()
+ }
+
+ fetchRecords = params => {
+ let qParams = {
+ page: this.state.queryParams.page,
+ per_page: this.state.queryParams.per_page,
+ list_id: this.state.queryParams.listID,
+ query: this.state.queryParams.query
}
- componentDidMount() {
- // Load lists on boot.
- this.props.modelRequest(cs.ModelLists, cs.Routes.GetLists, cs.MethodGet).then(() => {
- // If this is an individual list's view, pick up that list.
- if(this.state.queryParams.listID) {
- this.props.data[cs.ModelLists].forEach((l) => {
- if(l.id === this.state.queryParams.listID) {
- this.setState({ queryParams: { ...this.state.queryParams, list: l }})
- return false
- }
- })
- }
+ // The records are for a specific list.
+ if (this.state.queryParams.listID) {
+ qParams.list_id = this.state.queryParams.listID
+ }
+
+ if (params) {
+ qParams = { ...qParams, ...params }
+ }
+
+ this.props
+ .modelRequest(
+ cs.ModelSubscribers,
+ cs.Routes.GetSubscribers,
+ cs.MethodGet,
+ qParams
+ )
+ .then(() => {
+ this.setState({
+ queryParams: {
+ ...this.state.queryParams,
+ total: this.props.data[cs.ModelSubscribers].total,
+ perPage: this.props.data[cs.ModelSubscribers].per_page,
+ page: this.props.data[cs.ModelSubscribers].page,
+ query: this.props.data[cs.ModelSubscribers].query
+ }
+ })
+ })
+ }
+
+ 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`
})
+ // Reload the table.
this.fetchRecords()
- }
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
- fetchRecords = (params) => {
- let qParams = {
- page: this.state.queryParams.page,
- per_page: this.state.queryParams.per_page,
- list_id: this.state.queryParams.listID,
- query: this.state.queryParams.query
- }
-
- // The records are for a specific list.
- if(this.state.queryParams.listID) {
- qParams.list_id = this.state.queryParams.listID
- }
-
- if(params) {
- qParams = { ...qParams, ...params }
- }
-
- this.props.modelRequest(cs.ModelSubscribers, cs.Routes.GetSubscribers, cs.MethodGet, qParams).then(() => {
- this.setState({ queryParams: {
- ...this.state.queryParams,
- total: this.props.data[cs.ModelSubscribers].total,
- perPage: this.props.data[cs.ModelSubscribers].per_page,
- page: this.props.data[cs.ModelSubscribers].page,
- query: this.props.data[cs.ModelSubscribers].query,
- }})
+ handleDeleteRecords = records => {
+ this.props
+ .modelRequest(
+ cs.ModelSubscribers,
+ cs.Routes.DeleteSubscribers,
+ cs.MethodDelete,
+ { id: records.map(r => r.id) }
+ )
+ .then(() => {
+ notification["success"]({
+ message: "Subscriber(s) deleted",
+ description: "Selected subscribers deleted"
})
- }
- 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` })
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- })
- }
-
- handleDeleteRecords = (records) => {
- this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscribers, cs.MethodDelete, { id: records.map(r => r.id) })
- .then(() => {
- notification["success"]({ message: "Subscriber(s) deleted", description: "Selected subscribers deleted" })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- })
- }
-
- handleBlacklistSubscribers = (records) => {
- this.props.request(cs.Routes.BlacklistSubscribers, cs.MethodPut, { ids: records.map(r => r.id) })
- .then(() => {
- notification["success"]({ message: "Subscriber(s) blacklisted", description: "Selected subscribers blacklisted" })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- })
- }
-
- // Arbitrary query based calls.
- handleDeleteRecordsByQuery = (listIDs, query) => {
- this.props.modelRequest(cs.ModelSubscribers, cs.Routes.DeleteSubscribersByQuery, cs.MethodPost,
- { list_ids: listIDs, query: query })
- .then(() => {
- notification["success"]({ message: "Subscriber(s) deleted", description: "Selected subscribers have been deleted" })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- })
- }
-
- handleBlacklistSubscribersByQuery = (listIDs, query) => {
- this.props.request(cs.Routes.BlacklistSubscribersByQuery, cs.MethodPut,
- { list_ids: listIDs, query: query })
- .then(() => {
- notification["success"]({ message: "Subscriber(s) blacklisted", description: "Selected subscribers have been blacklisted" })
-
- // Reload the table.
- this.fetchRecords()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
- })
- }
-
- handleQuerySubscribersIntoLists = (query, sourceList, targetLists) => {
- let params = {
- query: query,
- source_list: sourceList,
- target_lists: targetLists
- }
-
- this.props.request(cs.Routes.QuerySubscribersIntoLists, cs.MethodPost, params).then((res) => {
- notification["success"]({ message: "Subscriber(s) added", description: `${ res.data.data.count } added` })
- this.handleToggleListModal()
- }).catch(e => {
- notification["error"]({ message: "Error", description: e.message })
+ handleBlacklistSubscribers = records => {
+ this.props
+ .request(cs.Routes.BlacklistSubscribers, cs.MethodPut, {
+ ids: records.map(r => r.id)
+ })
+ .then(() => {
+ notification["success"]({
+ message: "Subscriber(s) blacklisted",
+ description: "Selected subscribers blacklisted"
})
+
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
+
+ // Arbitrary query based calls.
+ handleDeleteRecordsByQuery = (listIDs, query) => {
+ this.props
+ .modelRequest(
+ cs.ModelSubscribers,
+ cs.Routes.DeleteSubscribersByQuery,
+ cs.MethodPost,
+ { list_ids: listIDs, query: query }
+ )
+ .then(() => {
+ notification["success"]({
+ message: "Subscriber(s) deleted",
+ description: "Selected subscribers have been deleted"
+ })
+
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
+
+ handleBlacklistSubscribersByQuery = (listIDs, query) => {
+ this.props
+ .request(cs.Routes.BlacklistSubscribersByQuery, cs.MethodPut, {
+ list_ids: listIDs,
+ query: query
+ })
+ .then(() => {
+ notification["success"]({
+ message: "Subscriber(s) blacklisted",
+ description: "Selected subscribers have been blacklisted"
+ })
+
+ // Reload the table.
+ this.fetchRecords()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
+
+ handleQuerySubscribersIntoLists = (query, sourceList, targetLists) => {
+ let params = {
+ query: query,
+ source_list: sourceList,
+ target_lists: targetLists
}
- handleHideForm = () => {
- this.setState({ formType: null })
+ this.props
+ .request(cs.Routes.QuerySubscribersIntoLists, cs.MethodPost, params)
+ .then(res => {
+ notification["success"]({
+ message: "Subscriber(s) added",
+ description: `${res.data.data.count} added`
+ })
+ this.handleToggleListModal()
+ })
+ .catch(e => {
+ notification["error"]({ message: "Error", description: e.message })
+ })
+ }
+
+ handleHideForm = () => {
+ this.setState({ formType: null })
+ }
+
+ handleShowCreateForm = () => {
+ this.setState({ formType: cs.FormCreate, attribs: [], record: {} })
+ }
+
+ handleShowEditForm = record => {
+ this.setState({ formType: cs.FormEdit, record: record })
+ }
+
+ handleToggleListsForm = () => {
+ this.setState({ listsFormVisible: !this.state.listsFormVisible })
+ }
+
+ handleSearch = q => {
+ q = q.trim().toLowerCase()
+ if (q === "") {
+ this.fetchRecords({ query: null })
+ return
}
- handleShowCreateForm = () => {
- this.setState({ formType: cs.FormCreate, attribs: [], record: {} })
+ q = q.replace(/'/g, "''")
+ const query = `(name ~* '${q}' OR email ~* '${q}')`
+ this.fetchRecords({ query: query })
+ }
+
+ handleSelectRow = (_, records) => {
+ this.setState({ allRowsSelected: false, selectedRows: records })
+ }
+
+ handleSelectAllRows = () => {
+ this.setState({
+ allRowsSelected: true,
+ selectedRows: this.props.data[cs.ModelSubscribers].results
+ })
+ }
+
+ clearSelectedRows = (_, records) => {
+ this.setState({ allRowsSelected: false, selectedRows: [] })
+ }
+
+ handleToggleQueryForm = () => {
+ this.setState({ queryFormVisible: !this.state.queryFormVisible })
+ }
+
+ handleToggleListModal = () => {
+ this.setState({ listModalVisible: !this.state.listModalVisible })
+ }
+
+ render() {
+ const pagination = {
+ ...this.paginationOptions,
+ ...this.state.queryParams
}
- handleShowEditForm = (record) => {
- this.setState({ formType: cs.FormEdit, record: record })
+ if (this.state.queryParams.list) {
+ this.props.pageTitle(this.state.queryParams.list.name + " / Subscribers")
+ } else {
+ this.props.pageTitle("Subscribers")
}
- handleToggleListsForm = () => {
- this.setState({ listsFormVisible: !this.state.listsFormVisible })
- }
+ return (
+
+
+
+
+
+ Subscribers
+ {this.props.data[cs.ModelSubscribers].total > 0 && (
+ ({this.props.data[cs.ModelSubscribers].total})
+ )}
+ {this.state.queryParams.list && (
+ » {this.state.queryParams.list.name}
+ )}
+
+
+
+
+ Add subscriber
+
+
+
+
- handleSearch = (q) => {
- q = q.trim().toLowerCase()
- if(q === "") {
- this.fetchRecords({ query: null })
- return
- }
-
- q = q.replace(/'/g, "''")
- const query = `(name ~* '${q}' OR email ~* '${q}')`
- this.fetchRecords({ query: query })
- }
-
- handleSelectRow = (_, records) => {
- this.setState({ allRowsSelected: false, selectedRows: records })
- }
-
- handleSelectAllRows = () => {
- this.setState({ allRowsSelected: true,
- selectedRows: this.props.data[cs.ModelSubscribers].results })
- }
-
- clearSelectedRows = (_, records) => {
- this.setState({ allRowsSelected: false, selectedRows: [] })
- }
-
- handleToggleQueryForm = () => {
- this.setState({ queryFormVisible: !this.state.queryFormVisible })
- }
-
- handleToggleListModal = () => {
- this.setState({ listModalVisible: !this.state.listModalVisible })
- }
-
- render() {
- const pagination = {
- ...this.paginationOptions,
- ...this.state.queryParams
- }
-
- if(this.state.queryParams.list) {
- this.props.pageTitle(this.state.queryParams.list.name + " / Subscribers")
- } else {
- this.props.pageTitle("Subscribers")
- }
-
- return (
-
-
-
-
-
- Subscribers
- { this.props.data[cs.ModelSubscribers].total > 0 &&
- ({ this.props.data[cs.ModelSubscribers].total }) }
- { this.state.queryParams.list &&
- » { this.state.queryParams.list.name } }
-
-
-
- Add subscriber
-
-
-
-
-
-
-
-
-
- Search subscribers
-
- {" "}
-
-
-
-
- Advanced
-
-
- { this.state.queryFormVisible &&
-
-
- Advanced query
- {
- this.setState({ queryParams: { ...this.state.queryParams, query: e.target.value } })
- }}
- value={ this.state.queryParams.query }
- autosize={{ minRows: 2, maxRows: 10 }} />
-
- Write a partial SQL expression to query the subscribers based on their primary information or attributes. Learn more.
-
-
-
- { this.fetchRecords() } }>Query
- {" "}
- { this.fetchRecords({ query: null }) } }>Reset
-
-
- }
-
-
- { this.state.selectedRows.length > 0 &&
-
-
- { this.state.allRowsSelected ? this.state.queryParams.total : this.state.selectedRows.length }
- {" "} subscriber(s) selected
- { !this.state.allRowsSelected && this.state.queryParams.total > this.state.queryParams.perPage &&
- —
- Select all { this.state.queryParams.total }?
-
- }
-
-
-
- Manage lists
-
- Send campaign
- {
- if(this.state.allRowsSelected) {
- this.handleDeleteRecordsByQuery(this.state.queryParams.listID ? [this.state.queryParams.listID] : [], this.state.queryParams.query)
- this.clearSelectedRows()
- } else {
- this.handleDeleteRecords(this.state.selectedRows)
- this.clearSelectedRows()
- }
- }}>
- Delete
-
- {
- if(this.state.allRowsSelected) {
- this.handleBlacklistSubscribersByQuery(this.state.queryParams.listID ? [this.state.queryParams.listID] : [], this.state.queryParams.query)
- this.clearSelectedRows()
- } else {
- this.handleBlacklistSubscribers(this.state.selectedRows)
- this.clearSelectedRows()
- }
- }}>
- Blacklist
-
-
-
- }
-
-
+
+
+
+
+
+ Search subscribers
+ {" "}
+
+
+
+
+
+ Advanced
+
+
+
+ {this.state.queryFormVisible && (
+
+
+ Advanced query
+ {
+ this.setState({
+ queryParams: {
+ ...this.state.queryParams,
+ query: e.target.value
+ }
+ })
+ }}
+ value={this.state.queryParams.query}
+ autosize={{ minRows: 2, maxRows: 10 }}
+ />
+
+ Write a partial SQL expression to query the subscribers
+ based on their primary information or attributes. Learn
+ more.
+
+
+
+ {
+ this.fetchRecords()
+ }}
+ >
+ Query
+ {" "}
+ {
+ this.fetchRecords({ query: null })
+ }}
+ >
+ Reset
+
+
+ )}
+
+
+ {this.state.selectedRows.length > 0 && (
+
+
+
+ {this.state.allRowsSelected
+ ? this.state.queryParams.total
+ : this.state.selectedRows.length}
+ {" "}
+ subscriber(s) selected
+ {!this.state.allRowsSelected &&
+ this.state.queryParams.total >
+ this.state.queryParams.perPage && (
+
+ {" "}
+ —{" "}
+
+ Select all {this.state.queryParams.total}?
+
+
+ )}
+
+
+
+ Manage lists
+
+
+ Send campaign
+
+ {
+ if (this.state.allRowsSelected) {
+ this.handleDeleteRecordsByQuery(
+ this.state.queryParams.listID
+ ? [this.state.queryParams.listID]
+ : [],
+ this.state.queryParams.query
+ )
+ this.clearSelectedRows()
+ } else {
+ this.handleDeleteRecords(this.state.selectedRows)
+ this.clearSelectedRows()
+ }
+ }}
+ >
+
+ Delete
+
+
+ {
+ if (this.state.allRowsSelected) {
+ this.handleBlacklistSubscribersByQuery(
+ this.state.queryParams.listID
+ ? [this.state.queryParams.listID]
+ : [],
+ this.state.queryParams.query
+ )
+ this.clearSelectedRows()
+ } else {
+ this.handleBlacklistSubscribers(
+ this.state.selectedRows
+ )
+ this.clearSelectedRows()
+ }
+ }}
+ >
+
+ Blacklist
+
+
+
+
+ )}
+
+
+
-
`sub-${record.id}` }
- dataSource={ this.props.data[cs.ModelSubscribers].results }
- loading={ this.props.reqStates[cs.ModelSubscribers] !== cs.StateDone }
- pagination={ pagination }
- rowSelection = {{
- columnWidth: "5%",
- onChange: this.handleSelectRow,
- selectedRowKeys: this.state.selectedRows.map(r => `sub-${r.id}`)
- }}
- />
+ `sub-${record.id}`}
+ dataSource={this.props.data[cs.ModelSubscribers].results}
+ loading={this.props.reqStates[cs.ModelSubscribers] !== cs.StateDone}
+ pagination={pagination}
+ rowSelection={{
+ columnWidth: "5%",
+ onChange: this.handleSelectRow,
+ selectedRowKeys: this.state.selectedRows.map(r => `sub-${r.id}`)
+ }}
+ />
- { this.state.formType !== null &&
- {
- if(!this.state.modalForm) {
- return;
- }
+ {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 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.state.listsFormVisible &&
- }
-
- )
- }
+ this.setState({ modalForm: r })
+ }}
+ />
+
+ )}
+
+ {this.state.listsFormVisible && (
+
+ )}
+
+ )
+ }
}
export default Subscribers
diff --git a/frontend/my/src/Templates.js b/frontend/my/src/Templates.js
index ddef599..866b231 100644
--- a/frontend/my/src/Templates.js
+++ b/frontend/my/src/Templates.js
@@ -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 (
+
+
+
+
+ {getFieldDecorator("name", {
+ initialValue: record.name,
+ rules: [{ required: true }]
+ })( )}
+
+
+ {getFieldDecorator("body", {
+ initialValue: record.body ? record.body : "",
+ rules: [{ required: true }]
+ })( )}
+
+ {this.props.form.getFieldValue("body") !== "" && (
+
+
+ this.handlePreview(
+ this.props.form.getFieldValue("name"),
+ this.props.form.getFieldValue("body")
+ )
+ }
+ >
+ Preview
+
+
+ )}
+
+
+
+
+
+ The placeholder{" "}
+
+ {"{"}
+ {"{"} template "content" . {"}"}
+ {"}"}
+
{" "}
+ should appear in the template.{" "}
+
+ Read more on templating
+
+ .
+
+
+
- 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 && (
+
{
- 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 (
-
- )
- }
+ previewURL={cs.Routes.PreviewNewTemplate}
+ body={this.state.previewBody}
+ onCancel={() => {
+ this.setState({ previewBody: null, previewName: null })
+ }}
+ />
+ )}
+
+ )
+ }
}
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 (
-
- )
- }
- },
- {
- 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 (
-
-
this.handlePreview(record)}>
+ this.columns = [
+ {
+ title: "Name",
+ dataIndex: "name",
+ sorter: true,
+ width: "50%",
+ render: (text, record) => {
+ return (
+
+ )
+ }
+ },
+ {
+ 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 (
+
- )
- }
- }]
- }
+ {record.id !== 1 && (
+
this.handleDeleteRecord(record)}
+ >
+
+
+
+
+
+
+ )}
+
+ )
+ }
+ }
+ ]
+ }
- 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 (
+
+
+
+ Templates ({this.props.data[cs.ModelTemplates].length})
+
+
+
+ Add template
+
+
+
+
- handleShowEditForm = (record) => {
- this.setState({ formType: cs.FormEdit, record: record })
- }
+ record.id}
+ dataSource={this.props.data[cs.ModelTemplates]}
+ loading={this.props.reqStates[cs.ModelTemplates] !== cs.StateDone}
+ pagination={false}
+ />
- render() {
- return (
-
-
- Templates ({this.props.data[cs.ModelTemplates].length})
-
- Add template
-
-
-
+
- record.id }
- dataSource={ this.props.data[cs.ModelTemplates] }
- loading={ this.props.reqStates[cs.ModelTemplates] !== cs.StateDone }
- pagination={ false }
- />
-
-
-
- { this.state.previewRecord &&
- {
- this.setState({ previewRecord: null })
- }}
- />
- }
-
- )
- }
+ {this.state.previewRecord && (
+ {
+ this.setState({ previewRecord: null })
+ }}
+ />
+ )}
+
+ )
+ }
}
export default Templates
diff --git a/frontend/my/src/Test.js b/frontend/my/src/Test.js
deleted file mode 100644
index 9234407..0000000
--- a/frontend/my/src/Test.js
+++ /dev/null
@@ -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 (
-
-
-
- )
- }
-}
-
-export default QuillEditor;
diff --git a/frontend/my/src/constants.js b/frontend/my/src/constants.js
index 009f388..06b95c1 100644
--- a/frontend/my/src/constants.js
+++ b/frontend/my/src/constants.js
@@ -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"
}
diff --git a/frontend/my/src/index.js b/frontend/my/src/index.js
index 90fb7e3..7fd0cad 100644
--- a/frontend/my/src/index.js
+++ b/frontend/my/src/index.js
@@ -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(( ), document.getElementById('root'))
+ReactDOM.render( , document.getElementById("root"))
diff --git a/frontend/my/src/utils.js b/frontend/my/src/utils.js
index fa8a863..959c2ef 100644
--- a/frontend/my/src/utils.js
+++ b/frontend/my/src/utils.js
@@ -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( ,
- 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( ,
- 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(
+ ,
+ document.getElementById("alert-container")
+ )
+ }
+ static ModalAlert = (msg, msgType) => {
+ document.getElementById("modal-alert-container").classList.add("visible")
+ ReactDOM.render(
+ ,
+ document.getElementById("modal-alert-container")
+ )
+ }
}
export default Utils
diff --git a/queries.go b/queries.go
index b343084..53e9052 100644
--- a/queries.go
+++ b/queries.go
@@ -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.