Skip to content

Commit

Permalink
Add support for searching lists + search UI. Closes #618.
Browse files Browse the repository at this point in the history
  • Loading branch information
knadh committed Dec 9, 2021
1 parent e9709e5 commit ca128df
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 27 deletions.
11 changes: 6 additions & 5 deletions cmd/campaigns.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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("&"))) + `%`
}
Expand Down
21 changes: 11 additions & 10 deletions cmd/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"strconv"
"strings"

"github.com/gofrs/uuid"
"github.com/knadh/listmonk/models"
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand Down
25 changes: 15 additions & 10 deletions frontend/cypress/integration/lists.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -93,34 +94,38 @@ 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.
const tr = `tbody tr:nth-child(${n + 1})`;
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.
it('Sorts 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]);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/ListForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/views/Lists.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -146,6 +165,7 @@ export default Vue.extend({
isFormVisible: false,
queryParams: {
page: 1,
query: '',
orderBy: 'id',
order: 'asc',
},
Expand Down Expand Up @@ -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,
});
Expand Down
3 changes: 2 additions & 1 deletion queries.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit ca128df

Please sign in to comment.