diff --git a/README.md b/README.md index fa3a7c5..408a42a 100644 --- a/README.md +++ b/README.md @@ -125,12 +125,15 @@ projects -- Lists projects workpackages -- Lists work packages # Discover flags: hitting completion key after -op update workpackge 42 - +op update workpackage 42 - # returns --action -a -- Executes a custom action on a work package --assignee -- Assign a user to the work package --attach -- Attach a file to the work package +--description -- Change the work package description +--description-file -- Read the work package description from a file --help -h -- help for workpackage +--parent -- Move the work package below another work package --subject -- Change the subject of the work package --type -t -- Change the work package type ``` @@ -146,10 +149,16 @@ of examples, that might be useful for a great number of people. # Creating a work package in a project only by subject. # Work package is created with many default values (as for type and status), # very similar to how a work package is created inline in a work package table. -op create workpackge --project 11 'Document new CLI tool' +op create workpackage --project 11 'Document new CLI tool' # Same command with shorthands and directly open it in a browser to continue working on it. -op create workpackge -p11 'Document new CLI tool' -o +op create workpackage -p11 'Document new CLI tool' -o + +# Create a feature below an epic. +op create workpackage -p11 --type Feature --parent 42 'Build export flow' + +# Create a work package with a markdown description from a file. +op create workpackage -p11 --description-file ./description.md 'Document new CLI tool' ``` #### Listing @@ -170,7 +179,13 @@ op update workpackage 42 --action Claim # Batch updating some properties of a work package # Valid input will get processed, while invalid (e.g. wrongly typed) input will get omitted -op update workpackage 42 --subject 'The new subject' --status 'In Progress' --type Implementation +op update workpackage 42 --subject 'The new subject' --type Implementation + +# Move a work package below another work package +op update workpackage 42 --parent 12 + +# Updating the description of a work package +op update workpackage 42 --description-file ./description.md # Uploading an attachment to a work package op update workpackage 42 --attach ./Downloads/Report.pdf diff --git a/cmd/create/create.go b/cmd/create/create.go index e03767c..2bc5808 100644 --- a/cmd/create/create.go +++ b/cmd/create/create.go @@ -34,5 +34,27 @@ func init() { "Change the work package type", ) + createWorkPackageCmd.Flags().StringVar( + &descriptionFlag, + "description", + "", + "Set the work package description", + ) + + createWorkPackageCmd.Flags().StringVar( + &descriptionFileFlag, + "description-file", + "", + "Read the work package description from a file", + ) + createWorkPackageCmd.MarkFlagsMutuallyExclusive("description", "description-file") + + createWorkPackageCmd.Flags().Uint64Var( + &parentFlag, + "parent", + 0, + "Create the work package below another work package", + ) + RootCmd.AddCommand(createWorkPackageCmd) } diff --git a/cmd/create/work_package.go b/cmd/create/work_package.go index 5e1bee4..663b508 100644 --- a/cmd/create/work_package.go +++ b/cmd/create/work_package.go @@ -2,6 +2,7 @@ package create import ( "fmt" + "strconv" "github.com/spf13/cobra" @@ -14,6 +15,9 @@ import ( var projectId uint64 var shouldOpenWorkPackageInBrowser bool var typeFlag string +var descriptionFlag string +var descriptionFileFlag string +var parentFlag uint64 var createWorkPackageCmd = &cobra.Command{ Use: "workpackage [subject]", @@ -53,6 +57,15 @@ func createOptions(subject string) map[work_packages.CreateOption]string { if len(typeFlag) > 0 { options[work_packages.CreateType] = typeFlag } + if len(descriptionFlag) > 0 { + options[work_packages.CreateDescription] = descriptionFlag + } + if len(descriptionFileFlag) > 0 { + options[work_packages.CreateDescriptionFile] = descriptionFileFlag + } + if parentFlag > 0 { + options[work_packages.CreateParent] = strconv.FormatUint(parentFlag, 10) + } return options } diff --git a/cmd/update/update.go b/cmd/update/update.go index d6e9fc1..86f646f 100644 --- a/cmd/update/update.go +++ b/cmd/update/update.go @@ -50,4 +50,23 @@ func addWorkPackageFlags() { "", "Change the work package type", ) + workPackageCmd.Flags().StringVar( + &descriptionFlag, + "description", + "", + "Change the work package description", + ) + workPackageCmd.Flags().StringVar( + &descriptionFileFlag, + "description-file", + "", + "Read the work package description from a file", + ) + workPackageCmd.MarkFlagsMutuallyExclusive("description", "description-file") + workPackageCmd.Flags().Uint64Var( + &parentFlag, + "parent", + 0, + "Move the work package below another work package", + ) } diff --git a/cmd/update/work_package.go b/cmd/update/work_package.go index ad80ec7..d0fa0eb 100644 --- a/cmd/update/work_package.go +++ b/cmd/update/work_package.go @@ -11,11 +11,14 @@ import ( ) var ( - actionFlag string - assigneeFlag uint64 - attachFlag string - subjectFlag string - typeFlag string + actionFlag string + assigneeFlag uint64 + attachFlag string + subjectFlag string + typeFlag string + descriptionFlag string + descriptionFileFlag string + parentFlag uint64 ) var workPackageCmd = &cobra.Command{ @@ -63,6 +66,15 @@ func updateOptions() map[work_packages.UpdateOption]string { if len(typeFlag) > 0 { options[work_packages.UpdateType] = typeFlag } + if len(descriptionFlag) > 0 { + options[work_packages.UpdateDescription] = descriptionFlag + } + if len(descriptionFileFlag) > 0 { + options[work_packages.UpdateDescriptionFile] = descriptionFileFlag + } + if parentFlag > 0 { + options[work_packages.UpdateParent] = strconv.FormatUint(parentFlag, 10) + } return options } diff --git a/components/resources/work_packages/create.go b/components/resources/work_packages/create.go index 65e5118..36a38d8 100644 --- a/components/resources/work_packages/create.go +++ b/components/resources/work_packages/create.go @@ -18,11 +18,17 @@ type CreateOption int const ( CreateSubject CreateOption = iota CreateType + CreateDescription + CreateDescriptionFile + CreateParent ) var createMap = map[CreateOption]func(projectId uint64, workPackage *dtos.WorkPackageDto, input string) error{ - CreateSubject: subjectCreate, - CreateType: typeCreate, + CreateSubject: subjectCreate, + CreateType: typeCreate, + CreateDescription: descriptionCreate, + CreateDescriptionFile: descriptionFileCreate, + CreateParent: parentCreate, } func subjectCreate(_ uint64, workPackage *dtos.WorkPackageDto, input string) error { @@ -60,6 +66,36 @@ func typeCreate(projectId uint64, workPackage *dtos.WorkPackageDto, input string return nil } +func descriptionCreate(_ uint64, workPackage *dtos.WorkPackageDto, input string) error { + workPackage.Description = description(input) + + return nil +} + +func descriptionFileCreate(_ uint64, workPackage *dtos.WorkPackageDto, input string) error { + description, err := descriptionFromFile(input) + if err != nil { + return err + } + + workPackage.Description = description + return nil +} + +func parentCreate(_ uint64, workPackage *dtos.WorkPackageDto, input string) error { + parent, err := parentLink(input) + if err != nil { + return err + } + + if workPackage.Links == nil { + workPackage.Links = &dtos.WorkPackageLinksDto{} + } + + workPackage.Links.Parent = parent + return nil +} + func Create(projectId uint64, options map[CreateOption]string) (*models.WorkPackage, error) { return create(projectId, options) } diff --git a/components/resources/work_packages/description.go b/components/resources/work_packages/description.go new file mode 100644 index 0000000..e0c2848 --- /dev/null +++ b/components/resources/work_packages/description.go @@ -0,0 +1,31 @@ +package work_packages + +import ( + "os" + "strconv" + + "github.com/opf/openproject-cli/components/paths" + "github.com/opf/openproject-cli/dtos" +) + +func description(input string) *dtos.LongTextDto { + return &dtos.LongTextDto{Format: "markdown", Raw: input} +} + +func descriptionFromFile(path string) (*dtos.LongTextDto, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + return description(string(data)), nil +} + +func parentLink(input string) (*dtos.LinkDto, error) { + parentId, err := strconv.ParseUint(input, 10, 64) + if err != nil { + return nil, err + } + + return &dtos.LinkDto{Href: paths.WorkPackage(parentId)}, nil +} diff --git a/components/resources/work_packages/update.go b/components/resources/work_packages/update.go index 7cc751d..67c3493 100644 --- a/components/resources/work_packages/update.go +++ b/components/resources/work_packages/update.go @@ -23,14 +23,20 @@ const ( UpdateAttachment UpdateSubject UpdateType + UpdateDescription + UpdateDescriptionFile + UpdateParent ) -var patchableUpdates = []UpdateOption{UpdateSubject, UpdateType, UpdateAssignee} +var patchableUpdates = []UpdateOption{UpdateSubject, UpdateType, UpdateAssignee, UpdateDescription, UpdateDescriptionFile, UpdateParent} var patchMap = map[UpdateOption]func(patch, workPackage *dtos.WorkPackageDto, input string) (string, error){ - UpdateAssignee: assigneePatch, - UpdateType: typePatch, - UpdateSubject: subjectPatch, + UpdateAssignee: assigneePatch, + UpdateType: typePatch, + UpdateSubject: subjectPatch, + UpdateDescription: descriptionPatch, + UpdateDescriptionFile: descriptionFilePatch, + UpdateParent: parentPatch, } func Update(id uint64, options map[UpdateOption]string) (*models.WorkPackage, error) { @@ -150,6 +156,35 @@ func subjectPatch(patch, _ *dtos.WorkPackageDto, input string) (string, error) { return fmt.Sprintf("Subject -> %s", input), nil } +func descriptionPatch(patch, _ *dtos.WorkPackageDto, input string) (string, error) { + patch.Description = description(input) + return "Description -> updated", nil +} + +func descriptionFilePatch(patch, _ *dtos.WorkPackageDto, input string) (string, error) { + description, err := descriptionFromFile(input) + if err != nil { + return "", err + } + + patch.Description = description + return fmt.Sprintf("Description -> %s", input), nil +} + +func parentPatch(patch, _ *dtos.WorkPackageDto, input string) (string, error) { + parent, err := parentLink(input) + if err != nil { + return "", err + } + + if patch.Links == nil { + patch.Links = &dtos.WorkPackageLinksDto{} + } + + patch.Links.Parent = parent + return fmt.Sprintf("Parent -> #%s", input), nil +} + func assigneePatch(patch, _ *dtos.WorkPackageDto, input string) (string, error) { userId, _ := strconv.ParseUint(input, 10, 64) diff --git a/dtos/long_text.go b/dtos/long_text.go index 8bd246f..b32b7e4 100644 --- a/dtos/long_text.go +++ b/dtos/long_text.go @@ -5,7 +5,7 @@ import "github.com/opf/openproject-cli/models" type LongTextDto struct { Format string `json:"format"` Raw string `json:"raw"` - Html string `json:"html"` + Html string `json:"html,omitempty"` } // ///////////// MODEL CONVERSION /////////////// diff --git a/dtos/work_package.go b/dtos/work_package.go index 8110746..adbcc86 100644 --- a/dtos/work_package.go +++ b/dtos/work_package.go @@ -9,6 +9,7 @@ type WorkPackageLinksDto struct { AddAttachment *LinkDto `json:"addAttachment,omitempty"` Status *LinkDto `json:"status,omitempty"` Project *LinkDto `json:"project,omitempty"` + Parent *LinkDto `json:"parent,omitempty"` Assignee *LinkDto `json:"assignee,omitempty"` Type *LinkDto `json:"type,omitempty"` CustomActions []*LinkDto `json:"customActions,omitempty"` @@ -21,7 +22,7 @@ type WorkPackageDto struct { Links *WorkPackageLinksDto `json:"_links,omitempty"` Description *LongTextDto `json:"description,omitempty"` Embedded *embeddedDto `json:"_embedded,omitempty"` - LockVersion int `json:"lockVersion,omitempty"` + LockVersion int `json:"lockVersion"` } type embeddedDto struct { @@ -48,13 +49,18 @@ type CreateWorkPackageDto struct { /////////////// MODEL CONVERSION /////////////// func (dto *WorkPackageDto) Convert() *models.WorkPackage { + description := "" + if dto.Description != nil { + description = dto.Description.Raw + } + return &models.WorkPackage{ Id: uint64(dto.Id), Subject: dto.Subject, Type: dto.Links.Type.Title, Assignee: dto.Links.Assignee.Title, Status: dto.Links.Status.Title, - Description: dto.Description.Raw, + Description: description, LockVersion: dto.LockVersion, } }