From bf74fabe44163ca4ed328c64167fa57fca20d387 Mon Sep 17 00:00:00 2001 From: XianwuLin Date: Thu, 9 Apr 2026 15:07:17 +0800 Subject: [PATCH 1/3] feat: support set task dueDate --- README.md | 20 ++++++++++-- cmd/task.go | 29 ++++++++++++++--- cmd/task_due_date.go | 48 ++++++++++++++++++++++++++++ cmd/task_due_date_test.go | 59 +++++++++++++++++++++++++++++++++++ internal/client/tasks_test.go | 34 +++++++++++++++++++- internal/models/task.go | 8 +++++ internal/models/task_test.go | 25 +++++++++++++++ 7 files changed, 215 insertions(+), 8 deletions(-) create mode 100644 cmd/task_due_date.go create mode 100644 cmd/task_due_date_test.go diff --git a/README.md b/README.md index c6edf7c..4061b21 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,8 @@ dida365 project data dida365 task create \ --title "Deploy to production" \ --project-id "proj123" \ - --content "Deploy v2.0.0" + --content "Deploy v2.0.0" \ + --due-date "2026-04-30" ``` **List tasks in a project:** @@ -122,7 +123,8 @@ dida365 task get task456 --project-id proj123 dida365 task update task456 \ --project-id proj123 \ --title "New title" \ - --content "Updated content" + --content "Updated content" \ + --due-date "2026-05-01 18:30" ``` **Complete a task:** @@ -144,6 +146,18 @@ dida365 project columns dida365 task move task456 --project-id proj123 --column-id ``` +**Accepted `--due-date` formats:** +```bash +# All-day deadline +dida365 task create --title "Monthly summary" --project-id "proj123" --due-date "2026-04-30" + +# Specific local time +dida365 task create --title "Release window" --project-id "proj123" --due-date "2026-04-30 18:30" + +# RFC3339 +dida365 task update task456 --project-id proj123 --due-date "2026-04-30T23:59:59+08:00" +``` + ## Scripting Examples ### Extract specific fields with jq @@ -217,6 +231,8 @@ Commands output the resource or array directly: "id": "task123", "projectId": "proj456", "title": "Task title", + "dueDate": "2026-04-30T15:59:59Z", + "isAllDay": false, "status": 0, "sortOrder": 1 } diff --git a/cmd/task.go b/cmd/task.go index ee7e432..c9f85a5 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -13,6 +13,7 @@ var ( taskProjectID string taskContent string taskColumnID string + taskDueDate string ) var taskCmd = &cobra.Command{ @@ -99,6 +100,7 @@ func init() { taskCreateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (required)") taskCreateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskCreateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskCreateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskCreateCmd.MarkFlagRequired("title") taskCreateCmd.MarkFlagRequired("project-id") @@ -109,14 +111,10 @@ func init() { // Flags for update command taskUpdateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (optional)") taskUpdateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskUpdateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskUpdateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskUpdateCmd.MarkFlagRequired("project-id") - // Flags for complete command - taskCompleteCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") - taskCompleteCmd.MarkFlagRequired("project-id") - - // Flags for delete command taskDeleteCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskDeleteCmd.MarkFlagRequired("project-id") } @@ -130,6 +128,16 @@ func runTaskCreate(cmd *cobra.Command, args []string) error { Content: taskContent, } + if cmd.Flags().Changed("due-date") { + normalizedDueDate, isAllDay, err := normalizeDueDateInput(taskDueDate) + if err != nil { + outputError(err, "VALIDATION_ERROR", 5) + return nil + } + taskCreate.DueDate = normalizedDueDate + taskCreate.IsAllDay = &isAllDay + } + task, err := c.CreateTask(taskCreate) if err != nil { outputError(err, "API_ERROR", 3) @@ -186,6 +194,17 @@ func runTaskUpdate(cmd *cobra.Command, args []string) error { hasChanges = true } + if cmd.Flags().Changed("due-date") { + normalizedDueDate, isAllDay, err := normalizeDueDateInput(taskDueDate) + if err != nil { + outputError(err, "VALIDATION_ERROR", 5) + return nil + } + updates.DueDate = &normalizedDueDate + updates.IsAllDay = &isAllDay + hasChanges = true + } + if !hasChanges { outputError(fmt.Errorf("no fields to update"), "VALIDATION_ERROR", 5) return nil diff --git a/cmd/task_due_date.go b/cmd/task_due_date.go new file mode 100644 index 0000000..9a843e8 --- /dev/null +++ b/cmd/task_due_date.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "fmt" + "strings" + "time" +) + +const apiDateTimeLayout = "2006-01-02T15:04:05-0700" + +var dueDateLayoutsWithZone = []string{ + time.RFC3339Nano, + time.RFC3339, + "2006-01-02T15:04:05.999999999-0700", + "2006-01-02T15:04:05-0700", +} + +var dueDateLayoutsLocal = []string{ + "2006-01-02 15:04", + "2006-01-02T15:04", + "2006-01-02 15:04:05", + "2006-01-02T15:04:05", +} + +func normalizeDueDateInput(input string) (string, bool, error) { + trimmed := strings.TrimSpace(input) + if trimmed == "" { + return "", false, fmt.Errorf("due date cannot be empty") + } + + if t, err := time.ParseInLocation("2006-01-02", trimmed, time.Local); err == nil { + return t.Format(apiDateTimeLayout), true, nil + } + + for _, layout := range dueDateLayoutsLocal { + if t, err := time.ParseInLocation(layout, trimmed, time.Local); err == nil { + return t.Format(apiDateTimeLayout), false, nil + } + } + + for _, layout := range dueDateLayoutsWithZone { + if t, err := time.Parse(layout, trimmed); err == nil { + return t.Format(apiDateTimeLayout), false, nil + } + } + + return "", false, fmt.Errorf("unsupported due date format %q; use YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339", input) +} \ No newline at end of file diff --git a/cmd/task_due_date_test.go b/cmd/task_due_date_test.go new file mode 100644 index 0000000..baaec56 --- /dev/null +++ b/cmd/task_due_date_test.go @@ -0,0 +1,59 @@ +package cmd + +import "testing" + +func TestNormalizeDueDateInput(t *testing.T) { + tests := []struct { + name string + input string + want string + wantAllDay bool + wantErr bool + }{ + { + name: "date only becomes all day", + input: "2026-04-30", + want: "2026-04-30T00:00:00+0800", + wantAllDay: true, + }, + { + name: "local datetime with space", + input: "2026-04-30 18:30", + want: "2026-04-30T18:30:00+0800", + wantAllDay: false, + }, + { + name: "rfc3339 with timezone", + input: "2026-04-30T23:59:59+08:00", + want: "2026-04-30T23:59:59+0800", + wantAllDay: false, + }, + { + name: "reject unsupported format", + input: "this saturday", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotAllDay, err := normalizeDueDateInput(tt.input) + if tt.wantErr { + if err == nil { + t.Fatal("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("normalizeDueDateInput() = %q, want %q", got, tt.want) + } + if gotAllDay != tt.wantAllDay { + t.Fatalf("normalizeDueDateInput() allDay = %v, want %v", gotAllDay, tt.wantAllDay) + } + }) + } +} \ No newline at end of file diff --git a/internal/client/tasks_test.go b/internal/client/tasks_test.go index 3490ca0..d08e3e5 100644 --- a/internal/client/tasks_test.go +++ b/internal/client/tasks_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/bearzk/dida365-cli/internal/config" "github.com/bearzk/dida365-cli/internal/models" @@ -30,12 +31,23 @@ func TestCreateTask(t *testing.T) { if req.ProjectID != "proj123" { t.Errorf("projectId: got %s, want proj123", req.ProjectID) } + if req.DueDate != "2026-04-30T15:59:59+0000" { + t.Errorf("dueDate: got %s, want 2026-04-30T15:59:59+0000", req.DueDate) + } + if req.IsAllDay == nil || *req.IsAllDay { + t.Errorf("isAllDay: got %v, want false", req.IsAllDay) + } + + dueDate := models.FlexTime{Time: time.Date(2026, 4, 30, 15, 59, 59, 0, time.UTC)} + isAllDay := false task := models.Task{ ID: "task456", ProjectID: req.ProjectID, Title: req.Title, Content: req.Content, + DueDate: &dueDate, + IsAllDay: &isAllDay, Status: 0, SortOrder: 0, } @@ -57,6 +69,8 @@ func TestCreateTask(t *testing.T) { Title: "Test Task", ProjectID: "proj123", Content: "Task description", + DueDate: "2026-04-30T15:59:59+0000", + IsAllDay: &[]bool{false}[0], } result, err := client.CreateTask(taskCreate) @@ -81,10 +95,15 @@ func TestGetTask(t *testing.T) { t.Errorf("path: got %s, want /open/v1/project/proj123/task/task456", r.URL.Path) } + dueDate := models.FlexTime{Time: time.Date(2026, 4, 30, 15, 59, 59, 0, time.UTC)} + isAllDay := false + task := models.Task{ ID: "task456", ProjectID: "proj123", Title: "Existing Task", + DueDate: &dueDate, + IsAllDay: &isAllDay, Status: 0, SortOrder: 1, } @@ -129,7 +148,7 @@ func TestListTasks(t *testing.T) { "id": "proj123", "name": "Test Project", "tasks": []models.Task{ - {ID: "task1", ProjectID: "proj123", Title: "Task 1", Status: 0, SortOrder: 1}, + {ID: "task1", ProjectID: "proj123", Title: "Task 1", DueDate: &models.FlexTime{Time: time.Date(2026, 4, 30, 15, 59, 59, 0, time.UTC)}, Status: 0, SortOrder: 1}, {ID: "task2", ProjectID: "proj123", Title: "Task 2", Status: 2, SortOrder: 2}, }, } @@ -190,12 +209,23 @@ func TestUpdateTask(t *testing.T) { if req.Title == nil || *req.Title != "Updated Title" { t.Error("title not updated correctly") } + if req.DueDate == nil || *req.DueDate != "2026-05-01T00:00:00+0000" { + t.Errorf("dueDate: got %v, want 2026-05-01T00:00:00+0000", req.DueDate) + } + if req.IsAllDay == nil || !*req.IsAllDay { + t.Errorf("isAllDay: got %v, want true", req.IsAllDay) + } + + dueDate := models.FlexTime{Time: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)} + isAllDay := true task := models.Task{ ID: "task456", ProjectID: "proj123", Title: newTitle, Content: newContent, + DueDate: &dueDate, + IsAllDay: &isAllDay, Status: 0, SortOrder: 1, } @@ -216,6 +246,8 @@ func TestUpdateTask(t *testing.T) { updates := &models.TaskUpdate{ Title: &newTitle, Content: &newContent, + DueDate: &[]string{"2026-05-01T00:00:00+0000"}[0], + IsAllDay: &[]bool{true}[0], } result, err := client.UpdateTask("proj123", "task456", updates) diff --git a/internal/models/task.go b/internal/models/task.go index 463fdd9..18d9858 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -6,6 +6,10 @@ type Task struct { ProjectID string `json:"projectId"` Title string `json:"title"` Content string `json:"content,omitempty"` + StartDate *FlexTime `json:"startDate,omitempty"` + DueDate *FlexTime `json:"dueDate,omitempty"` + TimeZone string `json:"timeZone,omitempty"` + IsAllDay *bool `json:"isAllDay,omitempty"` Status int `json:"status"` // 0=normal, 2=completed Priority int `json:"priority,omitempty"` // 0=none, 1=low, 3=med, 5=high CompletedTime *FlexTime `json:"completedTime,omitempty"` @@ -18,6 +22,8 @@ type TaskCreate struct { Title string `json:"title"` ProjectID string `json:"projectId"` Content string `json:"content,omitempty"` + DueDate string `json:"dueDate,omitempty"` + IsAllDay *bool `json:"isAllDay,omitempty"` } // TaskUpdate represents the payload for updating a task @@ -27,4 +33,6 @@ type TaskUpdate struct { Title *string `json:"title,omitempty"` Content *string `json:"content,omitempty"` ColumnID *string `json:"columnId,omitempty"` + DueDate *string `json:"dueDate,omitempty"` + IsAllDay *bool `json:"isAllDay,omitempty"` } diff --git a/internal/models/task_test.go b/internal/models/task_test.go index 207c32d..a10a549 100644 --- a/internal/models/task_test.go +++ b/internal/models/task_test.go @@ -8,6 +8,8 @@ import ( func TestTaskJSONMarshaling(t *testing.T) { nowFlex := FlexTime{Time: time.Now().UTC()} + dueFlex := FlexTime{Time: time.Date(2026, 4, 30, 15, 59, 59, 0, time.UTC)} + isAllDay := false tests := []struct { name string @@ -21,6 +23,9 @@ func TestTaskJSONMarshaling(t *testing.T) { ProjectID: "proj456", Title: "Buy groceries", Content: "Milk, eggs, bread", + DueDate: &dueFlex, + TimeZone: "Asia/Shanghai", + IsAllDay: &isAllDay, Status: 0, Priority: 3, CompletedTime: &nowFlex, @@ -31,6 +36,8 @@ func TestTaskJSONMarshaling(t *testing.T) { "projectId": "proj456", "title": "Buy groceries", "content": "Milk, eggs, bread", + "timeZone": "Asia/Shanghai", + "isAllDay": false, "status": float64(0), "priority": float64(3), "sortOrder": float64(1), @@ -106,6 +113,8 @@ func TestTaskCreateJSONMarshaling(t *testing.T) { Title: "New task", ProjectID: "proj123", Content: "Task description", + DueDate: "2026-04-30T15:59:59+0000", + IsAllDay: &[]bool{false}[0], } data, err := json.Marshal(tc) @@ -127,16 +136,26 @@ func TestTaskCreateJSONMarshaling(t *testing.T) { if result["content"] != "Task description" { t.Errorf("content: got %v, want Task description", result["content"]) } + if result["dueDate"] != "2026-04-30T15:59:59+0000" { + t.Errorf("dueDate: got %v, want 2026-04-30T15:59:59+0000", result["dueDate"]) + } + if result["isAllDay"] != false { + t.Errorf("isAllDay: got %v, want false", result["isAllDay"]) + } } func TestTaskUpdateJSONMarshaling(t *testing.T) { t.Run("update with non-empty values", func(t *testing.T) { title := "Updated title" content := "Updated content" + dueDate := "2026-04-30T15:59:59+0000" + isAllDay := false tu := TaskUpdate{ Title: &title, Content: &content, + DueDate: &dueDate, + IsAllDay: &isAllDay, } data, err := json.Marshal(tu) @@ -155,6 +174,12 @@ func TestTaskUpdateJSONMarshaling(t *testing.T) { if result["content"] != "Updated content" { t.Errorf("content: got %v, want Updated content", result["content"]) } + if result["dueDate"] != dueDate { + t.Errorf("dueDate: got %v, want %s", result["dueDate"], dueDate) + } + if result["isAllDay"] != false { + t.Errorf("isAllDay: got %v, want false", result["isAllDay"]) + } }) t.Run("empty update with all nil pointers", func(t *testing.T) { From a21a71ac04319253e3d71f8f58a4cdbc3fd15f63 Mon Sep 17 00:00:00 2001 From: XianwuLin Date: Fri, 10 Apr 2026 19:47:02 +0800 Subject: [PATCH 2/3] add start-date and fix task complete bug --- README.md | 27 +- cmd/task.go | 30 +- cmd/{task_due_date.go => task_date.go} | 2 +- ...ask_due_date_test.go => task_date_test.go} | 8 +- docs/dida365-openapi.md | 938 ++++++++++++++++++ internal/models/task.go | 2 + 6 files changed, 991 insertions(+), 16 deletions(-) rename cmd/{task_due_date.go => task_date.go} (94%) rename cmd/{task_due_date_test.go => task_date_test.go} (79%) create mode 100644 docs/dida365-openapi.md diff --git a/README.md b/README.md index 4061b21..b0b1124 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ dida365 task create \ --title "Deploy to production" \ --project-id "proj123" \ --content "Deploy v2.0.0" \ - --due-date "2026-04-30" + --due-date "2026-04-30" \ + --start-date "2026-05-01" ``` **List tasks in a project:** @@ -124,7 +125,8 @@ dida365 task update task456 \ --project-id proj123 \ --title "New title" \ --content "Updated content" \ - --due-date "2026-05-01 18:30" + --due-date "2026-05-01 18:30" \ + --start-date "2026-05-01 18:30" ``` **Complete a task:** @@ -146,16 +148,23 @@ dida365 project columns dida365 task move task456 --project-id proj123 --column-id ``` -**Accepted `--due-date` formats:** +**Accepted `--start-date` and `--due-date` formats:** ```bash -# All-day deadline -dida365 task create --title "Monthly summary" --project-id "proj123" --due-date "2026-04-30" +# All-day (date only) +dida365 task create --title "Monthly summary" --project-id "proj123" \ + --start-date "2026-04-28" --due-date "2026-04-30" -# Specific local time -dida365 task create --title "Release window" --project-id "proj123" --due-date "2026-04-30 18:30" +# Specific local time (space separator) +dida365 task create --title "Release window" --project-id "proj123" \ + --start-date "2026-04-30 10:00" --due-date "2026-04-30 18:30" -# RFC3339 -dida365 task update task456 --project-id proj123 --due-date "2026-04-30T23:59:59+08:00" +# Specific local time (T separator) +dida365 task create --title "Release window" --project-id "proj123" \ + --start-date "2026-04-30T10:00" --due-date "2026-04-30T18:30" + +# RFC3339 with timezone +dida365 task update task456 --project-id proj123 \ + --start-date "2026-04-30T09:00:00+08:00" --due-date "2026-04-30T23:59:59+08:00" ``` ## Scripting Examples diff --git a/cmd/task.go b/cmd/task.go index c9f85a5..c79f1b0 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -13,6 +13,7 @@ var ( taskProjectID string taskContent string taskColumnID string + taskStartDate string taskDueDate string ) @@ -100,6 +101,7 @@ func init() { taskCreateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (required)") taskCreateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskCreateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskCreateCmd.Flags().StringVar(&taskStartDate, "start-date", "", "Task start date, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskCreateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskCreateCmd.MarkFlagRequired("title") taskCreateCmd.MarkFlagRequired("project-id") @@ -111,12 +113,17 @@ func init() { // Flags for update command taskUpdateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (optional)") taskUpdateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskUpdateCmd.Flags().StringVar(&taskStartDate, "start-date", "", "Task start date, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskUpdateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskUpdateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskUpdateCmd.MarkFlagRequired("project-id") taskDeleteCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskDeleteCmd.MarkFlagRequired("project-id") + + // Flags for complete command + taskCompleteCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") + taskCompleteCmd.MarkFlagRequired("project-id") } func runTaskCreate(cmd *cobra.Command, args []string) error { @@ -128,8 +135,17 @@ func runTaskCreate(cmd *cobra.Command, args []string) error { Content: taskContent, } + if cmd.Flags().Changed("start-date") { + normalizedStartDate, _, err := normalizeDateInput(taskStartDate) + if err != nil { + outputError(err, "VALIDATION_ERROR", 5) + return nil + } + taskCreate.StartDate = normalizedStartDate + } + if cmd.Flags().Changed("due-date") { - normalizedDueDate, isAllDay, err := normalizeDueDateInput(taskDueDate) + normalizedDueDate, isAllDay, err := normalizeDateInput(taskDueDate) if err != nil { outputError(err, "VALIDATION_ERROR", 5) return nil @@ -194,8 +210,18 @@ func runTaskUpdate(cmd *cobra.Command, args []string) error { hasChanges = true } + if cmd.Flags().Changed("start-date") { + normalizedStartDate, _, err := normalizeDateInput(taskStartDate) + if err != nil { + outputError(err, "VALIDATION_ERROR", 5) + return nil + } + updates.StartDate = &normalizedStartDate + hasChanges = true + } + if cmd.Flags().Changed("due-date") { - normalizedDueDate, isAllDay, err := normalizeDueDateInput(taskDueDate) + normalizedDueDate, isAllDay, err := normalizeDateInput(taskDueDate) if err != nil { outputError(err, "VALIDATION_ERROR", 5) return nil diff --git a/cmd/task_due_date.go b/cmd/task_date.go similarity index 94% rename from cmd/task_due_date.go rename to cmd/task_date.go index 9a843e8..076f0ee 100644 --- a/cmd/task_due_date.go +++ b/cmd/task_date.go @@ -22,7 +22,7 @@ var dueDateLayoutsLocal = []string{ "2006-01-02T15:04:05", } -func normalizeDueDateInput(input string) (string, bool, error) { +func normalizeDateInput(input string) (string, bool, error) { trimmed := strings.TrimSpace(input) if trimmed == "" { return "", false, fmt.Errorf("due date cannot be empty") diff --git a/cmd/task_due_date_test.go b/cmd/task_date_test.go similarity index 79% rename from cmd/task_due_date_test.go rename to cmd/task_date_test.go index baaec56..0c5d005 100644 --- a/cmd/task_due_date_test.go +++ b/cmd/task_date_test.go @@ -2,7 +2,7 @@ package cmd import "testing" -func TestNormalizeDueDateInput(t *testing.T) { +func TestnormalizeDateInput(t *testing.T) { tests := []struct { name string input string @@ -37,7 +37,7 @@ func TestNormalizeDueDateInput(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, gotAllDay, err := normalizeDueDateInput(tt.input) + got, gotAllDay, err := normalizeDateInput(tt.input) if tt.wantErr { if err == nil { t.Fatal("expected error, got nil") @@ -49,10 +49,10 @@ func TestNormalizeDueDateInput(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if got != tt.want { - t.Fatalf("normalizeDueDateInput() = %q, want %q", got, tt.want) + t.Fatalf("normalizeDateInput() = %q, want %q", got, tt.want) } if gotAllDay != tt.wantAllDay { - t.Fatalf("normalizeDueDateInput() allDay = %v, want %v", gotAllDay, tt.wantAllDay) + t.Fatalf("normalizeDateInput() allDay = %v, want %v", gotAllDay, tt.wantAllDay) } }) } diff --git a/docs/dida365-openapi.md b/docs/dida365-openapi.md new file mode 100644 index 0000000..c98d113 --- /dev/null +++ b/docs/dida365-openapi.md @@ -0,0 +1,938 @@ +# Dida365 Open API + +## Introduction + +Welcome to the Dida365 Open API documentation. Dida365 is a powerful task management application that allows users to easily manage and organize their daily tasks, deadlines, and projects. With Dida365 Open API, developers can integrate Dida365's powerful task management features into their own applications and create a seamless user experience. + +## Getting Started +To get started using the Dida365 Open API, you will need to register your application and obtain a client ID and client secret. You can register your application by visiting the [Dida365 Developer Center](https://developer.dida365.com/manage). Once registered, you will receive a client ID and client secret which you will use to authenticate your requests. + +## Authorization +### Get Access Token +In order to call Dida365's Open API, it is necessary to obtain an access token for the corresponding user. Dida365 uses the OAuth2 protocol to obtain the access token. + +#### First Step +Redirect the user to the Dida365 authorization page, https://dida365.com/oauth/authorize. The required parameters are as follows: + +| Name | Description | +| ------ |----------------------------------------------------------------------------------------------| +| client_id | Application unique id | +| scope | Spaces-separated permission scope. The currently available scopes are tasks:write tasks:read | +| state | Passed to redirect url as is | +| redirect_uri | User-configured redirect url | +| response_type | Fixed as code | + +Example: +https://dida365.com/oauth/authorize?scope=scope&client_id=client_id&state=state&redirect_uri=redirect_uri&response_type=code + + + +#### Second Step +After the user grants access, Dida365 will redirect the user back to your application's `redirect_uri` with an authorization code as a query parameter. + +| Name | Description | +| ------ | ------ | +| code | Authorization code for subsequent access tokens | +| state | state parameter passed in the first step | + + +#### Third Step + +To exchange the authorization code for an access token, make a POST request to `https://dida365.com/oauth/token` with the following parameters(Content-Type: application/x-www-form-urlencoded): + +| Name | Description | +| ------ | ------ | +| client_id | The username is located in the **HEADER** using the **Basic Auth** authentication method | +| client_secret | The password is located in the **HEADER** using the **Basic Auth** authentication method | +| code | The code obtained in the second step | +| grant_type | grant type, now only authorization_code | +| scope | spaces-separated permission scope. The currently available scopes are tasks: write, tasks: read | +| redirect_uri | user-configured redirect url | + +Access_token for openapi request authentication in the request response +``` + { +... +"access_token": "access token value" +... +} +``` + + +#### Request OpenAPI +Set **Authorization** in the header, the value is **Bearer** `access token value` +``` +Authorization: Bearer e*****b +``` + + +## API Reference +The Dida365 Open API provides a RESTful interface for accessing and managing user tasks, lists, and other related resources. The API is based on the standard HTTP protocol and supports JSON data formats. + +### Task +#### Get Task By Project ID And Task ID +``` +GET /open/v1/project/{projectId}/task/{taskId} +``` + +##### Parameters +| Type | Name | Description | Schema | +| -------- | ------------------------ | ------------------ | ------ | +| **Path** | **projectId** *required* | Project identifier | string | +| **Path** | **taskId** *required* | Task identifier | string | + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|[Task](openapi.md#Task)| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example + +###### Request +``` http +GET /open/v1/project/{{projectId}}/task/{{taskId}} HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +###### Response +```json +{ +"id" : "63b7bebb91c0a5474805fcd4", +"isAllDay" : true, +"projectId" : "6226ff9877acee87727f6bca", +"title" : "Task Title", +"content" : "Task Content", +"desc" : "Task Description", +"timeZone" : "America/Los_Angeles", +"repeatFlag" : "RRULE:FREQ=DAILY;INTERVAL=1", +"startDate" : "2019-11-13T03:00:00+0000", +"dueDate" : "2019-11-14T03:00:00+0000", +"reminders" : [ "TRIGGER:P0DT9H0M0S", "TRIGGER:PT0S" ], +"priority" : 1, +"status" : 0, +"completedTime" : "2019-11-13T03:00:00+0000", +"sortOrder" : 12345, +"items" : [ { + "id" : "6435074647fd2e6387145f20", + "status" : 0, + "title" : "Item Title", + "sortOrder" : 12345, + "startDate" : "2019-11-13T03:00:00+0000", + "isAllDay" : false, + "timeZone" : "America/Los_Angeles", + "completedTime" : "2019-11-13T03:00:00+0000" + } ] +} +``` + + +#### Create Task +``` +POST /open/v1/task +``` + +##### Parameters +| **Type** | **Name** | **Description** | **Schema** | +| -------- |-----------------------| -------------------------------------------------------------------------------------------------------- | ------- | +| **Body** | title *required* | Task title | string | +| **Body** | projectId *required* | Project id | string | +| **Body** | content | Task content | string | +| **Body** | desc | Description of checklist | string | +| **Body** | isAllDay | All day | boolean | +| **Body** | startDate | Start date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | +| **Body** | dueDate | Due date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | +| **Body** | timeZone | The time zone in which the time is specified | String | +| **Body** | reminders | Lists of reminders specific to the task | list | +| **Body** | repeatFlag | Recurring rules of task | string | +| **Body** | priority | The priority of task, default is "0" | integer | +| **Body** | sortOrder | The order of task | integer | +| **Body** | items | The list of subtasks | list | +| **Body** | items.title | Subtask title | string | +| **Body** | items.startDate | Start date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format | date | +| **Body** | items.isAllDay | All day | boolean | +| **Body** | items.sortOrder | The order of subtask | integer | +| **Body** | items.timeZone | The time zone in which the Start time is specified | string | +| **Body** | items.status | The completion status of subtask | integer | +| **Body** | items.completedTime | Completed time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|[Task](openapi.md#Definitions#Task)| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + + +##### Example +###### Request +```http +POST /open/v1/task HTTP/1.1 +Host: api.dida365.com +Content-Type: application/json +Authorization: Bearer {{token}} +{ + ... + "title":"Task Title", + "projectId":"6226ff9877acee87727f6bca" + ... +} +``` + +###### Response +```json +{ +"id" : "63b7bebb91c0a5474805fcd4", +"projectId" : "6226ff9877acee87727f6bca", +"title" : "Task Title", +"content" : "Task Content", +"desc" : "Task Description", +"isAllDay" : true, +"startDate" : "2019-11-13T03:00:00+0000", +"dueDate" : "2019-11-14T03:00:00+0000", +"timeZone" : "America/Los_Angeles", +"reminders" : [ "TRIGGER:P0DT9H0M0S", "TRIGGER:PT0S" ], +"repeatFlag" : "RRULE:FREQ=DAILY;INTERVAL=1", +"priority" : 1, +"status" : 0, +"completedTime" : "2019-11-13T03:00:00+0000", +"sortOrder" : 12345, +"items" : [ { + "id" : "6435074647fd2e6387145f20", + "status" : 1, + "title" : "Subtask Title", + "sortOrder" : 12345, + "startDate" : "2019-11-13T03:00:00+0000", + "isAllDay" : false, + "timeZone" : "America/Los_Angeles", + "completedTime" : "2019-11-13T03:00:00+0000" + } ] +} +``` + + + +#### Update Task +``` +POST /open/v1/task/{taskId} +``` + +##### Parameters +| **Type** | **Name** | **Description** | **Schema** | +| -------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | ------- | +| **Path** | **taskId** *required* | Task identifier | string | +| **Body** | id *required* | Task id. | string | +| **Body** | projectId *required* | Project id. | string | +| **Body** | title | Task title | string | +| **Body** | content | Task content | string | +| **Body** | desc | Description of checklist | string | +| **Body** | isAllDay | All day | boolean | +| **Body** | startDate | Start date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | +| **Body** | dueDate | Due date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | +| **Body** | timeZone | The time zone in which the time is specified | String | +| **Body** | reminders | Lists of reminders specific to the task | list | +| **Body** | repeatFlag | Recurring rules of task | string | +| **Body** | priority | The priority of task, default is "normal" | integer | +| **Body** | sortOrder | The order of task | integer | +| **Body** | items | The list of subtasks | list | +| **Body** | items.title | Subtask title | string | +| **Body** | items.startDate | Start date and time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format | date | +| **Body** | items.isAllDay | All day | boolean | +| **Body** | items.sortOrder | The order of subtask | integer | +| **Body** | items.timeZone | The time zone in which the Start time is specified | string | +| **Body** | items.status | The completion status of subtask | integer | +| **Body** | items.completedTime | Completed time in `"yyyy-MM-dd'T'HH:mm:ssZ"` format
**Example** : `"2019-11-13T03:00:00+0000"` | date | + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|[Task](openapi.md#Definitions#Task)| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example + +###### Request +```http +POST /open/v1/task/{{taskId}} HTTP/1.1 +Host: api.dida365.com +Content-Type: application/json +Authorization: Bearer {{token}} +{ + "id": "{{taskId}}", + "projectId": "{{projectId}}", + "title": "Task Title", + "priority": 1, + ... +} +``` + +###### Response +```json +{ +"id" : "63b7bebb91c0a5474805fcd4", +"projectId" : "6226ff9877acee87727f6bca", +"title" : "Task Title", +"content" : "Task Content", +"desc" : "Task Description", +"isAllDay" : true, +"startDate" : "2019-11-13T03:00:00+0000", +"dueDate" : "2019-11-14T03:00:00+0000", +"timeZone" : "America/Los_Angeles", +"reminders" : [ "TRIGGER:P0DT9H0M0S", "TRIGGER:PT0S" ], +"repeatFlag" : "RRULE:FREQ=DAILY;INTERVAL=1", +"priority" : 1, +"status" : 0, +"completedTime" : "2019-11-13T03:00:00+0000", +"sortOrder" : 12345, +"items" : [ { + "id" : "6435074647fd2e6387145f20", + "status" : 1, + "title" : "Item Title", + "sortOrder" : 12345, + "startDate" : "2019-11-13T03:00:00+0000", + "isAllDay" : false, + "timeZone" : "America/Los_Angeles", + "completedTime" : "2019-11-13T03:00:00+0000" + } ], +"kind": "CHECKLIST" +} +``` + + + +#### Complete Task +``` +POST /open/v1/project/{projectId}/task/{taskId}/complete +``` + + +##### Parameters + +|Type|Name|Description|Schema| +|---|---|---|---| +|**Path**|**projectId** *required*|Project identifier|string| +|**Path**|**taskId** *required*|Task identifier|string| + + +##### Responses + +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|No Content| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example +###### Request +```http +POST /open/v1/project/{{projectId}}/task/{{taskId}}/complete HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +#### Delete Task +``` +DELETE /open/v1/project/{projectId}/task/{taskId} +``` + +##### Parameters +| Type | Name | Description | Schema | +| -------- | ------------------------ | ------------------ | ------ | +| **Path** | **projectId** *required* | Project identifier | string | +| **Path** | **taskId** *required* | Task identifier | string | + + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|No Content| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + + +##### Example + +###### Request +```http +DELETE /open/v1/project/{{projectId}}/task/{{taskId}} HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +#### Move Task +``` +POST /open/v1/task/move +``` +Moves one or more tasks between projects. + +##### Request Body +A JSON array containing task move operations. + +| Type | Name | Description | Schema | +|----------|------------------------------|-----------------------------------|--------| +| **Body** | **fromProjectId** *required* | The ID of the source project | string | +| **Body** | **toProjectId** *required* | The ID of the destination project | string | +| **Body** | **taskId** *required* | The ID of the task to move | string | + + +##### Responses +|HTTP Code|Description| Schema | +|---|---|--| +|**200**|OK| Returns an array of move results, including the task ID and its new etag) | +|**201**|Created| No Content | +|**401**|Unauthorized| No Content | +|**403**|Forbidden| No Content | +|**404**|Not Found| No Content | + + +##### Example + +###### Request +```http +POST /open/v1/task/move HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +[ + { + "fromProjectId":"69a850ef1c20d2030e148fdd", + "toProjectId":"69a850f41c20d2030e148fdf", + "taskId":"69a850f8b9061f374d54a046" + } +] +``` + +###### Response + +```json +[ + { + "id": "69a850f8b9061f374d54a046", + "etag": "43p2zso1" + } +] +``` + + +#### List Completed Tasks +``` +POST /open/v1/task/completed +``` +Retrieves a list of tasks marked as completed within specific projects and a given time range. + +##### Request Body +A JSON object containing filter criteria. All fields are optional, but at least one filter is recommended to narrow down results. + +| Type | Name | Description | Schema | +|----------|-------------|--|--------| +| **Body** | **projectIds** | List of project identifier | list | +| **Body** | **startDate** | The start of the time range (inclusive). Filters tasks where completedTime ≥ startDate | date | +| **Body** | **endDate** | The end of the time range (inclusive). Filters tasks where completedTime ≤ endDate | date | + + +##### Responses +| HTTP Code |Description| Schema | +|-----------|---|-----------------------------------------------------| +| **200** |OK| < [Task](openapi.md#Definitions#Task) > array | +| **201** |Created| No Content | +| **401** |Unauthorized| No Content | +| **403** |Forbidden| No Content | +| **404** |Not Found| No Content | + + +##### Example + +###### Request +```http +POST /open/v1/task/completed HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +{ + "projectIds": [ + "69a850f41c20d2030e148fdf" + ], + "startDate":"2026-03-01T00:58:20.000+0000", + "endDate":"2026-03-05T10:58:20.000+0000" +} +``` + +###### Response +```json +[ + { + "id": "69a850f8b9061f374d54a046", + "projectId": "69a850f41c20d2030e148fdf", + "sortOrder": -1099511627776, + "title": "update", + "content": "", + "timeZone": "America/Los_Angeles", + "isAllDay": false, + "priority": 0, + "completedTime": "2026-03-04T23:58:20.000+0000", + "status": 2, + "etag": "t3kc5m5f", + "kind": "TEXT" + } +] +``` + +#### Filter Tasks +``` +POST /open/v1/task/filter +``` + +Retrieves a list of tasks based on advanced filtering criteria, including project scope, date ranges, priority levels, tags, and status. + +##### Parameters +| Type | Name | Description | Schema | +|----------|----------------|-------------------------------------------------------|--------| +| **Body** | **projectIds** | Filters tasks belonging to the specified project ID | list | +| **Body** | **startDate** | Filters tasks where the task's startDate ≥ startDate | date | +| **Body** | **endDate** | Filters tasks where the task's startDate ≤ endDate | date | +| **Body** | **proiority** | Filters tasks by specific priority levels, Valid Values: None(0), Low(1), Mediunm(3), High(5) | list | +| **Body** | **tag** | Filters tasks that contain all of the specified tags | list | +| **Body** | **status** | Filters tasks by their current status codes (e.g., [0] for Open, [2] for Completed) | list | + + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +| **200** |OK| < [Task](openapi.md#Definitions#Task) > array | +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + + +##### Example + +###### Request +```http +POST /open/v1/task/filter HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +{ + "projectIds": [ + "69a850f41c20d2030e148fdf" + ], + "startDate":"2026-03-01T00:58:20.000+0000", + "endDate":"2026-03-06T10:58:20.000+0000", + "priority": [0], + "tag": ["urgent"], + "status": [0] +} +``` + +###### Response +```json +[ + { + "id": "69a85785b9061f3c217e9de6", + "projectId": "69a850f41c20d2030e148fdf", + "sortOrder": -2199023255552, + "title": "task1", + "content": "", + "desc": "", + "startDate": "2026-03-05T00:00:00.000+0000", + "dueDate": "2026-03-05T00:00:00.000+0000", + "timeZone": "America/Los_Angeles", + "isAllDay": false, + "priority": 0, + "status": 0, + "tags": [ + "tag" + ], + "etag": "cic6e3cg", + "kind": "TEXT" + }, + { + "id": "69a8ea79b9061f4d803f6b32", + "projectId": "69a850f41c20d2030e148fdf", + "sortOrder": -3298534883328, + "title": "task2", + "content": "", + "startDate": "2026-03-05T00:00:00.000+0000", + "dueDate": "2026-03-05T00:00:00.000+0000", + "timeZone": "America/Los_Angeles", + "isAllDay": false, + "priority": 0, + "status": 0, + "tags": [ + "tag" + ], + "etag": "0nvpcxzh", + "kind": "TEXT" + } +] +``` + +### Project +#### Get User Project +``` +GET /open/v1/project +``` + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|< [Project](openapi.md#Definitions#Project) > array| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example +###### Request +```http +GET /open/v1/project HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +###### Response +```json +[{ +"id": "6226ff9877acee87727f6bca", +"name": "project name", +"color": "#F18181", +"closed": false, +"groupId": "6436176a47fd2e05f26ef56e", +"viewMode": "list", +"permission": "write", +"kind": "TASK" +}] +``` + +#### Get Project By ID +``` +GET /open/v1/project/{projectId} +``` + +##### Parameters +| Type | Name | Description | Schema | +| -------- | ---------------------- | ------------------ | ------ | +| **Path** | **project** *required* | Project identifier | string | + +##### Responses +| HTTP Code | Description | Schema | +| --------- | ------------ | ----------------------- | +| **200** | OK | [Project](openapi.md#Definitions#Project)| +| **401** | Unauthorized | No Content | +| **403** | Forbidden | No Content | +| **404** | Not Found | No Content | + +##### Example + +###### Request path +```http +GET /open/v1/project/{{projectId}} HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +###### Response +```json +{ + "id": "6226ff9877acee87727f6bca", + "name": "project name", + "color": "#F18181", + "closed": false, + "groupId": "6436176a47fd2e05f26ef56e", + "viewMode": "list", + "kind": "TASK" +} +``` + + +#### Get Project With Data + +``` +GET /open/v1/project/{projectId}/data +``` + +##### Parameters +|Type|Name| Description |Schema| +|---|---|------------------------------|---| +|**Path**|**projectId** *required*| Project identifier, "inbox" |string| + +##### Responses + +| HTTP Code | Description | Schema | +| --------- | ------------ | ----------------------- | +| **200** | OK | [ProjectData](openapi.md#Definitions#ProjectData) | +| **401** | Unauthorized | No Content | +| **403** | Forbidden | No Content | +| **404** | Not Found | No Content | + +##### Example +###### Request +```http +GET /open/v1/project/{{projectId}}/data HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + +###### Response +```json +{ +"project": { + "id": "6226ff9877acee87727f6bca", + "name": "project name", + "color": "#F18181", + "closed": false, + "groupId": "6436176a47fd2e05f26ef56e", + "viewMode": "list", + "kind": "TASK" +}, +"tasks": [{ + "id": "6247ee29630c800f064fd145", + "isAllDay": true, + "projectId": "6226ff9877acee87727f6bca", + "title": "Task Title", + "content": "Task Content", + "desc": "Task Description", + "timeZone": "America/Los_Angeles", + "repeatFlag": "RRULE:FREQ=DAILY;INTERVAL=1", + "startDate": "2019-11-13T03:00:00+0000", + "dueDate": "2019-11-14T03:00:00+0000", + "reminders": [ + "TRIGGER:P0DT9H0M0S", + "TRIGGER:PT0S" + ], + "priority": 1, + "status": 0, + "completedTime": "2019-11-13T03:00:00+0000", + "sortOrder": 12345, + "items": [{ + "id": "6435074647fd2e6387145f20", + "status": 0, + "title": "Subtask Title", + "sortOrder": 12345, + "startDate": "2019-11-13T03:00:00+0000", + "isAllDay": false, + "timeZone": "America/Los_Angeles", + "completedTime": "2019-11-13T03:00:00+0000" + }] +}], +"columns": [{ + "id": "6226ff9e76e5fc39f2862d1b", + "projectId": "6226ff9877acee87727f6bca", + "name": "Column Name", + "sortOrder": 0 +}] +} +``` + +#### Create Project + +``` +POST /open/v1/project +``` + +##### Parameters +| **Type** | **Name** | **Description** | **Schema** | +| -------- | ---------------- | --------------------------------------- | --------------- | +| **Body** | name *required* | name of the project | string | +| **Body** | color | color of project, eg. "#F18181" | string | +| **Body** | sortOrder | sort order value of the project | integer (int64) | +| **Body** | viewMode | view mode, "list", "kanban", "timeline" | string | +| **Body** | kind | project kind, "TASK", "NOTE" | string | + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|[Project](openapi.md#Definitions#Project)| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example +###### Request +```http +POST /open/v1/project HTTP/1.1 +Host: api.dida365.com +Content-Type: application/json +Authorization: Bearer {{token}} +{ + "name": "project name", + "color": "#F18181", + "viewMode": "list", + "kind": "task" +} +``` + +###### Response +```json +{ +"id": "6226ff9877acee87727f6bca", +"name": "project name", +"color": "#F18181", +"sortOrder": 0, +"viewMode": "list", +"kind": "TASK" +} +``` + +#### Update Project +``` +POST /open/v1/project/{projectId} +``` + +##### Parameters +| **Type** | **Parameter** | **Description** | Schema | +| -------- | -------------------- | --------------------------------------- | --------------- | +| **Path** | projectId *required* | project identifier | string | +| **Body** | name | name of the project | string | +| **Body** | color | color of the project | string | +| **Body** | sortOrder | sort order value, default 0 | integer (int64) | +| **Body** | viewMode | view mode, "list", "kanban", "timeline" | string | +| **Body** | kind | project kind, "TASK", "NOTE" | string | + +##### Responses +|HTTP Code|Description|Schema| +|---|---|---| +|**200**|OK|[Project](openapi.md#Definitions#Project)| +|**201**|Created|No Content| +|**401**|Unauthorized|No Content| +|**403**|Forbidden|No Content| +|**404**|Not Found|No Content| + +##### Example +###### Request +```http +POST /open/v1/project/{{projectId}} HTTP/1.1 +Host: api.dida365.com +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "name": "Project Name", + "color": "#F18181", + "viewMode": "list", + "kind": "TASK" +} +``` + +###### Response +```json +{ +"id": "6226ff9877acee87727f6bca", +"name": "Project Name", +"color": "#F18181", +"sortOrder": 0, +"viewMode": "list", +"kind": "TASK" +} +``` + +#### Delete Project +``` +DELETE /open/v1/project/{projectId} +``` + +##### Parameters +| Type | Name | Description | Schema | +| ---- | ------------------------ | ------------------ | ------ | +| Path | **projectId** *required* | Project identifier | string | + +##### Responses +| HTTP Code | Description | Schema | +| --------- | ------------ | ---------- | +| **200** | OK | No Content | +| **401** | Unauthorized | No Content | +| **403** | Forbidden | No Content | +| **404** | Not Found | No Content | + +##### Example +###### Request +```http +DELETE /open/v1/project/{{projectId}} HTTP/1.1 +Host: api.dida365.com +Authorization: Bearer {{token}} +``` + + +## Definitions + +### ChecklistItem + +|Name|Description|Schema| +|---|---|---| +|**id**|Subtask identifier|string| +|**title**|Subtask title|string| +|**status**|The completion status of subtask
**Value** : Normal: `0`, Completed: `1`|integer (int32)| +|**completedTime**|Subtask completed time in `"yyyy-MM-dd'T'HH:mm:ssZ"`
**Example** : `"2019-11-13T03:00:00+0000"`|string (date-time)| +|**isAllDay**|All day|boolean| +|**sortOrder**|Subtask sort order
**Example** : `234444`|integer (int64)| +|**startDate**|Subtask start date time in `"yyyy-MM-dd'T'HH:mm:ssZ"`
**Example** : `"2019-11-13T03:00:00+0000"`|string (date-time)| +|**timeZone**|Subtask timezone
**Example** : `"America/Los_Angeles"`|string| + + +### Task + +| Name | Description | Schema | +|-------------------|----------------------------------------------------------------------------------------------------| --------------------------------------------------- | +| **id** | Task identifier | string | +| **projectId** | Task project id | string | +| **title** | Task title | string | +| **isAllDay** | All day | boolean | +| **completedTime** | Task completed time in ``"yyyy-MM-dd'T'HH:mm:ssZ"``
**Example** : `"2019-11-13T03:00:00+0000"` | string (date-time) | +| **content** | Task content | string | +| **desc** | Task description of checklist | string | +| **dueDate** | Task due date time in `"yyyy-MM-dd'T'HH:mm:ssZ"`
**Example** : `"2019-11-13T03:00:00+0000"` | string (date-time) | +| **items** | Subtasks of Task | < [ChecklistItem](openapi.md#checklistitem) > array | +| **priority** | Task priority
**Value** : None:`0`, Low:`1`, Medium:`3`, High`5` | integer (int32) | +| **reminders** | List of reminder triggers
**Example** : `[ "TRIGGER:P0DT9H0M0S", "TRIGGER:PT0S" ]` | < string > array | +| **repeatFlag** | Recurring rules of task
**Example** : `"RRULE:FREQ=DAILY;INTERVAL=1"` | string | +| **sortOrder** | Task sort order
**Example** : `12345` | integer (int64) | +| **startDate** | Start date time in `"yyyy-MM-dd'T'HH:mm:ssZ"`
**Example** : `"2019-11-13T03:00:00+0000"` | string (date-time) | +| **status** | Task completion status
**Value** : Normal: `0`, Completed: `2` | integer (int32) | +| **timeZone** | Task timezone
**Example** : `"America/Los_Angeles"` | string | +| **kind** | "TEXT", "NOTE", "CHECKLIST" | string | + + +### Project +| Name | Description | Schema | +| -------------- | --------------------------------------- | --------------- | +| **id** | Project identifier | string | +| **name** | Project name | string | +| **color** | Project color | string | +| **sortOrder** | Order value | integer (int64) | +| **closed** | Projcet closed | boolean | +| **groupId** | Project group identifier | string | +| **viewMode** | view mode, "list", "kanban", "timeline" | string | +| **permission** | "read", "write" or "comment" | string | +| **kind** | "TASK" or "NOTE" | string | + + +### Column +| Name | Description | Schema | +| --------- | ------------------ | --------------- | +| id | Column identifier | string | +| projectId | Project identifier | string | +| name | Column name | string | +| sortOrder | Order value | integer (int64) | + + +### ProjectData +| Name | Description | Schema | +| ------- | -------------------------- | ------------------ | +| project | Project info | [Project](openapi.md#Definitions#Project) | +| tasks | Undone tasks under project | <[Task](openapi.md#Definitions#Task)> array | +| columns | Columns under project | <[Column](openapi.md#Definitions#Column)> array | + + +## Feedback and Support + +If you have any questions or feedback regarding the Dida365 Open API documentation, please contact us at [support@dida365.com](mailto:support@dida365.com). We appreciate your input and will work to address any concerns or issues as quickly as possible. Thank you for choosing Dida! \ No newline at end of file diff --git a/internal/models/task.go b/internal/models/task.go index 18d9858..6d2ca8e 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -22,6 +22,7 @@ type TaskCreate struct { Title string `json:"title"` ProjectID string `json:"projectId"` Content string `json:"content,omitempty"` + StartDate string `json:"startDate,omitempty"` DueDate string `json:"dueDate,omitempty"` IsAllDay *bool `json:"isAllDay,omitempty"` } @@ -33,6 +34,7 @@ type TaskUpdate struct { Title *string `json:"title,omitempty"` Content *string `json:"content,omitempty"` ColumnID *string `json:"columnId,omitempty"` + StartDate *string `json:"startDate,omitempty"` DueDate *string `json:"dueDate,omitempty"` IsAllDay *bool `json:"isAllDay,omitempty"` } From a003ce256e106bcdf1e3be2d2cce9d7bccfad328 Mon Sep 17 00:00:00 2001 From: XianwuLin Date: Tue, 28 Apr 2026 13:58:05 +0800 Subject: [PATCH 3/3] feat: support set task desc --- cmd/task.go | 15 +++++++++++++++ internal/models/task.go | 3 +++ 2 files changed, 18 insertions(+) diff --git a/cmd/task.go b/cmd/task.go index c79f1b0..18de7cb 100644 --- a/cmd/task.go +++ b/cmd/task.go @@ -12,9 +12,11 @@ var ( taskTitle string taskProjectID string taskContent string + taskDesc string taskColumnID string taskStartDate string taskDueDate string + taskShowDesc bool ) var taskCmd = &cobra.Command{ @@ -101,6 +103,7 @@ func init() { taskCreateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (required)") taskCreateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") taskCreateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskCreateCmd.Flags().StringVar(&taskDesc, "desc", "", "Task description / checklist description (optional)") taskCreateCmd.Flags().StringVar(&taskStartDate, "start-date", "", "Task start date, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskCreateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskCreateCmd.MarkFlagRequired("title") @@ -108,11 +111,13 @@ func init() { // Flags for get command taskGetCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") + taskGetCmd.Flags().BoolVar(&taskShowDesc, "show-desc", false, "Show task description field") taskGetCmd.MarkFlagRequired("project-id") // Flags for update command taskUpdateCmd.Flags().StringVar(&taskTitle, "title", "", "Task title (optional)") taskUpdateCmd.Flags().StringVar(&taskContent, "content", "", "Task content (optional)") + taskUpdateCmd.Flags().StringVar(&taskDesc, "desc", "", "Task description / checklist description (optional)") taskUpdateCmd.Flags().StringVar(&taskStartDate, "start-date", "", "Task start date, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskUpdateCmd.Flags().StringVar(&taskDueDate, "due-date", "", "Task deadline, accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, YYYY-MM-DDTHH:MM, or RFC3339") taskUpdateCmd.Flags().StringVar(&taskProjectID, "project-id", "", "Project ID (required)") @@ -133,6 +138,7 @@ func runTaskCreate(cmd *cobra.Command, args []string) error { Title: taskTitle, ProjectID: taskProjectID, Content: taskContent, + Desc: taskDesc, } if cmd.Flags().Changed("start-date") { @@ -174,6 +180,10 @@ func runTaskGet(cmd *cobra.Command, args []string) error { return nil } + if !taskShowDesc { + task.Desc = "" + } + outputJSON(task) return nil } @@ -210,6 +220,11 @@ func runTaskUpdate(cmd *cobra.Command, args []string) error { hasChanges = true } + if cmd.Flags().Changed("desc") { + updates.Desc = &taskDesc + hasChanges = true + } + if cmd.Flags().Changed("start-date") { normalizedStartDate, _, err := normalizeDateInput(taskStartDate) if err != nil { diff --git a/internal/models/task.go b/internal/models/task.go index 6d2ca8e..afee737 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -6,6 +6,7 @@ type Task struct { ProjectID string `json:"projectId"` Title string `json:"title"` Content string `json:"content,omitempty"` + Desc string `json:"desc,omitempty"` StartDate *FlexTime `json:"startDate,omitempty"` DueDate *FlexTime `json:"dueDate,omitempty"` TimeZone string `json:"timeZone,omitempty"` @@ -22,6 +23,7 @@ type TaskCreate struct { Title string `json:"title"` ProjectID string `json:"projectId"` Content string `json:"content,omitempty"` + Desc string `json:"desc,omitempty"` StartDate string `json:"startDate,omitempty"` DueDate string `json:"dueDate,omitempty"` IsAllDay *bool `json:"isAllDay,omitempty"` @@ -33,6 +35,7 @@ type TaskUpdate struct { ProjectID string `json:"projectId"` Title *string `json:"title,omitempty"` Content *string `json:"content,omitempty"` + Desc *string `json:"desc,omitempty"` ColumnID *string `json:"columnId,omitempty"` StartDate *string `json:"startDate,omitempty"` DueDate *string `json:"dueDate,omitempty"`