Merge pull request #323 from MichaelMure/webui/typescript

Michael Muré created

Webui/typescript

Change summary

cache/repo_cache.go                          |   12 
graphql/graph/gen_graph.go                   |  627 +-----------
graphql/graphql_test.go                      |    4 
graphql/models/gen_models.go                 |   32 
graphql/resolvers/mutation.go                |   83 -
graphql/resolvers/query.go                   |   11 
graphql/resolvers/repo.go                    |    7 
graphql/schema/mutations.graphql             |   32 
graphql/schema/repository.graphql            |    5 
graphql/schema/root.graphql                  |   12 
webui/.eslintrc                              |    4 
webui/.eslintrc.js                           |   37 
webui/Makefile                               |    2 
webui/Readme.md                              |    5 
webui/codegen.yaml                           |   28 
webui/package-lock.json                      | 1090 +++++++++------------
webui/package.json                           |   25 
webui/src/.gitignore                         |    5 
webui/src/App.tsx                            |   15 
webui/src/Author.graphql                     |    8 
webui/src/Author.tsx                         |   25 
webui/src/Content.tsx                        |   12 
webui/src/CurrentIdentity.graphql            |    8 
webui/src/CurrentIdentity.js                 |   45 
webui/src/CurrentIdentity.tsx                |   30 
webui/src/Date.tsx                           |    5 
webui/src/Label.graphql                      |    8 
webui/src/Label.tsx                          |   33 
webui/src/__tests__/query.ts                 |    0 
webui/src/bug/Bug.graphql                    |   14 
webui/src/bug/Bug.tsx                        |   30 
webui/src/bug/BugQuery.graphql               |    9 
webui/src/bug/BugQuery.js                    |   30 
webui/src/bug/BugQuery.tsx                   |   22 
webui/src/bug/LabelChange.tsx                |   30 
webui/src/bug/LabelChangeFragment.graphql    |   13 
webui/src/bug/Message.tsx                    |   41 
webui/src/bug/MessageCommentFragment.graphql |    8 
webui/src/bug/MessageCreateFragment.graphql  |    8 
webui/src/bug/SetStatus.tsx                  |   24 
webui/src/bug/SetStatusFragment.graphql      |    7 
webui/src/bug/SetTitle.tsx                   |   25 
webui/src/bug/SetTitleFragment.graphql       |    8 
webui/src/bug/Timeline.js                    |   43 
webui/src/bug/Timeline.tsx                   |   48 
webui/src/bug/TimelineQuery.graphql          |   39 
webui/src/bug/TimelineQuery.js               |   53 -
webui/src/bug/TimelineQuery.tsx              |   30 
webui/src/index.tsx                          |    4 
webui/src/list/BugRow.graphql                |   14 
webui/src/list/BugRow.tsx                    |   49 
webui/src/list/Filter.tsx                    |  103 +
webui/src/list/FilterToolbar.graphql         |    7 
webui/src/list/FilterToolbar.tsx             |   85 
webui/src/list/List.tsx                      |    5 
webui/src/list/ListQuery.graphql             |   37 
webui/src/list/ListQuery.tsx                 |  181 +--
webui/src/react-app-env.d.ts                 |    1 
webui/src/tag/ImageTag.tsx                   |    7 
webui/src/tag/PreTag.tsx                     |    4 
webui/tsconfig.json                          |   20 
webui/types/remark-html/index.d.ts           |    6 
webui/types/remark-react/index.d.ts          |    6 
63 files changed, 1,333 insertions(+), 1,888 deletions(-)

Detailed changes

cache/repo_cache.go 🔗

@@ -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()

graphql/graph/gen_graph.go 🔗

@@ -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,7 +7478,7 @@ 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) {
+func (ec *executionContext) _Query_repository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
 			ec.Error(ctx, ec.Recover(ctx, r))
@@ -7836,9 +7493,16 @@ func (ec *executionContext) _Query_defaultRepository(ctx context.Context, field
 	}
 
 	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_repository_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.Query().DefaultRepository(rctx)
+		return ec.resolvers.Query().Repository(rctx, args["ref"].(*string))
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -7852,7 +7516,7 @@ func (ec *executionContext) _Query_defaultRepository(ctx context.Context, field
 	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___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
 			ec.Error(ctx, ec.Recover(ctx, r))
@@ -7868,7 +7532,7 @@ func (ec *executionContext) _Query_repository(ctx context.Context, field graphql
 
 	ctx = graphql.WithFieldContext(ctx, fc)
 	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := ec.field_Query_repository_args(ctx, rawArgs)
+	args, err := ec.field_Query___type_args(ctx, rawArgs)
 	if err != nil {
 		ec.Error(ctx, err)
 		return graphql.Null
@@ -7876,7 +7540,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.introspectType(args["name"].(string))
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -7885,12 +7549,12 @@ func (ec *executionContext) _Query_repository(ctx context.Context, field graphql
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(*models.Repository)
+	res := resTmp.(*introspection.Type)
 	fc.Result = res
-	return ec.marshalORepository2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐRepository(ctx, field.Selections, res)
+	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
 			ec.Error(ctx, ec.Recover(ctx, r))
@@ -7905,16 +7569,9 @@ func (ec *executionContext) _Query___type(ctx context.Context, field graphql.Col
 	}
 
 	ctx = graphql.WithFieldContext(ctx, fc)
-	rawArgs := field.ArgumentMap(ec.Variables)
-	args, err := ec.field_Query___type_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.introspectType(args["name"].(string))
+		return ec.introspectSchema()
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -7923,12 +7580,12 @@ func (ec *executionContext) _Query___type(ctx context.Context, field graphql.Col
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(*introspection.Type)
+	res := resTmp.(*introspection.Schema)
 	fc.Result = res
-	return ec.marshalO__Type2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐType(ctx, field.Selections, res)
+	return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res)
 }
 
-func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+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))
@@ -7936,7 +7593,7 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
 		}
 	}()
 	fc := &graphql.FieldContext{
-		Object:   "Query",
+		Object:   "Repository",
 		Field:    field,
 		Args:     nil,
 		IsMethod: true,
@@ -7945,7 +7602,7 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
 	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.introspectSchema()
+		return ec.resolvers.Repository().Name(rctx, obj)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -7954,9 +7611,9 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
 	if resTmp == nil {
 		return graphql.Null
 	}
-	res := resTmp.(*introspection.Schema)
+	res := resTmp.(*string)
 	fc.Result = res
-	return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, 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) {
@@ -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)
 }

graphql/graphql_test.go 🔗

@@ -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 {

graphql/models/gen_models.go 🔗

@@ -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"`

graphql/resolvers/mutation.go 🔗

@@ -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,17 +51,17 @@ 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)
+	b, err := r.getBug(input.RepoRef, input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(input.Prefix)
+	op, err := b.AddCommentWithFiles(input.Message, input.Files)
 	if err != nil {
 		return nil, err
 	}
 
-	op, err := b.AddCommentWithFiles(input.Message, input.Files)
+	err = b.Commit()
 	if err != nil {
 		return nil, err
 	}
@@ -65,17 +74,17 @@ 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)
+	b, err := r.getBug(input.RepoRef, input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(input.Prefix)
+	results, op, err := b.ChangeLabels(input.Added, input.Removed)
 	if err != nil {
 		return nil, err
 	}
 
-	results, op, err := b.ChangeLabels(input.Added, input.Removed)
+	err = b.Commit()
 	if err != nil {
 		return nil, err
 	}
@@ -94,17 +103,17 @@ 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)
+	b, err := r.getBug(input.RepoRef, input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(input.Prefix)
+	op, err := b.Open()
 	if err != nil {
 		return nil, err
 	}
 
-	op, err := b.Open()
+	err = b.Commit()
 	if err != nil {
 		return nil, err
 	}
@@ -117,17 +126,17 @@ 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)
+	b, err := r.getBug(input.RepoRef, input.Prefix)
 	if err != nil {
 		return nil, err
 	}
 
-	b, err := repo.ResolveBugPrefix(input.Prefix)
+	op, err := b.Close()
 	if err != nil {
 		return nil, err
 	}
 
-	op, err := b.Close()
+	err = b.Commit()
 	if err != nil {
 		return nil, err
 	}
@@ -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
 	}
 
-	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{
+	return &models.SetTitlePayload{
 		ClientMutationID: input.ClientMutationID,
 		Bug:              models.NewLoadedBug(b.Snapshot()),
+		Operation:        op,
 	}, nil
 }

