Skip to content

Commit 2bb8b78

Browse files
committed
feat: time entry commands allow client filter for project
1 parent 737626c commit 2bb8b78

18 files changed

+400
-33
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
## [Unreleased]
1313

14+
### Added
15+
16+
- new flag `--client` to filter projects by client when managing time entries
17+
1418
## [v0.46.0] - 2023-12-06
1519

1620
### Added

pkg/cmd/project/edit/edit.go

-5
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,6 @@ func NewCmdEdit(
126126
return err
127127
}
128128

129-
if ids, err = search.GetProjectsByName(
130-
c, w, ids); err != nil {
131-
return err
132-
}
133-
134129
if client != nil && *client != "" {
135130
if *client, err = search.GetClientByName(
136131
c, w, *client); err != nil {

pkg/cmd/project/get/get.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func NewCmdGet(
6363

6464
if f.Config().IsAllowNameForID() {
6565
if p.ProjectID, err = search.GetProjectByName(
66-
c, p.Workspace, p.ProjectID); err != nil {
66+
c, p.Workspace, p.ProjectID, ""); err != nil {
6767
return err
6868
}
6969
}

pkg/cmd/task/delete/delete.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func NewCmdDelete(
7070

7171
if f.Config().IsAllowNameForID() {
7272
if project, err = search.GetProjectByName(
73-
c, w, project); err != nil {
73+
c, w, project, ""); err != nil {
7474
return err
7575
}
7676

pkg/cmd/task/done/done.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func NewCmdDone(
8888

8989
if f.Config().IsAllowNameForID() {
9090
if project, err = search.GetProjectByName(
91-
c, workspace, project); err != nil {
91+
c, workspace, project, ""); err != nil {
9292
return err
9393
}
9494

pkg/cmd/task/list/list.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func NewCmdList(
6363
if f.Config().IsAllowNameForID() &&
6464
p.ProjectID != "" {
6565
if p.ProjectID, err = search.GetProjectByName(
66-
c, workspace, p.ProjectID); err != nil {
66+
c, workspace, p.ProjectID, ""); err != nil {
6767
return err
6868
}
6969
}

pkg/cmd/task/quick-add/quick-add.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func NewCmdQuickAdd(
6161

6262
p, _ := cmd.Flags().GetString("project")
6363
if f.Config().IsAllowNameForID() {
64-
if p, err = search.GetProjectByName(c, w, p); err != nil {
64+
if p, err = search.GetProjectByName(c, w, p, ""); err != nil {
6565
return err
6666
}
6767
}

pkg/cmd/task/util/read-flags.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func TaskReadFlags(cmd *cobra.Command, f cmdutil.Factory) (p FlagsDTO, err error
7979
}
8080

8181
if p.ProjectID, err = search.GetProjectByName(
82-
c, p.Workspace, p.ProjectID); err != nil {
82+
c, p.Workspace, p.ProjectID, ""); err != nil {
8383
return p, err
8484
}
8585

pkg/cmd/time-entry/in/in_test.go

+152
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package in_test
22

33
import (
44
"bytes"
5+
"errors"
56
"io"
67
"testing"
78
"time"
@@ -150,6 +151,157 @@ func TestNewCmdIn_ShouldNotSetBillable_WhenNotAsked(t *testing.T) {
150151
return
151152
}
152153

154+
t.Fatalf("err: %s", err)
155+
})
156+
}
157+
}
158+
159+
func TestNewCmdIn_ShouldLookupProject_WithAndWithoutClient(t *testing.T) {
160+
defaultStart := timehlp.Today().Add(8 * time.Hour)
161+
162+
projects := []dto.Project{
163+
{ID: "p1", Name: "first", ClientID: "c1", ClientName: "other"},
164+
{ID: "p2", Name: "second", ClientID: "c2", ClientName: "me"},
165+
{ID: "p3", Name: "second", ClientID: "c3", ClientName: "clockify"},
166+
{ID: "p4", Name: "third"},
167+
{ID: "p5", Name: "notonclient", ClientID: "c3", ClientName: "clockify"},
168+
}
169+
170+
tts := []struct {
171+
name string
172+
args []string
173+
param api.CreateTimeEntryParam
174+
err error
175+
}{
176+
{
177+
name: "only project",
178+
args: []string{"-s=08:00", "-p=first"},
179+
param: api.CreateTimeEntryParam{
180+
Workspace: w.ID,
181+
Start: defaultStart,
182+
ProjectID: projects[0].ID,
183+
},
184+
},
185+
{
186+
name: "project and client",
187+
args: []string{"-s=08:00", "-p=second", "-c=me"},
188+
param: api.CreateTimeEntryParam{
189+
Workspace: w.ID,
190+
Start: defaultStart,
191+
ProjectID: projects[1].ID,
192+
},
193+
},
194+
{
195+
name: "project and other client",
196+
args: []string{"-s=08:00", "-p=second", "-c=clockify"},
197+
param: api.CreateTimeEntryParam{
198+
Workspace: w.ID,
199+
Start: defaultStart,
200+
ProjectID: projects[2].ID,
201+
},
202+
},
203+
{
204+
name: "project without client",
205+
args: []string{"-s=08:00", "-p=third"},
206+
param: api.CreateTimeEntryParam{
207+
Workspace: w.ID,
208+
Start: defaultStart,
209+
ProjectID: projects[3].ID,
210+
},
211+
},
212+
{
213+
name: "project does not exist",
214+
args: []string{"-s=08:00", "-p=notfound"},
215+
err: errors.New(
216+
"No project with id or name containing 'notfound' " +
217+
"was found"),
218+
},
219+
{
220+
name: "project does not exist in this client",
221+
args: []string{"-s=08:00", "-p=notonclient", "-c=me"},
222+
err: errors.New(
223+
"No project with id or name containing 'notonclient' " +
224+
"was found for client 'me'"),
225+
},
226+
}
227+
228+
for i := range tts {
229+
tt := &tts[i]
230+
231+
t.Run(tt.name, func(t *testing.T) {
232+
f := mocks.NewMockFactory(t)
233+
234+
f.EXPECT().GetUserID().Return("u", nil)
235+
f.EXPECT().GetWorkspaceID().Return(w.ID, nil)
236+
237+
f.EXPECT().Config().Return(&mocks.SimpleConfig{
238+
AllowNameForID: true,
239+
})
240+
241+
c := mocks.NewMockClient(t)
242+
f.EXPECT().Client().Return(c, nil)
243+
244+
c.EXPECT().GetProjects(api.GetProjectsParam{
245+
Workspace: w.ID,
246+
PaginationParam: api.AllPages(),
247+
}).
248+
Return(projects, nil)
249+
250+
c.EXPECT().GetTimeEntryInProgress(api.GetTimeEntryInProgressParam{
251+
Workspace: w.ID,
252+
UserID: "u",
253+
}).
254+
Return(nil, nil)
255+
256+
if tt.err == nil {
257+
c.EXPECT().GetProject(api.GetProjectParam{
258+
Workspace: w.ID,
259+
ProjectID: tt.param.ProjectID,
260+
}).
261+
Return(&dto.Project{ID: tt.param.ProjectID}, nil)
262+
263+
f.EXPECT().GetWorkspace().Return(w, nil)
264+
265+
c.EXPECT().Out(api.OutParam{
266+
Workspace: w.ID,
267+
UserID: "u",
268+
End: tt.param.Start,
269+
}).Return(api.ErrorNotFound)
270+
271+
c.EXPECT().CreateTimeEntry(tt.param).
272+
Return(dto.TimeEntryImpl{ID: "te"}, nil)
273+
}
274+
275+
called := false
276+
cmd := in.NewCmdIn(f, func(
277+
_ dto.TimeEntryImpl, _ io.Writer, _ util.OutputFlags) error {
278+
called = true
279+
return nil
280+
})
281+
282+
cmd.SilenceUsage = true
283+
cmd.SilenceErrors = true
284+
285+
out := bytes.NewBufferString("")
286+
cmd.SetOut(out)
287+
cmd.SetErr(out)
288+
289+
cmd.SetArgs(append(tt.args, "-q"))
290+
_, err := cmd.ExecuteC()
291+
292+
if tt.err != nil {
293+
assert.EqualError(t, err, tt.err.Error())
294+
return
295+
}
296+
297+
t.Cleanup(func() {
298+
assert.True(t, called)
299+
})
300+
301+
if assert.NoError(t, err) {
302+
return
303+
}
304+
153305
t.Fatalf("err: %s", err)
154306
})
155307
}

pkg/cmd/time-entry/report/util/report.go

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package util
22

33
import (
4+
"errors"
45
"io"
56
"sort"
67
"time"
@@ -31,6 +32,7 @@ type ReportFlags struct {
3132
NotBillable bool
3233

3334
Description string
35+
Client string
3436
Project string
3537
TagIDs []string
3638
}
@@ -41,6 +43,12 @@ func (rf ReportFlags) Check() error {
4143
return err
4244
}
4345

46+
if rf.Client != "" && rf.Project == "" {
47+
return cmdutil.FlagErrorWrap(errors.New(
48+
"flag 'client' can't be used without flag 'project'",
49+
))
50+
}
51+
4452
return cmdutil.XorFlag(map[string]bool{
4553
"billable": rf.Billable,
4654
"not-billable": rf.NotBillable,
@@ -69,6 +77,10 @@ func AddReportFlags(
6977
"Will filter time entries using this project")
7078
_ = cmdcompl.AddSuggestionsToFlag(cmd, "project",
7179
cmdcomplutil.NewProjectAutoComplete(f))
80+
cmd.Flags().StringVarP(&rf.Client, "client", "c", "",
81+
"Will filter projects from this client")
82+
_ = cmdcompl.AddSuggestionsToFlag(cmd, "project",
83+
cmdcomplutil.NewProjectAutoComplete(f))
7284
cmd.Flags().StringSliceVarP(&rf.TagIDs, "tag", "T", []string{},
7385
"Will filter time entries using these tags")
7486
_ = cmdcompl.AddSuggestionsToFlag(cmd, "tag",
@@ -102,7 +114,7 @@ func ReportWithRange(
102114

103115
if rf.Project != "" && f.Config().IsAllowNameForID() {
104116
if rf.Project, err = search.GetProjectByName(
105-
c, workspace, rf.Project); err != nil {
117+
c, workspace, rf.Project, rf.Client); err != nil {
106118
return err
107119
}
108120
}

pkg/cmd/time-entry/report/util/report_flag_test.go

+27-4
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ import (
77
"github.com/stretchr/testify/assert"
88
)
99

10-
func TestReportFlagsChecks(t *testing.T) {
10+
func TestReportBillableFlagsChecks(t *testing.T) {
1111
rf := util.NewReportFlags()
1212
rf.Billable = true
1313
rf.NotBillable = true
1414

1515
err := rf.Check()
16-
assert.Error(t, err)
17-
assert.Regexp(t,
18-
"can't be used together.*billable.*not-billable", err.Error())
16+
if assert.Error(t, err) {
17+
assert.Regexp(t,
18+
"can't be used together.*billable.*not-billable", err.Error())
19+
}
1920

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

2829
assert.NoError(t, rf.Check())
2930
}
31+
32+
func TestReportProjectFlagsChecks(t *testing.T) {
33+
rf := util.NewReportFlags()
34+
rf.Client = "me"
35+
rf.Project = ""
36+
37+
err := rf.Check()
38+
if assert.Error(t, err) {
39+
assert.Equal(t,
40+
"flag 'client' can't be used without flag 'project'", err.Error())
41+
}
42+
43+
rf.Client = ""
44+
rf.Project = "mine"
45+
46+
assert.NoError(t, rf.Check())
47+
48+
rf.Client = "me"
49+
rf.Project = "mine"
50+
51+
assert.NoError(t, rf.Check())
52+
}

0 commit comments

Comments
 (0)