Merge pull request #682 from GlancingMind/webui-comment-and-reopen-bug

Michael Muré created

WebUI: Support comment-and-reopen a bug in one step

Change summary

api/graphql/graph/gen_graph.go                                               | 383 
api/graphql/models/gen_models.go                                             |  24 
api/graphql/resolvers/mutation.go                                            |  38 
api/graphql/schema/mutations.graphql                                         |  24 
api/graphql/schema/root.graphql                                              |   2 
webui/src/components/CloseBugButton/index.tsx                                |   3 
webui/src/components/CloseBugWithCommentButton/index.tsx                     |  10 
webui/src/components/ReopenBugButton/index.tsx                               |   5 
webui/src/components/ReopenBugWithCommentButton/ReopenBugWithComment.graphql |  11 
webui/src/components/ReopenBugWithCommentButton/index.tsx                    |  65 
webui/src/pages/bug/CommentForm.tsx                                          |  26 
11 files changed, 573 insertions(+), 18 deletions(-)

Detailed changes

api/graphql/graph/gen_graph.go 🔗

@@ -74,6 +74,13 @@ type ComplexityRoot struct {
 		StatusOperation  func(childComplexity int) int
 	}
 
+	AddCommentAndReopenBugPayload struct {
+		Bug              func(childComplexity int) int
+		ClientMutationID func(childComplexity int) int
+		CommentOperation func(childComplexity int) int
+		StatusOperation  func(childComplexity int) int
+	}
+
 	AddCommentOperation struct {
 		Author  func(childComplexity int) int
 		Date    func(childComplexity int) int
@@ -268,14 +275,15 @@ type ComplexityRoot struct {
 	}
 
 	Mutation struct {
-		AddComment         func(childComplexity int, input models.AddCommentInput) int
-		AddCommentAndClose func(childComplexity int, input models.AddCommentAndCloseBugInput) int
-		ChangeLabels       func(childComplexity int, input *models.ChangeLabelInput) int
-		CloseBug           func(childComplexity int, input models.CloseBugInput) int
-		EditComment        func(childComplexity int, input models.EditCommentInput) int
-		NewBug             func(childComplexity int, input models.NewBugInput) int
-		OpenBug            func(childComplexity int, input models.OpenBugInput) int
-		SetTitle           func(childComplexity int, input models.SetTitleInput) int
+		AddComment          func(childComplexity int, input models.AddCommentInput) int
+		AddCommentAndClose  func(childComplexity int, input models.AddCommentAndCloseBugInput) int
+		AddCommentAndReopen func(childComplexity int, input models.AddCommentAndReopenBugInput) int
+		ChangeLabels        func(childComplexity int, input *models.ChangeLabelInput) int
+		CloseBug            func(childComplexity int, input models.CloseBugInput) int
+		EditComment         func(childComplexity int, input models.EditCommentInput) int
+		NewBug              func(childComplexity int, input models.NewBugInput) int
+		OpenBug             func(childComplexity int, input models.OpenBugInput) int
+		SetTitle            func(childComplexity int, input models.SetTitleInput) int
 	}
 
 	NewBugPayload struct {
@@ -449,6 +457,7 @@ type MutationResolver interface {
 	NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error)
 	AddComment(ctx context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error)
 	AddCommentAndClose(ctx context.Context, input models.AddCommentAndCloseBugInput) (*models.AddCommentAndCloseBugPayload, error)
+	AddCommentAndReopen(ctx context.Context, input models.AddCommentAndReopenBugInput) (*models.AddCommentAndReopenBugPayload, error)
 	EditComment(ctx context.Context, input models.EditCommentInput) (*models.EditCommentPayload, error)
 	ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error)
 	OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error)
@@ -533,6 +542,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.AddCommentAndCloseBugPayload.StatusOperation(childComplexity), true
 