graphql/resolvers/query.go 🔗

@@ -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

graphql/resolvers/repo.go 🔗

@@ -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,

graphql/schema/mutations.graphql 🔗

@@ -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!
-}

graphql/schema/repository.graphql 🔗

@@ -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!
 

graphql/schema/root.graphql 🔗

@@ -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!
 }

webui/.eslintrc 🔗

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

webui/.eslintrc.js 🔗

@@ -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',
+      },
+    ],
+  },
+};

webui/Makefile 🔗

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

webui/Readme.md 🔗

@@ -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.

webui/codegen.yaml 🔗

@@ -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

webui/package-lock.json 🔗

@@ -114,6 +114,24 @@
         "@apollographql/graphql-language-service-types": "^2.0.0"
       }
     },
+    "@ardatan/graphql-tools": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/@ardatan/graphql-tools/-/graphql-tools-4.1.0.tgz",
+      "integrity": "sha512-0b+KH5RZN9vCMpEjxrwFwZ7v3K6QDjs1EH+R6eRrgKMR2X274JWqYraHKLWE1uJ8iwrkRaOYfCV12jLVuvWS+A==",
+      "dev": true,
+      "requires": {
+        "apollo-link": "^1.2.3",
+        "apollo-utilities": "^1.0.1",
+        "deprecated-decorator": "^0.1.6",
+        "iterall": "^1.1.3",
+        "uuid": "^3.1.0"
+      }
+    },
+    "@arrows/composition": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
+      "integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
+    },
     "@babel/code-frame": {
       "version": "7.8.3",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
@@ -123,36 +141,30 @@
       }
     },
     "@babel/compat-data": {
-      "version": "7.8.1",
-      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.1.tgz",
-      "integrity": "sha512-Z+6ZOXvyOWYxJ50BwxzdhRnRsGST8Y3jaZgxYig575lTjVSs3KtJnmESwZegg6e2Dn0td1eDhoWlp1wI4BTCPw==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.8.4.tgz",
+      "integrity": "sha512-lHLhlsvFjJAqNU71b7k6Vv9ewjmTXKvqaMv7n0G1etdCabWLw3nEYE8mmgoVOxMIFE07xOvo7H7XBASirX6Rrg==",
       "requires": {
-        "browserslist": "^4.8.2",
+        "browserslist": "^4.8.5",
         "invariant": "^2.2.4",
         "semver": "^5.5.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-        }
       }
     },
     "@babel/core": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.7.4.tgz",
-      "integrity": "sha512-+bYbx56j4nYBmpsWtnPUsKW3NdnYxbqyfrP2w9wILBuHzdfIKz9prieZK0DFPyIzkjYVUe4QkusGL07r5pXznQ==",
-      "requires": {
-        "@babel/code-frame": "^7.5.5",
-        "@babel/generator": "^7.7.4",
-        "@babel/helpers": "^7.7.4",
-        "@babel/parser": "^7.7.4",
-        "@babel/template": "^7.7.4",
-        "@babel/traverse": "^7.7.4",
-        "@babel/types": "^7.7.4",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz",
+      "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==",
+      "requires": {
+        "@babel/code-frame": "^7.8.3",
+        "@babel/generator": "^7.8.4",
+        "@babel/helpers": "^7.8.4",
+        "@babel/parser": "^7.8.4",
+        "@babel/template": "^7.8.3",
+        "@babel/traverse": "^7.8.4",
+        "@babel/types": "^7.8.3",
         "convert-source-map": "^1.7.0",
         "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.1",
         "json5": "^2.1.0",
         "lodash": "^4.17.13",
         "resolve": "^1.3.2",
@@ -160,22 +172,50 @@
         "source-map": "^0.5.0"
       },
       "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+        "@babel/parser": {
+          "version": "7.8.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
+          "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw=="
+        },
+        "@babel/traverse": {
+          "version": "7.8.4",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz",
+          "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==",
+          "requires": {
+            "@babel/code-frame": "^7.8.3",
+            "@babel/generator": "^7.8.4",
+            "@babel/helper-function-name": "^7.8.3",
+            "@babel/helper-split-export-declaration": "^7.8.3",
+            "@babel/parser": "^7.8.4",
+            "@babel/types": "^7.8.3",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0",
+            "lodash": "^4.17.13"
+          }
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
         }
       }
     },
     "@babel/generator": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.3.tgz",
-      "integrity": "sha512-WjoPk8hRpDRqqzRpvaR8/gDUPkrnOOeuT2m8cNICJtZH6mwaCo3v0OKMI7Y6SM1pBtyijnLtAL0HDi41pf41ug==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz",
+      "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==",
       "requires": {
         "@babel/types": "^7.8.3",
         "jsesc": "^2.5.1",
         "lodash": "^4.17.13",
         "source-map": "^0.5.0"
+      },
+      "dependencies": {
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+        }
       }
     },
     "@babel/helper-annotate-as-pure": {
@@ -215,22 +255,15 @@
       }
     },
     "@babel/helper-compilation-targets": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.3.tgz",
-      "integrity": "sha512-JLylPCsFjhLN+6uBSSh3iYdxKdeO9MNmoY96PE/99d8kyBFaXLORtAVhqN6iHa+wtPeqxKLghDOZry0+Aiw9Tw==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.4.tgz",
+      "integrity": "sha512-3k3BsKMvPp5bjxgMdrFyq0UaEO48HciVrOVF0+lon8pp95cyJ2ujAh0TrBHNMnJGT2rr0iKOJPFFbSqjDyf/Pg==",
       "requires": {
-        "@babel/compat-data": "^7.8.1",
-        "browserslist": "^4.8.2",
+        "@babel/compat-data": "^7.8.4",
+        "browserslist": "^4.8.5",
         "invariant": "^2.2.4",
-        "levenary": "^1.1.0",
+        "levenary": "^1.1.1",
         "semver": "^5.5.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-        }
       }
     },
     "@babel/helper-create-class-features-plugin": {
@@ -402,13 +435,36 @@
       }
     },
     "@babel/helpers": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.3.tgz",
-      "integrity": "sha512-LmU3q9Pah/XyZU89QvBgGt+BCsTPoQa+73RxAQh8fb8qkDyIfeQnmgs+hvzhTCKTzqOyk7JTkS3MS1S8Mq5yrQ==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz",
+      "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==",
       "requires": {
         "@babel/template": "^7.8.3",
-        "@babel/traverse": "^7.8.3",
+        "@babel/traverse": "^7.8.4",
         "@babel/types": "^7.8.3"
+      },
+      "dependencies": {
+        "@babel/parser": {
+          "version": "7.8.4",
+          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz",
+          "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw=="
+        },
+        "@babel/traverse": {
+          "version": "7.8.4",
+          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz",
+          "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==",
+          "requires": {
+            "@babel/code-frame": "^7.8.3",
+            "@babel/generator": "^7.8.4",
+            "@babel/helper-function-name": "^7.8.3",
+            "@babel/helper-split-export-declaration": "^7.8.3",
+            "@babel/parser": "^7.8.4",
+            "@babel/types": "^7.8.3",
+            "debug": "^4.1.0",
+            "globals": "^11.1.0",
+            "lodash": "^4.17.13"
+          }
+        }
       }
     },
     "@babel/highlight": {
@@ -419,6 +475,18 @@
         "chalk": "^2.0.0",
         "esutils": "^2.0.2",
         "js-tokens": "^4.0.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        }
       }
     },
     "@babel/parser": {
@@ -437,22 +505,22 @@
       }
     },
     "@babel/plugin-proposal-class-properties": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.7.4.tgz",
-      "integrity": "sha512-EcuXeV4Hv1X3+Q1TsuOmyyxeTRiSqurGJ26+I/FW1WbymmRRapVORm6x1Zl3iDIHyRxEs+VXWp6qnlcfcJSbbw==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz",
+      "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==",
       "requires": {
-        "@babel/helper-create-class-features-plugin": "^7.7.4",
-        "@babel/helper-plugin-utils": "^7.0.0"
+        "@babel/helper-create-class-features-plugin": "^7.8.3",
+        "@babel/helper-plugin-utils": "^7.8.3"
       }
     },
     "@babel/plugin-proposal-decorators": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.7.4.tgz",
