Merge pull request #323 from MichaelMure/webui/typescript

Webui/typescript
This commit is contained in:
Michael Muré 2020-02-13 00:05:04 +01:00 committed by GitHub
commit 0066f3d8c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 6277 additions and 3913 deletions

12
cache/repo_cache.go vendored
View File

@ -58,6 +58,9 @@ type RepoCache struct {
// the underlying repo // the underlying repo
repo repository.ClockedRepo repo repository.ClockedRepo
// the name of the repository, as defined in the MultiRepoCache
name string
muBug sync.RWMutex muBug sync.RWMutex
// excerpt of bugs data for all bugs // excerpt of bugs data for all bugs
bugExcerpts map[entity.Id]*BugExcerpt bugExcerpts map[entity.Id]*BugExcerpt
@ -75,8 +78,13 @@ type RepoCache struct {
} }
func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) { func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
return NewNamedRepoCache(r, "")
}
func NewNamedRepoCache(r repository.ClockedRepo, name string) (*RepoCache, error) {
c := &RepoCache{ c := &RepoCache{
repo: r, repo: r,
name: name,
bugs: make(map[entity.Id]*BugCache), bugs: make(map[entity.Id]*BugCache),
identities: make(map[entity.Id]*IdentityCache), identities: make(map[entity.Id]*IdentityCache),
} }
@ -102,6 +110,10 @@ func NewRepoCache(r repository.ClockedRepo) (*RepoCache, error) {
return c, c.write() return c, c.write()
} }
func (c *RepoCache) Name() string {
return c.name
}
// LocalConfig give access to the repository scoped configuration // LocalConfig give access to the repository scoped configuration
func (c *RepoCache) LocalConfig() repository.Config { func (c *RepoCache) LocalConfig() repository.Config {
return c.repo.LocalConfig() return c.repo.LocalConfig()

View File

@ -163,16 +163,6 @@ type ComplexityRoot struct {
Message func(childComplexity int) int Message func(childComplexity int) int
} }
CommitAsNeededPayload struct {
Bug func(childComplexity int) int
ClientMutationID func(childComplexity int) int
}
CommitPayload struct {
Bug func(childComplexity int) int
ClientMutationID func(childComplexity int) int
}
CreateOperation struct { CreateOperation struct {
Author func(childComplexity int) int Author func(childComplexity int) int
Date func(childComplexity int) int Date func(childComplexity int) int
@ -264,14 +254,12 @@ type ComplexityRoot struct {
} }
Mutation struct { Mutation struct {
AddComment func(childComplexity int, input models.AddCommentInput) int AddComment func(childComplexity int, input models.AddCommentInput) int
ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int
CloseBug func(childComplexity int, input models.CloseBugInput) int CloseBug func(childComplexity int, input models.CloseBugInput) int
Commit func(childComplexity int, input models.CommitInput) int NewBug func(childComplexity int, input models.NewBugInput) int
CommitAsNeeded func(childComplexity int, input models.CommitAsNeededInput) int OpenBug func(childComplexity int, input models.OpenBugInput) int
NewBug func(childComplexity int, input models.NewBugInput) int SetTitle func(childComplexity int, input models.SetTitleInput) int
OpenBug func(childComplexity int, input models.OpenBugInput) int
SetTitle func(childComplexity int, input models.SetTitleInput) int
} }
NewBugPayload struct { NewBugPayload struct {
@ -306,8 +294,7 @@ type ComplexityRoot struct {
} }
Query struct { Query struct {
DefaultRepository func(childComplexity int) int Repository func(childComplexity int, ref *string) int
Repository func(childComplexity int, ref string) int
} }
Repository struct { Repository struct {
@ -315,6 +302,7 @@ type ComplexityRoot struct {
AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int
Bug func(childComplexity int, prefix string) int Bug func(childComplexity int, prefix string) int
Identity func(childComplexity int, prefix string) int Identity func(childComplexity int, prefix string) int
Name func(childComplexity int) int
UserIdentity func(childComplexity int) int UserIdentity func(childComplexity int) int
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
} }
@ -448,14 +436,12 @@ type MutationResolver interface {
OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error)
CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error)
SetTitle(ctx context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) SetTitle(ctx context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error)
Commit(ctx context.Context, input models.CommitInput) (*models.CommitPayload, error)
CommitAsNeeded(ctx context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error)
} }
type QueryResolver interface { type QueryResolver interface {
DefaultRepository(ctx context.Context) (*models.Repository, error) Repository(ctx context.Context, ref *string) (*models.Repository, error)
Repository(ctx context.Context, ref string) (*models.Repository, error)
} }
type RepositoryResolver interface { type RepositoryResolver interface {
Name(ctx context.Context, obj *models.Repository) (*string, error)
AllBugs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, query *string) (*models.BugConnection, error) AllBugs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, query *string) (*models.BugConnection, error)
Bug(ctx context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error) Bug(ctx context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error)
AllIdentities(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error) AllIdentities(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error)
@ -925,34 +911,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.CommentHistoryStep.Message(childComplexity), true return e.complexity.CommentHistoryStep.Message(childComplexity), true
case "CommitAsNeededPayload.bug":
if e.complexity.CommitAsNeededPayload.Bug == nil {
break
}
return e.complexity.CommitAsNeededPayload.Bug(childComplexity), true
case "CommitAsNeededPayload.clientMutationId":
if e.complexity.CommitAsNeededPayload.ClientMutationID == nil {
break
}
return e.complexity.CommitAsNeededPayload.ClientMutationID(childComplexity), true
case "CommitPayload.bug":
if e.complexity.CommitPayload.Bug == nil {
break
}
return e.complexity.CommitPayload.Bug(childComplexity), true
case "CommitPayload.clientMutationId":
if e.complexity.CommitPayload.ClientMutationID == nil {
break
}
return e.complexity.CommitPayload.ClientMutationID(childComplexity), true
case "CreateOperation.author": case "CreateOperation.author":
if e.complexity.CreateOperation.Author == nil { if e.complexity.CreateOperation.Author == nil {
break break
@ -1367,30 +1325,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true
case "Mutation.commit":
if e.complexity.Mutation.Commit == nil {
break
}
args, err := ec.field_Mutation_commit_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.Commit(childComplexity, args["input"].(models.CommitInput)), true
case "Mutation.commitAsNeeded":
if e.complexity.Mutation.CommitAsNeeded == nil {
break
}
args, err := ec.field_Mutation_commitAsNeeded_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CommitAsNeeded(childComplexity, args["input"].(models.CommitAsNeededInput)), true
case "Mutation.newBug": case "Mutation.newBug":
if e.complexity.Mutation.NewBug == nil { if e.complexity.Mutation.NewBug == nil {
break break
@ -1539,13 +1473,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.PageInfo.StartCursor(childComplexity), true return e.complexity.PageInfo.StartCursor(childComplexity), true
case "Query.defaultRepository":
if e.complexity.Query.DefaultRepository == nil {
break
}
return e.complexity.Query.DefaultRepository(childComplexity), true
case "Query.repository": case "Query.repository":
if e.complexity.Query.Repository == nil { if e.complexity.Query.Repository == nil {
break break
@ -1556,7 +1483,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false return 0, false
} }
return e.complexity.Query.Repository(childComplexity, args["ref"].(string)), true return e.complexity.Query.Repository(childComplexity, args["ref"].(*string)), true
case "Repository.allBugs": case "Repository.allBugs":
if e.complexity.Repository.AllBugs == nil { if e.complexity.Repository.AllBugs == nil {
@ -1606,6 +1533,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Repository.Identity(childComplexity, args["prefix"].(string)), true return e.complexity.Repository.Identity(childComplexity, args["prefix"].(string)), true
case "Repository.name":
if e.complexity.Repository.Name == nil {
break
}
return e.complexity.Repository.Name(childComplexity), true
case "Repository.userIdentity": case "Repository.userIdentity":
if e.complexity.Repository.UserIdentity == nil { if e.complexity.Repository.UserIdentity == nil {
break break
@ -2184,38 +2118,6 @@ type SetTitlePayload {
"""The resulting operation""" """The resulting operation"""
operation: SetTitleOperation! operation: SetTitleOperation!
} }
input CommitInput {
"""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!
}
type CommitPayload {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
"""The affected bug."""
bug: Bug!
}
input CommitAsNeededInput {
"""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!
}
type CommitAsNeededPayload {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
"""The affected bug."""
bug: Bug!
}
`, BuiltIn: false}, `, BuiltIn: false},
&ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug.""" &ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug."""
interface Operation { interface Operation {
@ -2320,6 +2222,9 @@ type LabelChangeOperation implements Operation & Authored {
`, BuiltIn: false}, `, BuiltIn: false},
&ast.Source{Name: "schema/repository.graphql", Input: ` &ast.Source{Name: "schema/repository.graphql", Input: `
type Repository { type Repository {
"""The name of the repository"""
name: String
"""All the bugs""" """All the bugs"""
allBugs( allBugs(
"""Returns the elements in the list that come after the specified cursor.""" """Returns the elements in the list that come after the specified cursor."""
@ -2330,7 +2235,7 @@ type Repository {
first: Int first: Int
"""Returns the last _n_ elements from the list.""" """Returns the last _n_ elements from the list."""
last: Int last: Int
"""A query to select and order bugs""" """A query to select and order bugs."""
query: String query: String
): BugConnection! ): BugConnection!
@ -2366,12 +2271,8 @@ type Repository {
): LabelConnection! ): LabelConnection!
}`, BuiltIn: false}, }`, BuiltIn: false},
&ast.Source{Name: "schema/root.graphql", Input: `type Query { &ast.Source{Name: "schema/root.graphql", Input: `type Query {
"""The default unnamend repository.""" """Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
defaultRepository: Repository repository(ref: String): Repository
"""Access a repository by reference/name."""
repository(ref: String!): Repository
#TODO: connection for all repositories
} }
type Mutation { type Mutation {
@ -2387,10 +2288,6 @@ type Mutation {
closeBug(input: CloseBugInput!): CloseBugPayload! closeBug(input: CloseBugInput!): CloseBugPayload!
"""Change a bug's title""" """Change a bug's title"""
setTitle(input: SetTitleInput!): SetTitlePayload! setTitle(input: SetTitleInput!): SetTitlePayload!
"""Commit write the pending operations into storage. This mutation fail if nothing is pending"""
commit(input: CommitInput!): CommitPayload!
"""Commit write the pending operations into storage. This mutation succed if nothing is pending"""
commitAsNeeded(input: CommitAsNeededInput!): CommitAsNeededPayload!
} }
`, BuiltIn: false}, `, BuiltIn: false},
&ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events""" &ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events"""
@ -2750,34 +2647,6 @@ func (ec *executionContext) field_Mutation_closeBug_args(ctx context.Context, ra
return args, nil return args, nil
} }
func (ec *executionContext) field_Mutation_commitAsNeeded_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 models.CommitAsNeededInput
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNCommitAsNeededInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_commit_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 models.CommitInput
if tmp, ok := rawArgs["input"]; ok {
arg0, err = ec.unmarshalNCommitInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitInput(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) { func (ec *executionContext) field_Mutation_newBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@ -2837,9 +2706,9 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
func (ec *executionContext) field_Query_repository_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { func (ec *executionContext) field_Query_repository_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
var arg0 string var arg0 *string
if tmp, ok := rawArgs["ref"]; ok { if tmp, ok := rawArgs["ref"]; ok {
arg0, err = ec.unmarshalNString2string(ctx, tmp) arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -4998,136 +4867,6 @@ func (ec *executionContext) _CommentHistoryStep_date(ctx context.Context, field
return ec.marshalNTime2ᚖtimeᚐTime(ctx, field.Selections, res) return ec.marshalNTime2ᚖtimeᚐTime(ctx, field.Selections, res)
} }
func (ec *executionContext) _CommitAsNeededPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.CommitAsNeededPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommitAsNeededPayload",
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) _CommitAsNeededPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.CommitAsNeededPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommitAsNeededPayload",
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ᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
}
func (ec *executionContext) _CommitPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.CommitPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommitPayload",
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) _CommitPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.CommitPayload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CommitPayload",
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ᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
}
func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) (ret graphql.Marshaler) { func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7201,88 +6940,6 @@ func (ec *executionContext) _Mutation_setTitle(ctx context.Context, field graphq
return ec.marshalNSetTitlePayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐSetTitlePayload(ctx, field.Selections, res) return ec.marshalNSetTitlePayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐSetTitlePayload(ctx, field.Selections, res)
} }
func (ec *executionContext) _Mutation_commit(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_commit_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().Commit(rctx, args["input"].(models.CommitInput))
})
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.CommitPayload)
fc.Result = res
return ec.marshalNCommitPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_commitAsNeeded(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_commitAsNeeded_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().CommitAsNeeded(rctx, args["input"].(models.CommitAsNeededInput))
})
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.CommitAsNeededPayload)
fc.Result = res
return ec.marshalNCommitAsNeededPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx, field.Selections, res)
}
func (ec *executionContext) _NewBugPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.NewBugPayload) (ret graphql.Marshaler) { func (ec *executionContext) _NewBugPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.NewBugPayload) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7821,37 +7478,6 @@ func (ec *executionContext) _PageInfo_endCursor(ctx context.Context, field graph
return ec.marshalNString2string(ctx, field.Selections, res) return ec.marshalNString2string(ctx, field.Selections, res)
} }
func (ec *executionContext) _Query_defaultRepository(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: "Query",
Field: field,
Args: nil,
IsMethod: true,
}
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 ec.resolvers.Query().DefaultRepository(rctx)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*models.Repository)
fc.Result = res
return ec.marshalORepository2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐRepository(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_repository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { func (ec *executionContext) _Query_repository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -7876,7 +7502,7 @@ func (ec *executionContext) _Query_repository(ctx context.Context, field graphql
fc.Args = args fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().Repository(rctx, args["ref"].(string)) return ec.resolvers.Query().Repository(rctx, args["ref"].(*string))
}) })
if err != nil { if err != nil {
ec.Error(ctx, err) ec.Error(ctx, err)
@ -7959,6 +7585,37 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res)
} }
func (ec *executionContext) _Repository_name(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Repository",
Field: field,
Args: nil,
IsMethod: true,
}
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 ec.resolvers.Repository().Name(rctx, obj)
})
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) _Repository_allBugs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) { func (ec *executionContext) _Repository_allBugs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -10273,66 +9930,6 @@ func (ec *executionContext) unmarshalInputCloseBugInput(ctx context.Context, obj
return it, nil return it, nil
} }
func (ec *executionContext) unmarshalInputCommitAsNeededInput(ctx context.Context, obj interface{}) (models.CommitAsNeededInput, error) {
var it models.CommitAsNeededInput
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
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputCommitInput(ctx context.Context, obj interface{}) (models.CommitInput, error) {
var it models.CommitInput
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
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputNewBugInput(ctx context.Context, obj interface{}) (models.NewBugInput, error) { func (ec *executionContext) unmarshalInputNewBugInput(ctx context.Context, obj interface{}) (models.NewBugInput, error) {
var it models.NewBugInput var it models.NewBugInput
var asMap = obj.(map[string]interface{}) var asMap = obj.(map[string]interface{})
@ -11346,64 +10943,6 @@ func (ec *executionContext) _CommentHistoryStep(ctx context.Context, sel ast.Sel
return out return out
} }
var commitAsNeededPayloadImplementors = []string{"CommitAsNeededPayload"}
func (ec *executionContext) _CommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, obj *models.CommitAsNeededPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, commitAsNeededPayloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CommitAsNeededPayload")
case "clientMutationId":
out.Values[i] = ec._CommitAsNeededPayload_clientMutationId(ctx, field, obj)
case "bug":
out.Values[i] = ec._CommitAsNeededPayload_bug(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 commitPayloadImplementors = []string{"CommitPayload"}
func (ec *executionContext) _CommitPayload(ctx context.Context, sel ast.SelectionSet, obj *models.CommitPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, commitPayloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CommitPayload")
case "clientMutationId":
out.Values[i] = ec._CommitPayload_clientMutationId(ctx, field, obj)
case "bug":
out.Values[i] = ec._CommitPayload_bug(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 createOperationImplementors = []string{"CreateOperation", "Operation", "Authored"} var createOperationImplementors = []string{"CreateOperation", "Operation", "Authored"}
func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateOperation) graphql.Marshaler { func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateOperation) graphql.Marshaler {
@ -12172,16 +11711,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null { if out.Values[i] == graphql.Null {
invalids++ invalids++
} }
case "commit":
out.Values[i] = ec._Mutation_commit(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "commitAsNeeded":
out.Values[i] = ec._Mutation_commitAsNeeded(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default: default:
panic("unknown field " + strconv.Quote(field.Name)) panic("unknown field " + strconv.Quote(field.Name))
} }
@ -12392,17 +11921,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
switch field.Name { switch field.Name {
case "__typename": case "__typename":
out.Values[i] = graphql.MarshalString("Query") out.Values[i] = graphql.MarshalString("Query")
case "defaultRepository":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Query_defaultRepository(ctx, field)
return res
})
case "repository": case "repository":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@ -12440,6 +11958,17 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe
switch field.Name { switch field.Name {
case "__typename": case "__typename":
out.Values[i] = graphql.MarshalString("Repository") out.Values[i] = graphql.MarshalString("Repository")
case "name":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Repository_name(ctx, field, obj)
return res
})
case "allBugs": case "allBugs":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@ -13544,42 +13073,6 @@ func (ec *executionContext) marshalNCommentHistoryStep2ᚕgithubᚗcomᚋMichael
return ret return ret
} }
func (ec *executionContext) unmarshalNCommitAsNeededInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededInput(ctx context.Context, v interface{}) (models.CommitAsNeededInput, error) {
return ec.unmarshalInputCommitAsNeededInput(ctx, v)
}
func (ec *executionContext) marshalNCommitAsNeededPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, v models.CommitAsNeededPayload) graphql.Marshaler {
return ec._CommitAsNeededPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNCommitAsNeededPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, v *models.CommitAsNeededPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CommitAsNeededPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNCommitInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitInput(ctx context.Context, v interface{}) (models.CommitInput, error) {
return ec.unmarshalInputCommitInput(ctx, v)
}
func (ec *executionContext) marshalNCommitPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx context.Context, sel ast.SelectionSet, v models.CommitPayload) graphql.Marshaler {
return ec._CommitPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNCommitPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx context.Context, sel ast.SelectionSet, v *models.CommitPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CommitPayload(ctx, sel, v)
}
func (ec *executionContext) marshalNCreateOperation2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐCreateOperation(ctx context.Context, sel ast.SelectionSet, v bug.CreateOperation) graphql.Marshaler { func (ec *executionContext) marshalNCreateOperation2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐCreateOperation(ctx context.Context, sel ast.SelectionSet, v bug.CreateOperation) graphql.Marshaler {
return ec._CreateOperation(ctx, sel, &v) return ec._CreateOperation(ctx, sel, &v)
} }

View File

@ -25,7 +25,7 @@ func TestQueries(t *testing.T) {
query := ` query := `
query { query {
defaultRepository { repository {
allBugs(first: 2) { allBugs(first: 2) {
pageInfo { pageInfo {
endCursor endCursor
@ -162,7 +162,7 @@ func TestQueries(t *testing.T) {
} }
var resp struct { var resp struct {
DefaultRepository struct { Repository struct {
AllBugs struct { AllBugs struct {
PageInfo models.PageInfo PageInfo models.PageInfo
Nodes []struct { Nodes []struct {

View File

@ -111,38 +111,6 @@ type CommentEdge struct {
Node *bug.Comment `json:"node"` Node *bug.Comment `json:"node"`
} }
type CommitAsNeededInput 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"`
}
type CommitAsNeededPayload struct {
// A unique identifier for the client performing the mutation.
ClientMutationID *string `json:"clientMutationId"`
// The affected bug.
Bug BugWrapper `json:"bug"`
}
type CommitInput 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"`
}
type CommitPayload struct {
// A unique identifier for the client performing the mutation.
ClientMutationID *string `json:"clientMutationId"`
// The affected bug.
Bug BugWrapper `json:"bug"`
}
type IdentityConnection struct { type IdentityConnection struct {
Edges []*IdentityEdge `json:"edges"` Edges []*IdentityEdge `json:"edges"`
Nodes []IdentityWrapper `json:"nodes"` Nodes []IdentityWrapper `json:"nodes"`

View File

@ -23,6 +23,15 @@ func (r mutationResolver) getRepo(ref *string) (*cache.RepoCache, error) {
return r.cache.DefaultRepo() return r.cache.DefaultRepo()
} }
func (r mutationResolver) getBug(repoRef *string, bugPrefix string) (*cache.BugCache, error) {
repo, err := r.getRepo(repoRef)
if err != nil {
return nil, err
}
return repo.ResolveBugPrefix(bugPrefix)
}
func (r mutationResolver) NewBug(_ context.Context, input models.NewBugInput) (*models.NewBugPayload, error) { func (r mutationResolver) NewBug(_ context.Context, input models.NewBugInput) (*models.NewBugPayload, error) {
repo, err := r.getRepo(input.RepoRef) repo, err := r.getRepo(input.RepoRef)
if err != nil { if err != nil {
@ -42,12 +51,7 @@ func (r mutationResolver) NewBug(_ context.Context, input models.NewBugInput) (*
} }
func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) { func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) {
repo, err := r.getRepo(input.RepoRef) b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -57,6 +61,11 @@ func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentI
return nil, err return nil, err
} }
err = b.Commit()
if err != nil {
return nil, err
}
return &models.AddCommentPayload{ return &models.AddCommentPayload{
ClientMutationID: input.ClientMutationID, ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()), Bug: models.NewLoadedBug(b.Snapshot()),
@ -65,12 +74,7 @@ func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentI
} }
func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) { func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) {
repo, err := r.getRepo(input.RepoRef) b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -80,6 +84,11 @@ func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLa
return nil, err return nil, err
} }
err = b.Commit()
if err != nil {
return nil, err
}
resultsPtr := make([]*bug.LabelChangeResult, len(results)) resultsPtr := make([]*bug.LabelChangeResult, len(results))
for i, result := range results { for i, result := range results {
resultsPtr[i] = &result resultsPtr[i] = &result
@ -94,12 +103,7 @@ func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLa
} }
func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) { func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) {
repo, err := r.getRepo(input.RepoRef) b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -109,6 +113,11 @@ func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput)
return nil, err return nil, err
} }
err = b.Commit()
if err != nil {
return nil, err
}
return &models.OpenBugPayload{ return &models.OpenBugPayload{
ClientMutationID: input.ClientMutationID, ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()), Bug: models.NewLoadedBug(b.Snapshot()),
@ -117,12 +126,7 @@ func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput)
} }
func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) { func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) {
repo, err := r.getRepo(input.RepoRef) b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -132,6 +136,11 @@ func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput
return nil, err return nil, err
} }
err = b.Commit()
if err != nil {
return nil, err
}
return &models.CloseBugPayload{ return &models.CloseBugPayload{
ClientMutationID: input.ClientMutationID, ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()), Bug: models.NewLoadedBug(b.Snapshot()),
@ -140,12 +149,7 @@ func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput
} }
func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) { func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) {
repo, err := r.getRepo(input.RepoRef) b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -155,53 +159,14 @@ func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput
return nil, err return nil, err
} }
err = b.Commit()
if err != nil {
return nil, err
}
return &models.SetTitlePayload{ return &models.SetTitlePayload{
ClientMutationID: input.ClientMutationID, ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()), Bug: models.NewLoadedBug(b.Snapshot()),
Operation: op, Operation: op,
}, nil }, nil
} }
func (r mutationResolver) Commit(_ context.Context, input models.CommitInput) (*models.CommitPayload, error) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil {
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
return &models.CommitPayload{
ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()),
}, nil
}
func (r mutationResolver) CommitAsNeeded(_ context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
if err != nil {
return nil, err
}
err = b.CommitAsNeeded()
if err != nil {
return nil, err
}
return &models.CommitAsNeededPayload{
ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()),
}, nil
}

View File

@ -27,8 +27,15 @@ func (r rootQueryResolver) DefaultRepository(_ context.Context) (*models.Reposit
}, nil }, nil
} }
func (r rootQueryResolver) Repository(_ context.Context, ref string) (*models.Repository, error) { func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) {
repo, err := r.cache.ResolveRepo(ref) var repo *cache.RepoCache
var err error
if ref == nil {
repo, err = r.cache.DefaultRepo()
} else {
repo, err = r.cache.ResolveRepo(*ref)
}
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -15,6 +15,11 @@ var _ graph.RepositoryResolver = &repoResolver{}
type repoResolver struct{} type repoResolver struct{}
func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) {
name := obj.Repo.Name()
return &name, nil
}
func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, queryStr *string) (*models.BugConnection, error) { func (repoResolver) AllBugs(_ context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, queryStr *string) (*models.BugConnection, error) {
input := models.ConnectionInput{ input := models.ConnectionInput{
Before: before, Before: before,
@ -153,7 +158,7 @@ func (repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (mod
return models.NewLazyIdentity(obj.Repo, excerpt), nil return models.NewLazyIdentity(obj.Repo, excerpt), nil
} }
func (resolver repoResolver) ValidLabels(_ context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.LabelConnection, error) { func (repoResolver) ValidLabels(_ context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.LabelConnection, error) {
input := models.ConnectionInput{ input := models.ConnectionInput{
Before: before, Before: before,
After: after, After: after,

View File

@ -136,35 +136,3 @@ type SetTitlePayload {
"""The resulting operation""" """The resulting operation"""
operation: SetTitleOperation! operation: SetTitleOperation!
} }
input CommitInput {
"""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!
}
type CommitPayload {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
"""The affected bug."""
bug: Bug!
}
input CommitAsNeededInput {
"""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!
}
type CommitAsNeededPayload {
"""A unique identifier for the client performing the mutation."""
clientMutationId: String
"""The affected bug."""
bug: Bug!
}

View File

@ -1,5 +1,8 @@
type Repository { type Repository {
"""The name of the repository"""
name: String
"""All the bugs""" """All the bugs"""
allBugs( allBugs(
"""Returns the elements in the list that come after the specified cursor.""" """Returns the elements in the list that come after the specified cursor."""
@ -10,7 +13,7 @@ type Repository {
first: Int first: Int
"""Returns the last _n_ elements from the list.""" """Returns the last _n_ elements from the list."""
last: Int last: Int
"""A query to select and order bugs""" """A query to select and order bugs."""
query: String query: String
): BugConnection! ): BugConnection!

View File

@ -1,10 +1,6 @@
type Query { type Query {
"""The default unnamend repository.""" """Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
defaultRepository: Repository repository(ref: String): Repository
"""Access a repository by reference/name."""
repository(ref: String!): Repository
#TODO: connection for all repositories
} }
type Mutation { type Mutation {
@ -20,8 +16,4 @@ type Mutation {
closeBug(input: CloseBugInput!): CloseBugPayload! closeBug(input: CloseBugInput!): CloseBugPayload!
"""Change a bug's title""" """Change a bug's title"""
setTitle(input: SetTitleInput!): SetTitlePayload! setTitle(input: SetTitleInput!): SetTitlePayload!
"""Commit write the pending operations into storage. This mutation fail if nothing is pending"""
commit(input: CommitInput!): CommitPayload!
"""Commit write the pending operations into storage. This mutation succed if nothing is pending"""
commitAsNeeded(input: CommitAsNeededInput!): CommitAsNeededPayload!
} }

View File

@ -1,4 +0,0 @@
{
"extends": ["react-app", "plugin:prettier/recommended"],
"ignorePatterns": ["src/fragmentTypes.js"]
}

37
webui/.eslintrc.js Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
extends: [
'react-app',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
plugins: ['graphql'],
rules: {
'graphql/template-strings': [
'error',
{
schemaJson: require('./src/schema.json'),
env: 'literal',
},
],
'import/order': [
'error',
{
alphabetize: { order: 'asc' },
pathGroups: [
{
pattern: '@material-ui/**',
group: 'external',
position: 'after',
},
{
pattern: '*.generated',
group: 'sibling',
position: 'after',
},
],
groups: [['builtin', 'external'], 'parent', ['sibling', 'index']],
'newlines-between': 'always',
},
],
},
};

View File

@ -5,7 +5,9 @@ install:
npm install npm install
test: test:
npm run generate
npm run lint npm run lint
CI=true npm run test
build: build:
npm run build npm run build

View File

@ -7,10 +7,11 @@
2. Run the GraphQL backend on the port 3001 2. Run the GraphQL backend on the port 3001
- `./git-bug webui -p 3001` - `./git-bug webui -p 3001`
3. Run the hot-reloadable development WebUI 3. Run the hot-reloadable development WebUI
- run `npm start` in the **webui** directory - run `npm start` in the **webui** directory
The development version of the WebUI is configured to query the backend on the port 3001. You can now live edit the js code and use the normal backend. The development version of the WebUI is configured to query the backend on the port 3001. You can now live edit the js code and use the normal backend.
## Bundle the web UI ## Bundle the web UI
Once the webUI is good enough for a new release, run `make pack-webui` from the root directory to bundle the compiled js into the go binary. Once the webUI is good enough for a new release, run `make pack-webui` from the root directory to bundle the compiled js into the go binary.

View File

@ -1,8 +1,32 @@
schema: '../graphql/schema/*.graphql' schema: '../graphql/schema/*.graphql'
overwrite: true overwrite: true
documents: src/**/*.graphql
generates: generates:
./src/fragmentTypes.js: ./src/fragmentTypes.ts:
plugins: plugins:
- fragment-matcher - fragment-matcher
config: config:
module: es2015 module: es2015
./src/gqlTypes.ts:
plugins:
- typescript
./src/schema.json:
plugins:
- introspection
./src/:
plugins:
- add: '/* eslint-disable @typescript-eslint/no-unused-vars, import/order */'
- typescript-operations
- typescript-react-apollo
preset: near-operation-file
presetConfig:
extension: .generated.tsx
baseTypesPath: gqlTypes.ts
config:
withComponent: false
withHOC: false
withHooks: true
hooks:
afterAllFileWrite:
- prettier --write

8005
webui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,28 +4,41 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.3",
"@arrows/composition": "^1.2.2",
"@material-ui/core": "^4.9.0", "@material-ui/core": "^4.9.0",
"@material-ui/icons": "^4.2.1", "@material-ui/icons": "^4.2.1",
"@material-ui/lab": "^4.0.0-alpha.40", "@material-ui/lab": "^4.0.0-alpha.40",
"@material-ui/styles": "^4.9.0", "@material-ui/styles": "^4.9.0",
"@types/node": "^13.5.3",
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@types/react-router-dom": "^5.1.3",
"apollo-boost": "^0.4.7", "apollo-boost": "^0.4.7",
"graphql": "^14.3.0", "clsx": "^1.1.0",
"graphql": "^14.6.0",
"graphql.macro": "^1.4.2",
"moment": "^2.24.0", "moment": "^2.24.0",
"react": "^16.8.6", "react": "^16.8.6",
"react-apollo": "^3.1.3", "react-apollo": "^3.1.3",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-router": "^5.0.0", "react-router": "^5.0.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-scripts": "^3.1.1", "react-scripts": "^3.3.1",
"remark-html": "^10.0.0", "remark-html": "^10.0.0",
"remark-parse": "^7.0.2", "remark-parse": "^7.0.2",
"remark-react": "^6.0.0", "remark-react": "^6.0.0",
"typescript": "^3.7.5",
"unified": "^8.4.2" "unified": "^8.4.2"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^1.11.2", "@graphql-codegen/cli": "^1.12.1",
"@graphql-codegen/fragment-matcher": "^1.11.2", "@graphql-codegen/fragment-matcher": "^1.12.1",
"eslint-config-prettier": "^6.9.0", "@graphql-codegen/near-operation-file-preset": "^1.12.2-alpha-ea7264f9.15",
"@graphql-codegen/typescript-operations": "^1.12.1",
"@graphql-codegen/typescript-react-apollo": "^1.12.1",
"@graphql-codegen/introspection": "^1.12.2",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-graphql": "^3.1.1",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"prettier": "^1.19.1" "prettier": "^1.19.1"
}, },
@ -35,7 +48,7 @@
"test": "react-scripts test --env=jsdom", "test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"generate": "graphql-codegen", "generate": "graphql-codegen",
"lint": "eslint src/" "lint": "eslint src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql"
}, },
"proxy": "http://localhost:3001", "proxy": "http://localhost:3001",
"browserslist": [ "browserslist": [

View File

@ -1 +1,4 @@
fragmentTypes.js fragmentTypes.ts
gqlTypes.ts
*.generated.*
schema.json

View File

@ -1,15 +1,18 @@
import AppBar from '@material-ui/core/AppBar'; import AppBar from '@material-ui/core/AppBar';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/styles';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import {
createMuiTheme,
ThemeProvider,
makeStyles,
} from '@material-ui/core/styles';
import React from 'react'; import React from 'react';
import { Route, Switch } from 'react-router'; import { Route, Switch } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import CurrentIdentity from './CurrentIdentity';
import BugQuery from './bug/BugQuery'; import BugQuery from './bug/BugQuery';
import ListQuery from './list/ListQuery'; import ListQuery from './list/ListQuery';
import CurrentIdentity from './CurrentIdentity';
const theme = createMuiTheme({ const theme = createMuiTheme({
palette: { palette: {
@ -20,7 +23,9 @@ const theme = createMuiTheme({
}); });
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
offset: theme.mixins.toolbar, offset: {
...theme.mixins.toolbar,
},
filler: { filler: {
flexGrow: 1, flexGrow: 1,
}, },
@ -46,7 +51,7 @@ export default function App() {
<AppBar position="fixed" color="primary"> <AppBar position="fixed" color="primary">
<Toolbar> <Toolbar>
<Link to="/" className={classes.appTitle}> <Link to="/" className={classes.appTitle}>
<img src="logo.svg" className={classes.logo} alt="git-bug" /> <img src="/logo.svg" className={classes.logo} alt="git-bug" />
git-bug git-bug
</Link> </Link>
<div className={classes.filler}></div> <div className={classes.filler}></div>

8
webui/src/Author.graphql Normal file
View File

@ -0,0 +1,8 @@
fragment authored on Authored {
author {
name
email
displayName
avatarUrl
}
}

View File

@ -1,9 +1,15 @@
import gql from 'graphql-tag';
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import MAvatar from '@material-ui/core/Avatar'; import MAvatar from '@material-ui/core/Avatar';
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import React from 'react'; import React from 'react';
const Author = ({ author, ...props }) => { import { AuthoredFragment } from './Author.generated';
type Props = AuthoredFragment & {
className?: string;
bold?: boolean;
};
const Author = ({ author, ...props }: Props) => {
if (!author.email) { if (!author.email) {
return <span {...props}>{author.displayName}</span>; return <span {...props}>{author.displayName}</span>;
} }
@ -15,18 +21,7 @@ const Author = ({ author, ...props }) => {
); );
}; };
Author.fragment = gql` export const Avatar = ({ author, ...props }: Props) => {
fragment authored on Authored {
author {
name
email
displayName
avatarUrl
}
}
`;
export const Avatar = ({ author, ...props }) => {
if (author.avatarUrl) { if (author.avatarUrl) {
return <MAvatar src={author.avatarUrl} {...props} />; return <MAvatar src={author.avatarUrl} {...props} />;
} }

View File

@ -1,11 +1,14 @@
import unified from 'unified'; import React from 'react';
import parse from 'remark-parse';
import html from 'remark-html'; import html from 'remark-html';
import parse from 'remark-parse';
import remark2react from 'remark-react'; import remark2react from 'remark-react';
import unified from 'unified';
import ImageTag from './tag/ImageTag'; import ImageTag from './tag/ImageTag';
import PreTag from './tag/PreTag'; import PreTag from './tag/PreTag';
const Content = ({ markdown }) => { type Props = { markdown: string };
const Content: React.FC<Props> = ({ markdown }: Props) => {
const processor = unified() const processor = unified()
.use(parse) .use(parse)
.use(html) .use(html)
@ -16,7 +19,8 @@ const Content = ({ markdown }) => {
}, },
}); });
return processor.processSync(markdown).contents; const contents: React.ReactNode = processor.processSync(markdown).contents;
return <>{contents}</>;
}; };
export default Content; export default Content;

View File

@ -0,0 +1,8 @@
query CurrentIdentity {
repository {
userIdentity {
displayName
avatarUrl
}
}
}

View File

@ -1,45 +0,0 @@
import React from 'react';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';
import Avatar from '@material-ui/core/Avatar';
import { makeStyles } from '@material-ui/styles';
const useStyles = makeStyles(theme => ({
displayName: {
marginLeft: theme.spacing(2),
},
}));
const QUERY = gql`
{
defaultRepository {
userIdentity {
displayName
avatarUrl
}
}
}
`;
const CurrentIdentity = () => {
const classes = useStyles();
return (
<Query query={QUERY}>
{({ loading, error, data }) => {
if (error || loading || !data.defaultRepository.userIdentity)
return null;
const user = data.defaultRepository.userIdentity;
return (
<>
<Avatar src={user.avatarUrl}>
{user.displayName.charAt(0).toUpperCase()}
</Avatar>
<div className={classes.displayName}>{user.displayName}</div>
</>
);
}}
</Query>
);
};
export default CurrentIdentity;

View File

@ -0,0 +1,30 @@
import Avatar from '@material-ui/core/Avatar';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
const useStyles = makeStyles(theme => ({
displayName: {
marginLeft: theme.spacing(2),
},
}));
const CurrentIdentity = () => {
const classes = useStyles();
const { loading, error, data } = useCurrentIdentityQuery();
if (error || loading || !data?.repository?.userIdentity) return null;
const user = data.repository.userIdentity;
return (
<>
<Avatar src={user.avatarUrl ? user.avatarUrl : undefined}>
{user.displayName.charAt(0).toUpperCase()}
</Avatar>
<div className={classes.displayName}>{user.displayName}</div>
</>
);
};
export default CurrentIdentity;

View File

@ -1,8 +1,9 @@
import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import * as moment from 'moment'; import moment from 'moment';
import React from 'react'; import React from 'react';
const Date = ({ date }) => ( type Props = { date: string };
const Date = ({ date }: Props) => (
<Tooltip title={moment(date).format('MMMM D, YYYY, h:mm a')}> <Tooltip title={moment(date).format('MMMM D, YYYY, h:mm a')}>
<span> {moment(date).fromNow()} </span> <span> {moment(date).fromNow()} </span>
</Tooltip> </Tooltip>

8
webui/src/Label.graphql Normal file
View File

@ -0,0 +1,8 @@
fragment Label on Label {
name
color {
R
G
B
}
}

View File

@ -1,25 +1,28 @@
import React from 'react'; import { common } from '@material-ui/core/colors';
import gql from 'graphql-tag'; import { makeStyles } from '@material-ui/core/styles';
import { makeStyles } from '@material-ui/styles';
import { import {
getContrastRatio, getContrastRatio,
darken, darken,
} from '@material-ui/core/styles/colorManipulator'; } from '@material-ui/core/styles/colorManipulator';
import { common } from '@material-ui/core/colors'; import React from 'react';
import { LabelFragment } from './Label.generated';
import { Color } from './gqlTypes';
// Minimum contrast between the background and the text color // Minimum contrast between the background and the text color
const contrastThreshold = 2.5; const contrastThreshold = 2.5;
// Guess the text color based on the background color // Guess the text color based on the background color
const getTextColor = background => const getTextColor = (background: string) =>
getContrastRatio(background, common.white) >= contrastThreshold getContrastRatio(background, common.white) >= contrastThreshold
? common.white // White on dark backgrounds ? common.white // White on dark backgrounds
: common.black; // And black on light ones : common.black; // And black on light ones
const _rgb = color => 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')'; const _rgb = (color: Color) =>
'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
// Create a style object from the label RGB colors // Create a style object from the label RGB colors
const createStyle = color => ({ const createStyle = (color: Color) => ({
backgroundColor: _rgb(color), backgroundColor: _rgb(color),
color: getTextColor(_rgb(color)), color: getTextColor(_rgb(color)),
borderBottomColor: darken(_rgb(color), 0.2), borderBottomColor: darken(_rgb(color), 0.2),
@ -30,7 +33,7 @@ const useStyles = makeStyles(theme => ({
...theme.typography.body1, ...theme.typography.body1,
padding: '1px 6px 0.5px', padding: '1px 6px 0.5px',
fontSize: '0.9em', fontSize: '0.9em',
fontWeight: '500', fontWeight: 500,
margin: '0.05em 1px calc(-1.5px + 0.05em)', margin: '0.05em 1px calc(-1.5px + 0.05em)',
borderRadius: '3px', borderRadius: '3px',
display: 'inline-block', display: 'inline-block',
@ -39,7 +42,8 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function Label({ label }) { type Props = { label: LabelFragment };
function Label({ label }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<span className={classes.label} style={createStyle(label.color)}> <span className={classes.label} style={createStyle(label.color)}>
@ -48,15 +52,4 @@ function Label({ label }) {
); );
} }
Label.fragment = gql`
fragment Label on Label {
name
color {
R
G
B
}
}
`;
export default Label; export default Label;

14
webui/src/bug/Bug.graphql Normal file
View File

@ -0,0 +1,14 @@
#import "../Label.graphql"
#import "../Author.graphql"
fragment Bug on Bug {
id
humanId
status
title
labels {
...Label
}
createdAt
...authored
}

View File

@ -1,12 +1,14 @@
import { makeStyles } from '@material-ui/styles';
import Typography from '@material-ui/core/Typography/Typography'; import Typography from '@material-ui/core/Typography/Typography';
import gql from 'graphql-tag'; import { makeStyles } from '@material-ui/core/styles';
import React from 'react'; import React from 'react';
import Author from '../Author'; import Author from '../Author';
import Date from '../Date'; import Date from '../Date';
import TimelineQuery from './TimelineQuery';
import Label from '../Label'; import Label from '../Label';
import { BugFragment } from './Bug.generated';
import TimelineQuery from './TimelineQuery';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
main: { main: {
maxWidth: 800, maxWidth: 800,
@ -51,7 +53,11 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function Bug({ bug }) { type Props = {
bug: BugFragment;
};
function Bug({ bug }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<main className={classes.main}> <main className={classes.main}>
@ -85,20 +91,4 @@ function Bug({ bug }) {
); );
} }
Bug.fragment = gql`
fragment Bug on Bug {
id
humanId
status
title
labels {
...Label
}
createdAt
...authored
}
${Label.fragment}
${Author.fragment}
`;
export default Bug; export default Bug;

View File

@ -0,0 +1,9 @@
#import "./Bug.graphql"
query GetBug($id: String!) {
repository {
bug(prefix: $id) {
...Bug
}
}
}

View File

@ -1,30 +0,0 @@
import CircularProgress from '@material-ui/core/CircularProgress';
import gql from 'graphql-tag';
import React from 'react';
import { Query } from 'react-apollo';
import Bug from './Bug';
const QUERY = gql`
query GetBug($id: String!) {
defaultRepository {
bug(prefix: $id) {
...Bug
}
}
}
${Bug.fragment}
`;
const BugQuery = ({ match }) => (
<Query query={QUERY} variables={{ id: match.params.id }}>
{({ loading, error, data }) => {
if (loading) return <CircularProgress />;
if (error) return <p>Error: {error}</p>;
return <Bug bug={data.defaultRepository.bug} />;
}}
</Query>
);
export default BugQuery;

View File

@ -0,0 +1,22 @@
import CircularProgress from '@material-ui/core/CircularProgress';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import Bug from './Bug';
import { useGetBugQuery } from './BugQuery.generated';
type Props = RouteComponentProps<{
id: string;
}>;
const BugQuery: React.FC<Props> = ({ match }: Props) => {
const { loading, error, data } = useGetBugQuery({
variables: { id: match.params.id },
});
if (loading) return <CircularProgress />;
if (error) return <p>Error: {error}</p>;
if (!data?.repository?.bug) return <p>404.</p>;
return <Bug bug={data.repository.bug} />;
};
export default BugQuery;

View File

@ -1,10 +1,12 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
import gql from 'graphql-tag';
import React from 'react'; import React from 'react';
import Author from '../Author'; import Author from '../Author';
import Date from '../Date'; import Date from '../Date';
import Label from '../Label'; import Label from '../Label';
import { LabelChangeFragment } from './LabelChangeFragment.generated';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
main: { main: {
...theme.typography.body1, ...theme.typography.body1,
@ -15,7 +17,11 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function LabelChange({ op }) { type Props = {
op: LabelChangeFragment;
};
function LabelChange({ op }: Props) {
const { added, removed } = op; const { added, removed } = op;
const classes = useStyles(); const classes = useStyles();
return ( return (
@ -40,22 +46,4 @@ function LabelChange({ op }) {
); );
} }
LabelChange.fragment = gql`
fragment LabelChange on TimelineItem {
... on LabelChangeTimelineItem {
date
...authored
added {
...Label
}
removed {
...Label
}
}
}
${Label.fragment}
${Author.fragment}
`;
export default LabelChange; export default LabelChange;

View File

@ -0,0 +1,13 @@
#import "../Author.graphql"
#import "../Label.graphql"
fragment LabelChange on LabelChangeTimelineItem {
date
...authored
added {
...Label
}
removed {
...Label
}
}

View File

@ -1,11 +1,14 @@
import { makeStyles } from '@material-ui/styles';
import Paper from '@material-ui/core/Paper'; import Paper from '@material-ui/core/Paper';
import gql from 'graphql-tag'; import { makeStyles } from '@material-ui/core/styles';
import React from 'react'; import React from 'react';
import Author from '../Author'; import Author from '../Author';
import { Avatar } from '../Author'; import { Avatar } from '../Author';
import Date from '../Date';
import Content from '../Content'; import Content from '../Content';
import Date from '../Date';
import { AddCommentFragment } from './MessageCommentFragment.generated';
import { CreateFragment } from './MessageCreateFragment.generated';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
author: { author: {
@ -47,7 +50,11 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function Message({ op }) { type Props = {
op: AddCommentFragment | CreateFragment;
};
function Message({ op }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<article className={classes.container}> <article className={classes.container}>
@ -69,30 +76,4 @@ function Message({ op }) {
); );
} }
Message.createFragment = gql`
fragment Create on TimelineItem {
... on CreateTimelineItem {
createdAt
...authored
edited
message
}
}
${Author.fragment}
`;
Message.commentFragment = gql`
fragment AddComment on TimelineItem {
... on AddCommentTimelineItem {
createdAt
...authored
edited
message
}
}
${Author.fragment}
`;
export default Message; export default Message;

View File

@ -0,0 +1,8 @@
#import "../Author.graphql"
fragment AddComment on AddCommentTimelineItem {
createdAt
...authored
edited
message
}

View File

@ -0,0 +1,8 @@
#import "../Author.graphql"
fragment Create on CreateTimelineItem {
createdAt
...authored
edited
message
}

View File

@ -1,9 +1,11 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
import gql from 'graphql-tag';
import React from 'react'; import React from 'react';
import Author from '../Author'; import Author from '../Author';
import Date from '../Date'; import Date from '../Date';
import { SetStatusFragment } from './SetStatusFragment.generated';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
main: { main: {
...theme.typography.body1, ...theme.typography.body1,
@ -11,7 +13,11 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function SetStatus({ op }) { type Props = {
op: SetStatusFragment;
};
function SetStatus({ op }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<div className={classes.main}> <div className={classes.main}>
@ -22,16 +28,4 @@ function SetStatus({ op }) {
); );
} }
SetStatus.fragment = gql`
fragment SetStatus on TimelineItem {
... on SetStatusTimelineItem {
date
...authored
status
}
}
${Author.fragment}
`;
export default SetStatus; export default SetStatus;

View File

@ -0,0 +1,7 @@
#import "../Author.graphql"
fragment SetStatus on SetStatusTimelineItem {
date
...authored
status
}

View File

@ -1,9 +1,11 @@
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/core/styles';
import gql from 'graphql-tag';
import React from 'react'; import React from 'react';
import Author from '../Author'; import Author from '../Author';
import Date from '../Date'; import Date from '../Date';
import { SetTitleFragment } from './SetTitleFragment.generated';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
main: { main: {
...theme.typography.body1, ...theme.typography.body1,
@ -14,7 +16,11 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function SetTitle({ op }) { type Props = {
op: SetTitleFragment;
};
function SetTitle({ op }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<div className={classes.main}> <div className={classes.main}>
@ -28,17 +34,4 @@ function SetTitle({ op }) {
); );
} }
SetTitle.fragment = gql`
fragment SetTitle on TimelineItem {
... on SetTitleTimelineItem {
date
...authored
title
was
}
}
${Author.fragment}
`;
export default SetTitle; export default SetTitle;

View File

@ -0,0 +1,8 @@
#import "../Author.graphql"
fragment SetTitle on SetTitleTimelineItem {
date
...authored
title
was
}

View File

@ -1,43 +0,0 @@
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import LabelChange from './LabelChange';
import Message from './Message';
import SetStatus from './SetStatus';
import SetTitle from './SetTitle';
const useStyles = makeStyles(theme => ({
main: {
'& > *:not(:last-child)': {
marginBottom: theme.spacing(2),
},
},
}));
const componentMap = {
CreateTimelineItem: Message,
AddCommentTimelineItem: Message,
LabelChangeTimelineItem: LabelChange,
SetTitleTimelineItem: SetTitle,
SetStatusTimelineItem: SetStatus,
};
function Timeline({ ops }) {
const classes = useStyles();
return (
<div className={classes.main}>
{ops.map((op, index) => {
const Component = componentMap[op.__typename];
if (!Component) {
console.warn('unsupported operation type ' + op.__typename);
return null;
}
return <Component key={index} op={op} />;
})}
</div>
);
}
export default Timeline;

View File

@ -0,0 +1,48 @@
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import LabelChange from './LabelChange';
import Message from './Message';
import SetStatus from './SetStatus';
import SetTitle from './SetTitle';
import { TimelineItemFragment } from './TimelineQuery.generated';
const useStyles = makeStyles(theme => ({
main: {
'& > *:not(:last-child)': {
marginBottom: theme.spacing(2),
},
},
}));
type Props = {
ops: Array<TimelineItemFragment>;
};
function Timeline({ ops }: Props) {
const classes = useStyles();
return (
<div className={classes.main}>
{ops.map((op, index) => {
switch (op.__typename) {
case 'CreateTimelineItem':
return <Message key={index} op={op} />;
case 'AddCommentTimelineItem':
return <Message key={index} op={op} />;
case 'LabelChangeTimelineItem':
return <LabelChange key={index} op={op} />;
case 'SetTitleTimelineItem':
return <SetTitle key={index} op={op} />;
case 'SetStatusTimelineItem':
return <SetStatus key={index} op={op} />;
}
console.warn('unsupported operation type ' + op.__typename);
return null;
})}
</div>
);
}
export default Timeline;

View File

@ -0,0 +1,39 @@
#import "./MessageCreateFragment.graphql"
#import "./MessageCommentFragment.graphql"
#import "./LabelChangeFragment.graphql"
#import "./SetTitleFragment.graphql"
#import "./SetStatusFragment.graphql"
query Timeline($id: String!, $first: Int = 10, $after: String) {
repository {
bug(prefix: $id) {
timeline(first: $first, after: $after) {
nodes {
...TimelineItem
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
fragment TimelineItem on TimelineItem {
... on LabelChangeTimelineItem {
...LabelChange
}
... on SetStatusTimelineItem {
...SetStatus
}
... on SetTitleTimelineItem {
...SetTitle
}
... on AddCommentTimelineItem {
...AddComment
}
... on CreateTimelineItem {
...Create
}
}

View File

@ -1,53 +0,0 @@
import CircularProgress from '@material-ui/core/CircularProgress';
import gql from 'graphql-tag';
import React from 'react';
import { Query } from 'react-apollo';
import LabelChange from './LabelChange';
import SetStatus from './SetStatus';
import SetTitle from './SetTitle';
import Timeline from './Timeline';
import Message from './Message';
const QUERY = gql`
query($id: String!, $first: Int = 10, $after: String) {
defaultRepository {
bug(prefix: $id) {
timeline(first: $first, after: $after) {
nodes {
...LabelChange
...SetStatus
...SetTitle
...AddComment
...Create
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
${Message.createFragment}
${Message.commentFragment}
${LabelChange.fragment}
${SetTitle.fragment}
${SetStatus.fragment}
`;
const TimelineQuery = ({ id }) => (
<Query query={QUERY} variables={{ id, first: 100 }}>
{({ loading, error, data, fetchMore }) => {
if (loading) return <CircularProgress />;
if (error) return <p>Error: {error}</p>;
return (
<Timeline
ops={data.defaultRepository.bug.timeline.nodes}
fetchMore={fetchMore}
/>
);
}}
</Query>
);
export default TimelineQuery;

View File

@ -0,0 +1,30 @@
import CircularProgress from '@material-ui/core/CircularProgress';
import React from 'react';
import Timeline from './Timeline';
import { useTimelineQuery } from './TimelineQuery.generated';
type Props = {
id: string;
};
const TimelineQuery = ({ id }: Props) => {
const { loading, error, data } = useTimelineQuery({
variables: {
id,
first: 100,
},
});
if (loading) return <CircularProgress />;
if (error) return <p>Error: {error}</p>;
const nodes = data?.repository?.bug?.timeline.nodes;
if (!nodes) {
return null;
}
return <Timeline ops={nodes} />;
};
export default TimelineQuery;

View File

@ -1,5 +1,5 @@
import ThemeProvider from '@material-ui/styles/ThemeProvider';
import { createMuiTheme } from '@material-ui/core/styles'; import { createMuiTheme } from '@material-ui/core/styles';
import ThemeProvider from '@material-ui/styles/ThemeProvider';
import ApolloClient from 'apollo-boost'; import ApolloClient from 'apollo-boost';
import { import {
IntrospectionFragmentMatcher, IntrospectionFragmentMatcher,
@ -10,8 +10,8 @@ import { ApolloProvider } from 'react-apollo';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import introspectionQueryResultData from './fragmentTypes';
import App from './App'; import App from './App';
import introspectionQueryResultData from './fragmentTypes';
const theme = createMuiTheme(); const theme = createMuiTheme();

View File

@ -0,0 +1,14 @@
#import "../Author.graphql"
#import "../Label.graphql"
fragment BugRow on Bug {
id
humanId
title
status
createdAt
labels {
...Label
}
...authored
}

View File

@ -1,36 +1,43 @@
import { makeStyles } from '@material-ui/styles';
import TableCell from '@material-ui/core/TableCell/TableCell'; import TableCell from '@material-ui/core/TableCell/TableCell';
import TableRow from '@material-ui/core/TableRow/TableRow'; import TableRow from '@material-ui/core/TableRow/TableRow';
import Tooltip from '@material-ui/core/Tooltip/Tooltip'; import Tooltip from '@material-ui/core/Tooltip/Tooltip';
import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { makeStyles } from '@material-ui/core/styles';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
import gql from 'graphql-tag'; import ErrorOutline from '@material-ui/icons/ErrorOutline';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Date from '../Date'; import Date from '../Date';
import Label from '../Label'; import Label from '../Label';
import Author from '../Author'; import { Status } from '../gqlTypes';
const Open = ({ className }) => ( import { BugRowFragment } from './BugRow.generated';
type OpenClosedProps = { className: string };
const Open = ({ className }: OpenClosedProps) => (
<Tooltip title="Open"> <Tooltip title="Open">
<ErrorOutline htmlColor="#28a745" className={className} /> <ErrorOutline htmlColor="#28a745" className={className} />
</Tooltip> </Tooltip>
); );
const Closed = ({ className }) => ( const Closed = ({ className }: OpenClosedProps) => (
<Tooltip title="Closed"> <Tooltip title="Closed">
<CheckCircleOutline htmlColor="#cb2431" className={className} /> <CheckCircleOutline htmlColor="#cb2431" className={className} />
</Tooltip> </Tooltip>
); );
const Status = ({ status, className }) => { type StatusProps = { className: string; status: Status };
const BugStatus: React.FC<StatusProps> = ({
status,
className,
}: StatusProps) => {
switch (status) { switch (status) {
case 'OPEN': case 'OPEN':
return <Open className={className} />; return <Open className={className} />;
case 'CLOSED': case 'CLOSED':
return <Closed className={className} />; return <Closed className={className} />;
default: default:
return 'unknown status ' + status; return <p>{'unknown status ' + status}</p>;
} }
}; };
@ -57,7 +64,6 @@ const useStyles = makeStyles(theme => ({
fontWeight: 500, fontWeight: 500,
}, },
details: { details: {
...theme.typography.textSecondary,
lineHeight: '1.5rem', lineHeight: '1.5rem',
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
}, },
@ -69,12 +75,16 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
function BugRow({ bug }) { type Props = {
bug: BugRowFragment;
};
function BugRow({ bug }: Props) {
const classes = useStyles(); const classes = useStyles();
return ( return (
<TableRow hover> <TableRow hover>
<TableCell className={classes.cell}> <TableCell className={classes.cell}>
<Status status={bug.status} className={classes.status} /> <BugStatus status={bug.status} className={classes.status} />
<div className={classes.expand}> <div className={classes.expand}>
<Link to={'bug/' + bug.humanId}> <Link to={'bug/' + bug.humanId}>
<div className={classes.expand}> <div className={classes.expand}>
@ -99,21 +109,4 @@ function BugRow({ bug }) {
); );
} }
BugRow.fragment = gql`
fragment BugRow on Bug {
id
humanId
title
status
createdAt
labels {
...Label
}
...authored
}
${Label.fragment}
${Author.fragment}
`;
export default BugRow; export default BugRow;

View File

@ -1,13 +1,18 @@
import React, { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import { makeStyles } from '@material-ui/styles';
import Menu from '@material-ui/core/Menu'; import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem'; import MenuItem from '@material-ui/core/MenuItem';
import { SvgIconProps } from '@material-ui/core/SvgIcon';
import { makeStyles } from '@material-ui/core/styles';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown'; import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
import clsx from 'clsx';
import { LocationDescriptor } from 'history';
import React, { useState, useRef } from 'react';
import { Link } from 'react-router-dom';
function parse(query) { export type Query = { [key: string]: Array<string> };
function parse(query: string): Query {
// TODO: extract the rest of the query? // TODO: extract the rest of the query?
const params = {}; const params: Query = {};
// TODO: support escaping without quotes // TODO: support escaping without quotes
const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g; const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
@ -29,7 +34,7 @@ function parse(query) {
return params; return params;
} }
function quote(value) { function quote(value: string): string {
const hasSingle = value.includes("'"); const hasSingle = value.includes("'");
const hasDouble = value.includes('"'); const hasDouble = value.includes('"');
const hasSpaces = value.includes(' '); const hasSpaces = value.includes(' ');
@ -49,19 +54,19 @@ function quote(value) {
return `"${value}"`; return `"${value}"`;
} }
function stringify(params) { function stringify(params: Query): string {
const parts = Object.entries(params).map(([key, values]) => { const parts: string[][] = Object.entries(params).map(([key, values]) => {
return values.map(value => `${key}:${quote(value)}`); return values.map(value => `${key}:${quote(value)}`);
}); });
return [].concat(...parts).join(' '); return new Array<string>().concat(...parts).join(' ');
} }
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
element: { element: {
...theme.typography.body2, ...theme.typography.body2,
color: ({ active }) => (active ? '#333' : '#444'), color: '#444',
padding: theme.spacing(0, 1), padding: theme.spacing(0, 1),
fontWeight: ({ active }) => (active ? 600 : 400), fontWeight: 400,
textDecoration: 'none', textDecoration: 'none',
display: 'flex', display: 'flex',
background: 'none', background: 'none',
@ -69,21 +74,51 @@ const useStyles = makeStyles(theme => ({
}, },
itemActive: { itemActive: {
fontWeight: 600, fontWeight: 600,
color: '#333',
}, },
icon: { icon: {
paddingRight: theme.spacing(0.5), paddingRight: theme.spacing(0.5),
}, },
})); }));
function Dropdown({ children, dropdown, itemActive, to, ...props }) { type DropdownTuple = [string, string];
type FilterDropdownProps = {
children: React.ReactNode;
dropdown: DropdownTuple[];
itemActive: (key: string) => boolean;
icon?: React.ComponentType<SvgIconProps>;
to: (key: string) => LocationDescriptor;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function FilterDropdown({
children,
dropdown,
itemActive,
icon: Icon,
to,
...props
}: FilterDropdownProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const buttonRef = useRef(); const buttonRef = useRef<HTMLButtonElement>(null);
const classes = useStyles(); const classes = useStyles({ active: false });
const content = (
<>
{Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
<div>{children}</div>
</>
);
return ( return (
<> <>
<button ref={buttonRef} onClick={() => setOpen(!open)} {...props}> <button
{children} ref={buttonRef}
onClick={() => setOpen(!open)}
className={classes.element}
{...props}
>
{content}
<ArrowDropDown fontSize="small" /> <ArrowDropDown fontSize="small" />
</button> </button>
<Menu <Menu
@ -104,7 +139,7 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
<MenuItem <MenuItem
component={Link} component={Link}
to={to(key)} to={to(key)}
className={itemActive(key) ? classes.itemActive : null} className={itemActive(key) ? classes.itemActive : undefined}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
key={key} key={key}
> >
@ -116,8 +151,14 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
); );
} }
function Filter({ active, to, children, icon: Icon, dropdown, ...props }) { export type FilterProps = {
const classes = useStyles({ active }); active: boolean;
to: LocationDescriptor;
icon?: React.ComponentType<SvgIconProps>;
children: React.ReactNode;
};
function Filter({ active, to, children, icon: Icon }: FilterProps) {
const classes = useStyles();
const content = ( const content = (
<> <>
@ -126,29 +167,23 @@ function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
</> </>
); );
if (dropdown) {
return (
<Dropdown
{...props}
to={to}
dropdown={dropdown}
className={classes.element}
>
{content}
</Dropdown>
);
}
if (to) { if (to) {
return ( return (
<Link to={to} {...props} className={classes.element}> <Link
to={to}
className={clsx(classes.element, active && classes.itemActive)}
>
{content} {content}
</Link> </Link>
); );
} }
return <div className={classes.element}>{content}</div>; return (
<div className={clsx(classes.element, active && classes.itemActive)}>
{content}
</div>
);
} }
export default Filter; export default Filter;
export { parse, stringify, quote }; export { parse, stringify, quote, FilterDropdown, Filter };

View File

@ -0,0 +1,7 @@
query BugCount($query: String) {
repository {
bugs: allBugs(query: $query) {
totalCount
}
}
}

View File

@ -1,16 +1,20 @@
import { makeStyles } from '@material-ui/styles'; import { pipe } from '@arrows/composition';
import { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import React from 'react';
import Toolbar from '@material-ui/core/Toolbar'; import Toolbar from '@material-ui/core/Toolbar';
import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { makeStyles } from '@material-ui/core/styles';
import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
import Filter, { parse, stringify } from './Filter'; import ErrorOutline from '@material-ui/icons/ErrorOutline';
import { LocationDescriptor } from 'history';
import React from 'react';
// simple pipe operator import {
// pipe(o, f, g, h) <=> h(g(f(o))) FilterDropdown,
// TODO: move this out? FilterProps,
const pipe = (initial, ...funcs) => funcs.reduce((acc, f) => f(acc), initial); Filter,
parse,
stringify,
Query,
} from './Filter';
import { useBugCountQuery } from './FilterToolbar.generated';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
toolbar: { toolbar: {
@ -25,27 +29,21 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const BUG_COUNT_QUERY = gql`
query($query: String) {
defaultRepository {
bugs: allBugs(query: $query) {
totalCount
}
}
}
`;
// This prepends the filter text with a count // This prepends the filter text with a count
function CountingFilter({ query, children, ...props }) { type CountingFilterProps = {
const { data, loading, error } = useQuery(BUG_COUNT_QUERY, { query: string;
children: React.ReactNode;
} & FilterProps;
function CountingFilter({ query, children, ...props }: CountingFilterProps) {
const { data, loading, error } = useBugCountQuery({
variables: { query }, variables: { query },
}); });
var prefix; var prefix;
if (loading) prefix = '...'; if (loading) prefix = '...';
else if (error) prefix = '???'; else if (error || !data?.repository) prefix = '???';
// TODO: better prefixes & error handling // TODO: better prefixes & error handling
else prefix = data.defaultRepository.bugs.totalCount; else prefix = data.repository.bugs.totalCount;
return ( return (
<Filter {...props}> <Filter {...props}>
@ -54,18 +52,26 @@ function CountingFilter({ query, children, ...props }) {
); );
} }
function FilterToolbar({ query, queryLocation }) { type Props = {
query: string;
queryLocation: (query: string) => LocationDescriptor;
};
function FilterToolbar({ query, queryLocation }: Props) {
const classes = useStyles(); const classes = useStyles();
const params = parse(query); const params: Query = parse(query);
const hasKey = key => params[key] && params[key].length > 0; const hasKey = (key: string): boolean =>
const hasValue = (key, value) => hasKey(key) && params[key].includes(value); params[key] && params[key].length > 0;
const loc = params => pipe(params, stringify, queryLocation); const hasValue = (key: string, value: string): boolean =>
const replaceParam = (key, value) => params => ({ hasKey(key) && params[key].includes(value);
const loc = pipe(stringify, queryLocation);
const replaceParam = (key: string, value: string) => (
params: Query
): Query => ({
...params, ...params,
[key]: [value], [key]: [value],
}); });
const clearParam = key => params => ({ const clearParam = (key: string) => (params: Query): Query => ({
...params, ...params,
[key]: [], [key]: [],
}); });
@ -76,12 +82,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter <CountingFilter
active={hasValue('status', 'open')} active={hasValue('status', 'open')}
query={pipe( query={pipe(
params,
replaceParam('status', 'open'), replaceParam('status', 'open'),
clearParam('sort'), clearParam('sort'),
stringify stringify
)} )(params)}
to={pipe(params, replaceParam('status', 'open'), loc)} to={pipe(replaceParam('status', 'open'), loc)(params)}
icon={ErrorOutline} icon={ErrorOutline}
> >
open open
@ -89,12 +94,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter <CountingFilter
active={hasValue('status', 'closed')} active={hasValue('status', 'closed')}
query={pipe( query={pipe(
params,
replaceParam('status', 'closed'), replaceParam('status', 'closed'),
clearParam('sort'), clearParam('sort'),
stringify stringify
)} )(params)}
to={pipe(params, replaceParam('status', 'closed'), loc)} to={pipe(replaceParam('status', 'closed'), loc)(params)}
icon={CheckCircleOutline} icon={CheckCircleOutline}
> >
closed closed
@ -104,7 +108,7 @@ function FilterToolbar({ query, queryLocation }) {
<Filter active={hasKey('author')}>Author</Filter> <Filter active={hasKey('author')}>Author</Filter>
<Filter active={hasKey('label')}>Label</Filter> <Filter active={hasKey('label')}>Label</Filter>
*/} */}
<Filter <FilterDropdown
dropdown={[ dropdown={[
['id', 'ID'], ['id', 'ID'],
['creation', 'Newest'], ['creation', 'Newest'],
@ -112,12 +116,11 @@ function FilterToolbar({ query, queryLocation }) {
['edit', 'Recently updated'], ['edit', 'Recently updated'],
['edit-asc', 'Least recently updated'], ['edit-asc', 'Least recently updated'],
]} ]}
active={hasKey('sort')}
itemActive={key => hasValue('sort', key)} itemActive={key => hasValue('sort', key)}
to={key => pipe(params, replaceParam('sort', key), loc)} to={key => pipe(replaceParam('sort', key), loc)(params)}
> >
Sort Sort
</Filter> </FilterDropdown>
</Toolbar> </Toolbar>
); );
} }

View File

@ -1,9 +1,12 @@
import Table from '@material-ui/core/Table/Table'; import Table from '@material-ui/core/Table/Table';
import TableBody from '@material-ui/core/TableBody/TableBody'; import TableBody from '@material-ui/core/TableBody/TableBody';
import React from 'react'; import React from 'react';
import BugRow from './BugRow';
function List({ bugs }) { import BugRow from './BugRow';
import { BugListFragment } from './ListQuery.generated';
type Props = { bugs: BugListFragment };
function List({ bugs }: Props) {
return ( return (
<Table> <Table>
<TableBody> <TableBody>

View File

@ -0,0 +1,37 @@
#import "./BugRow.graphql"
query ListBugs(
$first: Int
$last: Int
$after: String
$before: String
$query: String
) {
repository {
bugs: allBugs(
first: $first
last: $last
after: $after
before: $before
query: $query
) {
...BugList
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
fragment BugList on BugConnection {
totalCount
edges {
cursor
node {
...BugRow
}
}
}

View File

@ -1,20 +1,21 @@
import { fade, makeStyles } from '@material-ui/core/styles';
import IconButton from '@material-ui/core/IconButton'; import IconButton from '@material-ui/core/IconButton';
import InputBase from '@material-ui/core/InputBase';
import Paper from '@material-ui/core/Paper';
import { fade, makeStyles, Theme } from '@material-ui/core/styles';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft'; import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight'; import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import Paper from '@material-ui/core/Paper';
import InputBase from '@material-ui/core/InputBase';
import Skeleton from '@material-ui/lab/Skeleton'; import Skeleton from '@material-ui/lab/Skeleton';
import gql from 'graphql-tag'; import { ApolloError } from 'apollo-boost';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { useLocation, useHistory, Link } from 'react-router-dom'; import { useLocation, useHistory, Link } from 'react-router-dom';
import BugRow from './BugRow';
import List from './List';
import FilterToolbar from './FilterToolbar';
const useStyles = makeStyles(theme => ({ import FilterToolbar from './FilterToolbar';
import List from './List';
import { useListBugsQuery } from './ListQuery.generated';
type StylesProps = { searching?: boolean };
const useStyles = makeStyles<Theme, StylesProps>(theme => ({
main: { main: {
maxWidth: 800, maxWidth: 800,
margin: 'auto', margin: 'auto',
@ -46,7 +47,11 @@ const useStyles = makeStyles(theme => ({
backgroundColor: fade(theme.palette.primary.main, 0.05), backgroundColor: fade(theme.palette.primary.main, 0.05),
padding: theme.spacing(0, 1), padding: theme.spacing(0, 1),
width: ({ searching }) => (searching ? '20rem' : '15rem'), width: ({ searching }) => (searching ? '20rem' : '15rem'),
transition: theme.transitions.create(), transition: theme.transitions.create([
'width',
'borderColor',
'backgroundColor',
]),
}, },
searchFocused: { searchFocused: {
borderColor: fade(theme.palette.primary.main, 0.4), borderColor: fade(theme.palette.primary.main, 0.4),
@ -91,51 +96,21 @@ const useStyles = makeStyles(theme => ({
}, },
})); }));
const QUERY = gql` function editParams(
query( params: URLSearchParams,
$first: Int callback: (params: URLSearchParams) => void
$last: Int ) {
$after: String
$before: String
$query: String
) {
defaultRepository {
bugs: allBugs(
first: $first
last: $last
after: $after
before: $before
query: $query
) {
totalCount
edges {
cursor
node {
...BugRow
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
${BugRow.fragment}
`;
function editParams(params, callback) {
const cloned = new URLSearchParams(params.toString()); const cloned = new URLSearchParams(params.toString());
callback(cloned); callback(cloned);
return cloned; return cloned;
} }
// TODO: factor this out // TODO: factor this out
const Placeholder = ({ count }) => { type PlaceholderProps = { count: number };
const classes = useStyles(); const Placeholder: React.FC<PlaceholderProps> = ({
count,
}: PlaceholderProps) => {
const classes = useStyles({});
return ( return (
<> <>
{new Array(count).fill(null).map((_, i) => ( {new Array(count).fill(null).map((_, i) => (
@ -158,7 +133,7 @@ const Placeholder = ({ count }) => {
// TODO: factor this out // TODO: factor this out
const NoBug = () => { const NoBug = () => {
const classes = useStyles(); const classes = useStyles({});
return ( return (
<div className={classes.message}> <div className={classes.message}>
<ErrorOutline fontSize="large" /> <ErrorOutline fontSize="large" />
@ -167,8 +142,9 @@ const NoBug = () => {
); );
}; };
const Error = ({ error }) => { type ErrorProps = { error: ApolloError };
const classes = useStyles(); const Error: React.FC<ErrorProps> = ({ error }: ErrorProps) => {
const classes = useStyles({});
return ( return (
<div className={[classes.errorBox, classes.message].join(' ')}> <div className={[classes.errorBox, classes.message].join(' ')}>
<ErrorOutline fontSize="large" /> <ErrorOutline fontSize="large" />
@ -194,7 +170,7 @@ function ListQuery() {
const classes = useStyles({ searching: !!input }); const classes = useStyles({ searching: !!input });
// TODO is this the right way to do it? // TODO is this the right way to do it?
const lastQuery = useRef(); const lastQuery = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (query !== lastQuery.current) { if (query !== lastQuery.current) {
setInput(query); setInput(query);
@ -202,9 +178,10 @@ function ListQuery() {
lastQuery.current = query; lastQuery.current = query;
}, [query, input, lastQuery]); }, [query, input, lastQuery]);
const num = (param: string | null) => (param ? parseInt(param) : null);
const page = { const page = {
first: params.get('first'), first: num(params.get('first')),
last: params.get('last'), last: num(params.get('last')),
after: params.get('after'), after: params.get('after'),
before: params.get('before'), before: params.get('before'),
}; };
@ -214,9 +191,9 @@ function ListQuery() {
page.first = 10; page.first = 10;
} }
const perPage = page.first || page.last; const perPage = (page.first || page.last || 10).toString();
const { loading, error, data } = useQuery(QUERY, { const { loading, error, data } = useListBugsQuery({
variables: { variables: {
...page, ...page,
query, query,
@ -225,34 +202,34 @@ function ListQuery() {
let nextPage = null; let nextPage = null;
let previousPage = null; let previousPage = null;
let hasNextPage = false;
let hasPreviousPage = false;
let count = 0; let count = 0;
if (!loading && !error && data.defaultRepository.bugs) { if (!loading && !error && data?.repository?.bugs) {
const bugs = data.defaultRepository.bugs; const bugs = data.repository.bugs;
hasNextPage = bugs.pageInfo.hasNextPage;
hasPreviousPage = bugs.pageInfo.hasPreviousPage;
count = bugs.totalCount; count = bugs.totalCount;
// This computes the URL for the next page // This computes the URL for the next page
nextPage = { if (bugs.pageInfo.hasNextPage) {
...location, nextPage = {
search: editParams(params, p => { ...location,
p.delete('last'); search: editParams(params, p => {
p.delete('before'); p.delete('last');
p.set('first', perPage); p.delete('before');
p.set('after', bugs.pageInfo.endCursor); p.set('first', perPage);
}).toString(), p.set('after', bugs.pageInfo.endCursor);
}; }).toString(),
};
}
// and this for the previous page // and this for the previous page
previousPage = { if (bugs.pageInfo.hasPreviousPage) {
...location, previousPage = {
search: editParams(params, p => { ...location,
p.delete('first'); search: editParams(params, p => {
p.delete('after'); p.delete('first');
p.set('last', perPage); p.delete('after');
p.set('before', bugs.pageInfo.startCursor); p.set('last', perPage);
}).toString(), p.set('before', bugs.pageInfo.startCursor);
}; }).toString(),
};
}
} }
// Prepare params without paging for editing filters // Prepare params without paging for editing filters
@ -263,7 +240,7 @@ function ListQuery() {
p.delete('after'); p.delete('after');
}); });
// Returns a new location with the `q` param edited // Returns a new location with the `q` param edited
const queryLocation = query => ({ const queryLocation = (query: string) => ({
...location, ...location,
search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(), search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(),
}); });
@ -273,8 +250,8 @@ function ListQuery() {
content = <Placeholder count={10} />; content = <Placeholder count={10} />;
} else if (error) { } else if (error) {
content = <Error error={error} />; content = <Error error={error} />;
} else { } else if (data?.repository) {
const bugs = data.defaultRepository.bugs; const bugs = data.repository.bugs;
if (bugs.totalCount === 0) { if (bugs.totalCount === 0) {
content = <NoBug />; content = <NoBug />;
@ -283,7 +260,7 @@ function ListQuery() {
} }
} }
const formSubmit = e => { const formSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
history.push(queryLocation(input)); history.push(queryLocation(input));
}; };
@ -296,7 +273,7 @@ function ListQuery() {
<InputBase <InputBase
placeholder="Filter" placeholder="Filter"
value={input} value={input}
onInput={e => setInput(e.target.value)} onInput={(e: any) => setInput(e.target.value)}
classes={{ classes={{
root: classes.search, root: classes.search,
focused: classes.searchFocused, focused: classes.searchFocused,
@ -310,21 +287,25 @@ function ListQuery() {
<FilterToolbar query={query} queryLocation={queryLocation} /> <FilterToolbar query={query} queryLocation={queryLocation} />
{content} {content}
<div className={classes.pagination}> <div className={classes.pagination}>
<IconButton {previousPage ? (
component={hasPreviousPage ? Link : 'button'} <IconButton component={Link} to={previousPage}>
to={previousPage} <KeyboardArrowLeft />
disabled={!hasPreviousPage} </IconButton>
> ) : (
<KeyboardArrowLeft /> <IconButton disabled>
</IconButton> <KeyboardArrowLeft />
</IconButton>
)}
<div>{loading ? 'Loading' : `Total: ${count}`}</div> <div>{loading ? 'Loading' : `Total: ${count}`}</div>
<IconButton {nextPage ? (
component={hasNextPage ? Link : 'button'} <IconButton component={Link} to={nextPage}>
to={nextPage} <KeyboardArrowRight />
disabled={!hasNextPage} </IconButton>
> ) : (
<KeyboardArrowRight /> <IconButton disabled>
</IconButton> <KeyboardArrowRight />
</IconButton>
)}
</div> </div>
</Paper> </Paper>
); );

1
webui/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -1,5 +1,5 @@
import React from 'react';
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/styles';
import React from 'react';
const useStyles = makeStyles({ const useStyles = makeStyles({
tag: { tag: {
@ -7,7 +7,10 @@ const useStyles = makeStyles({
}, },
}); });
const ImageTag = ({ alt, ...props }) => { const ImageTag = ({
alt,
...props
}: React.ImgHTMLAttributes<HTMLImageElement>) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<a href={props.src} target="_blank" rel="noopener noreferrer nofollow"> <a href={props.src} target="_blank" rel="noopener noreferrer nofollow">

View File

@ -1,5 +1,5 @@
import React from 'react';
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/styles';
import React from 'react';
const useStyles = makeStyles({ const useStyles = makeStyles({
tag: { tag: {
@ -8,7 +8,7 @@ const useStyles = makeStyles({
}, },
}); });
const PreTag = props => { const PreTag = (props: React.HTMLProps<HTMLPreElement>) => {
const classes = useStyles(); const classes = useStyles();
return <pre className={classes.tag} {...props}></pre>; return <pre className={classes.tag} {...props}></pre>;
}; };

20
webui/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"typeRoots": ["node_modules/@types/", "types/"]
},
"include": ["src"]
}

6
webui/types/remark-html/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module 'remark-html' {
import { Plugin } from 'unified';
const plugin: Plugin;
export default plugin;
}

6
webui/types/remark-react/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module 'remark-react' {
import { Plugin } from 'unified';
const plugin: Plugin;
export default plugin;
}