Skip to content

Commit

Permalink
feat: allow fallback comment (#27)
Browse files Browse the repository at this point in the history
Allow configuring an optional "fallback comment", which
will be used when the user creates work log entries without
a comment.
  • Loading branch information
dhth authored Jan 14, 2025
1 parent a21d431 commit 1942a38
Show file tree
Hide file tree
Showing 15 changed files with 136 additions and 51 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 11 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
9 changes: 6 additions & 3 deletions internal/common/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
DefaultBackgroundColor = "#282828"
issueStatusColor = "#665c54"
needsCommentColor = "#fb4934"
FallbackCommentColor = "#83a598"
syncedColor = "#b8bb26"
syncingColor = "#fabd2f"
notSyncedColor = "#928374"
Expand All @@ -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))
Expand Down
33 changes: 19 additions & 14 deletions internal/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -111,6 +112,7 @@ func (entry WorklogEntry) Description() string {
}

var syncedStatus string
var fallbackCommentStatus string
var durationMsg string

now := time.Now()
Expand All @@ -129,21 +131,24 @@ 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")
} else {
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 }
Expand Down
20 changes: 20 additions & 0 deletions internal/persistence/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 35 additions & 13 deletions internal/ui/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}
}
}

Expand Down
3 changes: 2 additions & 1 deletion internal/ui/initial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions internal/ui/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (

var (
helpMsgStyle = lipgloss.NewStyle().
PaddingLeft(1).
PaddingLeft(2).
Bold(true).
Foreground(lipgloss.Color(helpMsgColor))

Expand Down
4 changes: 2 additions & 2 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
15 changes: 12 additions & 3 deletions internal/ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
Loading

0 comments on commit 1942a38

Please sign in to comment.