diff --git a/README.md b/README.md index b665fa9..1d2db35 100644 --- a/README.md +++ b/README.md @@ -92,48 +92,56 @@ punchout \ ``` punchout Reference Manual -punchout has 4 panes: - - Issues List View Shows you issues matching your JQL query - - Worklog List View Shows you your worklog entries; you sync these entries - to JIRA from here - - Worklog Entry View You enter/update a worklog entry from here +punchout has 5 panes: + - Issues List View Shows you issues matching your JQL query + - Worklog List View Shows you your worklog entries; you sync these entries + to JIRA from here + - Worklog Entry View You enter/update a worklog entry from here + - Synced Worklog Entry View You view the worklog entries synced to JIRA + - Help View (this one) Keyboard Shortcuts General - 1 Switch to Issues List View - 2 Switch to Worklog List View - Go to next view/form entry - Go to previous view/form entry - ? Show help view + 1 Switch to Issues List View + 2 Switch to Worklog List View + 3 Switch to Synced Worklog List View + Go to next view/form entry + Go to previous view/form entry + ? Show help view General List Controls - h/ Move cursor up - k/ Move cursor down - h Go to previous page - l Go to next page - / Start filtering + h/ Move cursor up + k/ Move cursor down + h Go to previous page + l Go to next page + / Start filtering Issue List View - s Toggle recording time on the currently selected issue, - will open up a form to record a comment on the second - "s" keypress - Add manual worklog entry + s Toggle recording time on the currently selected issue, + will open up a form to record a comment on the second + "s" keypress + Add manual worklog entry Worklog List View - Update worklog entry - Delete worklog entry - s Sync all visible entries to JIRA - Refresh list + Update worklog entry + Delete worklog entry + s Sync all visible entries to JIRA + Refresh list Worklog Entry View - enter Save worklog entry + enter Save worklog entry + +Synced Worklog Entry View + + Refresh list + ``` Acknowledgements diff --git a/ui/cmds.go b/ui/cmds.go index bff41d7..d590237 100644 --- a/ui/cmds.go +++ b/ui/cmds.go @@ -131,6 +131,16 @@ func fetchLogEntries(db *sql.DB) tea.Cmd { } } +func fetchSyncedLogEntries(db *sql.DB) tea.Cmd { + return func() tea.Msg { + entries, err := fetchSyncedEntries(db) + return SyncedLogEntriesFetchedMsg{ + entries: entries, + err: err, + } + } +} + func deleteLogEntry(db *sql.DB, id int) tea.Cmd { return func() tea.Msg { err := deleteEntry(db, id) diff --git a/ui/help.go b/ui/help.go index 5917480..2d8fbeb 100644 --- a/ui/help.go +++ b/ui/help.go @@ -17,53 +17,61 @@ var ( %s %s %s +%s + %s %s `, helpHeaderStyle.Render("punchout Reference Manual"), helpSectionStyle.Render(` (scroll line by line with j/k/arrow keys or by half a page with /) - punchout has 4 panes: - - Issues List View Shows you issues matching your JQL query - - Worklog List View Shows you your worklog entries; you sync these entries - to JIRA from here - - Worklog Entry View You enter/update a worklog entry from here + punchout has 5 panes: + - Issues List View Shows you issues matching your JQL query + - Worklog List View Shows you your worklog entries; you sync these entries + to JIRA from here + - Worklog Entry View You enter/update a worklog entry from here + - Synced Worklog Entry View You view the worklog entries synced to JIRA - Help View (this one) `), helpHeaderStyle.Render("Keyboard Shortcuts"), helpHeaderStyle.Render("General"), helpSectionStyle.Render(` - 1 Switch to Issues List View - 2 Switch to Worklog List View - Go to next view/form entry - Go to previous view/form entry - ? Show help view + 1 Switch to Issues List View + 2 Switch to Worklog List View + 3 Switch to Synced Worklog List View + Go to next view/form entry + Go to previous view/form entry + ? Show help view `), helpHeaderStyle.Render("General List Controls"), helpSectionStyle.Render(` - h/ Move cursor up - k/ Move cursor down - h Go to previous page - l Go to next page - / Start filtering + h/ Move cursor up + k/ Move cursor down + h Go to previous page + l Go to next page + / Start filtering `), helpHeaderStyle.Render("Issue List View"), helpSectionStyle.Render(` - s Toggle recording time on the currently selected issue, - will open up a form to record a comment on the second - "s" keypress - Add manual worklog entry + s Toggle recording time on the currently selected issue, + will open up a form to record a comment on the second + "s" keypress + Add manual worklog entry `), helpHeaderStyle.Render("Worklog List View"), helpSectionStyle.Render(` - Update worklog entry - Delete worklog entry - s Sync all visible entries to JIRA - Refresh list + Update worklog entry + Delete worklog entry + s Sync all visible entries to JIRA + Refresh list `), helpHeaderStyle.Render("Worklog Entry View"), helpSectionStyle.Render(` - enter Save worklog entry + enter Save worklog entry +`), + helpHeaderStyle.Render("Synced Worklog Entry View"), + helpSectionStyle.Render(` + Refresh list `), ) ) diff --git a/ui/initial.go b/ui/initial.go index 289d845..f1bdbd0 100644 --- a/ui/initial.go +++ b/ui/initial.go @@ -12,6 +12,7 @@ import ( func InitialModel(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDeltaMins int) model { var stackItems []list.Item var worklogListItems []list.Item + var syncedWorklogListItems []list.Item itemDel := newItemDelegate() @@ -40,6 +41,7 @@ func InitialModel(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDelta jql: jql, issueList: list.New(stackItems, itemDel, listWidth, 0), worklogList: list.New(worklogListItems, itemDel, listWidth, 0), + syncedWorklogList: list.New(syncedWorklogListItems, itemDel, listWidth, 0), jiraTimeDeltaMins: jiraTimeDeltaMins, showHelpIndicator: true, trackingInputs: trackingInputs, @@ -61,5 +63,14 @@ func InitialModel(db *sql.DB, jiraClient *jira.Client, jql string, jiraTimeDelta m.worklogList.Styles.Title.Background(lipgloss.Color("#fe8019")) m.worklogList.Styles.Title.Bold(true) + m.syncedWorklogList.Title = "Synced Worklog Entries (from local db)" + m.syncedWorklogList.SetStatusBarItemName("entry", "entries") + m.syncedWorklogList.SetFilteringEnabled(false) + m.syncedWorklogList.DisableQuitKeybindings() + m.syncedWorklogList.SetShowHelp(false) + m.syncedWorklogList.Styles.Title.Foreground(lipgloss.Color("#282828")) + m.syncedWorklogList.Styles.Title.Background(lipgloss.Color("#fe8019")) + m.syncedWorklogList.Styles.Title.Bold(true) + return m } diff --git a/ui/model.go b/ui/model.go index 451ae45..bd111dd 100644 --- a/ui/model.go +++ b/ui/model.go @@ -30,6 +30,7 @@ type StateView uint const ( IssueListView StateView = iota WorklogView + SyncedWorklogView AskForCommentView ManualWorklogEntryView HelpView @@ -55,27 +56,29 @@ const ( ) type model struct { - activeView StateView - lastView StateView - db *sql.DB - jiraClient *jira.Client - jql string - issueList list.Model - worklogList list.Model - trackingInputs []textinput.Model - trackingFocussedField trackingFocussedField - helpVP viewport.Model - helpVPReady bool - lastChange DBChange - changesLocked bool - activeIssue string - worklogSaveType worklogSaveType - message string - errorMessage string - messages []string - jiraTimeDeltaMins int - showHelpIndicator bool - terminalHeight int + activeView StateView + lastView StateView + db *sql.DB + jiraClient *jira.Client + jql string + issueList list.Model + worklogList list.Model + syncedWorklogList list.Model + syncedWLEntriesFetched bool + trackingInputs []textinput.Model + trackingFocussedField trackingFocussedField + helpVP viewport.Model + helpVPReady bool + lastChange DBChange + changesLocked bool + activeIssue string + worklogSaveType worklogSaveType + message string + errorMessage string + messages []string + jiraTimeDeltaMins int + showHelpIndicator bool + terminalHeight int } func (m model) Init() tea.Cmd { diff --git a/ui/msgs.go b/ui/msgs.go index 0b52093..ca38244 100644 --- a/ui/msgs.go +++ b/ui/msgs.go @@ -38,6 +38,11 @@ type LogEntriesFetchedMsg struct { err error } +type SyncedLogEntriesFetchedMsg struct { + entries []SyncedWorklogEntry + err error +} + type LogEntriesDeletedMsg struct { err error } diff --git a/ui/types.go b/ui/types.go index 44d7e87..3310d78 100644 --- a/ui/types.go +++ b/ui/types.go @@ -57,6 +57,14 @@ type WorklogEntry struct { Error error } +type SyncedWorklogEntry struct { + Id int + IssueKey string + BeginTS time.Time + EndTS time.Time + Comment string +} + func (entry WorklogEntry) Title() string { return entry.Comment } @@ -83,3 +91,17 @@ func (entry WorklogEntry) Description() string { ) } func (entry WorklogEntry) FilterValue() string { return entry.IssueKey } + +func (entry SyncedWorklogEntry) Title() string { + return entry.Comment +} +func (entry SyncedWorklogEntry) Description() string { + minsSpent := int(entry.EndTS.Sub(entry.BeginTS).Minutes()) + minsSpentStr := fmt.Sprintf("spent %d mins", minsSpent) + return fmt.Sprintf("%s%s%s", + RightPadTrim(entry.IssueKey, int(listWidth/4)), + RightPadTrim("started: "+entry.BeginTS.Format("Mon, 3:04pm"), int(listWidth/4)), + RightPadTrim(minsSpentStr, int(listWidth/4)), + ) +} +func (entry SyncedWorklogEntry) FilterValue() string { return entry.IssueKey } diff --git a/ui/update.go b/ui/update.go index 49446af..3bb1736 100644 --- a/ui/update.go +++ b/ui/update.go @@ -92,7 +92,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.activeView { case IssueListView: m.activeView = WorklogView + cmds = append(cmds, fetchLogEntries(m.db)) case WorklogView: + m.activeView = SyncedWorklogView + cmds = append(cmds, fetchSyncedLogEntries(m.db)) + case SyncedWorklogView: m.activeView = IssueListView case ManualWorklogEntryView: switch m.trackingFocussedField { @@ -112,8 +116,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.activeView { case WorklogView: m.activeView = IssueListView - case IssueListView: + case SyncedWorklogView: m.activeView = WorklogView + cmds = append(cmds, fetchLogEntries(m.db)) + case IssueListView: + m.activeView = SyncedWorklogView + cmds = append(cmds, fetchSyncedLogEntries(m.db)) case ManualWorklogEntryView: switch m.trackingFocussedField { case entryBeginTS: @@ -165,6 +173,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case HelpView: m.activeView = IssueListView + default: + return m, tea.Quit } case "1": if m.activeView != IssueListView { @@ -175,6 +185,11 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.activeView = WorklogView cmds = append(cmds, fetchLogEntries(m.db)) } + case "3": + if m.activeView != SyncedWorklogView { + m.activeView = SyncedWorklogView + cmds = append(cmds, fetchSyncedLogEntries(m.db)) + } case "ctrl+r": switch m.activeView { case IssueListView: @@ -182,6 +197,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case WorklogView: cmds = append(cmds, fetchLogEntries(m.db)) m.worklogList.ResetSelected() + case SyncedWorklogView: + cmds = append(cmds, fetchSyncedLogEntries(m.db)) + m.syncedWorklogList.ResetSelected() } case "ctrl+s": if m.activeView == IssueListView { @@ -283,6 +301,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.terminalHeight = msg.Height m.issueList.SetHeight(msg.Height - h - 2) m.worklogList.SetHeight(msg.Height - h - 2) + m.syncedWorklogList.SetHeight(msg.Height - h - 2) if !m.helpVPReady { m.helpVP = viewport.New(w-5, m.terminalHeight-7) m.helpVP.HighPerformanceRendering = useHighPerformanceRenderer @@ -349,6 +368,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.worklogList.SetItems(items) } + case SyncedLogEntriesFetchedMsg: + if msg.err != nil { + message := msg.err.Error() + m.message = "Error fetching synced worklog entries: " + message + m.messages = append(m.messages, message) + } else { + var items []list.Item + for _, e := range msg.entries { + items = append(items, list.Item(e)) + } + m.syncedWorklogList.SetItems(items) + } case UpdateEntryMsg: if msg.err != nil { message := msg.err.Error() @@ -426,6 +457,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case WorklogView: m.worklogList, cmd = m.worklogList.Update(msg) cmds = append(cmds, cmd) + case SyncedWorklogView: + m.syncedWorklogList, cmd = m.syncedWorklogList.Update(msg) + cmds = append(cmds, cmd) case HelpView: m.helpVP, cmd = m.helpVP.Update(msg) cmds = append(cmds, cmd) diff --git a/ui/utils.go b/ui/utils.go index 42dff45..2458f36 100644 --- a/ui/utils.go +++ b/ui/utils.go @@ -77,7 +77,7 @@ func fetchEntries(db *sql.DB) ([]WorklogEntry, error) { SELECT ID, issue_key, begin_ts, end_ts, comment, active, synced FROM issue_log WHERE active=false AND synced=false -ORDER by begin_ts ASC; +ORDER by begin_ts DESC; `) if err != nil { return nil, err @@ -103,6 +103,38 @@ ORDER by begin_ts ASC; return logEntries, nil } +func fetchSyncedEntries(db *sql.DB) ([]SyncedWorklogEntry, error) { + + var logEntries []SyncedWorklogEntry + + rows, err := db.Query(` +SELECT ID, issue_key, begin_ts, end_ts, comment +FROM issue_log +WHERE active=false AND synced=true +ORDER by begin_ts DESC LIMIT 30; + `) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var entry SyncedWorklogEntry + err = rows.Scan(&entry.Id, + &entry.IssueKey, + &entry.BeginTS, + &entry.EndTS, + &entry.Comment, + ) + if err != nil { + return nil, err + } + logEntries = append(logEntries, entry) + + } + return logEntries, nil +} + func deleteEntry(db *sql.DB, id int) error { stmt, err := db.Prepare(` diff --git a/ui/view.go b/ui/view.go index 2ddf944..958adec 100644 --- a/ui/view.go +++ b/ui/view.go @@ -32,6 +32,8 @@ func (m model) View() string { content = stackListStyle.Render(m.issueList.View()) case WorklogView: content = stackListStyle.Render(m.worklogList.View()) + case SyncedWorklogView: + content = stackListStyle.Render(m.syncedWorklogList.View()) case AskForCommentView: content = fmt.Sprintf( `