Skip to content

Commit

Permalink
feat: smart filtering of PRs/issues to remote tracked by current dire…
Browse files Browse the repository at this point in the history
…ctory

If the current directory from which you launched gh-dash is a clone of a
GitHub repo, this change causes your PR/issue sections to be further
“smartly” auto-filtered down to only the PRs/issues for the remote repo
tracked by the clone directory from which you launched gh-dash.
  • Loading branch information
sideshowbarker authored and dlvhdr committed Feb 21, 2025
1 parent 5a1c54e commit e495592
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 50 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Check out this 10/10 video by [charm.sh ✨](https://charm.sh) explaining how gh
- 🔭 view details about a pr/issue with a detailed sidebar
- 🪟 write multiple configuration files to easily switch between completely different dashboards
- ♻️ set an interval for auto refreshing the dashboard
- 📁 smart filtering - auto-filter pr/issue lists to the remote tracked by the current directory

## 📦 Installation

Expand Down
46 changes: 24 additions & 22 deletions config/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ type PrsLayoutConfig struct {
}

type IssuesLayoutConfig struct {
UpdatedAt ColumnConfig `yaml:"updatedAt,omitempty"`
CreatedAt ColumnConfig `yaml:"createdAt,omitempty"`
State ColumnConfig `yaml:"state,omitempty"`
Repo ColumnConfig `yaml:"repo,omitempty"`
Title ColumnConfig `yaml:"title,omitempty"`
Creator ColumnConfig `yaml:"creator,omitempty"`
CreatorIcon ColumnConfig `yaml:"creatorIcon,omitempty"`
Assignees ColumnConfig `yaml:"assignees,omitempty"`
Comments ColumnConfig `yaml:"comments,omitempty"`
Reactions ColumnConfig `yaml:"reactions,omitempty"`
UpdatedAt ColumnConfig `yaml:"updatedAt,omitempty"`
CreatedAt ColumnConfig `yaml:"createdAt,omitempty"`
State ColumnConfig `yaml:"state,omitempty"`
Repo ColumnConfig `yaml:"repo,omitempty"`
Title ColumnConfig `yaml:"title,omitempty"`
Creator ColumnConfig `yaml:"creator,omitempty"`
CreatorIcon ColumnConfig `yaml:"creatorIcon,omitempty"`
Assignees ColumnConfig `yaml:"assignees,omitempty"`
Comments ColumnConfig `yaml:"comments,omitempty"`
Reactions ColumnConfig `yaml:"reactions,omitempty"`
}

type LayoutConfig struct {
Expand Down Expand Up @@ -215,16 +215,17 @@ type ThemeConfig struct {
}

type Config struct {
PRSections []PrsSectionConfig `yaml:"prSections"`
IssuesSections []IssuesSectionConfig `yaml:"issuesSections"`
Repo RepoConfig `yaml:"repo"`
Defaults Defaults `yaml:"defaults"`
Keybindings Keybindings `yaml:"keybindings"`
RepoPaths map[string]string `yaml:"repoPaths"`
Theme *ThemeConfig `yaml:"theme,omitempty" validate:"omitempty"`
Pager Pager `yaml:"pager"`
ConfirmQuit bool `yaml:"confirmQuit"`
ShowAuthorIcons bool `yaml:"showAuthorIcons"`
PRSections []PrsSectionConfig `yaml:"prSections"`
IssuesSections []IssuesSectionConfig `yaml:"issuesSections"`
Repo RepoConfig `yaml:"repo"`
Defaults Defaults `yaml:"defaults"`
Keybindings Keybindings `yaml:"keybindings"`
RepoPaths map[string]string `yaml:"repoPaths"`
Theme *ThemeConfig `yaml:"theme,omitempty" validate:"omitempty"`
Pager Pager `yaml:"pager"`
ConfirmQuit bool `yaml:"confirmQuit"`
ShowAuthorIcons bool `yaml:"showAuthorIcons"`
SmartFilteringAtLaunch bool `yaml:"smartFilteringAtLaunch" default:"true"`
}

type configError struct {
Expand Down Expand Up @@ -345,8 +346,9 @@ func (parser ConfigParser) getDefaultConfig() Config {
},
},
},
ConfirmQuit: false,
ShowAuthorIcons: true,
ConfirmQuit: false,
ShowAuthorIcons: true,
SmartFilteringAtLaunch: true,
}
}

