aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2021-03-24 20:28:06 +0100
committerGitHub <noreply@github.com>2021-03-24 20:28:06 +0100
commitfb6b2d873e1bbc653aa84ddce56c842202e00c33 (patch)
treef722d8ac4381dd770b2cb96bf66478feabc94678
parent01b949091290a3bfd5870c6457c941db1abf3d67 (diff)
parentbd6159a25b548f8f939f137b70dcf77a722e70dc (diff)
downloadgit-bug-fb6b2d873e1bbc653aa84ddce56c842202e00c33.tar.gz
Merge pull request #602 from GlancingMind/upstream-4-interface-to-edit-issues
WebUI: Edit comments and inspect edit history
-rw-r--r--api/graphql/graph/gen_graph.go340
-rw-r--r--api/graphql/models/gen_models.go24
-rw-r--r--api/graphql/resolvers/mutation.go29
-rw-r--r--api/graphql/schema/mutations.graphql24
-rw-r--r--api/graphql/schema/root.graphql2
-rw-r--r--go.sum2
-rw-r--r--webui/src/components/CommentInput/CommentInput.tsx5
-rw-r--r--webui/src/pages/bug/Bug.tsx3
-rw-r--r--webui/src/pages/bug/CommentForm.tsx1
-rw-r--r--webui/src/pages/bug/EditCommentForm.graphql16
-rw-r--r--webui/src/pages/bug/EditCommentForm.tsx123
-rw-r--r--webui/src/pages/bug/Message.tsx131
-rw-r--r--webui/src/pages/bug/MessageCommentFragment.graphql5
-rw-r--r--webui/src/pages/bug/MessageCreateFragment.graphql5
-rw-r--r--webui/src/pages/bug/MessageHistory.graphql15
-rw-r--r--webui/src/pages/bug/MessageHistoryDialog.tsx235
-rw-r--r--webui/src/pages/bug/Timeline.tsx8
-rw-r--r--webui/src/pages/bug/TimelineQuery.tsx9
18 files changed, 956 insertions, 21 deletions
diff --git a/api/graphql/graph/gen_graph.go b/api/graphql/graph/gen_graph.go
index 3ff86c3f..b70e70d8 100644
--- a/api/graphql/graph/gen_graph.go
+++ b/api/graphql/graph/gen_graph.go
@@ -193,6 +193,12 @@ type ComplexityRoot struct {
Target func(childComplexity int) int
}
+ EditCommentPayload struct {
+ Bug func(childComplexity int) int
+ ClientMutationID func(childComplexity int) int
+ Operation func(childComplexity int) int
+ }
+
Identity struct {
AvatarUrl func(childComplexity int) int
DisplayName func(childComplexity int) int
@@ -258,6 +264,7 @@ type ComplexityRoot struct {
AddComment func(childComplexity int, input models.AddCommentInput) 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
@@ -433,6 +440,7 @@ type LabelChangeTimelineItemResolver interface {
type MutationResolver interface {
NewBug(ctx context.Context, input models.NewBugInput) (*models.NewBugPayload, error)
AddComment(ctx context.Context, input models.AddCommentInput) (*models.AddCommentPayload, 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)
CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error)
@@ -1059,6 +1067,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.EditCommentOperation.Target(childComplexity), true
+ case "EditCommentPayload.bug":
+ if e.complexity.EditCommentPayload.Bug == nil {
+ break
+ }
+
+ return e.complexity.EditCommentPayload.Bug(childComplexity), true
+
+ case "EditCommentPayload.clientMutationId":
+ if e.complexity.EditCommentPayload.ClientMutationID == nil {
+ break
+ }
+
+ return e.complexity.EditCommentPayload.ClientMutationID(childComplexity), true
+
+ case "EditCommentPayload.operation":
+ if e.complexity.EditCommentPayload.Operation == nil {
+ break
+ }
+
+ return e.complexity.EditCommentPayload.Operation(childComplexity), true
+
case "Identity.avatarUrl":
if e.complexity.Identity.AvatarUrl == nil {
break
@@ -1333,6 +1362,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true
+ case "Mutation.editComment":
+ if e.complexity.Mutation.EditComment == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_editComment_args(context.TODO(), rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.EditComment(childComplexity, args["input"].(models.EditCommentInput)), true
+
case "Mutation.newBug":
if e.complexity.Mutation.NewBug == nil {
break
@@ -2034,6 +2075,30 @@ type AddCommentPayload {
operation: AddCommentOperation!
}
+input EditCommentInput {
+ """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 target."""
+ target: String!
+ """The new message to be set."""
+ message: String!
+ """The collection of file's hash required for the first message."""
+ files: [Hash!]
+}
+
+type EditCommentPayload {
+ """A unique identifier for the client performing the mutation."""
+ clientMutationId: String
+ """The affected bug."""
+ bug: Bug!
+ """The resulting operation."""
+ operation: EditCommentOperation!
+}
+
input ChangeLabelInput {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
@@ -2290,6 +2355,8 @@ type Mutation {
newBug(input: NewBugInput!): NewBugPayload!
"""Add a new comment to a bug"""
addComment(input: AddCommentInput!): AddCommentPayload!
+ """Change a comment of a bug"""
+ editComment(input: EditCommentInput!): EditCommentPayload!
"""Add or remove a set of label on a bug"""
changeLabels(input: ChangeLabelInput): ChangeLabelPayload!
"""Change a bug's status to open"""
@@ -2657,6 +2724,20 @@ func (ec *executionContext) field_Mutation_closeBug_args(ctx context.Context, ra
return args, nil
}
+func (ec *executionContext) field_Mutation_editComment_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+ var err error
+ args := map[string]interface{}{}
+ var arg0 models.EditCommentInput
+ if tmp, ok := rawArgs["input"]; ok {
+ arg0, err = ec.unmarshalNEditCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentInput(ctx, tmp)
+ if err != nil {
+ return nil, err
+ }
+ }
+ args["input"] = arg0
+ return args, nil
+}
+
func (ec *executionContext) field_Mutation_newBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -5591,6 +5672,105 @@ func (ec *executionContext) _EditCommentOperation_files(ctx context.Context, fie
return ec.marshalNHash2ᚕgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHashᚄ(ctx, field.Selections, res)
}
+func (ec *executionContext) _EditCommentPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "EditCommentPayload",
+ 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) _EditCommentPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "EditCommentPayload",
+ 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) _EditCommentPayload_operation(ctx context.Context, field graphql.CollectedField, obj *models.EditCommentPayload) (ret graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ fc := &graphql.FieldContext{
+ Object: "EditCommentPayload",
+ 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.Operation, 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.EditCommentOperation)
+ fc.Result = res
+ return ec.marshalNEditCommentOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Identity_id(ctx context.Context, field graphql.CollectedField, obj models.IdentityWrapper) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -6817,6 +6997,47 @@ func (ec *executionContext) _Mutation_addComment(ctx context.Context, field grap
return ec.marshalNAddCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐAddCommentPayload(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 {
+ 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_editComment_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().EditComment(rctx, args["input"].(models.EditCommentInput))
+ })
+ 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.EditCommentPayload)
+ fc.Result = res
+ return ec.marshalNEditCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx, field.Selections, res)
+}
+
func (ec *executionContext) _Mutation_changeLabels(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -9971,6 +10192,54 @@ func (ec *executionContext) unmarshalInputCloseBugInput(ctx context.Context, obj
return it, nil
}
+func (ec *executionContext) unmarshalInputEditCommentInput(ctx context.Context, obj interface{}) (models.EditCommentInput, error) {
+ var it models.EditCommentInput
+ 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 "target":
+ var err error
+ it.Target, 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) unmarshalInputNewBugInput(ctx context.Context, obj interface{}) (models.NewBugInput, error) {
var it models.NewBugInput
var asMap = obj.(map[string]interface{})
@@ -11254,6 +11523,40 @@ func (ec *executionContext) _EditCommentOperation(ctx context.Context, sel ast.S
return out
}
+var editCommentPayloadImplementors = []string{"EditCommentPayload"}
+
+func (ec *executionContext) _EditCommentPayload(ctx context.Context, sel ast.SelectionSet, obj *models.EditCommentPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, editCommentPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ var invalids uint32
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("EditCommentPayload")
+ case "clientMutationId":
+ out.Values[i] = ec._EditCommentPayload_clientMutationId(ctx, field, obj)
+ case "bug":
+ out.Values[i] = ec._EditCommentPayload_bug(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
+ case "operation":
+ out.Values[i] = ec._EditCommentPayload_operation(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 identityImplementors = []string{"Identity"}
func (ec *executionContext) _Identity(ctx context.Context, sel ast.SelectionSet, obj models.IdentityWrapper) graphql.Marshaler {
@@ -11734,6 +12037,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
+ case "editComment":
+ out.Values[i] = ec._Mutation_editComment(ctx, field)
+ if out.Values[i] == graphql.Null {
+ invalids++
+ }
case "changeLabels":
out.Values[i] = ec._Mutation_changeLabels(ctx, field)
if out.Values[i] == graphql.Null {
@@ -13130,6 +13438,38 @@ func (ec *executionContext) marshalNCreateOperation2ᚖgithubᚗcomᚋMichaelMur
return ec._CreateOperation(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNEditCommentInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentInput(ctx context.Context, v interface{}) (models.EditCommentInput, error) {
+ return ec.unmarshalInputEditCommentInput(ctx, v)
+}
+
+func (ec *executionContext) marshalNEditCommentOperation2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx context.Context, sel ast.SelectionSet, v bug.EditCommentOperation) graphql.Marshaler {
+ return ec._EditCommentOperation(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNEditCommentOperation2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐEditCommentOperation(ctx context.Context, sel ast.SelectionSet, v *bug.EditCommentOperation) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ return ec._EditCommentOperation(ctx, sel, v)
+}
+
+func (ec *executionContext) marshalNEditCommentPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx context.Context, sel ast.SelectionSet, v models.EditCommentPayload) graphql.Marshaler {
+ return ec._EditCommentPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNEditCommentPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋapiᚋgraphqlᚋmodelsᚐEditCommentPayload(ctx context.Context, sel ast.SelectionSet, v *models.EditCommentPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ return ec._EditCommentPayload(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNHash2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋrepositoryᚐHash(ctx context.Context, v interface{}) (repository.Hash, error) {
var res repository.Hash
return res, res.UnmarshalGQL(v)
diff --git a/api/graphql/models/gen_models.go b/api/graphql/models/gen_models.go
index 675c4b6b..1046d11a 100644
--- a/api/graphql/models/gen_models.go
+++ b/api/graphql/models/gen_models.go
@@ -111,6 +111,30 @@ type CommentEdge struct {
Node *bug.Comment `json:"node"`
}
+type EditCommentInput 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 target.
+ Target string `json:"target"`
+ // The new message to be set.
+ Message string `json:"message"`
+ // The collection of file's hash required for the first message.
+ Files []repository.Hash `json:"files"`
+}
+
+type EditCommentPayload struct {
+ // A unique identifier for the client performing the mutation.
+ ClientMutationID *string `json:"clientMutationId"`
+ // The affected bug.
+ Bug BugWrapper `json:"bug"`
+ // The resulting operation.
+ Operation *bug.EditCommentOperation `json:"operation"`
+}
+
type IdentityConnection struct {
Edges []*IdentityEdge `json:"edges"`
Nodes []IdentityWrapper `json:"nodes"`
diff --git a/api/graphql/resolvers/mutation.go b/api/graphql/resolvers/mutation.go
index 642a4fb9..9cd936a6 100644
--- a/api/graphql/resolvers/mutation.go
+++ b/api/graphql/resolvers/mutation.go
@@ -5,6 +5,7 @@ import (
"time"
"github.com/MichaelMure/git-bug/api/auth"
+ "github.com/MichaelMure/git-bug/entity"
"github.com/MichaelMure/git-bug/api/graphql/graph"
"github.com/MichaelMure/git-bug/api/graphql/models"
"github.com/MichaelMure/git-bug/bug"
@@ -89,6 +90,34 @@ func (r mutationResolver) AddComment(ctx context.Context, input models.AddCommen
}, 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 {
+ return nil, err
+ }
+
+ author, err := auth.UserFromCtx(ctx, repo)
+ if err != nil {
+ return nil, err
+ }
+
+ op, err := b.EditCommentRaw(author, time.Now().Unix(), entity.Id(input.Target), input.Message, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ err = b.Commit()
+ if err != nil {
+ return nil, err
+ }
+
+ return &models.EditCommentPayload{
+ ClientMutationID: input.ClientMutationID,
+ Bug: models.NewLoadedBug(b.Snapshot()),
+ Operation: op,
+ }, nil
+}
+
func (r mutationResolver) ChangeLabels(ctx context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) {
repo, b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
diff --git a/api/graphql/schema/mutations.graphql b/api/graphql/schema/mutations.graphql
index e6b70faf..d7adde1e 100644
--- a/api/graphql/schema/mutations.graphql
+++ b/api/graphql/schema/mutations.graphql
@@ -42,6 +42,30 @@ type AddCommentPayload {
operation: AddCommentOperation!
}
+input EditCommentInput {
+ """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 ID of the comment to be changed."""
+ target: String!
+ """The new message to be set."""
+ message: String!
+ """The collection of file's hash required for the first message."""
+ files: [Hash!]
+}
+
+type EditCommentPayload {
+ """A unique identifier for the client performing the mutation."""
+ clientMutationId: String
+ """The affected bug."""
+ bug: Bug!
+ """The resulting operation."""
+ operation: EditCommentOperation!
+}
+
input ChangeLabelInput {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
diff --git a/api/graphql/schema/root.graphql b/api/graphql/schema/root.graphql
index 94a0b530..884fd98d 100644
--- a/api/graphql/schema/root.graphql
+++ b/api/graphql/schema/root.graphql
@@ -8,6 +8,8 @@ type Mutation {
newBug(input: NewBugInput!): NewBugPayload!
"""Add a new comment to a bug"""
addComment(input: AddCommentInput!): AddCommentPayload!
+ """Change a comment of a bug"""
+ editComment(input: EditCommentInput!): EditCommentPayload!
"""Add or remove a set of label on a bug"""
changeLabels(input: ChangeLabelInput): ChangeLabelPayload!
"""Change a bug's status to open"""
diff --git a/go.sum b/go.sum
index 57d1a0a3..0a6188ef 100644
--- a/go.sum
+++ b/go.sum
@@ -535,6 +535,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -687,6 +688,7 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/webui/src/components/CommentInput/CommentInput.tsx b/webui/src/components/CommentInput/CommentInput.tsx
index 86cc7dbb..c574538e 100644
--- a/webui/src/components/CommentInput/CommentInput.tsx
+++ b/webui/src/components/CommentInput/CommentInput.tsx
@@ -51,6 +51,7 @@ const a11yProps = (index: number) => ({
type Props = {
inputProps?: any;
+ inputText?: string;
loading: boolean;
onChange: (comment: string) => void;
};
@@ -62,8 +63,8 @@ type Props = {
* @param loading Disable input when component not ready yet
* @param onChange Callback to return input value changes
*/
-function CommentInput({ inputProps, loading, onChange }: Props) {
- const [input, setInput] = useState<string>('');
+function CommentInput({ inputProps, inputText, loading, onChange }: Props) {
+ const [input, setInput] = useState<string>(inputText ? inputText : '');
const [tab, setTab] = useState(0);
const classes = useStyles();
diff --git a/webui/src/pages/bug/Bug.tsx b/webui/src/pages/bug/Bug.tsx
index 3b6b61e0..25281f96 100644
--- a/webui/src/pages/bug/Bug.tsx
+++ b/webui/src/pages/bug/Bug.tsx
@@ -65,6 +65,7 @@ const useStyles = makeStyles((theme) => ({
...theme.typography.body2,
},
commentForm: {
+ marginTop: theme.spacing(2),
marginLeft: 48,
},
}));
@@ -83,7 +84,7 @@ function Bug({ bug }: Props) {
</div>
<div className={classes.container}>
<div className={classes.timeline}>
- <TimelineQuery id={bug.id} />
+ <TimelineQuery bug={bug} />
<IfLoggedIn>
{() => (
<div className={classes.commentForm}>
diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx
index 6d27e398..e70348a6 100644
--- a/webui/src/pages/bug/CommentForm.tsx
+++ b/webui/src/pages/bug/CommentForm.tsx
@@ -15,7 +15,6 @@ import { TimelineDocument } from './TimelineQuery.generated';
type StyleProps = { loading: boolean };
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
container: {
- margin: theme.spacing(2, 0),
padding: theme.spacing(0, 2, 2, 2),
},
textarea: {},
diff --git a/webui/src/pages/bug/EditCommentForm.graphql b/webui/src/pages/bug/EditCommentForm.graphql
new file mode 100644
index 00000000..4765b75c
--- /dev/null
+++ b/webui/src/pages/bug/EditCommentForm.graphql
@@ -0,0 +1,16 @@
+#import "./MessageCommentFragment.graphql"
+#import "./MessageCreateFragment.graphql"
+
+mutation EditComment($input: EditCommentInput!) {
+ editComment(input: $input) {
+ bug {
+ id
+ timeline {
+ comments: nodes {
+ ...Create
+ ...AddComment
+ }
+ }
+ }
+ }
+}
diff --git a/webui/src/pages/bug/EditCommentForm.tsx b/webui/src/pages/bug/EditCommentForm.tsx
new file mode 100644
index 00000000..8fa659b3
--- /dev/null
+++ b/webui/src/pages/bug/EditCommentForm.tsx
@@ -0,0 +1,123 @@
+import React, { useState, useRef } from 'react';
+
+import Button from '@material-ui/core/Button';
+import Paper from '@material-ui/core/Paper';
+import { makeStyles, Theme } from '@material-ui/core/styles';
+
+import CommentInput from '../../components/CommentInput/CommentInput';
+
+import { BugFragment } from './Bug.generated';
+import { useEditCommentMutation } from './EditCommentForm.generated';
+import { AddCommentFragment } from './MessageCommentFragment.generated';
+import { CreateFragment } from './MessageCreateFragment.generated';
+
+type StyleProps = { loading: boolean };
+const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
+ container: {
+ padding: theme.spacing(0, 2, 2, 2),
+ },
+ textarea: {},
+ tabContent: {
+ margin: theme.spacing(2, 0),
+ },
+ preview: {
+ borderBottom: `solid 3px ${theme.palette.grey['200']}`,
+ minHeight: '5rem',
+ },
+ actions: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ },
+ greenButton: {
+ marginLeft: '8px',
+ backgroundColor: theme.palette.success.main,
+ color: theme.palette.success.contrastText,
+ '&:hover': {
+ backgroundColor: theme.palette.success.dark,
+ color: theme.palette.success.contrastText,
+ },
+ },
+}));
+
+type Props = {
+ bug: BugFragment;
+ comment: AddCommentFragment | CreateFragment;
+ onCancel?: () => void;
+ onPostSubmit?: (comments: any) => void;
+};
+
+function EditCommentForm({ bug, comment, onCancel, onPostSubmit }: Props) {
+ const [editComment, { loading }] = useEditCommentMutation();
+ const [message, setMessage] = useState<string>(comment.message);
+ const [inputProp, setInputProp] = useState<any>('');
+ const classes = useStyles({ loading });
+ const form = useRef<HTMLFormElement>(null);
+
+ const submit = () => {
+ editComment({
+ variables: {
+ input: {
+ prefix: bug.id,
+ message: message,
+ target: comment.id,
+ },
+ },
+ }).then((result) => {
+ const comments = result.data?.editComment.bug.timeline.comments as (
+ | AddCommentFragment
+ | CreateFragment
+ )[];
+ // NOTE Searching for the changed comment could be dropped if GraphQL get
+ // filter by id argument for timelineitems
+ const modifiedComment = comments.find((elem) => elem.id === comment.id);
+ if (onPostSubmit) onPostSubmit(modifiedComment);
+ });
+ resetForm();
+ };
+
+ function resetForm() {
+ setInputProp({
+ value: '',
+ });
+ }
+
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
+ e.preventDefault();
+ if (message.length > 0) submit();
+ };
+
+ function getCancelButton() {
+ return (
+ <Button onClick={onCancel} variant="contained">
+ Cancel
+ </Button>
+ );
+ }
+
+ return (
+ <Paper className={classes.container}>
+ <form onSubmit={handleSubmit} ref={form}>
+ <CommentInput
+ inputProps={inputProp}
+ loading={loading}
+ onChange={(message: string) => setMessage(message)}
+ inputText={comment.message}
+ />
+ <div className={classes.actions}>
+ {onCancel && getCancelButton()}
+ <Button
+ className={classes.greenButton}
+ variant="contained"
+ color="primary"
+ type="submit"
+ disabled={loading || message.length === 0}
+ >
+ Update Comment
+ </Button>
+ </div>
+ </form>
+ </Paper>
+ );
+}
+
+export default EditCommentForm;
diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx
index faff5356..2f4cbc59 100644
--- a/webui/src/pages/bug/Message.tsx
+++ b/webui/src/pages/bug/Message.tsx
@@ -1,14 +1,22 @@
-import React from 'react';
+import React, { useState } from 'react';
+import IconButton from '@material-ui/core/IconButton';
import Paper from '@material-ui/core/Paper';
+import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import { makeStyles } from '@material-ui/core/styles';
+import EditIcon from '@material-ui/icons/Edit';
+import HistoryIcon from '@material-ui/icons/History';
import Author, { Avatar } from 'src/components/Author';
import Content from 'src/components/Content';
import Date from 'src/components/Date';
+import IfLoggedIn from 'src/components/IfLoggedIn/IfLoggedIn';
+import { BugFragment } from './Bug.generated';
+import EditCommentForm from './EditCommentForm';
import { AddCommentFragment } from './MessageCommentFragment.generated';
import { CreateFragment } from './MessageCreateFragment.generated';
+import MessageHistoryDialog from './MessageHistoryDialog';
const useStyles = makeStyles((theme) => ({
author: {
@@ -51,30 +59,133 @@ const useStyles = makeStyles((theme) => ({
...theme.typography.body2,
padding: '0.5rem',
},
+ headerActions: {
+ color: theme.palette.info.contrastText,
+ padding: '0rem',
+ marginLeft: theme.spacing(1),
+ fontSize: '0.75rem',
+ '&:hover': {
+ backgroundColor: 'inherit',
+ },
+ },
}));
+type HistBtnProps = {
+ bugId: string;
+ commentId: string;
+};
+function HistoryMenuToggleButton({ bugId, commentId }: HistBtnProps) {
+ const classes = useStyles();
+ const [open, setOpen] = React.useState(false);
+
+ const handleClickOpen = () => {
+ setOpen(true);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+ <div>
+ <IconButton
+ aria-label="more"
+ aria-controls="long-menu"
+ aria-haspopup="true"
+ onClick={handleClickOpen}
+ className={classes.headerActions}
+ >
+ <HistoryIcon />
+ </IconButton>
+ {
+ // Render CustomizedDialogs on open to prevent fetching the history
+ // before opening the history menu.
+ open && (
+ <MessageHistoryDialog
+ bugId={bugId}
+ commentId={commentId}
+ open={open}
+ onClose={handleClose}
+ />
+ )
+ }
+ </div>
+ );
+}
+
type Props = {
+ bug: BugFragment;
op: AddCommentFragment | CreateFragment;
};
-
-function Message({ op }: Props) {
+function Message({ bug, op }: Props) {
const classes = useStyles();
- return (
- <article className={classes.container}>
- <Avatar author={op.author} className={classes.avatar} />
+ const [editMode, switchToEditMode] = useState(false);
+ const [comment, setComment] = useState(op);
+
+ const editComment = (id: String) => {
+ switchToEditMode(true);
+ };
+
+ function readMessageView() {
+ return (
<Paper elevation={1} className={classes.bubble}>
<header className={classes.header}>
<div className={classes.title}>
- <Author className={classes.author} author={op.author} />
+ <Author className={classes.author} author={comment.author} />
<span> commented </span>
- <Date date={op.createdAt} />
+ <Date date={comment.createdAt} />
</div>
- {op.edited && <div className={classes.tag}>Edited</div>}
+ {comment.edited && (
+ <HistoryMenuToggleButton bugId={bug.id} commentId={comment.id} />
+ )}
+ <IfLoggedIn>
+ {() => (
+ <Tooltip title="Edit Message" placement="top" arrow={true}>
+ <IconButton
+ disableRipple
+ className={classes.headerActions}
+ aria-label="edit message"
+ onClick={() => editComment(comment.id)}
+ >
+ <EditIcon />
+ </IconButton>
+ </Tooltip>
+ )}
+ </IfLoggedIn>
</header>
<section className={classes.body}>
- <Content markdown={op.message} />
+ <Content markdown={comment.message} />
</section>
</Paper>
+ );
+ }
+
+ function editMessageView() {
+ const cancelEdition = () => {
+ switchToEditMode(false);
+ };
+
+ const onPostSubmit = (comment: AddCommentFragment | CreateFragment) => {
+ setComment(comment);
+ switchToEditMode(false);
+ };
+
+ return (
+ <div className={classes.bubble}>
+ <EditCommentForm
+ bug={bug}
+ onCancel={cancelEdition}
+ onPostSubmit={onPostSubmit}
+ comment={comment}
+ />
+ </div>
+ );
+ }
+
+ return (
+ <article className={classes.container}>
+ <Avatar author={comment.author} className={classes.avatar} />
+ {editMode ? editMessageView() : readMessageView()}
</article>
);
}
diff --git a/webui/src/pages/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql
index 00f8342d..c852b4b0 100644
--- a/webui/src/pages/bug/MessageCommentFragment.graphql
+++ b/webui/src/pages/bug/MessageCommentFragment.graphql
@@ -1,8 +1,13 @@
#import "../../components/fragments.graphql"
fragment AddComment on AddCommentTimelineItem {
+ id
createdAt
...authored
edited
message
+ history {
+ message
+ date
+ }
}
diff --git a/webui/src/pages/bug/MessageCreateFragment.graphql b/webui/src/pages/bug/MessageCreateFragment.graphql
index 4cae819d..1f4647b6 100644
--- a/webui/src/pages/bug/MessageCreateFragment.graphql
+++ b/webui/src/pages/bug/MessageCreateFragment.graphql
@@ -1,8 +1,13 @@
#import "../../components/fragments.graphql"
fragment Create on CreateTimelineItem {
+ id
createdAt
...authored
edited
message
+ history {
+ message
+ date
+ }
}
diff --git a/webui/src/pages/bug/MessageHistory.graphql b/webui/src/pages/bug/MessageHistory.graphql
new file mode 100644
index 00000000..e90eb459
--- /dev/null
+++ b/webui/src/pages/bug/MessageHistory.graphql
@@ -0,0 +1,15 @@
+#import "./MessageCommentFragment.graphql"
+#import "./MessageCreateFragment.graphql"
+
+query MessageHistory($bugIdPrefix: String!) {
+ repository {
+ bug(prefix: $bugIdPrefix) {
+ timeline {
+ comments: nodes {
+ ...Create
+ ...AddComment
+ }
+ }
+ }
+ }
+}
diff --git a/webui/src/pages/bug/MessageHistoryDialog.tsx b/webui/src/pages/bug/MessageHistoryDialog.tsx
new file mode 100644
index 00000000..0ed33642
--- /dev/null
+++ b/webui/src/pages/bug/MessageHistoryDialog.tsx
@@ -0,0 +1,235 @@
+import moment from 'moment';
+import React from 'react';
+import Moment from 'react-moment';
+
+import MuiAccordion from '@material-ui/core/Accordion';
+import MuiAccordionDetails from '@material-ui/core/AccordionDetails';
+import MuiAccordionSummary from '@material-ui/core/AccordionSummary';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import Dialog from '@material-ui/core/Dialog';
+import MuiDialogContent from '@material-ui/core/DialogContent';
+import MuiDialogTitle from '@material-ui/core/DialogTitle';
+import Grid from '@material-ui/core/Grid';
+import IconButton from '@material-ui/core/IconButton';
+import Tooltip from '@material-ui/core/Tooltip/Tooltip';
+import Typography from '@material-ui/core/Typography';
+import {
+ createStyles,
+ Theme,
+ withStyles,
+ WithStyles,
+} from '@material-ui/core/styles';
+import CloseIcon from '@material-ui/icons/Close';
+import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
+
+import { AddCommentFragment } from './MessageCommentFragment.generated';
+import { CreateFragment } from './MessageCreateFragment.generated';
+import { useMessageHistoryQuery } from './MessageHistory.generated';
+
+const styles = (theme: Theme) =>
+ createStyles({
+ root: {
+ margin: 0,
+ padding: theme.spacing(2),
+ },
+ closeButton: {
+ position: 'absolute',
+ right: theme.spacing(1),
+ top: theme.spacing(1),
+ },
+ });
+
+export interface DialogTitleProps extends WithStyles<typeof styles> {
+ id: string;
+ children: React.ReactNode;
+ onClose: () => void;
+}
+
+const DialogTitle = withStyles(styles)((props: DialogTitleProps) => {
+ const { children, classes, onClose, ...other } = props;
+ return (
+ <MuiDialogTitle disableTypography className={classes.root} {...other}>
+ <Typography variant="h6">{children}</Typography>
+ {onClose ? (
+ <IconButton
+ aria-label="close"
+ className={classes.closeButton}
+ onClick={onClose}
+ >
+ <CloseIcon />
+ </IconButton>
+ ) : null}
+ </MuiDialogTitle>
+ );
+});
+
+const DialogContent = withStyles((theme: Theme) => ({
+ root: {
+ padding: theme.spacing(2),
+ },
+}))(MuiDialogContent);
+
+const Accordion = withStyles({
+ root: {
+ border: '1px solid rgba(0, 0, 0, .125)',
+ boxShadow: 'none',
+ '&:not(:last-child)': {
+ borderBottom: 0,
+ },
+ '&:before': {
+ display: 'none',
+ },
+ '&$expanded': {
+ margin: 'auto',
+ },
+ },
+ expanded: {},
+})(MuiAccordion);
+
+const AccordionSummary = withStyles((theme) => ({
+ root: {
+ backgroundColor: theme.palette.primary.light,
+ borderBottomWidth: '1px',
+ borderBottomStyle: 'solid',
+ borderBottomColor: theme.palette.divider,
+ marginBottom: -1,
+ minHeight: 56,
+ '&$expanded': {
+ minHeight: 56,
+ },
+ },
+ content: {
+ '&$expanded': {
+ margin: '12px 0',
+ },
+ },
+ expanded: {},
+}))(MuiAccordionSummary);
+
+const AccordionDetails = withStyles((theme) => ({
+ root: {
+ padding: theme.spacing(2),
+ },
+}))(MuiAccordionDetails);
+
+type Props = {
+ bugId: string;
+ commentId: string;
+ open: boolean;
+ onClose: () => void;
+};
+function MessageHistoryDialog({ bugId, commentId, open, onClose }: Props) {
+ const [expanded, setExpanded] = React.useState<string | false>('panel0');
+
+ const { loading, error, data } = useMessageHistoryQuery({
+ variables: { bugIdPrefix: bugId },
+ });
+ if (loading) {
+ return (
+ <Dialog
+ onClose={onClose}
+ aria-labelledby="customized-dialog-title"
+ open={open}
+ fullWidth
+ maxWidth="sm"
+ >
+ <DialogTitle id="customized-dialog-title" onClose={onClose}>
+ Loading...
+ </DialogTitle>
+ <DialogContent dividers>
+ <Grid container justify="center">
+ <CircularProgress />
+ </Grid>
+ </DialogContent>
+ </Dialog>
+ );
+ }
+ if (error) {
+ return (
+ <Dialog
+ onClose={onClose}
+ aria-labelledby="customized-dialog-title"
+ open={open}
+ fullWidth
+ maxWidth="sm"
+ >
+ <DialogTitle id="customized-dialog-title" onClose={onClose}>
+ Something went wrong...
+ </DialogTitle>
+ <DialogContent dividers>
+ <p>Error: {error}</p>
+ </DialogContent>
+ </Dialog>
+ );
+ }
+
+ const comments = data?.repository?.bug?.timeline.comments as (
+ | AddCommentFragment
+ | CreateFragment
+ )[];
+ // NOTE Searching for the changed comment could be dropped if GraphQL get
+ // filter by id argument for timelineitems
+ const comment = comments.find((elem) => elem.id === commentId);
+ // Sort by most recent edit. Must create a copy of constant history as
+ // reverse() modifies inplace.
+ const history = comment?.history.slice().reverse();
+ const editCount = history?.length === undefined ? 0 : history?.length - 1;
+
+ const handleChange = (panel: string) => (
+ event: React.ChangeEvent<{}>,
+ newExpanded: boolean
+ ) => {
+ setExpanded(newExpanded ? panel : false);
+ };
+
+ const getSummary = (index: number, date: Date) => {
+ const desc =
+ index === editCount ? 'Created ' : `#${editCount - index} • Edited `;
+ const mostRecent = index === 0 ? ' (most recent)' : '';
+ return (
+ <>
+ <Tooltip title={moment(date).format('LLLL')}>
+ <span>
+ {desc}
+ <Moment date={date} format="on ll" />
+ {mostRecent}
+ </span>
+ </Tooltip>
+ </>
+ );
+ };
+
+ return (
+ <Dialog
+ onClose={onClose}
+ aria-labelledby="customized-dialog-title"
+ open={open}
+ fullWidth
+ maxWidth="md"
+ >
+ <DialogTitle id="customized-dialog-title" onClose={onClose}>
+ {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`}
+ </DialogTitle>
+ <DialogContent dividers>
+ {history?.map((edit, index) => (
+ <Accordion
+ square
+ expanded={expanded === 'panel' + index}
+ onChange={handleChange('panel' + index)}
+ >
+ <AccordionSummary
+ expandIcon={<ExpandMoreIcon />}
+ aria-controls="panel1d-content"
+ id="panel1d-header"
+ >
+ <Typography>{getSummary(index, edit.date)}</Typography>
+ </AccordionSummary>
+ <AccordionDetails>{edit.message}</AccordionDetails>
+ </Accordion>
+ ))}
+ </DialogContent>
+ </Dialog>
+ );
+}
+
+export default MessageHistoryDialog;
diff --git a/webui/src/pages/bug/Timeline.tsx b/webui/src/pages/bug/Timeline.tsx
index 6e1d242e..60459a53 100644
--- a/webui/src/pages/bug/Timeline.tsx
+++ b/webui/src/pages/bug/Timeline.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
+import { BugFragment } from './Bug.generated';
import LabelChange from './LabelChange';
import Message from './Message';
import SetStatus from './SetStatus';
@@ -18,9 +19,10 @@ const useStyles = makeStyles((theme) => ({
type Props = {
ops: Array<TimelineItemFragment>;
+ bug: BugFragment;
};
-function Timeline({ ops }: Props) {
+function Timeline({ bug, ops }: Props) {
const classes = useStyles();
return (
@@ -28,9 +30,9 @@ function Timeline({ ops }: Props) {
{ops.map((op, index) => {
switch (op.__typename) {
case 'CreateTimelineItem':
- return <Message key={index} op={op} />;
+ return <Message key={index} op={op} bug={bug} />;
case 'AddCommentTimelineItem':
- return <Message key={index} op={op} />;
+ return <Message key={index} op={op} bug={bug} />;
case 'LabelChangeTimelineItem':
return <LabelChange key={index} op={op} />;
case 'SetTitleTimelineItem':
diff --git a/webui/src/pages/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx
index 74eed52b..d66c665b 100644
--- a/webui/src/pages/bug/TimelineQuery.tsx
+++ b/webui/src/pages/bug/TimelineQuery.tsx
@@ -2,17 +2,18 @@ import React from 'react';
import CircularProgress from '@material-ui/core/CircularProgress';
+import { BugFragment } from './Bug.generated';
import Timeline from './Timeline';
import { useTimelineQuery } from './TimelineQuery.generated';
type Props = {
- id: string;
+ bug: BugFragment;
};
-const TimelineQuery = ({ id }: Props) => {
+const TimelineQuery = ({ bug }: Props) => {
const { loading, error, data } = useTimelineQuery({
variables: {
- id,
+ id: bug.id,
first: 100,
},
});
@@ -25,7 +26,7 @@ const TimelineQuery = ({ id }: Props) => {
return null;
}
- return <Timeline ops={nodes} />;
+ return <Timeline ops={nodes} bug={bug} />;
};
export default TimelineQuery;