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"
>
+
+
+
+