Verified Commit b93a55e3 authored by Carlos Corona's avatar Carlos Corona Committed by GitLab
Browse files

feat(workitems): Implement `UpdateWorkItem()`

Changelog: Improvements
parent eec07a4a
Loading
Loading
Loading
Loading
+67 −14
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@ import (
	gitlab "gitlab.com/gitlab-org/api/client-go/v2"
)

func TestCreateWorkItem(t *testing.T) {
func TestCreateGetUpdateWorkItem(t *testing.T) {
	t.Parallel()

	client := SetupIntegrationClient(t)
@@ -19,22 +19,75 @@ func TestCreateWorkItem(t *testing.T) {
	// GIVEN a test group
	group := CreateTestGroup(t, client)

	// AND a work item creation options
	opt := gitlab.CreateWorkItemOptions{
		Title:       "Test Work Item",
		Description: gitlab.Ptr("This is a test work item"),
		Weight:      gitlab.Ptr(int64(100)),
	// STEP 1: Create a work item
	// WHEN creating a new work item with initial options
	createOpt := gitlab.CreateWorkItemOptions{
		Title:        "Integration Test Work Item",
		Description:  gitlab.Ptr("Initial description"),
		HealthStatus: gitlab.Ptr("onTrack"),
		Color:        gitlab.Ptr("green"),
	}

	// WHEN creating a new work item with the given options
	wi, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &opt)
	createdWI, err := CreateTestWorkItem(t, client, group.FullPath, gitlab.WorkItemTypeEpic, &createOpt)
	require.NoError(t, err, "CreateWorkItem failed")
	require.NotNil(t, createdWI)

	// THEN the work item should be created successfully
	assert.NotNil(t, wi)
	// THEN the work item should have the provided fields set correctly
	assert.Equal(t, "Integration Test Work Item", createdWI.Title, "Field: Title")
	assert.Equal(t, "Initial description", createdWI.Description, "Field: Description")
	assert.Equal(t, "onTrack", deref(t, createdWI.HealthStatus), "Field: HealthStatus")
	assert.Equal(t, "#008000", deref(t, createdWI.Color), "Field: Color")

	// AND all provided fields should be set correctly
	assert.Equal(t, "Test Work Item", wi.Title)
	assert.Equal(t, "This is a test work item", wi.Description)
	// assert.Equal(t, int64(100), wi.Weight)
	// STEP 2: Get the work item
	// WHEN retrieving the work item by full path and IID
	gotWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID)
	require.NoError(t, err, "GetWorkItem failed")
	require.NotNil(t, gotWI)

	// THEN the retrieved work item should have the same provided fields
	assert.Equal(t, createdWI.Title, gotWI.Title, "Field: Title")
	assert.Equal(t, createdWI.Description, gotWI.Description, "Field: Description")
	assert.Equal(t, deref(t, createdWI.HealthStatus), deref(t, gotWI.HealthStatus), "Field: HealthStatus")
	assert.Equal(t, deref(t, createdWI.Color), deref(t, gotWI.Color), "Field: Color")

	// STEP 3: Update the work item
	// WHEN updating the work item with new values
	updateOpt := gitlab.UpdateWorkItemOptions{
		Title:        gitlab.Ptr("Updated Work Item Title"),
		Description:  gitlab.Ptr("Updated description"),
		HealthStatus: gitlab.Ptr("needsAttention"),
		Color:        gitlab.Ptr("red"),
	}

	updatedWI, _, err := client.WorkItems.UpdateWorkItem(group.FullPath, createdWI.IID, &updateOpt)
	require.NoError(t, err, "UpdateWorkItem failed")
	require.NotNil(t, updatedWI)

	// THEN the work item should have the updated fields set correctly
	assert.Equal(t, "Updated Work Item Title", updatedWI.Title, "Field: Title")
	assert.Equal(t, "Updated description", updatedWI.Description, "Field: Description")
	assert.Equal(t, "needsAttention", deref(t, updatedWI.HealthStatus), "Field: HealthStatus")
	assert.Equal(t, "#FF0000", deref(t, updatedWI.Color), "Field: Color")

	// STEP 4: Get the work item again
	// WHEN retrieving the work item after update
	finalWI, _, err := client.WorkItems.GetWorkItem(group.FullPath, createdWI.IID)
	require.NoError(t, err, "GetWorkItem after update failed")
	require.NotNil(t, finalWI)

	// THEN the retrieved work item should have the same updated fields
	assert.Equal(t, updatedWI.Title, finalWI.Title, "Field: Title")
	assert.Equal(t, updatedWI.Description, finalWI.Description, "Field: Description")
	assert.Equal(t, deref(t, updatedWI.HealthStatus), deref(t, finalWI.HealthStatus), "Field: HealthStatus")
	assert.Equal(t, deref(t, updatedWI.Color), deref(t, finalWI.Color), "Field: Color")
}

func deref(t *testing.T, ptr *string) string {
	t.Helper()

	if ptr == nil {
		t.Fatal("pointer is nil")
	}

	return *ptr
}
+4 −0
Original line number Diff line number Diff line
@@ -146,6 +146,10 @@ func (id *gidGQL) UnmarshalJSON(b []byte) error {
	return nil
}

func (id gidGQL) IsZero() bool {
	return id.Type == "" && id.Int64 == 0
}

func (id gidGQL) String() string {
	return fmt.Sprintf("gid://gitlab/%s/%d", id.Type, id.Int64)
}
+26 −0
Original line number Diff line number Diff line
{
  "data": {
    "workItemUpdate": {
      "workItem": {
          "id": "gid://gitlab/WorkItem/179785913",
          "iid": "756",
          "workItemType": {
              "name": "Task"
          },
          "state": "OPEN",
          "title": "test title update",
          "description": "## Overview\n\nUpdate Runway Helm charts to generate Argo Rollout resources ...",
          "author": {
              "id": "gid://gitlab/User/5532616",
              "username": "swainaina",
              "name": "Silvester Wainaina",
              "state": "active",
              "createdAt": "2020-03-02T06:29:14Z",
              "avatarUrl": "/uploads/-/system/user/avatar/5532616/avatar.png",
              "webUrl": "https://gitlab.com/swainaina"
          }
      },
      "errors": []
    }
  }
}
+45 −0
Original line number Diff line number Diff line
@@ -173,3 +173,48 @@ func (c *MockWorkItemsServiceInterfaceListWorkItemsCall) DoAndReturn(f func(stri
	c.Call = c.Call.DoAndReturn(f)
	return c
}

// UpdateWorkItem mocks base method.
func (m *MockWorkItemsServiceInterface) UpdateWorkItem(fullPath string, iid int64, opt *gitlab.UpdateWorkItemOptions, options ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error) {
	m.ctrl.T.Helper()
	varargs := []any{fullPath, iid, opt}
	for _, a := range options {
		varargs = append(varargs, a)
	}
	ret := m.ctrl.Call(m, "UpdateWorkItem", varargs...)
	ret0, _ := ret[0].(*gitlab.WorkItem)
	ret1, _ := ret[1].(*gitlab.Response)
	ret2, _ := ret[2].(error)
	return ret0, ret1, ret2
}

// UpdateWorkItem indicates an expected call of UpdateWorkItem.
func (mr *MockWorkItemsServiceInterfaceMockRecorder) UpdateWorkItem(fullPath, iid, opt any, options ...any) *MockWorkItemsServiceInterfaceUpdateWorkItemCall {
	mr.mock.ctrl.T.Helper()
	varargs := append([]any{fullPath, iid, opt}, options...)
	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkItem", reflect.TypeOf((*MockWorkItemsServiceInterface)(nil).UpdateWorkItem), varargs...)
	return &MockWorkItemsServiceInterfaceUpdateWorkItemCall{Call: call}
}

// MockWorkItemsServiceInterfaceUpdateWorkItemCall wrap *gomock.Call
type MockWorkItemsServiceInterfaceUpdateWorkItemCall struct {
	*gomock.Call
}

// Return rewrite *gomock.Call.Return
func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Return(arg0 *gitlab.WorkItem, arg1 *gitlab.Response, arg2 error) *MockWorkItemsServiceInterfaceUpdateWorkItemCall {
	c.Call = c.Call.Return(arg0, arg1, arg2)
	return c
}

// Do rewrite *gomock.Call.Do
func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) Do(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall {
	c.Call = c.Call.Do(f)
	return c
}

// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockWorkItemsServiceInterfaceUpdateWorkItemCall) DoAndReturn(f func(string, int64, *gitlab.UpdateWorkItemOptions, ...gitlab.RequestOptionFunc) (*gitlab.WorkItem, *gitlab.Response, error)) *MockWorkItemsServiceInterfaceUpdateWorkItemCall {
	c.Call = c.Call.DoAndReturn(f)
	return c
}
+364 −1
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ package gitlab

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
	"text/template"
@@ -15,6 +16,7 @@ type (
		CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error)
		GetWorkItem(fullPath string, iid int64, options ...RequestOptionFunc) (*WorkItem, *Response, error)
		ListWorkItems(fullPath string, opt *ListWorkItemsOptions, options ...RequestOptionFunc) ([]*WorkItem, *Response, error)
		UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error)
	}

	// WorkItemsService handles communication with the work item related methods
