Collaborators have write access as default, and can be changed via repository collaboration settings page to change between read, write and admin.tags/v0.9.99
@@ -190,6 +190,8 @@ func runWeb(ctx *cli.Context) { | |||
bindIgnErr := binding.BindIgnErr | |||
// FIXME: not all routes need go through same middlewares. | |||
// Especially some AJAX requests, we can reduce middleware number to improve performance. | |||
// Routers. | |||
m.Get("/", ignSignIn, routers.Home) | |||
m.Get("/explore", ignSignIn, routers.Explore) | |||
@@ -400,7 +402,11 @@ func runWeb(ctx *cli.Context) { | |||
m.Group("/settings", func() { | |||
m.Combo("").Get(repo.Settings). | |||
Post(bindIgnErr(auth.RepoSettingForm{}), repo.SettingsPost) | |||
m.Combo("/collaboration").Get(repo.Collaboration).Post(repo.CollaborationPost) | |||
m.Group("/collaboration", func() { | |||
m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) | |||
m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | |||
m.Post("/delete", repo.DeleteCollaboration) | |||
}) | |||
m.Group("/hooks", func() { | |||
m.Get("", repo.Webhooks) |
@@ -221,8 +221,6 @@ still_own_repo = Your account still has ownership over at least one repository, | |||
still_has_org = Your account still has membership in at least one organization, you have to leave or delete your memberships first. | |||
org_still_own_repo = This organization still has ownership of repositories, you must delete or transfer them first. | |||
still_own_user = This authentication is still in use by at least one user, please remove them from the authentication and try again. | |||
target_branch_not_exist = Target branch does not exist. | |||
[user] | |||
@@ -615,6 +613,9 @@ settings.transfer_succeed = Repository ownership has been transferred successful | |||
settings.confirm_delete = Confirm Deletion | |||
settings.add_collaborator = Add New Collaborator | |||
settings.add_collaborator_success = New collaborator has been added. | |||
settings.delete_collaborator = Delete | |||
settings.collaborator_deletion = Collaborator Deletion | |||
settings.collaborator_deletion_desc = This user will no longer have collaboration access to this repository after deletion. Do you want to continue? | |||
settings.remove_collaborator_success = Collaborator has been removed. | |||
settings.search_user_placeholder = Search user... | |||
settings.org_not_allowed_to_be_collaborator = Organization is not allowed to be added as a collaborator. | |||
@@ -949,6 +950,7 @@ auths.update = Update Authentication Setting | |||
auths.delete = Delete This Authentication | |||
auths.delete_auth_title = Authentication Deletion | |||
auths.delete_auth_desc = This authentication is going to be deleted, do you want to continue? | |||
auths.still_in_used = This authentication is still used by some users, please delete or convert these users to another login type first. | |||
auths.deletion_success = Authentication has been deleted successfully! | |||
config.server_config = Server Configuration |
@@ -13,11 +13,11 @@ import ( | |||
type AccessMode int | |||
const ( | |||
ACCESS_MODE_NONE AccessMode = iota | |||
ACCESS_MODE_READ | |||
ACCESS_MODE_WRITE | |||
ACCESS_MODE_ADMIN | |||
ACCESS_MODE_OWNER | |||
ACCESS_MODE_NONE AccessMode = iota // 0 | |||
ACCESS_MODE_READ // 1 | |||
ACCESS_MODE_WRITE // 2 | |||
ACCESS_MODE_ADMIN // 3 | |||
ACCESS_MODE_OWNER // 4 | |||
) | |||
// Access represents the highest access level of a user to the repository. The only access type | |||
@@ -151,15 +151,14 @@ func (repo *Repository) refreshAccesses(e Engine, accessMap map[int64]AccessMode | |||
return nil | |||
} | |||
// FIXME: should be able to have read-only access. | |||
// Give all collaborators write access. | |||
// refreshCollaboratorAccesses retrieves repository collaborations with their access modes. | |||
func (repo *Repository) refreshCollaboratorAccesses(e Engine, accessMap map[int64]AccessMode) error { | |||
collaborators, err := repo.getCollaborators(e) | |||
collaborations, err := repo.getCollaborations(e) | |||
if err != nil { | |||
return fmt.Errorf("getCollaborators: %v", err) | |||
return fmt.Errorf("getCollaborations: %v", err) | |||
} | |||
for _, c := range collaborators { | |||
accessMap[c.Id] = ACCESS_MODE_WRITE | |||
for _, c := range collaborations { | |||
accessMap[c.UserID] = c.Mode | |||
} | |||
return nil | |||
} |
@@ -121,7 +121,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err | |||
return nil, err | |||
} | |||
// Compose comment action, could be plain comment, close or reopen issue. | |||
// Compose comment action, could be plain comment, close or reopen issue/pull request. | |||
// This object will be used to notify watchers in the end of function. | |||
act := &Action{ | |||
ActUserID: opts.Doer.Id, | |||
@@ -179,6 +179,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err | |||
if err != nil { | |||
return nil, err | |||
} | |||
case COMMENT_TYPE_CLOSE: | |||
act.OpType = ACTION_CLOSE_ISSUE | |||
if opts.Issue.IsPull { |
@@ -330,7 +330,6 @@ func (repo *Repository) RepoRelLink() string { | |||
return "/" + repo.MustOwner().Name + "/" + repo.Name | |||
} | |||
func (repo *Repository) ComposeCompareURL(oldCommitID, newCommitID string) string { | |||
return fmt.Sprintf("%s/%s/compare/%s...%s", repo.MustOwner().Name, repo.Name, oldCommitID, newCommitID) | |||
} | |||
@@ -1797,105 +1796,6 @@ func CheckRepoStats() { | |||
// ***** END: Repository.NumForks ***** | |||
} | |||
// _________ .__ .__ ___. __ .__ | |||
// \_ ___ \ ____ | | | | _____ \_ |__ ________________ _/ |_|__| ____ ____ | |||
// / \ \/ / _ \| | | | \__ \ | __ \ / _ \_ __ \__ \\ __\ |/ _ \ / \ | |||
// \ \___( <_> ) |_| |__/ __ \| \_\ ( <_> ) | \// __ \| | | ( <_> ) | \ | |||
// \______ /\____/|____/____(____ /___ /\____/|__| (____ /__| |__|\____/|___| / | |||
// \/ \/ \/ \/ \/ | |||
// A Collaboration is a relation between an individual and a repository | |||
type Collaboration struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
Created time.Time `xorm:"CREATED"` | |||
} | |||
// Add collaborator and accompanying access | |||
func (repo *Repository) AddCollaborator(u *User) error { | |||
collaboration := &Collaboration{ | |||
RepoID: repo.ID, | |||
UserID: u.Id, | |||
} | |||
has, err := x.Get(collaboration) | |||
if err != nil { | |||
return err | |||
} else if has { | |||
return nil | |||
} | |||
if err = repo.GetOwner(); err != nil { | |||
return fmt.Errorf("GetOwner: %v", err) | |||
} | |||
sess := x.NewSession() | |||
defer sessionRelease(sess) | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if _, err = sess.InsertOne(collaboration); err != nil { | |||
return err | |||
} | |||
if repo.Owner.IsOrganization() { | |||
err = repo.recalculateTeamAccesses(sess, 0) | |||
} else { | |||
err = repo.recalculateAccesses(sess) | |||
} | |||
if err != nil { | |||
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err) | |||
} | |||
return sess.Commit() | |||
} | |||
func (repo *Repository) getCollaborators(e Engine) ([]*User, error) { | |||
collaborations := make([]*Collaboration, 0) | |||
if err := e.Find(&collaborations, &Collaboration{RepoID: repo.ID}); err != nil { | |||
return nil, err | |||
} | |||
users := make([]*User, len(collaborations)) | |||
for i, c := range collaborations { | |||
user, err := getUserByID(e, c.UserID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
users[i] = user | |||
} | |||
return users, nil | |||
} | |||
// GetCollaborators returns the collaborators for a repository | |||
func (repo *Repository) GetCollaborators() ([]*User, error) { | |||
return repo.getCollaborators(x) | |||
} | |||
// Delete collaborator and accompanying access | |||
func (repo *Repository) DeleteCollaborator(u *User) (err error) { | |||
collaboration := &Collaboration{ | |||
RepoID: repo.ID, | |||
UserID: u.Id, | |||
} | |||
sess := x.NewSession() | |||
defer sessionRelease(sess) | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if has, err := sess.Delete(collaboration); err != nil || has == 0 { | |||
return err | |||
} else if err = repo.recalculateAccesses(sess); err != nil { | |||
return err | |||
} | |||
return sess.Commit() | |||
} | |||
// __ __ __ .__ | |||
// / \ / \_____ _/ |_ ____ | |__ | |||
// \ \/\/ /\__ \\ __\/ ___\| | \ |
@@ -0,0 +1,161 @@ | |||
// Copyright 2016 The Gogs Authors. All rights reserved. | |||
// Use of this source code is governed by a MIT-style | |||
// license that can be found in the LICENSE file. | |||
package models | |||
import ( | |||
"fmt" | |||
"time" | |||
) | |||
// Collaboration represent the relation between an individual and a repository. | |||
type Collaboration struct { | |||
ID int64 `xorm:"pk autoincr"` | |||
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"` | |||
Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` | |||
Created time.Time `xorm:"CREATED"` | |||
} | |||
func (c *Collaboration) ModeName() string { | |||
switch c.Mode { | |||
case ACCESS_MODE_READ: | |||
return "Read" | |||
case ACCESS_MODE_WRITE: | |||
return "Write" | |||
case ACCESS_MODE_ADMIN: | |||
return "Admin" | |||
} | |||
return "Undefined" | |||
} | |||
// AddCollaborator adds new collaboration relation between an individual and a repository. | |||
func (repo *Repository) AddCollaborator(u *User) error { | |||
collaboration := &Collaboration{ | |||
RepoID: repo.ID, | |||
UserID: u.Id, | |||
} | |||
has, err := x.Get(collaboration) | |||
if err != nil { | |||
return err | |||
} else if has { | |||
return nil | |||
} | |||
collaboration.Mode = ACCESS_MODE_WRITE | |||
sess := x.NewSession() | |||
defer sessionRelease(sess) | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if _, err = sess.InsertOne(collaboration); err != nil { | |||
return err | |||
} | |||
if repo.Owner.IsOrganization() { | |||
err = repo.recalculateTeamAccesses(sess, 0) | |||
} else { | |||
err = repo.recalculateAccesses(sess) | |||
} | |||
if err != nil { | |||
return fmt.Errorf("recalculateAccesses 'team=%v': %v", repo.Owner.IsOrganization(), err) | |||
} | |||
return sess.Commit() | |||
} | |||
func (repo *Repository) getCollaborations(e Engine) ([]*Collaboration, error) { | |||
collaborations := make([]*Collaboration, 0) | |||
return collaborations, e.Find(&collaborations, &Collaboration{RepoID: repo.ID}) | |||
} | |||
// Collaborator represents a user with collaboration details. | |||
type Collaborator struct { | |||
*User | |||
Collaboration *Collaboration | |||
} | |||
func (repo *Repository) getCollaborators(e Engine) ([]*Collaborator, error) { | |||
collaborations, err := repo.getCollaborations(e) | |||
if err != nil { | |||
return nil, fmt.Errorf("getCollaborations: %v", err) | |||
} | |||
collaborators := make([]*Collaborator, len(collaborations)) | |||
for i, c := range collaborations { | |||
user, err := getUserByID(e, c.UserID) | |||
if err != nil { | |||
return nil, err | |||
} | |||
collaborators[i] = &Collaborator{ | |||
User: user, | |||
Collaboration: c, | |||
} | |||
} | |||
return collaborators, nil | |||
} | |||
// GetCollaborators returns the collaborators for a repository | |||
func (repo *Repository) GetCollaborators() ([]*Collaborator, error) { | |||
return repo.getCollaborators(x) | |||
} | |||
// ChangeCollaborationAccessMode sets new access mode for the collaboration. | |||
func (repo *Repository) ChangeCollaborationAccessMode(uid int64, mode AccessMode) error { | |||
// Discard invalid input | |||
if mode <= ACCESS_MODE_NONE || mode > ACCESS_MODE_OWNER { | |||
return nil | |||
} | |||
collaboration := &Collaboration{ | |||
RepoID: repo.ID, | |||
UserID: uid, | |||
} | |||
has, err := x.Get(collaboration) | |||
if err != nil { | |||
return fmt.Errorf("get collaboration: %v", err) | |||
} else if !has { | |||
return nil | |||
} | |||
collaboration.Mode = mode | |||
sess := x.NewSession() | |||
defer sessionRelease(sess) | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if _, err = sess.Id(collaboration.ID).AllCols().Update(collaboration); err != nil { | |||
return fmt.Errorf("update collaboration: %v", err) | |||
} else if _, err = sess.Exec("UPDATE access SET mode = ? WHERE user_id = ? AND repo_id = ?", mode, uid, repo.ID); err != nil { | |||
return fmt.Errorf("update access table: %v", err) | |||
} | |||
return sess.Commit() | |||
} | |||
// DeleteCollaboration removes collaboration relation between the user and repository. | |||
func (repo *Repository) DeleteCollaboration(uid int64) (err error) { | |||
collaboration := &Collaboration{ | |||
RepoID: repo.ID, | |||
UserID: uid, | |||
} | |||
sess := x.NewSession() | |||
defer sessionRelease(sess) | |||
if err = sess.Begin(); err != nil { | |||
return err | |||
} | |||
if has, err := sess.Delete(collaboration); err != nil || has == 0 { | |||
return err | |||
} else if err = repo.recalculateAccesses(sess); err != nil { | |||
return err | |||
} | |||
return sess.Commit() | |||
} |
@@ -20,15 +20,6 @@ | |||
"outputPathIsOutsideProject": 0, | |||
"outputPathIsSetByUser": 0 | |||
}, | |||
"\/css\/gogs.min.css": { | |||
"fileType": 16, | |||
"ignore": 1, | |||
"ignoreWasSetByUser": 0, | |||
"inputAbbreviatedPath": "\/css\/gogs.min.css", | |||
"outputAbbreviatedPath": "No Output Path", | |||
"outputPathIsOutsideProject": 0, | |||
"outputPathIsSetByUser": 0 | |||
}, | |||
"\/css\/semantic-2.1.8.min.css": { | |||
"fileType": 16, | |||
"ignore": 0, |
@@ -5,7 +5,7 @@ | |||
background-size: contain; | |||
} | |||
body { | |||
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif, '微软雅黑'; | |||
font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important; | |||
background-color: #fff; | |||
overflow-y: scroll; | |||
} | |||
@@ -104,6 +104,9 @@ code.wrap { | |||
.ui.container.fluid.padded { | |||
padding: 0 10px 0 10px; | |||
} | |||
.ui.form .ui.button { | |||
font-weight: normal; | |||
} | |||
.ui .text.red { | |||
color: #d95c5c !important; | |||
} | |||
@@ -234,6 +237,10 @@ code.wrap { | |||
.ui.status.buttons .octicon { | |||
margin-right: 4px; | |||
} | |||
.ui.inline.delete-button { | |||
padding: 8px 15px; | |||
font-weight: normal; | |||
} | |||
.overflow.menu .items { | |||
max-height: 300px; | |||
overflow-y: auto; | |||
@@ -1984,10 +1991,11 @@ footer .container .links > *:first-child { | |||
.repository.settings.collaboration .collaborator.list { | |||
padding: 0; | |||
} | |||
.repository.settings.collaboration .collaborator.list .item { | |||
padding: 10px 20px; | |||
.repository.settings.collaboration .collaborator.list > .item { | |||
margin: 0; | |||
line-height: 2em; | |||
} | |||
.repository.settings.collaboration .collaborator.list .item:not(:last-child) { | |||
.repository.settings.collaboration .collaborator.list > .item:not(:last-child) { | |||
border-bottom: 1px solid #DDD; | |||
} | |||
.repository.settings.collaboration #repo-collab-form #search-user-box .results { |
@@ -458,6 +458,20 @@ function initRepository() { | |||
} | |||
} | |||
function initRepositoryCollaboration(){ | |||
console.log('initRepositoryCollaboration'); | |||
// Change collaborator access mode | |||
$('.access-mode.menu .item').click(function(){ | |||
var $menu = $(this).parent(); | |||
$.post($menu.data('url'), { | |||
"_csrf": csrf, | |||
"uid": $menu.data('uid'), | |||
"mode": $(this).data('value') | |||
}) | |||
}); | |||
} | |||
function initWiki() { | |||
if ($('.repository.wiki').length == 0) { | |||
return; | |||
@@ -964,7 +978,8 @@ $(document).ready(function () { | |||
initAdmin(); | |||
var routes = { | |||
'div.user.settings': initUserSettings | |||
'div.user.settings': initUserSettings, | |||
'div.repository.settings.collaboration': initRepositoryCollaboration | |||
}; | |||
var selector; |
@@ -1,7 +1,7 @@ | |||
@footer-margin: 40px; | |||
body { | |||
font-family: 'Helvetica Neue',Arial,Helvetica,sans-serif,'微软雅黑'; | |||
font-family: "Helvetica Neue", "Microsoft YaHei", Arial, Helvetica, sans-serif !important; | |||
background-color: #fff; | |||
overflow-y: scroll; | |||
} | |||
@@ -109,6 +109,12 @@ pre, code { | |||
} | |||
} | |||
&.form { | |||
.ui.button { | |||
font-weight: normal; | |||
} | |||
} | |||
.text { | |||
&.red { | |||
color: #d95c5c !important; | |||
@@ -260,6 +266,11 @@ pre, code { | |||
margin-right: 4px; | |||
} | |||
} | |||
&.inline.delete-button { | |||
padding: 8px 15px; | |||
font-weight: normal; | |||
} | |||
} | |||
.overflow.menu { |
@@ -1026,8 +1026,9 @@ | |||
.collaborator.list { | |||
padding: 0; | |||
.item { | |||
padding: 10px 20px; | |||
>.item { | |||
margin: 0; | |||
line-height: 2em; | |||
&:not(:last-child) { | |||
border-bottom: 1px solid #DDD; |
@@ -5,6 +5,8 @@ | |||
package admin | |||
import ( | |||
"fmt" | |||
"github.com/Unknwon/com" | |||
"github.com/go-xorm/core" | |||
@@ -218,11 +220,13 @@ func DeleteAuthSource(ctx *middleware.Context) { | |||
if err = models.DeleteSource(source); err != nil { | |||
switch err { | |||
case models.ErrAuthenticationUserUsed: | |||
ctx.Flash.Error("form.still_own_user") | |||
ctx.Redirect(setting.AppSubUrl + "/admin/auths/" + ctx.Params(":authid")) | |||
ctx.Flash.Error(ctx.Tr("admin.auths.still_in_used")) | |||
default: | |||
ctx.Handle(500, "DeleteSource", err) | |||
ctx.Flash.Error(fmt.Sprintf("DeleteSource: %v", err)) | |||
} | |||
ctx.JSON(200, map[string]interface{}{ | |||
"redirect": setting.AppSubUrl + "/admin/auths/" + ctx.Params(":authid"), | |||
}) | |||
return | |||
} | |||
log.Trace("Authentication deleted by admin(%s): %d", ctx.User.Name, source.ID) |
@@ -257,30 +257,13 @@ func Collaboration(ctx *middleware.Context) { | |||
ctx.Data["Title"] = ctx.Tr("repo.settings") | |||
ctx.Data["PageIsSettingsCollaboration"] = true | |||
// Delete collaborator. | |||
remove := strings.ToLower(ctx.Query("remove")) | |||
if len(remove) > 0 && remove != ctx.Repo.Owner.LowerName { | |||
u, err := models.GetUserByName(remove) | |||
if err != nil { | |||
ctx.Handle(500, "GetUserByName", err) | |||
return | |||
} | |||
if err := ctx.Repo.Repository.DeleteCollaborator(u); err != nil { | |||
ctx.Handle(500, "DeleteCollaborator", err) | |||
return | |||
} | |||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) | |||
ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||
return | |||
} | |||
users, err := ctx.Repo.Repository.GetCollaborators() | |||
if err != nil { | |||
ctx.Handle(500, "GetCollaborators", err) | |||
return | |||
} | |||
ctx.Data["Collaborators"] = users | |||
ctx.HTML(200, COLLABORATION) | |||
} | |||
@@ -332,6 +315,26 @@ func CollaborationPost(ctx *middleware.Context) { | |||
ctx.Redirect(setting.AppSubUrl + ctx.Req.URL.Path) | |||
} | |||
func ChangeCollaborationAccessMode(ctx *middleware.Context) { | |||
if err := ctx.Repo.Repository.ChangeCollaborationAccessMode( | |||
ctx.QueryInt64("uid"), | |||
models.AccessMode(ctx.QueryInt("mode"))); err != nil { | |||
log.Error(4, "ChangeCollaborationAccessMode: %v", err) | |||
} | |||
} | |||
func DeleteCollaboration(ctx *middleware.Context) { | |||
if err := ctx.Repo.Repository.DeleteCollaboration(ctx.QueryInt64("id")); err != nil { | |||
ctx.Flash.Error("DeleteCollaboration: " + err.Error()) | |||
} else { | |||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) | |||
} | |||
ctx.JSON(200, map[string]interface{}{ | |||
"redirect": ctx.Repo.RepoLink + "/settings/collaboration", | |||
}) | |||
} | |||
func parseOwnerAndRepo(ctx *middleware.Context) (*models.User, *models.Repository) { | |||
owner, err := models.GetUserByName(ctx.Params(":username")) | |||
if err != nil { |
@@ -11,14 +11,30 @@ | |||
</h4> | |||
<div class="ui attached segment collaborator list"> | |||
{{range .Collaborators}} | |||
<div class="item"> | |||
{{if not (eq .Id $.Owner.Id)}} | |||
<a href="{{$.RepoLink}}/settings/collaboration?remove={{.Name}}" class="ui right text red"><i class="fa fa-times"></i></a> | |||
{{end}} | |||
<a href="{{AppSubUrl}}/{{.Name}}"> | |||
<img class="ui avatar image" src="{{.AvatarLink}}"> | |||
{{.DisplayName}} | |||
</a> | |||
<div class="item ui grid"> | |||
<div class="ui five wide column"> | |||
<a href="{{AppSubUrl}}/{{.Name}}"> | |||
<img class="ui avatar image" src="{{.AvatarLink}}"> | |||
{{.DisplayName}} | |||
</a> | |||
</div> | |||
<div class="ui eight wide column"> | |||
<span class="octicon octicon-shield"></span> | |||
<div class="ui inline dropdown"> | |||
<div class="text">{{.Collaboration.ModeName}}</div> | |||
<i class="dropdown icon"></i> | |||
<div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.Id}}"> | |||
<div class="item" data-text="Admin" data-value="3">Admin</div> | |||
<div class="item" data-text="Write" data-value="2">Write</div> | |||
<div class="item" data-text="Read" data-value="1">Read</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div class="ui two wide column"> | |||
<button class="ui red tiny button inline text-thin delete-button" data-url="{{$.Link}}/delete" data-id="{{.Id}}"> | |||
{{$.i18n.Tr "repo.settings.delete_collaborator"}} | |||
</button> | |||
</div> | |||
</div> | |||
{{end}} | |||
</div> | |||
@@ -40,4 +56,15 @@ | |||
</div> | |||
</div> | |||
</div> | |||
<div class="ui small basic delete modal"> | |||
<div class="ui icon header"> | |||
<i class="trash icon"></i> | |||
{{.i18n.Tr "repo.settings.collaborator_deletion"}} | |||
</div> | |||
<div class="content"> | |||
<p>{{.i18n.Tr "repo.settings.collaborator_deletion_desc"}}</p> | |||
</div> | |||
{{template "base/delete_modal_actions" .}} | |||
</div> | |||
{{template "base/footer" .}} |