Skip to content
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- support to limit how many time entries should be listed on the `report` commands, and choose which page to
show

## [v0.54.2] - 2025-06-25

### Fixed
Expand Down
39 changes: 39 additions & 0 deletions pkg/cmd/time-entry/report/today/today_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,45 @@ func TestCmdToday(t *testing.T) {
time-entry-2
`),
},
{
name: "report only the first time entry",
args: "--limit 2 --page 10 -q",
factory: func(t *testing.T) cmdutil.Factory {
f := mocks.NewMockFactory(t)
f.On("GetUserID").Return("user-id", nil)
f.On("GetWorkspaceID").Return("w-id", nil)

f.On("Config").Return(&mocks.SimpleConfig{})

c := mocks.NewMockClient(t)
f.On("Client").Return(c, nil)

c.On("LogRange", api.LogRangeParam{
Workspace: "w-id",
UserID: "user-id",
FirstDate: first,
LastDate: last,
TagIDs: []string{},
PaginationParam: api.PaginationParam{
Page: 10,
PageSize: 2,
},
}).
Return(
[]dto.TimeEntry{
{ID: "time-entry-1"},
{ID: "time-entry-2"},
},
nil,
)

return f
},
expected: heredoc.Doc(`
time-entry-1
time-entry-2
`),
},
}

for i := range tts {
Expand Down
40 changes: 38 additions & 2 deletions pkg/cmd/time-entry/report/util/report.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package util

import (
"errors"
"io"
"sort"
"time"
Expand All @@ -27,6 +28,8 @@ type ReportFlags struct {
util.OutputFlags

FillMissingDates bool
Limit int
Page int

Billable bool
NotBillable bool
Expand All @@ -43,6 +46,18 @@ func (rf ReportFlags) Check() error {
return err
}

if rf.Page > 0 && rf.Limit <= 0 {
return cmdutil.FlagErrorWrap(
errors.New("page can't be used without limit"))
}

if err := cmdutil.XorFlag(map[string]bool{
"limit": rf.Limit > 0,
"fill-missing-dates": rf.FillMissingDates,
}); err != nil {
return err
}

return cmdutil.XorFlag(map[string]bool{
"billable": rf.Billable,
"not-billable": rf.NotBillable,
Expand All @@ -52,6 +67,7 @@ func (rf ReportFlags) Check() error {
// NewReportFlags helps creating a util.ReportFlags for report commands
func NewReportFlags() ReportFlags {
return ReportFlags{
Limit: 0,
OutputFlags: util.OutputFlags{
TimeFormat: timehlp.FullTimeFormat,
},
Expand All @@ -65,8 +81,12 @@ func AddReportFlags(
util.AddPrintTimeEntriesFlags(cmd, &rf.OutputFlags)
util.AddPrintMultipleTimeEntriesFlags(cmd)

cmd.Flags().IntVarP(&rf.Page, "page", "P", 0,
"set which page to return")
cmd.Flags().IntVarP(&rf.Limit, "limit", "l", 0,
"Only look for this quantity of time entries")
cmd.Flags().BoolVarP(&rf.FillMissingDates, "fill-missing-dates", "e", false,
"add empty lines for dates without time entries")
"Add empty lines for dates without time entries")
cmd.Flags().StringVarP(&rf.Description, "description", "d", "",
"will filter time entries that contains this on the description field")
cmd.Flags().StringSliceVarP(&rf.Projects, "project", "p", []string{},
Expand Down Expand Up @@ -157,6 +177,18 @@ func ReportWithRange(
wg := errgroup.Group{}
logs := make([][]dto.TimeEntry, len(rf.Projects))

pages := api.AllPages()
if rf.Limit > 0 {
pages = api.PaginationParam{
Page: 1,
PageSize: rf.Limit,
}

if rf.Page > 0 {
pages.Page = rf.Page
}
}

for i := range rf.Projects {
i := i
wg.Go(func() error {
Expand All @@ -169,7 +201,7 @@ func ReportWithRange(
Description: rf.Description,
ProjectID: rf.Projects[i],
TagIDs: rf.TagIDs,
PaginationParam: api.AllPages(),
PaginationParam: pages,
})

return err
Expand All @@ -195,6 +227,10 @@ func ReportWithRange(
)
})

if rf.Limit > 0 && len(log) > rf.Limit {
log = log[len(log)-rf.Limit:]
}

if rf.FillMissingDates && len(log) > 0 {
l := log
log = make([]dto.TimeEntry, 0, len(l))
Expand Down
115 changes: 82 additions & 33 deletions pkg/cmd/time-entry/report/util/report_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,91 @@ import (
"github.com/stretchr/testify/assert"
)

func TestReportBillableFlagsChecks(t *testing.T) {
rf := util.NewReportFlags()
rf.Billable = true
rf.NotBillable = true

err := rf.Check()
if assert.Error(t, err) {
assert.Regexp(t,
"can't be used together.*billable.*not-billable", err.Error())
func TestReportFlags_Check(t *testing.T) {
tts := map[string]struct {
rf util.ReportFlags
err string
}{
"just billable": {
rf: util.ReportFlags{
Billable: true,
NotBillable: false,
},
},
"just not-billable": {
rf: util.ReportFlags{
Billable: false,
NotBillable: true,
},
},
"only one billable": {
rf: util.ReportFlags{
Billable: true,
NotBillable: true,
},
err: "can't be used together.*billable.*not-billable",
},
"just client": {
rf: util.ReportFlags{
Client: "me",
},
},
"just projects": {
rf: util.ReportFlags{
Projects: []string{"mine"},
},
},
"client and project": {
rf: util.ReportFlags{
Client: "me",
Projects: []string{"mine"},
},
},
"fill missing dates": {
rf: util.ReportFlags{
FillMissingDates: true,
},
},
"limit": {
rf: util.ReportFlags{
Limit: 10,
},
},
"only limit or fill missing": {
rf: util.ReportFlags{
Limit: 10,
FillMissingDates: true,
},
err: "can't be used together.*fill-missing-dates.*limit",
},
"limit and page": {
rf: util.ReportFlags{
Limit: 10,
Page: 10,
},
},
"page needs limit": {
rf: util.ReportFlags{
Page: 10,
},
err: "page can't be used without limit",
},
}

rf.Billable = false
rf.NotBillable = true

assert.NoError(t, rf.Check())

rf.Billable = true
rf.NotBillable = false

assert.NoError(t, rf.Check())
}
for name, tt := range tts {
t.Run(name, func(t *testing.T) {
err := tt.rf.Check()

func TestReportProjectFlagsChecks(t *testing.T) {
rf := util.NewReportFlags()
rf.Client = "me"
rf.Projects = []string{}
if tt.err == "" {
assert.NoError(t, err)
return
}

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

rf.Client = ""
rf.Projects = []string{"mine"}

assert.NoError(t, rf.Check())

rf.Client = "me"
rf.Projects = []string{"mine"}

assert.NoError(t, rf.Check())
assert.Regexp(t, tt.err, err.Error())
})
}
}
Loading
Loading