-      "integrity": "sha512-GftcVDcLCwVdzKmwOBDjATd548+IE+mBo7ttgatqNDR7VG7GqIuZPtRWlMLHbhTXhcnFZiGER8iIYl1n/imtsg==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.8.3.tgz",
+      "integrity": "sha512-e3RvdvS4qPJVTe288DlXjwKflpfy1hr0j5dz5WpIYYeP7vQZg2WfAEIp8k5/Lwis/m5REXEteIz6rrcDtXXG7w==",
       "requires": {
-        "@babel/helper-create-class-features-plugin": "^7.7.4",
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-decorators": "^7.7.4"
+        "@babel/helper-create-class-features-plugin": "^7.8.3",
+        "@babel/helper-plugin-utils": "^7.8.3",
+        "@babel/plugin-syntax-decorators": "^7.8.3"
       }
     },
     "@babel/plugin-proposal-dynamic-import": {
@@ -483,12 +551,12 @@
       }
     },
     "@babel/plugin-proposal-numeric-separator": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.7.4.tgz",
-      "integrity": "sha512-CG605v7lLpVgVldSY6kxsN9ui1DxFOyepBfuX2AzU2TNriMAYApoU55mrGw9Jr4TlrTzPCG10CL8YXyi+E/iPw==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz",
+      "integrity": "sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==",
       "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-numeric-separator": "^7.7.4"
+        "@babel/helper-plugin-utils": "^7.8.3",
+        "@babel/plugin-syntax-numeric-separator": "^7.8.3"
       }
     },
     "@babel/plugin-proposal-object-rest-spread": {
@@ -535,6 +603,15 @@
         "@babel/helper-plugin-utils": "^7.8.0"
       }
     },
+    "@babel/plugin-syntax-class-properties": {
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz",
+      "integrity": "sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==",
+      "dev": true,
+      "requires": {
+        "@babel/helper-plugin-utils": "^7.8.3"
+      }
+    },
     "@babel/plugin-syntax-decorators": {
       "version": "7.8.3",
       "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz",
@@ -724,18 +801,18 @@
       }
     },
     "@babel/plugin-transform-flow-strip-types": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.7.4.tgz",
-      "integrity": "sha512-w9dRNlHY5ElNimyMYy0oQowvQpwt/PRHI0QS98ZJCTZU2bvSnKXo5zEiD5u76FBPigTm8TkqzmnUTg16T7qbkA==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.8.3.tgz",
+      "integrity": "sha512-g/6WTWG/xbdd2exBBzMfygjX/zw4eyNC4X8pRaq7aRHRoDUCzAIu3kGYIXviOv8BjCuWm8vDBwjHcjiRNgXrPA==",
       "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-syntax-flow": "^7.7.4"
+        "@babel/helper-plugin-utils": "^7.8.3",
+        "@babel/plugin-syntax-flow": "^7.8.3"
       }
     },
     "@babel/plugin-transform-for-of": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.3.tgz",
-      "integrity": "sha512-ZjXznLNTxhpf4Q5q3x1NsngzGA38t9naWH8Gt+0qYZEJAcvPI9waSStSh56u19Ofjr7QmD0wUsQ8hw8s/p1VnA==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.8.4.tgz",
+      "integrity": "sha512-iAXNlOWvcYUYoV8YIxwS7TxGRJcxyl8eQCfT+A5j8sKUzRFvJdcyjp97jL2IghWSRDaL2PU2O2tX8Cu9dTBq5A==",
       "requires": {
         "@babel/helper-plugin-utils": "^7.8.3"
       }
@@ -832,9 +909,9 @@
       }
     },
     "@babel/plugin-transform-parameters": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.3.tgz",
-      "integrity": "sha512-/pqngtGb54JwMBZ6S/D3XYylQDFtGjWrnoCF4gXZOUpFV/ujbxnoNGNvDGu6doFWRPBveE72qTx/RRU44j5I/Q==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.8.4.tgz",
+      "integrity": "sha512-IsS3oTxeTsZlE5KqzTbcC2sV0P9pXdec53SU+Yxv7o/6dvGM5AkTotQKhoSffhNgZ/dftsSiOoxy7evCYJXzVA==",
       "requires": {
         "@babel/helper-call-delegate": "^7.8.3",
         "@babel/helper-get-function-arity": "^7.8.3",
@@ -911,21 +988,14 @@
       }
     },
     "@babel/plugin-transform-runtime": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.7.4.tgz",
-      "integrity": "sha512-O8kSkS5fP74Ad/8pfsCMGa8sBRdLxYoSReaARRNSz3FbFQj3z/QUvoUmJ28gn9BO93YfnXc3j+Xyaqe8cKDNBQ==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.8.3.tgz",
+      "integrity": "sha512-/vqUt5Yh+cgPZXXjmaG9NT8aVfThKk7G4OqkVhrXqwsC5soMn/qTCxs36rZ2QFhpfTJcjw4SNDIZ4RUb8OL4jQ==",
       "requires": {
-        "@babel/helper-module-imports": "^7.7.4",
-        "@babel/helper-plugin-utils": "^7.0.0",
+        "@babel/helper-module-imports": "^7.8.3",
+        "@babel/helper-plugin-utils": "^7.8.3",
         "resolve": "^1.8.1",
         "semver": "^5.5.1"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-        }
       }
     },
     "@babel/plugin-transform-shorthand-properties": {
@@ -963,9 +1033,9 @@
       }
     },
     "@babel/plugin-transform-typeof-symbol": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.3.tgz",
-      "integrity": "sha512-3TrkKd4LPqm4jHs6nPtSDI/SV9Cm5PRJkHLUgTcqRQQTMAZ44ZaAdDZJtvWFSaRcvT0a1rTmJ5ZA5tDKjleF3g==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz",
+      "integrity": "sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==",
       "requires": {
         "@babel/helper-plugin-utils": "^7.8.3"
       }
@@ -990,12 +1060,12 @@
       }
     },
     "@babel/preset-env": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.3.tgz",
-      "integrity": "sha512-Rs4RPL2KjSLSE2mWAx5/iCH+GC1ikKdxPrhnRS6PfFVaiZeom22VFKN4X8ZthyN61kAaR05tfXTbCvatl9WIQg==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.8.4.tgz",
+      "integrity": "sha512-HihCgpr45AnSOHRbS5cWNTINs0TwaR8BS8xIIH+QwiW8cKL0llV91njQMpeMReEPVs+1Ao0x3RLEBLtt1hOq4w==",
       "requires": {
-        "@babel/compat-data": "^7.8.0",
-        "@babel/helper-compilation-targets": "^7.8.3",
+        "@babel/compat-data": "^7.8.4",
+        "@babel/helper-compilation-targets": "^7.8.4",
         "@babel/helper-module-imports": "^7.8.3",
         "@babel/helper-plugin-utils": "^7.8.3",
         "@babel/plugin-proposal-async-generator-functions": "^7.8.3",
@@ -1024,7 +1094,7 @@
         "@babel/plugin-transform-dotall-regex": "^7.8.3",
         "@babel/plugin-transform-duplicate-keys": "^7.8.3",
         "@babel/plugin-transform-exponentiation-operator": "^7.8.3",
-        "@babel/plugin-transform-for-of": "^7.8.3",
+        "@babel/plugin-transform-for-of": "^7.8.4",
         "@babel/plugin-transform-function-name": "^7.8.3",
         "@babel/plugin-transform-literals": "^7.8.3",
         "@babel/plugin-transform-member-expression-literals": "^7.8.3",
@@ -1035,7 +1105,7 @@
         "@babel/plugin-transform-named-capturing-groups-regex": "^7.8.3",
         "@babel/plugin-transform-new-target": "^7.8.3",
         "@babel/plugin-transform-object-super": "^7.8.3",
-        "@babel/plugin-transform-parameters": "^7.8.3",
+        "@babel/plugin-transform-parameters": "^7.8.4",
         "@babel/plugin-transform-property-literals": "^7.8.3",
         "@babel/plugin-transform-regenerator": "^7.8.3",
         "@babel/plugin-transform-reserved-words": "^7.8.3",
@@ -1043,21 +1113,14 @@
         "@babel/plugin-transform-spread": "^7.8.3",
         "@babel/plugin-transform-sticky-regex": "^7.8.3",
         "@babel/plugin-transform-template-literals": "^7.8.3",
-        "@babel/plugin-transform-typeof-symbol": "^7.8.3",
+        "@babel/plugin-transform-typeof-symbol": "^7.8.4",
         "@babel/plugin-transform-unicode-regex": "^7.8.3",
         "@babel/types": "^7.8.3",
-        "browserslist": "^4.8.2",
+        "browserslist": "^4.8.5",
         "core-js-compat": "^3.6.2",
         "invariant": "^2.2.2",
-        "levenary": "^1.1.0",
+        "levenary": "^1.1.1",
         "semver": "^5.5.0"
-      },
-      "dependencies": {
-        "semver": {
-          "version": "5.7.1",
-          "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-          "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
-        }
       }
     },
     "@babel/preset-react": {
@@ -1073,12 +1136,12 @@
       }
     },
     "@babel/preset-typescript": {
-      "version": "7.7.4",
-      "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.7.4.tgz",
-      "integrity": "sha512-rqrjxfdiHPsnuPur0jKrIIGQCIgoTWMTjlbWE69G4QJ6TIOVnnRnIJhUxNTL/VwDmEAVX08Tq3B1nirer5341w==",
+      "version": "7.8.3",
+      "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.8.3.tgz",
+      "integrity": "sha512-qee5LgPGui9zQ0jR1TeU5/fP9L+ovoArklEqY12ek8P/wV5ZeM/VYSQYwICeoT6FfpJTekG9Ilay5PhwsOpMHA==",
       "requires": {
-        "@babel/helper-plugin-utils": "^7.0.0",
-        "@babel/plugin-transform-typescript": "^7.7.4"
+        "@babel/helper-plugin-utils": "^7.8.3",
+        "@babel/plugin-transform-typescript": "^7.8.3"
       }
     },
     "@babel/runtime": {
@@ -1090,9 +1153,9 @@
       }
     },
     "@babel/runtime-corejs3": {
-      "version": "7.8.3",
-      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.3.tgz",
-      "integrity": "sha512-lrIU4aVbmlM/wQPzhEvzvNJskKyYptuXb0fGC0lTQTupTOYtR2Vqbu6/jf8vTr4M8Wt1nIzxVrSvPI5qESa/xA==",
+      "version": "7.8.4",
+      "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.8.4.tgz",
+      "integrity": "sha512-+wpLqy5+fbQhvbllvlJEVRIpYj+COUWnnsm+I4jZlA8Lo7/MJmBhGTCHyk1/RWfOqBRJ2MbadddG6QltTKTlrg==",
       "requires": {
         "core-js-pure": "^3.0.0",
         "regenerator-runtime": "^0.13.2"
@@ -1170,24 +1233,55 @@
         "tslib": "^1"
       }
     },
