Files
gitea/templates/org/team/new.tmpl
bircni 55250407dd feat(org): add team visibility so org members can discover teams (#37680)
Closes #37670.

Today, org members in Gitea only see teams they're a member of. In
larger orgs that hurts onboarding and discoverability — there's no way
to look up which team owns what without asking around. GitHub solves
this with a per-team visibility setting; this PR brings the same model
to Gitea.

## What changes

- Every team gets a `visibility` setting:
- `private` *(default)* — only team members and org owners can see the
team. Same as today's behavior.
- `limited` — listable by any member of the organization. Members and
the repos the team has access to are visible too. Non-org-members still
see nothing.
  - `public` — listable by any signed-in user.
- The Owners team visibility is fixed and cannot be changed via
settings.
- Existing teams default to `private`, so this is a no-op for anyone who
doesn't change anything.

## API

- `Team`, `CreateTeamOption`, `EditTeamOption` all gain a `visibility`
field (string enum: `private` | `limited` | `public`).
- `GET /orgs/{org}/teams` and `/orgs/{org}/teams/search` now apply the
same visibility rules as the web UI:
  - site admins and org owners still see every team
- other org members see their own teams plus any `limited` or `public`
team
  - `private` teams are no longer leaked through these endpoints
- Swagger/OpenAPI specs regenerated.

## UI

View from admin2 (not an owner):
<img width="1669" height="726"
src="https://github.com/user-attachments/assets/daf4bccb-644b-4426-b178-71963aeaf73b"
/>

View from admin (owner):

<img width="2559" height="863"
src="https://github.com/user-attachments/assets/4f22cebc-e9df-4fd2-8ed4-724d31fadb7a"
/>

---------

Signed-off-by: bircni <bircni@icloud.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-06-14 19:07:25 +00:00

201 lines
10 KiB
Handlebars

{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization new team">
{{template "org/header" .}}
<div class="ui container">
<div class="ui grid">
<div class="column">
<form class="ui form" action="{{if .PageIsOrgTeamsNew}}{{.OrgLink}}/teams/new{{else}}{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/edit{{end}}" data-delete-url="{{.OrgLink}}/teams/{{.Team.LowerName | PathEscape}}/delete" method="post">
<h3 class="ui top attached header">
{{if .PageIsOrgTeamsNew}}{{ctx.Locale.Tr "org.create_new_team"}}{{else}}{{ctx.Locale.Tr "org.teams.settings"}}{{end}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<div class="required field {{if .Err_TeamName}}error{{end}}">
<label for="team_name">{{ctx.Locale.Tr "org.team_name"}}</label>
{{if .Team.IsOwnerTeam}}
<input type="hidden" name="team_name" value="{{.Team.Name}}">
{{end}}
<input id="team_name" name="team_name" value="{{.Team.Name}}" required {{if .Team.IsOwnerTeam}}disabled{{end}} autofocus>
<span class="help">{{ctx.Locale.Tr "org.team_name_helper"}}</span>
</div>
<div class="field {{if .Err_Description}}error{{end}}">
<label for="description">{{ctx.Locale.Tr "org.team_desc"}}</label>
<input id="description" name="description" value="{{.Team.Description}}" maxlength="255">
<span class="help">{{ctx.Locale.Tr "org.team_desc_helper"}}</span>
</div>
{{if .Team.IsOwnerTeam}}
<div class="field">
<label>{{ctx.Locale.Tr "org.teams.visibility"}}</label>
<div class="tw-mb-1">
{{if .Team.IsPrivate}}
<span class="ui mini label">{{ctx.Locale.Tr "org.teams.visibility_private"}}</span>
{{else if .Team.IsLimited}}
<span class="ui mini label">{{ctx.Locale.Tr "org.teams.visibility_limited"}}</span>
{{else if .Team.IsPublic}}
<span class="ui mini label">{{ctx.Locale.Tr "org.teams.visibility_public"}}</span>
{{end}}
</div>
<span class="help">{{ctx.Locale.Tr "org.teams.owners_visibility_fixed"}}</span>
</div>
{{end}}
{{if not .Team.IsOwnerTeam}}
<div class="grouped field">
<label>{{ctx.Locale.Tr "org.teams.visibility"}}</label>
<br>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="visibility" value="private" {{if or .PageIsOrgTeamsNew .Team.IsPrivate}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.visibility_private"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.visibility_private_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="visibility" value="limited" {{if .Team.IsLimited}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.visibility_limited"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.visibility_limited_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="visibility" value="public" {{if .Team.IsPublic}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.visibility_public"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.visibility_public_helper"}}</span>
</div>
</div>
</div>
<div class="grouped field">
<label>{{ctx.Locale.Tr "org.team_access_desc"}}</label>
<br>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="repo_access" value="specific" {{if not .Team.IncludesAllRepositories}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.specific_repositories"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.specific_repositories_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="repo_access" value="all" {{if .Team.IncludesAllRepositories}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.all_repositories"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.all_repositories_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui checkbox">
<label for="can_create_org_repo">{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</label>
<input id="can_create_org_repo" name="can_create_org_repo" type="checkbox" {{if .Team.CanCreateOrgRepo}}checked{{end}}>
<span class="help">{{ctx.Locale.Tr "org.teams.can_create_org_repo_helper"}}</span>
</div>
</div>
</div>
<div class="grouped field">
<label>{{ctx.Locale.Tr "org.team_permission_desc"}}</label>
<br>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="permission" value="read" {{if or .PageIsOrgTeamsNew (eq .Team.AccessMode 0) (eq .Team.AccessMode 1) (eq .Team.AccessMode 2)}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.general_access"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.general_access_helper"}}</span>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="permission" value="admin" {{if eq .Team.AccessMode 3}}checked{{end}}>
<label>{{ctx.Locale.Tr "org.teams.admin_access"}}</label>
<span class="help">{{ctx.Locale.Tr "org.teams.admin_access_helper"}}</span>
</div>
</div>
</div>
<div class="divider"></div>
<div class="team-units required grouped field {{if eq .Team.AccessMode 3}}tw-hidden{{end}}">
<label>{{ctx.Locale.Tr "org.team_unit_desc"}}</label>
<table class="ui celled table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "units.unit"}}</th>
<th class="tw-text-center">{{ctx.Locale.Tr "org.teams.none_access"}}
<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.none_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
<th class="tw-text-center">{{ctx.Locale.Tr "org.teams.read_access"}}
<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.read_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
<th class="tw-text-center">{{ctx.Locale.Tr "org.teams.write_access"}}
<span class="tw-align-middle" data-tooltip-content="{{ctx.Locale.Tr "org.teams.write_access_helper"}}">{{svg "octicon-question" 16 "tw-ml-1"}}</span></th>
</tr>
</thead>
<tbody>
{{range $t, $unit := $.Units}}
{{if ge $unit.MaxPerm 2}}
<tr>
<td>
<div {{if $unit.Type.UnitGlobalDisabled}}class="field" data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{- else -}}class="field"{{end}}>
<div>
<label>{{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}}</label>
<span class="help">{{ctx.Locale.Tr $unit.DescKey}}</span>
</div>
</div>
</td>
<td class="tw-text-center">
<div class="ui radio checkbox">
<input type="radio" name="unit_{{$unit.Type.Value}}" value="0"{{if or ($unit.Type.UnitGlobalDisabled) (eq ($.Team.UnitAccessMode ctx $unit.Type) 0)}} checked{{end}} title="{{ctx.Locale.Tr "org.teams.none_access"}}">
</div>
</td>
<td class="tw-text-center">
<div class="ui radio checkbox">
<input type="radio" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode ctx $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}} title="{{ctx.Locale.Tr "org.teams.read_access"}}">
</div>
</td>
<td class="tw-text-center">
<div class="ui radio checkbox">
<input type="radio" name="unit_{{$unit.Type.Value}}" value="2"{{if (ge ($.Team.UnitAccessMode ctx $unit.Type) 2)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}} title="{{ctx.Locale.Tr "org.teams.write_access"}}">
</div>
</td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
{{range $t, $unit := $.Units}}
{{if lt $unit.MaxPerm 2}}
<div {{if $unit.Type.UnitGlobalDisabled}}class="field" data-tooltip-content="{{ctx.Locale.Tr "repo.unit_disabled"}}"{{else}}class="field"{{end}}>
<div class="ui checkbox">
<input type="checkbox" name="unit_{{$unit.Type.Value}}" value="1"{{if or (eq $.Team.ID 0) (eq ($.Team.UnitAccessMode ctx $unit.Type) 1)}} checked{{end}} {{if $unit.Type.UnitGlobalDisabled}}disabled{{end}}>
<label>{{ctx.Locale.Tr $unit.NameKey}}{{if $unit.Type.UnitGlobalDisabled}} {{ctx.Locale.Tr "org.team_unit_disabled"}}{{end}}</label>
<span class="help">{{ctx.Locale.Tr $unit.DescKey}}</span>
</div>
</div>
{{end}}
{{end}}
</div>
{{end}}
<div class="field">
{{if .PageIsOrgTeamsNew}}
<button class="ui primary button">{{ctx.Locale.Tr "org.create_team"}}</button>
{{else}}
<button class="ui primary button">{{ctx.Locale.Tr "org.teams.update_settings"}}</button>
{{if not .Team.IsOwnerTeam}}
<button class="ui red button delete-button" data-url="{{.OrgLink}}/teams/{{.Team.Name | PathEscape}}/delete">{{ctx.Locale.Tr "org.teams.delete_team"}}</button>
{{end}}
{{end}}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "org.teams.delete_team_title"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.teams.delete_team_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
{{template "base/footer" .}}