Skip to content

Commit ba24c64

Browse files
Bowrnaknadh
andauthored
Add subsriber blocklisting on the bounces UI (#2409)
Co-authored-by: Kailash Nadh <kailash@nadh.in>
1 parent c9c678c commit ba24c64

12 files changed

Lines changed: 126 additions & 42 deletions

File tree

cmd/bounce.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ func (a *App) DeleteBounces(c echo.Context) error {
8686
}
8787

8888
// Delete bounces from the DB.
89-
if err := a.core.DeleteBounces(ids); err != nil {
89+
if err := a.core.DeleteBounces(ids, all); err != nil {
9090
return err
9191
}
9292

@@ -97,7 +97,16 @@ func (a *App) DeleteBounces(c echo.Context) error {
9797
func (a *App) DeleteBounce(c echo.Context) error {
9898
// Delete bounces from the DB.
9999
id := getID(c)
100-
if err := a.core.DeleteBounces([]int{id}); err != nil {
100+
if err := a.core.DeleteBounces([]int{id}, false); err != nil {
101+
return err
102+
}
103+
104+
return c.JSON(http.StatusOK, okResp{true})
105+
}
106+
107+
// BlocklistBouncedSubscribers handles blocklisting of all bounced subscribers.
108+
func (a *App) BlocklistBouncedSubscribers(c echo.Context) error {
109+
if err := a.core.BlocklistBouncedSubscribers(); err != nil {
101110
return err
102111
}
103112

cmd/handlers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
121121
g.DELETE("/api/subscribers", pm(a.DeleteSubscribers, "subscribers:manage"))
122122

123123
g.GET("/api/bounces", pm(a.GetBounces, "bounces:get"))
124+
g.PUT("/api/bounces/blocklist", pm(a.BlocklistBouncedSubscribers, "bounces:manage"))
124125
g.GET("/api/bounces/:id", pm(hasID(a.GetBounce), "bounces:get"))
125126
g.DELETE("/api/bounces", pm(a.DeleteBounces, "bounces:manage"))
126127
g.DELETE("/api/bounces/:id", pm(hasID(a.DeleteBounce), "bounces:manage"))

cmd/subscribers.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,13 @@ func (a *App) BlocklistSubscribersByQuery(c echo.Context) error {
458458

459459
req.Search = strings.TrimSpace(req.Search)
460460
req.Query = formatSQLExp(req.Query)
461-
461+
if req.All {
462+
// If the "all" flag is set, ignore any subquery that may be present.
463+
req.Search = ""
464+
req.Query = ""
465+
} else if req.Search == "" && req.Query == "" {
466+
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", "query"))
467+
}
462468
// Does the user have the subscribers:sql_query permission?
463469
if req.Query != "" {
464470
if !user.HasPerm(auth.PermSubscribersSqlQuery) {

docs/swagger/collections.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ paths:
817817
page:
818818
type: integer
819819
delete:
820-
description: handles retrieval of bounce records.
820+
description: handles deletion of bounce records.
821821
operationId: deleteBounces
822822
tags:
823823
- Bounces
@@ -889,7 +889,7 @@ paths:
889889
properties:
890890
data:
891891
type: boolean
892-
892+
893893
/lists:
894894
get:
895895
description: retrieves lists with additional metadata like subscriber counts. This may be slow.
@@ -2160,6 +2160,10 @@ components:
21602160
type: string
21612161
analytics.toDate:
21622162
type: string
2163+
bounces.numSelected:
2164+
type: string
2165+
bounces.selectAll:
2166+
type: string
21632167
bounces.source:
21642168
type: string
21652169
bounces.unknownService:

frontend/src/api/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ export const deleteBounces = async (params) => http.delete(
189189
{ params, loading: models.bounces },
190190
);
191191

192+
export const blocklistBouncedSubscribers = async () => http.put(
193+
'/api/bounces/blocklist',
194+
{ loading: models.bounces },
195+
);
196+
192197
export const createSubscriber = (data) => http.post(
193198
'/api/subscribers',
194199
data,

frontend/src/assets/style.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,9 @@ section.lists {
848848
overflow-x: auto;
849849
max-width: 100%;
850850
}
851+
.blocklisted {
852+
color: red;
853+
}
851854
}
852855

853856
/* Import page */

frontend/src/views/Bounces.vue

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,43 @@
77
<span v-if="bounces.total > 0">({{ bounces.total }})</span>
88
</h1>
99
</div>
10-
<div class="column has-text-right buttons">
11-
<b-button v-if="bulk.checked.length > 0 || bulk.all" type="is-primary" icon-left="trash-can-outline"
12-
data-cy="btn-delete" @click.prevent="$utils.confirm(null, () => deleteBounces())">
13-
{{ $t('globals.buttons.clear') }}
14-
</b-button>
15-
<b-button v-if="bounces.total" icon-left="trash-can-outline" data-cy="btn-delete"
16-
@click.prevent="$utils.confirm(null, () => deleteBounces(true))">
17-
{{ $t('globals.buttons.clearAll') }}
18-
</b-button>
19-
</div>
2010
</header>
2111

2212
<b-table :data="bounces.results" :hoverable="true" :loading="loading.bounces" default-sort="createdAt" checkable
2313
@check-all="onTableCheck" @check="onTableCheck" :checked-rows.sync="bulk.checked" detailed show-detail-icon
24-
paginated backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="bounces.perPage"
25-
:total="bounces.total" backend-sorting @sort="onSort">
14+
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
15+
:current-page="queryParams.page" :per-page="bounces.perPage" :total="bounces.total" backend-sorting
16+
@sort="onSort">
17+
<template #top-left>
18+
<div class="actions">
19+
<template v-if="bulk.checked.length > 0">
20+
<a class="a" href="#" @click.prevent="$utils.confirm(null, () => deleteBounces())" data-cy="btn-delete">
21+
<b-icon icon="trash-can-outline" size="is-small" /> {{ $t('globals.buttons.delete') }}
22+
</a>
23+
<a class="a" href="#" @click.prevent="$utils.confirm(null, () => blocklistSubscribers())"
24+
data-cy="btn-manage-blocklist">
25+
<b-icon icon="account-off-outline" size="is-small" /> {{ $t('import.blocklist') }}
26+
</a>
27+
<span>
28+
{{ $t('globals.messages.numSelected', { num: numSelectedBounces }) }}
29+
<span v-if="!bulk.all && bounces.total > bounces.perPage">
30+
&mdash;
31+
<a href="#" @click.prevent="selectAllBounces">
32+
{{ $t('subscribers.selectAll', { num: bounces.total }) }}
33+
</a>
34+
</span>
35+
</span>
36+
</template>
37+
</div>
38+
</template>
2639
<b-table-column v-slot="props" field="email" :label="$t('subscribers.email')" :td-attrs="$utils.tdID" sortable>
27-
<router-link :to="{ name: 'subscriber', params: { id: props.row.subscriberId } }">
40+
<router-link :to="{ name: 'subscriber', params: { id: props.row.subscriberId } }"
41+
:class="{ 'blocklisted': props.row.subscriberStatus === 'blocklisted' }">
2842
{{ props.row.email }}
43+
<b-tag v-if="props.row.subscriberStatus !== 'enabled'" :class="props.row.subscriberStatus"
44+
data-cy="blocklisted">
45+
{{ $t(`subscribers.status.${props.row.subscriberStatus}`) }}
46+
</b-tag>
2947
</router-link>
3048
</b-table-column>
3149

@@ -119,7 +137,10 @@ export default Vue.extend({
119137
this.queryParams.page = p;
120138
this.getBounces();
121139
},
122-
140+
// Mark all bounces in the query as selected.
141+
selectAllBounces() {
142+
this.bulk.all = true;
143+
},
123144
onTableCheck() {
124145
// Disable bulk.all selection if there are no rows checked in the table.
125146
if (this.bulk.checked.length !== this.bounces.total) {
@@ -149,35 +170,47 @@ export default Vue.extend({
149170
});
150171
},
151172
152-
deleteBounces(all) {
153-
const fnSuccess = () => {
173+
deleteBounces() {
174+
const params = {};
175+
if (!this.bulk.all && this.bulk.checked.length > 0) {
176+
params.id = this.bulk.checked.map((s) => s.id);
177+
} else if (this.bulk.all) {
178+
params.all = true;
179+
}
180+
181+
this.$api.deleteBounces(params).then(() => {
154182
this.getBounces();
155183
this.$utils.toast(this.$t(
156184
'globals.messages.deletedCount',
157-
{ name: this.$tc('globals.terms.bounces'), num: this.bounces.total },
185+
{ name: this.$tc('globals.terms.bounces'), num: this.numSelectedBounces },
158186
));
187+
});
188+
},
189+
190+
blocklistSubscribers() {
191+
const cb = () => {
192+
this.getBounces();
193+
this.$utils.toast(this.$t('globals.messages.done'));
159194
};
160195
161-
if (all) {
162-
this.$api.deleteBounces({ all: true }).then(fnSuccess);
196+
if (!this.bulk.all && this.bulk.checked.length > 0) {
197+
const subIds = this.bulk.checked.map((s) => s.subscriberId);
198+
this.$api.blocklistSubscribers({ ids: subIds }).then(cb);
163199
return;
164200
}
165201
166-
const ids = this.bulk.checked.map((s) => s.id);
167-
this.$api.deleteBounces({ id: ids }).then(fnSuccess);
202+
this.$api.blocklistBouncedSubscribers({ all: true }).then(cb);
168203
},
169204
},
170205
171206
computed: {
172207
...mapState(['templates', 'loading']),
173-
174-
selectedBounces() {
208+
numSelectedBounces() {
175209
if (this.bulk.all) {
176210
return this.bounces.total;
177211
}
178212
return this.bulk.checked.length;
179213
},
180-
181214
},
182215
183216
mounted() {

i18n/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
"globals.fields.type": "Type",
175175
"globals.fields.updatedAt": "Updated",
176176
"globals.fields.uuid": "UUID",
177+
"globals.messages.numSelected": "{num} selected",
177178
"globals.messages.confirm": "Are you sure?",
178179
"globals.messages.confirmDiscard": "Discard changes?",
179180
"globals.messages.copied": "Copied",

internal/core/bounces.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,24 @@ func (c *Core) RecordBounce(b models.Bounce) error {
8686
return err
8787
}
8888

89+
// BlocklistBouncedSubscribers blocklists all bounced subscribers.
90+
func (c *Core) BlocklistBouncedSubscribers() error {
91+
if _, err := c.q.BlocklistBouncedSubscribers.Exec(); err != nil {
92+
c.log.Printf("error blocklisting bounced subscribers: %v", err)
93+
return echo.NewHTTPError(http.StatusInternalServerError, c.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
94+
}
95+
96+
return nil
97+
}
98+
8999
// DeleteBounce deletes a list.
90100
func (c *Core) DeleteBounce(id int) error {
91-
return c.DeleteBounces([]int{id})
101+
return c.DeleteBounces([]int{id}, false)
92102
}
93103

94104
// DeleteBounces deletes multiple lists.
95-
func (c *Core) DeleteBounces(ids []int) error {
96-
if _, err := c.q.DeleteBounces.Exec(pq.Array(ids)); err != nil {
105+
func (c *Core) DeleteBounces(ids []int, all bool) error {
106+
if _, err := c.q.DeleteBounces.Exec(pq.Array(ids), all); err != nil {
97107
c.log.Printf("error deleting lists: %v", err)
98108
return echo.NewHTTPError(http.StatusInternalServerError,
99109
c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.list}", "error", pqErrMsg(err)))

models/models.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,9 +331,10 @@ type Bounce struct {
331331
CreatedAt time.Time `db:"created_at" json:"created_at"`
332332

333333
// One of these should be provided.
334-
Email string `db:"email" json:"email,omitempty"`
335-
SubscriberUUID string `db:"subscriber_uuid" json:"subscriber_uuid,omitempty"`
336-
SubscriberID int `db:"subscriber_id" json:"subscriber_id,omitempty"`
334+
Email string `db:"email" json:"email,omitempty"`
335+
SubscriberUUID string `db:"subscriber_uuid" json:"subscriber_uuid,omitempty"`
336+
SubscriberID int `db:"subscriber_id" json:"subscriber_id,omitempty"`
337+
SubscriberStatus string `db:"subscriber_status" json:"subscriber_status"`
337338

338339
CampaignUUID string `db:"campaign_uuid" json:"campaign_uuid,omitempty"`
339340
Campaign *json.RawMessage `db:"campaign" json:"campaign"`

0 commit comments

Comments
 (0)