+    "@graphql-codegen/add": {
+      "version": "1.12.2-alpha-ea7264f9.15",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-1.12.2-alpha-ea7264f9.15.tgz",
+      "integrity": "sha512-XfOZH2lIR3qw/mHqXThb32EA7NR37nPJpzuNtx1McGTy0sEEd5PVTLP4u89cgvMXfx18cMMM7ZWAnz2T7XCCkQ==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+        "tslib": "1.10.0"
+      },
+      "dependencies": {
+        "@graphql-codegen/plugin-helpers": {
+          "version": "1.12.2-alpha-ea7264f9.15",
+          "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.2-alpha-ea7264f9.15.tgz",
+          "integrity": "sha512-EgRHQVFswVQUevMEtsrsA45JmTWj3UAUK8laMyDqbQQuOqlTOgpqdceTLYWWCpyfybaEaagw+rWpkwPXyUjWYQ==",
+          "dev": true,
+          "requires": {
+            "@graphql-toolkit/common": "0.9.7",
+            "camel-case": "4.1.1",
+            "common-tags": "1.8.0",
+            "constant-case": "3.0.3",
+            "import-from": "3.0.0",
+            "lower-case": "2.0.1",
+            "param-case": "3.0.3",
+            "pascal-case": "3.1.1",
+            "tslib": "1.10.0",
+            "upper-case": "2.0.1"
+          }
+        }
+      }
+    },
     "@graphql-codegen/cli": {
-      "version": "1.11.2",
-      "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.11.2.tgz",
-      "integrity": "sha512-ox0vqUq+BWngD+UBCpzXuLrOBTPQZza7IGPxKSF3Z7lOPim+NX75HL5sO/58xU8I/5lEjaZpHx3hvSZdiGu4kg==",
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-1.12.1.tgz",
+      "integrity": "sha512-ObbuUaBC48i8glFXcBNo9oBMfv9HZ4JQtP52UX6Ppv/KUOISQWCYvC1YmtWIkuVYRz65Ds2kHpNS9YknVf7eEQ==",
       "dev": true,
       "requires": {
-        "@babel/parser": "7.7.7",
-        "@graphql-codegen/core": "1.11.2",
-        "@graphql-codegen/plugin-helpers": "1.11.2",
-        "@graphql-toolkit/apollo-engine-loader": "0.9.0",
-        "@graphql-toolkit/code-file-loader": "0.9.0",
-        "@graphql-toolkit/core": "0.9.0",
-        "@graphql-toolkit/git-loader": "0.9.0",
-        "@graphql-toolkit/github-loader": "0.9.0",
-        "@graphql-toolkit/graphql-file-loader": "0.9.0",
-        "@graphql-toolkit/json-file-loader": "0.9.0",
-        "@graphql-toolkit/prisma-loader": "0.9.0",
-        "@graphql-toolkit/url-loader": "0.9.0",
+        "@babel/parser": "7.8.3",
+        "@graphql-codegen/core": "1.12.1",
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-toolkit/apollo-engine-loader": "0.9.7",
+        "@graphql-toolkit/code-file-loader": "0.9.7",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/core": "0.9.7",
+        "@graphql-toolkit/git-loader": "0.9.7",
+        "@graphql-toolkit/github-loader": "0.9.7",
+        "@graphql-toolkit/graphql-file-loader": "0.9.7",
+        "@graphql-toolkit/json-file-loader": "0.9.7",
+        "@graphql-toolkit/prisma-loader": "0.9.7",
+        "@graphql-toolkit/url-loader": "0.9.7",
         "@types/debounce": "1.2.0",
         "@types/is-glob": "4.0.1",
         "@types/mkdirp": "0.5.2",
@@ -1202,21 +1296,20 @@
         "debounce": "1.2.0",
         "detect-indent": "6.0.0",
         "glob": "7.1.6",
-        "graphql-config": "3.0.0-alpha.16",
+        "graphql-config": "3.0.0-alpha.18",
         "graphql-import": "0.7.1",
         "graphql-tag-pluck": "0.8.7",
         "graphql-tools": "4.0.6",
         "indent-string": "4.0.0",
-        "inquirer": "7.0.3",
+        "inquirer": "7.0.4",
         "is-glob": "4.0.1",
-        "is-valid-path": "0.1.1",
         "json-to-pretty-yaml": "1.2.2",
         "listr": "0.14.3",
         "listr-update-renderer": "0.5.0",
         "log-symbols": "3.0.0",
         "log-update": "3.3.0",
         "lower-case": "2.0.1",
-        "mkdirp": "0.5.1",
+        "mkdirp": "1.0.3",
         "pascal-case": "3.1.1",
         "prettier": "1.19.1",
         "request": "2.88.0",
@@ -1224,616 +1317,337 @@
         "tslib": "1.10.0",
         "upper-case": "2.0.1",
         "valid-url": "1.0.9"
+      }
+    },
+    "@graphql-codegen/core": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-1.12.1.tgz",
+      "integrity": "sha512-tlGVaqbJ6sWCC28wrNaLGigw7SXPti1lEse0In5+sePZRjbNof8AbNQIICBOgaNxvt9TVfZ80VVM/pCFNCU2vA==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/schema-merging": "0.9.7",
+        "tslib": "1.10.0"
+      }
+    },
+    "@graphql-codegen/fragment-matcher": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/fragment-matcher/-/fragment-matcher-1.12.1.tgz",
+      "integrity": "sha512-WDg1aNiz3/+ksCJ8QEOX6dk7Ki6o0jurdp/Itc2ZjVKbeLypH7omOFonDoM7mi9uq0uDsXk9MioW7wsoIVfHCA==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/plugin-helpers": "1.12.1"
+      }
+    },
+    "@graphql-codegen/introspection": {
+      "version": "1.12.2",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/introspection/-/introspection-1.12.2.tgz",
+      "integrity": "sha512-YMQIn1R2CxYZLkxkW6fUxqfTBr2WC3Cimp6/0oEcSyLA/W1scVuDw/IUeU5Iu4CEmz8IaqPSjSMEN+8l95wKlA==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/plugin-helpers": "1.12.2",
+        "tslib": "1.10.0"
       },
       "dependencies": {
-        "@babel/parser": {
-          "version": "7.7.7",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz",
-          "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==",
-          "dev": true
-        },
-        "ansi-styles": {
-          "version": "4.2.1",
-          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
-          "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
-          "dev": true,
-          "requires": {
-            "@types/color-name": "^1.1.1",
-            "color-convert": "^2.0.1"
-          }
-        },
-        "anymatch": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
-          "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
-          "dev": true,
-          "requires": {
-            "normalize-path": "^3.0.0",
-            "picomatch": "^2.0.4"
-          }
-        },
-        "binary-extensions": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
-          "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
-          "dev": true
-        },
-        "braces": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-          "dev": true,
-          "requires": {
-            "fill-range": "^7.0.1"
-          }
-        },
-        "camel-case": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz",
-          "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==",
-          "dev": true,
-          "requires": {
-            "pascal-case": "^3.1.1",
-            "tslib": "^1.10.0"
-          }
-        },
-        "chalk": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
-          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
-          "dev": true,
-          "requires": {
-            "ansi-styles": "^4.1.0",
-            "supports-color": "^7.1.0"
-          }
-        },
-        "chokidar": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz",
-          "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==",
-          "dev": true,
-          "requires": {
-            "anymatch": "~3.1.1",
-            "braces": "~3.0.2",
-            "fsevents": "~2.1.2",
-            "glob-parent": "~5.1.0",
-            "is-binary-path": "~2.1.0",
-            "is-glob": "~4.0.1",
-            "normalize-path": "~3.0.0",
-            "readdirp": "~3.3.0"
-          }
-        },
-        "color-convert": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-          "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-          "dev": true,
-          "requires": {
-            "color-name": "~1.1.4"
-          }
-        },
-        "color-name": {
-          "version": "1.1.4",
-          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-          "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-          "dev": true
-        },
-        "commander": {
-          "version": "4.1.0",
-          "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.0.tgz",
-          "integrity": "sha512-NIQrwvv9V39FHgGFm36+U9SMQzbiHvU79k+iADraJTpmrFFfx7Ds0IvDoAdZsDrknlkRk14OYoWXb57uTh7/sw==",
-          "dev": true
-        },
-        "cosmiconfig": {
-          "version": "6.0.0",
-          "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz",
-          "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==",
-          "dev": true,
-          "requires": {
-            "@types/parse-json": "^4.0.0",
-            "import-fresh": "^3.1.0",
-            "parse-json": "^5.0.0",
-            "path-type": "^4.0.0",
-            "yaml": "^1.7.2"
-          }
-        },
-        "fill-range": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-          "dev": true,
-          "requires": {
-            "to-regex-range": "^5.0.1"
-          }
-        },
-        "has-flag": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-          "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-          "dev": true
-        },
-        "import-fresh": {
-          "version": "3.2.1",
-          "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
-          "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
+        "@graphql-codegen/plugin-helpers": {
+          "version": "1.12.2",
+          "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.2.tgz",
+          "integrity": "sha512-N294rqdBh+mCi4HWHbhPV9wE0XLCVKx524pYL4yp8qWiSdAs3Iz9+q9C9QNsLBvHypZdqml44M8kBMG41A9I/Q==",
           "dev": true,
           "requires": {
-            "parent-module": "^1.0.0",
-            "resolve-from": "^4.0.0"
+            "@graphql-toolkit/common": "0.9.7",
+            "camel-case": "4.1.1",
+            "common-tags": "1.8.0",
+            "constant-case": "3.0.3",
+            "import-from": "3.0.0",
+            "lower-case": "2.0.1",
+            "param-case": "3.0.3",
+            "pascal-case": "3.1.1",
+            "tslib": "1.10.0",
+            "upper-case": "2.0.1"
           }
-        },
-        "is-binary-path": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-          "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+        }
+      }
+    },
+    "@graphql-codegen/near-operation-file-preset": {
+      "version": "1.12.2-alpha-ea7264f9.15",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/near-operation-file-preset/-/near-operation-file-preset-1.12.2-alpha-ea7264f9.15.tgz",
+      "integrity": "sha512-jbj7+2FlHRLpqN3e44EZ88n2juImhMuXzv6Mlun4CEVkxC8zW6MYkptaeAxb+iCn2r2nO3vXNrNEPs/1czF97w==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/add": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+        "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+        "@graphql-codegen/visitor-plugin-common": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+        "tslib": "1.10.0"
+      },
+      "dependencies": {
+        "@graphql-codegen/plugin-helpers": {
+          "version": "1.12.2-alpha-ea7264f9.15",
+          "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.2-alpha-ea7264f9.15.tgz",
+          "integrity": "sha512-EgRHQVFswVQUevMEtsrsA45JmTWj3UAUK8laMyDqbQQuOqlTOgpqdceTLYWWCpyfybaEaagw+rWpkwPXyUjWYQ==",
           "dev": true,
           "requires": {
-            "binary-extensions": "^2.0.0"
-          }
-        },
-        "is-number": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-          "dev": true
-        },
-        "lower-case": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz",
-          "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==",
+            "@graphql-toolkit/common": "0.9.7",
+            "camel-case": "4.1.1",
+            "common-tags": "1.8.0",
+            "constant-case": "3.0.3",
+            "import-from": "3.0.0",
+            "lower-case": "2.0.1",
+            "param-case": "3.0.3",
+            "pascal-case": "3.1.1",
+            "tslib": "1.10.0",
+            "upper-case": "2.0.1"
+          }
+        },
+        "@graphql-codegen/visitor-plugin-common": {
+          "version": "1.12.2-alpha-ea7264f9.15",
+          "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.12.2-alpha-ea7264f9.15.tgz",
+          "integrity": "sha512-Y+4b5ArGOcXtGZ7gCLKhfOfiElH36uNSYs/8y0+9bxbjV1OuGfunnluysvpDSqIqatyVXviJh+P832VjO5Cviw==",
           "dev": true,
           "requires": {
-            "tslib": "^1.10.0"
-          }
-        },
-        "normalize-path": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-          "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-          "dev": true
-        },
-        "parse-json": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz",
-          "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.0.0",
-            "error-ex": "^1.3.1",
-            "json-parse-better-errors": "^1.0.1",
-            "lines-and-columns": "^1.1.6"
-          }
-        },
-        "path-type": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-          "dev": true
-        },
-        "readdirp": {
-          "version": "3.3.0",
-          "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
-          "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==",
-          "dev": true,
-          "requires": {
-            "picomatch": "^2.0.7"
-          }
-        },
-        "resolve-from": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
-          "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
-          "dev": true
-        },
-        "supports-color": {
-          "version": "7.1.0",
-          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
-          "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
-          "dev": true,
-          "requires": {
-            "has-flag": "^4.0.0"
-          }
-        },
-        "to-regex-range": {
-          "version": "5.0.1",
-          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-          "dev": true,
-          "requires": {
-            "is-number": "^7.0.0"
-          }
-        },
-        "upper-case": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz",
-          "integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==",
-          "dev": true,
-          "requires": {
-            "tslib": "^1.10.0"
+            "@graphql-codegen/plugin-helpers": "1.12.2-alpha-ea7264f9.15+ea7264f9",
+            "@graphql-toolkit/relay-operation-optimizer": "0.9.7",
+            "auto-bind": "4.0.0",
+            "dependency-graph": "0.8.1",
+            "graphql-tag": "2.10.1",
+            "pascal-case": "3.1.1",
+            "tslib": "1.10.0"
           }
         }
       }
     },
