diff --git a/client_test.go b/client_test.go index d799527..a3a3d5f 100644 --- a/client_test.go +++ b/client_test.go @@ -139,6 +139,23 @@ func TestIntegrationProjectAndTaskLifecycle(t *testing.T) { start := Time(time.Now().AddDate(0, 0, -2)) deadline := Time(time.Now().AddDate(0, 0, 2)) + ms, err := c.CreateMilestone(NewMilestoneRequest{ + ProjectID: *project.ID, + Title: "integration milestone", + Deadline: deadline, + }) + if err != nil { + t.Fatalf("CreateMilestone: %v", err) + } + if ms == nil || ms.ID == nil { + t.Fatal("created milestone without id") + } + t.Cleanup(func() { + if err := c.DeleteMilestone(*ms.ID); err != nil { + t.Logf("cleanup DeleteMilestone: %v", err) + } + }) + task, err := c.CreateProjectTask(NewProjectTaskRequest{ ProjectId: *project.ID, Title: "integration parent task", @@ -146,6 +163,7 @@ func TestIntegrationProjectAndTaskLifecycle(t *testing.T) { StartDate: start, Deadline: deadline, Priority: int(TaskPriorityNormal), + MilestoneId: int(*ms.ID), }) if err != nil { t.Fatalf("CreateProjectTask: %v", err) diff --git a/cmd/oo/cli_test.go b/cmd/oo/cli_test.go index 6a9561e..65baef8 100644 --- a/cmd/oo/cli_test.go +++ b/cmd/oo/cli_test.go @@ -53,6 +53,13 @@ func TestProjectsAlias(t *testing.T) { if cmd.Name() != "projects" { t.Fatalf("prj alias resolved to %q", cmd.Name()) } + ms, _, err := rootCmd.Find([]string{"projects", "milestone-create"}) + if err != nil { + t.Fatal(err) + } + if ms.Name() != "milestone-create" { + t.Fatalf("milestone-create resolved to %q", ms.Name()) + } } func TestNewOOReturnsErrorWithoutCredentials(t *testing.T) { diff --git a/cmd/oo/projects.go b/cmd/oo/projects.go index 9822a4d..cdc4a7f 100644 --- a/cmd/oo/projects.go +++ b/cmd/oo/projects.go @@ -6,6 +6,7 @@ import ( "os/exec" "strconv" "strings" + "time" onlyoffice "github.com/eslider/go-onlyoffice" "github.com/spf13/cobra" @@ -22,6 +23,7 @@ func init() { projectsCmd.AddCommand(prjListCmd()) projectsCmd.AddCommand(prjGetCmd()) projectsCmd.AddCommand(prjMilestonesCmd()) + projectsCmd.AddCommand(prjMilestoneCreateCmd()) projectsCmd.AddCommand(prjCreateCmd()) projectsCmd.AddCommand(prjUpdateCmd()) projectsCmd.AddCommand(prjDeleteCmd()) @@ -118,6 +120,52 @@ func prjMilestonesCmd() *cobra.Command { } } +func prjMilestoneCreateCmd() *cobra.Command { + var deadline, desc string + var key bool + cmd := &cobra.Command{ + Use: "milestone-create PROJECT_ID TITLE", + Short: "Create a project milestone (Gantt row)", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + c, err := newOO(cmd) + if err != nil { + return err + } + pid, err := strconv.Atoi(args[0]) + if err != nil { + return fmt.Errorf("project id must be integer: %w", err) + } + if deadline == "" { + return fmt.Errorf("--deadline YYYY-MM-DD is required") + } + day, err := time.Parse("2006-01-02", deadline) + if err != nil { + return fmt.Errorf("deadline: %w", err) + } + ms, err := c.CreateMilestone(onlyoffice.NewMilestoneRequest{ + ProjectID: pid, + Title: args[1], + Deadline: onlyoffice.Time(day), + Description: desc, + IsKey: key, + }) + if err != nil { + return err + } + printObject(map[string]any{ + "id": derefInt64(ms.ID), + "title": derefString(ms.Title), + }) + return nil + }, + } + cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD") + cmd.Flags().StringVar(&desc, "description", "", "description") + cmd.Flags().BoolVar(&key, "key", false, "mark as key milestone") + return cmd +} + func prjCreateCmd() *cobra.Command { var desc, resp string var country, company string diff --git a/cmd/oo/tasks.go b/cmd/oo/tasks.go index 3bcc301..a3171c7 100644 --- a/cmd/oo/tasks.go +++ b/cmd/oo/tasks.go @@ -2,7 +2,11 @@ package main import ( "fmt" + "os" + "strconv" + "time" + onlyoffice "github.com/eslider/go-onlyoffice" "github.com/spf13/cobra" ) @@ -82,11 +86,12 @@ func taskGetCmd() *cobra.Command { } func taskCreateCmd() *cobra.Command { - var project, desc, deadline, prio string + var project, desc, deadline, start, prio string + var milestone int64 cmd := &cobra.Command{ Use: "create TITLE", Aliases: []string{"add"}, - Short: "Create a project task", + Short: "Create a project task (Gantt bar when --start/--deadline/--milestone set)", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { c, err := newOO(cmd) @@ -100,6 +105,51 @@ func taskCreateCmd() *cobra.Command { case "low": p = -1 } + if start != "" || milestone != 0 { + pid, err := strconv.Atoi(project) + if err != nil && project != "" { + return fmt.Errorf("project id: %w", err) + } + if project == "" { + pid, err = strconv.Atoi(os.Getenv("OO_PROJECT_ID")) + if err != nil { + return fmt.Errorf("--project or OO_PROJECT_ID required") + } + } + req := onlyoffice.NewProjectTaskRequest{ + ProjectId: pid, + Title: args[0], + Description: desc, + Priority: p, + MilestoneId: int(milestone), + } + if deadline == "" { + deadline = time.Now().Add(14 * 24 * time.Hour).Format("2006-01-02") + } + d, err := time.Parse("2006-01-02", deadline) + if err != nil { + return fmt.Errorf("deadline: %w", err) + } + req.Deadline = onlyoffice.Time(d) + if start != "" { + s, err := time.Parse("2006-01-02", start) + if err != nil { + return fmt.Errorf("start: %w", err) + } + req.StartDate = onlyoffice.Time(s) + } else { + req.StartDate = onlyoffice.Time(time.Now()) + } + task, err := c.CreateProjectTask(req) + if err != nil { + return err + } + printObject(map[string]any{ + "id": derefInt(task.ID), + "title": derefString(task.Title), + }) + return nil + } out, err := c.AddTask(cmd.Context(), project, args[0], desc, p, deadline) if err != nil { return err @@ -110,7 +160,9 @@ func taskCreateCmd() *cobra.Command { } cmd.Flags().StringVarP(&project, "project", "p", "", "project id (default $OO_PROJECT_ID)") cmd.Flags().StringVar(&desc, "description", "", "description") - cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD (default now+14d; always assigned to you)") + cmd.Flags().StringVar(&deadline, "deadline", "", "deadline YYYY-MM-DD (default now+14d)") + cmd.Flags().StringVar(&start, "start", "", "start YYYY-MM-DD (uses CreateProjectTask / Gantt)") + cmd.Flags().Int64Var(&milestone, "milestone", 0, "milestone id (Gantt row)") cmd.Flags().StringVar(&prio, "priority", "normal", "high|normal|low") return cmd } diff --git a/projects.go b/projects.go index d9d4a1c..4968121 100644 --- a/projects.go +++ b/projects.go @@ -134,6 +134,51 @@ func (c *Client) GetProjectByID(ctx context.Context, projectID string) (map[stri return c.ResponseObject(ctx, fmt.Sprintf("/api/2.0/project/%s.json", url.PathEscape(projectID))) } +// NewMilestoneRequest is the payload for CreateMilestone. +type NewMilestoneRequest struct { + ProjectID int `json:"-"` + Title string `json:"title"` + Deadline Time `json:"deadline"` + Description string `json:"description,omitempty"` + IsKey bool `json:"isKey"` + IsNotify bool `json:"isNotify"` + Responsible string `json:"responsible,omitempty"` +} + +// CreateMilestone creates a project milestone (Gantt row). +// POST /api/2.0/project/{id}/milestone +func (c *Client) CreateMilestone(req NewMilestoneRequest) (*Milestone, error) { + if req.Responsible == "" { + uid, err := c.SelfUserID(context.Background()) + if err != nil { + return nil, fmt.Errorf("CreateMilestone: self: %w", err) + } + req.Responsible = uid + } + ms := new(Milestone) + err := c.Query(Request{ + Uri: fmt.Sprintf("/api/2.0/project/%d/milestone", req.ProjectID), + Method: "POST", + Body: req, + }, &struct { + MetaResponse `json:",inline"` + Response *Milestone `json:"response"` + }{Response: ms}) + return ms, err +} + +// DeleteMilestone removes a milestone by id. +// DELETE /api/2.0/project/milestone/{id} +func (c *Client) DeleteMilestone(id int64) error { + return c.Query(Request{ + Uri: fmt.Sprintf("/api/2.0/project/milestone/%d", id), + Method: "DELETE", + }, &struct { + MetaResponse `json:",inline"` + Response *Milestone `json:"response"` + }{}) +} + // GetProjectMilestones returns milestones for the given project. // https://api1.onlyoffice.com/portals/method/project/post/api/2.0/project/%7bid%7d/milestone func (c *Client) GetProjectMilestones(project *Project) ([]*Milestone, error) {