Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2629,6 +2629,10 @@ members.remove = Remove
members.remove.detail = Remove %[1]s from %[2]s?
members.leave = Leave
members.leave.detail = Leave %s?
members.invite = Invite
members.invite_member = Invite Member
members.to = To
members.team = Team
members.invite_desc = Add a new member to %s:
members.invite_now = Invite Now

Expand Down
91 changes: 90 additions & 1 deletion routers/web/org/members.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ package org

import (
"net/http"
"net/url"
"strings"

"code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/routers/utils"
shared_user "code.gitea.io/gitea/routers/web/shared/user"
org_service "code.gitea.io/gitea/services/org"
)

const (
// tplMembers template for organization members page
tplMembers base.TplName = "org/member/members"
tplMembers base.TplName = "org/member/members"
tplMembersInvite base.TplName = "org/member/invite"
)

// Members render organization users page
Expand Down Expand Up @@ -76,6 +82,89 @@ func Members(ctx *context.Context) {
ctx.HTML(http.StatusOK, tplMembers)
}

// MembersInvite render organization members invite page
func MembersInvite(ctx *context.Context) {
org := ctx.Org.Organization
ctx.Data["Title"] = org.FullName
ctx.Data["PageIsOrgMembers"] = true
ctx.Data["Teams"] = ctx.Org.Teams
ctx.Data["IsEmailInviteEnabled"] = setting.MailService != nil
if err := shared_user.LoadHeaderCount(ctx); err != nil {
ctx.ServerError("LoadHeaderCount", err)
return
}
ctx.HTML(http.StatusOK, tplMembersInvite)
}

// MembersInviteAction response for invite a member to organization
func MembersInviteAction(ctx *context.Context) {
var err error
if !ctx.Org.IsOwner {
ctx.Error(http.StatusNotFound)
return
}
uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
var team *organization.Team
team, err = organization.GetTeam(ctx, ctx.Org.Organization.ID, ctx.FormString("team"))
if err != nil {
if organization.IsErrTeamNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
} else {
ctx.ServerError("GetTeam", err)
}
return
}
var u *user_model.User
u, err = user_model.GetUserByName(ctx, uname)
if err != nil {
if user_model.IsErrUserNotExist(err) {
if setting.MailService != nil && user_model.ValidateEmail(uname) == nil {
if err := org_service.CreateTeamInvite(ctx, ctx.Doer, team, uname); err != nil {
if organization.IsErrTeamInviteAlreadyExist(err) {
ctx.Flash.Error(ctx.Tr("form.duplicate_invite_to_team"))
} else if organization.IsErrUserEmailAlreadyAdded(err) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
ctx.ServerError("CreateTeamInvite", err)
return
}
}
} else {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(team.LowerName))
} else {
ctx.ServerError("GetUserByName", err)
}
return
}

if u.IsOrganization() {
ctx.Flash.Error(ctx.Tr("form.cannot_add_org_to_team"))
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(team.LowerName))
return
}

if team.IsMember(ctx, u.ID) {
ctx.Flash.Error(ctx.Tr("org.teams.add_duplicate_users"))
} else {
err = models.AddTeamMember(ctx, team, u.ID)
}
if err != nil {
if organization.IsErrLastOrgOwner(err) {
ctx.Flash.Error(ctx.Tr("form.last_org_owner"))
} else {
log.Error("Action(%s): %v", ctx.Params(":action"), err)
ctx.JSON(http.StatusOK, map[string]any{
"ok": false,
"err": err.Error(),
})
return
}
}
ctx.Redirect(ctx.Org.OrgLink + "/teams/" + url.PathEscape(team.LowerName))
}

// MembersAction response for operation to a member of organization
func MembersAction(ctx *context.Context) {
uid := ctx.FormInt64("uid")
Expand Down
2 changes: 2 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,8 @@ func registerRoutes(m *web.Route) {
m.Group("/org", func() {
m.Group("/{org}", func() {
m.Get("/members", org.Members)
m.Get("/members/invite", org.MembersInvite)
m.Post("/members/invite", org.MembersInviteAction)
}, context.OrgAssignment())
}, ignSignIn)

Expand Down
30 changes: 30 additions & 0 deletions templates/org/member/invite.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization invitemember">
{{template "org/header" .}}
<div class="ui container">
{{template "base/alert" .}}

<div class="center">
<h1 class="ui header">{{ctx.Locale.Tr "org.members.invite_member"}}</h1>
<form class="ui form input member-invite" action="{{$.OrgLink}}/members/invite" method="post">
<div class="inline field">
{{.CsrfTokenHtml}}
<input type="hidden" name="uid" value="{{.SignedUser.ID}}">
<label>{{ctx.Locale.Tr "org.members.invite"}}</label>
<div id="search-user-box" class="ui search input"{{if .IsEmailInviteEnabled}} data-allow-email="true" data-allow-email-description="{{ctx.Locale.Tr "org.teams.invite_team_member" $.Team.Name}}"{{end}}>
<input class="prompt" name="uname" placeholder="{{ctx.Locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" autofocus required>
</div>
<label>{{ctx.Locale.Tr "org.members.to"}}</label>
<select class="ui selection dropdown" name="team">
{{range .Teams}}
<option value="{{.LowerName | PathEscape}}">{{.Name}}</option>
{{end}}
</select>
<label>{{ctx.Locale.Tr "org.members.team"}}</label>
<button class="ui primary button">{{ctx.Locale.Tr "org.members.invite_now"}}</button>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}
8 changes: 7 additions & 1 deletion templates/org/member/members.tmpl
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization">
<div role="main" aria-label="{{.Title}}" class="page-content organization members">
{{template "org/header" .}}
<div class="ui container">
{{template "base/alert" .}}
{{if .IsOrganizationOwner}}
<div class="text right">
<a class="ui primary button" href="{{.OrgLink}}/members/invite">{{svg "octicon-plus"}} {{ctx.Locale.Tr "org.members.invite_member"}}</a>
</div>
<div class="divider"></div>
{{end}}

<div class="flex-list">
{{range .Members}}
Expand Down
18 changes: 18 additions & 0 deletions web_src/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,22 @@ a.label,
text-decoration: none !important;
}

.ui.search > .prompt {
color: var(--color-text);
background: var(--color-box-body);
border-color: var(--color-secondary);
}

.ui.search .prompt {
border-radius: var(--border-radius);
}

.ui.search > .prompt:focus {
border-color: var(--color-primary);
background: var(--color-box-body);
color: var(--color-text);
}

.ui.search > .results {
background: var(--color-body);
border-color: var(--color-secondary);
Expand Down Expand Up @@ -257,6 +273,8 @@ ol.ui.list li,
.ui.input.focus > input,
.ui.input > input:focus {
border-color: var(--color-primary);
background: var(--color-box-body);
color: var(--color-text);
}

.ui.action.input .ui.ui.button {
Expand Down