Skip to content

Commit

Permalink
feat: improved markdown output (#271)
Browse files Browse the repository at this point in the history
* feat: improved markdown output

* fix: not used
  • Loading branch information
lucassabreu authored Jun 15, 2024
1 parent a292ee7 commit 90eba3b
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 42 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- markdown output now tries to resemble the time entry calendar dialog

## [v0.53.1] - 2024-06-14

### Fixed
Expand Down
38 changes: 33 additions & 5 deletions api/dto/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dto

import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
Expand Down Expand Up @@ -41,8 +42,18 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return errors.Wrap(err, "unmarshal duration")
}

dc, err := StringToDuration(s)
if err != nil {
return err
}

*d = Duration{dc}
return err
}

func StringToDuration(s string) (time.Duration, error) {
if len(s) < 4 {
return errors.Errorf("duration %s is invalid", b)
return 0, errors.Errorf("duration %s is invalid", s)
}

var u, dc time.Duration
Expand All @@ -64,18 +75,35 @@ func (d *Duration) UnmarshalJSON(b []byte) error {

v, err := strconv.Atoi(s[j:i])
if err != nil {
return errors.Wrap(err, "unmarshal duration")
return 0, errors.Wrap(err, "cast cast "+s[j:i]+" to int")
}
dc = dc + time.Duration(v)*u
j = i + 1
}

*d = Duration{Duration: dc}
return nil
return dc, nil
}

func (d Duration) String() string {
return "PT" + strings.ToUpper(d.Duration.String())
s := d.Duration.String()
i := strings.LastIndex(s, ".")
if i > -1 {
s = s[0:i] + "s"
}

return "PT" + strings.ToUpper(s)
}

func (dd Duration) HumanString() string {
d := dd.Duration
p := ""
if d < 0 {
p = "-"
d = d * -1
}

return p + fmt.Sprintf("%d:%02d:%02d",
int64(d.Hours()), int64(d.Minutes())%60, int64(d.Seconds())%60)
}

type pagination struct {
Expand Down
9 changes: 1 addition & 8 deletions pkg/output/time-entry/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,5 @@ func tagsToStringSlice(tags []dto.Tag) []string {
}

func durationToString(d time.Duration) string {
p := ""
if d < 0 {
p = "-"
d = d * -1
}

return p + fmt.Sprintf("%d:%02d:%02d",
int64(d.Hours()), int64(d.Minutes())%60, int64(d.Seconds())%60)
return dto.Duration{Duration: d}.HumanString()
}
2 changes: 1 addition & 1 deletion pkg/output/time-entry/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/lucassabreu/clockify-cli/api/dto"
)

//go:embed template.gotmpl.md
//go:embed markdown.gotmpl.md
var mdTemplate string

// TimeEntriesMarkdownPrint will print time entries in "markdown blocks"
Expand Down
43 changes: 43 additions & 0 deletions pkg/output/time-entry/markdown.gotmpl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{{- $project := "" -}}
{{- if eq .Project nil }}
{{- $project = "No Project" -}}
{{- else -}}
{{- $project = concat "**" .Project.Name "**" -}}
{{- if ne .Task nil -}}
{{- $project = concat $project ": " .Task.Name -}}
{{- else if ne .Project.ClientName "" -}}
{{- $project = concat $project " - " .Project.ClientName -}}
{{- end -}}
{{- end -}}

{{- $bil := "No" -}}
{{- if .Billable -}}{{ $bil = "Yes" }}{{- end -}}

{{- $tags := "" -}}
{{- with .Tags -}}
{{- range $index, $element := . -}}
{{- if ne $index 0 }}{{ $tags = concat $tags ", " }}{{ end -}}
{{- $tags = concat $tags $element.Name -}}
{{- end -}}
{{- else -}}
{{- $tags = "No Tags" -}}
{{- end -}}

{{- $pad := maxLength .Description $project $tags $bil -}}

## _Time Entry_: {{ .ID }}

_Time and date_
**{{ dsf .TimeInterval.Duration }}** | {{ if eq .TimeInterval.End nil -}}
Start Time: _{{ formatTimeWS .TimeInterval.Start }}_ 🗓 Today
{{- else -}}
{{ formatTimeWS .TimeInterval.Start }} - {{ formatTimeWS .TimeInterval.End }} 🗓
{{- .TimeInterval.Start.Format " 01/02/2006" }}
{{- end }}

| | {{ pad "" $pad }} |
|---------------|-{{ repeatString "-" $pad }}-|
| _Description_ | {{ pad .Description $pad }} |
| _Project_ | {{ pad $project $pad }} |
| _Tags_ | {{ pad $tags $pad }} |
| _Billable_ | {{ pad $bil $pad }} |
253 changes: 253 additions & 0 deletions pkg/output/time-entry/markdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package timeentry_test

import (
"strings"
"testing"
"time"

"github.com/MakeNowJust/heredoc"
"github.com/lucassabreu/clockify-cli/api/dto"
timeentry "github.com/lucassabreu/clockify-cli/pkg/output/time-entry"
"github.com/lucassabreu/clockify-cli/pkg/timehlp"
"github.com/stretchr/testify/assert"
)

func TestTimeEntriesMarkdownPrint(t *testing.T) {
t65Min1SecAgo, _ := timehlp.ConvertToTime("-65m1s")
start, _ := time.Parse(timehlp.FullTimeFormat, "2024-06-15 10:00:01")
end := start.Add(2*time.Minute + 1*time.Second)

tts := []struct {
name string
tes []dto.TimeEntry
output string
}{
{
name: "open without tags or project",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: false,
Description: "Open and without project",
TimeInterval: dto.NewTimeInterval(t65Min1SecAgo, nil),
}},
output: heredoc.Docf(`
## _Time Entry_: te1
_Time and date_
**1:05:01** | Start Time: _%s_ 🗓 Today
| | |
|---------------|--------------------------|
| _Description_ | Open and without project |
| _Project_ | No Project |
| _Tags_ | No Tags |
| _Billable_ | No |
`, t65Min1SecAgo.UTC().Format(timehlp.SimplerOnlyTimeFormat)),
},
{
name: "closed without tags or project",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: false,
Description: "Closed and without project",
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|----------------------------|
| _Description_ | Closed and without project |
| _Project_ | No Project |
| _Tags_ | No Tags |
| _Billable_ | No |
`),
},
{
name: "Closed with project",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: false,
Description: "With project",
Project: &dto.Project{
Name: "Project Name",
},
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|------------------|
| _Description_ | With project |
| _Project_ | **Project Name** |
| _Tags_ | No Tags |
| _Billable_ | No |
`),
},
{
name: "Closed with project with client",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: true,
Description: "With project",
Project: &dto.Project{
Name: "Project Name",
ClientName: "Client Name",
},
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|--------------------------------|
| _Description_ | With project |
| _Project_ | **Project Name** - Client Name |
| _Tags_ | No Tags |
| _Billable_ | Yes |
`),
},
{
name: "Closed with project, client and task",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: true,
Description: "With project",
Project: &dto.Project{
Name: "Project Name",
ClientName: "Client Name",
},
Task: &dto.Task{
Name: "Task Name",
},
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|-----------------------------|
| _Description_ | With project |
| _Project_ | **Project Name**: Task Name |
| _Tags_ | No Tags |
| _Billable_ | Yes |
`),
},
{
name: "Closed with project, client, task and a tag",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: true,
Description: "With project",
Project: &dto.Project{
Name: "Project Name",
ClientName: "Client Name",
},
Task: &dto.Task{
Name: "Task Name",
},
Tags: []dto.Tag{
{Name: "Stand-up Meeting"},
},
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|-----------------------------|
| _Description_ | With project |
| _Project_ | **Project Name**: Task Name |
| _Tags_ | Stand-up Meeting |
| _Billable_ | Yes |
`),
},
{
name: "Closed with project, client, task and tags",
tes: []dto.TimeEntry{{
WorkspaceID: "w1",
ID: "te1",
Billable: true,
Description: "With project",
Project: &dto.Project{
Name: "Project Name",
ClientName: "Client Name",
},
Task: &dto.Task{
Name: "Task Name",
},
Tags: []dto.Tag{
{Name: "A Tag with long name"},
{Name: "Normal tag"},
},
TimeInterval: dto.NewTimeInterval(
start,
&end,
),
}},
output: heredoc.Doc(`
## _Time Entry_: te1
_Time and date_
**0:02:01** | 10:00 - 10:02 🗓 06/15/2024
| | |
|---------------|----------------------------------|
| _Description_ | With project |
| _Project_ | **Project Name**: Task Name |
| _Tags_ | A Tag with long name, Normal tag |
| _Billable_ | Yes |
`),
},
}

for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
buffer := &strings.Builder{}
err := timeentry.TimeEntriesMarkdownPrint(tt.tes, buffer)

if !assert.NoError(t, err) {
return
}

assert.Equal(t, tt.output+"\n", buffer.String())
})
}
}
Loading

0 comments on commit 90eba3b

Please sign in to comment.