feat: Add Invitation Code to Generate Invitation Link (#2666)

Add auto-population of invitation fields in the registration page based on the invitation code in the link
This commit is contained in:
HGZ-20 2024-02-02 21:12:56 +08:00 committed by GitHub
parent bbbda1982f
commit b7be1943fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 269 additions and 27 deletions

View File

@ -96,6 +96,7 @@ p, *, *, GET, /api/get-organization-names, *, *
p, *, *, GET, /api/get-all-objects, *, * p, *, *, GET, /api/get-all-objects, *, *
p, *, *, GET, /api/get-all-actions, *, * p, *, *, GET, /api/get-all-actions, *, *
p, *, *, GET, /api/get-all-roles, *, * p, *, *, GET, /api/get-all-roles, *, *
p, *, *, GET, /api/get-invitation-info, *, *
` `
sa := stringadapter.NewAdapter(ruleText) sa := stringadapter.NewAdapter(ruleText)

View File

@ -227,7 +227,7 @@ func (c *ApiController) Signup() {
if invitation != nil { if invitation != nil {
invitation.UsedCount += 1 invitation.UsedCount += 1
_, err := object.UpdateInvitation(invitation.GetId(), invitation) _, err := object.UpdateInvitation(invitation.GetId(), invitation, c.GetAcceptLanguage())
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return

View File

@ -84,6 +84,32 @@ func (c *ApiController) GetInvitation() {
c.ResponseOk(invitation) c.ResponseOk(invitation)
} }
// GetInvitationCodeInfo
// @Title GetInvitationCodeInfo
// @Tag Invitation API
// @Description get invitation code information
// @Param code query string true "Invitation code"
// @Success 200 {object} object.Invitation The Response object
// @router /get-invitation-info [get]
func (c *ApiController) GetInvitationCodeInfo() {
code := c.Input().Get("code")
applicationId := c.Input().Get("applicationId")
application, err := object.GetApplication(applicationId)
if err != nil {
c.ResponseError(err.Error())
return
}
invitation, msg := object.GetInvitationByCode(code, application.Organization, c.GetAcceptLanguage())
if msg != "" {
c.ResponseError(msg)
return
}
c.ResponseOk(object.GetMaskedInvitation(invitation))
}
// UpdateInvitation // UpdateInvitation
// @Title UpdateInvitation // @Title UpdateInvitation
// @Tag Invitation API // @Tag Invitation API
@ -102,7 +128,7 @@ func (c *ApiController) UpdateInvitation() {
return return
} }
c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation)) c.Data["json"] = wrapActionResponse(object.UpdateInvitation(id, &invitation, c.GetAcceptLanguage()))
c.ServeJSON() c.ServeJSON()
} }
@ -121,7 +147,7 @@ func (c *ApiController) AddInvitation() {
return return
} }
c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation)) c.Data["json"] = wrapActionResponse(object.AddInvitation(&invitation, c.GetAcceptLanguage()))
c.ServeJSON() c.ServeJSON()
} }

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein", "Affiliation cannot be blank": "Zugehörigkeit darf nicht leer sein",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Anzeigename kann nicht leer sein", "DisplayName cannot be blank": "Anzeigename kann nicht leer sein",
"DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname", "DisplayName is not valid real name": "DisplayName ist kein gültiger Vorname",
"Email already exists": "E-Mail existiert bereits", "Email already exists": "E-Mail existiert bereits",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Afiliación no puede estar en blanco", "Affiliation cannot be blank": "Afiliación no puede estar en blanco",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco", "DisplayName cannot be blank": "El nombre de visualización no puede estar en blanco",
"DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido", "DisplayName is not valid real name": "El nombre de pantalla no es un nombre real válido",
"Email already exists": "El correo electrónico ya existe", "Email already exists": "El correo electrónico ya existe",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation ne peut pas être vide", "Affiliation cannot be blank": "Affiliation ne peut pas être vide",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide", "DisplayName cannot be blank": "Le nom d'affichage ne peut pas être vide",
"DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide", "DisplayName is not valid real name": "DisplayName n'est pas un nom réel valide",
"Email already exists": "E-mail déjà existant", "Email already exists": "E-mail déjà existant",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Keterkaitan tidak boleh kosong", "Affiliation cannot be blank": "Keterkaitan tidak boleh kosong",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong", "DisplayName cannot be blank": "Nama Pengguna tidak boleh kosong",
"DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid", "DisplayName is not valid real name": "DisplayName bukanlah nama asli yang valid",
"Email already exists": "Email sudah ada", "Email already exists": "Email sudah ada",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "所属は空白にできません", "Affiliation cannot be blank": "所属は空白にできません",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "表示名は空白にできません", "DisplayName cannot be blank": "表示名は空白にできません",
"DisplayName is not valid real name": "表示名は有効な実名ではありません", "DisplayName is not valid real name": "表示名は有効な実名ではありません",
"Email already exists": "メールは既に存在します", "Email already exists": "メールは既に存在します",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "소속은 비워 둘 수 없습니다", "Affiliation cannot be blank": "소속은 비워 둘 수 없습니다",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다", "DisplayName cannot be blank": "DisplayName는 비어 있을 수 없습니다",
"DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다", "DisplayName is not valid real name": "DisplayName는 유효한 실제 이름이 아닙니다",
"Email already exists": "이메일이 이미 존재합니다", "Email already exists": "이메일이 이미 존재합니다",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Принадлежность не может быть пустым значением", "Affiliation cannot be blank": "Принадлежность не может быть пустым значением",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Имя отображения не может быть пустым", "DisplayName cannot be blank": "Имя отображения не может быть пустым",
"DisplayName is not valid real name": "DisplayName не является действительным именем", "DisplayName is not valid real name": "DisplayName не является действительным именем",
"Email already exists": "Электронная почта уже существует", "Email already exists": "Электронная почта уже существует",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Affiliation cannot be blank", "Affiliation cannot be blank": "Affiliation cannot be blank",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "DisplayName cannot be blank", "DisplayName cannot be blank": "DisplayName cannot be blank",
"DisplayName is not valid real name": "DisplayName is not valid real name", "DisplayName is not valid real name": "DisplayName is not valid real name",
"Email already exists": "Email already exists", "Email already exists": "Email already exists",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "Tình trạng liên kết không thể để trống", "Affiliation cannot be blank": "Tình trạng liên kết không thể để trống",
"Default code does not match the code's matching rules": "Default code does not match the code's matching rules",
"DisplayName cannot be blank": "Tên hiển thị không thể để trống", "DisplayName cannot be blank": "Tên hiển thị không thể để trống",
"DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ", "DisplayName is not valid real name": "DisplayName không phải là tên thật hợp lệ",
"Email already exists": "Email đã tồn tại", "Email already exists": "Email đã tồn tại",

