* Add teams to repo on collaboration page. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add option for repository admins to change teams access to repo. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add comment for functions Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Make RepoAdminChangeTeamAccess default false in xorm and make it default checked in template instead. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Make proper language strings and fix error redirection. * Add unit tests for adding and deleting team from repository. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Add database migration Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix redirect Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Fix locale string mismatch. Signed-off-by: David Svantesson <davidsvantesson@gmail.com> * Move team access mode text logic to template. * Move collaborator access mode text logic to template.tags/v1.21.12.1
| @@ -1370,6 +1370,23 @@ func (err ErrTeamAlreadyExist) Error() string { | |||||
| return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name) | return fmt.Sprintf("team already exists [org_id: %d, name: %s]", err.OrgID, err.Name) | ||||
| } | } | ||||
| // ErrTeamNotExist represents a "TeamNotExist" error | |||||
| type ErrTeamNotExist struct { | |||||
| OrgID int64 | |||||
| TeamID int64 | |||||
| Name string | |||||
| } | |||||
| // IsErrTeamNotExist checks if an error is a ErrTeamNotExist. | |||||
| func IsErrTeamNotExist(err error) bool { | |||||
| _, ok := err.(ErrTeamNotExist) | |||||
| return ok | |||||
| } | |||||
| func (err ErrTeamNotExist) Error() string { | |||||
| return fmt.Sprintf("team does not exist [org_id %d, team_id %d, name: %s]", err.OrgID, err.TeamID, err.Name) | |||||
| } | |||||
| // | // | ||||
| // Two-factor authentication | // Two-factor authentication | ||||
| // | // | ||||
| @@ -508,4 +508,15 @@ | |||||
| num_stars: 0 | num_stars: 0 | ||||
| num_forks: 0 | num_forks: 0 | ||||
| num_issues: 0 | num_issues: 0 | ||||
| is_mirror: false | |||||
| - | |||||
| id: 43 | |||||
| owner_id: 26 | |||||
| lower_name: repo26 | |||||
| name: repo26 | |||||
| is_private: true | |||||
| num_stars: 0 | |||||
| num_forks: 0 | |||||
| num_issues: 0 | |||||
| is_mirror: false | is_mirror: false | ||||
| @@ -87,3 +87,12 @@ | |||||
| authorize: 1 # owner | authorize: 1 # owner | ||||
| num_repos: 0 | num_repos: 0 | ||||
| num_members: 1 | num_members: 1 | ||||
| - | |||||
| id: 11 | |||||
| org_id: 26 | |||||
| lower_name: team11 | |||||
| name: team11 | |||||
| authorize: 1 # read | |||||
| num_repos: 0 | |||||
| num_members: 0 | |||||
| @@ -410,3 +410,21 @@ | |||||
| num_repos: 0 | num_repos: 0 | ||||
| num_members: 1 | num_members: 1 | ||||
| num_teams: 1 | num_teams: 1 | ||||
| - | |||||
| id: 26 | |||||
| lower_name: org26 | |||||
| name: org26 | |||||
| full_name: "Org26" | |||||
| email: org26@example.com | |||||
| email_notifications_preference: onmention | |||||
| passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a # password | |||||
| type: 1 # organization | |||||
| salt: ZogKvWdyEx | |||||
| is_admin: false | |||||
| avatar: avatar26 | |||||
| avatar_email: org26@example.com | |||||
| num_repos: 1 | |||||
| num_members: 0 | |||||
| num_teams: 1 | |||||
| repo_admin_change_team_access: true | |||||
| @@ -248,6 +248,8 @@ var migrations = []Migration{ | |||||
| NewMigration("add table columns for cross referencing issues", addCrossReferenceColumns), | NewMigration("add table columns for cross referencing issues", addCrossReferenceColumns), | ||||
| // v96 -> v97 | // v96 -> v97 | ||||
| NewMigration("delete orphaned attachments", deleteOrphanedAttachments), | NewMigration("delete orphaned attachments", deleteOrphanedAttachments), | ||||
| // v97 -> v98 | |||||
| NewMigration("add repo_admin_change_team_access to user", addRepoAdminChangeTeamAccessColumnForUser), | |||||
| } | } | ||||
| // Migrate database to current version | // Migrate database to current version | ||||
| @@ -0,0 +1,15 @@ | |||||
| // Copyright 2019 The Gitea 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 migrations | |||||
| import "github.com/go-xorm/xorm" | |||||
| func addRepoAdminChangeTeamAccessColumnForUser(x *xorm.Engine) error { | |||||
| type User struct { | |||||
| RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` | |||||
| } | |||||
| return x.Sync2(new(User)) | |||||
| } | |||||
| @@ -6,7 +6,6 @@ | |||||
| package models | package models | ||||
| import ( | import ( | ||||
| "errors" | |||||
| "fmt" | "fmt" | ||||
| "os" | "os" | ||||
| "strings" | "strings" | ||||
| @@ -20,11 +19,6 @@ import ( | |||||
| "xorm.io/builder" | "xorm.io/builder" | ||||
| ) | ) | ||||
| var ( | |||||
| // ErrTeamNotExist team does not exist | |||||
| ErrTeamNotExist = errors.New("Team does not exist") | |||||
| ) | |||||
| // IsOwnedBy returns true if given user is in the owner team. | // IsOwnedBy returns true if given user is in the owner team. | ||||
| func (org *User) IsOwnedBy(uid int64) (bool, error) { | func (org *User) IsOwnedBy(uid int64) (bool, error) { | ||||
| return IsOrganizationOwner(org.ID, uid) | return IsOrganizationOwner(org.ID, uid) | ||||
| @@ -304,7 +298,7 @@ type OrgUser struct { | |||||
| func isOrganizationOwner(e Engine, orgID, uid int64) (bool, error) { | func isOrganizationOwner(e Engine, orgID, uid int64) (bool, error) { | ||||
| ownerTeam, err := getOwnerTeam(e, orgID) | ownerTeam, err := getOwnerTeam(e, orgID) | ||||
| if err != nil { | if err != nil { | ||||
| if err == ErrTeamNotExist { | |||||
| if IsErrTeamNotExist(err) { | |||||
| log.Error("Organization does not have owner team: %d", orgID) | log.Error("Organization does not have owner team: %d", orgID) | ||||
| return false, nil | return false, nil | ||||
| } | } | ||||
| @@ -352,7 +352,7 @@ func getTeam(e Engine, orgID int64, name string) (*Team, error) { | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } else if !has { | } else if !has { | ||||
| return nil, ErrTeamNotExist | |||||
| return nil, ErrTeamNotExist{orgID, 0, name} | |||||
| } | } | ||||
| return t, nil | return t, nil | ||||
| } | } | ||||
| @@ -373,7 +373,7 @@ func getTeamByID(e Engine, teamID int64) (*Team, error) { | |||||
| if err != nil { | if err != nil { | ||||
| return nil, err | return nil, err | ||||
| } else if !has { | } else if !has { | ||||
| return nil, ErrTeamNotExist | |||||
| return nil, ErrTeamNotExist{0, teamID, ""} | |||||
| } | } | ||||
| return t, nil | return t, nil | ||||
| } | } | ||||
| @@ -64,11 +64,11 @@ func TestUser_GetTeam(t *testing.T) { | |||||
| assert.Equal(t, "team1", team.LowerName) | assert.Equal(t, "team1", team.LowerName) | ||||
| _, err = org.GetTeam("does not exist") | _, err = org.GetTeam("does not exist") | ||||
| assert.Equal(t, ErrTeamNotExist, err) | |||||
| assert.True(t, IsErrTeamNotExist(err)) | |||||
| nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | ||||
| _, err = nonOrg.GetTeam("team") | _, err = nonOrg.GetTeam("team") | ||||
| assert.Equal(t, ErrTeamNotExist, err) | |||||
| assert.True(t, IsErrTeamNotExist(err)) | |||||
| } | } | ||||
| func TestUser_GetOwnerTeam(t *testing.T) { | func TestUser_GetOwnerTeam(t *testing.T) { | ||||
| @@ -80,7 +80,7 @@ func TestUser_GetOwnerTeam(t *testing.T) { | |||||
| nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | nonOrg := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | ||||
| _, err = nonOrg.GetOwnerTeam() | _, err = nonOrg.GetOwnerTeam() | ||||
| assert.Equal(t, ErrTeamNotExist, err) | |||||
| assert.True(t, IsErrTeamNotExist(err)) | |||||
| } | } | ||||
| func TestUser_GetTeams(t *testing.T) { | func TestUser_GetTeams(t *testing.T) { | ||||
| @@ -16,20 +16,6 @@ type Collaboration struct { | |||||
| Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` | Mode AccessMode `xorm:"DEFAULT 2 NOT NULL"` | ||||
| } | } | ||||
| // ModeI18nKey returns the collaboration mode I18n Key | |||||
| func (c *Collaboration) ModeI18nKey() string { | |||||
| switch c.Mode { | |||||
| case AccessModeRead: | |||||
| return "repo.settings.collaboration.read" | |||||
| case AccessModeWrite: | |||||
| return "repo.settings.collaboration.write" | |||||
| case AccessModeAdmin: | |||||
| return "repo.settings.collaboration.admin" | |||||
| default: | |||||
| return "repo.settings.collaboration.undefined" | |||||
| } | |||||
| } | |||||
| // AddCollaborator adds new collaboration to a repository with default access mode. | // AddCollaborator adds new collaboration to a repository with default access mode. | ||||
| func (repo *Repository) AddCollaborator(u *User) error { | func (repo *Repository) AddCollaborator(u *User) error { | ||||
| collaboration := &Collaboration{ | collaboration := &Collaboration{ | ||||
| @@ -183,3 +169,17 @@ func (repo *Repository) DeleteCollaboration(uid int64) (err error) { | |||||
| return sess.Commit() | return sess.Commit() | ||||
| } | } | ||||
| func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) { | |||||
| return teams, e. | |||||
| Join("INNER", "team_repo", "team_repo.team_id = team.id"). | |||||
| Where("team.org_id = ?", repo.OwnerID). | |||||
| And("team_repo.repo_id=?", repo.ID). | |||||
| OrderBy("CASE WHEN name LIKE '" + ownerTeamName + "' THEN '' ELSE name END"). | |||||
| Find(&teams) | |||||
| } | |||||
| // GetRepoTeams gets the list of teams that has access to the repository | |||||
| func (repo *Repository) GetRepoTeams() ([]*Team, error) { | |||||
| return repo.getRepoTeams(x) | |||||
| } | |||||
| @@ -10,17 +10,6 @@ import ( | |||||
| "github.com/stretchr/testify/assert" | "github.com/stretchr/testify/assert" | ||||
| ) | ) | ||||
| func TestCollaboration_ModeI18nKey(t *testing.T) { | |||||
| assert.Equal(t, "repo.settings.collaboration.read", | |||||
| (&Collaboration{Mode: AccessModeRead}).ModeI18nKey()) | |||||
| assert.Equal(t, "repo.settings.collaboration.write", | |||||
| (&Collaboration{Mode: AccessModeWrite}).ModeI18nKey()) | |||||
| assert.Equal(t, "repo.settings.collaboration.admin", | |||||
| (&Collaboration{Mode: AccessModeAdmin}).ModeI18nKey()) | |||||
| assert.Equal(t, "repo.settings.collaboration.undefined", | |||||
| (&Collaboration{Mode: AccessModeNone}).ModeI18nKey()) | |||||
| } | |||||
| func TestRepository_AddCollaborator(t *testing.T) { | func TestRepository_AddCollaborator(t *testing.T) { | ||||
| assert.NoError(t, PrepareTestDatabase()) | assert.NoError(t, PrepareTestDatabase()) | ||||
| @@ -147,12 +147,13 @@ type User struct { | |||||
| NumRepos int | NumRepos int | ||||
| // For organization | // For organization | ||||
| NumTeams int | |||||
| NumMembers int | |||||
| Teams []*Team `xorm:"-"` | |||||
| Members UserList `xorm:"-"` | |||||
| MembersIsPublic map[int64]bool `xorm:"-"` | |||||
| Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` | |||||
| NumTeams int | |||||
| NumMembers int | |||||
| Teams []*Team `xorm:"-"` | |||||
| Members UserList `xorm:"-"` | |||||
| MembersIsPublic map[int64]bool `xorm:"-"` | |||||
| Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"` | |||||
| RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"` | |||||
| // Preferences | // Preferences | ||||
| DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` | DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` | ||||
| @@ -140,7 +140,10 @@ func TestSearchUsers(t *testing.T) { | |||||
| testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2}, | testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 3, PageSize: 2}, | ||||
| []int64{19, 25}) | []int64{19, 25}) | ||||
| testOrgSuccess(&SearchUserOptions{Page: 4, PageSize: 2}, | |||||
| testOrgSuccess(&SearchUserOptions{OrderBy: "id ASC", Page: 4, PageSize: 2}, | |||||
| []int64{26}) | |||||
| testOrgSuccess(&SearchUserOptions{Page: 5, PageSize: 2}, | |||||
| []int64{}) | []int64{}) | ||||
| // test users | // test users | ||||
| @@ -43,7 +43,7 @@ func (users UserList) loadOrganizationOwners(e Engine, orgID int64) (map[int64]* | |||||
| } | } | ||||
| ownerTeam, err := getOwnerTeam(e, orgID) | ownerTeam, err := getOwnerTeam(e, orgID) | ||||
| if err != nil { | if err != nil { | ||||
| if err == ErrTeamNotExist { | |||||
| if IsErrTeamNotExist(err) { | |||||
| log.Error("Organization does not have owner team: %d", orgID) | log.Error("Organization does not have owner team: %d", orgID) | ||||
| return nil, nil | return nil, nil | ||||
| } | } | ||||
| @@ -33,13 +33,14 @@ func (f *CreateOrgForm) Validate(ctx *macaron.Context, errs binding.Errors) bind | |||||
| // UpdateOrgSettingForm form for updating organization settings | // UpdateOrgSettingForm form for updating organization settings | ||||
| type UpdateOrgSettingForm struct { | type UpdateOrgSettingForm struct { | ||||
| Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` | |||||
| FullName string `binding:"MaxSize(100)"` | |||||
| Description string `binding:"MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Location string `binding:"MaxSize(50)"` | |||||
| Visibility structs.VisibleType | |||||
| MaxRepoCreation int | |||||
| Name string `binding:"Required;AlphaDashDot;MaxSize(40)" locale:"org.org_name_holder"` | |||||
| FullName string `binding:"MaxSize(100)"` | |||||
| Description string `binding:"MaxSize(255)"` | |||||
| Website string `binding:"ValidUrl;MaxSize(255)"` | |||||
| Location string `binding:"MaxSize(50)"` | |||||
| Visibility structs.VisibleType | |||||
| MaxRepoCreation int | |||||
| RepoAdminChangeTeamAccess bool | |||||
| } | } | ||||
| // Validate validates the fields | // Validate validates the fields | ||||
| @@ -6,14 +6,15 @@ package structs | |||||
| // Organization represents an organization | // Organization represents an organization | ||||
| type Organization struct { | type Organization struct { | ||||
| ID int64 `json:"id"` | |||||
| UserName string `json:"username"` | |||||
| FullName string `json:"full_name"` | |||||
| AvatarURL string `json:"avatar_url"` | |||||
| Description string `json:"description"` | |||||
| Website string `json:"website"` | |||||
| Location string `json:"location"` | |||||
| Visibility string `json:"visibility"` | |||||
| ID int64 `json:"id"` | |||||
| UserName string `json:"username"` | |||||
| FullName string `json:"full_name"` | |||||
| AvatarURL string `json:"avatar_url"` | |||||
| Description string `json:"description"` | |||||
| Website string `json:"website"` | |||||
| Location string `json:"location"` | |||||
| Visibility string `json:"visibility"` | |||||
| RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||||
| } | } | ||||
| // CreateOrgOption options for creating an organization | // CreateOrgOption options for creating an organization | ||||
| @@ -26,7 +27,8 @@ type CreateOrgOption struct { | |||||
| Location string `json:"location"` | Location string `json:"location"` | ||||
| // possible values are `public` (default), `limited` or `private` | // possible values are `public` (default), `limited` or `private` | ||||
| // enum: public,limited,private | // enum: public,limited,private | ||||
| Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||||
| Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||||
| RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||||
| } | } | ||||
| // EditOrgOption options for editing an organization | // EditOrgOption options for editing an organization | ||||
| @@ -37,5 +39,6 @@ type EditOrgOption struct { | |||||
| Location string `json:"location"` | Location string `json:"location"` | ||||
| // possible values are `public`, `limited` or `private` | // possible values are `public`, `limited` or `private` | ||||
| // enum: public,limited,private | // enum: public,limited,private | ||||
| Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||||
| Visibility string `json:"visibility" binding:"In(,public,limited,private)"` | |||||
| RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` | |||||
| } | } | ||||
| @@ -319,6 +319,7 @@ enterred_invalid_repo_name = The repository name you entered is incorrect. | |||||
| enterred_invalid_owner_name = The new owner name is not valid. | enterred_invalid_owner_name = The new owner name is not valid. | ||||
| enterred_invalid_password = The password you entered is incorrect. | enterred_invalid_password = The password you entered is incorrect. | ||||
| user_not_exist = The user does not exist. | user_not_exist = The user does not exist. | ||||
| team_not_exist = The team does not exist. | |||||
| last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner in any given team. | last_org_owner = You cannot remove the last user from the 'owners' team. There must be at least one owner in any given team. | ||||
| cannot_add_org_to_team = An organization cannot be added as a team member. | cannot_add_org_to_team = An organization cannot be added as a team member. | ||||
| @@ -1136,6 +1137,7 @@ settings.collaboration = Collaborators | |||||
| settings.collaboration.admin = Administrator | settings.collaboration.admin = Administrator | ||||
| settings.collaboration.write = Write | settings.collaboration.write = Write | ||||
| settings.collaboration.read = Read | settings.collaboration.read = Read | ||||
| settings.collaboration.owner = Owner | |||||
| settings.collaboration.undefined = Undefined | settings.collaboration.undefined = Undefined | ||||
| settings.hooks = Webhooks | settings.hooks = Webhooks | ||||
| settings.githooks = Git Hooks | settings.githooks = Git Hooks | ||||
| @@ -1217,6 +1219,11 @@ settings.collaborator_deletion_desc = Removing a collaborator will revoke their | |||||
| settings.remove_collaborator_success = The collaborator has been removed. | settings.remove_collaborator_success = The collaborator has been removed. | ||||
| settings.search_user_placeholder = Search user… | settings.search_user_placeholder = Search user… | ||||
| settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. | settings.org_not_allowed_to_be_collaborator = Organizations cannot be added as a collaborator. | ||||
| settings.change_team_access_not_allowed = Changing team access for repository has been restricted to organization owner | |||||
| settings.team_not_in_organization = The team is not in the same organization as the repository | |||||
| settings.add_team_duplicate = Team already has the repository | |||||
| settings.add_team_success = The team now have access to the repository. | |||||
| settings.remove_team_success = The team's access to the repository has been removed. | |||||
| settings.add_webhook = Add Webhook | settings.add_webhook = Add Webhook | ||||
| settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. | settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. | ||||
| settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>. | settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Gitea events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>. | ||||
| @@ -1475,6 +1482,8 @@ settings.options = Organization | |||||
| settings.full_name = Full Name | settings.full_name = Full Name | ||||
| settings.website = Website | settings.website = Website | ||||
| settings.location = Location | settings.location = Location | ||||
| settings.permission = Permissions | |||||
| settings.repoadminchangeteam = Repository admin can add and remove access for teams | |||||
| settings.visibility = Visibility | settings.visibility = Visibility | ||||
| settings.visibility.public = Public | settings.visibility.public = Public | ||||
| settings.visibility.limited = Limited (Visible to logged in users only) | settings.visibility.limited = Limited (Visible to logged in users only) | ||||
| @@ -747,6 +747,8 @@ footer .ui.left,footer .ui.right{line-height:40px} | |||||
| .repository.settings.collaboration .collaborator.list>.item:not(:last-child){border-bottom:1px solid #ddd} | .repository.settings.collaboration .collaborator.list>.item:not(:last-child){border-bottom:1px solid #ddd} | ||||
| .repository.settings.collaboration #repo-collab-form #search-user-box .results{left:7px} | .repository.settings.collaboration #repo-collab-form #search-user-box .results{left:7px} | ||||
| .repository.settings.collaboration #repo-collab-form .ui.button{margin-left:5px;margin-top:-3px} | .repository.settings.collaboration #repo-collab-form .ui.button{margin-left:5px;margin-top:-3px} | ||||
| .repository.settings.collaboration #repo-collab-team-form #search-team-box .results{left:7px} | |||||
| .repository.settings.collaboration #repo-collab-team-form .ui.button{margin-left:5px;margin-top:-3px} | |||||
| .repository.settings.branches .protected-branches .selection.dropdown{width:300px} | .repository.settings.branches .protected-branches .selection.dropdown{width:300px} | ||||
| .repository.settings.branches .protected-branches .item{border:1px solid #eaeaea;padding:10px 15px} | .repository.settings.branches .protected-branches .item{border:1px solid #eaeaea;padding:10px 15px} | ||||
| .repository.settings.branches .protected-branches .item:not(:last-child){border-bottom:0} | .repository.settings.branches .protected-branches .item:not(:last-child){border-bottom:0} | ||||
| @@ -783,6 +785,7 @@ footer .ui.left,footer .ui.right{line-height:40px} | |||||
| .user-cards .list .item .meta{margin-top:5px} | .user-cards .list .item .meta{margin-top:5px} | ||||
| #search-repo-box .results .result .image,#search-user-box .results .result .image{float:left;margin-right:8px;width:2em;height:2em} | #search-repo-box .results .result .image,#search-user-box .results .result .image{float:left;margin-right:8px;width:2em;height:2em} | ||||
| #search-repo-box .results .result .content,#search-user-box .results .result .content{margin:6px 0} | #search-repo-box .results .result .content,#search-user-box .results .result .content{margin:6px 0} | ||||
| #search-team-box .results .result .content{margin:6px 0} | |||||
| #issue-filters.hide{display:none} | #issue-filters.hide{display:none} | ||||
| #issue-actions{margin-top:-1rem!important} | #issue-actions{margin-top:-1rem!important} | ||||
| #issue-actions.hide{display:none} | #issue-actions.hide{display:none} | ||||
| @@ -1761,6 +1761,30 @@ function searchUsers() { | |||||
| }); | }); | ||||
| } | } | ||||
| function searchTeams() { | |||||
| const $searchTeamBox = $('#search-team-box'); | |||||
| $searchTeamBox.search({ | |||||
| minCharacters: 2, | |||||
| apiSettings: { | |||||
| url: suburl + '/api/v1/orgs/' + $searchTeamBox.data('org') + '/teams', | |||||
| headers: {"X-Csrf-Token": csrf}, | |||||
| onResponse: function(response) { | |||||
| const items = []; | |||||
| $.each(response, function (_i, item) { | |||||
| const title = item.name + ' (' + item.permission + ' access)'; | |||||
| items.push({ | |||||
| title: title, | |||||
| }) | |||||
| }); | |||||
| return { results: items } | |||||
| } | |||||
| }, | |||||
| searchFields: ['name', 'description'], | |||||
| showNoResults: false | |||||
| }); | |||||
| } | |||||
| function searchRepositories() { | function searchRepositories() { | ||||
| const $searchRepoBox = $('#search-repo-box'); | const $searchRepoBox = $('#search-repo-box'); | ||||
| $searchRepoBox.search({ | $searchRepoBox.search({ | ||||
| @@ -2171,6 +2195,7 @@ $(document).ready(function () { | |||||
| buttonsClickOnEnter(); | buttonsClickOnEnter(); | ||||
| searchUsers(); | searchUsers(); | ||||
| searchTeams(); | |||||
| searchRepositories(); | searchRepositories(); | ||||
| initCommentForm(); | initCommentForm(); | ||||
| @@ -1736,6 +1736,19 @@ | |||||
| margin-top: -3px; | margin-top: -3px; | ||||
| } | } | ||||
| } | } | ||||
| #repo-collab-team-form { | |||||
| #search-team-box { | |||||
| .results { | |||||
| left: 7px; | |||||
| } | |||||
| } | |||||
| .ui.button { | |||||
| margin-left: 5px; | |||||
| margin-top: -3px; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| &.branches { | &.branches { | ||||
| @@ -1936,6 +1949,16 @@ | |||||
| } | } | ||||
| } | } | ||||
| #search-team-box { | |||||
| .results { | |||||
| .result { | |||||
| .content { | |||||
| margin: 6px 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| #issue-filters.hide { | #issue-filters.hide { | ||||
| display: none; | display: none; | ||||
| } | } | ||||
| @@ -206,14 +206,15 @@ func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey { | |||||
| // ToOrganization convert models.User to api.Organization | // ToOrganization convert models.User to api.Organization | ||||
| func ToOrganization(org *models.User) *api.Organization { | func ToOrganization(org *models.User) *api.Organization { | ||||
| return &api.Organization{ | return &api.Organization{ | ||||
| ID: org.ID, | |||||
| AvatarURL: org.AvatarLink(), | |||||
| UserName: org.Name, | |||||
| FullName: org.FullName, | |||||
| Description: org.Description, | |||||
| Website: org.Website, | |||||
| Location: org.Location, | |||||
| Visibility: org.Visibility.String(), | |||||
| ID: org.ID, | |||||
| AvatarURL: org.AvatarLink(), | |||||
| UserName: org.Name, | |||||
| FullName: org.FullName, | |||||
| Description: org.Description, | |||||
| Website: org.Website, | |||||
| Location: org.Location, | |||||
| Visibility: org.Visibility.String(), | |||||
| RepoAdminChangeTeamAccess: org.RepoAdminChangeTeamAccess, | |||||
| } | } | ||||
| } | } | ||||
| @@ -95,14 +95,15 @@ func Create(ctx *context.APIContext, form api.CreateOrgOption) { | |||||
| } | } | ||||
| org := &models.User{ | org := &models.User{ | ||||
| Name: form.UserName, | |||||
| FullName: form.FullName, | |||||
| Description: form.Description, | |||||
| Website: form.Website, | |||||
| Location: form.Location, | |||||
| IsActive: true, | |||||
| Type: models.UserTypeOrganization, | |||||
| Visibility: visibility, | |||||
| Name: form.UserName, | |||||
| FullName: form.FullName, | |||||
| Description: form.Description, | |||||
| Website: form.Website, | |||||
| Location: form.Location, | |||||
| IsActive: true, | |||||
| Type: models.UserTypeOrganization, | |||||
| Visibility: visibility, | |||||
| RepoAdminChangeTeamAccess: form.RepoAdminChangeTeamAccess, | |||||
| } | } | ||||
| if err := models.CreateOrganization(org, ctx.User); err != nil { | if err := models.CreateOrganization(org, ctx.User); err != nil { | ||||
| if models.IsErrUserAlreadyExist(err) || | if models.IsErrUserAlreadyExist(err) || | ||||
| @@ -83,6 +83,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) { | |||||
| org.Website = form.Website | org.Website = form.Website | ||||
| org.Location = form.Location | org.Location = form.Location | ||||
| org.Visibility = form.Visibility | org.Visibility = form.Visibility | ||||
| org.RepoAdminChangeTeamAccess = form.RepoAdminChangeTeamAccess | |||||
| if err := models.UpdateUser(org); err != nil { | if err := models.UpdateUser(org); err != nil { | ||||
| ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
| return | return | ||||
| @@ -490,6 +490,18 @@ func Collaboration(ctx *context.Context) { | |||||
| } | } | ||||
| ctx.Data["Collaborators"] = users | ctx.Data["Collaborators"] = users | ||||
| teams, err := ctx.Repo.Repository.GetRepoTeams() | |||||
| if err != nil { | |||||
| ctx.ServerError("GetRepoTeams", err) | |||||
| return | |||||
| } | |||||
| ctx.Data["Teams"] = teams | |||||
| ctx.Data["Repo"] = ctx.Repo.Repository | |||||
| ctx.Data["OrgID"] = ctx.Repo.Repository.OwnerID | |||||
| ctx.Data["OrgName"] = ctx.Repo.Repository.OwnerName | |||||
| ctx.Data["Org"] = ctx.Repo.Repository.Owner | |||||
| ctx.Data["Units"] = models.Units | |||||
| ctx.HTML(200, tplCollaboration) | ctx.HTML(200, tplCollaboration) | ||||
| } | } | ||||
| @@ -566,6 +578,77 @@ func DeleteCollaboration(ctx *context.Context) { | |||||
| }) | }) | ||||
| } | } | ||||
| // AddTeamPost response for adding a team to a repository | |||||
| func AddTeamPost(ctx *context.Context) { | |||||
| if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { | |||||
| ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| return | |||||
| } | |||||
| name := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.Query("team"))) | |||||
| if len(name) == 0 || ctx.Repo.Owner.LowerName == name { | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| return | |||||
| } | |||||
| team, err := ctx.Repo.Owner.GetTeam(name) | |||||
| if err != nil { | |||||
| if models.IsErrTeamNotExist(err) { | |||||
| ctx.Flash.Error(ctx.Tr("form.team_not_exist")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| } else { | |||||
| ctx.ServerError("GetTeam", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| if team.OrgID != ctx.Repo.Repository.OwnerID { | |||||
| ctx.Flash.Error(ctx.Tr("repo.settings.team_not_in_organization")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| return | |||||
| } | |||||
| if models.HasTeamRepo(ctx.Repo.Repository.OwnerID, team.ID, ctx.Repo.Repository.ID) { | |||||
| ctx.Flash.Error(ctx.Tr("repo.settings.add_team_duplicate")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| return | |||||
| } | |||||
| if err = team.AddRepository(ctx.Repo.Repository); err != nil { | |||||
| ctx.ServerError("team.AddRepository", err) | |||||
| return | |||||
| } | |||||
| ctx.Flash.Success(ctx.Tr("repo.settings.add_team_success")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| } | |||||
| // DeleteTeam response for deleting a team from a repository | |||||
| func DeleteTeam(ctx *context.Context) { | |||||
| if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { | |||||
| ctx.Flash.Error(ctx.Tr("repo.settings.change_team_access_not_allowed")) | |||||
| ctx.Redirect(ctx.Repo.RepoLink + "/settings/collaboration") | |||||
| return | |||||
| } | |||||
| team, err := models.GetTeamByID(ctx.QueryInt64("id")) | |||||
| if err != nil { | |||||
| ctx.ServerError("GetTeamByID", err) | |||||
| return | |||||
| } | |||||
| if err = team.RemoveRepository(ctx.Repo.Repository.ID); err != nil { | |||||
| ctx.ServerError("team.RemoveRepositorys", err) | |||||
| return | |||||
| } | |||||
| ctx.Flash.Success(ctx.Tr("repo.settings.remove_team_success")) | |||||
| ctx.JSON(200, map[string]interface{}{ | |||||
| "redirect": ctx.Repo.RepoLink + "/settings/collaboration", | |||||
| }) | |||||
| } | |||||
| // parseOwnerAndRepo get repos by owner | // parseOwnerAndRepo get repos by owner | ||||
| func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | func parseOwnerAndRepo(ctx *context.Context) (*models.User, *models.Repository) { | ||||
| owner, err := models.GetUserByName(ctx.Params(":username")) | owner, err := models.GetUserByName(ctx.Params(":username")) | ||||
| @@ -185,3 +185,196 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) { | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | ||||
| assert.NotEmpty(t, ctx.Flash.ErrorMsg) | assert.NotEmpty(t, ctx.Flash.ErrorMsg) | ||||
| } | } | ||||
| func TestAddTeamPost(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "org26/repo43") | |||||
| ctx.Req.Form.Set("team", "team11") | |||||
| org := &models.User{ | |||||
| LowerName: "org26", | |||||
| Type: models.UserTypeOrganization, | |||||
| } | |||||
| team := &models.Team{ | |||||
| ID: 11, | |||||
| OrgID: 26, | |||||
| } | |||||
| re := &models.Repository{ | |||||
| ID: 43, | |||||
| Owner: org, | |||||
| OwnerID: 26, | |||||
| } | |||||
| repo := &context.Repository{ | |||||
| Owner: &models.User{ | |||||
| ID: 26, | |||||
| LowerName: "org26", | |||||
| RepoAdminChangeTeamAccess: true, | |||||
| }, | |||||
| Repository: re, | |||||
| } | |||||
| ctx.Repo = repo | |||||
| AddTeamPost(ctx) | |||||
| assert.True(t, team.HasRepository(re.ID)) | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
| assert.Empty(t, ctx.Flash.ErrorMsg) | |||||
| } | |||||
| func TestAddTeamPost_NotAllowed(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "org26/repo43") | |||||
| ctx.Req.Form.Set("team", "team11") | |||||
| org := &models.User{ | |||||
| LowerName: "org26", | |||||
| Type: models.UserTypeOrganization, | |||||
| } | |||||
| team := &models.Team{ | |||||
| ID: 11, | |||||
| OrgID: 26, | |||||
| } | |||||
| re := &models.Repository{ | |||||
| ID: 43, | |||||
| Owner: org, | |||||
| OwnerID: 26, | |||||
| } | |||||
| repo := &context.Repository{ | |||||
| Owner: &models.User{ | |||||
| ID: 26, | |||||
| LowerName: "org26", | |||||
| RepoAdminChangeTeamAccess: false, | |||||
| }, | |||||
| Repository: re, | |||||
| } | |||||
| ctx.Repo = repo | |||||
| AddTeamPost(ctx) | |||||
| assert.False(t, team.HasRepository(re.ID)) | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
| assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||||
| } | |||||
| func TestAddTeamPost_AddTeamTwice(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "org26/repo43") | |||||
| ctx.Req.Form.Set("team", "team11") | |||||
| org := &models.User{ | |||||
| LowerName: "org26", | |||||
| Type: models.UserTypeOrganization, | |||||
| } | |||||
| team := &models.Team{ | |||||
| ID: 11, | |||||
| OrgID: 26, | |||||
| } | |||||
| re := &models.Repository{ | |||||
| ID: 43, | |||||
| Owner: org, | |||||
| OwnerID: 26, | |||||
| } | |||||
| repo := &context.Repository{ | |||||
| Owner: &models.User{ | |||||
| ID: 26, | |||||
| LowerName: "org26", | |||||
| RepoAdminChangeTeamAccess: true, | |||||
| }, | |||||
| Repository: re, | |||||
| } | |||||
| ctx.Repo = repo | |||||
| AddTeamPost(ctx) | |||||
| AddTeamPost(ctx) | |||||
| assert.True(t, team.HasRepository(re.ID)) | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
| assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||||
| } | |||||
| func TestAddTeamPost_NonExistentTeam(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "org26/repo43") | |||||
| ctx.Req.Form.Set("team", "team-non-existent") | |||||
| org := &models.User{ | |||||
| LowerName: "org26", | |||||
| Type: models.UserTypeOrganization, | |||||
| } | |||||
| re := &models.Repository{ | |||||
| ID: 43, | |||||
| Owner: org, | |||||
| OwnerID: 26, | |||||
| } | |||||
| repo := &context.Repository{ | |||||
| Owner: &models.User{ | |||||
| ID: 26, | |||||
| LowerName: "org26", | |||||
| RepoAdminChangeTeamAccess: true, | |||||
| }, | |||||
| Repository: re, | |||||
| } | |||||
| ctx.Repo = repo | |||||
| AddTeamPost(ctx) | |||||
| assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) | |||||
| assert.NotEmpty(t, ctx.Flash.ErrorMsg) | |||||
| } | |||||
| func TestDeleteTeam(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "org3/team1/repo3") | |||||
| ctx.Req.Form.Set("id", "2") | |||||
| org := &models.User{ | |||||
| LowerName: "org3", | |||||
| Type: models.UserTypeOrganization, | |||||
| } | |||||
| team := &models.Team{ | |||||
| ID: 2, | |||||
| OrgID: 3, | |||||
| } | |||||
| re := &models.Repository{ | |||||
| ID: 3, | |||||
| Owner: org, | |||||
| OwnerID: 3, | |||||
| } | |||||
| repo := &context.Repository{ | |||||
| Owner: &models.User{ | |||||
| ID: 3, | |||||
| LowerName: "org3", | |||||
| RepoAdminChangeTeamAccess: true, | |||||
| }, | |||||
| Repository: re, | |||||
| } | |||||
| ctx.Repo = repo | |||||
| DeleteTeam(ctx) | |||||
| assert.False(t, team.HasRepository(re.ID)) | |||||
| } | |||||
| @@ -629,6 +629,10 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) | m.Combo("").Get(repo.Collaboration).Post(repo.CollaborationPost) | ||||
| m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | m.Post("/access_mode", repo.ChangeCollaborationAccessMode) | ||||
| m.Post("/delete", repo.DeleteCollaboration) | m.Post("/delete", repo.DeleteCollaboration) | ||||
| m.Group("/team", func() { | |||||
| m.Post("", repo.AddTeamPost) | |||||
| m.Post("/delete", repo.DeleteTeam) | |||||
| }) | |||||
| }) | }) | ||||
| m.Group("/branches", func() { | m.Group("/branches", func() { | ||||
| m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) | m.Combo("").Get(repo.ProtectedBranch).Post(repo.ProtectedBranchPost) | ||||
| @@ -32,6 +32,17 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="field" id="permission_box"> | |||||
| <label>{{.i18n.Tr "org.settings.permission"}}</label> | |||||
| <div class="field"> | |||||
| <div class="ui checkbox"> | |||||
| <input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/> | |||||
| <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="inline field"> | <div class="inline field"> | ||||
| <label></label> | <label></label> | ||||
| <button class="ui green button"> | <button class="ui green button"> | ||||
| @@ -56,6 +56,16 @@ | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="field" id="permission_box"> | |||||
| <label>{{.i18n.Tr "org.settings.permission"}}</label> | |||||
| <div class="field"> | |||||
| <div class="ui checkbox"> | |||||
| <input class="hidden" type="checkbox" name="repo_admin_change_team_access" checked/> | |||||
| <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {{if .SignedUser.IsAdmin}} | {{if .SignedUser.IsAdmin}} | ||||
| <div class="ui divider"></div> | <div class="ui divider"></div> | ||||
| @@ -20,7 +20,7 @@ | |||||
| <div class="ui eight wide column"> | <div class="ui eight wide column"> | ||||
| <span class="octicon octicon-shield"></span> | <span class="octicon octicon-shield"></span> | ||||
| <div class="ui inline dropdown"> | <div class="ui inline dropdown"> | ||||
| <div class="text">{{$.i18n.Tr .Collaboration.ModeI18nKey}}</div> | |||||
| <div class="text">{{if eq .Collaboration.Mode 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Collaboration.Mode 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Collaboration.Mode 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div> | |||||
| <i class="dropdown icon"></i> | <i class="dropdown icon"></i> | ||||
| <div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}"> | <div class="access-mode menu" data-url="{{$.Link}}/access_mode" data-uid="{{.ID}}"> | ||||
| <div class="item" data-text="{{$.i18n.Tr "repo.settings.collaboration.admin"}}" data-value="3">{{$.i18n.Tr "repo.settings.collaboration.admin"}}</div> | <div class="item" data-text="{{$.i18n.Tr "repo.settings.collaboration.admin"}}" data-value="3">{{$.i18n.Tr "repo.settings.collaboration.admin"}}</div> | ||||
| @@ -51,6 +51,63 @@ | |||||
| <button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button> | <button class="ui green button">{{.i18n.Tr "repo.settings.add_collaborator"}}</button> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| <h4 class="ui top attached header"> | |||||
| Teams | |||||
| </h4> | |||||
| {{ $allowedToChangeTeams := ( or (.Org.RepoAdminChangeTeamAccess) (.Permission.IsOwner)) }} | |||||
| {{if .Teams}} | |||||
| <div class="ui attached segment collaborator list"> | |||||
| {{range $t, $team := .Teams}} | |||||
| <div class="item ui grid"> | |||||
| <div class="ui five wide column"> | |||||
| <a href="{{AppSubUrl}}/org/{{$.OrgName}}/teams/{{.LowerName}}"> | |||||
| {{.Name}} | |||||
| </a> | |||||
| </div> | |||||
| <div class="ui eight wide column poping up" data-content="Team's permission is set on the team setting page and can't be changed per repository"> | |||||
| <span class="octicon octicon-shield"></span> | |||||
| <div class="ui inline dropdown"> | |||||
| <div class="text">{{if eq .Authorize 1}}{{$.i18n.Tr "repo.settings.collaboration.read"}}{{else if eq .Authorize 2}}{{$.i18n.Tr "repo.settings.collaboration.write"}}{{else if eq .Authorize 3}}{{$.i18n.Tr "repo.settings.collaboration.admin"}}{{else if eq .Authorize 4}}{{$.i18n.Tr "repo.settings.collaboration.owner"}}{{else}}{{$.i18n.Tr "repo.settings.collaboration.undefined"}}{{end}}</div> | |||||
| </div> | |||||
| {{ if or (eq .Authorize 1) (eq .Authorize 2) }} | |||||
| {{ $first := true }} | |||||
| <div class="description"> | |||||
| Sections: {{range $u, $unit := $.Units}}{{if and ($.Repo.UnitEnabled $unit.Type) ($team.UnitEnabled $unit.Type)}}{{if $first}}{{ $first = false }}{{else}}, {{end}}{{$.i18n.Tr $unit.NameKey}}{{end}}{{end}} {{if $first}}None{{end}} | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| {{if $allowedToChangeTeams}} | |||||
| {{ $globalRepoAccess := (eq .LowerName "owners") }} | |||||
| <div class="ui two wide column {{if $globalRepoAccess}}poping up{{end}}" {{if $globalRepoAccess}}data-content="This team has access to all repositories and can't be removed."{{end}}> | |||||
| <button class="ui red tiny button inline text-thin delete-button {{if $globalRepoAccess}}disabled{{end}}" data-url="{{$.Link}}/team/delete" data-id="{{.ID}}"> | |||||
| {{$.i18n.Tr "repo.settings.delete_collaborator"}} | |||||
| </button> | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| {{end}} | |||||
| <div class="ui bottom attached segment"> | |||||
| {{if $allowedToChangeTeams}} | |||||
| <form class="ui form" id="repo-collab-team-form" action="{{.Link}}/team" method="post"> | |||||
| {{.CsrfTokenHtml}} | |||||
| <div class="inline field ui left"> | |||||
| <div id="search-team-box" class="ui search" data-org="{{.OrgID}}"> | |||||
| <div class="ui input"> | |||||
| <input class="prompt" name="team" placeholder="Search teams..." autocomplete="off" autofocus required> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <button class="ui green button">Add Team</button> | |||||
| </form> | |||||
| {{else}} | |||||
| <div class="item"> | |||||
| Changing team access for repository has been restricted to organization owner | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -7718,6 +7718,10 @@ | |||||
| "type": "string", | "type": "string", | ||||
| "x-go-name": "Location" | "x-go-name": "Location" | ||||
| }, | }, | ||||
| "repo_admin_change_team_access": { | |||||
| "type": "boolean", | |||||
| "x-go-name": "RepoAdminChangeTeamAccess" | |||||
| }, | |||||
| "username": { | "username": { | ||||
| "type": "string", | "type": "string", | ||||
| "x-go-name": "UserName" | "x-go-name": "UserName" | ||||
| @@ -8262,6 +8266,10 @@ | |||||
| "type": "string", | "type": "string", | ||||
| "x-go-name": "Location" | "x-go-name": "Location" | ||||
| }, | }, | ||||
| "repo_admin_change_team_access": { | |||||
| "type": "boolean", | |||||
| "x-go-name": "RepoAdminChangeTeamAccess" | |||||
| }, | |||||
| "visibility": { | "visibility": { | ||||
| "description": "possible values are `public`, `limited` or `private`", | "description": "possible values are `public`, `limited` or `private`", | ||||
| "type": "string", | "type": "string", | ||||
| @@ -9271,6 +9279,10 @@ | |||||
| "type": "string", | "type": "string", | ||||
| "x-go-name": "Location" | "x-go-name": "Location" | ||||
| }, | }, | ||||
| "repo_admin_change_team_access": { | |||||
| "type": "boolean", | |||||
| "x-go-name": "RepoAdminChangeTeamAccess" | |||||
| }, | |||||
| "username": { | "username": { | ||||
| "type": "string", | "type": "string", | ||||
| "x-go-name": "UserName" | "x-go-name": "UserName" | ||||