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:
jordi fita mas 2024-05-03 19:00:02 +02:00
parent 3a7d454826
commit 674cdff87b
10 changed files with 98 additions and 75 deletions

View File

@ -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")

View File

@ -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
}

View File

@ -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")

View File

@ -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")

View File

@ -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"
@ -10,26 +11,24 @@ import (
) )
type filterForm struct { type filterForm struct {
company *auth.Company company *auth.Company
perPage int Name *form.Input
pagination bool Email *form.Input
Name *form.Input Cursor *form.Cursor
Email *form.Input
Cursor *form.Input
} }
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
}

33
pkg/form/cursor.go Normal file
View File

@ -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
}

View File

@ -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))

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}