Add multiselect board filters and letter-jump on picker menus.
Persist comma-separated list filters per project, toggle with space, and jump by first letter in board and project pickers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
package ticket
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// BoardList is a project-scoped view/filter over tickets by status.
|
||||
type BoardList string
|
||||
|
||||
@@ -29,6 +34,15 @@ func (b BoardList) Label() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut returns the board-menu hotkey (first letter of the label).
|
||||
func (b BoardList) Shortcut() rune {
|
||||
label := b.Label()
|
||||
if label == "" {
|
||||
return 0
|
||||
}
|
||||
return unicode.ToLower([]rune(label)[0])
|
||||
}
|
||||
|
||||
// Index returns the position in AllBoardLists.
|
||||
func (b BoardList) Index() int {
|
||||
for i, bl := range AllBoardLists {
|
||||
@@ -50,6 +64,114 @@ func ParseBoardList(s string) BoardList {
|
||||
}
|
||||
}
|
||||
|
||||
// BoardFilter is a multi-select set of board lists (OR semantics).
|
||||
type BoardFilter []BoardList
|
||||
|
||||
// ParseBoardLists parses a comma-separated list id string (legacy single values work).
|
||||
func ParseBoardLists(s string) BoardFilter {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return BoardFilter{BoardAll}
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
var out BoardFilter
|
||||
seen := map[BoardList]bool{}
|
||||
for _, p := range parts {
|
||||
bl := ParseBoardList(strings.TrimSpace(p))
|
||||
if seen[bl] {
|
||||
continue
|
||||
}
|
||||
seen[bl] = true
|
||||
out = append(out, bl)
|
||||
}
|
||||
return NormalizeFilter(out)
|
||||
}
|
||||
|
||||
// FormatBoardLists serializes a filter for config persistence.
|
||||
func FormatBoardLists(f BoardFilter) string {
|
||||
f = NormalizeFilter(f)
|
||||
parts := make([]string, len(f))
|
||||
for i, bl := range f {
|
||||
parts[i] = string(bl)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// NormalizeFilter ensures a non-empty filter; All alone if empty or only All.
|
||||
// If All is combined with others, All is dropped.
|
||||
func NormalizeFilter(f BoardFilter) BoardFilter {
|
||||
if len(f) == 0 {
|
||||
return BoardFilter{BoardAll}
|
||||
}
|
||||
hasAll := false
|
||||
var others BoardFilter
|
||||
seen := map[BoardList]bool{}
|
||||
for _, bl := range f {
|
||||
if bl == BoardAll {
|
||||
hasAll = true
|
||||
continue
|
||||
}
|
||||
if seen[bl] {
|
||||
continue
|
||||
}
|
||||
seen[bl] = true
|
||||
others = append(others, bl)
|
||||
}
|
||||
if len(others) > 0 {
|
||||
return others
|
||||
}
|
||||
if hasAll {
|
||||
return BoardFilter{BoardAll}
|
||||
}
|
||||
return BoardFilter{BoardAll}
|
||||
}
|
||||
|
||||
// Contains reports whether bl is in the filter.
|
||||
func (f BoardFilter) Contains(bl BoardList) bool {
|
||||
for _, x := range f {
|
||||
if x == bl {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Label returns a short header label for the active filter.
|
||||
func (f BoardFilter) Label() string {
|
||||
f = NormalizeFilter(f)
|
||||
if len(f) == 1 {
|
||||
return f[0].Label()
|
||||
}
|
||||
parts := make([]string, len(f))
|
||||
for i, bl := range f {
|
||||
parts[i] = bl.Label()
|
||||
}
|
||||
return strings.Join(parts, "+")
|
||||
}
|
||||
|
||||
// Matches reports whether a ticket status is visible under this filter.
|
||||
func (f BoardFilter) Matches(status Status) bool {
|
||||
f = NormalizeFilter(f)
|
||||
if f.Contains(BoardAll) {
|
||||
return true
|
||||
}
|
||||
for _, bl := range f {
|
||||
if bl.Matches(status) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DefaultStatus is used when creating a ticket under this filter.
|
||||
func (f BoardFilter) DefaultStatus() Status {
|
||||
f = NormalizeFilter(f)
|
||||
if len(f) == 1 {
|
||||
return f[0].DefaultStatus()
|
||||
}
|
||||
return StatusTodo
|
||||
}
|
||||
|
||||
// Matches reports whether a ticket belongs on this board list.
|
||||
func (b BoardList) Matches(status Status) bool {
|
||||
switch b {
|
||||
|
||||
Reference in New Issue
Block a user