-    "@graphql-codegen/core": {
-      "version": "1.11.2",
-      "resolved": "https://registry.npmjs.org/@graphql-codegen/core/-/core-1.11.2.tgz",
-      "integrity": "sha512-ZEHbCtivUQXXPkTd+vrb6sSmCss45Z7YjeyC1mf0kStcEeAKygs6XM2k7F5a9wUQn3mxnyJRAnqfqNIdoagoUg==",
+    "@graphql-codegen/plugin-helpers": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.12.1.tgz",
+      "integrity": "sha512-mrc+trorCTuJYeD0gJLXO7xxabAyOmwXvDkPTxecWNfw5oJIloKsIorWkK00ZFbPHjorzJlOgON8vgXljREzKA==",
+      "dev": true,
+      "requires": {
+        "@graphql-toolkit/common": "0.9.7",
+        "camel-case": "4.1.1",
+        "common-tags": "1.8.0",
+        "constant-case": "3.0.3",
+        "import-from": "3.0.0",
+        "lower-case": "2.0.1",
+        "param-case": "3.0.3",
+        "pascal-case": "3.1.1",
+        "tslib": "1.10.0",
+        "upper-case": "2.0.1"
+      }
+    },
+    "@graphql-codegen/typescript": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript/-/typescript-1.12.1.tgz",
+      "integrity": "sha512-j2iQrxSIMxF6MlzJ5OvnawRyewfEHlC21It+Z6Md2z7yJK5T2bYLs4jd0RkzDGVezodnpd0TzV/Yv+jGxdMjSQ==",
       "dev": true,
       "requires": {
-        "@graphql-codegen/plugin-helpers": "1.11.2",
-        "@graphql-toolkit/common": "0.9.0",
-        "@graphql-toolkit/schema-merging": "0.9.0",
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-codegen/visitor-plugin-common": "1.12.1",
+        "auto-bind": "4.0.0",
         "tslib": "1.10.0"
       }
     },
