Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ brew install ghp-cli

### Go Install
```bash
go install github.com/roboco-io/gh-project-cli@latest
go install github.com/roboco-io/gh-project-cli/cmd/ghp@latest
```

### Download Binary
Expand Down
Binary file modified bin/ghp
Binary file not shown.
2 changes: 0 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/roboco-io/gh-project-cli/internal/cmd/item"
"github.com/roboco-io/gh-project-cli/internal/cmd/project"
"github.com/roboco-io/gh-project-cli/internal/cmd/view"
"github.com/roboco-io/gh-project-cli/internal/cmd/workflow"
)

var (
Expand Down Expand Up @@ -77,7 +76,6 @@ Example:
cmd.AddCommand(item.NewItemCmd())
cmd.AddCommand(project.NewProjectCmd())
cmd.AddCommand(view.NewViewCmd())
cmd.AddCommand(workflow.NewWorkflowCmd())

// Initialize config
cobra.OnInitialize(initConfig)
Expand Down
4,760 changes: 4,760 additions & 0 deletions coverage.out

Large diffs are not rendered by default.

Binary file modified ghp
Binary file not shown.
52 changes: 51 additions & 1 deletion internal/api/graphql/fields.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package graphql

import "time"
import (
"strconv"
"strings"
"time"
)

// Field creation mutations and queries

Expand Down Expand Up @@ -47,11 +51,12 @@
}

// Field input types
type CreateFieldInput struct {

Check failure on line 54 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

fieldalignment: struct with 80 pointer bytes could be 72 (govet)
ProjectID string `json:"projectId"`
Name string `json:"name"`
DataType ProjectV2FieldDataType `json:"dataType"`
SingleSelectOptions []string `json:"singleSelectOptions,omitempty"`
Duration string `json:"duration,omitempty"`
}

type UpdateFieldInput struct {
Expand Down Expand Up @@ -100,6 +105,13 @@
inputMap["singleSelectOptions"] = options
}

// Add iteration field configuration
if input.DataType == ProjectV2FieldDataTypeIteration && input.Duration != "" {
inputMap["iterationSetting"] = map[string]interface{}{
"duration": parseDuration(input.Duration),
}
}

return map[string]interface{}{
"input": inputMap,
}
Expand Down Expand Up @@ -213,3 +225,41 @@
SingleSelectColorPink,
}
}

