Refactor campaigns view

- Fix sorting issues
- Add status filter
- Add name + subject search
This commit is contained in:
Kailash Nadh 2019-03-28 17:17:51 +05:30
parent 9655ce6f14
commit 178604dbbf
4 changed files with 145 additions and 32 deletions

View file

@ -38,51 +38,73 @@ type campaignStats struct {
Rate float64 `json:"rate"`
}
var regexFromAddress = regexp.MustCompile(`(.+?)\s<(.+?)@(.+?)>`)
type campsWrap struct {
Results []models.Campaign `json:"results"`
Query string `json:"query"`
Total int `json:"total"`
PerPage int `json:"per_page"`
Page int `json:"page"`
}
var (
regexFromAddress = regexp.MustCompile(`(.+?)\s<(.+?)@(.+?)>`)
regexFullTextQuery = regexp.MustCompile(`\s+`)
)
// handleGetCampaigns handles retrieval of campaigns.
func handleGetCampaigns(c echo.Context) error {
var (
app = c.Get("app").(*App)
pg = getPagination(c.QueryParams())
out models.Campaigns
out campsWrap
id, _ = strconv.Atoi(c.Param("id"))
status = c.FormValue("status")
single = false
status = c.QueryParams()["status"]
query = strings.TrimSpace(c.FormValue("query"))
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
single = false
)
// Fetch one list.
if id > 0 {
single = true
}
if query != "" {
query = string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&")))
}
err := app.Queries.GetCampaigns.Select(&out, id, status, pg.Offset, pg.Limit)
err := app.Queries.GetCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
} else if single && len(out) == 0 {
} else if single && len(out.Results) == 0 {
return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
} else if len(out) == 0 {
return c.JSON(http.StatusOK, okResp{[]struct{}{}})
} else if len(out.Results) == 0 {
out.Results = make([]models.Campaign, 0)
return c.JSON(http.StatusOK, out)
}
for i := 0; i < len(out); i++ {
for i := 0; i < len(out.Results); i++ {
// Replace null tags.
if out[i].Tags == nil {
out[i].Tags = make(pq.StringArray, 0)
if out.Results[i].Tags == nil {
out.Results[i].Tags = make(pq.StringArray, 0)
}
if noBody {
out[i].Body = ""
out.Results[i].Body = ""
}
}
if single {
return c.JSON(http.StatusOK, okResp{out[0]})
return c.JSON(http.StatusOK, okResp{out.Results[0]})
}
// Meta.
out.Total = out.Results[0].Total
out.Page = pg.Page
out.PerPage = pg.PerPage
return c.JSON(http.StatusOK, okResp{out})
}

View file

@ -26,7 +26,7 @@ class Campaigns extends React.PureComponent {
state = {
formType: null,
pollID: -1,
queryParams: "",
queryParams: {},
stats: {},
record: null,
previewRecord: null,
@ -37,19 +37,13 @@ class Campaigns extends React.PureComponent {
// Pagination config.
paginationOptions = {
hideOnSinglePage: true,
hideOnSinglePage: false,
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 })
}
showTotal: (total, range) => `${range[0]} to ${range[1]} of ${total}`
}
constructor(props) {
@ -62,6 +56,50 @@ class Campaigns extends React.PureComponent {
sorter: true,
width: "20%",
vAlign: "top",
filterIcon: filtered => (
<Icon
type="search"
style={{ color: filtered ? "#1890ff" : undefined }}
/>
),
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters
}) => (
<div style={{ padding: 8 }}>
<Input
ref={node => {
this.searchInput = node
}}
placeholder={`Search`}
onChange={e =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 188, marginBottom: 8, display: "block" }}
/>
<Button
type="primary"
onClick={() => confirm()}
icon="search"
size="small"
style={{ width: 90, marginRight: 8 }}
>
Search
</Button>
<Button
onClick={() => {
clearFilters()
}}
size="small"
style={{ width: 90 }}
>
Reset
</Button>
</div>
),
render: (text, record) => {
const out = []
out.push(
@ -86,6 +124,14 @@ class Campaigns extends React.PureComponent {
dataIndex: "status",
className: "status",
width: "10%",
filters: [
{ text: "Draft", value: "draft" },
{ text: "Running", value: "running" },
{ text: "Scheduled", value: "scheduled" },
{ text: "Paused", value: "paused" },
{ text: "Cancelled", value: "cancelled" },
{ text: "Finished", value: "finished" }
],
render: (status, record) => {
let color = cs.CampaignStatusColors.hasOwnProperty(status)
? cs.CampaignStatusColors[status]
@ -415,14 +461,17 @@ class Campaigns extends React.PureComponent {
}
fetchRecords = params => {
if (!params) {
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
// Avoid sending blank string where the enum check will fail.
if (!params.status) {
delete params.status
}
if (params) {
@ -437,6 +486,17 @@ class Campaigns extends React.PureComponent {
qParams
)
.then(r => {
this.setState({
queryParams: {
...this.state.queryParams,
total: this.props.data[cs.ModelCampaigns].total,
per_page: this.props.data[cs.ModelCampaigns].per_page,
page: this.props.data[cs.ModelCampaigns].page,
query: this.props.data[cs.ModelCampaigns].query,
status: params.status
}
})
this.startStatsPoll()
})
}
@ -447,7 +507,7 @@ class Campaigns extends React.PureComponent {
// If there's at least one running campaign, start polling.
let hasRunning = false
this.props.data[cs.ModelCampaigns].forEach(c => {
this.props.data[cs.ModelCampaigns].results.forEach(c => {
if (c.status === cs.CampaignStatusRunning) {
hasRunning = true
return
@ -605,12 +665,32 @@ class Campaigns extends React.PureComponent {
<br />
<Table
className="subscribers"
className="campaigns"
columns={this.columns}
rowKey={record => record.uuid}
dataSource={this.props.data[cs.ModelCampaigns]}
dataSource={(() => {
if (
!this.props.data[cs.ModelCampaigns] ||
!this.props.data[cs.ModelCampaigns].hasOwnProperty("results")
) {
return []
}
return this.props.data[cs.ModelCampaigns].results
})()}
loading={this.props.reqStates[cs.ModelCampaigns] !== cs.StateDone}
pagination={pagination}
onChange={(pagination, filters, sorter, records) => {
this.fetchRecords({
per_page: pagination.pageSize,
page: pagination.current,
status:
filters.status && filters.status.length > 0
? filters.status
: "",
query:
filters.name && filters.name.length > 0 ? filters.name[0] : ""
})
}}
/>
{this.state.previewRecord && (

View file

@ -141,6 +141,10 @@ type Campaign struct {
// TemplateBody is joined in from templates by the next-campaigns query.
TemplateBody string `db:"template_body" json:"-"`
Tpl *template.Template `json:"-"`
// Pseudofield for getting the total number of subscribers
// in searches and queries.
Total int `db:"total" json:"-"`
}
// CampaignMeta contains fields tracking a campaign's progress.

View file

@ -255,8 +255,12 @@ INSERT INTO campaign_lists (campaign_id, list_id, list_name)
-- name: get-campaigns
-- Here, 'lists' is returned as an aggregated JSON array from campaign_lists because
-- the list reference may have been deleted.
-- While the results are sliced using offset+limit,
-- there's a COUNT() OVER() that still returns the total result count
-- for pagination in the frontend, albeit being a field that'll repeat
-- with every resultant row.
WITH camps AS (
SELECT campaigns.*, (
SELECT COUNT(*) OVER () AS total, campaigns.*, (
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
SELECT COALESCE(campaign_lists.list_id, 0) AS id,
campaign_lists.list_name AS name
@ -264,8 +268,10 @@ WITH camps AS (
) l
) AS lists
FROM campaigns
WHERE ($1 = 0 OR id = $1) AND status=(CASE WHEN $2 != '' THEN $2::campaign_status ELSE status END)
ORDER BY created_at DESC OFFSET $3 LIMIT $4
WHERE ($1 = 0 OR id = $1)
AND status=ANY(CASE WHEN ARRAY_LENGTH($2::campaign_status[], 1) != 0 THEN $2::campaign_status[] ELSE ARRAY[status] END)
AND ($3 = '' OR (to_tsvector(name || subject) @@ to_tsquery($3)))
ORDER BY created_at DESC OFFSET $4 LIMIT $5
), views AS (
SELECT campaign_id, COUNT(campaign_id) as num FROM campaign_views
WHERE campaign_id = ANY(SELECT id FROM camps)
@ -281,7 +287,8 @@ SELECT *,
COALESCE(c.num, 0) AS clicks
FROM camps
LEFT JOIN views AS v ON (v.campaign_id = camps.id)
LEFT JOIN clicks AS c ON (c.campaign_id = camps.id);
LEFT JOIN clicks AS c ON (c.campaign_id = camps.id)
ORDER BY camps.created_at DESC;
-- name: get-campaign-for-preview
SELECT campaigns.*, COALESCE(templates.body, (SELECT body FROM templates WHERE is_default = true LIMIT 1)) AS template_body,