-    "@graphql-codegen/fragment-matcher": {
-      "version": "1.11.2",
-      "resolved": "https://registry.npmjs.org/@graphql-codegen/fragment-matcher/-/fragment-matcher-1.11.2.tgz",
-      "integrity": "sha512-pLXTwf1dN+CmeZb+5fIjVq41ygPFgQ4rnj6S5N/efHY8AYuGROS58+YXFjU4Nx0eMFj18ymsrrTBWEr2Xug7ZQ==",
+    "@graphql-codegen/typescript-operations": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-operations/-/typescript-operations-1.12.1.tgz",
+      "integrity": "sha512-lGF5wzi5Rndca4YoQCTczRamBrKkieuLIMW3/WT+t3s9wwTkC802HAuNLmUW9CUO+lcArrXV23qpyi9hElKZAA==",
       "dev": true,
       "requires": {
-        "@graphql-codegen/plugin-helpers": "1.11.2"
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-codegen/typescript": "1.12.1",
+        "@graphql-codegen/visitor-plugin-common": "1.12.1",
+        "auto-bind": "4.0.0",
+        "tslib": "1.10.0"
       }
     },
-    "@graphql-codegen/plugin-helpers": {
-      "version": "1.11.2",
-      "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.11.2.tgz",
-      "integrity": "sha512-jggDX2ykLU8EOdP8bpArkMtPTvJ72XYWa44f0GFIdhfSfZtK8PO/gMaSY8iPbV5DqD4HnYvMc3mXCoJTAPT8VQ==",
+    "@graphql-codegen/typescript-react-apollo": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/typescript-react-apollo/-/typescript-react-apollo-1.12.1.tgz",
+      "integrity": "sha512-uy6R2M6EvLXu9BWA9untZBpE+64OUnC2zjV0TO2cK6tz1Ktut9pyC7F4BNGoFHfTvmx80dxtKfwbqdmP48mddQ==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-codegen/visitor-plugin-common": "1.12.1",
+        "auto-bind": "4.0.0",
         "camel-case": "4.1.1",
-        "common-tags": "1.8.0",
-        "constant-case": "3.0.3",
-        "import-from": "3.0.0",
-        "lower-case": "2.0.1",
-        "param-case": "3.0.3",
         "pascal-case": "3.1.1",
-        "tslib": "1.10.0",
-        "upper-case": "2.0.1"
-      },
-      "dependencies": {
-        "camel-case": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz",
-          "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==",
-          "dev": true,
-          "requires": {
-            "pascal-case": "^3.1.1",
-            "tslib": "^1.10.0"
-          }
-        },
-        "import-from": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
-          "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
-          "dev": true,
-          "requires": {
-            "resolve-from": "^5.0.0"
-          }
-        },
-        "lower-case": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz",
-          "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==",
-          "dev": true,
-          "requires": {
-            "tslib": "^1.10.0"
-          }
-        },
-        "param-case": {
-          "version": "3.0.3",
-          "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz",
-          "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==",
-          "dev": true,
-          "requires": {
-            "dot-case": "^3.0.3",
-            "tslib": "^1.10.0"
-          }
-        },
-        "resolve-from": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
-          "dev": true
-        },
-        "upper-case": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.1.tgz",
-          "integrity": "sha512-laAsbea9SY5osxrv7S99vH9xAaJKrw5Qpdh4ENRLcaxipjKsiaBwiAsxfa8X5mObKNTQPsupSq0J/VIxsSJe3A==",
-          "dev": true,
-          "requires": {
-            "tslib": "^1.10.0"
-          }
-        }
+        "tslib": "1.10.0"
+      }
+    },
+    "@graphql-codegen/visitor-plugin-common": {
+      "version": "1.12.1",
+      "resolved": "https://registry.npmjs.org/@graphql-codegen/visitor-plugin-common/-/visitor-plugin-common-1.12.1.tgz",
+      "integrity": "sha512-dfcb0d1u0bGo7KgaGoWMUrFec9rKB5JiiFatHfs2CK6h6iT6giqkuyHy8247tH3yvjk+xFoIG2NVIqg9jUyv+w==",
+      "dev": true,
+      "requires": {
+        "@graphql-codegen/plugin-helpers": "1.12.1",
+        "@graphql-toolkit/relay-operation-optimizer": "0.9.7",
+        "auto-bind": "4.0.0",
+        "dependency-graph": "0.8.1",
+        "graphql-tag": "2.10.1",
+        "pascal-case": "3.1.1",
+        "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/apollo-engine-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/apollo-engine-loader/-/apollo-engine-loader-0.9.0.tgz",
-      "integrity": "sha512-CryB0LklKpMFSWTjp9YpFabrL7DZZsSzSt4jzY1f+cHC8Em/2icTJAe102NzUMxljKxVGo/FeAJIxOaV1hKVFQ==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/apollo-engine-loader/-/apollo-engine-loader-0.9.7.tgz",
+      "integrity": "sha512-gIrkMUOkkY1yIXxyR3K9e8xwj+sR4WHlCUWzjk5UTuReCcv4CZlm8omEqQjeZrPg9Naj+D2velKYxhgXhxm2ZQ==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "apollo-language-server": "1.17.2",
+        "@graphql-toolkit/common": "0.9.7",
+        "apollo-language-server": "1.18.0",
         "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/code-file-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/code-file-loader/-/code-file-loader-0.9.0.tgz",
-      "integrity": "sha512-Aswx0oFctDhzg53jY1nwyMwRl/NLYB8jP0Du2SQkXIeSWy5UDqMdMa48VQgwERnmXv13OIqeSR4Oh+Ri30bhyw==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/code-file-loader/-/code-file-loader-0.9.7.tgz",
+      "integrity": "sha512-Vs2E7ojJ2gmhTz+U0MRLMib8yPz4+U1THCE3QgP4Pqnrqnkp/kEI7qpt3G3XI7JRRBWDHU2gdwOydnHmM/Cjow==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "@graphql-toolkit/graphql-tag-pluck": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/graphql-tag-pluck": "0.9.7",
         "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/common": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/common/-/common-0.9.0.tgz",
-      "integrity": "sha512-bLuyt4yV/XIHUS+gP4aF5xjnb5M2K+uuB35Hojw0er+tkNhWiOuWQzRMWPovds/4WN2C9PuknQby/+ntgBOm/g==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/common/-/common-0.9.7.tgz",
+      "integrity": "sha512-dpSRBMLeIiRct2gkjj24bp0EV7hbK/7dpJAPqNgvDH2535LhOkprYiCXQJyP4N1LODAEkpN/zzlJfKMVn773MQ==",
       "dev": true,
       "requires": {
-        "@kamilkisiela/graphql-tools": "4.0.6",
+        "@ardatan/graphql-tools": "4.1.0",
         "aggregate-error": "3.0.1",
         "lodash": "4.17.15"
       }
     },
     "@graphql-toolkit/core": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/core/-/core-0.9.0.tgz",
-      "integrity": "sha512-/0Q9S4uw27D/Q1zJsfswrsMHO2cTsvBU9G9tVxQKSI5WjHQGNJUOn0mA+Fl0bfQAjRDoC0p4BSsH1PcS5yagPA==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/core/-/core-0.9.7.tgz",
+      "integrity": "sha512-w1WU0iOq6AEBTICDxcu1xjFruFfGCHg6ERdWTWdIBOTn30qysIC0ek+XWN67vF9yV9QIdAxNu66gXKjUUWm2Tg==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "@graphql-toolkit/schema-merging": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/schema-merging": "0.9.7",
         "aggregate-error": "3.0.1",
         "globby": "11.0.0",
+        "import-from": "^3.0.0",
         "is-glob": "4.0.1",
+        "lodash": "4.17.15",
         "resolve-from": "5.0.0",
         "tslib": "1.10.0",
         "unixify": "1.0.0",
         "valid-url": "1.0.9"
-      },
-      "dependencies": {
-        "@nodelib/fs.stat": {
-          "version": "2.0.3",
-          "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz",
-          "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==",
-          "dev": true
-        },
-        "array-union": {
-          "version": "2.1.0",
-          "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
-          "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
-          "dev": true
-        },
-        "braces": {
-          "version": "3.0.2",
-          "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-          "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-          "dev": true,
-          "requires": {
-            "fill-range": "^7.0.1"
-          }
-        },
-        "dir-glob": {
-          "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
-          "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
-          "dev": true,
-          "requires": {
-            "path-type": "^4.0.0"
-          }
-        },
-        "fast-glob": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz",
-          "integrity": "sha512-nTCREpBY8w8r+boyFYAx21iL6faSsQynliPHM4Uf56SbkyohCNxpVPEH9xrF5TXKy+IsjkPUHDKiUkzBVRXn9g==",
-          "dev": true,
-          "requires": {
-            "@nodelib/fs.stat": "^2.0.2",
-            "@nodelib/fs.walk": "^1.2.3",
-            "glob-parent": "^5.1.0",
-            "merge2": "^1.3.0",
-            "micromatch": "^4.0.2"
-          }
-        },
-        "fill-range": {
-          "version": "7.0.1",
-          "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-          "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-          "dev": true,
-          "requires": {
-            "to-regex-range": "^5.0.1"
-          }
-        },
-        "globby": {
-          "version": "11.0.0",
-          "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.0.tgz",
-          "integrity": "sha512-iuehFnR3xu5wBBtm4xi0dMe92Ob87ufyu/dHwpDYfbcpYpIbrO5OnS8M1vWvrBhSGEJ3/Ecj7gnX76P8YxpPEg==",
-          "dev": true,
-          "requires": {
-            "array-union": "^2.1.0",
-            "dir-glob": "^3.0.1",
-            "fast-glob": "^3.1.1",
-            "ignore": "^5.1.4",
-            "merge2": "^1.3.0",
-            "slash": "^3.0.0"
-          }
-        },
-        "ignore": {
-          "version": "5.1.4",
-          "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.4.tgz",
-          "integrity": "sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==",
-          "dev": true
-        },
-        "is-number": {
-          "version": "7.0.0",
-          "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-          "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-          "dev": true
-        },
-        "micromatch": {
-          "version": "4.0.2",
-          "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz",
-          "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==",
-          "dev": true,
-          "requires": {
-            "braces": "^3.0.1",
-            "picomatch": "^2.0.5"
-          }
-        },
-        "path-type": {
-          "version": "4.0.0",
-          "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
-          "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
-          "dev": true
-        },
-        "resolve-from": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
-          "dev": true
-        },
-        "slash": {
-          "version": "3.0.0",
-          "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
-          "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
-          "dev": true
-        },
-        "to-regex-range": {
-          "version": "5.0.1",
-          "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-          "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-          "dev": true,
-          "requires": {
-            "is-number": "^7.0.0"
-          }
-        }
+      }
+    },
+    "@graphql-toolkit/file-loading": {
+      "version": "0.9.1",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/file-loading/-/file-loading-0.9.1.tgz",
+      "integrity": "sha512-jiN2cvWjBR4ubwesU+IlpSCyXBZ1E7Xl5rakq63mFT9zVR5w0kEbO7Bs1nmIGlVbSvjbCzmn4d/kzrpgfwL7UQ==",
+      "dev": true,
+      "requires": {
+        "globby": "11.0.0",
+        "unixify": "1.0.0"
       }
     },
     "@graphql-toolkit/git-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/git-loader/-/git-loader-0.9.0.tgz",
