mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 17:51:44 +03:00
Merge remote-tracking branch 'origin/master' into cheshirekow-jira
This commit is contained in:
commit
e9aff2a2a1
24
.travis.yml
24
.travis.yml
@ -7,32 +7,12 @@ matrix:
|
||||
- language: go
|
||||
go: 1.13.x
|
||||
- language: node_js
|
||||
node_js: 8
|
||||
node_js: node
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
- language: node_js
|
||||
node_js: 9
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
- language: node_js
|
||||
node_js: 10
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
- language: node_js
|
||||
node_js: 11
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
- language: node_js
|
||||
node_js: 12
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
- language: node_js
|
||||
node_js: 13
|
||||
node_js: lts/*
|
||||
before_install:
|
||||
- cd webui
|
||||
after_success: []
|
||||
|
@ -44,13 +44,13 @@ That's all !
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>go get (unstable)</summary>
|
||||
<details><summary>go install (unstable)</summary>
|
||||
|
||||
```shell
|
||||
go get -u github.com/MichaelMure/git-bug
|
||||
go install github.com/MichaelMure/git-bug
|
||||
```
|
||||
|
||||
If it's not done already, add golang binary directory in your PATH:
|
||||
If it's not done already, add the golang binary directory in your PATH:
|
||||
|
||||
```bash
|
||||
export PATH=$PATH:$(go env GOROOT)/bin:$(go env GOPATH)/bin
|
||||
|
@ -1,6 +1,8 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
@ -16,15 +18,18 @@ const (
|
||||
configKeyKind = "kind"
|
||||
configKeyTarget = "target"
|
||||
configKeyCreateTime = "createtime"
|
||||
configKeySalt = "salt"
|
||||
configKeyPrefixMeta = "meta."
|
||||
|
||||
MetaKeyLogin = "login"
|
||||
MetaKeyLogin = "login"
|
||||
MetaKeyBaseURL = "base-url"
|
||||
)
|
||||
|
||||
type CredentialKind string
|
||||
|
||||
const (
|
||||
KindToken CredentialKind = "token"
|
||||
KindLogin CredentialKind = "login"
|
||||
KindLoginPassword CredentialKind = "login-password"
|
||||
)
|
||||
|
||||
@ -36,9 +41,10 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
|
||||
|
||||
type Credential interface {
|
||||
ID() entity.Id
|
||||
Target() string
|
||||
Kind() CredentialKind
|
||||
Target() string
|
||||
CreateTime() time.Time
|
||||
Salt() []byte
|
||||
Validate() error
|
||||
|
||||
Metadata() map[string]string
|
||||
@ -46,7 +52,7 @@ type Credential interface {
|
||||
SetMetadata(key string, value string)
|
||||
|
||||
// Return all the specific properties of the credential that need to be saved into the configuration.
|
||||
// This does not include Target, Kind, CreateTime and Metadata.
|
||||
// This does not include Target, Kind, CreateTime, Metadata or Salt.
|
||||
toConfig() map[string]string
|
||||
}
|
||||
|
||||
@ -107,15 +113,23 @@ func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, err
|
||||
}
|
||||
|
||||
var cred Credential
|
||||
var err error
|
||||
|
||||
switch CredentialKind(configs[configKeyKind]) {
|
||||
case KindToken:
|
||||
cred = NewTokenFromConfig(configs)
|
||||
cred, err = NewTokenFromConfig(configs)
|
||||
case KindLogin:
|
||||
cred, err = NewLoginFromConfig(configs)
|
||||
case KindLoginPassword:
|
||||
cred, err = NewLoginPasswordFromConfig(configs)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown credential type %s", configs[configKeyKind])
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading credential: %v", err)
|
||||
}
|
||||
|
||||
return cred, nil
|
||||
}
|
||||
|
||||
@ -133,6 +147,23 @@ func metaFromConfig(configs map[string]string) map[string]string {
|
||||
return result
|
||||
}
|
||||
|
||||
func makeSalt() []byte {
|
||||
result := make([]byte, 16)
|
||||
_, err := rand.Read(result)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func saltFromConfig(configs map[string]string) ([]byte, error) {
|
||||
val, ok := configs[configKeySalt]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no credential salt found")
|
||||
}
|
||||
return base64.StdEncoding.DecodeString(val)
|
||||
}
|
||||
|
||||
// List load all existing credentials
|
||||
func List(repo repository.RepoConfig, opts ...Option) ([]Credential, error) {
|
||||
rawConfigs, err := repo.GlobalConfig().ReadAll(configKeyPrefix + ".")
|
||||
@ -210,6 +241,16 @@ func Store(repo repository.RepoConfig, cred Credential) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Salt
|
||||
if len(cred.Salt()) != 16 {
|
||||
panic("credentials need to be salted")
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(cred.Salt())
|
||||
err = repo.GlobalConfig().StoreString(prefix+configKeySalt, encoded)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Metadata
|
||||
for key, val := range cred.Metadata() {
|
||||
err := repo.GlobalConfig().StoreString(prefix+configKeyPrefixMeta+key, val)
|
||||
|
90
bridge/core/auth/credential_base.go
Normal file
90
bridge/core/auth/credential_base.go
Normal file
@ -0,0 +1,90 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
type credentialBase struct {
|
||||
target string
|
||||
createTime time.Time
|
||||
salt []byte
|
||||
meta map[string]string
|
||||
}
|
||||
|
||||
func newCredentialBase(target string) *credentialBase {
|
||||
return &credentialBase{
|
||||
target: target,
|
||||
createTime: time.Now(),
|
||||
salt: makeSalt(),
|
||||
}
|
||||
}
|
||||
|
||||
func newCredentialBaseFromConfig(conf map[string]string) (*credentialBase, error) {
|
||||
base := &credentialBase{
|
||||
target: conf[configKeyTarget],
|
||||
meta: metaFromConfig(conf),
|
||||
}
|
||||
|
||||
if createTime, ok := conf[configKeyCreateTime]; ok {
|
||||
t, err := repository.ParseTimestamp(createTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.createTime = t
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing create time")
|
||||
}
|
||||
|
||||
salt, err := saltFromConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
base.salt = salt
|
||||
|
||||
return base, nil
|
||||
}
|
||||
|
||||
func (cb *credentialBase) Target() string {
|
||||
return cb.target
|
||||
}
|
||||
|
||||
func (cb *credentialBase) CreateTime() time.Time {
|
||||
return cb.createTime
|
||||
}
|
||||
|
||||
func (cb *credentialBase) Salt() []byte {
|
||||
return cb.salt
|
||||
}
|
||||
|
||||
func (cb *credentialBase) validate() error {
|
||||
if cb.target == "" {
|
||||
return fmt.Errorf("missing target")
|
||||
}
|
||||
if cb.createTime.IsZero() || cb.createTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("missing creation time")
|
||||
}
|
||||
if !core.TargetExist(cb.target) {
|
||||
return fmt.Errorf("unknown target")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cb *credentialBase) Metadata() map[string]string {
|
||||
return cb.meta
|
||||
}
|
||||
|
||||
func (cb *credentialBase) GetMetadata(key string) (string, bool) {
|
||||
val, ok := cb.meta[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (cb *credentialBase) SetMetadata(key string, value string) {
|
||||
if cb.meta == nil {
|
||||
cb.meta = make(map[string]string)
|
||||
}
|
||||
cb.meta[key] = value
|
||||
}
|
@ -14,7 +14,7 @@ func TestCredential(t *testing.T) {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
|
||||
storeToken := func(val string, target string) *Token {
|
||||
token := NewToken(val, target)
|
||||
token := NewToken(target, val)
|
||||
err := Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
return token
|
||||
@ -100,3 +100,25 @@ func sameIds(t *testing.T, a []Credential, b []Credential) {
|
||||
|
||||
assert.ElementsMatch(t, ids(a), ids(b))
|
||||
}
|
||||
|
||||
func testCredentialSerial(t *testing.T, original Credential) Credential {
|
||||
repo := repository.NewMockRepoForTest()
|
||||
|
||||
original.SetMetadata("test", "value")
|
||||
|
||||
assert.NotEmpty(t, original.ID().String())
|
||||
assert.NotEmpty(t, original.Salt())
|
||||
assert.NoError(t, Store(repo, original))
|
||||
|
||||
loaded, err := LoadWithId(repo, original.ID())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, original.ID(), loaded.ID())
|
||||
assert.Equal(t, original.Kind(), loaded.Kind())
|
||||
assert.Equal(t, original.Target(), loaded.Target())
|
||||
assert.Equal(t, original.CreateTime().Unix(), loaded.CreateTime().Unix())
|
||||
assert.Equal(t, original.Salt(), loaded.Salt())
|
||||
assert.Equal(t, original.Metadata(), loaded.Metadata())
|
||||
|
||||
return loaded
|
||||
}
|
||||
|
67
bridge/core/auth/login.go
Normal file
67
bridge/core/auth/login.go
Normal file
@ -0,0 +1,67 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
)
|
||||
|
||||
const (
|
||||
configKeyLoginLogin = "login"
|
||||
)
|
||||
|
||||
var _ Credential = &Login{}
|
||||
|
||||
type Login struct {
|
||||
*credentialBase
|
||||
Login string
|
||||
}
|
||||
|
||||
func NewLogin(target, login string) *Login {
|
||||
return &Login{
|
||||
credentialBase: newCredentialBase(target),
|
||||
Login: login,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLoginFromConfig(conf map[string]string) (*Login, error) {
|
||||
base, err := newCredentialBaseFromConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Login{
|
||||
credentialBase: base,
|
||||
Login: conf[configKeyLoginLogin],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lp *Login) ID() entity.Id {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write(lp.salt)
|
||||
_, _ = h.Write([]byte(lp.target))
|
||||
_, _ = h.Write([]byte(lp.Login))
|
||||
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
|
||||
}
|
||||
|
||||
func (lp *Login) Kind() CredentialKind {
|
||||
return KindLogin
|
||||
}
|
||||
|
||||
func (lp *Login) Validate() error {
|
||||
err := lp.credentialBase.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lp.Login == "" {
|
||||
return fmt.Errorf("missing login")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lp *Login) toConfig() map[string]string {
|
||||
return map[string]string{
|
||||
configKeyLoginLogin: lp.Login,
|
||||
}
|
||||
}
|
76
bridge/core/auth/login_password.go
Normal file
76
bridge/core/auth/login_password.go
Normal file
@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
)
|
||||
|
||||
const (
|
||||
configKeyLoginPasswordLogin = "login"
|
||||
configKeyLoginPasswordPassword = "password"
|
||||
)
|
||||
|
||||
var _ Credential = &LoginPassword{}
|
||||
|
||||
type LoginPassword struct {
|
||||
*credentialBase
|
||||
Login string
|
||||
Password string
|
||||
}
|
||||
|
||||
func NewLoginPassword(target, login, password string) *LoginPassword {
|
||||
return &LoginPassword{
|
||||
credentialBase: newCredentialBase(target),
|
||||
Login: login,
|
||||
Password: password,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLoginPasswordFromConfig(conf map[string]string) (*LoginPassword, error) {
|
||||
base, err := newCredentialBaseFromConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LoginPassword{
|
||||
credentialBase: base,
|
||||
Login: conf[configKeyLoginPasswordLogin],
|
||||
Password: conf[configKeyLoginPasswordPassword],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (lp *LoginPassword) ID() entity.Id {
|
||||
h := sha256.New()
|
||||
_, _ = h.Write(lp.salt)
|
||||
_, _ = h.Write([]byte(lp.target))
|
||||
_, _ = h.Write([]byte(lp.Login))
|
||||
_, _ = h.Write([]byte(lp.Password))
|
||||
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
|
||||
}
|
||||
|
||||
func (lp *LoginPassword) Kind() CredentialKind {
|
||||
return KindLoginPassword
|
||||
}
|
||||
|
||||
func (lp *LoginPassword) Validate() error {
|
||||
err := lp.credentialBase.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if lp.Login == "" {
|
||||
return fmt.Errorf("missing login")
|
||||
}
|
||||
if lp.Password == "" {
|
||||
return fmt.Errorf("missing password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lp *LoginPassword) toConfig() map[string]string {
|
||||
return map[string]string{
|
||||
configKeyLoginPasswordLogin: lp.Login,
|
||||
configKeyLoginPasswordPassword: lp.Password,
|
||||
}
|
||||
}
|
14
bridge/core/auth/login_password_test.go
Normal file
14
bridge/core/auth/login_password_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoginPasswordSerial(t *testing.T) {
|
||||
original := NewLoginPassword("github", "jean", "jacques")
|
||||
loaded := testCredentialSerial(t, original)
|
||||
assert.Equal(t, original.Login, loaded.(*LoginPassword).Login)
|
||||
assert.Equal(t, original.Password, loaded.(*LoginPassword).Password)
|
||||
}
|
13
bridge/core/auth/login_test.go
Normal file
13
bridge/core/auth/login_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoginSerial(t *testing.T) {
|
||||
original := NewLogin("github", "jean")
|
||||
loaded := testCredentialSerial(t, original)
|
||||
assert.Equal(t, original.Login, loaded.(*Login).Login)
|
||||
}
|
@ -3,104 +3,68 @@ package auth
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/MichaelMure/git-bug/bridge/core"
|
||||
"github.com/MichaelMure/git-bug/entity"
|
||||
"github.com/MichaelMure/git-bug/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenValueKey = "value"
|
||||
configKeyTokenValue = "value"
|
||||
)
|
||||
|
||||
var _ Credential = &Token{}
|
||||
|
||||
// Token holds an API access token data
|
||||
type Token struct {
|
||||
target string
|
||||
createTime time.Time
|
||||
Value string
|
||||
meta map[string]string
|
||||
*credentialBase
|
||||
Value string
|
||||
}
|
||||
|
||||
// NewToken instantiate a new token
|
||||
func NewToken(value, target string) *Token {
|
||||
func NewToken(target, value string) *Token {
|
||||
return &Token{
|
||||
target: target,
|
||||
createTime: time.Now(),
|
||||
Value: value,
|
||||
credentialBase: newCredentialBase(target),
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTokenFromConfig(conf map[string]string) *Token {
|
||||
token := &Token{}
|
||||
|
||||
token.target = conf[configKeyTarget]
|
||||
if createTime, ok := conf[configKeyCreateTime]; ok {
|
||||
if t, err := repository.ParseTimestamp(createTime); err == nil {
|
||||
token.createTime = t
|
||||
}
|
||||
func NewTokenFromConfig(conf map[string]string) (*Token, error) {
|
||||
base, err := newCredentialBaseFromConfig(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token.Value = conf[tokenValueKey]
|
||||
token.meta = metaFromConfig(conf)
|
||||
|
||||
return token
|
||||
return &Token{
|
||||
credentialBase: base,
|
||||
Value: conf[configKeyTokenValue],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Token) ID() entity.Id {
|
||||
sum := sha256.Sum256([]byte(t.target + t.Value))
|
||||
return entity.Id(fmt.Sprintf("%x", sum))
|
||||
}
|
||||
|
||||
func (t *Token) Target() string {
|
||||
return t.target
|
||||
h := sha256.New()
|
||||
_, _ = h.Write(t.salt)
|
||||
_, _ = h.Write([]byte(t.target))
|
||||
_, _ = h.Write([]byte(t.Value))
|
||||
return entity.Id(fmt.Sprintf("%x", h.Sum(nil)))
|
||||
}
|
||||
|
||||
func (t *Token) Kind() CredentialKind {
|
||||
return KindToken
|
||||
}
|
||||
|
||||
func (t *Token) CreateTime() time.Time {
|
||||
return t.createTime
|
||||
}
|
||||
|
||||
// Validate ensure token important fields are valid
|
||||
func (t *Token) Validate() error {
|
||||
err := t.credentialBase.validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Value == "" {
|
||||
return fmt.Errorf("missing value")
|
||||
}
|
||||
if t.target == "" {
|
||||
return fmt.Errorf("missing target")
|
||||
}
|
||||
if t.createTime.IsZero() || t.createTime.Equal(time.Time{}) {
|
||||
return fmt.Errorf("missing creation time")
|
||||
}
|
||||
if !core.TargetExist(t.target) {
|
||||
return fmt.Errorf("unknown target")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Token) Metadata() map[string]string {
|
||||
return t.meta
|
||||
}
|
||||
|
||||
func (t *Token) GetMetadata(key string) (string, bool) {
|
||||
val, ok := t.meta[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (t *Token) SetMetadata(key string, value string) {
|
||||
if t.meta == nil {
|
||||
t.meta = make(map[string]string)
|
||||
}
|
||||
t.meta[key] = value
|
||||
}
|
||||
|
||||
func (t *Token) toConfig() map[string]string {
|
||||
return map[string]string{
|
||||
tokenValueKey: t.Value,
|
||||
configKeyTokenValue: t.Value,
|
||||
}
|
||||
}
|
||||
|
13
bridge/core/auth/token_test.go
Normal file
13
bridge/core/auth/token_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTokenSerial(t *testing.T) {
|
||||
original := NewToken("github", "value")
|
||||
loaded := testCredentialSerial(t, original)
|
||||
assert.Equal(t, original.Value, loaded.(*Token).Value)
|
||||
}
|
@ -3,6 +3,7 @@ package github
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -70,25 +71,7 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
return nil, fmt.Errorf("invalid parameter owner: %v", owner)
|
||||
}
|
||||
|
||||
login := params.Login
|
||||
if login == "" {
|
||||
validator := func(name string, value string) (string, error) {
|
||||
ok, err := validateUsername(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return "invalid login", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
login, err = input.Prompt("Github login", "login", input.Required, validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var login string
|
||||
var cred auth.Credential
|
||||
|
||||
switch {
|
||||
@ -97,10 +80,27 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, ok := cred.GetMetadata(auth.MetaKeyLogin)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("credential doesn't have a login")
|
||||
}
|
||||
login = l
|
||||
case params.TokenRaw != "":
|
||||
cred = auth.NewToken(params.TokenRaw, target)
|
||||
cred.SetMetadata(auth.MetaKeyLogin, login)
|
||||
token := auth.NewToken(target, params.TokenRaw)
|
||||
login, err = getLoginFromToken(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
cred = token
|
||||
default:
|
||||
login = params.Login
|
||||
if login == "" {
|
||||
login, err = input.Prompt("Github login", "login", input.Required, usernameValidator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cred, err = promptTokenOptions(repo, login, owner, project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -159,6 +159,17 @@ func (*Github) ValidateConfig(conf core.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func usernameValidator(name string, value string) (string, error) {
|
||||
ok, err := validateUsername(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return "invalid login", nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func requestToken(note, login, password string, scope string) (*http.Response, error) {
|
||||
return requestTokenWith2FA(note, login, password, "", scope)
|
||||
}
|
||||
@ -231,7 +242,11 @@ func randomFingerprint() string {
|
||||
|
||||
func promptTokenOptions(repo repository.RepoConfig, login, owner, project string) (auth.Credential, error) {
|
||||
for {
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithMeta(auth.MetaKeyLogin, login))
|
||||
creds, err := auth.List(repo,
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
auth.WithMeta(auth.MetaKeyLogin, login),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -275,19 +290,13 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
|
||||
|
||||
switch index {
|
||||
case 1:
|
||||
value, err := promptToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := auth.NewToken(value, target)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
return token, nil
|
||||
return promptToken()
|
||||
case 2:
|
||||
value, err := loginAndRequestToken(login, owner, project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token := auth.NewToken(value, target)
|
||||
token := auth.NewToken(target, value)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
return token, nil
|
||||
default:
|
||||
@ -296,7 +305,7 @@ func promptTokenOptions(repo repository.RepoConfig, login, owner, project string
|
||||
}
|
||||
}
|
||||
|
||||
func promptToken() (string, error) {
|
||||
func promptToken() (*auth.Token, error) {
|
||||
fmt.Println("You can generate a new token by visiting https://github.com/settings/tokens.")
|
||||
fmt.Println("Choose 'Generate new token' and set the necessary access scope for your repository.")
|
||||
fmt.Println()
|
||||
@ -312,14 +321,28 @@ func promptToken() (string, error) {
|
||||
panic("regexp compile:" + err.Error())
|
||||
}
|
||||
|
||||
var login string
|
||||
|
||||
validator := func(name string, value string) (complaint string, err error) {
|
||||
if re.MatchString(value) {
|
||||
return "", nil
|
||||
if !re.MatchString(value) {
|
||||
return "token has incorrect format", nil
|
||||
}
|
||||
return "token has incorrect format", nil
|
||||
login, err = getLoginFromToken(auth.NewToken(target, value))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("token is invalid: %v", err), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return input.Prompt("Enter token", "token", input.Required, validator)
|
||||
rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := auth.NewToken(target, rawToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func loginAndRequestToken(login, owner, project string) (string, error) {
|
||||
@ -543,3 +566,22 @@ func validateProject(owner, project string, token *auth.Token) (bool, error) {
|
||||
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
|
||||
func getLoginFromToken(token *auth.Token) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
client := buildClient(token)
|
||||
|
||||
var q loginQuery
|
||||
|
||||
err := client.Query(ctx, &q, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if q.Viewer.Login == "" {
|
||||
return "", fmt.Errorf("github say username is empty")
|
||||
}
|
||||
|
||||
return q.Viewer.Login, nil
|
||||
}
|
||||
|
@ -154,8 +154,8 @@ func TestValidateProject(t *testing.T) {
|
||||
t.Skip("Env var GITHUB_TOKEN_PUBLIC missing")
|
||||
}
|
||||
|
||||
tokenPrivate := auth.NewToken(envPrivate, target)
|
||||
tokenPublic := auth.NewToken(envPublic, target)
|
||||
tokenPrivate := auth.NewToken(target, envPrivate)
|
||||
tokenPublic := auth.NewToken(target, envPublic)
|
||||
|
||||
type args struct {
|
||||
owner string
|
||||
|
@ -157,7 +157,7 @@ func TestPushPull(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
token := auth.NewToken(envToken, target)
|
||||
token := auth.NewToken(target, envToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
@ -168,14 +168,6 @@ type ghostQuery struct {
|
||||
} `graphql:"user(login: $login)"`
|
||||
}
|
||||
|
||||
type labelQuery struct {
|
||||
Repository struct {
|
||||
Label struct {
|
||||
ID string `graphql:"id"`
|
||||
} `graphql:"label(name: $label)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
type labelsQuery struct {
|
||||
Repository struct {
|
||||
Labels struct {
|
||||
@ -189,3 +181,9 @@ type labelsQuery struct {
|
||||
} `graphql:"labels(first: $first, after: $after)"`
|
||||
} `graphql:"repository(owner: $owner, name: $name)"`
|
||||
}
|
||||
|
||||
type loginQuery struct {
|
||||
Viewer struct {
|
||||
Login string `graphql:"login"`
|
||||
} `graphql:"viewer"`
|
||||
}
|
||||
|
@ -144,7 +144,7 @@ func Test_Importer(t *testing.T) {
|
||||
login := "test-identity"
|
||||
author.SetMetadata(metaKeyGithubLogin, login)
|
||||
|
||||
token := auth.NewToken(envToken, target)
|
||||
token := auth.NewToken(target, envToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
@ -35,9 +35,6 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
if params.Owner != "" {
|
||||
fmt.Println("warning: --owner is ineffective for a gitlab bridge")
|
||||
}
|
||||
if params.Login != "" {
|
||||
fmt.Println("warning: --login is ineffective for a gitlab bridge")
|
||||
}
|
||||
|
||||
conf := make(core.Configuration)
|
||||
var err error
|
||||
@ -53,24 +50,25 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
}
|
||||
}
|
||||
|
||||
var url string
|
||||
var projectURL string
|
||||
|
||||
// get project url
|
||||
switch {
|
||||
case params.URL != "":
|
||||
url = params.URL
|
||||
projectURL = params.URL
|
||||
default:
|
||||
// terminal prompt
|
||||
url, err = promptURL(repo, baseUrl)
|
||||
projectURL, err = promptProjectURL(repo, baseUrl)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "url prompt")
|
||||
}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(url, params.BaseURL) {
|
||||
return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, url)
|
||||
if !strings.HasPrefix(projectURL, params.BaseURL) {
|
||||
return nil, fmt.Errorf("base URL (%s) doesn't match the project URL (%s)", params.BaseURL, projectURL)
|
||||
}
|
||||
|
||||
var login string
|
||||
var cred auth.Credential
|
||||
|
||||
switch {
|
||||
@ -79,16 +77,30 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l, ok := cred.GetMetadata(auth.MetaKeyLogin)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("credential doesn't have a login")
|
||||
}
|
||||
login = l
|
||||
case params.TokenRaw != "":
|
||||
token := auth.NewToken(params.TokenRaw, target)
|
||||
login, err := getLoginFromToken(baseUrl, token)
|
||||
token := auth.NewToken(target, params.TokenRaw)
|
||||
login, err = getLoginFromToken(baseUrl, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
|
||||
cred = token
|
||||
default:
|
||||
cred, err = promptTokenOptions(repo, baseUrl)
|
||||
login := params.Login
|
||||
if login == "" {
|
||||
// TODO: validate username
|
||||
login, err = input.Prompt("Gitlab login", "login", input.Required)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
cred, err = promptTokenOptions(repo, login, baseUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -100,7 +112,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
}
|
||||
|
||||
// validate project url and get its ID
|
||||
id, err := validateProjectURL(baseUrl, url, token)
|
||||
id, err := validateProjectURL(baseUrl, projectURL, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "project validation")
|
||||
}
|
||||
@ -122,7 +134,7 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
|
||||
}
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
return conf, core.FinishConfig(repo, metaKeyGitlabLogin, login)
|
||||
}
|
||||
|
||||
func (g *Gitlab) ValidateConfig(conf core.Configuration) error {
|
||||
@ -176,9 +188,14 @@ func promptBaseUrl() (string, error) {
|
||||
return input.Prompt("Base url", "url", input.Required, validator)
|
||||
}
|
||||
|
||||
func promptTokenOptions(repo repository.RepoConfig, baseUrl string) (auth.Credential, error) {
|
||||
func promptTokenOptions(repo repository.RepoConfig, login, baseUrl string) (auth.Credential, error) {
|
||||
for {
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
creds, err := auth.List(repo,
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
auth.WithMeta(auth.MetaKeyLogin, login),
|
||||
auth.WithMeta(auth.MetaKeyBaseURL, baseUrl),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -248,7 +265,7 @@ func promptToken(baseUrl string) (*auth.Token, error) {
|
||||
if !re.MatchString(value) {
|
||||
return "token has incorrect format", nil
|
||||
}
|
||||
login, err = getLoginFromToken(baseUrl, auth.NewToken(value, target))
|
||||
login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
|
||||
if err != nil {
|
||||
return fmt.Sprintf("token is invalid: %v", err), nil
|
||||
}
|
||||
@ -260,13 +277,14 @@ func promptToken(baseUrl string) (*auth.Token, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := auth.NewToken(rawToken, target)
|
||||
token := auth.NewToken(target, rawToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
token.SetMetadata(auth.MetaKeyBaseURL, baseUrl)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
|
||||
func promptProjectURL(repo repository.RepoCommon, baseUrl string) (string, error) {
|
||||
// remote suggestions
|
||||
remotes, err := repo.GetRemotes()
|
||||
if err != nil {
|
||||
@ -317,13 +335,13 @@ func promptURL(repo repository.RepoCommon, baseUrl string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
url := strings.TrimSpace(line)
|
||||
if url == "" {
|
||||
projectURL := strings.TrimSpace(line)
|
||||
if projectURL == "" {
|
||||
fmt.Println("URL is empty")
|
||||
continue
|
||||
}
|
||||
|
||||
return url, nil
|
||||
return projectURL, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
|
||||
ge.repositoryID = ge.conf[keyProjectID]
|
||||
|
||||
// preload all clients
|
||||
err := ge.cacheAllClient(repo)
|
||||
err := ge.cacheAllClient(repo, ge.conf[keyGitlabBaseUrl])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -55,8 +55,12 @@ func (ge *gitlabExporter) Init(repo *cache.RepoCache, conf core.Configuration) e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache) error {
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
func (ge *gitlabExporter) cacheAllClient(repo *cache.RepoCache, baseURL string) error {
|
||||
creds, err := auth.List(repo,
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
auth.WithMeta(auth.MetaKeyBaseURL, baseURL),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -162,8 +162,9 @@ func TestPushPull(t *testing.T) {
|
||||
defer backend.Close()
|
||||
interrupt.RegisterCleaner(backend.Close)
|
||||
|
||||
token := auth.NewToken(envToken, target)
|
||||
token := auth.NewToken(target, envToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -194,7 +195,7 @@ func TestPushPull(t *testing.T) {
|
||||
exporter := &gitlabExporter{}
|
||||
err = exporter.Init(backend, core.Configuration{
|
||||
keyProjectID: strconv.Itoa(projectID),
|
||||
keyGitlabBaseUrl: "https://gitlab.com/",
|
||||
keyGitlabBaseUrl: defaultBaseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -222,7 +223,7 @@ func TestPushPull(t *testing.T) {
|
||||
importer := &gitlabImporter{}
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyProjectID: strconv.Itoa(projectID),
|
||||
keyGitlabBaseUrl: "https://gitlab.com/",
|
||||
keyGitlabBaseUrl: defaultBaseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -287,7 +288,7 @@ func generateRepoName() string {
|
||||
|
||||
// create repository need a token with scope 'repo'
|
||||
func createRepository(ctx context.Context, name string, token *auth.Token) (int, error) {
|
||||
client, err := buildClient("https://gitlab.com/", token)
|
||||
client, err := buildClient(defaultBaseURL, token)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@ -307,7 +308,7 @@ func createRepository(ctx context.Context, name string, token *auth.Token) (int,
|
||||
|
||||
// delete repository need a token with scope 'delete_repo'
|
||||
func deleteRepository(ctx context.Context, project int, token *auth.Token) error {
|
||||
client, err := buildClient("https://gitlab.com/", token)
|
||||
client, err := buildClient(defaultBaseURL, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -33,7 +33,11 @@ type gitlabImporter struct {
|
||||
func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) error {
|
||||
gi.conf = conf
|
||||
|
||||
creds, err := auth.List(repo, auth.WithTarget(target), auth.WithKind(auth.KindToken))
|
||||
creds, err := auth.List(repo,
|
||||
auth.WithTarget(target),
|
||||
auth.WithKind(auth.KindToken),
|
||||
auth.WithMeta(auth.MetaKeyBaseURL, conf[keyGitlabBaseUrl]),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -98,15 +98,16 @@ func TestImport(t *testing.T) {
|
||||
login := "test-identity"
|
||||
author.SetMetadata(metaKeyGitlabLogin, login)
|
||||
|
||||
token := auth.NewToken(envToken, target)
|
||||
token.SetMetadata(metaKeyGitlabLogin, login)
|
||||
token := auth.NewToken(target, envToken)
|
||||
token.SetMetadata(auth.MetaKeyLogin, login)
|
||||
token.SetMetadata(auth.MetaKeyBaseURL, defaultBaseURL)
|
||||
err = auth.Store(repo, token)
|
||||
require.NoError(t, err)
|
||||
|
||||
importer := &gitlabImporter{}
|
||||
err = importer.Init(backend, core.Configuration{
|
||||
keyProjectID: projectID,
|
||||
keyGitlabBaseUrl: "https://gitlab.com",
|
||||
keyGitlabBaseUrl: defaultBaseURL,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
12
cache/repo_cache.go
vendored
12
cache/repo_cache.go
vendored
@ -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()
|
||||
|
@ -86,7 +86,7 @@ func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
token := auth.NewToken(value, bridgeAuthAddTokenTarget)
|
||||
token := auth.NewToken(bridgeAuthAddTokenTarget, value)
|
||||
token.SetMetadata(auth.MetaKeyLogin, bridgeAuthAddTokenLogin)
|
||||
|
||||
if err := token.Validate(); err != nil {
|
||||
|
4
go.mod
4
go.mod
@ -13,7 +13,7 @@ require (
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/fatih/color v1.9.0
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
@ -26,7 +26,7 @@ require (
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/theckman/goconstraint v1.11.0
|
||||
github.com/vektah/gqlparser v1.3.1
|
||||
github.com/xanzy/go-gitlab v0.24.0
|
||||
github.com/xanzy/go-gitlab v0.25.0
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
|
4
go.sum
4
go.sum
@ -51,6 +51,8 @@ github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
@ -138,6 +140,8 @@ github.com/xanzy/go-gitlab v0.22.1 h1:TVxgHmoa35jQL+9FCkG0nwPDxU9dQZXknBTDtGaSFn
|
||||
github.com/xanzy/go-gitlab v0.22.1/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
|
||||
github.com/xanzy/go-gitlab v0.24.0 h1:zP1zC4K76Gha0coN5GhygOLhsHTCvUjrnqGL3kHXkVU=
|
||||
github.com/xanzy/go-gitlab v0.24.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
|
||||
github.com/xanzy/go-gitlab v0.25.0 h1:G5aTZeqZd66Q6qMVieBfmHBsPpF0jY92zCLAMpULe3I=
|
||||
github.com/xanzy/go-gitlab v0.25.0/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2HagfWKv5Og=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
|
@ -163,16 +163,6 @@ type ComplexityRoot struct {
|
||||
Message func(childComplexity int) int
|
||||
}
|
||||
|
||||
CommitAsNeededPayload struct {
|
||||
Bug func(childComplexity int) int
|
||||
ClientMutationID func(childComplexity int) int
|
||||
}
|
||||
|
||||
CommitPayload struct {
|
||||
Bug func(childComplexity int) int
|
||||
ClientMutationID func(childComplexity int) int
|
||||
}
|
||||
|
||||
CreateOperation struct {
|
||||
Author func(childComplexity int) int
|
||||
Date func(childComplexity int) int
|
||||
@ -264,14 +254,12 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Mutation struct {
|
||||
AddComment func(childComplexity int, input models.AddCommentInput) int
|
||||
ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int
|
||||
CloseBug func(childComplexity int, input models.CloseBugInput) int
|
||||
Commit func(childComplexity int, input models.CommitInput) int
|
||||
CommitAsNeeded func(childComplexity int, input models.CommitAsNeededInput) int
|
||||
NewBug func(childComplexity int, input models.NewBugInput) int
|
||||
OpenBug func(childComplexity int, input models.OpenBugInput) int
|
||||
SetTitle func(childComplexity int, input models.SetTitleInput) int
|
||||
AddComment func(childComplexity int, input models.AddCommentInput) int
|
||||
ChangeLabels func(childComplexity int, input *models.ChangeLabelInput) int
|
||||
CloseBug func(childComplexity int, input models.CloseBugInput) int
|
||||
NewBug func(childComplexity int, input models.NewBugInput) int
|
||||
OpenBug func(childComplexity int, input models.OpenBugInput) int
|
||||
SetTitle func(childComplexity int, input models.SetTitleInput) int
|
||||
}
|
||||
|
||||
NewBugPayload struct {
|
||||
@ -306,8 +294,7 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
Query struct {
|
||||
DefaultRepository func(childComplexity int) int
|
||||
Repository func(childComplexity int, ref string) int
|
||||
Repository func(childComplexity int, ref *string) int
|
||||
}
|
||||
|
||||
Repository struct {
|
||||
@ -315,6 +302,7 @@ type ComplexityRoot struct {
|
||||
AllIdentities func(childComplexity int, after *string, before *string, first *int, last *int) int
|
||||
Bug func(childComplexity int, prefix string) int
|
||||
Identity func(childComplexity int, prefix string) int
|
||||
Name func(childComplexity int) int
|
||||
UserIdentity func(childComplexity int) int
|
||||
ValidLabels func(childComplexity int, after *string, before *string, first *int, last *int) int
|
||||
}
|
||||
@ -448,14 +436,12 @@ type MutationResolver interface {
|
||||
OpenBug(ctx context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error)
|
||||
CloseBug(ctx context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error)
|
||||
SetTitle(ctx context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error)
|
||||
Commit(ctx context.Context, input models.CommitInput) (*models.CommitPayload, error)
|
||||
CommitAsNeeded(ctx context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error)
|
||||
}
|
||||
type QueryResolver interface {
|
||||
DefaultRepository(ctx context.Context) (*models.Repository, error)
|
||||
Repository(ctx context.Context, ref string) (*models.Repository, error)
|
||||
Repository(ctx context.Context, ref *string) (*models.Repository, error)
|
||||
}
|
||||
type RepositoryResolver interface {
|
||||
Name(ctx context.Context, obj *models.Repository) (*string, error)
|
||||
AllBugs(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int, query *string) (*models.BugConnection, error)
|
||||
Bug(ctx context.Context, obj *models.Repository, prefix string) (models.BugWrapper, error)
|
||||
AllIdentities(ctx context.Context, obj *models.Repository, after *string, before *string, first *int, last *int) (*models.IdentityConnection, error)
|
||||
@ -925,34 +911,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.CommentHistoryStep.Message(childComplexity), true
|
||||
|
||||
case "CommitAsNeededPayload.bug":
|
||||
if e.complexity.CommitAsNeededPayload.Bug == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.CommitAsNeededPayload.Bug(childComplexity), true
|
||||
|
||||
case "CommitAsNeededPayload.clientMutationId":
|
||||
if e.complexity.CommitAsNeededPayload.ClientMutationID == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.CommitAsNeededPayload.ClientMutationID(childComplexity), true
|
||||
|
||||
case "CommitPayload.bug":
|
||||
if e.complexity.CommitPayload.Bug == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.CommitPayload.Bug(childComplexity), true
|
||||
|
||||
case "CommitPayload.clientMutationId":
|
||||
if e.complexity.CommitPayload.ClientMutationID == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.CommitPayload.ClientMutationID(childComplexity), true
|
||||
|
||||
case "CreateOperation.author":
|
||||
if e.complexity.CreateOperation.Author == nil {
|
||||
break
|
||||
@ -1367,30 +1325,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.CloseBug(childComplexity, args["input"].(models.CloseBugInput)), true
|
||||
|
||||
case "Mutation.commit":
|
||||
if e.complexity.Mutation.Commit == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_commit_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.Commit(childComplexity, args["input"].(models.CommitInput)), true
|
||||
|
||||
case "Mutation.commitAsNeeded":
|
||||
if e.complexity.Mutation.CommitAsNeeded == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_commitAsNeeded_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.CommitAsNeeded(childComplexity, args["input"].(models.CommitAsNeededInput)), true
|
||||
|
||||
case "Mutation.newBug":
|
||||
if e.complexity.Mutation.NewBug == nil {
|
||||
break
|
||||
@ -1539,13 +1473,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.PageInfo.StartCursor(childComplexity), true
|
||||
|
||||
case "Query.defaultRepository":
|
||||
if e.complexity.Query.DefaultRepository == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Query.DefaultRepository(childComplexity), true
|
||||
|
||||
case "Query.repository":
|
||||
if e.complexity.Query.Repository == nil {
|
||||
break
|
||||
@ -1556,7 +1483,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Query.Repository(childComplexity, args["ref"].(string)), true
|
||||
return e.complexity.Query.Repository(childComplexity, args["ref"].(*string)), true
|
||||
|
||||
case "Repository.allBugs":
|
||||
if e.complexity.Repository.AllBugs == nil {
|
||||
@ -1606,6 +1533,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Repository.Identity(childComplexity, args["prefix"].(string)), true
|
||||
|
||||
case "Repository.name":
|
||||
if e.complexity.Repository.Name == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Repository.Name(childComplexity), true
|
||||
|
||||
case "Repository.userIdentity":
|
||||
if e.complexity.Repository.UserIdentity == nil {
|
||||
break
|
||||
@ -2184,38 +2118,6 @@ type SetTitlePayload {
|
||||
"""The resulting operation"""
|
||||
operation: SetTitleOperation!
|
||||
}
|
||||
|
||||
input CommitInput {
|
||||
"""A unique identifier for the client performing the mutation."""
|
||||
clientMutationId: String
|
||||
""""The name of the repository. If not set, the default repository is used."""
|
||||
repoRef: String
|
||||
"""The bug ID's prefix."""
|
||||
prefix: String!
|
||||
}
|
||||
|
||||
type CommitPayload {
|
||||
"""A unique identifier for the client performing the mutation."""
|
||||
clientMutationId: String
|
||||
"""The affected bug."""
|
||||
bug: Bug!
|
||||
}
|
||||
|
||||
input CommitAsNeededInput {
|
||||
"""A unique identifier for the client performing the mutation."""
|
||||
clientMutationId: String
|
||||
""""The name of the repository. If not set, the default repository is used."""
|
||||
repoRef: String
|
||||
"""The bug ID's prefix."""
|
||||
prefix: String!
|
||||
}
|
||||
|
||||
type CommitAsNeededPayload {
|
||||
"""A unique identifier for the client performing the mutation."""
|
||||
clientMutationId: String
|
||||
"""The affected bug."""
|
||||
bug: Bug!
|
||||
}
|
||||
`, BuiltIn: false},
|
||||
&ast.Source{Name: "schema/operations.graphql", Input: `"""An operation applied to a bug."""
|
||||
interface Operation {
|
||||
@ -2320,6 +2222,9 @@ type LabelChangeOperation implements Operation & Authored {
|
||||
`, BuiltIn: false},
|
||||
&ast.Source{Name: "schema/repository.graphql", Input: `
|
||||
type Repository {
|
||||
"""The name of the repository"""
|
||||
name: String
|
||||
|
||||
"""All the bugs"""
|
||||
allBugs(
|
||||
"""Returns the elements in the list that come after the specified cursor."""
|
||||
@ -2330,7 +2235,7 @@ type Repository {
|
||||
first: Int
|
||||
"""Returns the last _n_ elements from the list."""
|
||||
last: Int
|
||||
"""A query to select and order bugs"""
|
||||
"""A query to select and order bugs."""
|
||||
query: String
|
||||
): BugConnection!
|
||||
|
||||
@ -2366,12 +2271,8 @@ type Repository {
|
||||
): LabelConnection!
|
||||
}`, BuiltIn: false},
|
||||
&ast.Source{Name: "schema/root.graphql", Input: `type Query {
|
||||
"""The default unnamend repository."""
|
||||
defaultRepository: Repository
|
||||
"""Access a repository by reference/name."""
|
||||
repository(ref: String!): Repository
|
||||
|
||||
#TODO: connection for all repositories
|
||||
"""Access a repository by reference/name. If no ref is given, the default repository is returned if any."""
|
||||
repository(ref: String): Repository
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@ -2387,10 +2288,6 @@ type Mutation {
|
||||
closeBug(input: CloseBugInput!): CloseBugPayload!
|
||||
"""Change a bug's title"""
|
||||
setTitle(input: SetTitleInput!): SetTitlePayload!
|
||||
"""Commit write the pending operations into storage. This mutation fail if nothing is pending"""
|
||||
commit(input: CommitInput!): CommitPayload!
|
||||
"""Commit write the pending operations into storage. This mutation succed if nothing is pending"""
|
||||
commitAsNeeded(input: CommitAsNeededInput!): CommitAsNeededPayload!
|
||||
}
|
||||
`, BuiltIn: false},
|
||||
&ast.Source{Name: "schema/timeline.graphql", Input: `"""An item in the timeline of events"""
|
||||
@ -2750,34 +2647,6 @@ func (ec *executionContext) field_Mutation_closeBug_args(ctx context.Context, ra
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_commitAsNeeded_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 models.CommitAsNeededInput
|
||||
if tmp, ok := rawArgs["input"]; ok {
|
||||
arg0, err = ec.unmarshalNCommitAsNeededInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededInput(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["input"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_commit_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 models.CommitInput
|
||||
if tmp, ok := rawArgs["input"]; ok {
|
||||
arg0, err = ec.unmarshalNCommitInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitInput(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["input"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_newBug_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@ -2837,9 +2706,9 @@ func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs
|
||||
func (ec *executionContext) field_Query_repository_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 string
|
||||
var arg0 *string
|
||||
if tmp, ok := rawArgs["ref"]; ok {
|
||||
arg0, err = ec.unmarshalNString2string(ctx, tmp)
|
||||
arg0, err = ec.unmarshalOString2ᚖstring(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -4998,136 +4867,6 @@ func (ec *executionContext) _CommentHistoryStep_date(ctx context.Context, field
|
||||
return ec.marshalNTime2ᚖtimeᚐTime(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _CommitAsNeededPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.CommitAsNeededPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "CommitAsNeededPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.ClientMutationID, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _CommitAsNeededPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.CommitAsNeededPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "CommitAsNeededPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Bug, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(models.BugWrapper)
|
||||
fc.Result = res
|
||||
return ec.marshalNBug2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _CommitPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.CommitPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "CommitPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.ClientMutationID, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _CommitPayload_bug(ctx context.Context, field graphql.CollectedField, obj *models.CommitPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "CommitPayload",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Bug, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(models.BugWrapper)
|
||||
fc.Result = res
|
||||
return ec.marshalNBug2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐBugWrapper(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _CreateOperation_id(ctx context.Context, field graphql.CollectedField, obj *bug.CreateOperation) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -7201,88 +6940,6 @@ func (ec *executionContext) _Mutation_setTitle(ctx context.Context, field graphq
|
||||
return ec.marshalNSetTitlePayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐSetTitlePayload(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_commit(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Mutation_commit_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().Commit(rctx, args["input"].(models.CommitInput))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*models.CommitPayload)
|
||||
fc.Result = res
|
||||
return ec.marshalNCommitPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_commitAsNeeded(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Mutation_commitAsNeeded_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().CommitAsNeeded(rctx, args["input"].(models.CommitAsNeededInput))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*models.CommitAsNeededPayload)
|
||||
fc.Result = res
|
||||
return ec.marshalNCommitAsNeededPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _NewBugPayload_clientMutationId(ctx context.Context, field graphql.CollectedField, obj *models.NewBugPayload) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -7821,37 +7478,6 @@ func (ec *executionContext) _PageInfo_endCursor(ctx context.Context, field graph
|
||||
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_defaultRepository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Query",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Query().DefaultRepository(rctx)
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*models.Repository)
|
||||
fc.Result = res
|
||||
return ec.marshalORepository2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐRepository(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Query_repository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -7876,7 +7502,7 @@ func (ec *executionContext) _Query_repository(ctx context.Context, field graphql
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Query().Repository(rctx, args["ref"].(string))
|
||||
return ec.resolvers.Query().Repository(rctx, args["ref"].(*string))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
@ -7959,6 +7585,37 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C
|
||||
return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Repository_name(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Repository",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Repository().Name(rctx, obj)
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Repository_allBugs(ctx context.Context, field graphql.CollectedField, obj *models.Repository) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -10273,66 +9930,6 @@ func (ec *executionContext) unmarshalInputCloseBugInput(ctx context.Context, obj
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputCommitAsNeededInput(ctx context.Context, obj interface{}) (models.CommitAsNeededInput, error) {
|
||||
var it models.CommitAsNeededInput
|
||||
var asMap = obj.(map[string]interface{})
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "clientMutationId":
|
||||
var err error
|
||||
it.ClientMutationID, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "repoRef":
|
||||
var err error
|
||||
it.RepoRef, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "prefix":
|
||||
var err error
|
||||
it.Prefix, err = ec.unmarshalNString2string(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputCommitInput(ctx context.Context, obj interface{}) (models.CommitInput, error) {
|
||||
var it models.CommitInput
|
||||
var asMap = obj.(map[string]interface{})
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "clientMutationId":
|
||||
var err error
|
||||
it.ClientMutationID, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "repoRef":
|
||||
var err error
|
||||
it.RepoRef, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "prefix":
|
||||
var err error
|
||||
it.Prefix, err = ec.unmarshalNString2string(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputNewBugInput(ctx context.Context, obj interface{}) (models.NewBugInput, error) {
|
||||
var it models.NewBugInput
|
||||
var asMap = obj.(map[string]interface{})
|
||||
@ -11346,64 +10943,6 @@ func (ec *executionContext) _CommentHistoryStep(ctx context.Context, sel ast.Sel
|
||||
return out
|
||||
}
|
||||
|
||||
var commitAsNeededPayloadImplementors = []string{"CommitAsNeededPayload"}
|
||||
|
||||
func (ec *executionContext) _CommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, obj *models.CommitAsNeededPayload) graphql.Marshaler {
|
||||
fields := graphql.CollectFields(ec.OperationContext, sel, commitAsNeededPayloadImplementors)
|
||||
|
||||
out := graphql.NewFieldSet(fields)
|
||||
var invalids uint32
|
||||
for i, field := range fields {
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("CommitAsNeededPayload")
|
||||
case "clientMutationId":
|
||||
out.Values[i] = ec._CommitAsNeededPayload_clientMutationId(ctx, field, obj)
|
||||
case "bug":
|
||||
out.Values[i] = ec._CommitAsNeededPayload_bug(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
}
|
||||
out.Dispatch()
|
||||
if invalids > 0 {
|
||||
return graphql.Null
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var commitPayloadImplementors = []string{"CommitPayload"}
|
||||
|
||||
func (ec *executionContext) _CommitPayload(ctx context.Context, sel ast.SelectionSet, obj *models.CommitPayload) graphql.Marshaler {
|
||||
fields := graphql.CollectFields(ec.OperationContext, sel, commitPayloadImplementors)
|
||||
|
||||
out := graphql.NewFieldSet(fields)
|
||||
var invalids uint32
|
||||
for i, field := range fields {
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("CommitPayload")
|
||||
case "clientMutationId":
|
||||
out.Values[i] = ec._CommitPayload_clientMutationId(ctx, field, obj)
|
||||
case "bug":
|
||||
out.Values[i] = ec._CommitPayload_bug(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
}
|
||||
out.Dispatch()
|
||||
if invalids > 0 {
|
||||
return graphql.Null
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var createOperationImplementors = []string{"CreateOperation", "Operation", "Authored"}
|
||||
|
||||
func (ec *executionContext) _CreateOperation(ctx context.Context, sel ast.SelectionSet, obj *bug.CreateOperation) graphql.Marshaler {
|
||||
@ -12172,16 +11711,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "commit":
|
||||
out.Values[i] = ec._Mutation_commit(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "commitAsNeeded":
|
||||
out.Values[i] = ec._Mutation_commitAsNeeded(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@ -12392,17 +11921,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("Query")
|
||||
case "defaultRepository":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
}
|
||||
}()
|
||||
res = ec._Query_defaultRepository(ctx, field)
|
||||
return res
|
||||
})
|
||||
case "repository":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
@ -12440,6 +11958,17 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("Repository")
|
||||
case "name":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
}
|
||||
}()
|
||||
res = ec._Repository_name(ctx, field, obj)
|
||||
return res
|
||||
})
|
||||
case "allBugs":
|
||||
field := field
|
||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||
@ -13544,42 +13073,6 @@ func (ec *executionContext) marshalNCommentHistoryStep2ᚕgithubᚗcomᚋMichael
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNCommitAsNeededInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededInput(ctx context.Context, v interface{}) (models.CommitAsNeededInput, error) {
|
||||
return ec.unmarshalInputCommitAsNeededInput(ctx, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNCommitAsNeededPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, v models.CommitAsNeededPayload) graphql.Marshaler {
|
||||
return ec._CommitAsNeededPayload(ctx, sel, &v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNCommitAsNeededPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitAsNeededPayload(ctx context.Context, sel ast.SelectionSet, v *models.CommitAsNeededPayload) graphql.Marshaler {
|
||||
if v == nil {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._CommitAsNeededPayload(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNCommitInput2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitInput(ctx context.Context, v interface{}) (models.CommitInput, error) {
|
||||
return ec.unmarshalInputCommitInput(ctx, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNCommitPayload2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx context.Context, sel ast.SelectionSet, v models.CommitPayload) graphql.Marshaler {
|
||||
return ec._CommitPayload(ctx, sel, &v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNCommitPayload2ᚖgithubᚗcomᚋMichaelMureᚋgitᚑbugᚋgraphqlᚋmodelsᚐCommitPayload(ctx context.Context, sel ast.SelectionSet, v *models.CommitPayload) graphql.Marshaler {
|
||||
if v == nil {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._CommitPayload(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNCreateOperation2githubᚗcomᚋMichaelMureᚋgitᚑbugᚋbugᚐCreateOperation(ctx context.Context, sel ast.SelectionSet, v bug.CreateOperation) graphql.Marshaler {
|
||||
return ec._CreateOperation(ctx, sel, &v)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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"`
|
||||
|
@ -23,6 +23,15 @@ func (r mutationResolver) getRepo(ref *string) (*cache.RepoCache, error) {
|
||||
return r.cache.DefaultRepo()
|
||||
}
|
||||
|
||||
func (r mutationResolver) getBug(repoRef *string, bugPrefix string) (*cache.BugCache, error) {
|
||||
repo, err := r.getRepo(repoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repo.ResolveBugPrefix(bugPrefix)
|
||||
}
|
||||
|
||||
func (r mutationResolver) NewBug(_ context.Context, input models.NewBugInput) (*models.NewBugPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
@ -42,12 +51,7 @@ func (r mutationResolver) NewBug(_ context.Context, input models.NewBugInput) (*
|
||||
}
|
||||
|
||||
func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
b, err := r.getBug(input.RepoRef, input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -57,6 +61,11 @@ func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentI
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.AddCommentPayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
@ -65,12 +74,7 @@ func (r mutationResolver) AddComment(_ context.Context, input models.AddCommentI
|
||||
}
|
||||
|
||||
func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
b, err := r.getBug(input.RepoRef, input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -80,6 +84,11 @@ func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLa
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultsPtr := make([]*bug.LabelChangeResult, len(results))
|
||||
for i, result := range results {
|
||||
resultsPtr[i] = &result
|
||||
@ -94,12 +103,7 @@ func (r mutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLa
|
||||
}
|
||||
|
||||
func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
b, err := r.getBug(input.RepoRef, input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -109,6 +113,11 @@ func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.OpenBugPayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
@ -117,12 +126,7 @@ func (r mutationResolver) OpenBug(_ context.Context, input models.OpenBugInput)
|
||||
}
|
||||
|
||||
func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
b, err := r.getBug(input.RepoRef, input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -132,6 +136,11 @@ func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.CloseBugPayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
@ -140,12 +149,7 @@ func (r mutationResolver) CloseBug(_ context.Context, input models.CloseBugInput
|
||||
}
|
||||
|
||||
func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
b, err := r.getBug(input.RepoRef, input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -155,53 +159,14 @@ func (r mutationResolver) SetTitle(_ context.Context, input models.SetTitleInput
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.SetTitlePayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
Operation: op,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r mutationResolver) Commit(_ context.Context, input models.CommitInput) (*models.CommitPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.Commit()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.CommitPayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r mutationResolver) CommitAsNeeded(_ context.Context, input models.CommitAsNeededInput) (*models.CommitAsNeededPayload, error) {
|
||||
repo, err := r.getRepo(input.RepoRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := repo.ResolveBugPrefix(input.Prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.CommitAsNeeded()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.CommitAsNeededPayload{
|
||||
ClientMutationID: input.ClientMutationID,
|
||||
Bug: models.NewLoadedBug(b.Snapshot()),
|
||||
}, nil
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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!
|
||||
}
|
||||
|
@ -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!
|
||||
|
||||
|
@ -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!
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": ["react-app", "plugin:prettier/recommended"],
|
||||
"ignorePatterns": ["src/fragmentTypes.js"]
|
||||
}
|
37
webui/.eslintrc.js
Normal file
37
webui/.eslintrc.js
Normal file
@ -0,0 +1,37 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
'react-app',
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
plugins: ['graphql'],
|
||||
rules: {
|
||||
'graphql/template-strings': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./src/schema.json'),
|
||||
env: 'literal',
|
||||
},
|
||||
],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: { order: 'asc' },
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: '@material-ui/**',
|
||||
group: 'external',
|
||||
position: 'after',
|
||||
},
|
||||
{
|
||||
pattern: '*.generated',
|
||||
group: 'sibling',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
groups: [['builtin', 'external'], 'parent', ['sibling', 'index']],
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -5,7 +5,9 @@ install:
|
||||
npm install
|
||||
|
||||
test:
|
||||
npm run generate
|
||||
npm run lint
|
||||
CI=true npm run test
|
||||
|
||||
build:
|
||||
npm run build
|
||||
|
@ -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.
|
||||
|
@ -1,8 +1,32 @@
|
||||
schema: '../graphql/schema/*.graphql'
|
||||
overwrite: true
|
||||
documents: src/**/*.graphql
|
||||
generates:
|
||||
./src/fragmentTypes.js:
|
||||
./src/fragmentTypes.ts:
|
||||
plugins:
|
||||
- fragment-matcher
|
||||
- fragment-matcher
|
||||
config:
|
||||
module: es2015
|
||||
./src/gqlTypes.ts:
|
||||
plugins:
|
||||
- typescript
|
||||
./src/schema.json:
|
||||
plugins:
|
||||
- introspection
|
||||
./src/:
|
||||
plugins:
|
||||
- add: '/* eslint-disable @typescript-eslint/no-unused-vars, import/order */'
|
||||
- typescript-operations
|
||||
- typescript-react-apollo
|
||||
preset: near-operation-file
|
||||
presetConfig:
|
||||
extension: .generated.tsx
|
||||
baseTypesPath: gqlTypes.ts
|
||||
config:
|
||||
withComponent: false
|
||||
withHOC: false
|
||||
withHooks: true
|
||||
|
||||
hooks:
|
||||
afterAllFileWrite:
|
||||
- prettier --write
|
||||
|
8005
webui/package-lock.json
generated
8005
webui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": [
|
||||
|
5
webui/src/.gitignore
vendored
5
webui/src/.gitignore
vendored
@ -1 +1,4 @@
|
||||
fragmentTypes.js
|
||||
fragmentTypes.ts
|
||||
gqlTypes.ts
|
||||
*.generated.*
|
||||
schema.json
|
||||
|
@ -1,15 +1,18 @@
|
||||
import AppBar from '@material-ui/core/AppBar';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import Toolbar from '@material-ui/core/Toolbar';
|
||||
import {
|
||||
createMuiTheme,
|
||||
ThemeProvider,
|
||||
makeStyles,
|
||||
} from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import CurrentIdentity from './CurrentIdentity';
|
||||
import BugQuery from './bug/BugQuery';
|
||||
import ListQuery from './list/ListQuery';
|
||||
import CurrentIdentity from './CurrentIdentity';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
palette: {
|
||||
@ -20,7 +23,9 @@ const theme = createMuiTheme({
|
||||
});
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
offset: theme.mixins.toolbar,
|
||||
offset: {
|
||||
...theme.mixins.toolbar,
|
||||
},
|
||||
filler: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
@ -46,7 +51,7 @@ export default function App() {
|
||||
<AppBar position="fixed" color="primary">
|
||||
<Toolbar>
|
||||
<Link to="/" className={classes.appTitle}>
|
||||
<img src="logo.svg" className={classes.logo} alt="git-bug" />
|
||||
<img src="/logo.svg" className={classes.logo} alt="git-bug" />
|
||||
git-bug
|
||||
</Link>
|
||||
<div className={classes.filler}></div>
|
8
webui/src/Author.graphql
Normal file
8
webui/src/Author.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
fragment authored on Authored {
|
||||
author {
|
||||
name
|
||||
email
|
||||
displayName
|
||||
avatarUrl
|
||||
}
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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;
|
8
webui/src/CurrentIdentity.graphql
Normal file
8
webui/src/CurrentIdentity.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
query CurrentIdentity {
|
||||
repository {
|
||||
userIdentity {
|
||||
displayName
|
||||
avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
30
webui/src/CurrentIdentity.tsx
Normal file
30
webui/src/CurrentIdentity.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
displayName: {
|
||||
marginLeft: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const CurrentIdentity = () => {
|
||||
const classes = useStyles();
|
||||
const { loading, error, data } = useCurrentIdentityQuery();
|
||||
|
||||
if (error || loading || !data?.repository?.userIdentity) return null;
|
||||
|
||||
const user = data.repository.userIdentity;
|
||||
return (
|
||||
<>
|
||||
<Avatar src={user.avatarUrl ? user.avatarUrl : undefined}>
|
||||
{user.displayName.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<div className={classes.displayName}>{user.displayName}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentIdentity;
|
@ -1,8 +1,9 @@
|
||||
import Tooltip from '@material-ui/core/Tooltip/Tooltip';
|
||||
import * as moment from 'moment';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
const Date = ({ date }) => (
|
||||
type Props = { date: string };
|
||||
const Date = ({ date }: Props) => (
|
||||
<Tooltip title={moment(date).format('MMMM D, YYYY, h:mm a')}>
|
||||
<span> {moment(date).fromNow()} </span>
|
||||
</Tooltip>
|
8
webui/src/Label.graphql
Normal file
8
webui/src/Label.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
fragment Label on Label {
|
||||
name
|
||||
color {
|
||||
R
|
||||
G
|
||||
B
|
||||
}
|
||||
}
|
@ -1,25 +1,28 @@
|
||||
import React from 'react';
|
||||
import gql from 'graphql-tag';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import { common } from '@material-ui/core/colors';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
getContrastRatio,
|
||||
darken,
|
||||
} from '@material-ui/core/styles/colorManipulator';
|
||||
import { common } from '@material-ui/core/colors';
|
||||
import React from 'react';
|
||||
|
||||
import { LabelFragment } from './Label.generated';
|
||||
import { Color } from './gqlTypes';
|
||||
|
||||
// Minimum contrast between the background and the text color
|
||||
const contrastThreshold = 2.5;
|
||||
|
||||
// Guess the text color based on the background color
|
||||
const getTextColor = background =>
|
||||
const getTextColor = (background: string) =>
|
||||
getContrastRatio(background, common.white) >= contrastThreshold
|
||||
? common.white // White on dark backgrounds
|
||||
: common.black; // And black on light ones
|
||||
|
||||
const _rgb = color => 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
|
||||
const _rgb = (color: Color) =>
|
||||
'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
|
||||
|
||||
// Create a style object from the label RGB colors
|
||||
const createStyle = color => ({
|
||||
const createStyle = (color: Color) => ({
|
||||
backgroundColor: _rgb(color),
|
||||
color: getTextColor(_rgb(color)),
|
||||
borderBottomColor: darken(_rgb(color), 0.2),
|
||||
@ -30,7 +33,7 @@ const useStyles = makeStyles(theme => ({
|
||||
...theme.typography.body1,
|
||||
padding: '1px 6px 0.5px',
|
||||
fontSize: '0.9em',
|
||||
fontWeight: '500',
|
||||
fontWeight: 500,
|
||||
margin: '0.05em 1px calc(-1.5px + 0.05em)',
|
||||
borderRadius: '3px',
|
||||
display: 'inline-block',
|
||||
@ -39,7 +42,8 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function Label({ label }) {
|
||||
type Props = { label: LabelFragment };
|
||||
function Label({ label }: Props) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<span className={classes.label} style={createStyle(label.color)}>
|
||||
@ -48,15 +52,4 @@ function Label({ label }) {
|
||||
);
|
||||
}
|
||||
|
||||
Label.fragment = gql`
|
||||
fragment Label on Label {
|
||||
name
|
||||
color {
|
||||
R
|
||||
G
|
||||
B
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Label;
|
14
webui/src/bug/Bug.graphql
Normal file
14
webui/src/bug/Bug.graphql
Normal file
@ -0,0 +1,14 @@
|
||||
#import "../Label.graphql"
|
||||
#import "../Author.graphql"
|
||||
|
||||
fragment Bug on Bug {
|
||||
id
|
||||
humanId
|
||||
status
|
||||
title
|
||||
labels {
|
||||
...Label
|
||||
}
|
||||
createdAt
|
||||
...authored
|
||||
}
|
@ -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;
|
9
webui/src/bug/BugQuery.graphql
Normal file
9
webui/src/bug/BugQuery.graphql
Normal file
@ -0,0 +1,9 @@
|
||||
#import "./Bug.graphql"
|
||||
|
||||
query GetBug($id: String!) {
|
||||
repository {
|
||||
bug(prefix: $id) {
|
||||
...Bug
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
22
webui/src/bug/BugQuery.tsx
Normal file
22
webui/src/bug/BugQuery.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import Bug from './Bug';
|
||||
import { useGetBugQuery } from './BugQuery.generated';
|
||||
|
||||
type Props = RouteComponentProps<{
|
||||
id: string;
|
||||
}>;
|
||||
|
||||
const BugQuery: React.FC<Props> = ({ match }: Props) => {
|
||||
const { loading, error, data } = useGetBugQuery({
|
||||
variables: { id: match.params.id },
|
||||
});
|
||||
if (loading) return <CircularProgress />;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
if (!data?.repository?.bug) return <p>404.</p>;
|
||||
return <Bug bug={data.repository.bug} />;
|
||||
};
|
||||
|
||||
export default BugQuery;
|
@ -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;
|
13
webui/src/bug/LabelChangeFragment.graphql
Normal file
13
webui/src/bug/LabelChangeFragment.graphql
Normal file
@ -0,0 +1,13 @@
|
||||
#import "../Author.graphql"
|
||||
#import "../Label.graphql"
|
||||
|
||||
fragment LabelChange on LabelChangeTimelineItem {
|
||||
date
|
||||
...authored
|
||||
added {
|
||||
...Label
|
||||
}
|
||||
removed {
|
||||
...Label
|
||||
}
|
||||
}
|
@ -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;
|
8
webui/src/bug/MessageCommentFragment.graphql
Normal file
8
webui/src/bug/MessageCommentFragment.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
#import "../Author.graphql"
|
||||
|
||||
fragment AddComment on AddCommentTimelineItem {
|
||||
createdAt
|
||||
...authored
|
||||
edited
|
||||
message
|
||||
}
|
8
webui/src/bug/MessageCreateFragment.graphql
Normal file
8
webui/src/bug/MessageCreateFragment.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
#import "../Author.graphql"
|
||||
|
||||
fragment Create on CreateTimelineItem {
|
||||
createdAt
|
||||
...authored
|
||||
edited
|
||||
message
|
||||
}
|
@ -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;
|
7
webui/src/bug/SetStatusFragment.graphql
Normal file
7
webui/src/bug/SetStatusFragment.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
#import "../Author.graphql"
|
||||
|
||||
fragment SetStatus on SetStatusTimelineItem {
|
||||
date
|
||||
...authored
|
||||
status
|
||||
}
|
@ -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;
|
8
webui/src/bug/SetTitleFragment.graphql
Normal file
8
webui/src/bug/SetTitleFragment.graphql
Normal file
@ -0,0 +1,8 @@
|
||||
#import "../Author.graphql"
|
||||
|
||||
fragment SetTitle on SetTitleTimelineItem {
|
||||
date
|
||||
...authored
|
||||
title
|
||||
was
|
||||
}
|
@ -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;
|
48
webui/src/bug/Timeline.tsx
Normal file
48
webui/src/bug/Timeline.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
|
||||
import LabelChange from './LabelChange';
|
||||
import Message from './Message';
|
||||
import SetStatus from './SetStatus';
|
||||
import SetTitle from './SetTitle';
|
||||
import { TimelineItemFragment } from './TimelineQuery.generated';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
main: {
|
||||
'& > *:not(:last-child)': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
ops: Array<TimelineItemFragment>;
|
||||
};
|
||||
|
||||
function Timeline({ ops }: Props) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.main}>
|
||||
{ops.map((op, index) => {
|
||||
switch (op.__typename) {
|
||||
case 'CreateTimelineItem':
|
||||
return <Message key={index} op={op} />;
|
||||
case 'AddCommentTimelineItem':
|
||||
return <Message key={index} op={op} />;
|
||||
case 'LabelChangeTimelineItem':
|
||||
return <LabelChange key={index} op={op} />;
|
||||
case 'SetTitleTimelineItem':
|
||||
return <SetTitle key={index} op={op} />;
|
||||
case 'SetStatusTimelineItem':
|
||||
return <SetStatus key={index} op={op} />;
|
||||
}
|
||||
|
||||
console.warn('unsupported operation type ' + op.__typename);
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Timeline;
|
39
webui/src/bug/TimelineQuery.graphql
Normal file
39
webui/src/bug/TimelineQuery.graphql
Normal file
@ -0,0 +1,39 @@
|
||||
#import "./MessageCreateFragment.graphql"
|
||||
#import "./MessageCommentFragment.graphql"
|
||||
#import "./LabelChangeFragment.graphql"
|
||||
#import "./SetTitleFragment.graphql"
|
||||
#import "./SetStatusFragment.graphql"
|
||||
|
||||
query Timeline($id: String!, $first: Int = 10, $after: String) {
|
||||
repository {
|
||||
bug(prefix: $id) {
|
||||
timeline(first: $first, after: $after) {
|
||||
nodes {
|
||||
...TimelineItem
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment TimelineItem on TimelineItem {
|
||||
... on LabelChangeTimelineItem {
|
||||
...LabelChange
|
||||
}
|
||||
... on SetStatusTimelineItem {
|
||||
...SetStatus
|
||||
}
|
||||
... on SetTitleTimelineItem {
|
||||
...SetTitle
|
||||
}
|
||||
... on AddCommentTimelineItem {
|
||||
...AddComment
|
||||
}
|
||||
... on CreateTimelineItem {
|
||||
...Create
|
||||
}
|
||||
}
|
@ -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;
|
30
webui/src/bug/TimelineQuery.tsx
Normal file
30
webui/src/bug/TimelineQuery.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import React from 'react';
|
||||
|
||||
import Timeline from './Timeline';
|
||||
import { useTimelineQuery } from './TimelineQuery.generated';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const TimelineQuery = ({ id }: Props) => {
|
||||
const { loading, error, data } = useTimelineQuery({
|
||||
variables: {
|
||||
id,
|
||||
first: 100,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) return <CircularProgress />;
|
||||
if (error) return <p>Error: {error}</p>;
|
||||
|
||||
const nodes = data?.repository?.bug?.timeline.nodes;
|
||||
if (!nodes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Timeline ops={nodes} />;
|
||||
};
|
||||
|
||||
export default TimelineQuery;
|
@ -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();
|
||||
|
14
webui/src/list/BugRow.graphql
Normal file
14
webui/src/list/BugRow.graphql
Normal file
@ -0,0 +1,14 @@
|
||||
#import "../Author.graphql"
|
||||
#import "../Label.graphql"
|
||||
|
||||
fragment BugRow on Bug {
|
||||
id
|
||||
humanId
|
||||
title
|
||||
status
|
||||
createdAt
|
||||
labels {
|
||||
...Label
|
||||
}
|
||||
...authored
|
||||
}
|
@ -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;
|
@ -1,13 +1,18 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import { SvgIconProps } from '@material-ui/core/SvgIcon';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
|
||||
import clsx from 'clsx';
|
||||
import { LocationDescriptor } from 'history';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function parse(query) {
|
||||
export type Query = { [key: string]: Array<string> };
|
||||
|
||||
function parse(query: string): Query {
|
||||
// TODO: extract the rest of the query?
|
||||
const params = {};
|
||||
const params: Query = {};
|
||||
|
||||
// TODO: support escaping without quotes
|
||||
const re = /(\w+):([A-Za-z0-9-]+|(["'])(([^\3]|\\.)*)\3)+/g;
|
||||
@ -29,7 +34,7 @@ function parse(query) {
|
||||
return params;
|
||||
}
|
||||
|
||||
function quote(value) {
|
||||
function quote(value: string): string {
|
||||
const hasSingle = value.includes("'");
|
||||
const hasDouble = value.includes('"');
|
||||
const hasSpaces = value.includes(' ');
|
||||
@ -49,19 +54,19 @@ function quote(value) {
|
||||
return `"${value}"`;
|
||||
}
|
||||
|
||||
function stringify(params) {
|
||||
const parts = Object.entries(params).map(([key, values]) => {
|
||||
function stringify(params: Query): string {
|
||||
const parts: string[][] = Object.entries(params).map(([key, values]) => {
|
||||
return values.map(value => `${key}:${quote(value)}`);
|
||||
});
|
||||
return [].concat(...parts).join(' ');
|
||||
return new Array<string>().concat(...parts).join(' ');
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
element: {
|
||||
...theme.typography.body2,
|
||||
color: ({ active }) => (active ? '#333' : '#444'),
|
||||
color: '#444',
|
||||
padding: theme.spacing(0, 1),
|
||||
fontWeight: ({ active }) => (active ? 600 : 400),
|
||||
fontWeight: 400,
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
background: 'none',
|
||||
@ -69,21 +74,51 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
itemActive: {
|
||||
fontWeight: 600,
|
||||
color: '#333',
|
||||
},
|
||||
icon: {
|
||||
paddingRight: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
function Dropdown({ children, dropdown, itemActive, to, ...props }) {
|
||||
type DropdownTuple = [string, string];
|
||||
|
||||
type FilterDropdownProps = {
|
||||
children: React.ReactNode;
|
||||
dropdown: DropdownTuple[];
|
||||
itemActive: (key: string) => boolean;
|
||||
icon?: React.ComponentType<SvgIconProps>;
|
||||
to: (key: string) => LocationDescriptor;
|
||||
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
function FilterDropdown({
|
||||
children,
|
||||
dropdown,
|
||||
itemActive,
|
||||
icon: Icon,
|
||||
to,
|
||||
...props
|
||||
}: FilterDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const buttonRef = useRef();
|
||||
const classes = useStyles();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const classes = useStyles({ active: false });
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button ref={buttonRef} onClick={() => setOpen(!open)} {...props}>
|
||||
{children}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={() => setOpen(!open)}
|
||||
className={classes.element}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
<ArrowDropDown fontSize="small" />
|
||||
</button>
|
||||
<Menu
|
||||
@ -104,7 +139,7 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={to(key)}
|
||||
className={itemActive(key) ? classes.itemActive : null}
|
||||
className={itemActive(key) ? classes.itemActive : undefined}
|
||||
onClick={() => setOpen(false)}
|
||||
key={key}
|
||||
>
|
||||
@ -116,8 +151,14 @@ function Dropdown({ children, dropdown, itemActive, to, ...props }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
|
||||
const classes = useStyles({ active });
|
||||
export type FilterProps = {
|
||||
active: boolean;
|
||||
to: LocationDescriptor;
|
||||
icon?: React.ComponentType<SvgIconProps>;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
function Filter({ active, to, children, icon: Icon }: FilterProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const content = (
|
||||
<>
|
||||
@ -126,29 +167,23 @@ function Filter({ active, to, children, icon: Icon, dropdown, ...props }) {
|
||||
</>
|
||||
);
|
||||
|
||||
if (dropdown) {
|
||||
return (
|
||||
<Dropdown
|
||||
{...props}
|
||||
to={to}
|
||||
dropdown={dropdown}
|
||||
className={classes.element}
|
||||
>
|
||||
{content}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<Link to={to} {...props} className={classes.element}>
|
||||
<Link
|
||||
to={to}
|
||||
className={clsx(classes.element, active && classes.itemActive)}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={classes.element}>{content}</div>;
|
||||
return (
|
||||
<div className={clsx(classes.element, active && classes.itemActive)}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Filter;
|
||||
export { parse, stringify, quote };
|
||||
export { parse, stringify, quote, FilterDropdown, Filter };
|
7
webui/src/list/FilterToolbar.graphql
Normal file
7
webui/src/list/FilterToolbar.graphql
Normal file
@ -0,0 +1,7 @@
|
||||
query BugCount($query: String) {
|
||||
repository {
|
||||
bugs: allBugs(query: $query) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import Table from '@material-ui/core/Table/Table';
|
||||
import TableBody from '@material-ui/core/TableBody/TableBody';
|
||||
import React from 'react';
|
||||
import BugRow from './BugRow';
|
||||
|
||||
function List({ bugs }) {
|
||||
import BugRow from './BugRow';
|
||||
import { BugListFragment } from './ListQuery.generated';
|
||||
|
||||
type Props = { bugs: BugListFragment };
|
||||
function List({ bugs }: Props) {
|
||||
return (
|
||||
<Table>
|
||||
<TableBody>
|
37
webui/src/list/ListQuery.graphql
Normal file
37
webui/src/list/ListQuery.graphql
Normal file
@ -0,0 +1,37 @@
|
||||
#import "./BugRow.graphql"
|
||||
|
||||
query ListBugs(
|
||||
$first: Int
|
||||
$last: Int
|
||||
$after: String
|
||||
$before: String
|
||||
$query: String
|
||||
) {
|
||||
repository {
|
||||
bugs: allBugs(
|
||||
first: $first
|
||||
last: $last
|
||||
after: $after
|
||||
before: $before
|
||||
query: $query
|
||||
) {
|
||||
...BugList
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment BugList on BugConnection {
|
||||
totalCount
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
...BugRow
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +1,21 @@
|
||||
import { fade, makeStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import InputBase from '@material-ui/core/InputBase';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { fade, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import ErrorOutline from '@material-ui/icons/ErrorOutline';
|
||||
import KeyboardArrowLeft from '@material-ui/icons/KeyboardArrowLeft';
|
||||
import KeyboardArrowRight from '@material-ui/icons/KeyboardArrowRight';
|
||||
import ErrorOutline from '@material-ui/icons/ErrorOutline';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import InputBase from '@material-ui/core/InputBase';
|
||||
import Skeleton from '@material-ui/lab/Skeleton';
|
||||
import gql from 'graphql-tag';
|
||||
import { ApolloError } from 'apollo-boost';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { useLocation, useHistory, Link } from 'react-router-dom';
|
||||
import BugRow from './BugRow';
|
||||
import List from './List';
|
||||
import FilterToolbar from './FilterToolbar';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
import FilterToolbar from './FilterToolbar';
|
||||
import List from './List';
|
||||
import { useListBugsQuery } from './ListQuery.generated';
|
||||
|
||||
type StylesProps = { searching?: boolean };
|
||||
const useStyles = makeStyles<Theme, StylesProps>(theme => ({
|
||||
main: {
|
||||
maxWidth: 800,
|
||||
margin: 'auto',
|
||||
@ -46,7 +47,11 @@ const useStyles = makeStyles(theme => ({
|
||||
backgroundColor: fade(theme.palette.primary.main, 0.05),
|
||||
padding: theme.spacing(0, 1),
|
||||
width: ({ searching }) => (searching ? '20rem' : '15rem'),
|
||||
transition: theme.transitions.create(),
|
||||
transition: theme.transitions.create([
|
||||
'width',
|
||||
'borderColor',
|
||||
'backgroundColor',
|
||||
]),
|
||||
},
|
||||
searchFocused: {
|
||||
borderColor: fade(theme.palette.primary.main, 0.4),
|
||||
@ -91,51 +96,21 @@ const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const QUERY = gql`
|
||||
query(
|
||||
$first: Int
|
||||
$last: Int
|
||||
$after: String
|
||||
$before: String
|
||||
$query: String
|
||||
) {
|
||||
defaultRepository {
|
||||
bugs: allBugs(
|
||||
first: $first
|
||||
last: $last
|
||||
after: $after
|
||||
before: $before
|
||||
query: $query
|
||||
) {
|
||||
totalCount
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
...BugRow
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${BugRow.fragment}
|
||||
`;
|
||||
|
||||
function editParams(params, callback) {
|
||||
function editParams(
|
||||
params: URLSearchParams,
|
||||
callback: (params: URLSearchParams) => void
|
||||
) {
|
||||
const cloned = new URLSearchParams(params.toString());
|
||||
callback(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
// TODO: factor this out
|
||||
const Placeholder = ({ count }) => {
|
||||
const classes = useStyles();
|
||||
type PlaceholderProps = { count: number };
|
||||
const Placeholder: React.FC<PlaceholderProps> = ({
|
||||
count,
|
||||
}: PlaceholderProps) => {
|
||||
const classes = useStyles({});
|
||||
return (
|
||||
<>
|
||||
{new Array(count).fill(null).map((_, i) => (
|
||||
@ -158,7 +133,7 @@ const Placeholder = ({ count }) => {
|
||||
|
||||
// TODO: factor this out
|
||||
const NoBug = () => {
|
||||
const classes = useStyles();
|
||||
const classes = useStyles({});
|
||||
return (
|
||||
<div className={classes.message}>
|
||||
<ErrorOutline fontSize="large" />
|
||||
@ -167,8 +142,9 @@ const NoBug = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const Error = ({ error }) => {
|
||||
const classes = useStyles();
|
||||
type ErrorProps = { error: ApolloError };
|
||||
const Error: React.FC<ErrorProps> = ({ error }: ErrorProps) => {
|
||||
const classes = useStyles({});
|
||||
return (
|
||||
<div className={[classes.errorBox, classes.message].join(' ')}>
|
||||
<ErrorOutline fontSize="large" />
|
||||
@ -194,7 +170,7 @@ function ListQuery() {
|
||||
const classes = useStyles({ searching: !!input });
|
||||
|
||||
// TODO is this the right way to do it?
|
||||
const lastQuery = useRef();
|
||||
const lastQuery = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (query !== lastQuery.current) {
|
||||
setInput(query);
|
||||
@ -202,9 +178,10 @@ function ListQuery() {
|
||||
lastQuery.current = query;
|
||||
}, [query, input, lastQuery]);
|
||||
|
||||
const num = (param: string | null) => (param ? parseInt(param) : null);
|
||||
const page = {
|
||||
first: params.get('first'),
|
||||
last: params.get('last'),
|
||||
first: num(params.get('first')),
|
||||
last: num(params.get('last')),
|
||||
after: params.get('after'),
|
||||
before: params.get('before'),
|
||||
};
|
||||
@ -214,9 +191,9 @@ function ListQuery() {
|
||||
page.first = 10;
|
||||
}
|
||||
|
||||
const perPage = page.first || page.last;
|
||||
const perPage = (page.first || page.last || 10).toString();
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY, {
|
||||
const { loading, error, data } = useListBugsQuery({
|
||||
variables: {
|
||||
...page,
|
||||
query,
|
||||
@ -225,34 +202,34 @@ function ListQuery() {
|
||||
|
||||
let nextPage = null;
|
||||
let previousPage = null;
|
||||
let hasNextPage = false;
|
||||
let hasPreviousPage = false;
|
||||
let count = 0;
|
||||
if (!loading && !error && data.defaultRepository.bugs) {
|
||||
const bugs = data.defaultRepository.bugs;
|
||||
hasNextPage = bugs.pageInfo.hasNextPage;
|
||||
hasPreviousPage = bugs.pageInfo.hasPreviousPage;
|
||||
if (!loading && !error && data?.repository?.bugs) {
|
||||
const bugs = data.repository.bugs;
|
||||
count = bugs.totalCount;
|
||||
// This computes the URL for the next page
|
||||
nextPage = {
|
||||
...location,
|
||||
search: editParams(params, p => {
|
||||
p.delete('last');
|
||||
p.delete('before');
|
||||
p.set('first', perPage);
|
||||
p.set('after', bugs.pageInfo.endCursor);
|
||||
}).toString(),
|
||||
};
|
||||
if (bugs.pageInfo.hasNextPage) {
|
||||
nextPage = {
|
||||
...location,
|
||||
search: editParams(params, p => {
|
||||
p.delete('last');
|
||||
p.delete('before');
|
||||
p.set('first', perPage);
|
||||
p.set('after', bugs.pageInfo.endCursor);
|
||||
}).toString(),
|
||||
};
|
||||
}
|
||||
// and this for the previous page
|
||||
previousPage = {
|
||||
...location,
|
||||
search: editParams(params, p => {
|
||||
p.delete('first');
|
||||
p.delete('after');
|
||||
p.set('last', perPage);
|
||||
p.set('before', bugs.pageInfo.startCursor);
|
||||
}).toString(),
|
||||
};
|
||||
if (bugs.pageInfo.hasPreviousPage) {
|
||||
previousPage = {
|
||||
...location,
|
||||
search: editParams(params, p => {
|
||||
p.delete('first');
|
||||
p.delete('after');
|
||||
p.set('last', perPage);
|
||||
p.set('before', bugs.pageInfo.startCursor);
|
||||
}).toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare params without paging for editing filters
|
||||
@ -263,7 +240,7 @@ function ListQuery() {
|
||||
p.delete('after');
|
||||
});
|
||||
// Returns a new location with the `q` param edited
|
||||
const queryLocation = query => ({
|
||||
const queryLocation = (query: string) => ({
|
||||
...location,
|
||||
search: editParams(paramsWithoutPaging, p => p.set('q', query)).toString(),
|
||||
});
|
||||
@ -273,8 +250,8 @@ function ListQuery() {
|
||||
content = <Placeholder count={10} />;
|
||||
} else if (error) {
|
||||
content = <Error error={error} />;
|
||||
} else {
|
||||
const bugs = data.defaultRepository.bugs;
|
||||
} else if (data?.repository) {
|
||||
const bugs = data.repository.bugs;
|
||||
|
||||
if (bugs.totalCount === 0) {
|
||||
content = <NoBug />;
|
||||
@ -283,7 +260,7 @@ function ListQuery() {
|
||||
}
|
||||
}
|
||||
|
||||
const formSubmit = e => {
|
||||
const formSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
history.push(queryLocation(input));
|
||||
};
|
||||
@ -296,7 +273,7 @@ function ListQuery() {
|
||||
<InputBase
|
||||
placeholder="Filter"
|
||||
value={input}
|
||||
onInput={e => setInput(e.target.value)}
|
||||
onInput={(e: any) => setInput(e.target.value)}
|
||||
classes={{
|
||||
root: classes.search,
|
||||
focused: classes.searchFocused,
|
||||
@ -310,21 +287,25 @@ function ListQuery() {
|
||||
<FilterToolbar query={query} queryLocation={queryLocation} />
|
||||
{content}
|
||||
<div className={classes.pagination}>
|
||||
<IconButton
|
||||
component={hasPreviousPage ? Link : 'button'}
|
||||
to={previousPage}
|
||||
disabled={!hasPreviousPage}
|
||||
>
|
||||
<KeyboardArrowLeft />
|
||||
</IconButton>
|
||||
{previousPage ? (
|
||||
<IconButton component={Link} to={previousPage}>
|
||||
<KeyboardArrowLeft />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton disabled>
|
||||
<KeyboardArrowLeft />
|
||||
</IconButton>
|
||||
)}
|
||||
<div>{loading ? 'Loading' : `Total: ${count}`}</div>
|
||||
<IconButton
|
||||
component={hasNextPage ? Link : 'button'}
|
||||
to={nextPage}
|
||||
disabled={!hasNextPage}
|
||||
>
|
||||
<KeyboardArrowRight />
|
||||
</IconButton>
|
||||
{nextPage ? (
|
||||
<IconButton component={Link} to={nextPage}>
|
||||
<KeyboardArrowRight />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton disabled>
|
||||
<KeyboardArrowRight />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
1
webui/src/react-app-env.d.ts
vendored
Normal file
1
webui/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
@ -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">
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
tag: {
|
||||
@ -8,7 +8,7 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
const PreTag = props => {
|
||||
const PreTag = (props: React.HTMLProps<HTMLPreElement>) => {
|
||||
const classes = useStyles();
|
||||
return <pre className={classes.tag} {...props}></pre>;
|
||||
};
|
20
webui/tsconfig.json
Normal file
20
webui/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"typeRoots": ["node_modules/@types/", "types/"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
6
webui/types/remark-html/index.d.ts
vendored
Normal file
6
webui/types/remark-html/index.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module 'remark-html' {
|
||||
import { Plugin } from 'unified';
|
||||
|
||||
const plugin: Plugin;
|
||||
export default plugin;
|
||||
}
|
6
webui/types/remark-react/index.d.ts
vendored
Normal file
6
webui/types/remark-react/index.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
declare module 'remark-react' {
|
||||
import { Plugin } from 'unified';
|
||||
|
||||
const plugin: Plugin;
|
||||
export default plugin;
|
||||
}
|
Loading…
Reference in New Issue
Block a user