From e69e52388d8d470bbd637c2dd1b4467fa7ee1a47 Mon Sep 17 00:00:00 2001 From: Kyosuke Fujimoto Date: Sun, 14 Apr 2024 13:58:26 +0900 Subject: [PATCH] Implement status filter --- internal/ui/prs_list_all.go | 175 +++++++++++++++++++++++++-- internal/ui/prs_list_all_delegate.go | 2 +- 2 files changed, 168 insertions(+), 9 deletions(-) diff --git a/internal/ui/prs_list_all.go b/internal/ui/prs_list_all.go index 605ec5a..c4ffb6b 100644 --- a/internal/ui/prs_list_all.go +++ b/internal/ui/prs_list_all.go @@ -1,25 +1,63 @@ package ui import ( + "fmt" "sort" + "strings" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" "github.com/lusingander/ghcv-cli/internal/gh" + "github.com/lusingander/kasane" ) +var ( + pullRequestsListAllDialogBodyStyle = lipgloss.NewStyle(). + Padding(0, 2) + + pullRequestsListAllDialogStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()) + + pullRequestsListAllDialogSelectedStyle = lipgloss.NewStyle(). + Foreground(selectedColor1) + + pullRequestsListAllDialogNotSelectedStyle = lipgloss.NewStyle() +) + +type pullRequestListAllSortType int + +const ( + pullRequestListAllSortByCreatedAtDesc pullRequestListAllSortType = iota + pullRequestListAllSortByCreatedAtAsc +) + +type pullRequestStatus struct { + name string + count int +} + type pullRequestsListAllModel struct { prs *gh.UserPullRequests - list list.Model - delegateKeys pullRequestsListAllDelegateKeyMap + list list.Model + originalItems []list.Item + delegateKeys pullRequestsListAllDelegateKeyMap + filterStatusDialogDelegateKeys pullRequestsListAllFilterStatusDialogDelegateKeyMap selectedUser string width, height int + + pullRequestListAllSortType + + statuses []*pullRequestStatus + statusIdx int + statusDialogOpened bool } type pullRequestsListAllDelegateKeyMap struct { + stat key.Binding open key.Binding back key.Binding tog key.Binding @@ -28,6 +66,10 @@ type pullRequestsListAllDelegateKeyMap struct { func newPullRequestsListAllDelegateKeyMap() pullRequestsListAllDelegateKeyMap { return pullRequestsListAllDelegateKeyMap{ + stat: key.NewBinding( + key.WithKeys("T"), + key.WithHelp("T", "filter by status"), + ), open: key.NewBinding( key.WithKeys("x"), key.WithHelp("x", "open in browser"), @@ -47,9 +89,33 @@ func newPullRequestsListAllDelegateKeyMap() pullRequestsListAllDelegateKeyMap { } } +type pullRequestsListAllFilterStatusDialogDelegateKeyMap struct { + next key.Binding + prev key.Binding + close key.Binding +} + +func newPullRequestsListAllFilterStatusDialogDelegateKeyMap() pullRequestsListAllFilterStatusDialogDelegateKeyMap { + return pullRequestsListAllFilterStatusDialogDelegateKeyMap{ + next: key.NewBinding( + key.WithKeys("j"), + key.WithHelp("j", "select next"), + ), + prev: key.NewBinding( + key.WithKeys("k"), + key.WithHelp("k", "select prev"), + ), + close: key.NewBinding( + key.WithKeys("T", "esc", "enter"), + key.WithHelp("T", "close dialog"), + ), + } +} + func newPullRequestsListAllModel() *pullRequestsListAllModel { delegateKeys := newPullRequestsListAllDelegateKeyMap() delegate := newPullRequestsListAllDelegate(delegateKeys) + filterStatusDialogDelegateKeys := newPullRequestsListAllFilterStatusDialogDelegateKeyMap() l := list.New(nil, delegate, 0, 0) l.KeyMap.Quit = delegateKeys.quit @@ -58,8 +124,9 @@ func newPullRequestsListAllModel() *pullRequestsListAllModel { l.SetShowStatusBar(false) return &pullRequestsListAllModel{ - list: l, - delegateKeys: delegateKeys, + list: l, + delegateKeys: delegateKeys, + filterStatusDialogDelegateKeys: filterStatusDialogDelegateKeys, } } @@ -75,7 +142,9 @@ func (m *pullRequestsListAllModel) SetUser(id string) { func (m *pullRequestsListAllModel) updatePrs(prs *gh.UserPullRequests) { m.prs = prs + items := make([]list.Item, 0) + statusesMap := make(map[string]int) for _, owner := range m.prs.Owners { for _, repo := range owner.Repositories { for _, pr := range repo.PullRequests { @@ -99,19 +168,61 @@ func (m *pullRequestsListAllModel) updatePrs(prs *gh.UserPullRequests) { }, } items = append(items, item) + statusesMap[pr.State] += 1 } } } m.list.SetItems(items) + m.originalItems = items m.sortItems() + + m.statuses = []*pullRequestStatus{ + {name: "All", count: len(items)}, + {name: "OPEN", count: statusesMap["OPEN"]}, + {name: "MERGED", count: statusesMap["MERGED"]}, + {name: "CLOSED", count: statusesMap["CLOSED"]}, + } + m.statusIdx = 0 } func (m *pullRequestsListAllModel) sortItems() { items := m.list.Items() - sort.Slice(items, func(i, j int) bool { - return items[i].(pullRequestsListAllItem).createdAt.After(items[j].(pullRequestsListAllItem).createdAt) - }) + switch m.pullRequestListAllSortType { + case pullRequestListAllSortByCreatedAtDesc: + sort.Slice(items, func(i, j int) bool { + return items[i].(pullRequestsListAllItem).createdAt.After(items[j].(pullRequestsListAllItem).createdAt) + }) + case pullRequestListAllSortByCreatedAtAsc: + sort.Slice(items, func(i, j int) bool { + return items[i].(pullRequestsListAllItem).createdAt.Before(items[j].(pullRequestsListAllItem).createdAt) + }) + } + m.list.SetItems(items) +} + +func (m *pullRequestsListAllModel) updateStatusIdx(reverse bool) { + n := len(m.statuses) + if reverse { + m.statusIdx = ((m.statusIdx-1)%n + n) % n + } else { + m.statusIdx = (m.statusIdx + 1) % n + } +} + +func (m *pullRequestsListAllModel) filterItems() { + if m.statuses[m.statusIdx].name == "All" { + m.list.SetItems(m.originalItems) + m.sortItems() + return + } + items := make([]list.Item, 0) + for _, i := range m.originalItems { + if i.(pullRequestsListAllItem).status == m.statuses[m.statusIdx].name { + items = append(items, i) + } + } m.list.SetItems(items) + m.sortItems() } func (m pullRequestsListAllModel) Init() tea.Cmd { @@ -131,7 +242,25 @@ func (m pullRequestsListAllModel) Update(msg tea.Msg) (pullRequestsListAllModel, var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: + if m.statusDialogOpened { + switch { + case key.Matches(msg, m.filterStatusDialogDelegateKeys.close): + m.statusDialogOpened = false + case key.Matches(msg, m.filterStatusDialogDelegateKeys.next): + m.list.ResetSelected() + m.updateStatusIdx(false) + m.filterItems() + case key.Matches(msg, m.filterStatusDialogDelegateKeys.prev): + m.list.ResetSelected() + m.updateStatusIdx(true) + m.filterItems() + } + return m, nil + } switch { + case key.Matches(msg, m.delegateKeys.stat): + m.statusDialogOpened = true + return m, nil case key.Matches(msg, m.delegateKeys.open): item := m.list.SelectedItem().(pullRequestsListAllItem) return m, m.openPullRequestPageInBrowser(item) @@ -152,7 +281,37 @@ func (m pullRequestsListAllModel) Update(msg tea.Msg) (pullRequestsListAllModel, } func (m pullRequestsListAllModel) View() string { - return titleView(m.breadcrumb()) + listView(m.list) + ret := titleView(m.breadcrumb()) + listView(m.list) + if m.statusDialogOpened { + return m.withStatusDialogView(ret) + } + return ret +} + +func (m pullRequestsListAllModel) withStatusDialogView(base string) string { + title := repositoriesDialogTitleStyle.Render("Status") + + ivs := make([]string, len(m.statuses)) + for i, s := range m.statuses { + ivs[i] = m.statusKeySelectItemView(s) + } + body := strings.Join(ivs, "\n") + body = pullRequestsListAllDialogBodyStyle.Render(body) + + dialog := pullRequestsListAllDialogStyle.Render(lipgloss.JoinVertical(lipgloss.Left, title, body)) + + dw, dh := lipgloss.Size(dialog) + top := (m.height / 2) - (dh / 2) + left := (m.width / 2) - (dw / 2) + return kasane.OverlayString(base, dialog, top, left, kasane.WithPadding(m.width)) +} + +func (m pullRequestsListAllModel) statusKeySelectItemView(status *pullRequestStatus) string { + if m.statuses[m.statusIdx].name == status.name { + return pullRequestsListAllDialogSelectedStyle.Render(fmt.Sprintf("> %s (%d)", status.name, status.count)) + } else { + return pullRequestsListAllDialogNotSelectedStyle.Render(fmt.Sprintf(" %s (%d)", status.name, status.count)) + } } func (m pullRequestsListAllModel) breadcrumb() []string { diff --git a/internal/ui/prs_list_all_delegate.go b/internal/ui/prs_list_all_delegate.go index 5d92759..2b51603 100644 --- a/internal/ui/prs_list_all_delegate.go +++ b/internal/ui/prs_list_all_delegate.go @@ -47,7 +47,7 @@ func newPullRequestsListAllDelegate(delegateKeys pullRequestsListAllDelegateKeyM return []key.Binding{delegateKeys.back, delegateKeys.tog} } fullHelpFunc := func() [][]key.Binding { - return [][]key.Binding{{delegateKeys.open, delegateKeys.back, delegateKeys.tog}} + return [][]key.Binding{{delegateKeys.stat, delegateKeys.open, delegateKeys.back, delegateKeys.tog}} } return pullRequestsListAllDelegate{ shortHelpFunc: shortHelpFunc,