Expand Down
1 change: 1 addition & 0 deletions docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ and **issues** by filters you care about.
- ![icon:layout-dashboard](lucide) write multiple configuration files to easily switch between
completely different dashboards
- ![icon:recycle](lucide) set an interval for auto refreshing the dashboard
- ![icon:folder-git](lucide) [smart filtering](/getting-started/smartfiltering) - auto-filter prs/issues to the remote tracked by the current directory

<!-- Link reference definitions -->
[shield-release]: https://img.shields.io/github/release/dlvhdr/gh-dash.svg
Expand Down
37 changes: 37 additions & 0 deletions docs/content/getting-started/smartfiltering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: About Smart Filtering
linkTitle: >-
![icon:folder-git](lucide)&nbsp;About Smart Filtering
summary: >-
About the Smart Filtering feature
weight: 3
---

By default, if the directory you launch `gh-dash` from is a clone of a remote GitHub repo (or if you
have the `GH_REPO` environment variable set to a particular remote), then for any of your PR
sections and issue sections with `filters` values in your [configuration](/configuration) that don’t
have an explicit `repo:` field, `gh-dash` adds a `repo:<RepoName>` field to the search-bar value for
them (where _`<RepoName>`_ is the name of the remote repo).

That is, `gh-dash` further filters those sections down to only the PRs/issues for the GitHub
repo name specified in your `GH_REPO` environment variable — or else the repo name of the remote
tracked by the clone directory from which `gh-dash` launched.

For that, `gh-dash` first checks and uses the repo name in the `GH_REPO` environment variable (if
you have that set). If `gh-dash` doesn’t find that, then it next checks for the value of the remote
repo name tracked by the clone directory from which you launched `gh-dash` — by looking through all
GitHub remotes configured for that clone in the following order:

1. `upstream`
2. `github`
3. `origin`

…and, otherwise, if `gh-dash` finds no remotes with any of those names, then it uses the repo name
for the first remote in the output that `git remote` shows.