-      "integrity": "sha512-B3QpM3YQhekjrLVIBev8qi95HK6As/gezjfdwT75sTYZ9+VgJwU2MDo08EKUWjHU1KoV1MDBGJNOtzopiogUlA==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/git-loader/-/git-loader-0.9.7.tgz",
+      "integrity": "sha512-/uJa6kudtPv/M8iPlGpm87mnJIAYQ5hcNG8wPY4frgol5a13Jvq9ro8jvaCueTa+kpybgUtVl4ZuCqsGa2P6CA==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "simple-git": "1.129.0"
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/graphql-tag-pluck": "0.9.7",
+        "simple-git": "1.131.0"
       }
     },
     "@graphql-toolkit/github-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/github-loader/-/github-loader-0.9.0.tgz",
-      "integrity": "sha512-srLPYVXkqZkLzJsqRo+IcLAu+HfRm6QXjLNrn6XOUb0JLmqZnXoFOMGG9k3rlHO8mPm36F+bkq2hE4tmk/mW5A==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/github-loader/-/github-loader-0.9.7.tgz",
+      "integrity": "sha512-30f0IMCn/abufu05YGiLuTdzwZk7N93zY+AEfEx7nSCpxVZYu7FSWuDxYxdMFQ2eWJvFXrh6u0As+TXB0njHZA==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/graphql-tag-pluck": "0.9.7",
         "cross-fetch": "3.0.4"
       }
     },
     "@graphql-toolkit/graphql-file-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/graphql-file-loader/-/graphql-file-loader-0.9.0.tgz",
-      "integrity": "sha512-mUgmjCF1oLhYbbQC0bcX/5DdQJNJ63Pmm8JCyPhgZOy2pdbLwLKQj6KnWznny2VOGF63jjTiGUp1JfxsvYpXmQ==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/graphql-file-loader/-/graphql-file-loader-0.9.7.tgz",
+      "integrity": "sha512-t7CfYjghuXAtIqzwHhkUoE/u0a918UTOOVtHdLHh8rojjIUfsSeLeqMcFacRv+/z+kyKl9lgi4TE/qiyIpyR5A==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
         "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/graphql-tag-pluck": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/graphql-tag-pluck/-/graphql-tag-pluck-0.9.0.tgz",
-      "integrity": "sha512-MiMxyBM4DK+7hZNjdWeiNTw/K/8M/gSHkEPtbSGaCn0csy9GYeLM6ojz2JDdg0rxNT4Zb9Z1DzQcauMpnX3tmA==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/graphql-tag-pluck/-/graphql-tag-pluck-0.9.7.tgz",
+      "integrity": "sha512-hfHs9m/6rK0JRPrZg9LW8fPmQiNy7zvueey+TUH+qLHOkQiklDVDtQ/2yje35B16bwiyk3axxmHZ/H3fb5nWiQ==",
       "dev": true,
       "requires": {
-        "@babel/parser": "7.7.7",
-        "@babel/traverse": "7.7.4",
-        "@babel/types": "7.7.4",
-        "vue-template-compiler": "^2.6.10"
-      },
-      "dependencies": {
-        "@babel/parser": {
-          "version": "7.7.7",
-          "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.7.7.tgz",
-          "integrity": "sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==",
-          "dev": true
-        },
-        "@babel/traverse": {
-          "version": "7.7.4",
-          "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.7.4.tgz",
-          "integrity": "sha512-P1L58hQyupn8+ezVA2z5KBm4/Zr4lCC8dwKCMYzsa5jFMDMQAzaBNy9W5VjB+KAmBjb40U7a/H6ao+Xo+9saIw==",
-          "dev": true,
-          "requires": {
-            "@babel/code-frame": "^7.5.5",
-            "@babel/generator": "^7.7.4",
-            "@babel/helper-function-name": "^7.7.4",
-            "@babel/helper-split-export-declaration": "^7.7.4",
-            "@babel/parser": "^7.7.4",
-            "@babel/types": "^7.7.4",
-            "debug": "^4.1.0",
-            "globals": "^11.1.0",
-            "lodash": "^4.17.13"
-          }
-        },
-        "@babel/types": {
-          "version": "7.7.4",
-          "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.4.tgz",
-          "integrity": "sha512-cz5Ji23KCi4T+YIE/BolWosrJuSmoZeN1EFnRtBwF+KKLi8GG/Z2c2hOJJeCXPk4mwk4QFvTmwIodJowXgttRA==",
-          "dev": true,
-          "requires": {
-            "esutils": "^2.0.2",
-            "lodash": "^4.17.13",
-            "to-fast-properties": "^2.0.0"
-          }
-        }
+        "@babel/parser": "7.8.3",
+        "@babel/traverse": "7.8.3",
+        "@babel/types": "7.8.3",
+        "@graphql-toolkit/common": "0.9.7",
+        "vue-template-compiler": "^2.6.11"
       }
     },
     "@graphql-toolkit/json-file-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/json-file-loader/-/json-file-loader-0.9.0.tgz",
