Skip to content

Commit

Permalink
(release): v0.18.0
Browse files Browse the repository at this point in the history
* (fix): none project option and incomplete time entry

* (fix): remove no-closing

* (feat): validating time entries before creation

* (ref): deduplicating edit and newEntry

* (release): v0.18.0
  • Loading branch information
lucassabreu authored Jul 9, 2021
1 parent 0f4dac4 commit 00aadb9
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 85 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [v0.18.0] - 2021-07-08

### Added

- commands `in`, `clone` and `manual` will show a new "None" option on the projects list on the
interactive mode if the workspace allows time entries without projects.
- config `allow-incomplete` allows the user to set if they want to create "incomplete time entries"
or to validated then before creation. Flag `--allow-incomplete` and environment variable
`CLOCKIFY_ALLOW_INCOMPLETE` can be used for the same purpose. by default time entries will be
validated.

### Changed

- commands `in` and `clone` when creating an "open" time entry will not validate if the workspace
requires a project or not, allowing the creation of open incomplete/invalid time entries, similar
to the browser application.
- `newEntry` function changed to `manageEntry` and will allow a callback to deal with the filled and
validated time entry instead of always creating a new one, that way same code that were duplicated
between it and the `edit` command can be united.

### Fixed

- `no-closing` configuration was removed, because was not used anymore.

## [v0.17.2] - 2021-06-17

### Fixed
Expand Down
19 changes: 19 additions & 0 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ func (c *Client) GetWorkspaces(f GetWorkspaces) ([]dto.Workspace, error) {
return ws, nil
}

type GetWorkspace struct {
ID string
}

func (c *Client) GetWorkspace(p GetWorkspace) (dto.Workspace, error) {
ws, err := c.GetWorkspaces(GetWorkspaces{})
if err != nil {
return dto.Workspace{}, err
}

for _, w := range ws {
if w.ID == p.ID {
return w, nil
}
}

return dto.Workspace{}, dto.Error{Message: "not found", Code: 404}
}

// WorkspaceUsersParam params to query workspace users
type WorkspaceUsersParam struct {
Workspace string
Expand Down
11 changes: 10 additions & 1 deletion cmd/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,16 @@ var cloneCmd = &cobra.Command{
format, _ := cmd.Flags().GetString("format")
noClosing, _ := cmd.Flags().GetBool("no-closing")
asJSON, _ := cmd.Flags().GetBool("json")
return newEntry(c, tec, viper.GetBool(INTERACTIVE), viper.GetBool(ALLOW_PROJECT_NAME), !noClosing, format, asJSON)
return manageEntry(
c,
tec,
createTimeEntry(c),
viper.GetBool(INTERACTIVE),
viper.GetBool(ALLOW_PROJECT_NAME),
!noClosing,
format, asJSON,
!viper.GetBool(ALLOW_INCOMPLETE),
)
}),
}

Expand Down
104 changes: 73 additions & 31 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,12 @@ func getProjectByNameOrId(c *api.Client, workspace, project string) (string, err
return "", stackedErrors.Errorf("No project with id or name containing: %s", project)
}

