From 291a819ed6aaa6177eabc97c67a0ff49913b3eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Zywert?= Date: Wed, 8 May 2024 22:02:59 +0200 Subject: [PATCH] Search for `gojira worklogs` and some shortcut update (#11) --- CHANGELOG.md | 14 ++++- README.md | 11 +++- gojira/dayview.go | 130 ++++++++++++++++++++++++++++++++++++------- gojira/errorview.go | 10 +++- gojira/loaderview.go | 4 ++ 5 files changed, 143 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 013d630..623ad1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [0.8.0] - 2024-05-08 +### Added +- `gojira worklogs` search bar - looks for a text or issue key if passed uppercased like `ISSUE-123` +- `Enter` now submits worklog straight from time spent input, `Delete` removes it +- `Delete` also works on selected worklog on the list - no confirmation required though so watch out + +### Changed +- `SetFocusFunc`/`SetBlurFunc` now handles decorated windows instead of original mess + ## [0.7.0] - 2024-05-05 ### Added - Fetch national holidays from [date.nager.at](https://date.nager.at) if LC_TIME is present in environment. Holidays will be marked on calendar and excluded from month summary. @@ -100,7 +109,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - Initial release of gojira -[Unreleased]: https://github.com/jzyinq/gojira/compare/0.7.0...master +[Unreleased]: https://github.com/jzyinq/gojira/compare/0.8.0...master +[0.8.0]: https://github.com/jzyinq/gojira/compare/0.7.0...0.8.0 [0.7.0]: https://github.com/jzyinq/gojira/compare/0.6.0...0.7.0 [0.6.0]: https://github.com/jzyinq/gojira/compare/0.5.4...0.6.0 [0.5.4]: https://github.com/jzyinq/gojira/compare/0.5.3...0.5.4 @@ -114,4 +124,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), [0.2.2]: https://github.com/jzyinq/gojira/compare/0.2.1...0.2.2 [0.2.1]: https://github.com/jzyinq/gojira/compare/0.2.0...0.2.1 [0.2.0]: https://github.com/jzyinq/gojira/compare/0.1.0...0.2.0 -[0.1.0]: https://github.com/jzyinq/gojira/tree/0.1.0 +[0.1.0]: https://github.com/jzyinq/gojira/tree/0.1.0 \ No newline at end of file diff --git a/README.md b/README.md index 04b051a..c20501f 100644 --- a/README.md +++ b/README.md @@ -70,17 +70,22 @@ Just remember to urldecode it. Save it and you should ready to go! ## Todo list +- [ ] Refactor ShowError focus return +- [ ] Open issue in modal if it's the only result? +- [ ] Prompt `are you sure want to exit` on escape key +- [ ] Enter key should save worklog if it's focused +- [ ] Add shortcut hints to the bottom of the screen or along sections - [ ] delete worklog through simple cli version for today - [ ] ticket status change prompt after logging time - [ ] tests -- [ ] unify workLogs and worklogsIssues structs - use one for both - - Reduce jira/tempo spaghetti and unnecessary structs and functions - [ ] godtools cli semantics update - `gojira log -i TICKET` -> `gojira log -i TICKET` - `gojira log -i TICKET -t 1h30m` - `gojira` -> `gojira recent` - `gojira` -> `gojira --help` -- [ ] trigger ui updates after worklog change more efficiently +- [x] trigger ui updates after worklog change more efficiently +- [x] unify workLogs and worklogsIssues structs - use one for both +- [x] Reduce jira/tempo spaghetti and unnecessary structs and functions - [x] cli version does not update worklogs if they exist already - [x] fetch worklogs from current day and propose them for selection - [x] Add worklogs from ui diff --git a/gojira/dayview.go b/gojira/dayview.go index 0fc6f9c..2654cf0 100644 --- a/gojira/dayview.go +++ b/gojira/dayview.go @@ -18,6 +18,7 @@ type DayView struct { worklogStatus *tview.TextView latestIssuesList *tview.Table latestIssuesStatus *tview.TextView + searchInput *tview.InputField } func NewDayView() *DayView { //nolint:funlen @@ -31,11 +32,36 @@ func NewDayView() *DayView { //nolint:funlen app.ui.app.Draw() }), } + dayView.searchInput = tview.NewInputField().SetLabel("(/)Search: ").SetFieldWidth(60).SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEnter { + go func() { + dayView.SearchIssues(dayView.searchInput.GetText()) + }() + } + if key == tcell.KeyEscape { + app.ui.app.SetFocus(dayView.latestIssuesList) + } + }).SetFieldStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorBlack)) - // FIXME instead border we could color code it or add some prompt to given section dayView.worklogList.SetBorder(true) dayView.latestIssuesList.SetBorder(true) dayView.latestIssuesList.SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray)) + dayView.latestIssuesList.SetFocusFunc(func() { + dayView.latestIssuesList.SetSelectedStyle( + tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite)) + }) + dayView.latestIssuesList.SetBlurFunc(func() { + dayView.latestIssuesList.SetSelectedStyle( + tcell.StyleDefault.Background(tcell.ColorGrey).Foreground(tcell.ColorWhite)) + }) + dayView.worklogList.SetFocusFunc(func() { + dayView.worklogList.SetSelectedStyle( + tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite)) + }) + dayView.worklogList.SetBlurFunc(func() { + dayView.worklogList.SetSelectedStyle( + tcell.StyleDefault.Background(tcell.ColorGrey).Foreground(tcell.ColorWhite)) + }) dayView.worklogStatus.SetText( fmt.Sprintf("Worklogs - %s - [?h[white]]", app.time.Format("2006-01-02"), @@ -45,7 +71,8 @@ func NewDayView() *DayView { //nolint:funlen AddItem(dayView.worklogStatus, 1, 1, false). AddItem(dayView.worklogList, 0, 1, true). AddItem(dayView.latestIssuesStatus, 1, 1, false). - AddItem(dayView.latestIssuesList, 0, 1, false) + AddItem(dayView.latestIssuesList, 0, 1, false). + AddItem(dayView.searchInput, 1, 1, false) dayView.worklogList.SetCell(0, IssueKeyColumn, tview.NewTableCell("Loading...").SetAlign(tview.AlignLeft), @@ -54,21 +81,16 @@ func NewDayView() *DayView { //nolint:funlen // Make tab key able to switch between the two tables // Change focues table active row color to yellow and inactive to white flexView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - // FIXME - it's not ideal - we should to check if given table is focused instead if event.Key() == tcell.KeyTab { if app.ui.app.GetFocus() == dayView.worklogList { app.ui.app.SetFocus(dayView.latestIssuesList) - dayView.latestIssuesList.SetSelectedStyle( - tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite)) - dayView.worklogList.SetSelectedStyle( - tcell.StyleDefault.Background(tcell.ColorGrey).Foreground(tcell.ColorWhite)) return nil } app.ui.app.SetFocus(dayView.worklogList) - dayView.worklogList.SetSelectedStyle( - tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tcell.ColorWhite)) - dayView.latestIssuesList.SetSelectedStyle( - tcell.StyleDefault.Background(tcell.ColorGrey).Foreground(tcell.ColorWhite)) + return nil + } + if event.Rune() == '/' { + app.ui.app.SetFocus(dayView.searchInput) return nil } return event @@ -93,7 +115,7 @@ func loadWorklogs() { defer func() { <-loadingWorklogs }() err := NewWorklogIssues() if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) } app.ui.dayView.update() }() @@ -126,6 +148,27 @@ func (d *DayView) update() { }).SetSelectedFunc(func(row, column int) { NewUpdateWorklogForm(d, logs, row) }) + d.worklogList.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyDelete: + go func() { + app.ui.loaderView.Show("Deleting worklog...") + defer app.ui.loaderView.Hide() + row, _ := d.worklogList.GetSelection() + err := app.workLogs.Delete(logs[row].Worklog) + if err != nil { + app.ui.errorView.ShowError(err.Error(), nil) + return + } + d.update() + app.ui.pages.RemovePage("worklog-form") + app.ui.calendar.update() + app.ui.summary.update() + }() + default: + } + return event + }) timeSpent := CalculateTimeSpent(getWorklogsFromWorklogIssues(logs)) d.worklogStatus.SetText( fmt.Sprintf("Worklogs - %s - [%s%s[white]]", @@ -139,7 +182,7 @@ func (d *DayView) loadLatest() { d.latestIssuesStatus.SetText("Latest issues").SetDynamicColors(true) issues, err := NewJiraClient().GetLatestIssues() if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } d.latestIssuesList.Clear() @@ -162,6 +205,49 @@ func (d *DayView) loadLatest() { }) } +func (d *DayView) SearchIssues(search string) { + go func() { + app.ui.loaderView.Show("Searching...") + defer func() { + app.ui.loaderView.Hide() + }() + if search == "" { + return + } + jql := fmt.Sprintf("text ~ \"%s\"", search) + if FindIssueKeyInString(search) != "" { + jql = fmt.Sprintf("(text ~ \"%s\" OR issuekey = \"%s\")", search, search) + } + issues, err := NewJiraClient().GetIssuesByJQL( + fmt.Sprintf("%s ORDER BY updated DESC, created DESC", jql), 10, + ) + if err != nil { + app.ui.errorView.ShowError(err.Error(), d.searchInput) + return + } + d.latestIssuesList.Clear() + d.latestIssuesList.SetSelectable(true, false) + color := tcell.ColorWhite + for r := 0; r < len(issues.Issues); r++ { + d.latestIssuesList.SetCell(r, IssueKeyColumn, + tview.NewTableCell((issues.Issues)[r].Key).SetTextColor(color).SetAlign(tview.AlignLeft), + ) + d.latestIssuesList.SetCell(r, IssueSummaryColumn, + tview.NewTableCell((issues.Issues)[r].Fields.Summary).SetTextColor(color).SetAlign(tview.AlignLeft), + ) + } + d.latestIssuesList.Select(0, IssueKeyColumn).SetFixed(1, 1).SetDoneFunc(func(key tcell.Key) { + if key == tcell.KeyEscape { + app.ui.app.Stop() + } + }).SetSelectedFunc(func(row, column int) { + NewAddWorklogForm(d, issues.Issues, row) + }) + d.latestIssuesStatus.SetText("Search results:") + app.ui.app.SetFocus(d.latestIssuesList) + }() +} + // DateRange is a struct for holding the start and end dates type DateRange struct { StartDate time.Time @@ -222,25 +308,25 @@ func NewAddWorklogForm(d *DayView, issues []Issue, row int) *tview.Form { defer app.ui.loaderView.Hide() issue, err := NewJiraClient().GetIssue(issues[row].Key) if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } // TODO use ParseDateRange and LogWork for each day in range dateRange, err := ParseDateRange(logTime) if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } for day := dateRange.StartDate; day.Before(dateRange.EndDate.AddDate(0, 0, 1)); day = day.AddDate(0, 0, 1) { err := issue.LogWork(&day, timeSpent) app.ui.loaderView.UpdateText(fmt.Sprintf("Adding worklog for %s ...", day.Format(dateLayout))) if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } } if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } d.update() @@ -260,6 +346,8 @@ func NewAddWorklogForm(d *DayView, issues []Issue, row int) *tview.Form { }) form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { + case tcell.KeyEnter: + newWorklog() case tcell.KeyEscape: app.ui.pages.RemovePage("worklog-form") app.ui.app.SetFocus(app.ui.dayView.latestIssuesList) @@ -286,7 +374,7 @@ func NewUpdateWorklogForm(d *DayView, workLogIssues []*WorklogIssue, row int) *t defer app.ui.loaderView.Hide() err := workLogIssues[row].Worklog.Update(timeSpent) if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } d.update() @@ -302,7 +390,7 @@ func NewUpdateWorklogForm(d *DayView, workLogIssues []*WorklogIssue, row int) *t defer app.ui.loaderView.Hide() err := app.workLogs.Delete(workLogIssues[row].Worklog) if err != nil { - app.ui.errorView.ShowError(err.Error()) + app.ui.errorView.ShowError(err.Error(), nil) return } d.update() @@ -322,6 +410,10 @@ func NewUpdateWorklogForm(d *DayView, workLogIssues []*WorklogIssue, row int) *t }) form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { + case tcell.KeyEnter: + updateWorklog() + case tcell.KeyDelete: + deleteWorklog() case tcell.KeyEscape: app.ui.pages.RemovePage("worklog-form") app.ui.app.SetFocus(app.ui.dayView.worklogList) diff --git a/gojira/errorview.go b/gojira/errorview.go index 782e6bb..6ca47f9 100644 --- a/gojira/errorview.go +++ b/gojira/errorview.go @@ -8,10 +8,11 @@ import ( type ErrorView struct { *tview.Modal + previousFocus tview.Primitive } func NewErrorView() *ErrorView { - errorView := &ErrorView{tview.NewModal()} + errorView := &ErrorView{tview.NewModal(), nil} errorView.SetText("Something went wrong") errorView.SetTitle("Error!") errorView.SetBackgroundColor(tcell.ColorRed.TrueColor()) @@ -20,6 +21,9 @@ func NewErrorView() *ErrorView { switch event.Key() { case tcell.KeyEnter: app.ui.pages.HidePage("error") + if errorView.previousFocus != nil { + app.ui.app.SetFocus(errorView.previousFocus) + } } return event }) @@ -27,9 +31,11 @@ func NewErrorView() *ErrorView { return errorView } -func (e *ErrorView) ShowError(error string) { +func (e *ErrorView) ShowError(error string, previousFocus tview.Primitive) { + e.previousFocus = previousFocus app.ui.pages.SendToFront("error") e.SetText(fmt.Sprintf("Error: %s", error)) app.ui.pages.ShowPage("error") + app.ui.app.SetFocus(e) app.ui.app.Draw() } diff --git a/gojira/loaderview.go b/gojira/loaderview.go index 21951b3..7a61fad 100644 --- a/gojira/loaderview.go +++ b/gojira/loaderview.go @@ -58,9 +58,13 @@ func (e *LoaderView) UpdateText(msg string) { } func (e *LoaderView) Hide() { + focusedPrimitive := app.ui.app.GetFocus() if e.cancel != nil { e.cancel() } app.ui.pages.HidePage("loader") + if focusedPrimitive != e { + app.ui.app.SetFocus(focusedPrimitive) + } app.ui.app.Draw() }