Merge remote-tracking branch 'origin/main' into LPM-110
This commit is contained in:
@@ -6,18 +6,18 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"charm.land/bubbles/v2/list"
|
||||
"charm.land/bubbles/v2/textarea"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/LazyBachelor/LazyPM/internal/app"
|
||||
"github.com/LazyBachelor/LazyPM/internal/models"
|
||||
"github.com/LazyBachelor/LazyPM/internal/style"
|
||||
"github.com/LazyBachelor/LazyPM/pkg/tui/styles"
|
||||
"github.com/charmbracelet/bubbles/list"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
)
|
||||
|
||||
|
||||
type IssueList struct {
|
||||
list list.Model
|
||||
app *app.App
|
||||
@@ -26,7 +26,6 @@ type IssueList struct {
|
||||
highlightSelected bool
|
||||
}
|
||||
|
||||
|
||||
type ListIssue struct {
|
||||
models.Issue
|
||||
}
|
||||
@@ -43,29 +42,54 @@ type tableColumn struct {
|
||||
|
||||
func getTableColumns(width int) []tableColumn {
|
||||
switch {
|
||||
case width < 20:
|
||||
return []tableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
}
|
||||
case width < 30:
|
||||
return []tableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: 10, label: "TITLE", key: "title"},
|
||||
}
|
||||
case width < 45:
|
||||
return []tableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: uint(width - 10), label: "TITLE", key: "title"},
|
||||
{width: 20, label: "TITLE", key: "title"},
|
||||
}
|
||||
case width < 60:
|
||||
return []tableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: 20, label: "TITLE", key: "title"},
|
||||
{width: 22, label: "TITLE", key: "title"},
|
||||
{width: 15, label: "STATUS", key: "status"},
|
||||
}
|
||||
case width < 75:
|
||||
return []tableColumn{
|
||||
{width: 10, label: "ID", key: "id"},
|
||||
{width: 25, label: "TITLE", key: "title"},
|
||||
{width: 15, label: "STATUS", key: "status"},
|
||||
{width: 10, label: "TYPE", key: "type"},
|
||||
}
|
||||
case width < 95:
|
||||
return []tableColumn{
|
||||
{width: 12, label: "ID", key: "id"},
|
||||
{width: 20, label: "TITLE", key: "title"},
|
||||
{width: 25, label: "TITLE", key: "title"},
|
||||
{width: 15, label: "STATUS", key: "status"},
|
||||
{width: 10, label: "TYPE", key: "type"},
|
||||
{width: 15, label: "PRIORITY", key: "priority"},
|
||||
}
|
||||
case width < 120:
|
||||
return []tableColumn{
|
||||
{width: 12, label: "ID", key: "id"},
|
||||
{width: 30, label: "TITLE", key: "title"},
|
||||
{width: 15, label: "STATUS", key: "status"},
|
||||
{width: 11, label: "TYPE", key: "type"},
|
||||
{width: 14, label: "PRIORITY", key: "priority"},
|
||||
{width: 12, label: "ASSIGNEE", key: "assignee"},
|
||||
}
|
||||
default:
|
||||
return []tableColumn{
|
||||
{width: 12, label: "ID", key: "id"},
|
||||
{width: 20, label: "TITLE", key: "title"},
|
||||
{width: 40, label: "TITLE", key: "title"},
|
||||
{width: 15, label: "STATUS", key: "status"},
|
||||
{width: 11, label: "TYPE", key: "type"},
|
||||
{width: 14, label: "PRIORITY", key: "priority"},
|
||||
@@ -93,7 +117,7 @@ var priorityCodeNames = map[int]string{
|
||||
|
||||
func renderHeaders(cols []tableColumn) string {
|
||||
var parts []string
|
||||
headerStyle := lipgloss.NewStyle().Foreground(styles.FaintText).Bold(true)
|
||||
headerStyle := lipgloss.NewStyle().Foreground(style.FaintText).Bold(true)
|
||||
|
||||
for _, col := range cols {
|
||||
colWidth := col.width
|
||||
@@ -157,7 +181,6 @@ func ListenForValidation(ch chan models.ValidationFeedback) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func NewIssueList(app *app.App, width, height int) IssueList {
|
||||
// NewIssueList creates an IssueList populated from the app.
|
||||
issues, err := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{})
|
||||
@@ -180,10 +203,6 @@ func NewIssueList(app *app.App, width, height int) IssueList {
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.FilterInput.PromptStyle = styles.FilterPromptStyle
|
||||
l.FilterInput.Cursor.Style = styles.FilterStyle
|
||||
l.FilterInput.TextStyle = styles.FilterInputStyle
|
||||
l.FilterInput.Prompt = "🔍 "
|
||||
|
||||
return IssueList{
|
||||
list: l,
|
||||
@@ -211,11 +230,6 @@ func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height
|
||||
l.SetShowHelp(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetFilteringEnabled(true)
|
||||
l.DisableQuitKeybindings()
|
||||
l.FilterInput.PromptStyle = styles.FilterPromptStyle
|
||||
l.FilterInput.Cursor.Style = styles.FilterStyle
|
||||
l.FilterInput.TextStyle = styles.FilterInputStyle
|
||||
l.FilterInput.Prompt = "🔍 "
|
||||
return IssueList{
|
||||
list: l,
|
||||
app: app,
|
||||
@@ -225,31 +239,9 @@ func NewIssueListFromIssues(app *app.App, issues []*models.Issue, width, height
|
||||
}
|
||||
}
|
||||
|
||||
func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue {
|
||||
// used to display open & in-progress issues in the first window in the dashboard
|
||||
out := make([]*models.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Status == models.StatusOpen ||
|
||||
issue.Status == models.StatusInProgress ||
|
||||
issue.Status == models.StatusBlocked ||
|
||||
issue.Status == models.StatusReadyToSprint {
|
||||
out = append(out, issue)
|
||||
}
|
||||
}
|
||||
sortByPriorityDesc(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func ClosedOnly(issues []*models.Issue) []*models.Issue {
|
||||
// used to display issues in the second window in the dashboard
|
||||
out := make([]*models.Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Status == models.StatusClosed {
|
||||
out = append(out, issue)
|
||||
}
|
||||
}
|
||||
sortByPriorityDesc(out)
|
||||
return out
|
||||
func SortedIssues(issues []*models.Issue) []*models.Issue {
|
||||
sortByClosedThenPriorityDesc(issues)
|
||||
return issues
|
||||
}
|
||||
|
||||
// StatusOnly returns issues that exactly match the given status, sorted by priority.
|
||||
@@ -261,13 +253,24 @@ func StatusOnly(issues []*models.Issue, status models.Status) []*models.Issue {
|
||||
out = append(out, issue)
|
||||
}
|
||||
}
|
||||
sortByPriorityDesc(out)
|
||||
sortByClosedThenPriorityDesc(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func sortByPriorityDesc(issues []*models.Issue) {
|
||||
sort.Slice(issues, func(i, j int) bool {
|
||||
return issues[i].Priority > issues[j].Priority
|
||||
func sortByClosedThenPriorityDesc(issues []*models.Issue) {
|
||||
sort.SliceStable(issues, func(i, j int) bool {
|
||||
iClosed := issues[i].Status == models.StatusClosed
|
||||
jClosed := issues[j].Status == models.StatusClosed
|
||||
|
||||
if iClosed != jClosed {
|
||||
return !iClosed
|
||||
}
|
||||
|
||||
if issues[i].Priority != issues[j].Priority {
|
||||
return issues[i].Priority > issues[j].Priority
|
||||
}
|
||||
|
||||
return issues[i].ID < issues[j].ID
|
||||
})
|
||||
}
|
||||
|
||||
@@ -300,14 +303,14 @@ func (l IssueList) renderResponsive() string {
|
||||
|
||||
if l.list.FilterState() == list.Filtering {
|
||||
filterText := l.list.FilterInput.Value()
|
||||
filterView := styles.FilterStyle.Render("🔍 " + filterText)
|
||||
filterView := style.FilterStyle.Render("🔍 " + filterText)
|
||||
content = append(content, filterView)
|
||||
}
|
||||
|
||||
itemsView := l.renderFilteredItems()
|
||||
content = append(content, header, itemsView)
|
||||
|
||||
return styles.ContainerStyle.
|
||||
return style.ContainerStyle.
|
||||
Width(l.width).
|
||||
MaxWidth(l.width).
|
||||
MaxHeight(l.height).
|
||||
@@ -365,7 +368,7 @@ func (l *IssueList) SetIssues(issues []*models.Issue) tea.Cmd {
|
||||
|
||||
func (l *IssueList) SelectIssueID(issueID string) {
|
||||
items := l.list.Items()
|
||||
for i := 0; i < len(items); i++ {
|
||||
for i := range items {
|
||||
if item, ok := items[i].(ListIssue); ok && item.ID == issueID {
|
||||
l.list.Select(i)
|
||||
return
|
||||
@@ -407,13 +410,16 @@ func renderRow(issue ListIssue, isSelected bool, cols []tableColumn) string {
|
||||
colWidth = 1
|
||||
}
|
||||
|
||||
style := lipgloss.NewStyle().Width(int(colWidth))
|
||||
cellStyle := lipgloss.NewStyle().Width(int(colWidth))
|
||||
if isSelected {
|
||||
style = style.Background(styles.SelectedBackground).Bold(true)
|
||||
cellStyle = cellStyle.Background(style.SelectedBackground).Bold(true)
|
||||
}
|
||||
if issue.Status == models.StatusClosed {
|
||||
cellStyle = cellStyle.Strikethrough(true).Foreground(style.FaintText)
|
||||
}
|
||||
|
||||
truncated := truncate.StringWithTail(value, colWidth, "...")
|
||||
parts = append(parts, style.Render(truncated))
|
||||
parts = append(parts, cellStyle.Render(truncated))
|
||||
}
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
|
||||
|
||||
Reference in New Issue
Block a user