func confirmEntryInteractively(c *api.Client, te dto.TimeEntryImpl) (dto.TimeEntryImpl, error) {
func confirmEntryInteractively(c *api.Client, te dto.TimeEntryImpl, w dto.Workspace) (dto.TimeEntryImpl, error) {
var err error
te.ProjectID, err = getProjectID(te.ProjectID, te.WorkspaceID, c)
te.ProjectID, err = getProjectID(te.ProjectID, w, c)
if err != nil {
return te, err
}
if te.ProjectID == "" {
return te, errors.New("project must be informed")
}

te.Description = getDescription(te.Description)

Expand Down Expand Up @@ -163,6 +160,23 @@ func confirmEntryInteractively(c *api.Client, te dto.TimeEntryImpl) (dto.TimeEnt
return te, nil
}

func validateTimeEntry(te dto.TimeEntryImpl, w dto.Workspace) error {

if w.Settings.ForceProjects && te.ProjectID == "" {
return errors.New("workspace requires project")
}

if w.Settings.ForceDescription && strings.TrimSpace(te.Description) == "" {
return errors.New("workspace requires description")
}

if w.Settings.ForceTags && len(te.TagIDs) == 0 {
return errors.New("workspace requires at least one tag")
}

return nil
}

func printTimeEntryImpl(c *api.Client, tei dto.TimeEntryImpl, asJSON bool, format string) error {
fte, err := c.ConvertIntoFullTimeEntry(tei)
if err != nil {
Expand All @@ -184,7 +198,17 @@ func printTimeEntryImpl(c *api.Client, tei dto.TimeEntryImpl, asJSON bool, forma
return reportFn(&fte, os.Stdout)
}

func newEntry(c *api.Client, te dto.TimeEntryImpl, interactive, allowProjectByName, autoClose bool, format string, asJSON bool) error {
func manageEntry(
c *api.Client,
te dto.TimeEntryImpl,
callback func(dto.TimeEntryImpl) (dto.TimeEntryImpl, error),
interactive,
allowProjectByName,
autoClose bool,
format string,
asJSON bool,
validate bool,
) error {
var err error

if allowProjectByName && te.ProjectID != "" {
Expand All @@ -194,47 +218,60 @@ func newEntry(c *api.Client, te dto.TimeEntryImpl, interactive, allowProjectByNa
}
}

if interactive {
te, err = confirmEntryInteractively(c, te)
if interactive || validate {
w, err := c.GetWorkspace(api.GetWorkspace{ID: te.WorkspaceID})
if err != nil {
return err
}
} else if te.ProjectID == "" {
return errors.New("project must be informed")

if interactive {
te, err = confirmEntryInteractively(c, te, w)
if err != nil {
return err
}
}

if validate {
if err = validateTimeEntry(te, w); err != nil {
return err
}
}
}

if autoClose {
err = c.Out(api.OutParam{
Workspace: te.WorkspaceID,
End: te.TimeInterval.Start,
})

if err != nil {
if err = c.Out(api.OutParam{Workspace: te.WorkspaceID, End: te.TimeInterval.Start}); err != nil {
return err
}
}

tei, err := c.CreateTimeEntry(api.CreateTimeEntryParam{
Workspace: te.WorkspaceID,
Billable: te.Billable,
Start: te.TimeInterval.Start,
End: te.TimeInterval.End,
ProjectID: te.ProjectID,
Description: te.Description,
TagIDs: te.TagIDs,
TaskID: te.TaskID,
})

tei, err := callback(te)
if err != nil {
return err
}

return printTimeEntryImpl(c, tei, asJSON, format)
}

func getProjectID(projectID string, workspace string, c *api.Client) (string, error) {
func createTimeEntry(c *api.Client) func(dto.TimeEntryImpl) (dto.TimeEntryImpl, error) {
return func(te dto.TimeEntryImpl) (dto.TimeEntryImpl, error) {
return c.CreateTimeEntry(api.CreateTimeEntryParam{
Workspace: te.WorkspaceID,
Billable: te.Billable,
Start: te.TimeInterval.Start,
End: te.TimeInterval.End,
ProjectID: te.ProjectID,
Description: te.Description,
TagIDs: te.TagIDs,
TaskID: te.TaskID,
})
}
}

const noProject = "No Project"

func getProjectID(projectID string, w dto.Workspace, c *api.Client) (string, error) {
projects, err := c.GetProjects(api.GetProjectsParam{
Workspace: workspace,
Workspace: w.ID,
PaginationParam: api.PaginationParam{AllPages: true},
})

Expand Down Expand Up @@ -282,8 +319,13 @@ func getProjectID(projectID string, workspace string, c *api.Client) (string, er
projectID = projectsString[found]
}

if projectID, err = ui.AskFromOptions("Choose your project:", projectsString, projectID); err != nil || projectID == "" {
return "", nil
if !w.Settings.ForceProjects {
projectsString = append([]string{noProject}, projectsString...)
}

projectID, err = ui.AskFromOptions("Choose your project:", projectsString, projectID)
if err != nil || projectID == noProject || projectID == "" {
return "", err
}

return strings.TrimSpace(projectID[0:strings.Index(projectID, " - ")]), nil
Expand Down
22 changes: 11 additions & 11 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,21 @@ import (
const (
WORKWEEK_DAYS = "workweek-days"
INTERACTIVE = "interactive"
NO_CLOSING = "no-closing"
ALLOW_PROJECT_NAME = "allow-project-name"
USER_ID = "user.id"
WORKSPACE = "workspace"
TOKEN = "token"
ALLOW_INCOMPLETE = "allow-incomplete"
)

var configValidArgs = completion.ValigsArgsMap{
TOKEN: `clockify's token`,
WORKSPACE: "workspace to be used",
USER_ID: "user id from the token",
ALLOW_PROJECT_NAME: "should allow use of project when id is asked",
NO_CLOSING: "should not close any active time entry",
INTERACTIVE: "show interactive mode",
WORKWEEK_DAYS: "days of the week were your expected to work (use comma to set multiple)",
ALLOW_INCOMPLETE: "should allow starting time entries with missing required values",
}

var weekdays []string
Expand Down Expand Up @@ -205,15 +205,6 @@ func configInit(cmd *cobra.Command, args []string) error {
}
viper.Set(ALLOW_PROJECT_NAME, allowProjectName)

autoClose := !viper.GetBool(NO_CLOSING)
if autoClose, err = ui.Confirm(
`Should auto-close previous/current time entry before opening a new one?`,
autoClose,
); err != nil {
return err
}
viper.Set(NO_CLOSING, !autoClose)

interactive := viper.GetBool(INTERACTIVE)
if interactive, err = ui.Confirm(
`Should use "Interactive Mode" by default?`,
Expand All @@ -233,6 +224,15 @@ func configInit(cmd *cobra.Command, args []string) error {
}
viper.Set(WORKWEEK_DAYS, workweekDays)

allowIncomplete := viper.GetBool(ALLOW_INCOMPLETE)
if allowIncomplete, err = ui.Confirm(
`Should allow starting time entries with incomplete data?`,
allowIncomplete,
); err != nil {
return err
}
viper.Set(ALLOW_INCOMPLETE, allowIncomplete)

return configSaveFile()
}

Expand Down
55 changes: 24 additions & 31 deletions cmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

"github.com/lucassabreu/clockify-cli/api"
"github.com/lucassabreu/clockify-cli/api/dto"
"github.com/lucassabreu/clockify-cli/cmd/completion"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand All @@ -32,7 +33,6 @@ var editCmd = &cobra.Command{
Short: `Edit a time entry, use id "current" to apply to time entry in progress`,
RunE: withClockifyClient(func(cmd *cobra.Command, args []string, c *api.Client) error {
var err error
interactive := viper.GetBool(INTERACTIVE)

userID, err := getUserId(c)
if err != nil {
Expand All @@ -52,12 +52,6 @@ var editCmd = &cobra.Command{

if cmd.Flags().Changed("project") {
tei.ProjectID, _ = cmd.Flags().GetString("project")
if viper.GetBool(ALLOW_PROJECT_NAME) && tei.ProjectID != "" {
tei.ProjectID, err = getProjectByNameOrId(c, tei.WorkspaceID, tei.ProjectID)
if err != nil && !interactive {
return err
}
}
}

if cmd.Flags().Changed("description") {
Expand Down Expand Up @@ -95,32 +89,31 @@ var editCmd = &cobra.Command{
tei.TimeInterval.End = &v
}

if interactive {
tei, err = confirmEntryInteractively(c, tei)
if err != nil {
return err
}
}

tei, err = c.UpdateTimeEntry(api.UpdateTimeEntryParam{
Workspace: tei.WorkspaceID,
TimeEntryID: tei.ID,
Description: tei.Description,
Start: tei.TimeInterval.Start,
End: tei.TimeInterval.End,
Billable: tei.Billable,
ProjectID: tei.ProjectID,
TaskID: tei.TaskID,
TagIDs: tei.TagIDs,
})

if err != nil {
return err
}

format, _ := cmd.Flags().GetString("format")
asJSON, _ := cmd.Flags().GetBool("json")
return printTimeEntryImpl(c, tei, asJSON, format)
return manageEntry(
c,
tei,
func(tei dto.TimeEntryImpl) (dto.TimeEntryImpl, error) {
return c.UpdateTimeEntry(api.UpdateTimeEntryParam{
Workspace: tei.WorkspaceID,
TimeEntryID: tei.ID,
Description: tei.Description,
Start: tei.TimeInterval.Start,
End: tei.TimeInterval.End,
Billable: tei.Billable,
ProjectID: tei.ProjectID,
TaskID: tei.TaskID,
TagIDs: tei.TagIDs,
})
},
viper.GetBool(INTERACTIVE),
viper.GetBool(ALLOW_PROJECT_NAME),
false,
format,
asJSON,
!viper.GetBool(ALLOW_INCOMPLETE),
)
}),
}

Expand Down
Loading

0 comments on commit 00aadb9

Please sign in to comment.