Skip to content

Commit

Permalink
Search for gojira worklogs and some shortcut update (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
jzyinq authored May 8, 2024
1 parent 57f0c99 commit 291a819
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 26 deletions.
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 111 additions & 19 deletions gojira/dayview.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type DayView struct {
worklogStatus *tview.TextView
latestIssuesList *tview.Table
latestIssuesStatus *tview.TextView
searchInput *tview.InputField
}

func NewDayView() *DayView { //nolint:funlen
Expand All @@ -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"),
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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()
}()
Expand Down Expand Up @@ -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]]",
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions gojira/errorview.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -20,16 +21,21 @@ 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
})
app.ui.pages.AddPage("error", errorView, true, false)
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()
}
4 changes: 4 additions & 0 deletions gojira/loaderview.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

0 comments on commit 291a819

Please sign in to comment.