View File

@ -30,6 +30,7 @@
}, },
"check": { "check": {
"Affiliation cannot be blank": "工作单位不可为空", "Affiliation cannot be blank": "工作单位不可为空",
"Default code does not match the code's matching rules": "邀请码默认值和邀请码规则不匹配",
"DisplayName cannot be blank": "显示名称不可为空", "DisplayName cannot be blank": "显示名称不可为空",
"DisplayName is not valid real name": "显示名称必须是真实姓名", "DisplayName is not valid real name": "显示名称必须是真实姓名",
"Email already exists": "该邮箱已存在", "Email already exists": "该邮箱已存在",

View File

@ -184,6 +184,15 @@ func CheckInvitationCode(application *Application, organization *Organization, a
} }
} }
func CheckInvitationDefaultCode(code string, defaultCode string, lang string) error {
if matched, err := util.IsInvitationCodeMatch(code, defaultCode); err != nil {
return err
} else if !matched {
return fmt.Errorf(i18n.Translate(lang, "check:Default code does not match the code's matching rules"))
}
return nil
}
func checkSigninErrorTimes(user *User, lang string) error { func checkSigninErrorTimes(user *User, lang string) error {
failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user) failedSigninLimit, failedSigninFrozenTime, err := GetFailedSigninConfigByUser(user)
if err != nil { if err != nil {

View File

@ -40,6 +40,7 @@ type Invitation struct {
Phone string `xorm:"varchar(100)" json:"phone"` Phone string `xorm:"varchar(100)" json:"phone"`
SignupGroup string `xorm:"varchar(100)" json:"signupGroup"` SignupGroup string `xorm:"varchar(100)" json:"signupGroup"`
DefaultCode string `xorm:"varchar(100)" json:"defaultCode"`
State string `xorm:"varchar(100)" json:"state"` State string `xorm:"varchar(100)" json:"state"`
} }
@ -93,7 +94,45 @@ func GetInvitation(id string) (*Invitation, error) {
return getInvitation(owner, name) return getInvitation(owner, name)
} }
func UpdateInvitation(id string, invitation *Invitation) (bool, error) { func GetInvitationByCode(code string, organizationName string, lang string) (*Invitation, string) {
invitations, err := GetInvitations(organizationName)
if err != nil {
return nil, err.Error()
}
errMsg := ""
for _, invitation := range invitations {
if isValid, msg := invitation.SimpleCheckInvitationCode(code, lang); isValid {
return invitation, msg
} else if msg != "" && errMsg == "" {
errMsg = msg
}
}
if errMsg != "" {
return nil, errMsg
} else {
return nil, i18n.Translate(lang, "check:Invitation code is invalid")
}
}
func GetMaskedInvitation(invitation *Invitation) *Invitation {
if invitation == nil {
return nil
}
invitation.CreatedTime = ""
invitation.UpdatedTime = ""
invitation.Code = "***"
invitation.DefaultCode = "***"
invitation.IsRegexp = false
invitation.Quota = -1
invitation.UsedCount = -1
invitation.SignupGroup = ""
return invitation
}
func UpdateInvitation(id string, invitation *Invitation, lang string) (bool, error) {
owner, name := util.GetOwnerAndNameFromId(id) owner, name := util.GetOwnerAndNameFromId(id)
if p, err := getInvitation(owner, name); err != nil { if p, err := getInvitation(owner, name); err != nil {
return false, err return false, err
@ -107,6 +146,11 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
invitation.IsRegexp = isRegexp invitation.IsRegexp = isRegexp
} }
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
if err != nil {
return false, err
}
affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation) affected, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(invitation)
if err != nil { if err != nil {
return false, err return false, err
@ -115,13 +159,18 @@ func UpdateInvitation(id string, invitation *Invitation) (bool, error) {
return affected != 0, nil return affected != 0, nil
} }
func AddInvitation(invitation *Invitation) (bool, error) { func AddInvitation(invitation *Invitation, lang string) (bool, error) {
if isRegexp, err := util.IsRegexp(invitation.Code); err != nil { if isRegexp, err := util.IsRegexp(invitation.Code); err != nil {
return false, err return false, err
} else { } else {
invitation.IsRegexp = isRegexp invitation.IsRegexp = isRegexp
} }
err := CheckInvitationDefaultCode(invitation.Code, invitation.DefaultCode, lang)
if err != nil {
return false, err
}
affected, err := ormer.Engine.Insert(invitation) affected, err := ormer.Engine.Insert(invitation)
if err != nil { if err != nil {
return false, err return false, err
@ -147,7 +196,7 @@ func VerifyInvitation(id string) (payment *Payment, attachInfo map[string]interf
return nil, nil, fmt.Errorf("the invitation: %s does not exist", id) return nil, nil, fmt.Errorf("the invitation: %s does not exist", id)
} }
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) { func (invitation *Invitation) SimpleCheckInvitationCode(invitationCode string, lang string) (bool, string) {
if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil { if matched, err := util.IsInvitationCodeMatch(invitation.Code, invitationCode); err != nil {
return false, err.Error() return false, err.Error()
} else if !matched { } else if !matched {
@ -160,15 +209,6 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
if invitation.UsedCount >= invitation.Quota { if invitation.UsedCount >= invitation.Quota {
return false, i18n.Translate(lang, "check:Invitation code exhausted") return false, i18n.Translate(lang, "check:Invitation code exhausted")
} }
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
}
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
}
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
}
// Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters // Determine whether the invitation code is in the form of a regular expression other than pure numbers and letters
if invitation.IsRegexp { if invitation.IsRegexp {
@ -179,3 +219,19 @@ func (invitation *Invitation) IsInvitationCodeValid(application *Application, in
} }
return true, "" return true, ""
} }
func (invitation *Invitation) IsInvitationCodeValid(application *Application, invitationCode string, username string, email string, phone string, lang string) (bool, string) {
if isValid, msg := invitation.SimpleCheckInvitationCode(invitationCode, lang); !isValid {
return false, msg
}
if application.IsSignupItemRequired("Username") && invitation.Username != "" && invitation.Username != username {
return false, i18n.Translate(lang, "check:Please register using the username corresponding to the invitation code")
}
if application.IsSignupItemRequired("Email") && invitation.Email != "" && invitation.Email != email {
return false, i18n.Translate(lang, "check:Please register using the email corresponding to the invitation code")
}
if application.IsSignupItemRequired("Phone") && invitation.Phone != "" && invitation.Phone != phone {
return false, i18n.Translate(lang, "check:Please register using the phone corresponding to the invitation code")
}
return true, ""
}

View File

@ -94,6 +94,7 @@ func initAPI() {
beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations") beego.Router("/api/get-invitations", &controllers.ApiController{}, "GET:GetInvitations")
beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation") beego.Router("/api/get-invitation", &controllers.ApiController{}, "GET:GetInvitation")
beego.Router("/api/get-invitation-info", &controllers.ApiController{}, "GET:GetInvitationCodeInfo")
beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation") beego.Router("/api/update-invitation", &controllers.ApiController{}, "POST:UpdateInvitation")
beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation") beego.Router("/api/add-invitation", &controllers.ApiController{}, "POST:AddInvitation")
beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation") beego.Router("/api/delete-invitation", &controllers.ApiController{}, "POST:DeleteInvitation")

View File

@ -19,6 +19,7 @@ import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend"; import * as ApplicationBackend from "./backend/ApplicationBackend";
import * as Setting from "./Setting"; import * as Setting from "./Setting";
import i18next from "i18next"; import i18next from "i18next";
import copy from "copy-to-clipboard";
const {Option} = Select; const {Option} = Select;
@ -99,6 +100,18 @@ class InvitationEditPage extends React.Component {
{this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}     {this.state.mode === "add" ? i18next.t("invitation:New Invitation") : i18next.t("invitation:Edit Invitation")}    
<Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} onClick={() => {
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner"> } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
@ -140,10 +153,24 @@ class InvitationEditPage extends React.Component {
</Col> </Col>
<Col span={22} > <Col span={22} >
<Input value={this.state.invitation.code} onChange={e => { <Input value={this.state.invitation.code} onChange={e => {
const regex = /[^a-zA-Z0-9]/;
if (!regex.test(e.target.value)) {
this.updateInvitationField("defaultCode", e.target.value);
}
this.updateInvitationField("code", e.target.value); this.updateInvitationField("code", e.target.value);
}} /> }} />
</Col> </Col>
</Row> </Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Default code"), i18next.t("invitation:Default code - Tooltip"))} :
</Col>
<Col span={22} >
<Input value={this.state.invitation.defaultCode} onChange={e => {
this.updateInvitationField("defaultCode", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} > <Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> <Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} : {Setting.getLabel(i18next.t("invitation:Quota"), i18next.t("invitation:Quota - Tooltip"))} :
@ -274,6 +301,18 @@ class InvitationEditPage extends React.Component {
<div style={{marginTop: "20px", marginLeft: "40px"}}> <div style={{marginTop: "20px", marginLeft: "40px"}}>
<Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button> <Button size="large" onClick={() => this.submitInvitationEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button> <Button style={{marginLeft: "20px"}} type="primary" size="large" onClick={() => this.submitInvitationEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
<Button style={{marginLeft: "20px"}} size="large" onClick={() => {
let defaultApplication;
if (this.state.invitation.owner === "built-in") {
defaultApplication = "app-built-in";
} else {
defaultApplication = Setting.getArrayItem(this.state.organizations, "name", this.state.invitation.owner).defaultApplication;
}
copy(`${window.location.origin}/signup/${defaultApplication}?invitationCode=${this.state.invitation?.defaultCode}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null} {this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} size="large" onClick={() => this.deleteInvitation()}>{i18next.t("general:Cancel")}</Button> : null}
</div> </div>
</div> </div>

View File

@ -22,19 +22,20 @@ import * as InvitationBackend from "./backend/InvitationBackend";
import i18next from "i18next"; import i18next from "i18next";
import BaseListPage from "./BaseListPage"; import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal"; import PopconfirmModal from "./common/modal/PopconfirmModal";
import copy from "copy-to-clipboard";
class InvitationListPage extends BaseListPage { class InvitationListPage extends BaseListPage {
newInvitation() { newInvitation() {
const randomName = Setting.getRandomName(); const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account); const owner = Setting.getRequestOrganization(this.props.account);
const code = Math.random().toString(36).slice(-10);
return { return {
owner: owner, owner: owner,
name: `invitation_${randomName}`, name: `invitation_${randomName}`,
createdTime: moment().format(), createdTime: moment().format(),
updatedTime: moment().format(), updatedTime: moment().format(),
displayName: `New Invitation - ${randomName}`, displayName: `New Invitation - ${randomName}`,
code: Math.random().toString(36).slice(-10), code: code,
defaultCode: code,
quota: 1, quota: 1,
usedCount: 0, usedCount: 0,
application: "All", application: "All",
@ -225,17 +226,11 @@ class InvitationListPage extends BaseListPage {
title: i18next.t("general:Action"), title: i18next.t("general:Action"),
dataIndex: "", dataIndex: "",
key: "op", key: "op",
width: "350px", width: "180px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => {
copy(`${window.location.origin}/login/${record.owner}?invitation_code=${record.code}`);
Setting.showMessage("success", i18next.t("general:Copied to clipboard successfully"));
}}>
{i18next.t("application:Copy signup page URL")}
</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/invitations/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}

View File

@ -29,6 +29,7 @@ import LanguageSelect from "../common/select/LanguageSelect";
import {withRouter} from "react-router-dom"; import {withRouter} from "react-router-dom";
import {CountryCodeSelect} from "../common/select/CountryCodeSelect"; import {CountryCodeSelect} from "../common/select/CountryCodeSelect";
import * as PasswordChecker from "../common/PasswordChecker"; import * as PasswordChecker from "../common/PasswordChecker";
import * as InvitationBackend from "../backend/InvitationBackend";
const formItemLayout = { const formItemLayout = {
labelCol: { labelCol: {
@ -93,6 +94,15 @@ class SignupPage extends React.Component {
if (this.getApplicationObj() === undefined) { if (this.getApplicationObj() === undefined) {
if (this.state.applicationName !== null) { if (this.state.applicationName !== null) {
this.getApplication(this.state.applicationName); this.getApplication(this.state.applicationName);
const sp = new URLSearchParams(window.location.search);
if (sp.has("invitationCode")) {
const invitationCode = sp.get("invitationCode");
this.setState({invitationCode: invitationCode});
if (invitationCode !== "") {
this.getInvitationCodeInfo(invitationCode, "admin/" + this.state.applicationName);
}
}
} else if (oAuthParams !== null) { } else if (oAuthParams !== null) {
this.getApplicationLogin(oAuthParams); this.getApplicationLogin(oAuthParams);
} else { } else {
@ -133,6 +143,17 @@ class SignupPage extends React.Component {
}); });
} }
getInvitationCodeInfo(invitationCode, application) {
InvitationBackend.getInvitationCodeInfo(invitationCode, application)
.then((res) => {
if (res.status === "error") {
Setting.showMessage("error", res.msg);
return;
}
this.setState({invitation: res.data});
});
}
getResultPath(application, signupParams) { getResultPath(application, signupParams) {
if (signupParams?.plan && signupParams?.pricing) { if (signupParams?.plan && signupParams?.pricing) {
// the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page // the prompt page needs the user to be signed in, so for paid-user sign up, just go to buy-plan page
@ -235,7 +256,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.username !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Display name") { } else if (signupItem.name === "Display name") {
@ -363,7 +384,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} onChange={e => this.setState({email: e.target.value})} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation.email !== ""} onChange={e => this.setState({email: e.target.value})} />
</Form.Item> </Form.Item>
{ {
signupItem.rule !== "No verification" && signupItem.rule !== "No verification" &&
@ -434,6 +455,7 @@ class SignupPage extends React.Component {
<Input <Input
placeholder={signupItem.placeholder} placeholder={signupItem.placeholder}
style={{width: "65%"}} style={{width: "65%"}}
disabled={this.state.invitation !== undefined && this.state.invitation.phone !== ""}
onChange={e => this.setState({phone: e.target.value})} onChange={e => this.setState({phone: e.target.value})}
/> />
</Form.Item> </Form.Item>
@ -524,7 +546,7 @@ class SignupPage extends React.Component {
}, },
]} ]}
> >
<Input placeholder={signupItem.placeholder} /> <Input placeholder={signupItem.placeholder} disabled={this.state.invitation !== undefined && this.state.invitation !== ""} />
</Form.Item> </Form.Item>
); );
} else if (signupItem.name === "Agreement") { } else if (signupItem.name === "Agreement") {
@ -554,6 +576,20 @@ class SignupPage extends React.Component {
</Result> </Result>
); );
} }
if (this.state.invitation !== undefined) {
if (this.state.invitation.username !== "") {
this.form.current?.setFieldValue("username", this.state.invitation.username);
}
if (this.state.invitation.email !== "") {
this.form.current?.setFieldValue("email", this.state.invitation.email);
}
if (this.state.invitation.phone !== "") {
this.form.current?.setFieldValue("phone", this.state.invitation.phone);
}
if (this.state.invitationCode !== "") {
this.form.current?.setFieldValue("invitationCode", this.state.invitationCode);
}
}
return ( return (
<Form <Form
{...formItemLayout} {...formItemLayout}

View File

@ -34,6 +34,16 @@ export function getInvitation(owner, name) {
}).then(res => res.json()); }).then(res => res.json());
} }
export function getInvitationCodeInfo(code, applicationName) {
return fetch(`${Setting.ServerUrl}/api/get-invitation-info?code=${code}&applicationId=${encodeURIComponent(applicationName)}`, {
method: "GET",
credentials: "include",
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}
export function updateInvitation(owner, name, invitation) { export function updateInvitation(owner, name, invitation) {
const newInvitation = Setting.deepCopy(invitation); const newInvitation = Setting.deepCopy(invitation);
return fetch(`${Setting.ServerUrl}/api/update-invitation?id=${owner}/${encodeURIComponent(name)}`, { return fetch(`${Setting.ServerUrl}/api/update-invitation?id=${owner}/${encodeURIComponent(name)}`, {

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -390,6 +390,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Can be a single string as an invitation code, or a regular expression. All strings matching the regular expression are valid invitation codes", "Code - Tooltip": "Can be a single string as an invitation code, or a regular expression. All strings matching the regular expression are valid invitation codes",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -388,6 +388,8 @@
"invitation": { "invitation": {
"Code": "Code", "Code": "Code",
"Code - Tooltip": "Code - Tooltip", "Code - Tooltip": "Code - Tooltip",
"Default code": "Default code",
"Default code - Tooltip": "When the invitation code is a regular expression, please enter the invitation code that matches the regular expression rule as the default invitation code for the invitation link",
"Edit Invitation": "Edit Invitation", "Edit Invitation": "Edit Invitation",
"New Invitation": "New Invitation", "New Invitation": "New Invitation",
"Quota": "Quota", "Quota": "Quota",

View File

@ -390,6 +390,8 @@
"invitation": { "invitation": {
"Code": "邀请码", "Code": "邀请码",
"Code - Tooltip": "可以是一个单独的字符串作为邀请码,也可以是正则表达式,所有符合正则表达式的字符串都是合法的邀请码", "Code - Tooltip": "可以是一个单独的字符串作为邀请码,也可以是正则表达式,所有符合正则表达式的字符串都是合法的邀请码",
"Default code": "默认邀请码",
"Default code - Tooltip": "当邀请码为正则表达式时,请输入符合正则表达式规则的邀请码作为邀请链接的默认邀请码",
"Edit Invitation": "编辑邀请码", "Edit Invitation": "编辑邀请码",
"New Invitation": "新建邀请码", "New Invitation": "新建邀请码",
"Quota": "配额", "Quota": "配额",