+	case "AddCommentAndReopenBugPayload.bug":
+		if e.complexity.AddCommentAndReopenBugPayload.Bug == nil {
+			break
+		}
+
+		return e.complexity.AddCommentAndReopenBugPayload.Bug(childComplexity), true
+
+	case "AddCommentAndReopenBugPayload.clientMutationId":
+		if e.complexity.AddCommentAndReopenBugPayload.ClientMutationID == nil {
+			break
+		}
+
+		return e.complexity.AddCommentAndReopenBugPayload.ClientMutationID(childComplexity), true
+
+	case "AddCommentAndReopenBugPayload.commentOperation":
+		if e.complexity.AddCommentAndReopenBugPayload.CommentOperation == nil {
+			break
+		}
+
+		return e.complexity.AddCommentAndReopenBugPayload.CommentOperation(childComplexity), true
+
+	case "AddCommentAndReopenBugPayload.statusOperation":
+		if e.complexity.AddCommentAndReopenBugPayload.StatusOperation == nil {
+			break
+		}
+
+		return e.complexity.AddCommentAndReopenBugPayload.StatusOperation(childComplexity), true
+
 	case "AddCommentOperation.author":
 		if e.complexity.AddCommentOperation.Author == nil {
 			break
@@ -1387,6 +1424,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.AddCommentAndClose(childComplexity, args["input"].(models.AddCommentAndCloseBugInput)), true
 
+	case "Mutation.addCommentAndReopen":
+		if e.complexity.Mutation.AddCommentAndReopen == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_addCommentAndReopen_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.AddCommentAndReopen(childComplexity, args["input"].(models.AddCommentAndReopenBugInput)), true
+
 	case "Mutation.changeLabels":
 		if e.complexity.Mutation.ChangeLabels == nil {
 			break
@@ -2148,6 +2197,30 @@ type AddCommentAndCloseBugPayload {
     statusOperation: SetStatusOperation!
 }
 
+input AddCommentAndReopenBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The message to be added to the bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type AddCommentAndReopenBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting AddComment operation."""
+    commentOperation: AddCommentOperation!
+    """The resulting SetStatusOperation."""
+    statusOperation: SetStatusOperation!
+}
+
 input EditCommentInput {
     """A unique identifier for the client performing the mutation."""
     clientMutationId: String
@@ -2431,6 +2504,8 @@ type Mutation {
     addComment(input: AddCommentInput!): AddCommentPayload!
     """Add a new comment to a bug and close it"""
     addCommentAndClose(input: AddCommentAndCloseBugInput!): AddCommentAndCloseBugPayload!
+    """Add a new comment to a bug and reopen it"""
+    addCommentAndReopen(input: AddCommentAndReopenBugInput!): AddCommentAndReopenBugPayload!
     """Change a comment of a bug"""
     editComment(input: EditCommentInput!): EditCommentPayload!
     """Add or remove a set of label on a bug"""
@@ -2772,6 +2847,20 @@ func (ec *executionContext) field_Mutation_addCommentAndClose_args(ctx context.C
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_addCommentAndReopen_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 models.AddCommentAndReopenBugInput
+	if tmp, ok := rawArgs["input"]; ok {
+		arg0, err = ec.unmarshalNAddCommentAndReopenBugInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndReopenBugInput(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["input"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Mutation_addComment_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -3217,6 +3306,139 @@ func (ec *executionContext) _AddCommentAndCloseBugPayload_statusOperation(ctx co
 	return ec.marshalNSetStatusOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐSetStatusOperation(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _AddCommentAndReopenBugPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentAndReopenBugPayload) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "AddCommentAndReopenBugPayload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.ClientMutationID, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _AddCommentAndReopenBugPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentAndReopenBugPayload) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "AddCommentAndReopenBugPayload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Bug, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(models.BugWrapper)
+	fc.Result = res
+	return ec.marshalNBug2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _AddCommentAndReopenBugPayload_commentOperation(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentAndReopenBugPayload) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "AddCommentAndReopenBugPayload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.CommentOperation, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*bug.AddCommentOperation)
+	fc.Result = res
+	return ec.marshalNAddCommentOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐAddCommentOperation(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _AddCommentAndReopenBugPayload_statusOperation(ctx context.Context, field graphql.CollectedField, obj *models.AddCommentAndReopenBugPayload) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "AddCommentAndReopenBugPayload",
+		Field:    field,
+		Args:     nil,
+		IsMethod: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.StatusOperation, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*bug.SetStatusOperation)
+	fc.Result = res
+	return ec.marshalNSetStatusOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐSetStatusOperation(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _AddCommentOperation_id(ctx context.Context, field graphql.CollectedField, obj *bug.AddCommentOperation) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -7261,6 +7483,47 @@ func (ec *executionContext) _Mutation_addCommentAndClose(ctx context.Context, fi
 	return ec.marshalNAddCommentAndCloseBugPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndCloseBugPayload(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Mutation_addCommentAndReopen(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "Mutation",
+		Field:    field,
+		Args:     nil,
+		IsMethod: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Mutation_addCommentAndReopen_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Mutation().AddCommentAndReopen(rctx, args["input"].(models.AddCommentAndReopenBugInput))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(*models.AddCommentAndReopenBugPayload)
+	fc.Result = res
+	return ec.marshalNAddCommentAndReopenBugPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndReopenBugPayload(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Mutation_editComment(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -10384,6 +10647,48 @@ func (ec *executionContext) unmarshalInputAddCommentAndCloseBugInput(ctx context
 	return it, nil
 }
 
+func (ec *executionContext) unmarshalInputAddCommentAndReopenBugInput(ctx context.Context, obj interface{}) (models.AddCommentAndReopenBugInput, error) {
+	var it models.AddCommentAndReopenBugInput
+	var asMap = obj.(map[string]interface{})
+
+	for k, v := range asMap {
+		switch k {
+		case "clientMutationId":
+			var err error
+			it.ClientMutationID, err = ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		case "repoRef":
+			var err error
+			it.RepoRef, err = ec.unmarshalOString2ᚖstring(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		case "prefix":
+			var err error
+			it.Prefix, err = ec.unmarshalNString2string(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		case "message":
+			var err error
+			it.Message, err = ec.unmarshalNString2string(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		case "files":
+			var err error
+			it.Files, err = ec.unmarshalOHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHashᚄ(ctx, v)
+			if err != nil {
+				return it, err
+			}
+		}
+	}
+
+	return it, nil
+}
+
 func (ec *executionContext) unmarshalInputAddCommentInput(ctx context.Context, obj interface{}) (models.AddCommentInput, error) {
 	var it models.AddCommentInput
 	var asMap = obj.(map[string]interface{})
@@ -10856,6 +11161,45 @@ func (ec *executionContext) _AddCommentAndCloseBugPayload(ctx context.Context, s
 	return out
 }
 
+var addCommentAndReopenBugPayloadImplementors = []string{"AddCommentAndReopenBugPayload"}
+
+func (ec *executionContext) _AddCommentAndReopenBugPayload(ctx context.Context, sel ast.SelectionSet, obj *models.AddCommentAndReopenBugPayload) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, addCommentAndReopenBugPayloadImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	var invalids uint32
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("AddCommentAndReopenBugPayload")
+		case "clientMutationId":
+			out.Values[i] = ec._AddCommentAndReopenBugPayload_clientMutationId(ctx, field, obj)
+		case "bug":
+			out.Values[i] = ec._AddCommentAndReopenBugPayload_bug(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		case "commentOperation":
+			out.Values[i] = ec._AddCommentAndReopenBugPayload_commentOperation(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		case "statusOperation":
+			out.Values[i] = ec._AddCommentAndReopenBugPayload_statusOperation(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch()
+	if invalids > 0 {
+		return graphql.Null
+	}
+	return out
+}
+
 var addCommentOperationImplementors = []string{"AddCommentOperation", "Operation", "Authored"}
 
 func (ec *executionContext) _AddCommentOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.AddCommentOperation) graphql.Marshaler {
@@ -12387,6 +12731,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 			if out.Values[i] == graphql.Null {
 				invalids++
 			}
+		case "addCommentAndReopen":
+			out.Values[i] = ec._Mutation_addCommentAndReopen(ctx, field)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
 		case "editComment":
 			out.Values[i] = ec._Mutation_editComment(ctx, field)
 			if out.Values[i] == graphql.Null {
@@ -13431,6 +13780,24 @@ func (ec *executionContext) marshalNAddCommentAndCloseBugPayload2ᚖgithubᚗcom
 	return ec._AddCommentAndCloseBugPayload(ctx, sel, v)
 }
 
+func (ec *executionContext) unmarshalNAddCommentAndReopenBugInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndReopenBugInput(ctx context.Context, v interface{}) (models.AddCommentAndReopenBugInput, error) {
+	return ec.unmarshalInputAddCommentAndReopenBugInput(ctx, v)
+}
+
+func (ec *executionContext) marshalNAddCommentAndReopenBugPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndReopenBugPayload(ctx context.Context, sel ast.SelectionSet, v models.AddCommentAndReopenBugPayload) graphql.Marshaler {
+	return ec._AddCommentAndReopenBugPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNAddCommentAndReopenBugPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentAndReopenBugPayload(ctx context.Context, sel ast.SelectionSet, v *models.AddCommentAndReopenBugPayload) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	return ec._AddCommentAndReopenBugPayload(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalNAddCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentInput(ctx context.Context, v interface{}) (models.AddCommentInput, error) {
 	return ec.unmarshalInputAddCommentInput(ctx, v)
 }

api/graphql/models/gen_models.go 🔗

@@ -40,6 +40,30 @@ type AddCommentAndCloseBugPayload struct {
 	StatusOperation *bug.SetStatusOperation `json:"statusOperation"`
 }
 
+type AddCommentAndReopenBugInput struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The name of the repository. If not set, the default repository is used.
+	RepoRef *string `json:"repoRef"`
+	// The bug ID's prefix.
+	Prefix string `json:"prefix"`
+	// The message to be added to the bug.
+	Message string `json:"message"`
+	// The collection of file's hash required for the first message.
+	Files []repository.Hash `json:"files"`
+}
+
+type AddCommentAndReopenBugPayload struct {
+	// A unique identifier for the client performing the mutation.
+	ClientMutationID *string `json:"clientMutationId"`
+	// The affected bug.
+	Bug BugWrapper `json:"bug"`
+	// The resulting AddComment operation.
+	CommentOperation *bug.AddCommentOperation `json:"commentOperation"`
+	// The resulting SetStatusOperation.
+	StatusOperation *bug.SetStatusOperation `json:"statusOperation"`
+}
+
 type AddCommentInput struct {
 	// A unique identifier for the client performing the mutation.
 	ClientMutationID *string `json:"clientMutationId"`

api/graphql/resolvers/mutation.go 🔗

@@ -138,6 +138,44 @@ func (r mutationResolver) AddCommentAndClose(ctx context.Context, input models.A
 	}, nil
 }
 
+func (r mutationResolver) AddCommentAndReopen(ctx context.Context, input models.AddCommentAndReopenBugInput) (*models.AddCommentAndReopenBugPayload, error) {
+	repo, b, err := r.getBug(input.RepoRef, input.Prefix)
+	if err != nil {
+		return nil, err
+	}
+
+	author, err := auth.UserFromCtx(ctx, repo)
+	if err != nil {
+		return nil, err
+	}
+
+	opAddComment, err := b.AddCommentRaw(author,
+		time.Now().Unix(),
+		text.Cleanup(input.Message),
+		input.Files,
+		nil)
+	if err != nil {
+		return nil, err
+	}
+
+	opReopen, err := b.OpenRaw(author, time.Now().Unix(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	err = b.Commit()
+	if err != nil {
+		return nil, err
+	}
+
+	return &models.AddCommentAndReopenBugPayload{
+		ClientMutationID: input.ClientMutationID,
+		Bug:              models.NewLoadedBug(b.Snapshot()),
+		CommentOperation: opAddComment,
+		StatusOperation:  opReopen,
+	}, nil
+}
+
 func (r mutationResolver) EditComment(ctx context.Context, input models.EditCommentInput) (*models.EditCommentPayload, error) {
 	repo, b, err := r.getBug(input.RepoRef, input.Prefix)
 	if err != nil {

api/graphql/schema/mutations.graphql 🔗

@@ -66,6 +66,30 @@ type AddCommentAndCloseBugPayload {
     statusOperation: SetStatusOperation!
 }
 
+input AddCommentAndReopenBugInput {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The name of the repository. If not set, the default repository is used."""
+    repoRef: String
+    """The bug ID's prefix."""
+    prefix: String!
+    """The message to be added to the bug."""
+    message: String!
+    """The collection of file's hash required for the first message."""
+    files: [Hash!]
+}
+
+type AddCommentAndReopenBugPayload {
+    """A unique identifier for the client performing the mutation."""
+    clientMutationId: String
+    """The affected bug."""
+    bug: Bug!
+    """The resulting AddComment operation."""
+    commentOperation: AddCommentOperation!
+    """The resulting SetStatusOperation."""
+    statusOperation: SetStatusOperation!
+}
+
 input EditCommentInput {
     """A unique identifier for the client performing the mutation."""
     clientMutationId: String

api/graphql/schema/root.graphql 🔗

@@ -10,6 +10,8 @@ type Mutation {
     addComment(input: AddCommentInput!): AddCommentPayload!
     """Add a new comment to a bug and close it"""
     addCommentAndClose(input: AddCommentAndCloseBugInput!): AddCommentAndCloseBugPayload!
+    """Add a new comment to a bug and reopen it"""
+    addCommentAndReopen(input: AddCommentAndReopenBugInput!): AddCommentAndReopenBugPayload!
     """Change a comment of a bug"""
     editComment(input: EditCommentInput!): EditCommentPayload!
     """Add or remove a set of label on a bug"""

webui/src/components/CloseBugButton/CloseBugButton.tsx → webui/src/components/CloseBugButton/index.tsx 🔗

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import Button from '@material-ui/core/Button';
+import CircularProgress from '@material-ui/core/CircularProgress';
 import { makeStyles, Theme } from '@material-ui/core/styles';
 import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
 
@@ -46,7 +47,7 @@ function CloseBugButton({ bug, disabled }: Props) {
     });
   }
 
-  if (loading) return <div>Loading...</div>;
+  if (loading) return <CircularProgress />;
   if (error) return <div>Error</div>;
 
   return (

webui/src/components/CloseBugWithCommentButton/CloseBugWithCommentButton.tsx → webui/src/components/CloseBugWithCommentButton/index.tsx 🔗

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import Button from '@material-ui/core/Button';
+import CircularProgress from '@material-ui/core/CircularProgress';
 import { makeStyles, Theme } from '@material-ui/core/styles';
 import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline';
 
@@ -19,9 +20,10 @@ const useStyles = makeStyles((theme: Theme) => ({
 interface Props {
   bug: BugFragment;
   comment: string;
+  postClick?: () => void;
 }
 
-function CloseBugWithCommentButton({ bug, comment }: Props) {
+function CloseBugWithCommentButton({ bug, comment, postClick }: Props) {
   const [
     addCommentAndCloseBug,
     { loading, error },
@@ -47,10 +49,14 @@ function CloseBugWithCommentButton({ bug, comment }: Props) {
         },
       ],
       awaitRefetchQueries: true,
+    }).then(() => {
+      if (postClick) {
+        postClick();
+      }
     });
   }
 
-  if (loading) return <div>Loading...</div>;
+  if (loading) return <CircularProgress />;
   if (error) return <div>Error</div>;
 
   return (

webui/src/components/ReopenBugButton/ReopenBugButton.tsx → webui/src/components/ReopenBugButton/index.tsx 🔗

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import Button from '@material-ui/core/Button';
+import CircularProgress from '@material-ui/core/CircularProgress';
 
 import { BugFragment } from 'src/pages/bug/Bug.generated';
 import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated';
@@ -9,7 +10,7 @@ import { useOpenBugMutation } from './OpenBug.generated';
 
 interface Props {
   bug: BugFragment;
-  disabled: boolean;
+  disabled?: boolean;
 }
 
 function ReopenBugButton({ bug, disabled }: Props) {
@@ -36,7 +37,7 @@ function ReopenBugButton({ bug, disabled }: Props) {
     });
   }
 
-  if (loading) return <div>Loading...</div>;
+  if (loading) return <CircularProgress />;
   if (error) return <div>Error</div>;
 
   return (

webui/src/components/ReopenBugWithCommentButton/index.tsx 🔗

@@ -0,0 +1,65 @@
+import React from 'react';
+
+import Button from '@material-ui/core/Button';
+import CircularProgress from '@material-ui/core/CircularProgress';
+
+import { BugFragment } from 'src/pages/bug/Bug.generated';
+import { TimelineDocument } from 'src/pages/bug/TimelineQuery.generated';
+
+import { useAddCommentAndReopenBugMutation } from './ReopenBugWithComment.generated';
+
+interface Props {
+  bug: BugFragment;
+  comment: string;
+  postClick?: () => void;
+}
+
+function ReopenBugWithCommentButton({ bug, comment, postClick }: Props) {
+  const [
+    addCommentAndReopenBug,
+    { loading, error },
+  ] = useAddCommentAndReopenBugMutation();
+
+  function addCommentAndReopenBugAction() {
+    addCommentAndReopenBug({
+      variables: {
+        input: {
+          prefix: bug.id,
+          message: comment,
+        },
+      },
+      refetchQueries: [
+        // TODO: update the cache instead of refetching
+        {
+          query: TimelineDocument,
+          variables: {
+            id: bug.id,
+            first: 100,
+          },
+        },
+      ],
+      awaitRefetchQueries: true,
+    }).then(() => {
+      if (postClick) {
+        postClick();
+      }
+    });
+  }
+
+  if (loading) return <CircularProgress />;
+  if (error) return <div>Error</div>;
+
+  return (
+    <div>
+      <Button
+        variant="contained"
+        type="submit"
+        onClick={() => addCommentAndReopenBugAction()}
+      >
+        Reopen bug with comment
+      </Button>
+    </div>
+  );
+}
+
+export default ReopenBugWithCommentButton;

webui/src/pages/bug/CommentForm.tsx 🔗

@@ -5,9 +5,10 @@ import Paper from '@material-ui/core/Paper';
 import { makeStyles, Theme } from '@material-ui/core/styles';
 
 import CommentInput from '../../components/CommentInput/CommentInput';
-import CloseBugButton from 'src/components/CloseBugButton/CloseBugButton';
-import CloseBugWithCommentButton from 'src/components/CloseBugWithCommentButton/CloseBugWithCommentButton';
-import ReopenBugButton from 'src/components/ReopenBugButton/ReopenBugButton';
+import CloseBugButton from 'src/components/CloseBugButton';
+import CloseBugWithCommentButton from 'src/components/CloseBugWithCommentButton';
+import ReopenBugButton from 'src/components/ReopenBugButton';
+import ReopenBugWithCommentButton from 'src/components/ReopenBugWithCommentButton';
 
 import { BugFragment } from './Bug.generated';
 import { useAddCommentMutation } from './CommentForm.generated';
@@ -80,12 +81,27 @@ function CommentForm({ bug }: Props) {
 
   function getBugStatusButton() {
     if (bug.status === 'OPEN' && issueComment.length > 0) {
-      return <CloseBugWithCommentButton bug={bug} comment={issueComment} />;
+      return (
+        <CloseBugWithCommentButton
+          bug={bug}
+          comment={issueComment}
+          postClick={resetForm}
+        />
+      );
     }
     if (bug.status === 'OPEN') {
       return <CloseBugButton bug={bug} />;
     }
-    return <ReopenBugButton bug={bug} disabled={issueComment.length > 0} />;
+    if (bug.status === 'CLOSED' && issueComment.length > 0) {
+      return (
+        <ReopenBugWithCommentButton
+          bug={bug}
+          comment={issueComment}
+          postClick={resetForm}
+        />
+      );
+    }
+    return <ReopenBugButton bug={bug} />;
   }
 
   return (