// parseDuration parses duration string like "2w", "1m" into days
func parseDuration(duration string) int {
if duration == "" {
return 14 // Default 2 weeks

Check failure on line 232 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 14, in <return> detected (mnd)
}

duration = strings.ToLower(strings.TrimSpace(duration))

// Handle numeric part and unit
var numStr string
var unit string

for i, char := range duration {
if char >= '0' && char <= '9' {
numStr += string(char)
} else {
unit = duration[i:]
break
}
}

num, err := strconv.Atoi(numStr)
if err != nil || num <= 0 {
return 14 // Default fallback

Check failure on line 252 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 14, in <return> detected (mnd)
}

switch unit {
case "d", "day", "days":
return num
case "w", "week", "weeks":
return num * 7

Check failure on line 259 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 7, in <return> detected (mnd)
case "m", "month", "months":
return num * 30

Check failure on line 261 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 30, in <return> detected (mnd)
default:
return 14 // Default fallback

Check failure on line 263 in internal/api/graphql/fields.go

View workflow job for this annotation

GitHub Actions / Lint

Magic number: 14, in <return> detected (mnd)
}
}
34 changes: 27 additions & 7 deletions internal/api/graphql/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,8 +191,12 @@ type RemoveItemFromProjectMutation struct {

// CreateProjectInput represents input for creating a project
type CreateProjectInput struct {
OwnerID string `json:"ownerId"`
Title string `json:"title"`
OwnerID string `json:"ownerId"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Readme string `json:"readme,omitempty"`
Visibility string `json:"visibility,omitempty"`
Repository string `json:"repository,omitempty"`
}

// UpdateProjectInput represents input for updating a project
Expand Down Expand Up @@ -230,12 +234,28 @@ type RemoveItemInput struct {
// Variable Builders

// BuildCreateProjectVariables builds variables for project creation
func BuildCreateProjectVariables(input CreateProjectInput) map[string]interface{} {
func BuildCreateProjectVariables(input *CreateProjectInput) map[string]interface{} {
inputMap := map[string]interface{}{
"ownerId": input.OwnerID,
"title": input.Title,
}

// Add optional fields only if provided
if input.Description != "" {
inputMap["description"] = input.Description
}
if input.Readme != "" {
inputMap["readme"] = input.Readme
}
if input.Visibility != "" {
inputMap["visibility"] = input.Visibility
}
if input.Repository != "" {
inputMap["repository"] = input.Repository
}

return map[string]interface{}{
"input": map[string]interface{}{
"ownerId": input.OwnerID,
"title": input.Title,
},
"input": inputMap,
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/graphql/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func TestVariableBuilders(t *testing.T) {
Title: "Test Project",
}

variables := BuildCreateProjectVariables(input)
variables := BuildCreateProjectVariables(&input)

assert.NotNil(t, variables)
assert.Contains(t, variables, "input")
Expand Down
78 changes: 62 additions & 16 deletions internal/cmd/field/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
)

// CreateOptions holds options for the create command
type CreateOptions struct {

Check failure on line 17 in internal/cmd/field/create.go

View workflow job for this annotation

GitHub Actions / Lint

fieldalignment: struct with 128 pointer bytes could be 120 (govet)
ProjectRef string
ProjectID string
Owner string
Name string
FieldType string
Format string
Options []string
Duration string
Number int
Org bool
}
Expand All @@ -30,7 +32,7 @@
opts := &CreateOptions{}

cmd := &cobra.Command{
Use: "create <owner>/<number> <name> <type>",
Use: "create [owner/number] [name] [type]",
Short: "Create a new project field",
Long: `Create a new custom field in a GitHub Project.

Expand All @@ -45,19 +47,28 @@
iteration - Iteration field for sprint/cycle planning

For single select fields, you can provide initial options using --options.
For iteration fields, you can specify duration using --duration.

Examples:
# Traditional syntax
ghp field create octocat/123 "Priority" text

# New syntax with flags (Issue #18)
ghp field create --project-id PROJECT_ID --name "Priority" --type single-select --options "Critical,High,Medium,Low"
ghp field create --project-id PROJECT_ID --name "Sprint" --type iteration --duration 2w
ghp field create octocat/123 "Story Points" number
ghp field create octocat/123 "Due Date" date
ghp field create octocat/123 "Status" single_select --options "Todo,In Progress,Done"
ghp field create --org myorg/456 "Sprint" iteration`,

Args: cobra.ExactArgs(3),
Args: cobra.MaximumNArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
opts.ProjectRef = args[0]
opts.Name = args[1]
opts.FieldType = args[2]
// Support both traditional args and new flag-based syntax
if len(args) == 3 {
opts.ProjectRef = args[0]
opts.Name = args[1]
opts.FieldType = args[2]
}
opts.Format = cmd.Flag("format").Value.String()
return runCreate(cmd.Context(), opts)
},
Expand All @@ -66,19 +77,41 @@
cmd.Flags().BoolVar(&opts.Org, "org", false, "Project belongs to an organization")
cmd.Flags().StringSliceVar(&opts.Options, "options", []string{}, "Options for single select field (comma-separated)")

// New flags for Issue #18 syntax
cmd.Flags().StringVar(&opts.ProjectID, "project-id", "", "Project ID (alternative to owner/number)")
cmd.Flags().StringVar(&opts.Name, "name", "", "Field name")
cmd.Flags().StringVar(&opts.FieldType, "type", "", "Field type (text, number, date, single_select, iteration)")
cmd.Flags().StringVar(&opts.Duration, "duration", "", "Duration for iteration field (e.g., 2w, 1m)")

return cmd
}

func runCreate(ctx context.Context, opts *CreateOptions) error {
// Parse project reference
// Support both traditional args and new flag-based syntax
var err error
if strings.Contains(opts.ProjectRef, "/") {
opts.Owner, opts.Number, err = service.ParseProjectReference(opts.ProjectRef)
if err != nil {
return fmt.Errorf("invalid project reference: %w", err)
var projectID string

if opts.ProjectID != "" {
// New syntax: --project-id flag
projectID = opts.ProjectID
if opts.Name == "" {
return fmt.Errorf("--name is required when using --project-id")
}
if opts.FieldType == "" {
return fmt.Errorf("--type is required when using --project-id")
}
} else if opts.ProjectRef != "" {
// Traditional syntax: positional args
if strings.Contains(opts.ProjectRef, "/") {
opts.Owner, opts.Number, err = service.ParseProjectReference(opts.ProjectRef)
if err != nil {
return fmt.Errorf("invalid project reference: %w", err)
}
} else {
return fmt.Errorf("project reference must be in format owner/number")
}
} else {
return fmt.Errorf("project reference must be in format owner/number")
return fmt.Errorf("either project reference (owner/number) or --project-id must be provided")
}

// Validate field name
Expand All @@ -104,18 +137,31 @@
fieldService := service.NewFieldService(client)
projectService := service.NewProjectService(client)

// Get project to verify it exists and get project ID
project, err := projectService.GetProject(ctx, opts.Owner, opts.Number, opts.Org)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
var project *graphql.ProjectV2

if opts.ProjectID != "" {
// New syntax: use project ID directly
// For new syntax, we'll create a mock project for output
project = &graphql.ProjectV2{
ID: projectID,
Title: fmt.Sprintf("Project %s", projectID),
}
} else {
// Traditional syntax: get project by owner/number
project, err = projectService.GetProject(ctx, opts.Owner, opts.Number, opts.Org)
if err != nil {
return fmt.Errorf("failed to get project: %w", err)
}
projectID = project.ID
}

// Create field
input := service.CreateFieldInput{
ProjectID: project.ID,
ProjectID: projectID,
Name: opts.Name,
DataType: dataType,
SingleSelectOptions: opts.Options,
Duration: opts.Duration,
}

field, err := fieldService.CreateField(ctx, input)
Expand Down
Loading
Loading