Merge pull request 'Add pagination and filters to ticket tables' (#63) from pagination-and-filtering into main
Reviewed-on: https://git.ts.mattnite.net/mattnite/forgejo-tickets/pulls/63
This commit is contained in:
commit
51eeb4c9c1
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -35,6 +36,15 @@ type ticketListRow struct {
|
|||
|
||||
func (h *TicketHandler) List(c *gin.Context) {
|
||||
statusFilter := c.Query("status")
|
||||
priorityFilter := c.Query("priority")
|
||||
productFilter := c.Query("product")
|
||||
reporterFilter := c.Query("reporter")
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
const pageSize = 20
|
||||
|
||||
// Load all ticket mappings with User and Repo joins
|
||||
type ticketMapping struct {
|
||||
|
|
@ -53,7 +63,6 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
Joins("JOIN repos ON repos.id = tickets.repo_id").
|
||||
Joins("JOIN users ON users.id = tickets.user_id").
|
||||
Order("tickets.created_at DESC").
|
||||
Limit(100).
|
||||
Scan(&mappings).Error; err != nil {
|
||||
log.Error().Err(err).Msg("list tickets error")
|
||||
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load tickets")
|
||||
|
|
@ -83,7 +92,7 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
apiState = "open"
|
||||
}
|
||||
|
||||
// Fetch issues from Forgejo per repo and match
|
||||
// Fetch issues from Forgejo per repo and build all rows
|
||||
var tickets []ticketListRow
|
||||
for _, group := range repoGroups {
|
||||
issues, err := h.deps.ForgejoClient.ListIssues(group.owner, group.repo, apiState, "")
|
||||
|
|
@ -114,17 +123,11 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
status := forgejo.DeriveStatus(issue)
|
||||
|
||||
// Apply client-side status filter
|
||||
if statusFilter != "" && status != statusFilter {
|
||||
continue
|
||||
}
|
||||
|
||||
tickets = append(tickets, ticketListRow{
|
||||
ID: m.ID,
|
||||
Title: issue.Title,
|
||||
Status: status,
|
||||
Status: forgejo.DeriveStatus(issue),
|
||||
Priority: forgejo.DerivePriority(issue),
|
||||
Pinned: issue.PinOrder > 0,
|
||||
RepoName: m.RepoName,
|
||||
|
|
@ -137,6 +140,47 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Collect unique product names and reporter names before filtering
|
||||
productSet := map[string]bool{}
|
||||
reporterSet := map[string]bool{}
|
||||
for _, t := range tickets {
|
||||
if t.RepoName != "" {
|
||||
productSet[t.RepoName] = true
|
||||
}
|
||||
if t.UserName != "" {
|
||||
reporterSet[t.UserName] = true
|
||||
}
|
||||
}
|
||||
var products []string
|
||||
for p := range productSet {
|
||||
products = append(products, p)
|
||||
}
|
||||
sort.Strings(products)
|
||||
var reporters []string
|
||||
for r := range reporterSet {
|
||||
reporters = append(reporters, r)
|
||||
}
|
||||
sort.Strings(reporters)
|
||||
|
||||
// Apply in-memory filters
|
||||
filtered := tickets[:0]
|
||||
for _, t := range tickets {
|
||||
if statusFilter != "" && t.Status != statusFilter {
|
||||
continue
|
||||
}
|
||||
if priorityFilter != "" && t.Priority != priorityFilter {
|
||||
continue
|
||||
}
|
||||
if productFilter != "" && t.RepoName != productFilter {
|
||||
continue
|
||||
}
|
||||
if reporterFilter != "" && t.UserName != reporterFilter {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, t)
|
||||
}
|
||||
tickets = filtered
|
||||
|
||||
// Sort: pinned first, then by created date
|
||||
sort.SliceStable(tickets, func(i, j int) bool {
|
||||
if tickets[i].Pinned != tickets[j].Pinned {
|
||||
|
|
@ -145,9 +189,31 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
return false
|
||||
})
|
||||
|
||||
// Paginate
|
||||
totalPages := (len(tickets) + pageSize - 1) / pageSize
|
||||
if totalPages < 1 {
|
||||
totalPages = 1
|
||||
}
|
||||
if page > totalPages {
|
||||
page = totalPages
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if end > len(tickets) {
|
||||
end = len(tickets)
|
||||
}
|
||||
tickets = tickets[start:end]
|
||||
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "admin/tickets/list", map[string]interface{}{
|
||||
"Tickets": tickets,
|
||||
"StatusFilter": statusFilter,
|
||||
"PriorityFilter": priorityFilter,
|
||||
"ProductFilter": productFilter,
|
||||
"ReporterFilter": reporterFilter,
|
||||
"Products": products,
|
||||
"Reporters": reporters,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,18 @@ type TicketHandler struct {
|
|||
func (h *TicketHandler) List(c *gin.Context) {
|
||||
user := auth.CurrentUser(c)
|
||||
|
||||
statusFilter := c.Query("status")
|
||||
priorityFilter := c.Query("priority")
|
||||
productFilter := c.Query("product")
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
const pageSize = 20
|
||||
|
||||
var tickets []models.Ticket
|
||||
if err := h.deps.DB.Preload("Repo").Where("user_id = ?", user.ID).Order("created_at DESC").Limit(50).Find(&tickets).Error; err != nil {
|
||||
if err := h.deps.DB.Preload("Repo").Where("user_id = ?", user.ID).Order("created_at DESC").Find(&tickets).Error; err != nil {
|
||||
log.Error().Err(err).Msg("list tickets error")
|
||||
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load tickets")
|
||||
return
|
||||
|
|
@ -35,6 +45,9 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
if len(tickets) == 0 {
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "tickets/list", map[string]interface{}{
|
||||
"Tickets": []map[string]interface{}{},
|
||||
"StatusFilter": statusFilter,
|
||||
"PriorityFilter": priorityFilter,
|
||||
"ProductFilter": productFilter,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -52,6 +65,14 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
repoMap[t.RepoID].tickets = append(repoMap[t.RepoID].tickets, t)
|
||||
}
|
||||
|
||||
// Determine Forgejo API state filter
|
||||
apiState := "all"
|
||||
if statusFilter == "closed" {
|
||||
apiState = "closed"
|
||||
} else if statusFilter == "open" || statusFilter == "in_progress" {
|
||||
apiState = "open"
|
||||
}
|
||||
|
||||
// Fetch issues from Forgejo per repo and build view models
|
||||
type ticketView struct {
|
||||
ID uuid.UUID
|
||||
|
|
@ -66,7 +87,7 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
var views []ticketView
|
||||
|
||||
for _, group := range repoMap {
|
||||
issues, err := h.deps.ForgejoClient.ListIssues(group.repo.ForgejoOwner, group.repo.ForgejoRepo, "all", "")
|
||||
issues, err := h.deps.ForgejoClient.ListIssues(group.repo.ForgejoOwner, group.repo.ForgejoRepo, apiState, "")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("repo", group.repo.Name).Msg("forgejo list issues error")
|
||||
// Show tickets with unknown status on API failure
|
||||
|
|
@ -103,6 +124,35 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Collect unique product names before filtering
|
||||
productSet := map[string]bool{}
|
||||
for _, v := range views {
|
||||
if v.RepoName != "" {
|
||||
productSet[v.RepoName] = true
|
||||
}
|
||||
}
|
||||
var products []string
|
||||
for p := range productSet {
|
||||
products = append(products, p)
|
||||
}
|
||||
sort.Strings(products)
|
||||
|
||||
// Apply in-memory filters
|
||||
filtered := views[:0]
|
||||
for _, v := range views {
|
||||
if statusFilter != "" && v.Status != statusFilter {
|
||||
continue
|
||||
}
|
||||
if priorityFilter != "" && v.Priority != priorityFilter {
|
||||
continue
|
||||
}
|
||||
if productFilter != "" && v.RepoName != productFilter {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, v)
|
||||
}
|
||||
views = filtered
|
||||
|
||||
// Sort: pinned first, then by created date
|
||||
sort.SliceStable(views, func(i, j int) bool {
|
||||
if views[i].Pinned != views[j].Pinned {
|
||||
|
|
@ -111,8 +161,29 @@ func (h *TicketHandler) List(c *gin.Context) {
|
|||
return false // preserve existing order for non-pinned
|
||||
})
|
||||
|
||||
// Paginate
|
||||
totalPages := (len(views) + pageSize - 1) / pageSize
|
||||
if totalPages < 1 {
|
||||
totalPages = 1
|
||||
}
|
||||
if page > totalPages {
|
||||
page = totalPages
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if end > len(views) {
|
||||
end = len(views)
|
||||
}
|
||||
views = views[start:end]
|
||||
|
||||
h.deps.Renderer.Render(c.Writer, c.Request, "tickets/list", map[string]interface{}{
|
||||
"Tickets": views,
|
||||
"StatusFilter": statusFilter,
|
||||
"PriorityFilter": priorityFilter,
|
||||
"ProductFilter": productFilter,
|
||||
"Products": products,
|
||||
"Page": page,
|
||||
"TotalPages": totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package templates
|
|||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -87,5 +88,48 @@ func templateFuncs() template.FuncMap {
|
|||
}
|
||||
return dict
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"sub": func(a, b int) int {
|
||||
return a - b
|
||||
},
|
||||
"filterURL": func(baseURL string, params map[string]interface{}, setKey, setValue string) string {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return baseURL
|
||||
}
|
||||
q := url.Values{}
|
||||
for k, v := range params {
|
||||
if k == "page" {
|
||||
continue
|
||||
}
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
q.Set(k, s)
|
||||
}
|
||||
}
|
||||
if setValue != "" {
|
||||
q.Set(setKey, setValue)
|
||||
} else {
|
||||
q.Del(setKey)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
},
|
||||
"pageURL": func(baseURL string, params map[string]interface{}, page int) string {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return baseURL
|
||||
}
|
||||
q := u.Query()
|
||||
for k, v := range params {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
q.Set(k, s)
|
||||
}
|
||||
}
|
||||
q.Set("page", fmt.Sprintf("%d", page))
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,40 @@
|
|||
<h1 class="text-2xl font-bold text-gray-900 mb-6">All Tickets</h1>
|
||||
|
||||
{{with .Data}}
|
||||
<div class="mb-4 flex gap-2">
|
||||
<a href="/admin/tickets" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .StatusFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
<a href="/admin/tickets?status=open" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "open"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Open</a>
|
||||
<a href="/admin/tickets?status=in_progress" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "in_progress"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">In Progress</a>
|
||||
<a href="/admin/tickets?status=closed" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "closed"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Closed</a>
|
||||
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter "reporter" .ReporterFilter}}
|
||||
<div class="mb-4 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "status" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .StatusFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "status" "open"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "open"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Open</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "status" "in_progress"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "in_progress"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">In Progress</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "status" "closed"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "closed"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Closed</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Priority</span>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "priority" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .PriorityFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "priority" "high"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "high"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">High</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "priority" "medium"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "medium"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Medium</a>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "priority" "low"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "low"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Low</a>
|
||||
</div>
|
||||
{{if .Products}}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
{{range .Products}}
|
||||
<a href="{{filterURL "/admin/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ProductFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Reporters}}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Reporter</span>
|
||||
<a href="{{filterURL "/admin/tickets" $filters "reporter" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ReporterFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
{{range .Reporters}}
|
||||
<a href="{{filterURL "/admin/tickets" $filters "reporter" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ReporterFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Tickets}}
|
||||
|
|
@ -45,8 +74,27 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{if gt .TotalPages 1}}
|
||||
<nav class="flex items-center justify-between mt-4 px-1">
|
||||
<div>
|
||||
{{if gt .Page 1}}
|
||||
<a href="{{pageURL "/admin/tickets" $filters (sub .Page 1)}}" class="rounded-md px-3 py-1.5 text-sm font-medium bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50">Previous</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">Page {{.Page}} of {{.TotalPages}}</span>
|
||||
<div>
|
||||
{{if lt .Page .TotalPages}}
|
||||
<a href="{{pageURL "/admin/tickets" $filters (add .Page 1)}}" class="rounded-md px-3 py-1.5 text-sm font-medium bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50">Next</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
{{else}}
|
||||
{{if or .StatusFilter .PriorityFilter .ProductFilter .ReporterFilter}}
|
||||
<p class="text-sm text-gray-500">No tickets match the current filters.</p>
|
||||
{{else}}
|
||||
<p class="text-sm text-gray-500">No tickets found.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,33 @@
|
|||
</div>
|
||||
|
||||
{{with .Data}}
|
||||
{{$filters := dict "status" .StatusFilter "priority" .PriorityFilter "product" .ProductFilter}}
|
||||
<div class="mb-4 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Status</span>
|
||||
<a href="{{filterURL "/tickets" $filters "status" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .StatusFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
<a href="{{filterURL "/tickets" $filters "status" "open"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "open"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Open</a>
|
||||
<a href="{{filterURL "/tickets" $filters "status" "in_progress"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "in_progress"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">In Progress</a>
|
||||
<a href="{{filterURL "/tickets" $filters "status" "closed"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .StatusFilter "closed"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Closed</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Priority</span>
|
||||
<a href="{{filterURL "/tickets" $filters "priority" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .PriorityFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
<a href="{{filterURL "/tickets" $filters "priority" "high"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "high"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">High</a>
|
||||
<a href="{{filterURL "/tickets" $filters "priority" "medium"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "medium"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Medium</a>
|
||||
<a href="{{filterURL "/tickets" $filters "priority" "low"}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq .PriorityFilter "low"}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">Low</a>
|
||||
</div>
|
||||
{{if .Products}}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase w-16">Product</span>
|
||||
<a href="{{filterURL "/tickets" $filters "product" ""}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if not .ProductFilter}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">All</a>
|
||||
{{range .Products}}
|
||||
<a href="{{filterURL "/tickets" $filters "product" .}}" class="rounded-md px-3 py-1.5 text-sm font-medium {{if eq $.ProductFilter .}}bg-gray-900 text-white{{else}}bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50{{end}}">{{.}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Tickets}}
|
||||
<div class="overflow-hidden bg-white shadow ring-1 ring-gray-200 rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
|
|
@ -39,10 +66,29 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{if gt .TotalPages 1}}
|
||||
<nav class="flex items-center justify-between mt-4 px-1">
|
||||
<div>
|
||||
{{if gt .Page 1}}
|
||||
<a href="{{pageURL "/tickets" $filters (sub .Page 1)}}" class="rounded-md px-3 py-1.5 text-sm font-medium bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50">Previous</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">Page {{.Page}} of {{.TotalPages}}</span>
|
||||
<div>
|
||||
{{if lt .Page .TotalPages}}
|
||||
<a href="{{pageURL "/tickets" $filters (add .Page 1)}}" class="rounded-md px-3 py-1.5 text-sm font-medium bg-white text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50">Next</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="text-center py-12 bg-white rounded-lg shadow ring-1 ring-gray-200">
|
||||
{{if or .StatusFilter .PriorityFilter .ProductFilter}}
|
||||
<p class="text-gray-500">No tickets match the current filters.</p>
|
||||
{{else}}
|
||||
<p class="text-gray-500">No tickets yet.</p>
|
||||
<a href="/tickets/new" class="mt-4 inline-block text-sm font-medium text-blue-600 hover:text-blue-500">Create your first ticket</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue