Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved markdown output #271

Merged
merged 2 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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()

Check warning on line 182 in pkg/output/time-entry/default.go

View check run for this annotation

Codecov / codecov/patch

pkg/output/time-entry/default.go#L182

Added line #L182 was not covered by tests
}
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
Loading