@@ -89,6 +91,14 @@ type LinkedWorkItem struct {
	LinkType string
}

// WorkItemStateEvent represents a state change event for a work item.
type WorkItemStateEvent string

const (
	WorkItemStateEventClose  WorkItemStateEvent = "CLOSE"
	WorkItemStateEventReopen WorkItemStateEvent = "REOPEN"
)

// workItemTemplate defines the common fields for a work item in GraphQL queries.
// It's chained from userCoreBasicTemplate so nested templates work.
var workItemTemplate = template.Must(template.Must(userCoreBasicTemplate.Clone()).New("WorkItem").Parse(`
@@ -249,7 +259,7 @@ func (s *WorkItemsService) GetWorkItem(fullPath string, iid int64, options ...Re
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitems
//
// Experimental: The Work Item API is work in progress and subject to change even between minor versions.
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type ListWorkItemsOptions struct {
	AssigneeUsernames    []string
	AssigneeWildcardID   *string
@@ -505,6 +515,8 @@ func (s *WorkItemsService) ListWorkItems(fullPath string, opt *ListWorkItemsOpti
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type CreateWorkItemOptions struct {
	// Title of the work item. Required.
	Title string
@@ -558,6 +570,9 @@ type CreateWorkItemOptions struct {
	Color *string
}

// CreateWorkItemOptionsLinkedItems represents linked items to be added to a work item.
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type CreateWorkItemOptionsLinkedItems struct {
	LinkType    *string // enum: BLOCKED_BY, BLOCKS, RELATED
	WorkItemIDs []int64
@@ -650,6 +665,37 @@ type workItemWidgetColorInputGQL struct {
	Color *string `json:"color,omitempty"`
}

// workItemWidgetCRMContactsUpdateInputGQL represents the CRM contacts widget input for updates.
type workItemWidgetCRMContactsUpdateInputGQL struct {
	ContactIDs    []string `json:"contactIds,omitempty"`
	OperationMode *string  `json:"operationMode,omitempty"`
}

// workItemWidgetHierarchyUpdateInputGQL represents the hierarchy widget input for updates.
type workItemWidgetHierarchyUpdateInputGQL struct {
	ParentID           *string  `json:"parentId,omitempty"`
	AdjacentWorkItemID *string  `json:"adjacentWorkItemId,omitempty"`
	ChildrenIDs        []string `json:"childrenIds,omitempty"`
	RelativePosition   *string  `json:"relativePosition,omitempty"`
}

// workItemWidgetLabelsUpdateInputGQL represents the labels widget input for updates.
type workItemWidgetLabelsUpdateInputGQL struct {
	AddLabelIDs    []string `json:"addLabelIds,omitempty"`
	RemoveLabelIDs []string `json:"removeLabelIds,omitempty"`
}

// workItemWidgetStartAndDueDateUpdateInputGQL represents the start and due date widget input for updates.
type workItemWidgetStartAndDueDateUpdateInputGQL struct {
	StartDate *string `json:"startDate,omitempty"`
	DueDate   *string `json:"dueDate,omitempty"`
}

// workItemWidgetStatusInputGQL represents the status widget input.
type workItemWidgetStatusInputGQL struct {
	Status *WorkItemStatusID `json:"status,omitempty"`
}

// newWorkItemCreateInput converts the user-facing CreateWorkItemOptions to the
// backend-facing GraphQL-aligned workItemCreateInputGQL struct.
func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID WorkItemTypeID) *workItemCreateInputGQL {
@@ -751,6 +797,31 @@ func (opt *CreateWorkItemOptions) wrap(namespacePath string, workItemTypeID Work
	return input
}

// updateWorkItemTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var updateWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("UpdateWorkItem").Parse(`
	mutation UpdateWorkItem($input: WorkItemUpdateInput!) {
		workItemUpdate(input: $input) {
			workItem {
				{{ template "WorkItem" }}
			}
			errors
		}
	}
`))

const (
	getWorkItemIDQuery = `
		query GetWorkItemID($fullPath: ID!, $iid: String!) {
			namespace(fullPath: $fullPath) {
				workItem(iid: $iid) {
					id
				}
			}
		}
	`
)

// createWorkItemTemplate is chained from workItemTemplate so it has access to both
// UserCoreBasic and WorkItem templates.
var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone()).New("CreateWorkItem").Parse(`
@@ -772,6 +843,8 @@ var createWorkItemTemplate = template.Must(template.Must(workItemTemplate.Clone(
//
// GitLab API docs:
// https://docs.gitlab.com/ee/api/graphql/reference/#workitemcreateinput
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkItemTypeID, opt *CreateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) {
	var queryBuilder strings.Builder
	if err := createWorkItemTemplate.Execute(&queryBuilder, nil); err != nil {
@@ -826,6 +899,280 @@ func (s *WorkItemsService) CreateWorkItem(fullPath string, workItemTypeID WorkIt
	return wiQL.unwrap(), resp, nil
}

// UpdateWorkItemOptions represents the available UpdateWorkItem() options.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type UpdateWorkItemOptions struct {
	// Title of the work item.
	Title *string

	// State event for the work item. Possible values: CLOSE, REOPEN
	StateEvent *WorkItemStateEvent

	// Description of the work item.
	Description *string

	// Global IDs of assignees. An empty (non-nil) slice can be used to remove all assignees.
	AssigneeIDs []int64

	// Global ID of the milestone to assign to the work item.
	MilestoneID *int64

	// CRM contact IDs to set. An empty (non-nil) slice can be used to remove all contacts.
	CRMContactIDs []int64

	// Global ID of the parent work item.
	ParentID *int64

	// Global IDs of labels to be added to the work item.
	AddLabelIDs []int64

	// Global IDs of labels to be removed from the work item.
	RemoveLabelIDs []int64

	// Start date for the work item.
	StartDate *ISOTime

	// Due date for the work item.
	DueDate *ISOTime

	// Weight of the work item.
	Weight *int64

	// Health status to be assigned to the work item. Possible values: onTrack, needsAttention, atRisk
	HealthStatus *string

	// Global ID of the iteration to assign to the work item.
	IterationID *int64

	// Color of the work item, represented as a hex code or named color. Example: "#fefefe"
	Color *string

	// Global ID of the work item status.
	Status *WorkItemStatusID
}

func (opt *UpdateWorkItemOptions) wrap(gid gidGQL) *workItemUpdateInputGQL {
	if opt == nil {
		return &workItemUpdateInputGQL{
			ID: gid.String(),
		}
	}

	input := &workItemUpdateInputGQL{
		ID:         gid.String(),
		Title:      opt.Title,
		StateEvent: opt.StateEvent,
	}

	if opt.Description != nil {
		input.DescriptionWidget = &workItemWidgetDescriptionInputGQL{
			Description: opt.Description,
		}
	}

	if len(opt.AssigneeIDs) > 0 {
		input.AssigneesWidget = &workItemWidgetAssigneesInputGQL{
			AssigneeIDs: newGIDStrings("User", opt.AssigneeIDs...),
		}
	}

	if opt.MilestoneID != nil {
		input.MilestoneWidget = &workItemWidgetMilestoneInputGQL{
			MilestoneID: Ptr(gidGQL{"Milestone", *opt.MilestoneID}.String()),
		}
	}

	if len(opt.CRMContactIDs) > 0 {
		input.CRMContactsWidget = &workItemWidgetCRMContactsUpdateInputGQL{
			ContactIDs:    newGIDStrings("CustomerRelations::Contact", opt.CRMContactIDs...),
			OperationMode: Ptr("REPLACE"),
		}
	}

	if opt.ParentID != nil {
		input.HierarchyWidget = &workItemWidgetHierarchyUpdateInputGQL{
			ParentID: Ptr(gidGQL{"WorkItem", *opt.ParentID}.String()),
		}
	}

	if len(opt.AddLabelIDs) > 0 || len(opt.RemoveLabelIDs) > 0 {
		widget := &workItemWidgetLabelsUpdateInputGQL{}
		if len(opt.AddLabelIDs) > 0 {
			widget.AddLabelIDs = newGIDStrings("Label", opt.AddLabelIDs...)
		}
		if len(opt.RemoveLabelIDs) > 0 {
			widget.RemoveLabelIDs = newGIDStrings("Label", opt.RemoveLabelIDs...)
		}
		input.LabelsWidget = widget
	}

	if opt.StartDate != nil || opt.DueDate != nil {
		widget := &workItemWidgetStartAndDueDateUpdateInputGQL{}
		if opt.StartDate != nil {
			widget.StartDate = Ptr(opt.StartDate.String())
		}
		if opt.DueDate != nil {
			widget.DueDate = Ptr(opt.DueDate.String())
		}
		input.StartAndDueDateWidget = widget
	}

	if opt.Weight != nil {
		input.WeightWidget = &workItemWidgetWeightInputGQL{
			Weight: opt.Weight,
		}
	}

	if opt.HealthStatus != nil {
		input.HealthStatusWidget = &workItemWidgetHealthStatusInputGQL{
			HealthStatus: opt.HealthStatus,
		}
	}

	if opt.IterationID != nil {
		input.IterationWidget = &workItemWidgetIterationInputGQL{
			IterationID: Ptr(gidGQL{"Iteration", *opt.IterationID}.String()),
		}
	}

	if opt.Color != nil {
		input.ColorWidget = &workItemWidgetColorInputGQL{
			Color: opt.Color,
		}
	}

	if opt.Status != nil {
		input.StatusWidget = &workItemWidgetStatusInputGQL{
			Status: opt.Status,
		}
	}

	return input
}

// UpdateWorkItem updates a work item by fullPath and iid.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#mutationworkitemupdate
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
func (s *WorkItemsService) UpdateWorkItem(fullPath string, iid int64, opt *UpdateWorkItemOptions, options ...RequestOptionFunc) (*WorkItem, *Response, error) {
	gid, resp, err := s.workItemGID(fullPath, iid, options...)
	if err != nil {
		return nil, resp, err
	}

	var queryBuilder strings.Builder
	if err := updateWorkItemTemplate.Execute(&queryBuilder, nil); err != nil {
		return nil, nil, err
	}

	q := GraphQLQuery{
		Query: queryBuilder.String(),
		Variables: map[string]any{
			"input": opt.wrap(gid),
		},
	}

	var result struct {
		Data struct {
			WorkItemUpdate struct {
				WorkItem *workItemGQL `json:"workItem"`
				Errors   []string     `json:"errors"`
			} `json:"workItemUpdate"`
		}
		GenericGraphQLErrors
	}

	resp, err = s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return nil, resp, err
	}

	if len(result.Errors) != 0 {
		return nil, resp, &GraphQLResponseError{
			Err:    errors.New("GraphQL query failed"),
			Errors: result.GenericGraphQLErrors,
		}
	}

	if len(result.Data.WorkItemUpdate.Errors) != 0 {
		return nil, resp, errors.New(result.Data.WorkItemUpdate.Errors[0])
	}

	wiQL := result.Data.WorkItemUpdate.WorkItem
	if wiQL == nil {
		return nil, resp, ErrNotFound
	}

	return wiQL.unwrap(), resp, nil
}

// workItemUpdateInputGQL represents the GraphQL input structure for updating a work item.
type workItemUpdateInputGQL struct {
	ID                    string                                       `json:"id"`
	Title                 *string                                      `json:"title,omitempty"`
	StateEvent            *WorkItemStateEvent                          `json:"stateEvent,omitempty"`
	DescriptionWidget     *workItemWidgetDescriptionInputGQL           `json:"descriptionWidget,omitempty"`
	AssigneesWidget       *workItemWidgetAssigneesInputGQL             `json:"assigneesWidget,omitempty"`
	MilestoneWidget       *workItemWidgetMilestoneInputGQL             `json:"milestoneWidget,omitempty"`
	CRMContactsWidget     *workItemWidgetCRMContactsUpdateInputGQL     `json:"crmContactsWidget,omitempty"`
	HierarchyWidget       *workItemWidgetHierarchyUpdateInputGQL       `json:"hierarchyWidget,omitempty"`
	LabelsWidget          *workItemWidgetLabelsUpdateInputGQL          `json:"labelsWidget,omitempty"`
	StartAndDueDateWidget *workItemWidgetStartAndDueDateUpdateInputGQL `json:"startAndDueDateWidget,omitempty"`
	WeightWidget          *workItemWidgetWeightInputGQL                `json:"weightWidget,omitempty"`
	HealthStatusWidget    *workItemWidgetHealthStatusInputGQL          `json:"healthStatusWidget,omitempty"`
	IterationWidget       *workItemWidgetIterationInputGQL             `json:"iterationWidget,omitempty"`
	ColorWidget           *workItemWidgetColorInputGQL                 `json:"colorWidget,omitempty"`
	StatusWidget          *workItemWidgetStatusInputGQL                `json:"statusWidget,omitempty"`
}

// workItemGID queries for a work item's Global ID using fullPath and iid.
// Returns the Global ID string or ErrNotFound if the work item doesn't exist.
//
// API docs: https://docs.gitlab.com/api/graphql/reference/#namespaceworkitem
func (s *WorkItemsService) workItemGID(fullPath string, iid int64, options ...RequestOptionFunc) (gidGQL, *Response, error) {
	q := GraphQLQuery{
		Query: getWorkItemIDQuery,
		Variables: map[string]any{
			"fullPath": fullPath,
			"iid":      strconv.FormatInt(iid, 10),
		},
	}

	var result struct {
		Data struct {
			Namespace struct {
				WorkItem struct {
					ID gidGQL `json:"id"`
				} `json:"workItem"`
			}
		}
		GenericGraphQLErrors
	}

	resp, err := s.client.GraphQL.Do(q, &result, options...)
	if err != nil {
		return gidGQL{}, resp, err
	}

	if len(result.Errors) != 0 {
		return gidGQL{}, resp, &GraphQLResponseError{
			Err:    fmt.Errorf("looking up global ID of %s#%d failed", fullPath, iid),
			Errors: result.GenericGraphQLErrors,
		}
	}

	id := result.Data.Namespace.WorkItem.ID
	if id.IsZero() {
		return gidGQL{}, resp, fmt.Errorf("looking up global ID of %s#%d failed: %w", fullPath, iid, ErrEmptyResponse)
	}

	return id, resp, nil
}

// workItemGQL represents the JSON structure returned by the GraphQL query.
// It is used to parse the response and convert it to the more user-friendly WorkItem type.
type workItemGQL struct {
@@ -1152,3 +1499,19 @@ const (
	WorkItemTypeEpic        WorkItemTypeID = `gid://gitlab/WorkItems::Type/8`
	WorkItemTypeTicket      WorkItemTypeID = `gid://gitlab/WorkItems::Type/9`
)

// WorkItemStatusID represents the global ID of a work item status.
//
// GitLab API docs: https://docs.gitlab.com/api/graphql/reference/#workitemsstatus
//
// Experimental: The Work Items API is a work in progress and may introduce breaking changes even between minor versions.
type WorkItemStatusID string

// WorkItemStatusID constants for the system-defined work item statuses.
const (
	WorkItemStatusToDo       WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/1`
	WorkItemStatusInProgress WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/2`
	WorkItemStatusDone       WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/3`
	WorkItemStatusWontDo     WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/4`
	WorkItemStatusDuplicate  WorkItemStatusID = `gid://gitlab/WorkItems::Statuses::SystemDefined::Status/5`
)
Loading