Add a new Cursor form type
To hold the common logic of detecting pagination, forming the key, and splitting its values later on. I can take advantage that a form with action="get" already adds its fields to the query string to have a common template for pagination. The only problem is that i have different column spans for different tables, therefore had to add a colspan to the struct.
This commit is contained in:
parent
3a7d454826
commit
674cdff87b
|
@ -172,7 +172,7 @@ func collectBookingEntries(ctx context.Context, conn *database.Conn, lang langua
|
||||||
order by lower(stay) desc
|
order by lower(stay) desc
|
||||||
, booking_id desc
|
, booking_id desc
|
||||||
LIMIT %d
|
LIMIT %d
|
||||||
`, where, filters.perPage+1), args...)
|
`, where, filters.PerPage()+1), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,7 @@ func (page bookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user
|
||||||
}
|
}
|
||||||
ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
|
ods.MustWriteResponse(w, table, user.Locale.Pgettext("bookings.ods", "filename"))
|
||||||
default:
|
default:
|
||||||
if httplib.IsHTMxRequest(r) && page.Filters.pagination {
|
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||||
template.MustRenderAdminNoLayout(w, r, user, company, "booking/results.gohtml", page)
|
template.MustRenderAdminNoLayout(w, r, user, company, "booking/results.gohtml", page)
|
||||||
} else {
|
} else {
|
||||||
template.MustRenderAdminFiles(w, r, user, company, page, "booking/index.gohtml", "booking/results.gohtml")
|
template.MustRenderAdminFiles(w, r, user, company, page, "booking/index.gohtml", "booking/results.gohtml")
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
@ -13,22 +14,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type filterForm struct {
|
type filterForm struct {
|
||||||
locale *locale.Locale
|
|
||||||
company *auth.Company
|
company *auth.Company
|
||||||
perPage int
|
|
||||||
pagination bool
|
|
||||||
HolderName *form.Input
|
HolderName *form.Input
|
||||||
BookingStatus *form.Select
|
BookingStatus *form.Select
|
||||||
FromDate *form.Input
|
FromDate *form.Input
|
||||||
ToDate *form.Input
|
ToDate *form.Input
|
||||||
Cursor *form.Input
|
Cursor *form.Cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm {
|
func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Company, locale *locale.Locale) *filterForm {
|
||||||
return &filterForm{
|
return &filterForm{
|
||||||
locale: locale,
|
|
||||||
company: company,
|
company: company,
|
||||||
perPage: 25,
|
|
||||||
HolderName: &form.Input{
|
HolderName: &form.Input{
|
||||||
Name: "holder_name",
|
Name: "holder_name",
|
||||||
},
|
},
|
||||||
|
@ -42,8 +38,9 @@ func newFilterForm(ctx context.Context, conn *database.Conn, company *auth.Compa
|
||||||
ToDate: &form.Input{
|
ToDate: &form.Input{
|
||||||
Name: "to_date",
|
Name: "to_date",
|
||||||
},
|
},
|
||||||
Cursor: &form.Input{
|
Cursor: &form.Cursor{
|
||||||
Name: "cursor",
|
Name: "cursor",
|
||||||
|
PerPage: 25,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +65,6 @@ func (f *filterForm) Parse(r *http.Request) error {
|
||||||
f.FromDate.FillValue(r)
|
f.FromDate.FillValue(r)
|
||||||
f.ToDate.FillValue(r)
|
f.ToDate.FillValue(r)
|
||||||
f.Cursor.FillValue(r)
|
f.Cursor.FillValue(r)
|
||||||
f.pagination = f.Cursor.Val != ""
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,8 +96,8 @@ func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||||
maybeAppendWhere("lower(stay) >= $%d", f.FromDate.Val, nil)
|
maybeAppendWhere("lower(stay) >= $%d", f.FromDate.Val, nil)
|
||||||
maybeAppendWhere("lower(stay) <= $%d", f.ToDate.Val, nil)
|
maybeAppendWhere("lower(stay) <= $%d", f.ToDate.Val, nil)
|
||||||
|
|
||||||
if f.Cursor.Val != "" {
|
if f.Paginated() {
|
||||||
params := strings.Split(f.Cursor.Val, ";")
|
params := f.Cursor.Params()
|
||||||
if len(params) == 2 {
|
if len(params) == 2 {
|
||||||
where = append(where, fmt.Sprintf("(lower(stay), booking_id) < ($%d, $%d)", len(args)+1, len(args)+2))
|
where = append(where, fmt.Sprintf("(lower(stay), booking_id) < ($%d, $%d)", len(args)+1, len(args)+2))
|
||||||
args = append(args, params[0])
|
args = append(args, params[0])
|
||||||
|
@ -113,14 +109,9 @@ func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *filterForm) buildCursor(bookings []*bookingEntry) []*bookingEntry {
|
func (f *filterForm) buildCursor(bookings []*bookingEntry) []*bookingEntry {
|
||||||
if len(bookings) <= f.perPage {
|
return form.BuildCursor(f.Cursor, bookings, func(entry *bookingEntry) []string {
|
||||||
f.Cursor.Val = ""
|
return []string{entry.ArrivalDate.Format(database.ISODateFormat), strconv.Itoa(entry.ID)}
|
||||||
return bookings
|
})
|
||||||
}
|
|
||||||
bookings = bookings[:f.perPage]
|
|
||||||
last := bookings[f.perPage-1]
|
|
||||||
f.Cursor.Val = fmt.Sprintf("%s;%d", last.ArrivalDate.Format(database.ISODateFormat), last.ID)
|
|
||||||
return bookings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *filterForm) HasValue() bool {
|
func (f *filterForm) HasValue() bool {
|
||||||
|
@ -129,3 +120,11 @@ func (f *filterForm) HasValue() bool {
|
||||||
f.FromDate.Val != "" ||
|
f.FromDate.Val != "" ||
|
||||||
f.ToDate.Val != ""
|
f.ToDate.Val != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *filterForm) PerPage() int {
|
||||||
|
return f.Cursor.PerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filterForm) Paginated() bool {
|
||||||
|
return f.Cursor.Pagination
|
||||||
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ type prebookingIndex struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (page prebookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func (page prebookingIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
if httplib.IsHTMxRequest(r) && page.Filters.pagination {
|
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||||
template.MustRenderAdminNoLayout(w, r, user, company, "prebooking/results.gohtml", page)
|
template.MustRenderAdminNoLayout(w, r, user, company, "prebooking/results.gohtml", page)
|
||||||
} else {
|
} else {
|
||||||
template.MustRenderAdminFiles(w, r, user, company, page, "prebooking/index.gohtml", "prebooking/results.gohtml")
|
template.MustRenderAdminFiles(w, r, user, company, page, "prebooking/index.gohtml", "prebooking/results.gohtml")
|
||||||
|
|
|
@ -109,7 +109,7 @@ func collectCustomerEntries(ctx context.Context, conn *database.Conn, company *a
|
||||||
where (%s)
|
where (%s)
|
||||||
order by name, contact_id
|
order by name, contact_id
|
||||||
LIMIT %d
|
LIMIT %d
|
||||||
`, where, filters.perPage+1), args...)
|
`, where, filters.PerPage()+1), args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,7 @@ type customerIndex struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func (page *customerIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
if httplib.IsHTMxRequest(r) && page.Filters.pagination {
|
if httplib.IsHTMxRequest(r) && page.Filters.Paginated() {
|
||||||
template.MustRenderAdminNoLayout(w, r, user, company, "customer/results.gohtml", page)
|
template.MustRenderAdminNoLayout(w, r, user, company, "customer/results.gohtml", page)
|
||||||
} else {
|
} else {
|
||||||
template.MustRenderAdminFiles(w, r, user, company, page, "customer/index.gohtml", "customer/results.gohtml")
|
template.MustRenderAdminFiles(w, r, user, company, page, "customer/index.gohtml", "customer/results.gohtml")
|
||||||
|
|
|
@ -3,6 +3,7 @@ package customer
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
|
@ -11,25 +12,23 @@ import (
|
||||||
|
|
||||||
type filterForm struct {
|
type filterForm struct {
|
||||||
company *auth.Company
|
company *auth.Company
|
||||||
perPage int
|
|
||||||
pagination bool
|
|
||||||
Name *form.Input
|
Name *form.Input
|
||||||
Email *form.Input
|
Email *form.Input
|
||||||
Cursor *form.Input
|
Cursor *form.Cursor
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFilterForm(company *auth.Company) *filterForm {
|
func newFilterForm(company *auth.Company) *filterForm {
|
||||||
return &filterForm{
|
return &filterForm{
|
||||||
company: company,
|
company: company,
|
||||||
perPage: 25,
|
|
||||||
Name: &form.Input{
|
Name: &form.Input{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
},
|
},
|
||||||
Email: &form.Input{
|
Email: &form.Input{
|
||||||
Name: "email",
|
Name: "email",
|
||||||
},
|
},
|
||||||
Cursor: &form.Input{
|
Cursor: &form.Cursor{
|
||||||
Name: "cursor",
|
Name: "cursor",
|
||||||
|
PerPage: 25,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +40,6 @@ func (f *filterForm) Parse(r *http.Request) error {
|
||||||
f.Name.FillValue(r)
|
f.Name.FillValue(r)
|
||||||
f.Email.FillValue(r)
|
f.Email.FillValue(r)
|
||||||
f.Cursor.FillValue(r)
|
f.Cursor.FillValue(r)
|
||||||
f.pagination = f.Cursor.Val != ""
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,8 +67,8 @@ func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||||
return "%" + v + "%"
|
return "%" + v + "%"
|
||||||
})
|
})
|
||||||
|
|
||||||
if f.Cursor.Val != "" {
|
if f.Paginated() {
|
||||||
params := strings.Split(f.Cursor.Val, ";")
|
params := f.Cursor.Params()
|
||||||
if len(params) == 2 {
|
if len(params) == 2 {
|
||||||
where = append(where, fmt.Sprintf("(name, contact_id) > ($%d, $%d)", len(args)+1, len(args)+2))
|
where = append(where, fmt.Sprintf("(name, contact_id) > ($%d, $%d)", len(args)+1, len(args)+2))
|
||||||
args = append(args, params[0])
|
args = append(args, params[0])
|
||||||
|
@ -82,17 +80,20 @@ func (f *filterForm) BuildQuery(args []interface{}) (string, []interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *filterForm) buildCursor(customers []*customerEntry) []*customerEntry {
|
func (f *filterForm) buildCursor(customers []*customerEntry) []*customerEntry {
|
||||||
if len(customers) <= f.perPage {
|
return form.BuildCursor(f.Cursor, customers, func(entry *customerEntry) []string {
|
||||||
f.Cursor.Val = ""
|
return []string{entry.Name, strconv.Itoa(entry.ID)}
|
||||||
return customers
|
})
|
||||||
}
|
|
||||||
customers = customers[:f.perPage]
|
|
||||||
last := customers[f.perPage-1]
|
|
||||||
f.Cursor.Val = fmt.Sprintf("%s;%d", last.Name, last.ID)
|
|
||||||
return customers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *filterForm) HasValue() bool {
|
func (f *filterForm) HasValue() bool {
|
||||||
return f.Name.Val != "" ||
|
return f.Name.Val != "" ||
|
||||||
f.Email.Val != ""
|
f.Email.Val != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *filterForm) PerPage() int {
|
||||||
|
return f.Cursor.PerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filterForm) Paginated() bool {
|
||||||
|
return f.Cursor.Pagination
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package form
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cursor struct {
|
||||||
|
PerPage int
|
||||||
|
Pagination bool
|
||||||
|
Name string
|
||||||
|
Val string
|
||||||
|
Colspan int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cursor *Cursor) FillValue(r *http.Request) {
|
||||||
|
cursor.Val = strings.TrimSpace(r.FormValue(cursor.Name))
|
||||||
|
cursor.Pagination = cursor.Val != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cursor *Cursor) Params() []string {
|
||||||
|
return strings.Split(cursor.Val, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildCursor[K interface{}](cursor *Cursor, elems []K, build func(K) []string) []K {
|
||||||
|
if len(elems) <= cursor.PerPage {
|
||||||
|
cursor.Val = ""
|
||||||
|
return elems
|
||||||
|
}
|
||||||
|
elems = elems[:cursor.PerPage]
|
||||||
|
cursor.Val = strings.Join(build(elems[cursor.PerPage-1]), ";")
|
||||||
|
return elems
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/build"
|
"dev.tandem.ws/tandem/camper/pkg/build"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -161,6 +162,10 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
||||||
return int(num)
|
return int(num)
|
||||||
},
|
},
|
||||||
"slugify": Slugify,
|
"slugify": Slugify,
|
||||||
|
"colspan": func(colspan int, cursor *form.Cursor) *form.Cursor {
|
||||||
|
cursor.Colspan = colspan
|
||||||
|
return cursor
|
||||||
|
},
|
||||||
})
|
})
|
||||||
templates = append(templates, "form.gohtml")
|
templates = append(templates, "form.gohtml")
|
||||||
files := make([]string, len(templates))
|
files := make([]string, len(templates))
|
||||||
|
|
|
@ -7,22 +7,4 @@
|
||||||
<td class="booking-status">{{ .StatusLabel }}</td>
|
<td class="booking-status">{{ .StatusLabel }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ if .Filters.Cursor.Val }}
|
{{ template "pagination" .Filters.Cursor | colspan 5 }}
|
||||||
<tr>
|
|
||||||
<td colspan="5">
|
|
||||||
{{ with .Filters -}}
|
|
||||||
<form data-hx-get="/admin/bookings" data-hx-target="closest tr" data-hx-swap="outerHTML">
|
|
||||||
{{ with .HolderName -}}<input type="hidden" name="{{ .Name }}"
|
|
||||||
value="{{ .Val }}">{{- end }}
|
|
||||||
{{ with .BookingStatus -}}<input type="hidden" name="{{ .Name }}"
|
|
||||||
value="{{ .String }}">{{- end }}
|
|
||||||
{{ with .FromDate -}}<input type="hidden" name="{{ .Name }}"
|
|
||||||
value="{{ .Val }}">{{- end }}
|
|
||||||
{{ with .ToDate -}}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{- end }}
|
|
||||||
{{ with .Cursor -}}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{- end }}
|
|
||||||
<button type="submit">{{( pgettext "Load more" "action" )}}</button>
|
|
||||||
</form>
|
|
||||||
{{- end }}
|
|
||||||
<td>
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
|
|
|
@ -5,15 +5,4 @@
|
||||||
<td>{{ .Phone }}</td>
|
<td>{{ .Phone }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{ if .Filters.Cursor.Val }}
|
{{ template "pagination" .Filters.Cursor | colspan 3 }}
|
||||||
<tr>
|
|
||||||
<td colspan="3">
|
|
||||||
{{ with .Filters -}}
|
|
||||||
<form method="get" data-hx-push-url="false" data-hx-boost="true" data-hx-target="closest tr" data-hx-swap="outerHTML">
|
|
||||||
{{ with .Cursor -}}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{- end }}
|
|
||||||
<button type="submit">{{( pgettext "Load more" "action" )}}</button>
|
|
||||||
</form>
|
|
||||||
{{- end }}
|
|
||||||
<td>
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
|
|
|
@ -82,3 +82,17 @@
|
||||||
@click="document.body.classList.toggle('filters-visible')"
|
@click="document.body.classList.toggle('filters-visible')"
|
||||||
type="button">{{(pgettext "Filters" "action")}}</button>
|
type="button">{{(pgettext "Filters" "action")}}</button>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
{{ define "pagination" -}}
|
||||||
|
{{ if .Val }}
|
||||||
|
<tr>
|
||||||
|
<td colspan="{{ .Colspan }}">
|
||||||
|
<form method="get" data-hx-push-url="false" data-hx-boost="true" data-hx-target="closest tr"
|
||||||
|
data-hx-swap="outerHTML">
|
||||||
|
<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">
|
||||||
|
<button type="submit">{{( pgettext "Load more" "action" )}}</button>
|
||||||
|
</form>
|
||||||
|
<td>
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue