diff --git a/api/graphql/graph/gen_graph.go b/api/graphql/graph/gen_graph.go index 3ff86c3fd5a49017528a78812350281aea99f17a..b70e70d8e3f83614f452fcbfc1e3415702a82387 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 675c4b6b59f24dfdf570d3d0b5b428cd2a59145c..1046d11a4d66540ee0ca89cf394f3cc29a392779 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 642a4fb981f604aeedb7ed0e6858b56d985c0ddd..9cd936a68e34f29ed2af24caa39f4a858c8f7b28 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 e6b70fafa57ccd9e8c10d0d11918e2439c26fedc..d7adde1e76070fac833d592e06a2dedb87ab6244 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 94a0b5309af76c11f5b41031c033bd7d65ffa0d9..884fd98db4170774eec7b06bf760e2bf7e91c78e 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 57d1a0a3a5404f7e9151a3e4ee5b9544a0b53fe3..0a6188efb9a18664cd105c417b1afd1d461e7e71 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 86cc7dbbdf8fa885668c4b3e7f6ef25510fd7e09..c574538e886f220675bf839dc91c6ef7ac25c4b8 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(''); +function CommentInput({ inputProps, inputText, loading, onChange }: Props) { + const [input, setInput] = useState(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 3b6b61e0afcfb4f703937fc53fcd6222e6a47410..25281f963082de500ba3610ec8793bc9bc56e2f6 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) {
- + {() => (
diff --git a/webui/src/pages/bug/CommentForm.tsx b/webui/src/pages/bug/CommentForm.tsx index 6d27e398780cef110d666364f5ec3b2c7eecc510..e70348a6e883f7ffcb6be3a331322d9f4698250c 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) => ({ 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 0000000000000000000000000000000000000000..4765b75caaa79609f51fe4195c6f556a7180aa23 --- /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 0000000000000000000000000000000000000000..8fa659b3bf2b65866b32ef0a05da64b24246b94e --- /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) => ({ + 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(comment.message); + const [inputProp, setInputProp] = useState(''); + const classes = useStyles({ loading }); + const form = useRef(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) => { + e.preventDefault(); + if (message.length > 0) submit(); + }; + + function getCancelButton() { + return ( + + ); + } + + return ( + +
+ setMessage(message)} + inputText={comment.message} + /> +
+ {onCancel && getCancelButton()} + +
+ +
+ ); +} + +export default EditCommentForm; diff --git a/webui/src/pages/bug/Message.tsx b/webui/src/pages/bug/Message.tsx index faff5356bf47f541825d15a962ceac22b10a74a6..2f4cbc592ee6f3016426eec371899c3efcd43b99 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 ( +
+ + + + { + // Render CustomizedDialogs on open to prevent fetching the history + // before opening the history menu. + open && ( + + ) + } +
+ ); +} + type Props = { + bug: BugFragment; op: AddCommentFragment | CreateFragment; }; - -function Message({ op }: Props) { +function Message({ bug, op }: Props) { const classes = useStyles(); - return ( -
- + const [editMode, switchToEditMode] = useState(false); + const [comment, setComment] = useState(op); + + const editComment = (id: String) => { + switchToEditMode(true); + }; + + function readMessageView() { + return (
- + commented - +
- {op.edited &&
Edited
} + {comment.edited && ( + + )} + + {() => ( + + editComment(comment.id)} + > + + + + )} +
- +
+ ); + } + + function editMessageView() { + const cancelEdition = () => { + switchToEditMode(false); + }; + + const onPostSubmit = (comment: AddCommentFragment | CreateFragment) => { + setComment(comment); + switchToEditMode(false); + }; + + return ( +
+ +
+ ); + } + + return ( +
+ + {editMode ? editMessageView() : readMessageView()}
); } diff --git a/webui/src/pages/bug/MessageCommentFragment.graphql b/webui/src/pages/bug/MessageCommentFragment.graphql index 00f8342d749d97256fa9d6624e1dfe3ab582f711..c852b4b0bf3685e38efc5c39ee923acfc67fd9af 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 4cae819db1a0131edafed4a9e7b95b3e2acfbe96..1f4647b65205a22c934ccc37bd54ed0f302ce4ca 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 0000000000000000000000000000000000000000..e90eb45958280f519690b148859e4c53943064b4 --- /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 0000000000000000000000000000000000000000..0ed33642cec6366e438457e2a5184d687b6018e1 --- /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 { + id: string; + children: React.ReactNode; + onClose: () => void; +} + +const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { + const { children, classes, onClose, ...other } = props; + return ( + + {children} + {onClose ? ( + + + + ) : null} + + ); +}); + +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('panel0'); + + const { loading, error, data } = useMessageHistoryQuery({ + variables: { bugIdPrefix: bugId }, + }); + if (loading) { + return ( + + + Loading... + + + + + + + + ); + } + if (error) { + return ( + + + Something went wrong... + + +

Error: {error}

+
+
+ ); + } + + 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 ( + <> + + + {desc} + + {mostRecent} + + + + ); + }; + + return ( + + + {`Edited ${editCount} ${editCount > 1 ? 'times' : 'time'}.`} + + + {history?.map((edit, index) => ( + + } + aria-controls="panel1d-content" + id="panel1d-header" + > + {getSummary(index, edit.date)} + + {edit.message} + + ))} + + + ); +} + +export default MessageHistoryDialog; diff --git a/webui/src/pages/bug/Timeline.tsx b/webui/src/pages/bug/Timeline.tsx index 6e1d242e074f6abdd4608029300ccd82c2956e90..60459a532505375c8d8a50a6b8e84dbc165beb39 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; + 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 ; + return ; case 'AddCommentTimelineItem': - return ; + return ; case 'LabelChangeTimelineItem': return ; case 'SetTitleTimelineItem': diff --git a/webui/src/pages/bug/TimelineQuery.tsx b/webui/src/pages/bug/TimelineQuery.tsx index 74eed52b213d12c7166515f2b6b7408a0fdaff8b..d66c665b83b2f019fcbe8c58df781d3fd7e63ad3 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 ; + return ; }; export default TimelineQuery;