Skip to content

Commit 41dbd18

Browse files
authored
(feat): "in clone" is now interactive (#23)
* (fix): better name * (feat): newEntry function to be reusable * (feat): in clone is interactive now * (feat): changelog
1 parent b2eeb54 commit 41dbd18

File tree

5 files changed

+309
-306
lines changed

5 files changed

+309
-306
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
- improvements to the code moving interactive logic of the "in" command into `cmd/common.go`
10+
- "in clone" is now interactive and will ask the user to confirm the time entry data before
11+
creating it.
12+
- minor grammar bug fixes
13+
914
## [v0.2.2] - 2020-03-18
1015

1116
## Fixed

cmd/common.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
"time"
10+
11+
"github.com/lucassabreu/clockify-cli/api"
12+
"github.com/lucassabreu/clockify-cli/api/dto"
13+
"github.com/spf13/cobra"
14+
"github.com/spf13/viper"
15+
"gopkg.in/AlecAivazis/survey.v1"
16+
)
17+
18+
var fullTimeFormat = "2006-01-02 15:04:05"
19+
var simplerTimeFormat = "2006-01-02 15:04"
20+
var onlyTimeFormat = "15:04:05"
21+
var simplerOnlyTimeFormat = "15:04"
22+
var nowTimeFormat = "now"
23+
24+
func withClockifyClient(fn func(cmd *cobra.Command, args []string, c *api.Client)) func(*cobra.Command, []string) {
25+
return func(cmd *cobra.Command, args []string) {
26+
c, err := getAPIClient()
27+
if err != nil {
28+
printError(err)
29+
return
30+
}
31+
32+
fn(cmd, args, c)
33+
}
34+
}
35+
36+
func printError(err error) {
37+
fmt.Fprintln(os.Stderr, err.Error())
38+
os.Exit(1)
39+
}
40+
41+
func convertToTime(timeString string) (t time.Time, err error) {
42+
timeString = strings.TrimSpace(timeString)
43+
44+
if nowTimeFormat == strings.ToLower(timeString) {
45+
return time.Now().In(time.Local), nil
46+
}
47+
48+
if len(fullTimeFormat) != len(timeString) && len(simplerTimeFormat) != len(timeString) && len(onlyTimeFormat) != len(timeString) && len(simplerOnlyTimeFormat) != len(timeString) {
49+
return t, fmt.Errorf(
50+
"supported formats are: %s",
51+
strings.Join(
52+
[]string{fullTimeFormat, simplerTimeFormat, onlyTimeFormat, simplerOnlyTimeFormat, nowTimeFormat},
53+
", ",
54+
),
55+
)
56+
}
57+
58+
if len(simplerOnlyTimeFormat) == len(timeString) || len(simplerTimeFormat) == len(timeString) {
59+
timeString = timeString + ":00"
60+
}
61+
62+
if len(onlyTimeFormat) == len(timeString) {
63+
timeString = time.Now().Format("2006-01-02") + " " + timeString
64+
}
65+
66+
return time.ParseInLocation(fullTimeFormat, timeString, time.Local)
67+
}
68+
69+
func getAPIClient() (*api.Client, error) {
70+
c, err := api.NewClient(viper.GetString("token"))
71+
if err != nil {
72+
return c, err
73+
}
74+
75+
if viper.GetBool("debug") {
76+
c.SetDebugLogger(
77+
log.New(os.Stdout, "DEBUG ", log.LstdFlags),
78+
)
79+
}
80+
81+
return c, err
82+
}
83+
84+
func getDateTimeParam(name string, required bool, value string, convert func(string) (time.Time, error)) (*time.Time, error) {
85+
var t time.Time
86+
var err error
87+
88+
message := fmt.Sprintf("%s (leave it blank for empty):", name)
89+
if required {
90+
message = fmt.Sprintf("%s:", name)
91+
}
92+
93+
for {
94+
_ = survey.AskOne(
95+
&survey.Input{
96+
Message: message,
97+
Default: value,
98+
},
99+
&value,
100+
nil,
101+
)
102+
103+
if value == "" && !required {
104+
return nil, nil
105+
}
106+
107+
if t, err = convertToTime(value); err != nil {
108+
fmt.Fprintln(os.Stderr, err.Error())
109+
continue
110+
}
111+
112+
return &t, err
113+
}
114+
}
115+
116+
func newEntry(c *api.Client, te dto.TimeEntryImpl, interactive, autoClose bool) (dto.TimeEntryImpl, error) {
117+
var err error
118+
119+
if interactive {
120+
te.ProjectID, err = getProjectID(te.ProjectID, te.WorkspaceID, c)
121+
if err != nil {
122+
return te, err
123+
}
124+
}
125+
126+
if te.ProjectID == "" {
127+
return te, errors.New("project must be informed")
128+
}
129+
130+
if interactive {
131+
te.Description = getDescription(te.Description)
132+
}
133+
134+
if interactive {
135+
te.TagIDs, err = getTagIDs(te.TagIDs, te.WorkspaceID, c)
136+
if err != nil {
137+
return te, err
138+
}
139+
140+
var date *time.Time
141+
if date, err = getDateTimeParam("Start", true, whenString, convertToTime); err != nil {
142+
return te, err
143+
}
144+
te.TimeInterval.Start = *date
145+
146+
if date, err = getDateTimeParam("End", false, whenToCloseString, convertToTime); err != nil {
147+
return te, err
148+
}
149+
te.TimeInterval.End = date
150+
}
151+
152+
if autoClose {
153+
err = c.Out(api.OutParam{
154+
Workspace: te.WorkspaceID,
155+
End: te.TimeInterval.Start,
156+
})
157+
158+
if err != nil {
159+
return te, err
160+
}
161+
}
162+
163+
return c.CreateTimeEntry(api.CreateTimeEntryParam{
164+
Workspace: te.WorkspaceID,
165+
Billable: !notBillable,
166+
Start: te.TimeInterval.Start,
167+
End: te.TimeInterval.End,
168+
ProjectID: te.ProjectID,
169+
Description: te.Description,
170+
TagIDs: te.TagIDs,
171+
TaskID: te.TaskID,
172+
})
173+
}
174+
175+
func getProjectID(projectID string, workspace string, c *api.Client) (string, error) {
176+
projects, err := c.GetProjects(api.GetProjectsParam{
177+
Workspace: workspace,
178+
})
179+
180+
if err != nil {
181+
return "", err
182+
}
183+
184+
projectsString := make([]string, len(projects))
185+
for i, u := range projects {
186+
projectsString[i] = fmt.Sprintf("%s - %s", u.ID, u.Name)
187+
if u.ID == projectID {
188+
projectID = projectsString[i]
189+
}
190+
}
191+
println(projectID)
192+
err = survey.AskOne(
193+
&survey.Select{
194+
Message: "Choose your project:",
195+
Options: projectsString,
196+
Default: projectID,
197+
},
198+
&projectID,
199+
nil,
200+
)
201+
202+
if err != nil {
203+
return "", nil
204+
}
205+
206+
return strings.TrimSpace(projectID[0:strings.Index(projectID, " - ")]), nil
207+
}
208+
209+
func getDescription(description string) string {
210+
_ = survey.AskOne(
211+
&survey.Input{
212+
Message: "Description:",
213+
Default: description,
214+
},
215+
&description,
216+
nil,
217+
)
218+
219+
return description
220+
}
221+
222+
func getTagIDs(tagIDs []string, workspace string, c *api.Client) ([]string, error) {
223+
if len(tagIDs) > 0 {
224+
return tagIDs, nil
225+
}
226+
227+
if !viper.GetBool("interactive") {
228+
return nil, nil
229+
}
230+
231+
tags, err := c.GetTags(api.GetTagsParam{
232+
Workspace: workspace,
233+
})
234+
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
tagsString := make([]string, len(tags))
240+
for i, u := range tags {
241+
tagsString[i] = fmt.Sprintf("%s - %s", u.ID, u.Name)
242+
}
243+
244+
err = survey.AskOne(
245+
&survey.MultiSelect{
246+
Message: "Choose your tags:",
247+
Options: tagsString,
248+
},
249+
&tagIDs,
250+
nil,
251+
)
252+
253+
if err != nil {
254+
return nil, nil
255+
}
256+
257+
for i, t := range tagIDs {
258+
tagIDs[i] = strings.TrimSpace(t[0:strings.Index(t, " - ")])
259+
}
260+
261+
return tagIDs, nil
262+
}

0 commit comments

Comments
 (0)