diff --git a/README.md b/README.md index 66fd1e0..0148f74 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,9 @@ jql = "assignee = currentUser() AND updatedDate >= -14d ORDER BY updatedDate DES # I need this, since the JIRA on-premise server I use runs 5 hours behind # the actual time, for whatever reason 🤷 jira_time_delta_mins = 300 + +# this comment will be used for worklogs when you don't provide one; optional" +fallback_comment = "comment" ``` ### Basic usage diff --git a/cmd/config.go b/cmd/config.go index cd5ae07..1857afb 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -16,6 +16,7 @@ type JiraConfig struct { JiraTimeDeltaMins int `toml:"jira_time_delta_mins"` JiraToken *string `toml:"jira_token"` JiraUsername *string `toml:"jira_username"` + FallbackComment *string `toml:"fallback_comment"` } type POConfig struct { diff --git a/cmd/root.go b/cmd/root.go index c7552b8..007c77a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" jiraCloud "github.com/andygrunwald/go-jira/v2/cloud" jiraOnPremise "github.com/andygrunwald/go-jira/v2/onpremise" @@ -28,6 +29,7 @@ var ( jiraToken = flag.String("jira-token", "", "jira token (PAT for on-premise installation, API token for cloud installation)") jiraUsername = flag.String("jira-username", "", "username for authentication") jql = flag.String("jql", "", "JQL to use to query issues") + fallbackComment = flag.String("fallback-comment", "", "Fallback comment to use for worklog entries") jiraTimeDeltaMinsStr = flag.String("jira-time-delta-mins", "", "Time delta (in minutes) between your timezone and the timezone of the server; can be +/-") listConfig = flag.Bool("list-config", false, "print the config that punchout will use") ) @@ -128,6 +130,10 @@ func Execute() error { cfg.Jira.JiraTimeDeltaMins = jiraTimeDeltaMins } + if *fallbackComment != "" { + cfg.Jira.FallbackComment = fallbackComment + } + // validations var installationType ui.JiraInstallationType switch cfg.Jira.InstallationType { @@ -156,6 +162,10 @@ func Execute() error { return fmt.Errorf("jira-username cannot be empty for cloud installation") } + if cfg.Jira.FallbackComment != nil && strings.TrimSpace(*cfg.Jira.FallbackComment) == "" { + return fmt.Errorf("fallback-comment cannot be empty") + } + configKeyMaxLen := 40 if *listConfig { fmt.Fprint(os.Stdout, "Config:\n\n") @@ -207,5 +217,5 @@ func Execute() error { return fmt.Errorf("%w: %s", errCouldntCreateJiraClient, err.Error()) } - return ui.RenderUI(db, cl, installationType, *cfg.Jira.JQL, cfg.Jira.JiraTimeDeltaMins) + return ui.RenderUI(db, cl, installationType, *cfg.Jira.JQL, cfg.Jira.JiraTimeDeltaMins, cfg.Jira.FallbackComment) } diff --git a/internal/common/styles.go b/internal/common/styles.go index 43f2393..7ea76c6 100644 --- a/internal/common/styles.go +++ b/internal/common/styles.go @@ -10,6 +10,7 @@ const ( DefaultBackgroundColor = "#282828" issueStatusColor = "#665c54" needsCommentColor = "#fb4934" + FallbackCommentColor = "#83a598" syncedColor = "#b8bb26" syncingColor = "#fabd2f" notSyncedColor = "#928374" @@ -27,10 +28,12 @@ var ( statusStyle = BaseStyle. Bold(true). Align(lipgloss.Center). - Width(18) + Width(14) - needsCommentStyle = statusStyle. - Background(lipgloss.Color(needsCommentColor)) + usingFallbackCommentStyle = statusStyle. + Width(20). + MarginLeft(2). + Background(lipgloss.Color(FallbackCommentColor)) syncedStyle = statusStyle. Background(lipgloss.Color(syncedColor)) diff --git a/internal/common/types.go b/internal/common/types.go index 3a42486..f7b6728 100644 --- a/internal/common/types.go +++ b/internal/common/types.go @@ -66,15 +66,16 @@ func (issue Issue) Description() string { func (issue Issue) FilterValue() string { return issue.IssueKey } type WorklogEntry struct { - ID int - IssueKey string - BeginTS time.Time - EndTS *time.Time - Comment *string - Active bool - Synced bool - SyncInProgress bool - Error error + ID int + IssueKey string + BeginTS time.Time + EndTS *time.Time + Comment *string + FallbackComment *string + Active bool + Synced bool + SyncInProgress bool + Error error } type SyncedWorklogEntry struct { @@ -111,6 +112,7 @@ func (entry WorklogEntry) Description() string { } var syncedStatus string + var fallbackCommentStatus string var durationMsg string now := time.Now() @@ -129,9 +131,7 @@ func (entry WorklogEntry) Description() string { timeSpentStr := HumanizeDuration(int(entry.EndTS.Sub(entry.BeginTS).Seconds())) - if entry.NeedsComment() { - syncedStatus = needsCommentStyle.Render("needs comment") - } else if entry.Synced { + if entry.Synced { syncedStatus = syncedStyle.Render("synced") } else if entry.SyncInProgress { syncedStatus = syncingStyle.Render("syncing") @@ -139,11 +139,16 @@ func (entry WorklogEntry) Description() string { syncedStatus = notSyncedStyle.Render("not synced") } - return fmt.Sprintf("%s%s%s%s", + if entry.NeedsComment() && entry.FallbackComment != nil { + fallbackCommentStatus = usingFallbackCommentStyle.Render("fallback comment") + } + + return fmt.Sprintf("%s%s%s%s%s", RightPadTrim(entry.IssueKey, listWidth/4), RightPadTrim(durationMsg, listWidth/4), - RightPadTrim(fmt.Sprintf("(%s)", timeSpentStr), listWidth/4), + RightPadTrim(fmt.Sprintf("(%s)", timeSpentStr), listWidth/6), syncedStatus, + fallbackCommentStatus, ) } func (entry WorklogEntry) FilterValue() string { return entry.IssueKey } diff --git a/internal/persistence/queries.go b/internal/persistence/queries.go index fe9684d..71b1eb1 100644 --- a/internal/persistence/queries.go +++ b/internal/persistence/queries.go @@ -235,6 +235,26 @@ WHERE id = ?; return nil } +func UpdateSyncStatusAndComment(db *sql.DB, id int, comment string) error { + stmt, err := db.Prepare(` +UPDATE issue_log +SET synced = 1, + comment = ? +WHERE id = ?; +`) + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(comment, id) + if err != nil { + return err + } + + return nil +} + func DeleteActiveLogInDB(db *sql.DB) error { stmt, err := db.Prepare(` DELETE FROM issue_log diff --git a/internal/ui/cmds.go b/internal/ui/cmds.go index 6a92914..8effc5a 100644 --- a/internal/ui/cmds.go +++ b/internal/ui/cmds.go @@ -15,10 +15,7 @@ import ( _ "modernc.org/sqlite" // sqlite driver ) -var ( - errWorklogsEndTSIsEmpty = errors.New("worklog's end timestamp is empty") - errWorklogsCommentIsEmpty = errors.New("worklog's comment is empty") -) +var errWorklogsEndTSIsEmpty = errors.New("worklog's end timestamp is empty") func toggleTracking(db *sql.DB, selectedIssue string, beginTs, endTs time.Time, comment string) tea.Cmd { return func() tea.Msg { @@ -178,9 +175,19 @@ func deleteLogEntry(db *sql.DB, id int) tea.Cmd { } } -func updateSyncStatusForEntry(db *sql.DB, entry common.WorklogEntry, index int) tea.Cmd { +func updateSyncStatusForEntry(db *sql.DB, entry common.WorklogEntry, index int, fallbackCommentUsed bool) tea.Cmd { return func() tea.Msg { - err := pers.UpdateSyncStatus(db, entry.ID) + var err error + var comment string + if entry.Comment != nil { + comment = *entry.Comment + } + if fallbackCommentUsed { + err = pers.UpdateSyncStatusAndComment(db, entry.ID, comment) + } else { + err = pers.UpdateSyncStatus(db, entry.ID) + } + return logEntrySyncUpdated{ entry: entry, index: index, @@ -226,18 +233,33 @@ func fetchJIRAIssues(cl *jira.Client, jql string) tea.Cmd { } } -func syncWorklogWithJIRA(cl *jira.Client, entry common.WorklogEntry, index int, timeDeltaMins int) tea.Cmd { +func syncWorklogWithJIRA(cl *jira.Client, entry common.WorklogEntry, fallbackComment *string, index int, timeDeltaMins int) tea.Cmd { return func() tea.Msg { + var fallbackCmtUsed bool if entry.EndTS == nil { - return wlAddedOnJIRA{index, entry, errWorklogsEndTSIsEmpty} + return wlAddedOnJIRA{index, entry, fallbackCmtUsed, errWorklogsEndTSIsEmpty} } - if entry.Comment == nil { - return wlAddedOnJIRA{index, entry, errWorklogsCommentIsEmpty} - } + // if entry.Comment == nil && fallbackComment == nil { + // return wlAddedOnJIRA{index, entry, fallbackCmtUsed, errWorklogsCommentIsEmpty} + // } - err := addWLtoJira(cl, entry.IssueKey, entry.BeginTS, *entry.EndTS, *entry.Comment, timeDeltaMins) - return wlAddedOnJIRA{index, entry, err} + var comment string + if entry.NeedsComment() && fallbackComment != nil { + comment = *fallbackComment + fallbackCmtUsed = true + } else { + comment = *entry.Comment + } + // if !entry.NeedsComment() { + // comment = *entry.Comment + // } else { + // comment = *fallbackComment + // fallbackCmtUsed = true + // } + + err := addWLtoJira(cl, entry.IssueKey, entry.BeginTS, *entry.EndTS, comment, timeDeltaMins) + return wlAddedOnJIRA{index, entry, fallbackCmtUsed, err} } } diff --git a/internal/ui/initial.go b/internal/ui/initial.go index f2f95b1..36a5603 100644 --- a/internal/ui/initial.go +++ b/internal/ui/initial.go @@ -10,7 +10,7 @@ import ( c "github.com/dhth/punchout/internal/common" ) -func InitialModel(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int, debug bool) Model { +func InitialModel(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int, fallbackComment *string, debug bool) Model { var stackItems []list.Item var worklogListItems []list.Item var syncedWorklogListItems []list.Item @@ -39,6 +39,7 @@ func InitialModel(db *sql.DB, jiraClient *jira.Client, installationType JiraInst jiraClient: jiraClient, installationType: installationType, jql: jql, + fallbackComment: fallbackComment, issueList: list.New(stackItems, newItemDelegate(lipgloss.Color(issueListColor)), listWidth, 0), issueMap: make(map[string]*c.Issue), issueIndexMap: make(map[string]int), diff --git a/internal/ui/model.go b/internal/ui/model.go index b2d7bee..32da197 100644 --- a/internal/ui/model.go +++ b/internal/ui/model.go @@ -73,6 +73,7 @@ type Model struct { jiraClient *jira.Client installationType JiraInstallationType jql string + fallbackComment *string issueList list.Model issueMap map[string]*c.Issue issueIndexMap map[string]int diff --git a/internal/ui/msgs.go b/internal/ui/msgs.go index 7f507a6..335138c 100644 --- a/internal/ui/msgs.go +++ b/internal/ui/msgs.go @@ -69,9 +69,10 @@ type issuesFetchedFromJIRAMsg struct { } type wlAddedOnJIRA struct { - index int - entry c.WorklogEntry - err error + index int + entry c.WorklogEntry + fallbackCommentUsed bool + err error } type urlOpenedinBrowserMsg struct { diff --git a/internal/ui/styles.go b/internal/ui/styles.go index c4932cc..cd77909 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -29,7 +29,7 @@ const ( var ( helpMsgStyle = lipgloss.NewStyle(). - PaddingLeft(1). + PaddingLeft(2). Bold(true). Foreground(lipgloss.Color(helpMsgColor)) diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 90ac38c..459d526 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -8,7 +8,7 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -func RenderUI(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int) error { +func RenderUI(db *sql.DB, jiraClient *jira.Client, installationType JiraInstallationType, jql string, jiraTimeDeltaMins int, fallbackComment *string) error { if len(os.Getenv("DEBUG_LOG")) > 0 { f, err := tea.LogToFile("debug.log", "debug") if err != nil { @@ -18,7 +18,7 @@ func RenderUI(db *sql.DB, jiraClient *jira.Client, installationType JiraInstalla } debug := os.Getenv("DEBUG") == "true" - p := tea.NewProgram(InitialModel(db, jiraClient, installationType, jql, jiraTimeDeltaMins, debug), tea.WithAltScreen()) + p := tea.NewProgram(InitialModel(db, jiraClient, installationType, jql, jiraTimeDeltaMins, fallbackComment, debug), tea.WithAltScreen()) if _, err := p.Run(); err != nil { return err } diff --git a/internal/ui/update.go b/internal/ui/update.go index 641c447..bf08f0e 100644 --- a/internal/ui/update.go +++ b/internal/ui/update.go @@ -468,10 +468,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { toSyncNum := 0 for i, entry := range m.worklogList.Items() { if wl, ok := entry.(c.WorklogEntry); ok { - if !wl.Synced && !wl.NeedsComment() { + // needsComment := wl.NeedsComment() + // a worklog entry must be enqueued for syncing to jira if + // - it's not already synced + // - (it has a comment) or (it doesn't have a comment, but there's a fallback comment configured) + if !wl.Synced { + // && (!needsComment || (needsComment && m.fallbackComment != nil)) { wl.SyncInProgress = true m.worklogList.SetItem(i, wl) - cmds = append(cmds, syncWorklogWithJIRA(m.jiraClient, wl, i, m.jiraTimeDeltaMins)) + cmds = append(cmds, syncWorklogWithJIRA(m.jiraClient, wl, m.fallbackComment, i, m.jiraTimeDeltaMins)) toSyncNum++ } } @@ -586,6 +591,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var secsSpent int for _, e := range msg.entries { secsSpent += e.SecsSpent() + e.FallbackComment = m.fallbackComment items = append(items, list.Item(e)) } m.worklogList.SetItems(items) @@ -668,7 +674,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } else { msg.entry.Synced = true msg.entry.SyncInProgress = false - cmds = append(cmds, updateSyncStatusForEntry(m.db, msg.entry, msg.index)) + if msg.fallbackCommentUsed { + msg.entry.Comment = m.fallbackComment + } + cmds = append(cmds, updateSyncStatusForEntry(m.db, msg.entry, msg.index, msg.fallbackCommentUsed)) } m.worklogList.SetItem(msg.index, msg.entry) case trackingToggledMsg: diff --git a/internal/ui/view.go b/internal/ui/view.go index b5e302b..c0e80e4 100644 --- a/internal/ui/view.go +++ b/internal/ui/view.go @@ -19,6 +19,12 @@ func (m Model) View() string { statusBar = c.Trim(m.message, 120) } var activeMsg string + + var fallbackCommentMsg string + if m.fallbackComment != nil { + fallbackCommentMsg = " (a fallback is configured)" + } + if m.issuesFetched { if m.activeIssue != "" { var issueSummaryMsg, trackingSinceMsg string @@ -41,9 +47,9 @@ func (m Model) View() string { // first time help if m.activeView == issueListView && len(m.syncedWorklogList.Items()) == 0 && m.unsyncedWLCount == 0 { if m.trackingActive { - helpMsg += " " + initialHelpMsgStyle.Render("Press s to stop tracking time") + helpMsg += initialHelpMsgStyle.Render("Press s to stop tracking time") } else { - helpMsg += " " + initialHelpMsgStyle.Render("Press s to start tracking time") + helpMsg += initialHelpMsgStyle.Render("Press s to start tracking time") } } } @@ -87,7 +93,7 @@ func (m Model) View() string { formFieldNameStyle.Render("End Time* (format: 2006/01/02 15:04)"), m.trackingInputs[entryEndTS].View(), formHelpStyle.Render("(k/j/K/J/h/l moves time, when correct)"), - formFieldNameStyle.Render("Comment (you can add this later as well)"), + formFieldNameStyle.Render(fmt.Sprintf("Comment%s", fallbackCommentMsg)), m.trackingInputs[entryComment].View(), formContextStyle.Render("Press enter to submit"), ) @@ -132,7 +138,7 @@ func (m Model) View() string { formFieldNameStyle.Render("End Time* (format: 2006/01/02 15:04)"), m.trackingInputs[entryEndTS].View(), formHelpStyle.Render("(k/j/K/J moves time, when correct)"), - formFieldNameStyle.Render("Comment"), + formFieldNameStyle.Render(fmt.Sprintf("Comment%s", fallbackCommentMsg)), m.trackingInputs[entryComment].View(), formContextStyle.Render("Press enter to submit"), ) @@ -152,7 +158,7 @@ func (m Model) View() string { Background(lipgloss.Color("#7c6f64")) if m.showHelpIndicator { - helpMsg += " " + helpMsgStyle.Render("Press ? for help") + helpMsg += helpMsgStyle.Render("Press ? for help") } var unsyncedMsg string @@ -164,6 +170,7 @@ func (m Model) View() string { unsyncedTimeMsg := c.HumanizeDuration(m.unsyncedWLSecsSpent) unsyncedMsg = unsyncedCountStyle.Render(fmt.Sprintf("%d unsynced %s (%s)", m.unsyncedWLCount, entryWord, unsyncedTimeMsg)) } + footerStr := fmt.Sprintf("%s%s%s%s", modeStyle.Render("punchout"), helpMsg, diff --git a/tests/test.sh b/tests/test.sh index e640abc..05c166f 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash cat <