diff --git a/clickup/client.go b/clickup/client.go index 82c05b2..6e3dc0c 100644 --- a/clickup/client.go +++ b/clickup/client.go @@ -55,6 +55,7 @@ type Client struct { Tasks *TasksService TaskTemplates *TaskTemplatesService Teams *TeamsService + TimeTrackings *TimeTrackingsService SharedHierarchy *SharedHierarchyService Spaces *SpacesService Folders *FoldersService @@ -138,6 +139,7 @@ func NewClient(httpClient *http.Client, APIKey string) *Client { c.Tasks = (*TasksService)(&c.common) c.TaskTemplates = (*TaskTemplatesService)(&c.common) c.Teams = (*TeamsService)(&c.common) + c.TimeTrackings = (*TimeTrackingsService)(&c.common) c.SharedHierarchy = (*SharedHierarchyService)(&c.common) c.Spaces = (*SpacesService)(&c.common) c.Folders = (*FoldersService)(&c.common) diff --git a/clickup/comments.go b/clickup/comments.go index e5ad643..260656b 100644 --- a/clickup/comments.go +++ b/clickup/comments.go @@ -20,7 +20,7 @@ type UpdateCommentRequest struct { } type CreateCommentResponse struct { - ID string `json:"id"` + ID int `json:"id"` HistId string `json:"hist_id"` Date *Date `json:"date"` } @@ -35,7 +35,7 @@ type TaskCommentOptions struct { } type Comment struct { - ID string `json:"id"` + ID int `json:"id"` Comment []CommentInComment `json:"comment"` CommentText string `json:"comment_text"` User User `json:"user"` diff --git a/clickup/comments_test.go b/clickup/comments_test.go index a136861..e6f22a7 100644 --- a/clickup/comments_test.go +++ b/clickup/comments_test.go @@ -31,7 +31,7 @@ func TestCommentsService_CreateTaskComment(t *testing.T) { fmt.Fprint(w, `{ - "id": "458", + "id": 458, "hist_id": "26508", "date": 1568036964079 }`, @@ -44,7 +44,7 @@ func TestCommentsService_CreateTaskComment(t *testing.T) { t.Errorf("Actions.ListArtifacts returned error: %v", err) } - want := &CreateCommentResponse{ID: "458", HistId: "26508", Date: NewDateWithUnixTime(1568036964079)} + want := &CreateCommentResponse{ID: 458, HistId: "26508", Date: NewDateWithUnixTime(1568036964079)} if !cmp.Equal(artifacts, want) { t.Errorf("Actions.ListArtifacts returned %+v, want %+v", artifacts, want) } @@ -60,7 +60,7 @@ func TestCommentsService_GetTaskComments(t *testing.T) { `{ "comments": [ { - "id": "458", + "id": 458, "comment": [ { "text": "Task comment content" @@ -115,7 +115,7 @@ func TestCommentsService_GetTaskComments(t *testing.T) { ProfilePicture: "https://attachments-public.clickup.com/profilePictures/183_abc.jpg", } comment := Comment{ - ID: "458", + ID: 458, Comment: []CommentInComment{{Text: "Task comment content"}}, CommentText: "Task comment content", User: user, diff --git a/clickup/tasks.go b/clickup/tasks.go index 91b7960..3440336 100644 --- a/clickup/tasks.go +++ b/clickup/tasks.go @@ -37,6 +37,31 @@ type TaskRequest struct { CustomItemId int `json:"custom_item_id,omitempty"` // To create a task that doesn't use a custom task type, either don't include this field in the request body, or send 'null'. To create this task as a Milestone, send a value of 1. To use a custom task type, send the custom task type ID as defined in your Workspace, such as 2. } +type TaskUpdateRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Assignees TaskAssigneeUpdateRequest `json:"assignees,omitempty"` + Tags []string `json:"tags,omitempty"` + Status string `json:"status,omitempty"` + Priority int `json:"priority,omitempty"` + DueDate *Date `json:"due_date,omitempty"` + DueDateTime bool `json:"due_date_time,omitempty"` + TimeEstimate int `json:"time_estimate,omitempty"` + StartDate *Date `json:"start_date,omitempty"` + StartDateTime bool `json:"start_date_time,omitempty"` + NotifyAll bool `json:"notify_all,omitempty"` + Parent string `json:"parent,omitempty"` + LinksTo string `json:"links_to,omitempty"` + CheckRequiredCustomFields bool `json:"check_required_custom_fields,omitempty"` + CustomFields []CustomFieldInTaskRequest `json:"custom_fields,omitempty"` + CustomItemId int `json:"custom_item_id,omitempty"` // To create a task that doesn't use a custom task type, either don't include this field in the request body, or send 'null'. To create this task as a Milestone, send a value of 1. To use a custom task type, send the custom task type ID as defined in your Workspace, such as 2. +} + +type TaskAssigneeUpdateRequest struct { + Add []int `json:"add,omitempty"` + Rem []int `json:"rem,omitempty"` +} + type CustomFieldInTaskRequest struct { ID string `json:"id"` Value interface{} `json:"value"` @@ -129,6 +154,7 @@ type TasksInStatus struct { } type TaskStatus struct { + ID string `json:"id"` Status string `json:"status"` Color string `json:"color"` Type string `json:"type"` @@ -282,7 +308,7 @@ func (s *TasksService) CreateTask(ctx context.Context, listID string, tr *TaskRe } // FIXME: assignees add/rem -func (s *TasksService) UpdateTask(ctx context.Context, taskID string, opts *GetTaskOptions, tr *TaskRequest) (*Task, *Response, error) { +func (s *TasksService) UpdateTask(ctx context.Context, taskID string, opts *GetTaskOptions, tr *TaskUpdateRequest) (*Task, *Response, error) { u := fmt.Sprintf("task/%v/", taskID) u, err := addOptions(u, opts) if err != nil { diff --git a/clickup/time_tracking.go b/clickup/time_tracking.go new file mode 100644 index 0000000..bdf243a --- /dev/null +++ b/clickup/time_tracking.go @@ -0,0 +1,110 @@ +package clickup + +import ( + "context" + "fmt" +) + +type TimeTrackingsService service + +type GetTimeTrackingResponse struct { + Data TimeTrackingData `json:"data"` +} + +type CreateTimeTrackingResponse struct { + Data TimeTrackingData `json:"data"` +} + +// See https://clickup.com/api/clickupreference/operation/Createatimeentry/ +type TimeTrackingRequest struct { + Description string `json:"description,omitempty"` + Tags []TimeTrackingTag `json:"tags,omitempty"` + Start int64 `json:"start"` + End int64 `json:"end,omitempty"` + Stop int64 `json:"stop,omitempty"` + Billable bool `json:"billable,omitempty"` + Duration int32 `json:"duration"` + Assignee int `json:"assignee,omitempty"` + Tid string `json:"tid,omitempty"` +} + +type TimeTrackingTag struct { + Name string `json:"name"` + TagBg string `json:"tag_bg"` + TagFg string `json:"tag_fg"` + Creator int `json:"creator"` +} + +type TimeTrackingData struct { + ID string `json:"id"` + Wid string `json:"wid"` + User User `json:"user"` + Billable bool `json:"billable"` + Start int `json:"start"` + End string `json:"end"` + Duration int `json:"duration"` + Description string `json:"description"` + Source string `json:"source"` + At int `json:"at"` + IsLocked bool `json:"is_locked"` + TaskLocation TaskLocation `json:"task_location"` + Task Task `json:"task"` + Tags []TimeTrackingTag `json:"tags"` + TaskURL string `json:"task_url"` +} + +type TaskLocation struct { + ListID int `json:"list_id"` + FolderID int `json:"folder_id"` + SpaceID int `json:"space_id"` + ListName string `json:"list_name"` + FolderName string `json:"folder_name"` + SpaceName string `json:"space_name"` +} + +type CreateTimeTrackingOptions struct { + CustomTaskIDs bool `url:"custom_task_ids,omitempty"` + TeamID int `url:"team_id,omitempty"` +} + +func (s *TimeTrackingsService) CreateTimeTracking(ctx context.Context, teamID string, opts *CreateTimeTrackingOptions, ttr *TimeTrackingRequest) (*CreateTimeTrackingResponse, *Response, error) { + u := fmt.Sprintf("team/%s/time_entries", teamID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("POST", u, ttr) + if err != nil { + return nil, nil, err + } + + timeTracking := new(CreateTimeTrackingResponse) + resp, err := s.client.Do(ctx, req, timeTracking) + if err != nil { + return nil, resp, err + } + + return timeTracking, resp, nil +} + +func (s *TimeTrackingsService) GetTimeTracking(ctx context.Context, teamID string, timerID string, opts *CreateTimeTrackingOptions, ttr *TimeTrackingRequest) (*GetTimeTrackingResponse, *Response, error) { + u := fmt.Sprintf("team/%s/time_entries/%s", teamID, timerID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, ttr) + if err != nil { + return nil, nil, err + } + + getTimeTrackingResponse := new(GetTimeTrackingResponse) + resp, err := s.client.Do(ctx, req, getTimeTrackingResponse) + if err != nil { + return nil, resp, err + } + + return getTimeTrackingResponse, resp, nil +} diff --git a/clickup/time_tracking_test.go b/clickup/time_tracking_test.go new file mode 100644 index 0000000..75179d1 --- /dev/null +++ b/clickup/time_tracking_test.go @@ -0,0 +1,98 @@ +package clickup + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestTimeTrackingService_CreateTimeTracking(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + input := &TimeTrackingRequest{ + Description: "description", + Start: 1719595398, + Duration: 120000, + Assignee: 99999999, + Tid: "9hz", + Billable: true, + } + + mux.HandleFunc("/team/123/time_entries", func(w http.ResponseWriter, r *http.Request) { + v := new(TimeTrackingRequest) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "POST") + if !cmp.Equal(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + fmt.Fprint(w, + `{ + "data": { + "id": "4090130922962924695", + "task": { + "id": "9hz", + "name": "Test", + "status": { + "status": "in progress", + "id": "p99999999999_99asdhAS", + "color": "#5f55ee", + "type": "custom", + "orderindex": 1 + } + }, + "wid": "9999999999", + "user": { + "id": 99999999, + "username": "John", + "email": "john@mail.com", + "color": "#afb42b", + "initials": "J", + "profilePicture": "https://attachments.clickup.com/profilePictures/99999999_tX9.jpg" + }, + "billable": true, + "start": 1719595398, + "end": "1719715398", + "duration": 120000, + "description": "description", + "tags": [], + "at": 1719586940375, + "is_locked": false, + "task_location": {} + } + }`, + ) + }) + + ctx := context.Background() + artifacts, _, err := client.TimeTrackings.CreateTimeTracking(ctx, "123", nil, input) + if err != nil { + t.Errorf("Actions.ListArtifacts returned error: %v", err) + } + + want := &CreateTimeTrackingResponse{Data: TimeTrackingData{ + ID: "4090130922962924695", + Wid: "9999999999", + User: User{ID: 99999999, Username: "John", Email: "john@mail.com", Color: "#afb42b", Initials: "J", ProfilePicture: "https://attachments.clickup.com/profilePictures/99999999_tX9.jpg"}, + Billable: true, + Start: 1719595398, + End: "1719715398", + Duration: 120000, + Description: "description", + Tags: []TimeTrackingTag{}, + At: 1719586940375, + IsLocked: false, + TaskLocation: TaskLocation{}, + Task: Task{ID: "9hz", Name: "Test", Status: TaskStatus{ID: "p99999999999_99asdhAS", Status: "in progress", Color: "#5f55ee", Type: "custom", Orderindex: json.Number("1")}}, + }} + if !cmp.Equal(artifacts, want) { + t.Errorf("Actions.ListArtifacts returned %+v, want %+v", artifacts, want) + } + +} diff --git a/example/update-duedate/main.go b/example/update-duedate/main.go index 0a71f28..c854cbc 100644 --- a/example/update-duedate/main.go +++ b/example/update-duedate/main.go @@ -27,17 +27,17 @@ func main() { getTask(ctx, client, taskId) fmt.Println("\nUpdate due date of the task to 2122/01/02 03:04:05:06") - updateTask(ctx, client, taskId, &clickup.TaskRequest{ + updateTask(ctx, client, taskId, &clickup.TaskUpdateRequest{ DueDate: clickup.NewDate( time.Date(2122, 1, 2, 3, 4, 5, 6, time.Now().Location()), ), }) fmt.Println("\nUpdate the task with empty TaskRequest") - updateTask(ctx, client, taskId, &clickup.TaskRequest{}) + updateTask(ctx, client, taskId, &clickup.TaskUpdateRequest{}) fmt.Println("\nRemove task due date with NullDate()") - updateTask(ctx, client, taskId, &clickup.TaskRequest{ + updateTask(ctx, client, taskId, &clickup.TaskUpdateRequest{ DueDate: clickup.NullDate(), }) } @@ -50,7 +50,7 @@ func getTask(ctx context.Context, client *clickup.Client, taskID string) { fmt.Println(task.Name, task.DueDate) } -func updateTask(ctx context.Context, client *clickup.Client, taskID string, tr *clickup.TaskRequest) { +func updateTask(ctx context.Context, client *clickup.Client, taskID string, tr *clickup.TaskUpdateRequest) { task, _, err := client.Tasks.UpdateTask(ctx, taskID, &clickup.GetTaskOptions{}, tr) if err != nil { log.Fatalln(err) diff --git a/go.mod b/go.mod index 96772d5..da3b72f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/raksul/go-clickup -go 1.21.1 +go 1.22.3 require ( github.com/google/go-cmp v0.5.8