-      "integrity": "sha512-YyS7HP0gVj3SuyjFAu550BUi7nQ7m++OF8ZOsEkV6KHObrKXRF4GYocffRDcdCefb02MjjrkLwcXzvMYNq8dKQ==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/json-file-loader/-/json-file-loader-0.9.7.tgz",
+      "integrity": "sha512-MNnCX201p011FPOm/rlDLkBTpx4LvooG9pdMU1ijRD/sqpHSkhZ2U/aKyoiDDKrLUgK7cvHws1KXBvLcg7r6aQ==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
         "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/prisma-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/prisma-loader/-/prisma-loader-0.9.0.tgz",
-      "integrity": "sha512-+vQgKp+7hhcd5DNv/fB9OQcgoThqiHflaI7UzKOPikUf24wYRNknZiAwie+9u3IP8NEymvnFaOy2nUQxqRCc3w==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/prisma-loader/-/prisma-loader-0.9.7.tgz",
+      "integrity": "sha512-SSl7bqGomTzhpXORHSEa4fThF6XC2zOuHJlOqwxGMdsbkouogR77vn7OXlCxRhNxClfBF/f7nSQdSOONVlFBvw==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "@graphql-toolkit/url-loader": "0.9.0",
+        "@graphql-toolkit/common": "0.9.7",
+        "@graphql-toolkit/url-loader": "0.9.7",
         "prisma-yml": "1.34.10",
         "tslib": "1.10.0"
       }
     },
+    "@graphql-toolkit/relay-operation-optimizer": {
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/relay-operation-optimizer/-/relay-operation-optimizer-0.9.7.tgz",
+      "integrity": "sha512-IPFAbKMOX3RdjyDuamK9ziuTFD5tsCiTVvHACHA2wgg+32krJZJsV6STKhFLqIwytS40vt5zhZydQCFxIrCD5g==",
+      "dev": true,
+      "requires": {
+        "@graphql-toolkit/common": "0.9.7",
+        "relay-compiler": "8.0.0"
+      }
+    },
     "@graphql-toolkit/schema-merging": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/schema-merging/-/schema-merging-0.9.0.tgz",
-      "integrity": "sha512-vrzkqkFXxZ4dXQrHeNGDDWONbOAVDeJmGPwK0cRu2aVszftvkYVJXBrmkMYzZJHwk+tGVkNywf1r00GR6prpOw==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/schema-merging/-/schema-merging-0.9.7.tgz",
+      "integrity": "sha512-RLhP0+XT4JGoPGCvlcTPdCE8stA7l0D5+gZ8ZP0snqzZOdsDFG4cNxpJtwf48i7uArsXkfu5OjOvTwh0MR0Wrw==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
-        "@kamilkisiela/graphql-tools": "4.0.6",
+        "@ardatan/graphql-tools": "4.1.0",
+        "@graphql-toolkit/common": "0.9.7",
         "deepmerge": "4.2.2",
         "tslib": "1.10.0"
       }
     },
     "@graphql-toolkit/url-loader": {
-      "version": "0.9.0",
-      "resolved": "https://registry.npmjs.org/@graphql-toolkit/url-loader/-/url-loader-0.9.0.tgz",
-      "integrity": "sha512-umz+V9KbFv4oCWjZWiiIIH2PQrdK7tPxs8vbLdNy+EvdQrFSV5lJSknfsusyAoJfOpH+W294Ugfs0ySH5jZDxw==",
+      "version": "0.9.7",
+      "resolved": "https://registry.npmjs.org/@graphql-toolkit/url-loader/-/url-loader-0.9.7.tgz",
+      "integrity": "sha512-cOT2XJVZLWOKG4V9ucVtUTqJMW0BJqEqrHvpR8YcIWffrEChmzZQX+ug3BkRNomaUe8ywgExJ80aZuKWeSHvew==",
       "dev": true,
       "requires": {
-        "@graphql-toolkit/common": "0.9.0",
+        "@ardatan/graphql-tools": "4.1.0",
+        "@graphql-toolkit/common": "0.9.7",
         "cross-fetch": "3.0.4",
         "tslib": "1.10.0",
         "valid-url": "1.0.9"

webui/package.json 🔗

@@ -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": [

webui/src/App.js → webui/src/App.tsx 🔗

@@ -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>

webui/src/Author.js → webui/src/Author.tsx 🔗

@@ -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} />;
   }

webui/src/Content.js → webui/src/Content.tsx 🔗

@@ -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;

webui/src/CurrentIdentity.js 🔗

@@ -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;

webui/src/CurrentIdentity.tsx 🔗

@@ -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;

webui/src/Date.js → webui/src/Date.tsx 🔗

@@ -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>

webui/src/Label.js → webui/src/Label.tsx 🔗

@@ -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;

webui/src/bug/Bug.graphql 🔗

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

webui/src/bug/Bug.js → webui/src/bug/Bug.tsx 🔗

@@ -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;

webui/src/bug/BugQuery.js 🔗

@@ -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;

webui/src/bug/BugQuery.tsx 🔗

@@ -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;

webui/src/bug/LabelChange.js → webui/src/bug/LabelChange.tsx 🔗

@@ -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;

webui/src/bug/LabelChangeFragment.graphql 🔗

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

webui/src/bug/Message.js → webui/src/bug/Message.tsx 🔗

@@ -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;

webui/src/bug/SetStatus.js → webui/src/bug/SetStatus.tsx 🔗

@@ -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;

webui/src/bug/SetTitle.js → webui/src/bug/SetTitle.tsx 🔗

@@ -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;

webui/src/bug/Timeline.js 🔗

@@ -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;

webui/src/bug/Timeline.tsx 🔗

@@ -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;

webui/src/bug/TimelineQuery.graphql 🔗

@@ -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
+  }
+}

webui/src/bug/TimelineQuery.js 🔗

@@ -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;

webui/src/bug/TimelineQuery.tsx 🔗

@@ -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;

webui/src/index.js → webui/src/index.tsx 🔗

@@ -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();
 

webui/src/list/BugRow.graphql 🔗

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

webui/src/list/BugRow.js → webui/src/list/BugRow.tsx 🔗

@@ -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;

webui/src/list/Filter.js → webui/src/list/Filter.tsx 🔗

@@ -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) {
+  if (to) {
     return (
-      <Dropdown
-        {...props}
+      <Link
         to={to}
-        dropdown={dropdown}
-        className={classes.element}
+        className={clsx(classes.element, active && classes.itemActive)}
       >
         {content}
-      </Dropdown>
-    );
-  }
-
-  if (to) {
-    return (
-      <Link to={to} {...props} className={classes.element}>
-        {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 };

webui/src/list/FilterToolbar.js → webui/src/list/FilterToolbar.tsx 🔗

@@ -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>
   );
 }

webui/src/list/List.js → webui/src/list/List.tsx 🔗

@@ -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';
+import { BugListFragment } from './ListQuery.generated';
 
-function List({ bugs }) {
+type Props = { bugs: BugListFragment };
+function List({ bugs }: Props) {
   return (
     <Table>
       <TableBody>

webui/src/list/ListQuery.graphql 🔗

@@ -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
+    }
+  }
+}

webui/src/list/ListQuery.js → webui/src/list/ListQuery.tsx 🔗

@@ -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';
+import List from './List';
+import { useListBugsQuery } from './ListQuery.generated';
 
-const useStyles = makeStyles(theme => ({
+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>
   );

webui/src/tag/ImageTag.js → webui/src/tag/ImageTag.tsx 🔗

@@ -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">

webui/src/tag/PreTag.js → webui/src/tag/PreTag.tsx 🔗

@@ -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>;
 };

webui/tsconfig.json 🔗

@@ -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"]
+}