Add support for searching lists + search UI. Closes #618.
This commit is contained in:
parent
e9709e54ee
commit
ca128df49a
6 changed files with 56 additions and 27 deletions
|
@ -102,13 +102,13 @@ func handleGetCampaigns(c echo.Context) error {
|
|||
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
||||
)
|
||||
|
||||
// Fetch one list.
|
||||
// Fetch one campaign.
|
||||
single := false
|
||||
if id > 0 {
|
||||
single = true
|
||||
}
|
||||
|
||||
queryStr, stmt := makeCampaignQuery(query, orderBy, order, app.queries.QueryCampaigns)
|
||||
queryStr, stmt := makeSearchQuery(query, orderBy, order, app.queries.QueryCampaigns)
|
||||
|
||||
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
||||
if err := db.Select(&out.Results, stmt, id, pq.StringArray(status), queryStr, pg.Offset, pg.Limit); err != nil {
|
||||
|
@ -791,9 +791,10 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
|
|||
return o, nil
|
||||
}
|
||||
|
||||
// makeCampaignQuery cleans an optional campaign search string and prepares the
|
||||
// campaign SQL statement (string) and returns them.
|
||||
func makeCampaignQuery(q, orderBy, order, query string) (string, string) {
|
||||
// makeSearchQuery cleans an optional search string and prepares the
|
||||
// query SQL statement (string interpolated) and returns the
|
||||
// search query string along with the SQL expression.
|
||||
func makeSearchQuery(q, orderBy, order, query string) (string, string) {
|
||||
if q != "" {
|
||||
q = `%` + string(regexFullTextQuery.ReplaceAll([]byte(q), []byte("&"))) + `%`
|
||||
}
|
||||
|
|
21
cmd/lists.go
21
cmd/lists.go
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/knadh/listmonk/models"
|
||||
|
@ -31,20 +32,21 @@ func handleGetLists(c echo.Context) error {
|
|||
out listsWrap
|
||||
|
||||
pg = getPagination(c.QueryParams(), 20)
|
||||
query = strings.TrimSpace(c.FormValue("query"))
|
||||
orderBy = c.FormValue("order_by")
|
||||
order = c.FormValue("order")
|
||||
minimal, _ = strconv.ParseBool(c.FormValue("minimal"))
|
||||
listID, _ = strconv.Atoi(c.Param("id"))
|
||||
single = false
|
||||
)
|
||||
|
||||
// Fetch one list.
|
||||
single := false
|
||||
if listID > 0 {
|
||||
single = true
|
||||
}
|
||||
|
||||
// Minimal query simply returns the list of all lists without JOIN subscriber counts. This is fast.
|
||||
if !single && minimal {
|
||||
// Minimal query simply returns the list of all lists with no additional metadata. This is fast.
|
||||
if err := app.queries.GetLists.Select(&out.Results, "", "id"); err != nil {
|
||||
app.log.Printf("error fetching lists: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
|
@ -65,15 +67,14 @@ func handleGetLists(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// Sort params.
|
||||
if !strSliceContains(orderBy, listQuerySortFields) {
|
||||
orderBy = "created_at"
|
||||
}
|
||||
if order != sortAsc && order != sortDesc {
|
||||
order = sortAsc
|
||||
}
|
||||
queryStr, stmt := makeSearchQuery(query, orderBy, order, app.queries.QueryLists)
|
||||
|
||||
if err := db.Select(&out.Results, fmt.Sprintf(app.queries.QueryLists, orderBy, order), listID, pg.Offset, pg.Limit); err != nil {
|
||||
if err := db.Select(&out.Results,
|
||||
stmt,
|
||||
listID,
|
||||
queryStr,
|
||||
pg.Offset,
|
||||
pg.Limit); err != nil {
|
||||
app.log.Printf("error fetching lists: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("globals.messages.errorFetching",
|
||||
|
|
|
@ -50,8 +50,9 @@ describe('Lists', () => {
|
|||
cy.get('input[name=name]').clear().type(`list-${n}`);
|
||||
cy.get('select[name=type]').select('public');
|
||||
cy.get('select[name=optin]').select('double');
|
||||
cy.get('input[name=tags]').clear().type(`tag${n}`);
|
||||
cy.get('button[type=submit]').click();
|
||||
cy.get('input[name=tags]').clear().type(`tag${n}{enter}`);
|
||||
cy.get('[data-cy=btn-save]').click();
|
||||
cy.wait(100);
|
||||
});
|
||||
cy.wait(250);
|
||||
|
||||
|
@ -93,7 +94,7 @@ describe('Lists', () => {
|
|||
cy.get('select[name=type]').select(t);
|
||||
cy.get('select[name=optin]').select(o);
|
||||
cy.get('input[name=tags]').type(`tag${n}{enter}${t}{enter}${o}{enter}`);
|
||||
cy.get('button[type=submit]').click();
|
||||
cy.get('[data-cy=btn-save]').click();
|
||||
cy.wait(200);
|
||||
|
||||
// Confirm the addition by inspecting the newly created list row.
|
||||
|
@ -101,17 +102,21 @@ describe('Lists', () => {
|
|||
cy.get(`${tr} td[data-label=Name]`).contains(name);
|
||||
cy.get(`${tr} td[data-label=Type] .tag[data-cy=type-${t}]`);
|
||||
cy.get(`${tr} td[data-label=Type] .tag[data-cy=optin-${o}]`);
|
||||
cy.get(`${tr} .tags`)
|
||||
.should('contain', `tag${n}`)
|
||||
.and('contain', t, { matchCase: false })
|
||||
.and('contain', o, { matchCase: false });
|
||||
|
||||
n++;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Searches lists', () => {
|
||||
cy.get('[data-cy=query]').clear().type('list-public-single-2{enter}');
|
||||
cy.wait(200)
|
||||
cy.get('tbody tr').its('length').should('eq', 1);
|
||||
cy.get('tbody td[data-label="Name"]').first().contains('list-public-single-2');
|
||||
cy.get('[data-cy=query]').clear().type('{enter}');
|
||||
});
|
||||
|
||||
|
||||
// Sort lists by clicking on various headers. At this point, there should be four
|
||||
// lists with IDs = [3, 4, 5, 6]. Sort the items be columns and match them with
|
||||
// the expected order of IDs.
|
||||
|
@ -119,8 +124,8 @@ describe('Lists', () => {
|
|||
cy.sortTable('thead th.cy-name', [4, 3, 6, 5]);
|
||||
cy.sortTable('thead th.cy-name', [5, 6, 3, 4]);
|
||||
|
||||
cy.sortTable('thead th.cy-type', [5, 6, 4, 3]);
|
||||
cy.sortTable('thead th.cy-type', [4, 3, 5, 6]);
|
||||
cy.sortTable('thead th.cy-type', [3, 4, 5, 6]);
|
||||
cy.sortTable('thead th.cy-type', [6, 5, 4, 3]);
|
||||
|
||||
cy.sortTable('thead th.cy-created_at', [3, 4, 5, 6]);
|
||||
cy.sortTable('thead th.cy-created_at', [6, 5, 4, 3]);
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<footer class="modal-card-foot has-text-right">
|
||||
<b-button @click="$parent.close()">{{ $t('globals.buttons.close') }}</b-button>
|
||||
<b-button native-type="submit" type="is-primary"
|
||||
:loading="loading.lists">{{ $t('globals.buttons.save') }}</b-button>
|
||||
:loading="loading.lists" data-cy="btn-save">{{ $t('globals.buttons.save') }}</b-button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -25,6 +25,25 @@
|
|||
:current-page="queryParams.page" :per-page="lists.perPage" :total="lists.total"
|
||||
backend-sorting @sort="onSort"
|
||||
>
|
||||
<template #top-left>
|
||||
<div class="columns">
|
||||
<div class="column is-6">
|
||||
<form @submit.prevent="getLists">
|
||||
<div>
|
||||
<b-field>
|
||||
<b-input v-model="queryParams.query" name="query" expanded
|
||||
icon="magnify" ref="query" data-cy="query" />
|
||||
<p class="controls">
|
||||
<b-button native-type="submit" type="is-primary" icon-left="magnify"
|
||||
data-cy="btn-query" />
|
||||
</p>
|
||||
</b-field>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<b-table-column v-slot="props" field="name" :label="$t('globals.fields.name')"
|
||||
header-class="cy-name" sortable width="25%"
|
||||
paginated backend-pagination pagination-position="both"
|
||||
|
@ -146,6 +165,7 @@ export default Vue.extend({
|
|||
isFormVisible: false,
|
||||
queryParams: {
|
||||
page: 1,
|
||||
query: '',
|
||||
orderBy: 'id',
|
||||
order: 'asc',
|
||||
},
|
||||
|
@ -192,6 +212,7 @@ export default Vue.extend({
|
|||
getLists() {
|
||||
this.$api.getLists({
|
||||
page: this.queryParams.page,
|
||||
query: this.queryParams.query,
|
||||
order_by: this.queryParams.orderBy,
|
||||
order: this.queryParams.order,
|
||||
});
|
||||
|
|
|
@ -342,7 +342,8 @@ SELECT * FROM lists WHERE (CASE WHEN $1 = '' THEN 1=1 ELSE type=$1::list_type EN
|
|||
-- name: query-lists
|
||||
WITH ls AS (
|
||||
SELECT COUNT(*) OVER () AS total, lists.* FROM lists
|
||||
WHERE ($1 = 0 OR id = $1) OFFSET $2 LIMIT (CASE WHEN $3 = 0 THEN NULL ELSE $3 END)
|
||||
WHERE ($1 = 0 OR id = $1) AND ($2 = '' OR name ILIKE $2)
|
||||
OFFSET $3 LIMIT (CASE WHEN $4 = 0 THEN NULL ELSE $4 END)
|
||||
),
|
||||
counts AS (
|
||||
SELECT COUNT(*) as subscriber_count, list_id FROM subscriber_lists
|
||||
|
|
Loading…
Reference in a new issue