To disable Smart Filtering at launch, set [`smartFilteringAtLaunch`](/configuration/gh-dash/#smartfilteringatlaunch)
to `false` in your [configuration](/configuration).

To toggle Smart Filtering on or off for the current section you’re currently viewing, either use the
`t` key — or else use whatever custom keybinding you have set for the `togglesearch` builtin in the
`keybindings` section of your [configuration](/configuration).
7 changes: 7 additions & 0 deletions docs/data/schemas/gh-dash.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,10 @@ properties:
type: boolean
schematize:
weight: 8
smartFilteringAtLaunch:
title: Smart Filtering At Launch
description: |
Set this to `false` to disable [Smart Filtering](/getting-started/smartfiltering) at `gh-dash` launch.
type: boolean
schematize:
weight: 8
2 changes: 1 addition & 1 deletion ui/common/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
var (
SearchHeight = 3
FooterHeight = 1
ExpandedHelpHeight = 14
ExpandedHelpHeight = 15
InputBoxHeight = 8
SingleRuneWidth = 4
MainContentPadding = 1
Expand Down
19 changes: 19 additions & 0 deletions ui/components/issuessection/issuessection.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"time"

"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"

"github.com/dlvhdr/gh-dash/v4/config"
Expand All @@ -13,6 +14,7 @@ import (
"github.com/dlvhdr/gh-dash/v4/ui/components/table"
"github.com/dlvhdr/gh-dash/v4/ui/constants"
"github.com/dlvhdr/gh-dash/v4/ui/context"
"github.com/dlvhdr/gh-dash/v4/ui/keys"
"github.com/dlvhdr/gh-dash/v4/utils"
)

Expand Down Expand Up @@ -103,6 +105,23 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) {
break
}

switch {

case key.Matches(msg, keys.IssueKeys.ToggleSmartFiltering):
if !m.HasRepoNameInConfiguredFilter() {
m.IsFilteredByCurrentRemote = !m.IsFilteredByCurrentRemote
}
searchValue := m.GetSearchValue()
if m.SearchValue != searchValue {
m.SearchValue = searchValue
m.SearchBar.SetValue(searchValue)
m.SetIsSearching(false)
m.ResetRows()
return &m, tea.Batch(m.FetchNextPageSectionRows()...)
}

}

case UpdateIssueMsg:
for i, currIssue := range m.Issues {
if currIssue.Number == msg.IssueNumber {
Expand Down
13 changes: 13 additions & 0 deletions ui/components/prssection/prssection.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) {
case key.Matches(msg, keys.PRKeys.Diff):
cmd = m.diff()

case key.Matches(msg, keys.PRKeys.ToggleSmartFiltering):
if !m.HasRepoNameInConfiguredFilter() {
m.IsFilteredByCurrentRemote = !m.IsFilteredByCurrentRemote
}
searchValue := m.GetSearchValue()
if m.SearchValue != searchValue {
m.SearchValue = searchValue
m.SearchBar.SetValue(searchValue)
m.SetIsSearching(false)
m.ResetRows()
return &m, tea.Batch(m.FetchNextPageSectionRows()...)
}

case key.Matches(msg, keys.PRKeys.Checkout):
cmd, err = m.checkout()
if err != nil {
Expand Down
78 changes: 70 additions & 8 deletions ui/components/section/section.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package section

import (
"fmt"
"strings"
"time"

"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/cli/go-gh/v2/pkg/repository"

"github.com/dlvhdr/gh-dash/v4/config"
"github.com/dlvhdr/gh-dash/v4/data"
Expand Down Expand Up @@ -40,11 +42,13 @@ type BaseModel struct {
LastFetchTaskId string
IsSearchSupported bool
ShowAuthorIcon bool
IsFilteredByCurrentRemote bool
}

type NewSectionOptions struct {
Id int
Config config.SectionConfig
Ctx *context.ProgramContext
Type string
Columns []table.Column
Singular string
Expand All @@ -53,6 +57,23 @@ type NewSectionOptions struct {
CreatedAt time.Time
}

func (options NewSectionOptions) GetConfigFiltersWithCurrentRemoteAdded(ctx *context.ProgramContext) string {
searchValue := options.Config.Filters
if !ctx.Config.SmartFilteringAtLaunch {
return searchValue
}
repo, err := repository.Current()
if err != nil {
return searchValue
}
for _, token := range strings.Fields(searchValue) {
if strings.HasPrefix(token, "repo:") {
return searchValue
}
}
return fmt.Sprintf("repo:%s/%s %s", repo.Owner, repo.Name, searchValue)
}

func NewModel(
ctx *context.ProgramContext,
options NewSectionOptions,
Expand All @@ -68,14 +89,18 @@ func NewModel(
PluralForm: options.Plural,
SearchBar: search.NewModel(ctx, search.SearchOptions{
Prefix: fmt.Sprintf("is:%s", options.Type),
InitialValue: options.Config.Filters,
InitialValue: options.GetConfigFiltersWithCurrentRemoteAdded(ctx),
}),
SearchValue: options.Config.Filters,
IsSearching: false,
TotalCount: 0,
PageInfo: nil,
PromptConfirmationBox: prompt.NewModel(ctx),
ShowAuthorIcon: ctx.Config.ShowAuthorIcons,
SearchValue: options.GetConfigFiltersWithCurrentRemoteAdded(ctx),
IsSearching: false,
IsFilteredByCurrentRemote: options.GetConfigFiltersWithCurrentRemoteAdded(ctx) != options.Config.Filters,
TotalCount: 0,
PageInfo: nil,
PromptConfirmationBox: prompt.NewModel(ctx),
ShowAuthorIcon: ctx.Config.ShowAuthorIcons,
}
if !ctx.Config.SmartFilteringAtLaunch {
m.IsFilteredByCurrentRemote = false
}
m.Table = table.NewModel(
*ctx,
Expand Down Expand Up @@ -142,6 +167,7 @@ type Search interface {
ResetFilters()
GetFilters() string
ResetPageInfo()
IsFilteringByClone() bool
}

type PromptConfirmation interface {
Expand All @@ -159,6 +185,38 @@ func (m *BaseModel) GetDimensions() constants.Dimensions {
}
}

func (m *BaseModel) HasRepoNameInConfiguredFilter() bool {
filters := m.Config.Filters
for _, token := range strings.Fields(filters) {
if strings.HasPrefix(token, "repo:") {
return true
}
}
return false
}

func (m *BaseModel) GetSearchValue() string {
searchValue := m.SearchValue
repo, err := repository.Current()
if err != nil {
return searchValue
}
if m.HasRepoNameInConfiguredFilter() {
return searchValue
}
currentCloneFilter := fmt.Sprintf("repo:%s/%s", repo.Owner, repo.Name)
var searchValueWithoutCurrentCloneFilter []string
for _, token := range strings.Fields(searchValue) {
if !strings.HasPrefix(token, currentCloneFilter) {
searchValueWithoutCurrentCloneFilter = append(searchValueWithoutCurrentCloneFilter, token)
}
}
if m.IsFilteredByCurrentRemote {
return fmt.Sprintf("%s %s", currentCloneFilter, strings.Join(searchValueWithoutCurrentCloneFilter, " "))
}
return strings.Join(searchValueWithoutCurrentCloneFilter, " ")
}

func (m *BaseModel) UpdateProgramContext(ctx *context.ProgramContext) {
oldDimensions := m.GetDimensions()
m.Ctx = ctx
Expand Down Expand Up @@ -230,7 +288,7 @@ func (m *BaseModel) SetIsSearching(val bool) tea.Cmd {
}

func (m *BaseModel) ResetFilters() {
m.SearchBar.SetValue(m.Config.Filters)
m.SearchBar.SetValue(m.GetSearchValue())
}

func (m *BaseModel) ResetPageInfo() {
Expand Down Expand Up @@ -285,6 +343,10 @@ func (m *BaseModel) GetFilters() string {
return m.SearchBar.Value()
}

func (m *BaseModel) IsFilteringByClone() bool {
return m.IsFilteredByCurrentRemote
}

func (m *BaseModel) GetMainContent() string {
if m.Table.Rows == nil {
d := m.GetDimensions()
Expand Down
18 changes: 12 additions & 6 deletions ui/keys/issueKeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import (
)

type IssueKeyMap struct {
Assign key.Binding
Unassign key.Binding
Comment key.Binding
Close key.Binding
Reopen key.Binding
ViewPRs key.Binding
Assign key.Binding
Unassign key.Binding
Comment key.Binding
Close key.Binding
Reopen key.Binding
ToggleSmartFiltering key.Binding
ViewPRs key.Binding
}

var IssueKeys = IssueKeyMap{
Expand All @@ -39,6 +40,10 @@ var IssueKeys = IssueKeyMap{
key.WithKeys("X"),
key.WithHelp("X", "reopen"),
),
ToggleSmartFiltering: key.NewBinding(
key.WithKeys("t"),
key.WithHelp("t", "toggle smart filtering"),
),
ViewPRs: key.NewBinding(
key.WithKeys("s"),
key.WithHelp("s", "switch to PRs"),
Expand All @@ -52,6 +57,7 @@ func IssueFullHelp() []key.Binding {
IssueKeys.Comment,
IssueKeys.Close,
IssueKeys.Reopen,
IssueKeys.ToggleSmartFiltering,
IssueKeys.ViewPRs,
}
}
Expand Down
Loading

0 comments on commit e495592

Please sign in to comment.