Add bulk deletion (by id or query) to lists and campaigns.

- Like subscribers, select one-or more or 'all' items and delete them
  on the lists and campaigns UIs.
- New `DELETE /api/lists` and `DELETE /api/campaigns` endpoints that
  take one or more `id` params or a single `query` param.
This commit is contained in:
Kailash Nadh
2025-11-23 22:21:20 +05:30
parent 2b60907338
commit 583f92a6fc
16 changed files with 409 additions and 39 deletions

View File

@@ -153,6 +153,11 @@ export const deleteList = (id) => http.delete(
{ loading: models.lists },
);
export const deleteLists = (params) => http.delete(
'/api/lists',
{ params, loading: models.lists },
);
// Subscribers.
export const getSubscribers = async (params) => http.get(
'/api/subscribers',
@@ -353,6 +358,11 @@ export const deleteCampaign = async (id) => http.delete(
{ loading: models.campaigns },
);
export const deleteCampaigns = (params) => http.delete(
'/api/campaigns',
{ params, loading: models.campaigns },
);
// Media.
export const getMedia = async (params) => http.get(
'/api/media',

View File

@@ -17,9 +17,10 @@
</div>
</header>
<b-table :data="campaigns.results" :loading="loading.campaigns" :row-class="highlightedRow" paginated
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
:per-page="campaigns.perPage" :total="campaigns.total" hoverable backend-sorting @sort="onSort">
<b-table :data="campaigns.results" :loading="loading.campaigns" :row-class="highlightedRow"
@check-all="onTableCheck" @check="onTableCheck" :checked-rows.sync="bulk.checked" paginated backend-pagination
pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
:per-page="campaigns.perPage" :total="campaigns.total" hoverable checkable backend-sorting @sort="onSort">
<template #top-left>
<div class="columns">
<div class="column is-6">
@@ -36,6 +37,20 @@
</form>
</div>
</div>
<div class="actions" v-if="bulk.checked.length > 0">
<a class="a" href="#" @click.prevent="deleteCampaigns" data-cy="btn-delete-campaigns">
<b-icon icon="trash-can-outline" size="is-small" /> Delete
</a>
<span class="a">
{{ $tc('globals.messages.numSelected', numSelectedCampaigns, { num: numSelectedCampaigns }) }}
<span v-if="!bulk.all && campaigns.total > campaigns.perPage">
&mdash;
<a href="#" @click.prevent="onSelectAll">
{{ $tc('globals.messages.selectAll', campaigns.total, { num: campaigns.total }) }}
</a>
</span>
</span>
</div>
</template>
<b-table-column v-slot="props" cell-class="status" field="status" :label="$t('globals.fields.status')" width="10%"
@@ -285,6 +300,12 @@ export default Vue.extend({
},
pollID: null,
campaignStatsData: {},
// Table bulk row selection states.
bulk: {
checked: [],
all: false,
},
};
},
@@ -456,10 +477,57 @@ export default Vue.extend({
this.$utils.toast(this.$t('globals.messages.deleted', { name: c.name }));
});
},
// Mark all campaigns in the query as selected.
onSelectAll() {
this.bulk.all = true;
},
onTableCheck() {
// Disable bulk.all selection if there are no rows checked in the table.
if (this.bulk.checked.length !== this.campaigns.total) {
this.bulk.all = false;
}
},
deleteCampaigns() {
const name = this.$tc('globals.terms.campaign', this.numSelectedCampaigns);
const fn = () => {
const params = {};
if (!this.bulk.all && this.bulk.checked.length > 0) {
// If 'all' is not selected, delete campaigns by IDs.
params.id = this.bulk.checked.map((c) => c.id);
} else {
// 'All' is selected, delete by query.
params.query = this.queryParams.query.replace(/[^\p{L}\p{N}\s]/gu, ' ');
}
this.$api.deleteCampaigns(params)
.then(() => {
this.getCampaigns();
this.$utils.toast(this.$tc(
'globals.messages.deletedCount',
this.numSelectedCampaigns,
{ num: this.numSelectedCampaigns, name },
));
});
};
this.$utils.confirm(this.$tc(
'globals.messages.confirmDelete',
this.numSelectedCampaigns,
{ num: this.numSelectedCampaigns, name: name.toLowerCase() },
), fn);
},
},
computed: {
...mapState(['campaigns', 'loading']),
numSelectedCampaigns() {
return this.bulk.all ? this.campaigns.total : this.bulk.checked.length;
},
},
mounted() {

View File

@@ -26,9 +26,10 @@
</div>
</header>
<b-table :data="lists.results" :loading="loading.listsFull" hoverable default-sort="createdAt" paginated
backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page"
:per-page="lists.perPage" :total="lists.total" backend-sorting @sort="onSort">
<b-table :data="lists.results" :loading="loading.listsFull" @check-all="onTableCheck" @check="onTableCheck"
:checked-rows.sync="bulk.checked" hoverable default-sort="createdAt" paginated backend-pagination
pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="lists.perPage"
:total="lists.total" checkable backend-sorting @sort="onSort">
<template #top-left>
<div class="columns">
<div class="column is-6">
@@ -42,6 +43,20 @@
</form>
</div>
</div>
<div class="actions" v-if="bulk.checked.length > 0">
<a class="a" href="#" @click.prevent="deleteLists" data-cy="btn-delete-lists">
<b-icon icon="trash-can-outline" size="is-small" /> Delete
</a>
<span class="a">
{{ $tc('globals.messages.numSelected', numSelectedLists, { num: numSelectedLists }) }}
<span v-if="!bulk.all && lists.total > lists.perPage">
&mdash;
<a href="#" @click.prevent="onSelectAll">
{{ $tc('globals.messages.selectAll', lists.total, { num: lists.total }) }}
</a>
</span>
</span>
</div>
</template>
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')" header-class="cy-name" sortable
@@ -195,6 +210,12 @@ export default Vue.extend({
order: 'asc',
status: this.$route.query.status || 'active',
},
// Table bulk row selection states.
bulk: {
checked: [],
all: false,
},
};
},
@@ -272,6 +293,49 @@ export default Vue.extend({
);
},
// Mark all lists in the query as selected.
onSelectAll() {
this.bulk.all = true;
},
onTableCheck() {
// Disable bulk.all selection if there are no rows checked in the table.
if (this.bulk.checked.length !== this.lists.total) {
this.bulk.all = false;
}
},
deleteLists() {
const name = this.$tc('globals.terms.list', this.numSelectedCampaigns);
const fn = () => {
const params = {};
if (!this.bulk.all && this.bulk.checked.length > 0) {
// If 'all' is not selected, delete lists by IDs.
params.id = this.bulk.checked.map((l) => l.id);
} else {
// 'All' is selected, delete by query.
params.query = this.queryParams.query.replace(/[^\p{L}\p{N}\s]/gu, ' ');
}
this.$api.deleteLists(params)
.then(() => {
this.getLists();
this.$utils.toast(this.$tc(
'globals.messages.deletedCount',
this.numSelectedLists,
{ num: this.numSelectedLists, name },
));
});
};
this.$utils.confirm(this.$tc(
'globals.messages.confirmDelete',
this.numSelectedLists,
{ num: this.numSelectedLists, name: name.toLowerCase() },
), fn);
},
createOptinCampaign(list) {
const data = {
name: this.$t('lists.optinTo', { name: list.name }),
@@ -292,6 +356,10 @@ export default Vue.extend({
computed: {
...mapState(['loading', 'settings']),
numSelectedLists() {
return this.bulk.all ? this.lists.total : this.bulk.checked.length;
},
},
mounted() {

View File

@@ -9,7 +9,8 @@
</span>
<span v-if="currentList">
&raquo; {{ currentList.name }}
<span v-if="queryParams.subStatus" class="has-text-grey has-text-weight-normal is-capitalized">({{ queryParams.subStatus }})</span>
<span v-if="queryParams.subStatus" class="has-text-grey has-text-weight-normal is-capitalized">({{
queryParams.subStatus }})</span>
</span>
</h1>
</div>
@@ -92,11 +93,11 @@
<b-icon icon="account-off-outline" size="is-small" /> Blocklist
</a>
<span class="a">
{{ $t('subscribers.numSelected', { num: numSelectedSubscribers }) }}
{{ $t('globals.messages.numSelected', { num: numSelectedSubscribers }) }}
<span v-if="!bulk.all && subscribers.total > subscribers.perPage">
&mdash;
<a href="#" @click.prevent="selectAllSubscribers">
{{ $t('subscribers.selectAll', { num: subscribers.total }) }}
{{ $t('globals.messages.selectAll', { num: subscribers.total }) }}
</a>
</span>
</span>