Skip to content

Commit

Permalink
Refactor campaigns view
Browse files Browse the repository at this point in the history
- Fix sorting issues
- Add status filter
- Add name + subject search
  • Loading branch information
knadh committed Mar 28, 2019
1 parent 9655ce6 commit 178604d
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 32 deletions.
48 changes: 35 additions & 13 deletions campaigns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}

Expand Down
110 changes: 95 additions & 15 deletions frontend/my/src/Campaigns.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Campaigns extends React.PureComponent {
state = {
formType: null,
pollID: -1,
queryParams: "",
queryParams: {},
stats: {},
record: null,
previewRecord: null,
Expand All @@ -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) {
Expand All @@ -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(
Expand All @@ -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]
Expand Down Expand Up @@ -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) {
Expand All @@ -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()
})
}
Expand All @@ -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
Expand Down Expand Up @@ -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 && (
Expand Down
4 changes: 4 additions & 0 deletions models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 11 additions & 4 deletions queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -255,17 +255,23 @@ 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
FROM campaign_lists WHERE campaign_lists.campaign_id = campaigns.id
) 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)
Expand All @@ -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,
Expand Down

0 comments on commit 178604d

Please sign in to comment.