Skip to content

Commit

Permalink
feat: time entry commands allow client filter for project
Browse files Browse the repository at this point in the history
  • Loading branch information
lucassabreu committed Feb 9, 2024
1 parent 737626c commit de75011
Show file tree
Hide file tree
Showing 17 changed files with 399 additions and 32 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]

### Added

- new flag `--client` to filter projects by client when managing time entries

## [v0.46.0] - 2023-12-06

### Added
Expand Down
5 changes: 0 additions & 5 deletions pkg/cmd/project/edit/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,6 @@ func NewCmdEdit(
return err
}

if ids, err = search.GetProjectsByName(
c, w, ids); err != nil {
return err
}

if client != nil && *client != "" {
if *client, err = search.GetClientByName(
c, w, *client); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/task/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func NewCmdDelete(

if f.Config().IsAllowNameForID() {
if project, err = search.GetProjectByName(
c, w, project); err != nil {
c, w, project, ""); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/task/done/done.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func NewCmdDone(

if f.Config().IsAllowNameForID() {
if project, err = search.GetProjectByName(
c, workspace, project); err != nil {
c, workspace, project, ""); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/task/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func NewCmdList(
if f.Config().IsAllowNameForID() &&
p.ProjectID != "" {
if p.ProjectID, err = search.GetProjectByName(
c, workspace, p.ProjectID); err != nil {
c, workspace, p.ProjectID, ""); err != nil {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/task/quick-add/quick-add.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func NewCmdQuickAdd(

p, _ := cmd.Flags().GetString("project")
if f.Config().IsAllowNameForID() {
if p, err = search.GetProjectByName(c, w, p); err != nil {
if p, err = search.GetProjectByName(c, w, p, ""); err != nil {
return err
}
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/task/util/read-flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func TaskReadFlags(cmd *cobra.Command, f cmdutil.Factory) (p FlagsDTO, err error
}

if p.ProjectID, err = search.GetProjectByName(
c, p.Workspace, p.ProjectID); err != nil {
c, p.Workspace, p.ProjectID, ""); err != nil {
return p, err
}

Expand Down
152 changes: 152 additions & 0 deletions pkg/cmd/time-entry/in/in_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package in_test

import (
"bytes"
"errors"
"io"
"testing"
"time"
Expand Down Expand Up @@ -150,6 +151,157 @@ func TestNewCmdIn_ShouldNotSetBillable_WhenNotAsked(t *testing.T) {
return
}

t.Fatalf("err: %s", err)
})
}
}

func TestNewCmdIn_ShouldLookupProject_WithAndWithoutClient(t *testing.T) {
defaultStart := timehlp.Today().Add(8 * time.Hour)

projects := []dto.Project{
{ID: "p1", Name: "first", ClientID: "c1", ClientName: "other"},
{ID: "p2", Name: "second", ClientID: "c2", ClientName: "me"},
{ID: "p3", Name: "second", ClientID: "c3", ClientName: "clockify"},
{ID: "p4", Name: "third"},
{ID: "p5", Name: "notonclient", ClientID: "c3", ClientName: "clockify"},
}

tts := []struct {
name string
args []string
param api.CreateTimeEntryParam
err error
}{
{
name: "only project",
args: []string{"-s=08:00", "-p=first"},
param: api.CreateTimeEntryParam{
Workspace: w.ID,
Start: defaultStart,
ProjectID: projects[0].ID,
},
},
{
name: "project and client",
args: []string{"-s=08:00", "-p=second", "-c=me"},
param: api.CreateTimeEntryParam{
Workspace: w.ID,
Start: defaultStart,
ProjectID: projects[1].ID,
},
},
{
name: "project and other client",
args: []string{"-s=08:00", "-p=second", "-c=clockify"},
param: api.CreateTimeEntryParam{
Workspace: w.ID,
Start: defaultStart,
ProjectID: projects[2].ID,
},
},
{
name: "project without client",
args: []string{"-s=08:00", "-p=third"},
param: api.CreateTimeEntryParam{
Workspace: w.ID,
Start: defaultStart,
ProjectID: projects[3].ID,
},
},
{
name: "project does not exist",
args: []string{"-s=08:00", "-p=notfound"},
err: errors.New(
"No project with id or name containing 'notfound' " +
"was found"),
},
{
name: "project does not exist in this client",
args: []string{"-s=08:00", "-p=notonclient", "-c=me"},
err: errors.New(
"No project with id or name containing 'notonclient' " +
"was found for client 'me'"),
},
}

for i := range tts {
tt := &tts[i]

t.Run(tt.name, func(t *testing.T) {
f := mocks.NewMockFactory(t)

f.EXPECT().GetUserID().Return("u", nil)
f.EXPECT().GetWorkspaceID().Return(w.ID, nil)

f.EXPECT().Config().Return(&mocks.SimpleConfig{
AllowNameForID: true,
})

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

c.EXPECT().GetProjects(api.GetProjectsParam{
Workspace: w.ID,
PaginationParam: api.AllPages(),
}).
Return(projects, nil)

c.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{
Workspace: w.ID,
UserID: "u",
}).
Return(nil, nil)

if tt.err == nil {
c.EXPECT().GetProject(api.GetProjectParam{
Workspace: w.ID,
ProjectID: tt.param.ProjectID,
}).
Return(&dto.Project{ID: tt.param.ProjectID}, nil)

f.EXPECT().GetWorkspace().Return(w, nil)

c.EXPECT().Out(api.OutParam{
Workspace: w.ID,
UserID: "u",
End: tt.param.Start,
}).Return(api.ErrorNotFound)

c.EXPECT().CreateTimeEntry(tt.param).
Return(dto.TimeEntryImpl{ID: "te"}, nil)
}

called := false
cmd := in.NewCmdIn(f, func(
_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {
called = true
return nil
})

cmd.SilenceUsage = true
cmd.SilenceErrors = true

out := bytes.NewBufferString("")
cmd.SetOut(out)
cmd.SetErr(out)

cmd.SetArgs(append(tt.args, "-q"))
_, err := cmd.ExecuteC()

if tt.err != nil {
assert.EqualError(t, err, tt.err.Error())
return
}

t.Cleanup(func() {
assert.True(t, called)
})

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

t.Fatalf("err: %s", err)
})
}
Expand Down
14 changes: 13 additions & 1 deletion 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 Down Expand Up @@ -31,6 +32,7 @@ type ReportFlags struct {
NotBillable bool

Description string
Client string
Project string
TagIDs []string
}
Expand All @@ -41,6 +43,12 @@ func (rf ReportFlags) Check() error {
return err
}

if rf.Client != "" && rf.Project == "" {
return cmdutil.FlagErrorWrap(errors.New(
"flag 'client' can't be used without flag 'project'",
))
}

return cmdutil.XorFlag(map[string]bool{
"billable": rf.Billable,
"not-billable": rf.NotBillable,
Expand Down Expand Up @@ -69,6 +77,10 @@ func AddReportFlags(
"Will filter time entries using this project")
_ = cmdcompl.AddSuggestionsToFlag(cmd, "project",
cmdcomplutil.NewProjectAutoComplete(f))
cmd.Flags().StringVarP(&rf.Client, "client", "c", "",
"Will filter projects from this client")
_ = cmdcompl.AddSuggestionsToFlag(cmd, "project",
cmdcomplutil.NewProjectAutoComplete(f))
cmd.Flags().StringSliceVarP(&rf.TagIDs, "tag", "T", []string{},
"Will filter time entries using these tags")
_ = cmdcompl.AddSuggestionsToFlag(cmd, "tag",
Expand Down Expand Up @@ -102,7 +114,7 @@ func ReportWithRange(

if rf.Project != "" && f.Config().IsAllowNameForID() {
if rf.Project, err = search.GetProjectByName(
c, workspace, rf.Project); err != nil {
c, workspace, rf.Project, rf.Client); err != nil {
return err
}
}
Expand Down
31 changes: 27 additions & 4 deletions pkg/cmd/time-entry/report/util/report_flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import (
"github.com/stretchr/testify/assert"
)

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

err := rf.Check()
assert.Error(t, err)
assert.Regexp(t,
"can't be used together.*billable.*not-billable", err.Error())
if assert.Error(t, err) {
assert.Regexp(t,
"can't be used together.*billable.*not-billable", err.Error())
}

rf.Billable = false
rf.NotBillable = true
Expand All @@ -27,3 +28,25 @@ func TestReportFlagsChecks(t *testing.T) {

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

func TestReportProjectFlagsChecks(t *testing.T) {
rf := util.NewReportFlags()
rf.Client = "me"
rf.Project = ""

err := rf.Check()
if assert.Error(t, err) {
assert.Equal(t,
"flag 'client' can't be used without flag 'project'", err.Error())
}

rf.Client = ""
rf.Project = "mine"

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

rf.Client = "me"
rf.Project = "mine"

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

0 comments on commit de75011

Please sign in to comment.