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

View File

@ -163,16 +163,6 @@ type ComplexityRoot struct {
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 {
Author func(childComplexity int) int
Date func(childComplexity int) int
@ -264,14 +254,12 @@ type ComplexityRoot struct {
}
Mutation struct {
AddComment func(childComplexity int, input models.AddCommentInput) int
ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int
CloseBug func(childComplexity int, input models.CloseBugInput) int
Commit func(childComplexity int, input models.CommitInput) int
CommitAsNeeded func(childComplexity int, input models.CommitAsNeededInput) int
NewBug func(childComplexity int, input models.NewBugInput) int
OpenBug func(childComplexity int, input models.OpenBugInput) int
SetTitle func(childComplexity int, input models.SetTitleInput) int
AddComment func(childComplexity int, input models.AddCommentInput) int
ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int
CloseBug func(childComplexity int, input models.CloseBugInput) int
NewBug func(childComplexity int, input models.NewBugInput) int
OpenBug func(childComplexity int, input models.OpenBugInput) int
SetTitle func(childComplexity int, input models.SetTitleInput) int
}
NewBugPayload struct {
@ -306,8 +294,7 @@ type ComplexityRoot struct {
}
Query struct {
DefaultRepository func(childComplexity int) int
Repository func(childComplexity int, ref string) int
Repository func(childComplexity int, ref *string) int
}
Repository struct {
@ -315,6 +302,7 @@ type ComplexityRoot struct {
AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int
Bug func(childComplexity int, prefix string) int
Identity func(childComplexity int, prefix string) int
Name func(childComplexity int) int
UserIdentity func(childComplexity 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)
CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, 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 {
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 {
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)
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)
@ -925,34 +911,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.CreateOperation.Author == nil {
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
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":
if e.complexity.Mutation.NewBug == nil {
break
@ -1539,13 +1473,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
if e.complexity.Query.Repository == nil {
break
@ -1556,7 +1483,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
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":
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
case "Repository.name":
if e.complexity.Repository.Name == nil {
break
}
return e.complexity.Repository.Name(childComplexity), true
case "Repository.userIdentity":
if e.complexity.Repository.UserIdentity == nil {
break
@ -2184,38 +2118,6 @@ type SetTitlePayload {
"""The resulting operation"""
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},
&ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug."""
interface Operation {
@ -2320,6 +2222,9 @@ type LabelChangeOperation implements Operation & Authored {
`, BuiltIn: false},
&ast.Source{Name: "schema/repository.graphql", Input: `
type Repository {
"""The name of the repository"""
name: String
"""All the bugs"""
allBugs(
"""Returns the elements in the list that come after the specified cursor."""
@ -2330,7 +2235,7 @@ type Repository {
first: Int
"""Returns the last _n_ elements from the list."""
last: Int
"""A query to select and order bugs"""
"""A query to select and order bugs."""
query: String
): BugConnection!
@ -2366,12 +2271,8 @@ type Repository {
): LabelConnection!
}`, BuiltIn: false},
&ast.Source{Name: "schema/root.graphql", Input: `type Query {
"""The default unnamend repository."""
defaultRepository: Repository
"""Access a repository by reference/name."""
repository(ref: String!): Repository
#TODO: connection for all repositories
"""Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
repository(ref: String): Repository
}
type Mutation {
@ -2387,10 +2288,6 @@ type Mutation {
closeBug(input: CloseBugInput!): CloseBugPayload!
"""Change a bug's title"""
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},
&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
}
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) {
var err error
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) {
var err error
args := map[string]interface{}{}
var arg0 string
var arg0 *string
if tmp, ok := rawArgs["ref"]; ok {
arg0, err = ec.unmarshalNString2string(ctx, tmp)
arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
if err != nil {
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)
}
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) {
defer func() {
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)
}
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) {
defer func() {
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)
}
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) {
defer func() {
if r := recover(); r != nil {
@ -7876,7 +7502,7 @@ func (ec *executionContext) _Query_repository(ctx context.Context, field graphql
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.Query().Repository(rctx, args["ref"].(string))
return ec.resolvers.Query().Repository(rctx, args["ref"].(*string))
})
if err != nil {
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)
}
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) {
defer func() {
if r := recover(); r != nil {
@ -10273,66 +9930,6 @@ func (ec *executionContext) unmarshalInputCloseBugInput(ctx context.Context, obj
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) {
var it models.NewBugInput
var asMap = obj.(map[string]interface{})
@ -11346,64 +10943,6 @@ func (ec *executionContext) _CommentHistoryStep(ctx context.Context, sel ast.Sel
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"}
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 {
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:
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 {
case "__typename":
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":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -12440,6 +11958,17 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe
switch field.Name {
case "__typename":
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":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -13544,42 +13073,6 @@ func (ec *executionContext) marshalNCommentHistoryStep2ᚕgithubᚗcomᚋMichael
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 {
return ec._CreateOperation(ctx, sel, &v)
}

View File

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

View File

@ -111,38 +111,6 @@ type CommentEdge struct {
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 {
Edges []*IdentityEdge `json:"edges"`
Nodes []IdentityWrapper `json:"nodes"`

View File

@ -23,6 +23,15 @@ func (r mutationResolver) getRepo(ref *string) (*cache.RepoCache, error) {
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) {
repo, err := r.getRepo(input.RepoRef)
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) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
@ -57,6 +61,11 @@ func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentI
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
return &models.AddCommentPayload{
ClientMutationID: input.ClientMutationID,
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) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
@ -80,6 +84,11 @@ func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLa
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
resultsPtr := make([]*bug.LabelChangeResult, len(results))
for i, result := range results {
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) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
@ -109,6 +113,11 @@ func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput)
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
return &models.OpenBugPayload{
ClientMutationID: input.ClientMutationID,
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) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
@ -132,6 +136,11 @@ func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
return &models.CloseBugPayload{
ClientMutationID: input.ClientMutationID,
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) {
repo, err := r.getRepo(input.RepoRef)
if err != nil {
return nil, err
}
b, err := repo.ResolveBugPrefix(input.Prefix)
b, err := r.getBug(input.RepoRef, input.Prefix)
if err != nil {
return nil, err
}
@ -155,53 +159,14 @@ func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput
return nil, err
}
err = b.Commit()
if err != nil {
return nil, err
}
return &models.SetTitlePayload{
ClientMutationID: input.ClientMutationID,
Bug: models.NewLoadedBug(b.Snapshot()),
Operation: op,
}, 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
}
func (r rootQueryResolver) Repository(_ context.Context, ref string) (*models.Repository, error) {
repo, err := r.cache.ResolveRepo(ref)
func (r rootQueryResolver) Repository(_ context.Context, ref *string) (*models.Repository, error) {
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 {
return nil, err

View File

@ -15,6 +15,11 @@ var _ graph.RepositoryResolver = &repoResolver{}
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) {
input := models.ConnectionInput{
Before: before,
@ -153,7 +158,7 @@ func (repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (mod
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{
Before: before,
After: after,

View File

@ -136,35 +136,3 @@ type SetTitlePayload {
"""The resulting operation"""
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 {
"""The name of the repository"""
name: String
"""All the bugs"""
allBugs(
"""Returns the elements in the list that come after the specified cursor."""
@ -10,7 +13,7 @@ type Repository {
first: Int
"""Returns the last _n_ elements from the list."""
last: Int
"""A query to select and order bugs"""
"""A query to select and order bugs."""
query: String
): BugConnection!

View File

@ -1,10 +1,6 @@
type Query {
"""The default unnamend repository."""
defaultRepository: Repository
"""Access a repository by reference/name."""
repository(ref: String!): Repository
#TODO: connection for all repositories
"""Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
repository(ref: String): Repository
}
type Mutation {
@ -20,8 +16,4 @@ type Mutation {
closeBug(input: CloseBugInput!): CloseBugPayload!
"""Change a bug's title"""
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
test:
npm run generate
npm run lint
CI=true npm run test
build:
npm run build

View File

@ -7,10 +7,11 @@
2. Run the GraphQL backend on the port 3001
- `./git-bug webui -p 3001`
3. Run the hot-reloadable development WebUI
- 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.
## 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'
overwrite: true
documents: src/**/*.graphql
generates:
./src/fragmentTypes.js:
./src/fragmentTypes.ts:
plugins:
- fragment-matcher
- fragment-matcher
config:
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,
"dependencies": {
"@apollo/react-hooks": "^3.1.3",
"@arrows/composition": "^1.2.2",
"@material-ui/core": "^4.9.0",
"@material-ui/icons": "^4.2.1",
"@material-ui/lab": "^4.0.0-alpha.40",
"@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",
"graphql": "^14.3.0",
"clsx": "^1.1.0",
"graphql": "^14.6.0",
"graphql.macro": "^1.4.2",
"moment": "^2.24.0",
"react": "^16.8.6",
"react-apollo": "^3.1.3",
"react-dom": "^16.8.6",
"react-router": "^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-parse": "^7.0.2",
"remark-react": "^6.0.0",
"typescript": "^3.7.5",
"unified": "^8.4.2"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.11.2",
"@graphql-codegen/fragment-matcher": "^1.11.2",
"eslint-config-prettier": "^6.9.0",
"@graphql-codegen/cli": "^1.12.1",
"@graphql-codegen/fragment-matcher": "^1.12.1",
"@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",
"prettier": "^1.19.1"
},
@ -35,7 +48,7 @@
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"generate": "graphql-codegen",
"lint": "eslint src/"
"lint": "eslint src --ext .ts --ext .tsx --ext .js --ext .jsx --ext .graphql"
},
"proxy": "http://localhost:3001",
"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 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 {
createMuiTheme,
ThemeProvider,
makeStyles,
} from '@material-ui/core/styles';
import React from 'react';
import { Route, Switch } from 'react-router';
import { Link } from 'react-router-dom';
import CurrentIdentity from './CurrentIdentity';
import BugQuery from './bug/BugQuery';
import ListQuery from './list/ListQuery';
import CurrentIdentity from './CurrentIdentity';
const theme = createMuiTheme({
palette: {
@ -20,7 +23,9 @@ const theme = createMuiTheme({
});
const useStyles = makeStyles(theme => ({
offset: theme.mixins.toolbar,
offset: {
...theme.mixins.toolbar,
},
filler: {
flexGrow: 1,
},
@ -46,7 +51,7 @@ export default function App() {
<AppBar position="fixed" color="primary">
<Toolbar>
<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
</Link>
<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 Tooltip from '@material-ui/core/Tooltip/Tooltip';
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) {
return <span {...props}>{author.displayName}</span>;
}
@ -15,18 +21,7 @@ const Author = ({ author, ...props }) => {
);
};
Author.fragment = gql`
fragment authored on Authored {
author {
name
email
displayName
avatarUrl
}
}
`;
export const Avatar = ({ author, ...props }) => {
export const Avatar = ({ author, ...props }: Props) => {
if (author.avatarUrl) {
return <MAvatar src={author.avatarUrl} {...props} />;
}

View File

@ -1,11 +1,14 @@
import unified from 'unified';
import parse from 'remark-parse';
import React from 'react';
import html from 'remark-html';
import parse from 'remark-parse';
import remark2react from 'remark-react';
import unified from 'unified';
import ImageTag from './tag/ImageTag';
import PreTag from './tag/PreTag';
const Content = ({ markdown }) => {
type Props = { markdown: string };
const Content: React.FC<Props> = ({ markdown }: Props) => {
const processor = unified()
.use(parse)
.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;

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 * as moment from 'moment';
import moment from 'moment';
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')}>
<span> {moment(date).fromNow()} </span>
</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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/styles';
import { common } from '@material-ui/core/colors';
import { makeStyles } from '@material-ui/core/styles';
import {
getContrastRatio,
darken,
} 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
const contrastThreshold = 2.5;
// Guess the text color based on the background color
const getTextColor = background =>
const getTextColor = (background: string) =>
getContrastRatio(background, common.white) >= contrastThreshold
? common.white // White on dark backgrounds
: 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
const createStyle = color => ({
const createStyle = (color: Color) => ({
backgroundColor: _rgb(color),
color: getTextColor(_rgb(color)),
borderBottomColor: darken(_rgb(color), 0.2),
@ -30,7 +33,7 @@ const useStyles = makeStyles(theme => ({
...theme.typography.body1,
padding: '1px 6px 0.5px',
fontSize: '0.9em',
fontWeight: '500',
fontWeight: 500,
margin: '0.05em 1px calc(-1.5px + 0.05em)',
borderRadius: '3px',
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();
return (
<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;

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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Author from '../Author';
import Date from '../Date';
import TimelineQuery from './TimelineQuery';
import Label from '../Label';
import { BugFragment } from './Bug.generated';
import TimelineQuery from './TimelineQuery';
const useStyles = makeStyles(theme => ({
main: {
maxWidth: 800,
@ -51,7 +53,11 @@ const useStyles = makeStyles(theme => ({
},
}));
function Bug({ bug }) {
type Props = {
bug: BugFragment;
};
function Bug({ bug }: Props) {
const classes = useStyles();
return (
<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;

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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Author from '../Author';
import Date from '../Date';
import Label from '../Label';
import { LabelChangeFragment } from './LabelChangeFragment.generated';
const useStyles = makeStyles(theme => ({
main: {
...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 classes = useStyles();
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;

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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Author from '../Author';
import { Avatar } from '../Author';
import Date from '../Date';
import Content from '../Content';
import Date from '../Date';
import { AddCommentFragment } from './MessageCommentFragment.generated';
import { CreateFragment } from './MessageCreateFragment.generated';
const useStyles = makeStyles(theme => ({
author: {
@ -47,7 +50,11 @@ const useStyles = makeStyles(theme => ({
},
}));
function Message({ op }) {
type Props = {
op: AddCommentFragment | CreateFragment;
};
function Message({ op }: Props) {
const classes = useStyles();
return (
<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;

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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Author from '../Author';
import Date from '../Date';
import { SetStatusFragment } from './SetStatusFragment.generated';
const useStyles = makeStyles(theme => ({
main: {
...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();
return (
<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;

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 gql from 'graphql-tag';
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import Author from '../Author';
import Date from '../Date';
import { SetTitleFragment } from './SetTitleFragment.generated';
const useStyles = makeStyles(theme => ({
main: {
...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();
return (
<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;

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 ThemeProvider from '@material-ui/styles/ThemeProvider';
import ApolloClient from 'apollo-boost';
import {
IntrospectionFragmentMatcher,
@ -10,8 +10,8 @@ import { ApolloProvider } from 'react-apollo';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import introspectionQueryResultData from './fragmentTypes';
import App from './App';
import introspectionQueryResultData from './fragmentTypes';
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 TableRow from '@material-ui/core/TableRow/TableRow';
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 gql from 'graphql-tag';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import React from 'react';
import { Link } from 'react-router-dom';
import Date from '../Date';
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">
<ErrorOutline htmlColor="#28a745" className={className} />
</Tooltip>
);
const Closed = ({ className }) => (
const Closed = ({ className }: OpenClosedProps) => (
<Tooltip title="Closed">
<CheckCircleOutline htmlColor="#cb2431" className={className} />
</Tooltip>
);
const Status = ({ status, className }) => {
type StatusProps = { className: string; status: Status };
const BugStatus: React.FC<StatusProps> = ({
status,
className,
}: StatusProps) => {
switch (status) {
case 'OPEN':
return <Open className={className} />;
case 'CLOSED':
return <Closed className={className} />;
default:
return 'unknown status ' + status;
return <p>{'unknown status ' + status}</p>;
}
};
@ -57,7 +64,6 @@ const useStyles = makeStyles(theme => ({
fontWeight: 500,
},
details: {
...theme.typography.textSecondary,
lineHeight: '1.5rem',
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();
return (
<TableRow hover>
<TableCell className={classes.cell}>
<Status status={bug.status} className={classes.status} />
<BugStatus status={bug.status} className={classes.status} />
<div className={classes.expand}>
<Link to={'bug/' + bug.humanId}>
<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;

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 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 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?
const params = {};
const params: Query = {};
// TODO: support escaping without quotes
const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
@ -29,7 +34,7 @@ function parse(query) {
return params;
}
function quote(value) {
function quote(value: string): string {
const hasSingle = value.includes("'");
const hasDouble = value.includes('"');
const hasSpaces = value.includes(' ');
@ -49,19 +54,19 @@ function quote(value) {
return `"${value}"`;
}
function stringify(params) {
const parts = Object.entries(params).map(([key, values]) => {
function stringify(params: Query): string {
const parts: string[][] = Object.entries(params).map(([key, values]) => {
return values.map(value => `${key}:${quote(value)}`);
});
return [].concat(...parts).join(' ');
return new Array<string>().concat(...parts).join(' ');
}
const useStyles = makeStyles(theme => ({
element: {
...theme.typography.body2,
color: ({ active }) => (active ? '#333' : '#444'),
color: '#444',
padding: theme.spacing(0, 1),
fontWeight: ({ active }) => (active ? 600 : 400),
fontWeight: 400,
textDecoration: 'none',
display: 'flex',
background: 'none',
@ -69,21 +74,51 @@ const useStyles = makeStyles(theme => ({
},
itemActive: {
fontWeight: 600,
color: '#333',
},
icon: {
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 buttonRef = useRef();
const classes = useStyles();
const buttonRef = useRef<HTMLButtonElement>(null);
const classes = useStyles({ active: false });
const content = (
<>
{Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
<div>{children}</div>
</>
);
return (
<>
<button ref={buttonRef} onClick={() => setOpen(!open)} {...props}>
{children}
<button
ref={buttonRef}
onClick={() => setOpen(!open)}
className={classes.element}
{...props}
>
{content}
<ArrowDropDown fontSize="small" />
</button>
<Menu
@ -104,7 +139,7 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
<MenuItem
component={Link}
to={to(key)}
className={itemActive(key) ? classes.itemActive : null}
className={itemActive(key) ? classes.itemActive : undefined}
onClick={() => setOpen(false)}
key={key}
>
@ -116,8 +151,14 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
);
}
function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
const classes = useStyles({ active });
export type FilterProps = {
active: boolean;
to: LocationDescriptor;
icon?: React.ComponentType<SvgIconProps>;
children: React.ReactNode;
};
function Filter({ active, to, children, icon: Icon }: FilterProps) {
const classes = useStyles();
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) {
return (
<Link to={to} {...props} className={classes.element}>
<Link
to={to}
className={clsx(classes.element, active && classes.itemActive)}
>
{content}
</Link>
);
}
return <div className={classes.element}>{content}</div>;
return (
<div className={clsx(classes.element, active && classes.itemActive)}>
{content}
</div>
);
}
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 { useQuery } from '@apollo/react-hooks';
import gql from 'graphql-tag';
import React from 'react';
import { pipe } from '@arrows/composition';
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 Filter, { parse, stringify } from './Filter';
import ErrorOutline from '@material-ui/icons/ErrorOutline';
import { LocationDescriptor } from 'history';
import React from 'react';
// simple pipe operator
// pipe(o, f, g, h) <=> h(g(f(o)))
// TODO: move this out?
const pipe = (initial, ...funcs) => funcs.reduce((acc, f) => f(acc), initial);
import {
FilterDropdown,
FilterProps,
Filter,
parse,
stringify,
Query,
} from './Filter';
import { useBugCountQuery } from './FilterToolbar.generated';
const useStyles = makeStyles(theme => ({
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
function CountingFilter({ query, children, ...props }) {
const { data, loading, error } = useQuery(BUG_COUNT_QUERY, {
type CountingFilterProps = {
query: string;
children: React.ReactNode;
} & FilterProps;
function CountingFilter({ query, children, ...props }: CountingFilterProps) {
const { data, loading, error } = useBugCountQuery({
variables: { query },
});
var prefix;
if (loading) prefix = '...';
else if (error) prefix = '???';
else if (error || !data?.repository) prefix = '???';
// TODO: better prefixes & error handling
else prefix = data.defaultRepository.bugs.totalCount;
else prefix = data.repository.bugs.totalCount;
return (
<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 params = parse(query);
const params: Query = parse(query);
const hasKey = key => params[key] && params[key].length > 0;
const hasValue = (key, value) => hasKey(key) && params[key].includes(value);
const loc = params => pipe(params, stringify, queryLocation);
const replaceParam = (key, value) => params => ({
const hasKey = (key: string): boolean =>
params[key] && params[key].length > 0;
const hasValue = (key: string, value: string): boolean =>
hasKey(key) && params[key].includes(value);
const loc = pipe(stringify, queryLocation);
const replaceParam = (key: string, value: string) => (
params: Query
): Query => ({
...params,
[key]: [value],
});
const clearParam = key => params => ({
const clearParam = (key: string) => (params: Query): Query => ({
...params,
[key]: [],
});
@ -76,12 +82,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter
active={hasValue('status', 'open')}
query={pipe(
params,
replaceParam('status', 'open'),
clearParam('sort'),
stringify
)}
to={pipe(params, replaceParam('status', 'open'), loc)}
)(params)}
to={pipe(replaceParam('status', 'open'), loc)(params)}
icon={ErrorOutline}
>
open
@ -89,12 +94,11 @@ function FilterToolbar({ query, queryLocation }) {
<CountingFilter
active={hasValue('status', 'closed')}
query={pipe(
params,
replaceParam('status', 'closed'),
clearParam('sort'),
stringify
)}
to={pipe(params, replaceParam('status', 'closed'), loc)}
)(params)}
to={pipe(replaceParam('status', 'closed'), loc)(params)}
icon={CheckCircleOutline}
>
closed
@ -104,7 +108,7 @@ function FilterToolbar({ query, queryLocation }) {
<Filter active={hasKey('author')}>Author</Filter>
<Filter active={hasKey('label')}>Label</Filter>
*/}
<Filter
<FilterDropdown
dropdown={[
['id', 'ID'],
['creation', 'Newest'],
@ -112,12 +116,11 @@ function FilterToolbar({ query, queryLocation }) {
['edit', 'Recently updated'],
['edit-asc', 'Least recently updated'],
]}
active={hasKey('sort')}
itemActive={key => hasValue('sort', key)}
to={key => pipe(params, replaceParam('sort', key), loc)}
to={key => pipe(replaceParam('sort', key), loc)(params)}
>
Sort
</Filter>
</FilterDropdown>
</Toolbar>
);
}

View File

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