// Vikunja is a to-do list application to facilitate your life. // Copyright 2018-present Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package commands import ( "context" "encoding/json" "strings" "github.com/spf13/cobra" "code.vikunja.io/veans/internal/client" "code.vikunja.io/veans/internal/output" "code.vikunja.io/veans/internal/status" ) type createFlags struct { description string statusName string priority int64 labels []string parent string blockedBy []string } func newCreateCmd() *cobra.Command { f := &createFlags{} cmd := &cobra.Command{ Use: "create ", Aliases: []string{"c"}, Short: "Create a new task", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { rt, err := loadRuntime() if err != nil { return err } task, err := runCreate(cmd.Context(), rt, args[0], f) if err != nil { return err } return json.NewEncoder(cmd.OutOrStdout()).Encode(task) }, } cmd.Flags().StringVarP(&f.description, "description", "d", "", "task description (markdown)") cmd.Flags().StringVarP(&f.statusName, "status", "s", "todo", "initial status (defaults to todo)") cmd.Flags().Int64Var(&f.priority, "priority", 0, "priority (0=unset, 1=low, 5=DO_NOW)") cmd.Flags().StringSliceVar(&f.labels, "label", nil, "labels to attach (repeatable; veans: prefix added if missing)") cmd.Flags().StringVar(&f.parent, "parent", "", "parent task ID (creates parenttask relation)") cmd.Flags().StringSliceVar(&f.blockedBy, "blocked-by", nil, "task IDs that block this one (repeatable)") return cmd } func runCreate(ctx context.Context, rt *runtime, title string, f *createFlags) (*client.Task, error) { st, err := status.Parse(f.statusName) if err != nil { return nil, err } bucketID, err := status.BucketID(st, rt.cfg.Buckets) if err != nil { return nil, err } created, err := rt.client.CreateTask(ctx, rt.cfg.ProjectID, &client.Task{ Title: strings.TrimSpace(title), Description: f.description, Priority: f.priority, ProjectID: rt.cfg.ProjectID, BucketID: bucketID, Done: st.Done(), }) if err != nil { return nil, err } // Vikunja places newly-created tasks in the view's default bucket // regardless of bucket_id in the create payload — move it explicitly // when the requested status isn't Todo. if st != status.Todo { if err := rt.client.MoveTaskToBucket(ctx, rt.cfg.ProjectID, rt.cfg.ViewID, bucketID, created.ID); err != nil { return nil, output.Wrap(output.CodeUnknown, err, "set initial bucket: %v", err) } } // Attach labels (lazily creating them under veans: namespace). for _, raw := range f.labels { title := normalizeLabelTitle(raw) l, err := getOrCreateLabelByTitle(ctx, rt.client, title) if err != nil { return nil, output.Wrap(output.CodeUnknown, err, "label %q: %v", title, err) } if err := rt.client.AddLabelToTask(ctx, created.ID, l.ID); err != nil { return nil, err } } // Parent relation. if f.parent != "" { parentID, err := rt.resolveTaskID(ctx, f.parent) if err != nil { return nil, err } if _, err := rt.client.CreateRelation(ctx, created.ID, parentID, "parenttask"); err != nil { return nil, err } } // Blocked-by relations. for _, ref := range f.blockedBy { blockerID, err := rt.resolveTaskID(ctx, ref) if err != nil { return nil, err } if _, err := rt.client.CreateRelation(ctx, created.ID, blockerID, "blocked"); err != nil { return nil, err } } // Re-fetch so the response reflects the labels and any post-create state. // The refetch is best-effort: if it fails (token expired mid-operation, // transient network blip), we still return the create result so callers // see the new task ID rather than a confusing error. final, err := rt.client.GetTask(ctx, created.ID) if err != nil { return created, nil //nolint:nilerr // intentional best-effort refetch } return final, nil }