Add serverside sort to tables.
Lists, campaigns, and subscribers tables now support server-side sorting from the UI. This significantly changes the internal queries from prepared to string interpolated to support dynamic sort params.
This commit is contained in:
parent
a0b36bb01b
commit
1aecd6f2e1
12 changed files with 174 additions and 48 deletions
|
@ -63,6 +63,8 @@ type campsWrap struct {
|
||||||
var (
|
var (
|
||||||
regexFromAddress = regexp.MustCompile(`(.+?)\s<(.+?)@(.+?)>`)
|
regexFromAddress = regexp.MustCompile(`(.+?)\s<(.+?)@(.+?)>`)
|
||||||
regexFullTextQuery = regexp.MustCompile(`\s+`)
|
regexFullTextQuery = regexp.MustCompile(`\s+`)
|
||||||
|
|
||||||
|
campaignQuerySortFields = []string{"name", "status", "created_at", "updated_at"}
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleGetCampaigns handles retrieval of campaigns.
|
// handleGetCampaigns handles retrieval of campaigns.
|
||||||
|
@ -75,11 +77,13 @@ func handleGetCampaigns(c echo.Context) error {
|
||||||
id, _ = strconv.Atoi(c.Param("id"))
|
id, _ = strconv.Atoi(c.Param("id"))
|
||||||
status = c.QueryParams()["status"]
|
status = c.QueryParams()["status"]
|
||||||
query = strings.TrimSpace(c.FormValue("query"))
|
query = strings.TrimSpace(c.FormValue("query"))
|
||||||
|
orderBy = c.FormValue("order_by")
|
||||||
|
order = c.FormValue("order")
|
||||||
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
||||||
single = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fetch one list.
|
// Fetch one list.
|
||||||
|
single := false
|
||||||
if id > 0 {
|
if id > 0 {
|
||||||
single = true
|
single = true
|
||||||
}
|
}
|
||||||
|
@ -88,8 +92,18 @@ func handleGetCampaigns(c echo.Context) error {
|
||||||
string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&"))) + `%`
|
string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&"))) + `%`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.queries.QueryCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit)
|
// Sort params.
|
||||||
if err != nil {
|
if !strSliceContains(orderBy, campaignQuerySortFields) {
|
||||||
|
orderBy = "created_at"
|
||||||
|
}
|
||||||
|
if order != sortAsc && order != sortDesc {
|
||||||
|
order = sortDesc
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(app.queries.QueryCampaigns, orderBy, order)
|
||||||
|
|
||||||
|
// Unsafe to ignore scanning fields not present in models.Campaigns.
|
||||||
|
if err := db.Select(&out.Results, stmt, id, pq.StringArray(status), query, pg.Offset, pg.Limit); err != nil {
|
||||||
app.log.Printf("error fetching campaigns: %v", err)
|
app.log.Printf("error fetching campaigns: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
|
||||||
|
|
|
@ -14,6 +14,9 @@ import (
|
||||||
const (
|
const (
|
||||||
// stdInputMaxLen is the maximum allowed length for a standard input field.
|
// stdInputMaxLen is the maximum allowed length for a standard input field.
|
||||||
stdInputMaxLen = 200
|
stdInputMaxLen = 200
|
||||||
|
|
||||||
|
sortAsc = "asc"
|
||||||
|
sortDesc = "desc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type okResp struct {
|
type okResp struct {
|
||||||
|
|
|
@ -197,6 +197,7 @@ func initQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem, prepareQue
|
||||||
if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
|
if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
|
||||||
lo.Fatalf("error preparing SQL queries: %v", err)
|
lo.Fatalf("error preparing SQL queries: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return qMap, &q
|
return qMap, &q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
cmd/lists.go
17
cmd/lists.go
|
@ -20,6 +20,10 @@ type listsWrap struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listQuerySortFields = []string{"name", "type", "subscriber_count", "created_at", "updated_at"}
|
||||||
|
)
|
||||||
|
|
||||||
// handleGetLists handles retrieval of lists.
|
// handleGetLists handles retrieval of lists.
|
||||||
func handleGetLists(c echo.Context) error {
|
func handleGetLists(c echo.Context) error {
|
||||||
var (
|
var (
|
||||||
|
@ -27,6 +31,8 @@ func handleGetLists(c echo.Context) error {
|
||||||
out listsWrap
|
out listsWrap
|
||||||
|
|
||||||
pg = getPagination(c.QueryParams(), 20, 50)
|
pg = getPagination(c.QueryParams(), 20, 50)
|
||||||
|
orderBy = c.FormValue("order_by")
|
||||||
|
order = c.FormValue("order")
|
||||||
listID, _ = strconv.Atoi(c.Param("id"))
|
listID, _ = strconv.Atoi(c.Param("id"))
|
||||||
single = false
|
single = false
|
||||||
)
|
)
|
||||||
|
@ -36,8 +42,15 @@ func handleGetLists(c echo.Context) error {
|
||||||
single = true
|
single = true
|
||||||
}
|
}
|
||||||
|
|
||||||
err := app.queries.GetLists.Select(&out.Results, listID, pg.Offset, pg.Limit)
|
// Sort params.
|
||||||
if err != nil {
|
if !strSliceContains(orderBy, listQuerySortFields) {
|
||||||
|
orderBy = "created_at"
|
||||||
|
}
|
||||||
|
if order != sortAsc && order != sortDesc {
|
||||||
|
order = sortAsc
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Select(&out.Results, fmt.Sprintf(app.queries.GetLists, orderBy, order), listID, pg.Offset, pg.Limit); err != nil {
|
||||||
app.log.Printf("error fetching lists: %v", err)
|
app.log.Printf("error fetching lists: %v", err)
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||||
fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
|
fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
|
||||||
|
|
|
@ -42,14 +42,14 @@ type Queries struct {
|
||||||
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
|
UnsubscribeSubscribersFromListsByQuery string `query:"unsubscribe-subscribers-from-lists-by-query"`
|
||||||
|
|
||||||
CreateList *sqlx.Stmt `query:"create-list"`
|
CreateList *sqlx.Stmt `query:"create-list"`
|
||||||
GetLists *sqlx.Stmt `query:"get-lists"`
|
GetLists string `query:"get-lists"`
|
||||||
GetListsByOptin *sqlx.Stmt `query:"get-lists-by-optin"`
|
GetListsByOptin *sqlx.Stmt `query:"get-lists-by-optin"`
|
||||||
UpdateList *sqlx.Stmt `query:"update-list"`
|
UpdateList *sqlx.Stmt `query:"update-list"`
|
||||||
UpdateListsDate *sqlx.Stmt `query:"update-lists-date"`
|
UpdateListsDate *sqlx.Stmt `query:"update-lists-date"`
|
||||||
DeleteLists *sqlx.Stmt `query:"delete-lists"`
|
DeleteLists *sqlx.Stmt `query:"delete-lists"`
|
||||||
|
|
||||||
CreateCampaign *sqlx.Stmt `query:"create-campaign"`
|
CreateCampaign *sqlx.Stmt `query:"create-campaign"`
|
||||||
QueryCampaigns *sqlx.Stmt `query:"query-campaigns"`
|
QueryCampaigns string `query:"query-campaigns"`
|
||||||
GetCampaign *sqlx.Stmt `query:"get-campaign"`
|
GetCampaign *sqlx.Stmt `query:"get-campaign"`
|
||||||
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
GetCampaignForPreview *sqlx.Stmt `query:"get-campaign-for-preview"`
|
||||||
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
GetCampaignStats *sqlx.Stmt `query:"get-campaign-stats"`
|
||||||
|
|
|
@ -58,11 +58,15 @@ type subOptin struct {
|
||||||
Lists []models.List
|
Lists []models.List
|
||||||
}
|
}
|
||||||
|
|
||||||
var dummySubscriber = models.Subscriber{
|
var (
|
||||||
Email: "dummy@listmonk.app",
|
dummySubscriber = models.Subscriber{
|
||||||
Name: "Dummy Subscriber",
|
Email: "dummy@listmonk.app",
|
||||||
UUID: dummyUUID,
|
Name: "Dummy Subscriber",
|
||||||
}
|
UUID: dummyUUID,
|
||||||
|
}
|
||||||
|
|
||||||
|
subQuerySortFields = []string{"email", "name", "created_at", "updated_at"}
|
||||||
|
)
|
||||||
|
|
||||||
// handleGetSubscriber handles the retrieval of a single subscriber by ID.
|
// handleGetSubscriber handles the retrieval of a single subscriber by ID.
|
||||||
func handleGetSubscriber(c echo.Context) error {
|
func handleGetSubscriber(c echo.Context) error {
|
||||||
|
@ -89,8 +93,10 @@ func handleQuerySubscribers(c echo.Context) error {
|
||||||
listID, _ = strconv.Atoi(c.FormValue("list_id"))
|
listID, _ = strconv.Atoi(c.FormValue("list_id"))
|
||||||
|
|
||||||
// The "WHERE ?" bit.
|
// The "WHERE ?" bit.
|
||||||
query = sanitizeSQLExp(c.FormValue("query"))
|
query = sanitizeSQLExp(c.FormValue("query"))
|
||||||
out subsWrap
|
orderBy = c.FormValue("order_by")
|
||||||
|
order = c.FormValue("order")
|
||||||
|
out subsWrap
|
||||||
)
|
)
|
||||||
|
|
||||||
listIDs := pq.Int64Array{}
|
listIDs := pq.Int64Array{}
|
||||||
|
@ -100,17 +106,21 @@ func handleQuerySubscribers(c echo.Context) error {
|
||||||
listIDs = append(listIDs, int64(listID))
|
listIDs = append(listIDs, int64(listID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// There's an arbitrary query condition from the frontend.
|
// There's an arbitrary query condition.
|
||||||
var (
|
cond := ""
|
||||||
cond = ""
|
|
||||||
ordBy = "updated_at"
|
|
||||||
ord = "DESC"
|
|
||||||
)
|
|
||||||
if query != "" {
|
if query != "" {
|
||||||
cond = " AND " + query
|
cond = " AND " + query
|
||||||
}
|
}
|
||||||
|
|
||||||
stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond, ordBy, ord)
|
// Sort params.
|
||||||
|
if !strSliceContains(orderBy, subQuerySortFields) {
|
||||||
|
orderBy = "updated_at"
|
||||||
|
}
|
||||||
|
if order != sortAsc && order != sortDesc {
|
||||||
|
order = sortAsc
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond, orderBy, order)
|
||||||
|
|
||||||
// Create a readonly transaction to prevent mutations.
|
// Create a readonly transaction to prevent mutations.
|
||||||
tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
||||||
|
|
11
cmd/utils.go
11
cmd/utils.go
|
@ -132,3 +132,14 @@ func generateRandomString(n int) (string, error) {
|
||||||
func strHasLen(str string, min, max int) bool {
|
func strHasLen(str string, min, max int) bool {
|
||||||
return len(str) >= min && len(str) <= max
|
return len(str) >= min && len(str) <= max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// strSliceContains checks if a string is present in the string slice.
|
||||||
|
func strSliceContains(str string, sl []string) bool {
|
||||||
|
for _, s := range sl {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
37
frontend/src/components/LogView.vue
Normal file
37
frontend/src/components/LogView.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<section class="log-view">
|
||||||
|
<b-loading :active="loading" :is-full-page="false" />
|
||||||
|
<pre class="lines" ref="lines">
|
||||||
|
<template v-for="(l, i) in lines"><span v-html="formatLine(l)" :key="i" class="line"></span>
|
||||||
|
</template></pre>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const reFormatLine = new RegExp(/^(.*) (.+?)\.go:[0-9]+:\s/g);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LogView',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
loading: Boolean,
|
||||||
|
lines: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
formatLine: (l) => l.replace(reFormatLine, '<span class="stamp">$1</span> '),
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
lines() {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.lines.scrollTop = this.$refs.lines.scrollHeight;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -26,10 +26,10 @@
|
||||||
:row-class="highlightedRow"
|
:row-class="highlightedRow"
|
||||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||||
:current-page="queryParams.page" :per-page="campaigns.perPage" :total="campaigns.total"
|
:current-page="queryParams.page" :per-page="campaigns.perPage" :total="campaigns.total"
|
||||||
hoverable>
|
hoverable backend-sorting @sort="onSort">
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column class="status" field="status" label="Status"
|
<b-table-column class="status" field="status" label="Status"
|
||||||
width="10%" :id="props.row.id">
|
width="10%" :id="props.row.id" sortable>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
<router-link :to="{ name: 'campaign', params: { 'id': props.row.id }}">
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="updatedAt" label="Timestamps" width="19%" sortable>
|
<b-table-column field="created_at" label="Timestamps" width="19%" sortable>
|
||||||
<div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
|
<div class="fields timestamps" :set="stats = getCampaignStats(props.row)">
|
||||||
<p>
|
<p>
|
||||||
<label>Created</label>
|
<label>Created</label>
|
||||||
|
@ -96,7 +96,7 @@
|
||||||
</div>
|
</div>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column :class="props.row.status" label="Stats" width="18%">
|
<b-table-column field="stats" :class="props.row.status" label="Stats" width="18%">
|
||||||
<div class="fields stats" :set="stats = getCampaignStats(props.row)">
|
<div class="fields stats" :set="stats = getCampaignStats(props.row)">
|
||||||
<p>
|
<p>
|
||||||
<label>Views</label>
|
<label>Views</label>
|
||||||
|
@ -215,6 +215,8 @@ export default Vue.extend({
|
||||||
queryParams: {
|
queryParams: {
|
||||||
page: 1,
|
page: 1,
|
||||||
query: '',
|
query: '',
|
||||||
|
orderBy: 'created_at',
|
||||||
|
order: 'desc',
|
||||||
},
|
},
|
||||||
pollID: null,
|
pollID: null,
|
||||||
campaignStatsData: {},
|
campaignStatsData: {},
|
||||||
|
@ -264,6 +266,12 @@ export default Vue.extend({
|
||||||
this.getCampaigns();
|
this.getCampaigns();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSort(field, direction) {
|
||||||
|
this.queryParams.orderBy = field;
|
||||||
|
this.queryParams.order = direction;
|
||||||
|
this.getCampaigns();
|
||||||
|
},
|
||||||
|
|
||||||
// Campaign actions.
|
// Campaign actions.
|
||||||
previewCampaign(c) {
|
previewCampaign(c) {
|
||||||
this.previewItem = c;
|
this.previewItem = c;
|
||||||
|
@ -277,6 +285,8 @@ export default Vue.extend({
|
||||||
this.$api.getCampaigns({
|
this.$api.getCampaigns({
|
||||||
page: this.queryParams.page,
|
page: this.queryParams.page,
|
||||||
query: this.queryParams.query,
|
query: this.queryParams.query,
|
||||||
|
order_by: this.queryParams.orderBy,
|
||||||
|
order: this.queryParams.order,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
hoverable default-sort="createdAt"
|
hoverable default-sort="createdAt"
|
||||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||||
:current-page="queryParams.page" :per-page="lists.perPage" :total="lists.total"
|
:current-page="queryParams.page" :per-page="lists.perPage" :total="lists.total"
|
||||||
|
backend-sorting @sort="onSort"
|
||||||
>
|
>
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column field="name" label="Name" sortable width="25%"
|
<b-table-column field="name" label="Name" sortable width="25%"
|
||||||
|
@ -51,16 +52,16 @@
|
||||||
</div>
|
</div>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="subscriberCount" label="Subscribers" numeric sortable centered>
|
<b-table-column field="subscriber_count" label="Subscribers" numeric sortable centered>
|
||||||
<router-link :to="`/subscribers/lists/${props.row.id}`">
|
<router-link :to="`/subscribers/lists/${props.row.id}`">
|
||||||
{{ props.row.subscriberCount }}
|
{{ props.row.subscriberCount }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="createdAt" label="Created" sortable>
|
<b-table-column field="created_at" label="Created" sortable>
|
||||||
{{ $utils.niceDate(props.row.createdAt) }}
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
<b-table-column field="updatedAt" label="Updated" sortable>
|
<b-table-column field="updated_at" label="Updated" sortable>
|
||||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
|
@ -115,7 +116,11 @@ export default Vue.extend({
|
||||||
curItem: null,
|
curItem: null,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
isFormVisible: false,
|
isFormVisible: false,
|
||||||
queryParams: { page: 1 },
|
queryParams: {
|
||||||
|
page: 1,
|
||||||
|
orderBy: 'created_at',
|
||||||
|
order: 'asc',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -125,6 +130,13 @@ export default Vue.extend({
|
||||||
this.getLists();
|
this.getLists();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSort(field, direction) {
|
||||||
|
this.queryParams.orderBy = field;
|
||||||
|
this.queryParams.order = direction;
|
||||||
|
this.getLists();
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
// Show the edit list form.
|
// Show the edit list form.
|
||||||
showEditForm(list) {
|
showEditForm(list) {
|
||||||
this.curItem = list;
|
this.curItem = list;
|
||||||
|
@ -144,7 +156,11 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
getLists() {
|
getLists() {
|
||||||
this.$api.getLists({ page: this.queryParams.page });
|
this.$api.getLists({
|
||||||
|
page: this.queryParams.page,
|
||||||
|
order_by: this.queryParams.orderBy,
|
||||||
|
order: this.queryParams.order,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteList(list) {
|
deleteList(list) {
|
||||||
|
|
|
@ -94,17 +94,16 @@
|
||||||
:checked-rows.sync="bulk.checked"
|
:checked-rows.sync="bulk.checked"
|
||||||
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
paginated backend-pagination pagination-position="both" @page-change="onPageChange"
|
||||||
:current-page="queryParams.page" :per-page="subscribers.perPage" :total="subscribers.total"
|
:current-page="queryParams.page" :per-page="subscribers.perPage" :total="subscribers.total"
|
||||||
hoverable
|
hoverable checkable backend-sorting @sort="onSort">
|
||||||
checkable>
|
|
||||||
<template slot-scope="props">
|
<template slot-scope="props">
|
||||||
<b-table-column field="status" label="Status">
|
<b-table-column field="status" label="Status" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
<b-tag :class="props.row.status">{{ props.row.status }}</b-tag>
|
<b-tag :class="props.row.status">{{ props.row.status }}</b-tag>
|
||||||
</a>
|
</a>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="email" label="E-mail">
|
<b-table-column field="email" label="E-mail" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
{{ props.row.email }}
|
{{ props.row.email }}
|
||||||
|
@ -119,7 +118,7 @@
|
||||||
</b-taglist>
|
</b-taglist>
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="name" label="Name">
|
<b-table-column field="name" label="Name" sortable>
|
||||||
<a :href="`/subscribers/${props.row.id}`"
|
<a :href="`/subscribers/${props.row.id}`"
|
||||||
@click.prevent="showEditForm(props.row)">
|
@click.prevent="showEditForm(props.row)">
|
||||||
{{ props.row.name }}
|
{{ props.row.name }}
|
||||||
|
@ -130,11 +129,11 @@
|
||||||
{{ listCount(props.row.lists) }}
|
{{ listCount(props.row.lists) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="createdAt" label="Created">
|
<b-table-column field="created_at" label="Created" sortable>
|
||||||
{{ $utils.niceDate(props.row.createdAt) }}
|
{{ $utils.niceDate(props.row.createdAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
<b-table-column field="updatedAt" label="Updated">
|
<b-table-column field="updated_at" label="Updated" sortable>
|
||||||
{{ $utils.niceDate(props.row.updatedAt) }}
|
{{ $utils.niceDate(props.row.updatedAt) }}
|
||||||
</b-table-column>
|
</b-table-column>
|
||||||
|
|
||||||
|
@ -217,6 +216,8 @@ export default Vue.extend({
|
||||||
// ID of the list the current subscriber view is filtered by.
|
// ID of the list the current subscriber view is filtered by.
|
||||||
listID: null,
|
listID: null,
|
||||||
page: 1,
|
page: 1,
|
||||||
|
orderBy: 'updated_at',
|
||||||
|
order: 'desc',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -279,15 +280,17 @@ export default Vue.extend({
|
||||||
this.isBulkListFormVisible = true;
|
this.isBulkListFormVisible = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
sortSubscribers(field, order, event) {
|
|
||||||
console.log(field, order, event);
|
|
||||||
},
|
|
||||||
|
|
||||||
onPageChange(p) {
|
onPageChange(p) {
|
||||||
this.queryParams.page = p;
|
this.queryParams.page = p;
|
||||||
this.querySubscribers();
|
this.querySubscribers();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onSort(field, direction) {
|
||||||
|
this.queryParams.orderBy = field;
|
||||||
|
this.queryParams.order = direction;
|
||||||
|
this.querySubscribers();
|
||||||
|
},
|
||||||
|
|
||||||
// Prepares an SQL expression for simple name search inputs and saves it
|
// Prepares an SQL expression for simple name search inputs and saves it
|
||||||
// in this.queryExp.
|
// in this.queryExp.
|
||||||
onSimpleQueryInput(v) {
|
onSimpleQueryInput(v) {
|
||||||
|
@ -308,6 +311,8 @@ export default Vue.extend({
|
||||||
list_id: this.queryParams.listID,
|
list_id: this.queryParams.listID,
|
||||||
query: this.queryParams.queryExp,
|
query: this.queryParams.queryExp,
|
||||||
page: this.queryParams.page,
|
page: this.queryParams.page,
|
||||||
|
order_by: this.queryParams.orderBy,
|
||||||
|
order: this.queryParams.order,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.bulk.checked = [];
|
this.bulk.checked = [];
|
||||||
});
|
});
|
||||||
|
|
20
queries.sql
20
queries.sql
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
-- subscribers
|
-- subscribers
|
||||||
-- name: get-subscriber
|
-- name: get-subscriber
|
||||||
-- Get a single subscriber by id or UUID.
|
-- Get a single subscriber by id or UUID.
|
||||||
|
@ -297,7 +298,7 @@ SELECT COUNT(*) OVER () AS total, lists.*, COUNT(subscriber_lists.subscriber_id)
|
||||||
FROM lists LEFT JOIN subscriber_lists
|
FROM lists LEFT JOIN subscriber_lists
|
||||||
ON (subscriber_lists.list_id = lists.id AND subscriber_lists.status != 'unsubscribed')
|
ON (subscriber_lists.list_id = lists.id AND subscriber_lists.status != 'unsubscribed')
|
||||||
WHERE ($1 = 0 OR id = $1)
|
WHERE ($1 = 0 OR id = $1)
|
||||||
GROUP BY lists.id ORDER BY lists.created_at OFFSET $2 LIMIT (CASE WHEN $3 = 0 THEN NULL ELSE $3 END);
|
GROUP BY lists.id ORDER BY %s %s OFFSET $2 LIMIT (CASE WHEN $3 = 0 THEN NULL ELSE $3 END);
|
||||||
|
|
||||||
-- name: get-lists-by-optin
|
-- name: get-lists-by-optin
|
||||||
-- Can have a list of IDs or a list of UUIDs.
|
-- Can have a list of IDs or a list of UUIDs.
|
||||||
|
@ -370,18 +371,23 @@ INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
-- there's a COUNT() OVER() that still returns the total result count
|
-- there's a COUNT() OVER() that still returns the total result count
|
||||||
-- for pagination in the frontend, albeit being a field that'll repeat
|
-- for pagination in the frontend, albeit being a field that'll repeat
|
||||||
-- with every resultant row.
|
-- with every resultant row.
|
||||||
SELECT COUNT(*) OVER () AS total, campaigns.*, (
|
SELECT campaigns.id, campaigns.uuid, campaigns.name, campaigns.subject, campaigns.from_email,
|
||||||
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
|
campaigns.messenger, campaigns.started_at, campaigns.to_send, campaigns.sent, campaigns.type,
|
||||||
SELECT COALESCE(campaign_lists.list_id, 0) AS id,
|
campaigns.body, campaigns.send_at, campaigns.status, campaigns.content_type, campaigns.tags,
|
||||||
campaign_lists.list_name AS name
|
campaigns.template_id, campaigns.created_at, campaigns.updated_at,
|
||||||
FROM campaign_lists WHERE campaign_lists.campaign_id = campaigns.id
|
COUNT(*) OVER () AS total,
|
||||||
|
(
|
||||||
|
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(l)), '[]') FROM (
|
||||||
|
SELECT COALESCE(campaign_lists.list_id, 0) AS id,
|
||||||
|
campaign_lists.list_name AS name
|
||||||
|
FROM campaign_lists WHERE campaign_lists.campaign_id = campaigns.id
|
||||||
) l
|
) l
|
||||||
) AS lists
|
) AS lists
|
||||||
FROM campaigns
|
FROM campaigns
|
||||||
WHERE ($1 = 0 OR id = $1)
|
WHERE ($1 = 0 OR id = $1)
|
||||||
AND status=ANY(CASE WHEN ARRAY_LENGTH($2::campaign_status[], 1) != 0 THEN $2::campaign_status[] ELSE ARRAY[status] END)
|
AND status=ANY(CASE WHEN ARRAY_LENGTH($2::campaign_status[], 1) != 0 THEN $2::campaign_status[] ELSE ARRAY[status] END)
|
||||||
AND ($3 = '' OR CONCAT(name, subject) ILIKE $3)
|
AND ($3 = '' OR CONCAT(name, subject) ILIKE $3)
|
||||||
ORDER BY campaigns.updated_at DESC OFFSET $4 LIMIT $5;
|
ORDER BY %s %s OFFSET $4 LIMIT $5;
|
||||||
|
|
||||||
-- name: get-campaign
|
-- name: get-campaign
|
||||||
SELECT campaigns.*,
|
SELECT campaigns.*,
|
||||||
|
|
Loading…
Reference in a new issue