mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-14 08:45:30 +03:00
parent
23228101a2
commit
4a28f25347
@ -19,15 +19,17 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/MichaelMure/git-bug/graphql"
|
"github.com/MichaelMure/git-bug/graphql"
|
||||||
|
"github.com/MichaelMure/git-bug/graphql/config"
|
||||||
"github.com/MichaelMure/git-bug/repository"
|
"github.com/MichaelMure/git-bug/repository"
|
||||||
"github.com/MichaelMure/git-bug/util/git"
|
"github.com/MichaelMure/git-bug/util/git"
|
||||||
"github.com/MichaelMure/git-bug/webui"
|
"github.com/MichaelMure/git-bug/webui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
webUIPort int
|
webUIPort int
|
||||||
webUIOpen bool
|
webUIOpen bool
|
||||||
webUINoOpen bool
|
webUINoOpen bool
|
||||||
|
webUIReadOnly bool
|
||||||
)
|
)
|
||||||
|
|
||||||
const webUIOpenConfigKey = "git-bug.webui.open"
|
const webUIOpenConfigKey = "git-bug.webui.open"
|
||||||
@ -46,7 +48,7 @@ func runWebUI(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
|
|
||||||
graphqlHandler, err := graphql.NewHandler(repo)
|
graphqlHandler, err := graphql.NewHandler(repo, config.Config{ReadOnly: webUIReadOnly})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -261,5 +263,6 @@ func init() {
|
|||||||
webUICmd.Flags().BoolVar(&webUIOpen, "open", false, "Automatically open the web UI in the default browser")
|
webUICmd.Flags().BoolVar(&webUIOpen, "open", false, "Automatically open the web UI in the default browser")
|
||||||
webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
|
webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser")
|
||||||
webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)")
|
webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)")
|
||||||
|
webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,10 @@ Available git config:
|
|||||||
\fB\-p\fP, \fB\-\-port\fP=0
|
\fB\-p\fP, \fB\-\-port\fP=0
|
||||||
Port to listen to (default is random)
|
Port to listen to (default is random)
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-read\-only\fP[=false]
|
||||||
|
Whether to run the web UI in read\-only mode
|
||||||
|
|
||||||
.PP
|
.PP
|
||||||
\fB\-h\fP, \fB\-\-help\fP[=false]
|
\fB\-h\fP, \fB\-\-help\fP[=false]
|
||||||
help for webui
|
help for webui
|
||||||
|
@ -17,10 +17,11 @@ git-bug webui [flags]
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
--open Automatically open the web UI in the default browser
|
--open Automatically open the web UI in the default browser
|
||||||
--no-open Prevent the automatic opening of the web UI in the default browser
|
--no-open Prevent the automatic opening of the web UI in the default browser
|
||||||
-p, --port int Port to listen to (default is random)
|
-p, --port int Port to listen to (default is random)
|
||||||
-h, --help help for webui
|
--read-only Whether to run the web UI in read-only mode
|
||||||
|
-h, --help help for webui
|
||||||
```
|
```
|
||||||
|
|
||||||
### SEE ALSO
|
### SEE ALSO
|
||||||
|
7
graphql/config/config.go
Normal file
7
graphql/config/config.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Package config contains configuration for GraphQL stuff.
|
||||||
|
package config
|
||||||
|
|
||||||
|
// Config holds configuration elements.
|
||||||
|
type Config struct {
|
||||||
|
ReadOnly bool
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/99designs/gqlgen/client"
|
"github.com/99designs/gqlgen/client"
|
||||||
|
|
||||||
|
"github.com/MichaelMure/git-bug/graphql/config"
|
||||||
"github.com/MichaelMure/git-bug/graphql/models"
|
"github.com/MichaelMure/git-bug/graphql/models"
|
||||||
"github.com/MichaelMure/git-bug/misc/random_bugs"
|
"github.com/MichaelMure/git-bug/misc/random_bugs"
|
||||||
"github.com/MichaelMure/git-bug/repository"
|
"github.com/MichaelMure/git-bug/repository"
|
||||||
@ -16,7 +17,7 @@ func TestQueries(t *testing.T) {
|
|||||||
|
|
||||||
random_bugs.FillRepoWithSeed(repo, 10, 42)
|
random_bugs.FillRepoWithSeed(repo, 10, 42)
|
||||||
|
|
||||||
handler, err := NewHandler(repo)
|
handler, err := NewHandler(repo, config.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"github.com/99designs/gqlgen/graphql/handler"
|
||||||
|
|
||||||
|
"github.com/MichaelMure/git-bug/graphql/config"
|
||||||
"github.com/MichaelMure/git-bug/graphql/graph"
|
"github.com/MichaelMure/git-bug/graphql/graph"
|
||||||
"github.com/MichaelMure/git-bug/graphql/resolvers"
|
"github.com/MichaelMure/git-bug/graphql/resolvers"
|
||||||
"github.com/MichaelMure/git-bug/repository"
|
"github.com/MichaelMure/git-bug/repository"
|
||||||
@ -19,9 +20,9 @@ type Handler struct {
|
|||||||
*resolvers.RootResolver
|
*resolvers.RootResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(repo repository.ClockedRepo) (Handler, error) {
|
func NewHandler(repo repository.ClockedRepo, cfg config.Config) (Handler, error) {
|
||||||
h := Handler{
|
h := Handler{
|
||||||
RootResolver: resolvers.NewRootResolver(),
|
RootResolver: resolvers.NewRootResolver(cfg),
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.RootResolver.RegisterDefaultRepository(repo)
|
err := h.RootResolver.RegisterDefaultRepository(repo)
|
||||||
|
@ -7,8 +7,32 @@ import (
|
|||||||
"github.com/MichaelMure/git-bug/cache"
|
"github.com/MichaelMure/git-bug/cache"
|
||||||
"github.com/MichaelMure/git-bug/graphql/graph"
|
"github.com/MichaelMure/git-bug/graphql/graph"
|
||||||
"github.com/MichaelMure/git-bug/graphql/models"
|
"github.com/MichaelMure/git-bug/graphql/models"
|
||||||
|
"github.com/vektah/gqlparser/gqlerror"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ graph.MutationResolver = &readonlyMutationResolver{}
|
||||||
|
|
||||||
|
type readonlyMutationResolver struct{}
|
||||||
|
|
||||||
|
func (readonlyMutationResolver) NewBug(_ context.Context, _ models.NewBugInput) (*models.NewBugPayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
func (readonlyMutationResolver) AddComment(_ context.Context, input models.AddCommentInput) (*models.AddCommentPayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
func (readonlyMutationResolver) ChangeLabels(_ context.Context, input *models.ChangeLabelInput) (*models.ChangeLabelPayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
func (readonlyMutationResolver) OpenBug(_ context.Context, input models.OpenBugInput) (*models.OpenBugPayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
func (readonlyMutationResolver) CloseBug(_ context.Context, input models.CloseBugInput) (*models.CloseBugPayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
func (readonlyMutationResolver) SetTitle(_ context.Context, input models.SetTitleInput) (*models.SetTitlePayload, error) {
|
||||||
|
return nil, gqlerror.Errorf("readonly mode")
|
||||||
|
}
|
||||||
|
|
||||||
var _ graph.MutationResolver = &mutationResolver{}
|
var _ graph.MutationResolver = &mutationResolver{}
|
||||||
|
|
||||||
type mutationResolver struct {
|
type mutationResolver struct {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/MichaelMure/git-bug/bug"
|
"github.com/MichaelMure/git-bug/bug"
|
||||||
"github.com/MichaelMure/git-bug/entity"
|
"github.com/MichaelMure/git-bug/entity"
|
||||||
|
"github.com/MichaelMure/git-bug/graphql/config"
|
||||||
"github.com/MichaelMure/git-bug/graphql/connections"
|
"github.com/MichaelMure/git-bug/graphql/connections"
|
||||||
"github.com/MichaelMure/git-bug/graphql/graph"
|
"github.com/MichaelMure/git-bug/graphql/graph"
|
||||||
"github.com/MichaelMure/git-bug/graphql/models"
|
"github.com/MichaelMure/git-bug/graphql/models"
|
||||||
@ -13,7 +14,7 @@ import (
|
|||||||
|
|
||||||
var _ graph.RepositoryResolver = &repoResolver{}
|
var _ graph.RepositoryResolver = &repoResolver{}
|
||||||
|
|
||||||
type repoResolver struct{}
|
type repoResolver struct{ cfg config.Config }
|
||||||
|
|
||||||
func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) {
|
func (repoResolver) Name(_ context.Context, obj *models.Repository) (*string, error) {
|
||||||
name := obj.Repo.Name()
|
name := obj.Repo.Name()
|
||||||
@ -149,7 +150,10 @@ func (repoResolver) Identity(_ context.Context, obj *models.Repository, prefix s
|
|||||||
return models.NewLazyIdentity(obj.Repo, excerpt), nil
|
return models.NewLazyIdentity(obj.Repo, excerpt), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (models.IdentityWrapper, error) {
|
func (r repoResolver) UserIdentity(_ context.Context, obj *models.Repository) (models.IdentityWrapper, error) {
|
||||||
|
if r.cfg.ReadOnly {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
excerpt, err := obj.Repo.GetUserIdentityExcerpt()
|
excerpt, err := obj.Repo.GetUserIdentityExcerpt()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -3,6 +3,7 @@ package resolvers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/MichaelMure/git-bug/cache"
|
"github.com/MichaelMure/git-bug/cache"
|
||||||
|
"github.com/MichaelMure/git-bug/graphql/config"
|
||||||
"github.com/MichaelMure/git-bug/graphql/graph"
|
"github.com/MichaelMure/git-bug/graphql/graph"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -10,11 +11,13 @@ var _ graph.ResolverRoot = &RootResolver{}
|
|||||||
|
|
||||||
type RootResolver struct {
|
type RootResolver struct {
|
||||||
cache.MultiRepoCache
|
cache.MultiRepoCache
|
||||||
|
cfg config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRootResolver() *RootResolver {
|
func NewRootResolver(cfg config.Config) *RootResolver {
|
||||||
return &RootResolver{
|
return &RootResolver{
|
||||||
MultiRepoCache: cache.NewMultiRepoCache(),
|
MultiRepoCache: cache.NewMultiRepoCache(),
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,13 +28,16 @@ func (r RootResolver) Query() graph.QueryResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r RootResolver) Mutation() graph.MutationResolver {
|
func (r RootResolver) Mutation() graph.MutationResolver {
|
||||||
|
if r.cfg.ReadOnly {
|
||||||
|
return &readonlyMutationResolver{}
|
||||||
|
}
|
||||||
return &mutationResolver{
|
return &mutationResolver{
|
||||||
cache: &r.MultiRepoCache,
|
cache: &r.MultiRepoCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (RootResolver) Repository() graph.RepositoryResolver {
|
func (r RootResolver) Repository() graph.RepositoryResolver {
|
||||||
return &repoResolver{}
|
return &repoResolver{r.cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (RootResolver) Bug() graph.BugResolver {
|
func (RootResolver) Bug() graph.BugResolver {
|
||||||
@ -50,7 +56,7 @@ func (RootResolver) Label() graph.LabelResolver {
|
|||||||
return &labelResolver{}
|
return &labelResolver{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r RootResolver) Identity() graph.IdentityResolver {
|
func (RootResolver) Identity() graph.IdentityResolver {
|
||||||
return &identityResolver{}
|
return &identityResolver{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1213,6 +1213,8 @@ _git-bug_webui()
|
|||||||
two_word_flags+=("--port")
|
two_word_flags+=("--port")
|
||||||
two_word_flags+=("-p")
|
two_word_flags+=("-p")
|
||||||
local_nonpersistent_flags+=("--port=")
|
local_nonpersistent_flags+=("--port=")
|
||||||
|
flags+=("--read-only")
|
||||||
|
local_nonpersistent_flags+=("--read-only")
|
||||||
|
|
||||||
must_have_one_flag=()
|
must_have_one_flag=()
|
||||||
must_have_one_noun=()
|
must_have_one_noun=()
|
||||||
|
@ -240,6 +240,7 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
|
|||||||
[CompletionResult]::new('--no-open', 'no-open', [CompletionResultType]::ParameterName, 'Prevent the automatic opening of the web UI in the default browser')
|
[CompletionResult]::new('--no-open', 'no-open', [CompletionResultType]::ParameterName, 'Prevent the automatic opening of the web UI in the default browser')
|
||||||
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)')
|
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)')
|
||||||
[CompletionResult]::new('--port', 'port', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)')
|
[CompletionResult]::new('--port', 'port', [CompletionResultType]::ParameterName, 'Port to listen to (default is random)')
|
||||||
|
[CompletionResult]::new('--read-only', 'read-only', [CompletionResultType]::ParameterName, 'Whether to run the web UI in read-only mode')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -459,6 +459,7 @@ function _git-bug_webui {
|
|||||||
_arguments \
|
_arguments \
|
||||||
'--open[Automatically open the web UI in the default browser]' \
|
'--open[Automatically open the web UI in the default browser]' \
|
||||||
'--no-open[Prevent the automatic opening of the web UI in the default browser]' \
|
'--no-open[Prevent the automatic opening of the web UI in the default browser]' \
|
||||||
'(-p --port)'{-p,--port}'[Port to listen to (default is random)]:'
|
'(-p --port)'{-p,--port}'[Port to listen to (default is random)]:' \
|
||||||
|
'--read-only[Whether to run the web UI in read-only mode]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import Avatar from '@material-ui/core/Avatar';
|
import Avatar from '@material-ui/core/Avatar';
|
||||||
import { makeStyles } from '@material-ui/core/styles';
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
|
import CurrentIdentityContext from './CurrentIdentityContext';
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles(theme => ({
|
||||||
displayName: {
|
displayName: {
|
||||||
@ -13,18 +13,26 @@ const useStyles = makeStyles(theme => ({
|
|||||||
|
|
||||||
const CurrentIdentity = () => {
|
const CurrentIdentity = () => {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { loading, error, data } = useCurrentIdentityQuery();
|
|
||||||
|
|
||||||
if (error || loading || !data?.repository?.userIdentity) return null;
|
|
||||||
|
|
||||||
const user = data.repository.userIdentity;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<CurrentIdentityContext.Consumer>
|
||||||
<Avatar src={user.avatarUrl ? user.avatarUrl : undefined}>
|
{context => {
|
||||||
{user.displayName.charAt(0).toUpperCase()}
|
if (!context) return null;
|
||||||
</Avatar>
|
const { loading, error, data } = context as any;
|
||||||
<div className={classes.displayName}>{user.displayName}</div>
|
|
||||||
</>
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</CurrentIdentityContext.Consumer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
6
webui/src/layout/CurrentIdentityContext.tsx
Normal file
6
webui/src/layout/CurrentIdentityContext.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { CurrentIdentityQueryResult } from './CurrentIdentity.generated';
|
||||||
|
|
||||||
|
const Context = React.createContext(null as CurrentIdentityQueryResult | null);
|
||||||
|
export default Context;
|
19
webui/src/layout/ReadonlyHidden.tsx
Normal file
19
webui/src/layout/ReadonlyHidden.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CurrentIdentityContext from './CurrentIdentityContext';
|
||||||
|
|
||||||
|
type Props = { children: React.ReactNode };
|
||||||
|
const ReadonlyHidden = ({ children }: Props) => (
|
||||||
|
<CurrentIdentityContext.Consumer>
|
||||||
|
{context => {
|
||||||
|
if (!context) return null;
|
||||||
|
const { loading, error, data } = context;
|
||||||
|
|
||||||
|
if (error || loading || !data?.repository?.userIdentity) return null;
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}}
|
||||||
|
</CurrentIdentityContext.Consumer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ReadonlyHidden;
|
@ -2,16 +2,18 @@ import React from 'react';
|
|||||||
|
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||||
|
|
||||||
|
import { useCurrentIdentityQuery } from './CurrentIdentity.generated';
|
||||||
|
import CurrentIdentityContext from './CurrentIdentityContext';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
|
||||||
type Props = { children: React.ReactNode };
|
type Props = { children: React.ReactNode };
|
||||||
function Layout({ children }: Props) {
|
function Layout({ children }: Props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<CurrentIdentityContext.Provider value={useCurrentIdentityQuery()}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</CurrentIdentityContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core/styles';
|
|||||||
import Author from 'src/components/Author';
|
import Author from 'src/components/Author';
|
||||||
import Date from 'src/components/Date';
|
import Date from 'src/components/Date';
|
||||||
import Label from 'src/components/Label';
|
import Label from 'src/components/Label';
|
||||||
|
import ReadonlyHidden from 'src/layout/ReadonlyHidden';
|
||||||
|
|
||||||
import { BugFragment } from './Bug.generated';
|
import { BugFragment } from './Bug.generated';
|
||||||
import CommentForm from './CommentForm';
|
import CommentForm from './CommentForm';
|
||||||
@ -88,9 +89,11 @@ function Bug({ bug }: Props) {
|
|||||||
<div className={classes.container}>
|
<div className={classes.container}>
|
||||||
<div className={classes.timeline}>
|
<div className={classes.timeline}>
|
||||||
<TimelineQuery id={bug.id} />
|
<TimelineQuery id={bug.id} />
|
||||||
<div className={classes.commentForm}>
|
<ReadonlyHidden>
|
||||||
<CommentForm bugId={bug.id} />
|
<div className={classes.commentForm}>
|
||||||
</div>
|
<CommentForm bugId={bug.id} />
|
||||||
|
</div>
|
||||||
|
</ReadonlyHidden>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.sidebar}>
|
<div className={classes.sidebar}>
|
||||||
<span className={classes.sidebarTitle}>Labels</span>
|
<span className={classes.sidebarTitle}>Labels</span>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
mutation AddComment($input: AddCommentInput!) {
|
mutation AddComment($input: AddCommentInput!) {
|
||||||
addComment(input: $input) {
|
addComment(input: $input) {
|
||||||
operation { id }
|
operation {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user