From 144d589ec550dfbd67b22cdb2422b2043245d934 Mon Sep 17 00:00:00 2001 From: Orlando Romo Date: Fri, 14 Jul 2023 09:39:43 -0600 Subject: [PATCH] feat(ui): ability to prompt a confirmation before an action is performed --- ui/common/styles.go | 2 +- ui/components/issuessection/issuessection.go | 34 ++++-- ui/components/prompt/prompt.go | 62 ++++++++++ ui/components/prssection/prssection.go | 47 +++++-- ui/components/section/section.go | 122 +++++++++++++++---- ui/ui.go | 25 +++- 6 files changed, 243 insertions(+), 49 deletions(-) create mode 100644 ui/components/prompt/prompt.go diff --git a/ui/common/styles.go b/ui/common/styles.go index e655c43b..eb7c0f2c 100644 --- a/ui/common/styles.go +++ b/ui/common/styles.go @@ -8,7 +8,7 @@ import ( ) var ( - SearchHeight = 3 + SearchHeight = 4 FooterHeight = 1 ExpandedHelpHeight = 11 InputBoxHeight = 8 diff --git a/ui/components/issuessection/issuessection.go b/ui/components/issuessection/issuessection.go index ef6bc761..70549526 100644 --- a/ui/components/issuessection/issuessection.go +++ b/ui/components/issuessection/issuessection.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/dlvhdr/gh-dash/config" @@ -14,7 +13,6 @@ import ( "github.com/dlvhdr/gh-dash/ui/components/table" "github.com/dlvhdr/gh-dash/ui/constants" "github.com/dlvhdr/gh-dash/ui/context" - "github.com/dlvhdr/gh-dash/ui/keys" "github.com/dlvhdr/gh-dash/utils" ) @@ -72,13 +70,35 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) { break } - switch { - case key.Matches(msg, keys.IssueKeys.Close): - cmd = m.close() + if m.IsPromptConfirmationFocused() { - case key.Matches(msg, keys.IssueKeys.Reopen): - cmd = m.reopen() + var promptCmd tea.Cmd + switch { + + case msg.Type == tea.KeyCtrlC, msg.Type == tea.KeyEsc: + m.PromptConfirmationBox.Reset() + cmd = m.SetIsPromptConfirmationShown(false) + return &m, cmd + case msg.Type == tea.KeyEnter: + input := m.PromptConfirmationBox.Value() + action := m.GetPromptConfirmationAction() + if input == "Y" || input == "y" { + switch action { + case "close": + cmd = m.close() + case "reopen": + cmd = m.reopen() + } + } + + m.PromptConfirmationBox.Reset() + blinkCmd := m.SetIsPromptConfirmationShown(false) + + return &m, tea.Batch(cmd, blinkCmd) + } + m.PromptConfirmationBox, promptCmd = m.PromptConfirmationBox.Update(msg) + return &m, promptCmd } case UpdateIssueMsg: diff --git a/ui/components/prompt/prompt.go b/ui/components/prompt/prompt.go new file mode 100644 index 00000000..3380271e --- /dev/null +++ b/ui/components/prompt/prompt.go @@ -0,0 +1,62 @@ +package prompt + +import ( + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/dlvhdr/gh-dash/ui/context" +) + +type Model struct { + ctx *context.ProgramContext + prompt textinput.Model +} + +func NewModel(ctx *context.ProgramContext) Model { + ti := textinput.New() + ti.Focus() + ti.Blur() + ti.CursorStart() + + return Model{ + ctx: ctx, + prompt: ti, + } +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd + m.prompt, cmd = m.prompt.Update(msg) + return m, cmd +} + +func (m Model) View() string { + return m.prompt.View() +} + +func (m Model) Init() tea.Cmd { + return textinput.Blink +} + +func (m *Model) Blur() { + m.prompt.Blur() +} + +func (m *Model) Focus() tea.Cmd { + return m.prompt.Focus() +} + +func (m *Model) SetValue(value string) { + m.prompt.SetValue(value) +} + +func (m *Model) Value() string { + return m.prompt.Value() +} + +func (m *Model) SetPrompt(prompt string) { + m.prompt.Prompt = prompt +} + +func (m *Model) Reset() { + m.prompt.Reset() +} diff --git a/ui/components/prssection/prssection.go b/ui/components/prssection/prssection.go index 29aa391d..06dbdf94 100644 --- a/ui/components/prssection/prssection.go +++ b/ui/components/prssection/prssection.go @@ -74,6 +74,40 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) { break } + if m.IsPromptConfirmationFocused() { + var promptCmd tea.Cmd + switch { + + case msg.Type == tea.KeyCtrlC, msg.Type == tea.KeyEsc: + m.PromptConfirmationBox.Reset() + cmd = m.SetIsPromptConfirmationShown(false) + return &m, cmd + + case msg.Type == tea.KeyEnter: + input := m.PromptConfirmationBox.Value() + action := m.GetPromptConfirmationAction() + if input == "Y" || input == "y" { + switch action { + case "close": + cmd = m.close() + case "reopen": + cmd = m.reopen() + case "ready": + cmd = m.ready() + case "merge": + cmd = m.merge() + } + } + + m.PromptConfirmationBox.Reset() + blinkCmd := m.SetIsPromptConfirmationShown(false) + + return &m, tea.Batch(cmd, blinkCmd) + } + m.PromptConfirmationBox, promptCmd = m.PromptConfirmationBox.Update(msg) + return &m, promptCmd + } + switch { case key.Matches(msg, keys.PRKeys.Diff): @@ -84,19 +118,6 @@ func (m Model) Update(msg tea.Msg) (section.Section, tea.Cmd) { if err != nil { m.Ctx.Error = err } - - case key.Matches(msg, keys.PRKeys.Close): - cmd = m.close() - - case key.Matches(msg, keys.PRKeys.Ready): - cmd = m.ready() - - case key.Matches(msg, keys.PRKeys.Merge): - cmd = m.merge() - - case key.Matches(msg, keys.PRKeys.Reopen): - cmd = m.reopen() - } case UpdatePRMsg: diff --git a/ui/components/section/section.go b/ui/components/section/section.go index 8beb3209..1f444053 100644 --- a/ui/components/section/section.go +++ b/ui/components/section/section.go @@ -11,6 +11,7 @@ import ( "github.com/dlvhdr/gh-dash/config" "github.com/dlvhdr/gh-dash/data" "github.com/dlvhdr/gh-dash/ui/common" + "github.com/dlvhdr/gh-dash/ui/components/prompt" "github.com/dlvhdr/gh-dash/ui/components/search" "github.com/dlvhdr/gh-dash/ui/components/table" "github.com/dlvhdr/gh-dash/ui/constants" @@ -19,20 +20,23 @@ import ( ) type Model struct { - Id int - Config config.SectionConfig - Ctx *context.ProgramContext - Spinner spinner.Model - SearchBar search.Model - IsSearching bool - SearchValue string - Table table.Model - Type string - SingularForm string - PluralForm string - Columns []table.Column - TotalCount int - PageInfo *data.PageInfo + Id int + Config config.SectionConfig + Ctx *context.ProgramContext + Spinner spinner.Model + SearchBar search.Model + IsSearching bool + SearchValue string + Table table.Model + Type string + SingularForm string + PluralForm string + Columns []table.Column + TotalCount int + PageInfo *data.PageInfo + PromptConfirmationBox prompt.Model + IsPromptConfirmationShown bool + PromptConfirmationAction string } func NewModel( @@ -45,19 +49,20 @@ func NewModel( lastUpdated time.Time, ) Model { m := Model{ - Id: id, - Type: sType, - Config: cfg, - Ctx: ctx, - Spinner: spinner.Model{Spinner: spinner.Dot}, - Columns: columns, - SingularForm: singular, - PluralForm: plural, - SearchBar: search.NewModel(sType, ctx, cfg.Filters), - SearchValue: cfg.Filters, - IsSearching: false, - TotalCount: 0, - PageInfo: nil, + Id: id, + Type: sType, + Config: cfg, + Ctx: ctx, + Spinner: spinner.Model{Spinner: spinner.Dot}, + Columns: columns, + SingularForm: singular, + PluralForm: plural, + SearchBar: search.NewModel(sType, ctx, cfg.Filters), + SearchValue: cfg.Filters, + IsSearching: false, + TotalCount: 0, + PageInfo: nil, + PromptConfirmationBox: prompt.NewModel(ctx), } m.Table = table.NewModel( *ctx, @@ -81,6 +86,7 @@ type Section interface { Component Table Search + PromptConfirmation UpdateProgramContext(ctx *context.ProgramContext) MakeSectionCmd(cmd tea.Cmd) tea.Cmd LastUpdated() time.Time @@ -121,6 +127,14 @@ type Search interface { ResetPageInfo() } +type PromptConfirmation interface { + SetIsPromptConfirmationShown(val bool) tea.Cmd + IsPromptConfirmationFocused() bool + SetPromptConfirmationAction(action string) + GetPromptConfirmationAction() string + GetPromptConfirmation() string +} + func (m *Model) CreateNextTickCmd(nextTickCmd tea.Cmd) tea.Cmd { if m == nil || nextTickCmd == nil { return nil @@ -221,6 +235,29 @@ func (m *Model) ResetPageInfo() { m.PageInfo = nil } +func (m *Model) IsPromptConfirmationFocused() bool { + return m.IsPromptConfirmationShown +} + +func (m *Model) SetIsPromptConfirmationShown(val bool) tea.Cmd { + m.IsPromptConfirmationShown = val + if val { + m.PromptConfirmationBox.Focus() + return m.PromptConfirmationBox.Init() + } + + m.PromptConfirmationBox.Blur() + return nil +} + +func (m *Model) SetPromptConfirmationAction(action string) { + m.PromptConfirmationAction = action +} + +func (m *Model) GetPromptConfirmationAction() string { + return m.PromptConfirmationAction +} + type SectionMsg struct { Id int Type string @@ -276,6 +313,7 @@ func (m *Model) View() string { lipgloss.Left, search, m.GetMainContent(), + m.GetPromptConfirmation(), ), ) } @@ -308,3 +346,33 @@ func (m *Model) GetPagerContent() string { pager := m.Ctx.Styles.ListViewPort.PagerStyle.Copy().Render(pagerContent) return pager } + +func (m *Model) GetPromptConfirmation() string { + if m.IsPromptConfirmationShown { + var prompt string + switch { + case m.PromptConfirmationAction == "close" && m.Ctx.View == config.PRsView: + prompt = "Are you sure you want to close this PR? (Y/n) " + + case m.PromptConfirmationAction == "reopen" && m.Ctx.View == config.PRsView: + prompt = "Are you sure you want to reopen this PR? (Y/n) " + + case m.PromptConfirmationAction == "ready" && m.Ctx.View == config.PRsView: + prompt = "Are you sure you want to mark this PR as ready? (Y/n) " + + case m.PromptConfirmationAction == "merge" && m.Ctx.View == config.PRsView: + prompt = "Are you sure you want to merge this PR? (Y/n) " + + case m.PromptConfirmationAction == "close" && m.Ctx.View == config.IssuesView: + prompt = "Are you sure you want to close this issue? (Y/n) " + + case m.PromptConfirmationAction == "reopen" && m.Ctx.View == config.IssuesView: + prompt = "Are you sure you want to reopen this issue? (Y/n) " + } + m.PromptConfirmationBox.SetPrompt(prompt) + + return m.PromptConfirmationBox.View() + } + + return "" +} diff --git a/ui/ui.go b/ui/ui.go index 74e91f7a..d5bd9208 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -129,7 +129,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { log.Debug("Key pressed", "key", msg.String()) m.ctx.Error = nil - if currSection != nil && currSection.IsSearchFocused() { + if currSection != nil && currSection.IsSearchFocused() || currSection.IsPromptConfirmationFocused() { cmd = m.updateSection(currSection.GetId(), currSection.GetType(), msg) return m, cmd } @@ -244,6 +244,29 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.sidebar.ScrollToBottom() return m, cmd + case key.Matches(msg, keys.PRKeys.Close, keys.PRKeys.Reopen, keys.PRKeys.Ready, keys.PRKeys.Merge, keys.IssueKeys.Close, keys.IssueKeys.Reopen): + + var action string + switch { + case key.Matches(msg, keys.PRKeys.Close, keys.IssueKeys.Close): + action = "close" + + case key.Matches(msg, keys.PRKeys.Reopen, keys.IssueKeys.Reopen): + action = "reopen" + + case key.Matches(msg, keys.PRKeys.Ready): + action = "ready" + + case key.Matches(msg, keys.PRKeys.Merge): + action = "merge" + } + + if currSection != nil { + currSection.SetPromptConfirmationAction(action) + cmd = currSection.SetIsPromptConfirmationShown(true) + } + return m, cmd + case key.Matches(msg, keys.IssueKeys.Assign), key.Matches(msg, keys.PRKeys.Assign): m.sidebar.IsOpen = true if m.ctx.View == config.PRsView {