From ca128df49a1710e27320748bb83553329e956a07 Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Thu, 9 Dec 2021 21:34:38 +0530 Subject: [PATCH] Add support for searching lists + search UI. Closes #618. --- cmd/campaigns.go | 11 ++++++----- cmd/lists.go | 21 +++++++++++---------- frontend/cypress/integration/lists.js | 25 +++++++++++++++---------- frontend/src/views/ListForm.vue | 2 +- frontend/src/views/Lists.vue | 21 +++++++++++++++++++++ queries.sql | 3 ++- 6 files changed, 56 insertions(+), 27 deletions(-) diff --git a/cmd/campaigns.go b/cmd/campaigns.go index 1bbfad3f..0a5959f8 100644 --- a/cmd/campaigns.go +++ b/cmd/campaigns.go @@ -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("&"))) + `%` } diff --git a/cmd/lists.go b/cmd/lists.go index 842d1054..578f9d8f 100644 --- a/cmd/lists.go +++ b/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", diff --git a/frontend/cypress/integration/lists.js b/frontend/cypress/integration/lists.js index 8d5ecf80..f8e50899 100644 --- a/frontend/cypress/integration/lists.js +++ b/frontend/cypress/integration/lists.js @@ -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]); diff --git a/frontend/src/views/ListForm.vue b/frontend/src/views/ListForm.vue index f9e1de9a..0a0a912d 100644 --- a/frontend/src/views/ListForm.vue +++ b/frontend/src/views/ListForm.vue @@ -42,7 +42,7 @@ diff --git a/frontend/src/views/Lists.vue b/frontend/src/views/Lists.vue index 7e3f6db8..8497315b 100644 --- a/frontend/src/views/Lists.vue +++ b/frontend/src/views/Lists.vue @@ -25,6 +25,25 @@ :current-page="queryParams.page" :per-page="lists.perPage" :total="lists.total" backend-sorting @sort="onSort" > + +