feat: complete group tree (#1967)

* feat: complete group tree

* feat: ui

* fix: i18n

* refactor code

* fix: support remove user from group

* fix: format code

* Update organization.go

* Update organization.go

* Update user_group.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
This commit is contained in:
Yaodong Yu
2023-06-14 23:27:46 +08:00
committed by GitHub
parent 8e6755845f
commit 7058a34f87
29 changed files with 436 additions and 148 deletions

View File

@ -143,5 +143,6 @@ func (c *ApiController) DeleteGroup() {
return return
} }
c.ResponseOk(wrapActionResponse(object.DeleteGroup(&group))) c.Data["json"] = wrapActionResponse(object.DeleteGroup(&group))
c.ServeJSON()
} }

View File

@ -46,6 +46,15 @@ func (c *ApiController) GetOrganizations() {
c.Data["json"] = maskedOrganizations c.Data["json"] = maskedOrganizations
c.ServeJSON() c.ServeJSON()
} else {
isGlobalAdmin := c.IsGlobalAdmin()
if !isGlobalAdmin {
maskedOrganizations, err := object.GetMaskedOrganizations(object.GetOrganizations(owner, c.getCurrentUser().Owner))
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(maskedOrganizations)
} else { } else {
limit := util.ParseInt(limit) limit := util.ParseInt(limit)
count, err := object.GetOrganizationCount(owner, field, value) count, err := object.GetOrganizationCount(owner, field, value)
@ -63,6 +72,7 @@ func (c *ApiController) GetOrganizations() {
c.ResponseOk(organizations, paginator.Nums()) c.ResponseOk(organizations, paginator.Nums())
} }
}
} }
// GetOrganization ... // GetOrganization ...
@ -74,14 +84,13 @@ func (c *ApiController) GetOrganizations() {
// @router /get-organization [get] // @router /get-organization [get]
func (c *ApiController) GetOrganization() { func (c *ApiController) GetOrganization() {
id := c.Input().Get("id") id := c.Input().Get("id")
maskedOrganization, err := object.GetMaskedOrganization(object.GetOrganization(id)) maskedOrganization, err := object.GetMaskedOrganization(object.GetOrganization(id))
if err != nil { if err != nil {
panic(err) c.ResponseError(err.Error())
return
} }
c.Data["json"] = maskedOrganization c.ResponseOk(maskedOrganization)
c.ServeJSON()
} }
// UpdateOrganization ... // UpdateOrganization ...

View File

@ -90,7 +90,7 @@ func (c *ApiController) GetUsers() {
if limit == "" || page == "" { if limit == "" || page == "" {
if groupId != "" { if groupId != "" {
maskedUsers, err := object.GetMaskedUsers(object.GetUsersByGroup(groupId)) maskedUsers, err := object.GetMaskedUsers(object.GetGroupUsers(groupId))
if err != nil { if err != nil {
c.ResponseError(err.Error()) c.ResponseError(err.Error())
return return
@ -550,3 +550,12 @@ func (c *ApiController) AddUserkeys() {
c.ResponseOk(affected) c.ResponseOk(affected)
} }
func (c *ApiController) RemoveUserFromGroup() {
owner := c.Ctx.Request.Form.Get("owner")
name := c.Ctx.Request.Form.Get("name")
groupId := c.Ctx.Request.Form.Get("groupId")
c.Data["json"] = wrapActionResponse(object.RemoveUserFromGroup(owner, name, groupId))
c.ServeJSON()
}

1
go.mod
View File

@ -59,6 +59,7 @@ require (
github.com/tealeg/xlsx v1.0.5 github.com/tealeg/xlsx v1.0.5
github.com/thanhpk/randstr v1.0.4 github.com/thanhpk/randstr v1.0.4
github.com/tklauser/go-sysconf v0.3.10 // indirect github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/xorm-io/builder v0.3.13 // indirect
github.com/xorm-io/core v0.7.4 github.com/xorm-io/core v0.7.4
github.com/xorm-io/xorm v1.1.6 github.com/xorm-io/xorm v1.1.6
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect

View File

@ -15,6 +15,7 @@
package object package object
import ( import (
"errors"
"fmt" "fmt"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
@ -32,7 +33,7 @@ type Group struct {
Manager string `xorm:"varchar(100)" json:"manager"` Manager string `xorm:"varchar(100)" json:"manager"`
ContactEmail string `xorm:"varchar(100)" json:"contactEmail"` ContactEmail string `xorm:"varchar(100)" json:"contactEmail"`
Type string `xorm:"varchar(100)" json:"type"` Type string `xorm:"varchar(100)" json:"type"`
ParentGroupId string `xorm:"varchar(100)" json:"parentGroupId"` ParentId string `xorm:"varchar(100)" json:"parentId"`
IsTopGroup bool `xorm:"bool" json:"isTopGroup"` IsTopGroup bool `xorm:"bool" json:"isTopGroup"`
Users *[]string `xorm:"-" json:"users"` Users *[]string `xorm:"-" json:"users"`
@ -158,11 +159,45 @@ func AddGroups(groups []*Group) (bool, error) {
} }
func DeleteGroup(group *Group) (bool, error) { func DeleteGroup(group *Group) (bool, error) {
affected, err := adapter.Engine.ID(core.PK{group.Owner, group.Name}).Delete(&Group{}) _, err := adapter.Engine.Get(group)
if err != nil { if err != nil {
return false, err return false, err
} }
if count, err := adapter.Engine.Where("parent_id = ?", group.Id).Count(&Group{}); err != nil {
return false, err
} else if count > 0 {
return false, errors.New("group has children group")
}
if count, err := GetGroupUserCount(group.GetId(), "", ""); err != nil {
return false, err
} else if count > 0 {
return false, errors.New("group has users")
}
session := adapter.Engine.NewSession()
defer session.Close()
if err := session.Begin(); err != nil {
return false, err
}
if _, err := session.Delete(&UserGroupRelation{GroupId: group.Id}); err != nil {
session.Rollback()
return false, err
}
affected, err := session.ID(core.PK{group.Owner, group.Name}).Delete(&Group{})
if err != nil {
session.Rollback()
return false, err
}
if err := session.Commit(); err != nil {
return false, err
}
return affected != 0, nil return affected != 0, nil
} }
@ -170,11 +205,11 @@ func (group *Group) GetId() string {
return fmt.Sprintf("%s/%s", group.Owner, group.Name) return fmt.Sprintf("%s/%s", group.Owner, group.Name)
} }
func ConvertToTreeData(groups []*Group, parentGroupId string) []*Group { func ConvertToTreeData(groups []*Group, parentId string) []*Group {
treeData := []*Group{} treeData := []*Group{}
for _, group := range groups { for _, group := range groups {
if group.ParentGroupId == parentGroupId { if group.ParentId == parentId {
node := &Group{ node := &Group{
Title: group.DisplayName, Title: group.DisplayName,
Key: group.Name, Key: group.Name,

View File

@ -22,6 +22,7 @@ import (
"github.com/casdoor/casdoor/cred" "github.com/casdoor/casdoor/cred"
"github.com/casdoor/casdoor/i18n" "github.com/casdoor/casdoor/i18n"
"github.com/casdoor/casdoor/util" "github.com/casdoor/casdoor/util"
"github.com/xorm-io/builder"
"github.com/xorm-io/core" "github.com/xorm-io/core"
) )
@ -75,12 +76,19 @@ func GetOrganizationCount(owner, field, value string) (int64, error) {
return session.Count(&Organization{}) return session.Count(&Organization{})
} }
func GetOrganizations(owner string) ([]*Organization, error) { func GetOrganizations(owner string, name ...string) ([]*Organization, error) {
organizations := []*Organization{} organizations := []*Organization{}
if name != nil && len(name) > 0 {
err := adapter.Engine.Desc("created_time").Where(builder.In("name", name)).Find(&organizations)
if err != nil {
return nil, err
}
} else {
err := adapter.Engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner}) err := adapter.Engine.Desc("created_time").Find(&organizations, &Organization{Owner: owner})
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
return organizations, nil return organizations, nil
} }

View File

@ -225,14 +225,7 @@ func GetUserCount(owner, field, value string, groupId string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "") session := GetSession(owner, -1, -1, field, value, "", "")
if groupId != "" { if groupId != "" {
group, err := GetGroup(groupId) return GetGroupUserCount(groupId, field, value)
if group == nil || err != nil {
return 0, err
}
// users count in group
return adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Count(&UserGroupRelation{})
} }
return session.Count(&User{}) return session.Count(&User{})
@ -276,20 +269,7 @@ func GetPaginationUsers(owner string, offset, limit int, field, value, sortField
users := []*User{} users := []*User{}
if groupId != "" { if groupId != "" {
group, err := GetGroup(groupId) return GetPaginationGroupUsers(groupId, offset, limit, field, value, sortField, sortOrder)
if group == nil || err != nil {
return []*User{}, err
}
session := adapter.Engine.Prepare()
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
err = session.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
return users, err
} }
session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder) session := GetSessionForUser(owner, offset, limit, field, value, sortField, sortOrder)
@ -300,23 +280,6 @@ func GetPaginationUsers(owner string, offset, limit int, field, value, sortField
return users, nil return users, nil
} }
func GetUsersByGroup(groupId string) ([]*User, error) {
group, err := GetGroup(groupId)
if group == nil || err != nil {
return []*User{}, err
}
users := []*User{}
err = adapter.Engine.Table("user_group_relation").Join("INNER", "user AS u", "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func getUser(owner string, name string) (*User, error) { func getUser(owner string, name string) (*User, error) {
if owner == "" || name == "" { if owner == "" || name == "" {
return nil, nil return nil, nil
@ -574,7 +537,7 @@ func updateUser(oldUser, user *User, columns []string) (int64, error) {
session.Begin() session.Begin()
if util.ContainsString(columns, "groups") { if util.ContainsString(columns, "groups") {
affected, err := updateGroupRelation(session, user) affected, err := updateUserGroupRelation(session, user)
if err != nil { if err != nil {
session.Rollback() session.Rollback()
return affected, err return affected, err
@ -763,6 +726,11 @@ func DeleteUser(user *User) (bool, error) {
return false, err return false, err
} }
affected, err = deleteRelationByUser(user.Id)
if err != nil {
return false, err
}
return affected != 0, nil return affected != 0, nil
} }

View File

@ -2,7 +2,10 @@ package object
import ( import (
"errors" "errors"
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
"github.com/xorm-io/xorm" "github.com/xorm-io/xorm"
) )
@ -14,9 +17,7 @@ type UserGroupRelation struct {
UpdatedTime string `xorm:"updated" json:"updatedTime"` UpdatedTime string `xorm:"updated" json:"updatedTime"`
} }
func updateGroupRelation(session *xorm.Session, user *User) (int64, error) { func updateUserGroupRelation(session *xorm.Session, user *User) (int64, error) {
groupIds := user.Groups
physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{}) physicalGroupCount, err := session.Where("type = ?", "Physical").In("id", user.Groups).Count(Group{})
if err != nil { if err != nil {
return 0, err return 0, err
@ -26,12 +27,12 @@ func updateGroupRelation(session *xorm.Session, user *User) (int64, error) {
} }
groups := []*Group{} groups := []*Group{}
err = session.In("id", groupIds).Find(&groups) err = session.In("id", user.Groups).Find(&groups)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if len(groups) == 0 || len(groups) != len(groupIds) { if len(groups) != len(user.Groups) {
return 0, nil return 0, errors.New("group not found")
} }
_, err = session.Delete(&UserGroupRelation{UserId: user.Id}) _, err = session.Delete(&UserGroupRelation{UserId: user.Id})
@ -43,6 +44,9 @@ func updateGroupRelation(session *xorm.Session, user *User) (int64, error) {
for _, group := range groups { for _, group := range groups {
relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id}) relations = append(relations, &UserGroupRelation{UserId: user.Id, GroupId: group.Id})
} }
if len(relations) == 0 {
return 1, nil
}
_, err = session.Insert(relations) _, err = session.Insert(relations)
if err != nil { if err != nil {
return 0, err return 0, err
@ -50,3 +54,104 @@ func updateGroupRelation(session *xorm.Session, user *User) (int64, error) {
return 1, nil return 1, nil
} }
func RemoveUserFromGroup(owner, name, groupId string) (bool, error) {
user, err := getUser(owner, name)
if err != nil {
return false, err
}
groups := []string{}
for _, group := range user.Groups {
if group != groupId {
groups = append(groups, group)
}
}
user.Groups = groups
_, err = UpdateUser(util.GetId(owner, name), user, []string{"groups"}, false)
if err != nil {
return false, err
}
return true, nil
}
func deleteUserGroupRelation(session *xorm.Session, userId, groupId string) (int64, error) {
affected, err := session.ID(core.PK{userId, groupId}).Delete(&UserGroupRelation{})
return affected, err
}
func deleteRelationByUser(id string) (int64, error) {
affected, err := adapter.Engine.Delete(&UserGroupRelation{UserId: id})
return affected, err
}
func GetGroupUserCount(id string, field, value string) (int64, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return 0, err
}
if field == "" && value == "" {
return adapter.Engine.Count(UserGroupRelation{GroupId: group.Id})
} else {
return adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_id = ?", group.Id).
And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%").
Count()
}
}
func GetPaginationGroupUsers(id string, offset, limit int, field, value, sortField, sortOrder string) ([]*User, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return nil, err
}
users := []*User{}
session := adapter.Engine.Table("user").
Join("INNER", []string{"user_group_relation", "r"}, "user.id = r.user_id").
Where("r.group_id = ?", group.Id)
if offset != -1 && limit != -1 {
session.Limit(limit, offset)
}
if field != "" && value != "" {
session = session.And(fmt.Sprintf("user.%s LIKE ?", util.CamelToSnakeCase(field)), "%"+value+"%")
}
if sortField == "" || sortOrder == "" {
sortField = "created_time"
}
if sortOrder == "ascend" {
session = session.Asc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
} else {
session = session.Desc(fmt.Sprintf("user.%s", util.SnakeString(sortField)))
}
err = session.Find(&users)
if err != nil {
return nil, err
}
return users, nil
}
func GetGroupUsers(id string) ([]*User, error) {
group, err := GetGroup(id)
if group == nil || err != nil {
return []*User{}, err
}
users := []*User{}
err = adapter.Engine.Table("user_group_relation").Join("INNER", []string{"user", "u"}, "user_group_relation.user_id = u.id").
Where("user_group_relation.group_id = ?", group.Id).
Find(&users)
if err != nil {
return nil, err
}
return users, nil
}

View File

@ -77,6 +77,7 @@ func initAPI() {
beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser") beego.Router("/api/add-user", &controllers.ApiController{}, "POST:AddUser")
beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser") beego.Router("/api/delete-user", &controllers.ApiController{}, "POST:DeleteUser")
beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers") beego.Router("/api/upload-users", &controllers.ApiController{}, "POST:UploadUsers")
beego.Router("/api/remove-user-from-group", &controllers.ApiController{}, "POST:RemoveUserFromGroup")
beego.Router("/api/get-groups", &controllers.ApiController{}, "GET:GetGroups") beego.Router("/api/get-groups", &controllers.ApiController{}, "GET:GetGroups")
beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup") beego.Router("/api/get-group", &controllers.ApiController{}, "GET:GetGroup")

View File

@ -131,7 +131,7 @@ class App extends Component {
}); });
if (uri === "/") { if (uri === "/") {
this.setState({selectedMenuKey: "/"}); this.setState({selectedMenuKey: "/"});
} else if (uri.includes("/organizations")) { } else if (uri.includes("/organizations") || uri.includes("/trees")) {
this.setState({selectedMenuKey: "/organizations"}); this.setState({selectedMenuKey: "/organizations"});
} else if (uri.includes("/users")) { } else if (uri.includes("/users")) {
this.setState({selectedMenuKey: "/users"}); this.setState({selectedMenuKey: "/users"});
@ -410,15 +410,13 @@ class App extends Component {
res.push(Setting.getItem(<Link to="/">{i18next.t("general:Home")}</Link>, "/")); res.push(Setting.getItem(<Link to="/">{i18next.t("general:Home")}</Link>, "/"));
if (Setting.isAdminUser(this.state.account)) { if (Setting.isLocalAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>, res.push(Setting.getItem(<Link to="/organizations">{i18next.t("general:Organizations")}</Link>,
"/organizations")); "/organizations"));
res.push(Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>, res.push(Setting.getItem(<Link to="/groups">{i18next.t("general:Groups")}</Link>,
"/groups")); "/groups"));
}
if (Setting.isLocalAdminUser(this.state.account)) {
res.push(Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>, res.push(Setting.getItem(<Link to="/users">{i18next.t("general:Users")}</Link>,
"/users" "/users"
)); ));
@ -560,8 +558,8 @@ class App extends Component {
<Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} /> <Route exact path="/organizations" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationListPage account={this.state.account} {...props} />)} />
<Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} onChangeTheme={this.setTheme} {...props} />)} /> <Route exact path="/organizations/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<OrganizationEditPage account={this.state.account} onChangeTheme={this.setTheme} {...props} />)} />
<Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} /> <Route exact path="/organizations/:organizationName/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
<Route exact path="/group-tree/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} /> <Route exact path="/trees/:organizationName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
<Route exact path="/group-tree/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} /> <Route exact path="/trees/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupTreePage account={this.state.account} {...props} />)} />
<Route exact path="/groups" render={(props) => this.renderLoginIfNotLoggedIn(<GroupListPage account={this.state.account} {...props} />)} /> <Route exact path="/groups" render={(props) => this.renderLoginIfNotLoggedIn(<GroupListPage account={this.state.account} {...props} />)} />
<Route exact path="/groups/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupEditPage account={this.state.account} {...props} />)} /> <Route exact path="/groups/:organizationName/:groupName" render={(props) => this.renderLoginIfNotLoggedIn(<GroupEditPage account={this.state.account} {...props} />)} />
<Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} /> <Route exact path="/users" render={(props) => this.renderLoginIfNotLoggedIn(<UserListPage account={this.state.account} {...props} />)} />
@ -632,7 +630,7 @@ class App extends Component {
isWithoutCard() { isWithoutCard() {
return Setting.isMobile() || window.location.pathname === "/chat" || return Setting.isMobile() || window.location.pathname === "/chat" ||
window.location.pathname.startsWith("/group-tree"); window.location.pathname.startsWith("/trees");
} }
renderContent() { renderContent() {

View File

@ -44,6 +44,11 @@ class GroupEditPage extends React.Component {
GroupBackend.getGroup(this.state.organizationName, this.state.groupName) GroupBackend.getGroup(this.state.organizationName, this.state.groupName)
.then((res) => { .then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
if (res.data === null) {
this.props.history.push("/404");
return;
}
this.setState({ this.setState({
group: res.data, group: res.data,
}); });
@ -171,8 +176,8 @@ class GroupEditPage extends React.Component {
<Col span={22} > <Col span={22} >
<Select style={{width: "100%"}} <Select style={{width: "100%"}}
options={this.getParentIdOptions()} options={this.getParentIdOptions()}
value={this.state.group.parentGroupId} onChange={(value => { value={this.state.group.parentId} onChange={(value => {
this.updateGroupField("parentGroupId", value); this.updateGroupField("parentId", value);
} }
)} /> )} />
</Col> </Col>
@ -193,7 +198,7 @@ class GroupEditPage extends React.Component {
submitGroupEdit(willExist) { submitGroupEdit(willExist) {
const group = Setting.deepCopy(this.state.group); const group = Setting.deepCopy(this.state.group);
group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentGroupId); group["isTopGroup"] = this.state.organizations.some((organization) => organization.name === group.parentId);
GroupBackend.updateGroup(this.state.organizationName, this.state.groupName, group) GroupBackend.updateGroup(this.state.organizationName, this.state.groupName, group)
.then((res) => { .then((res) => {

View File

@ -56,7 +56,7 @@ class GroupListPage extends BaseListPage {
updatedTime: moment().format(), updatedTime: moment().format(),
displayName: `New Group - ${randomName}`, displayName: `New Group - ${randomName}`,
type: "Virtual", type: "Virtual",
parentGroupId: this.props.account.owner, parentId: this.props.account.owner,
isTopGroup: true, isTopGroup: true,
isEnabled: true, isEnabled: true,
}; };
@ -96,7 +96,7 @@ class GroupListPage extends BaseListPage {
}); });
} }
renderTable(groups) { renderTable(data) {
const columns = [ const columns = [
{ {
title: i18next.t("general:Name"), title: i18next.t("general:Name"),
@ -174,15 +174,15 @@ class GroupListPage extends BaseListPage {
}, },
{ {
title: i18next.t("group:Parent group"), title: i18next.t("group:Parent group"),
dataIndex: "parentGroupId", dataIndex: "parentId",
key: "parentGroupId", key: "parentId",
width: "110px", width: "110px",
sorter: true, sorter: true,
...this.getColumnSearchProps("parentGroupId"), ...this.getColumnSearchProps("parentId"),
render: (text, record, index) => { render: (text, record, index) => {
if (record.isTopGroup) { if (record.isTopGroup) {
return <Link to={`/organizations/${record.parentGroupId}`}> return <Link to={`/organizations/${record.parentId}`}>
{record.parentGroupId} {record.parentId}
</Link>; </Link>;
} }
const parentGroup = this.state.groups.find((group) => group.id === text); const parentGroup = this.state.groups.find((group) => group.id === text);
@ -201,10 +201,12 @@ class GroupListPage extends BaseListPage {
width: "170px", width: "170px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
const haveChildren = this.state.groups.find((group) => group.parentId === record.id) !== undefined;
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/groups/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
disabled={haveChildren}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteGroup(index)} onConfirm={() => this.deleteGroup(index)}
> >
@ -224,7 +226,7 @@ class GroupListPage extends BaseListPage {
return ( return (
<div> <div>
<Table scroll={{x: "max-content"}} columns={columns} dataSource={groups} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps} <Table scroll={{x: "max-content"}} columns={columns} dataSource={data} rowKey={(record) => `${record.owner}/${record.name}`} size="middle" bordered pagination={paginationProps}
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Groups")}&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -27,11 +27,12 @@ class GroupTreePage extends React.Component {
super(props); super(props);
this.state = { this.state = {
classes: props, classes: props,
owner: Setting.isAdminUser(this.props.account) ? "" : this.props.account.owner,
organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName, organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
groupName: this.props.match?.params.groupName, groupName: this.props.match?.params.groupName,
groupId: "", groupId: undefined,
treeData: [], treeData: [],
selectedKeys: [], selectedKeys: [this.props.match?.params.groupName],
}; };
} }
@ -52,9 +53,9 @@ class GroupTreePage extends React.Component {
getTreeData() { getTreeData() {
GroupBackend.getGroups(this.state.organizationName, true).then((res) => { GroupBackend.getGroups(this.state.organizationName, true).then((res) => {
if (res.status === "ok") { if (res.status === "ok") {
const tree = res.data;
this.setState({ this.setState({
treeData: tree, treeData: res.data,
groupId: this.findNodeId({children: res.data}, this.state.groupName),
}); });
} else { } else {
Setting.showMessage("error", res.msg); Setting.showMessage("error", res.msg);
@ -62,6 +63,21 @@ class GroupTreePage extends React.Component {
}); });
} }
findNodeId(node, targetName) {
if (node.key === targetName) {
return node.id;
}
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const result = this.findNodeId(node.children[i], targetName);
if (result) {
return result;
}
}
}
return null;
}
setTreeTitle(treeData) { setTreeTitle(treeData) {
const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0; const haveChildren = Array.isArray(treeData.children) && treeData.children.length > 0;
const isSelected = this.state.groupName === treeData.key; const isSelected = this.state.groupName === treeData.key;
@ -121,6 +137,7 @@ class GroupTreePage extends React.Component {
this.props.history.push(`/groups/${this.state.organizationName}/${treeData.key}`); this.props.history.push(`/groups/${this.state.organizationName}/${treeData.key}`);
}} }}
/> />
{!haveChildren &&
<DeleteOutlined <DeleteOutlined
style={{ style={{
visibility: "visible", visibility: "visible",
@ -155,6 +172,7 @@ class GroupTreePage extends React.Component {
}); });
}} }}
/> />
}
</React.Fragment> </React.Fragment>
)} )}
</Space>, </Space>,
@ -185,7 +203,7 @@ class GroupTreePage extends React.Component {
groupName: info.node.key, groupName: info.node.key,
groupId: info.node.id, groupId: info.node.id,
}); });
this.props.history.push(`/group-tree/${this.state.organizationName}/${info.node.key}`); this.props.history.push(`/trees/${this.state.organizationName}/${info.node.key}`);
}; };
const onExpand = (expandedKeysValue) => { const onExpand = (expandedKeysValue) => {
this.setState({ this.setState({
@ -203,6 +221,7 @@ class GroupTreePage extends React.Component {
blockNode={true} blockNode={true}
defaultSelectedKeys={[this.state.groupName]} defaultSelectedKeys={[this.state.groupName]}
defaultExpandAll={true} defaultExpandAll={true}
selectedKeys={this.state.selectedKeys}
expandedKeys={this.state.expandedKeys} expandedKeys={this.state.expandedKeys}
onSelect={onSelect} onSelect={onSelect}
onExpand={onExpand} onExpand={onExpand}
@ -213,16 +232,20 @@ class GroupTreePage extends React.Component {
} }
renderOrganizationSelect() { renderOrganizationSelect() {
return <OrganizationSelect if (Setting.isAdminUser(this.props.account)) {
return (
<OrganizationSelect
initValue={this.state.organizationName} initValue={this.state.organizationName}
style={{width: "100%"}} style={{width: "100%"}}
onChange={(value) => { onChange={(value) => {
this.setState({ this.setState({
organizationName: value, organizationName: value,
}); });
this.props.history.push(`/group-tree/${value}`); this.props.history.push(`/trees/${value}`);
}} }}
/>; />
);
}
} }
newGroup(isRoot) { newGroup(isRoot) {
@ -234,7 +257,7 @@ class GroupTreePage extends React.Component {
updatedTime: moment().format(), updatedTime: moment().format(),
displayName: `New Group - ${randomName}`, displayName: `New Group - ${randomName}`,
type: "Virtual", type: "Virtual",
parentGroupId: isRoot ? this.state.organizationName : this.state.groupId, parentId: isRoot ? this.state.organizationName : this.state.groupId,
isTopGroup: isRoot, isTopGroup: isRoot,
isEnabled: true, isEnabled: true,
}; };
@ -267,25 +290,25 @@ class GroupTreePage extends React.Component {
<Row> <Row>
<Col span={5}> <Col span={5}>
<Row> <Row>
<Col span={24} style={{textAlign: "left"}}> <Col span={24} style={{textAlign: "center"}}>
{this.renderOrganizationSelect()} {this.renderOrganizationSelect()}
</Col> </Col>
</Row> </Row>
<Row> <Row>
<Col span={24} style={{marginTop: "10px", textAlign: "left"}}> <Col span={24} style={{marginTop: "10px"}}>
<Button <Button size={"small"}
onClick={() => { onClick={() => {
this.setState({ this.setState({
selectedKeys: [],
groupName: null, groupName: null,
groupId: null, groupId: undefined,
}); });
this.props.history.push(`/group-tree/${this.state.organizationName}`); this.props.history.push(`/trees/${this.state.organizationName}`);
}}> }}
{i18next.t("group:Show organization users")}
</Button>
<Button size={"small"} type={"primary"} style={{marginLeft: "10px"}}
onClick={() => this.addGroup(true)}
> >
{i18next.t("group:Show all")}
</Button>
<Button size={"small"} type={"primary"} style={{marginLeft: "10px"}} onClick={() => this.addGroup(true)}>
{i18next.t("general:Add")} {i18next.t("general:Add")}
</Button> </Button>
</Col> </Col>
@ -301,7 +324,8 @@ class GroupTreePage extends React.Component {
organizationName={this.state.organizationName} organizationName={this.state.organizationName}
groupName={this.state.groupName} groupName={this.state.groupName}
groupId={this.state.groupId} groupId={this.state.groupId}
{...this.props} /> {...this.props}
/>
</Col> </Col>
</Row> </Row>
</div> </div>

View File

@ -49,7 +49,9 @@ class OrganizationEditPage extends React.Component {
getOrganization() { getOrganization() {
OrganizationBackend.getOrganization("admin", this.state.organizationName) OrganizationBackend.getOrganization("admin", this.state.organizationName)
.then((organization) => { .then((res) => {
if (res.status === "ok") {
const organization = res.data;
if (organization === null) { if (organization === null) {
this.props.history.push("/404"); this.props.history.push("/404");
return; return;
@ -58,6 +60,9 @@ class OrganizationEditPage extends React.Component {
this.setState({ this.setState({
organization: organization, organization: organization,
}); });
} else {
Setting.showMessage("error", res.msg);
}
}); });
} }

View File

@ -228,7 +228,7 @@ class OrganizationListPage extends BaseListPage {
render: (text, record, index) => { render: (text, record, index) => {
return ( return (
<div> <div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/group-tree/${record.name}`)}>{i18next.t("general:Groups")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/trees/${record.name}`)}>{i18next.t("general:Groups")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/organizations/${record.name}/users`)}>{i18next.t("general:Users")}</Button>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button> <Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/organizations/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal <PopconfirmModal
@ -256,7 +256,7 @@ class OrganizationListPage extends BaseListPage {
title={() => ( title={() => (
<div> <div>
{i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp; {i18next.t("general:Organizations")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={this.addOrganization.bind(this)}>{i18next.t("general:Add")}</Button> <Button type="primary" size="small" disabled={!Setting.isAdminUser(this.props.account)} onClick={this.addOrganization.bind(this)}>{i18next.t("general:Add")}</Button>
</div> </div>
)} )}
loading={this.state.loading} loading={this.state.loading}

View File

@ -14,7 +14,7 @@
import React from "react"; import React from "react";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Button, Switch, Table, Upload} from "antd"; import {Button, Space, Switch, Table, Upload} from "antd";
import {UploadOutlined} from "@ant-design/icons"; import {UploadOutlined} from "@ant-design/icons";
import moment from "moment"; import moment from "moment";
import * as OrganizationBackend from "./backend/OrganizationBackend"; import * as OrganizationBackend from "./backend/OrganizationBackend";
@ -75,7 +75,7 @@ class UserListPage extends BaseListPage {
phone: Setting.getRandomNumber(), phone: Setting.getRandomNumber(),
countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "", countryCode: this.state.organization.countryCodes?.length > 0 ? this.state.organization.countryCodes[0] : "",
address: [], address: [],
groups: this.props.groupId !== undefined ? [this.props.groupId] : [], groups: this.props.groupId ? [this.props.groupId] : [],
affiliation: "Example Inc.", affiliation: "Example Inc.",
tag: "staff", tag: "staff",
region: "", region: "",
@ -124,6 +124,26 @@ class UserListPage extends BaseListPage {
}); });
} }
removeUserFromGroup(i) {
const user = this.state.data[i];
const group = this.props.groupId;
UserBackend.removeUserFromGroup({groupId: group, owner: user.owner, name: user.name})
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully removed"));
this.setState({
data: Setting.deleteRow(this.state.data, i),
pagination: {total: this.state.pagination.total - 1},
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to remove")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
uploadFile(info) { uploadFile(info) {
const {status, response: res} = info.file; const {status, response: res} = info.file;
if (status === "done") { if (status === "done") {
@ -142,10 +162,14 @@ class UserListPage extends BaseListPage {
getOrganization(organizationName) { getOrganization(organizationName) {
OrganizationBackend.getOrganization("admin", organizationName) OrganizationBackend.getOrganization("admin", organizationName)
.then((organization) => { .then((res) => {
if (res.status === "ok") {
this.setState({ this.setState({
organization: organization, organization: res.data,
}); });
} else {
Setting.showMessage("error", `Failed to get organization: ${res.msg}`);
}
}); });
} }
@ -372,20 +396,30 @@ class UserListPage extends BaseListPage {
width: "190px", width: "190px",
fixed: (Setting.isMobile()) ? "false" : "right", fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => { render: (text, record, index) => {
const isTreePage = this.props.groupId !== undefined;
const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin"); const disabled = (record.owner === this.props.account.owner && record.name === this.props.account.name) || (record.owner === "built-in" && record.name === "admin");
return ( return (
<div> <Space>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => { <Button size={isTreePage ? "small" : "middle"} type="primary" onClick={() => {
sessionStorage.setItem("userListUrl", window.location.pathname); sessionStorage.setItem("userListUrl", window.location.pathname);
this.props.history.push(`/users/${record.owner}/${record.name}`); this.props.history.push(`/users/${record.owner}/${record.name}`);
}}>{i18next.t("general:Edit")}</Button> }}>{i18next.t("general:Edit")}
</Button>
{isTreePage ?
<PopconfirmModal
text={i18next.t("general:remove")}
title={i18next.t("general:Sure to remove") + `: ${record.name} ?`}
onConfirm={() => this.removeUserFromGroup(index)}
disabled={disabled}
size="small"
/> : null}
<PopconfirmModal <PopconfirmModal
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteUser(index)} onConfirm={() => this.deleteUser(index)}
disabled={disabled} disabled={disabled}
> size={isTreePage ? "small" : "default"}
</PopconfirmModal> />
</div> </Space>
); );
}, },
}, },

View File

@ -222,3 +222,18 @@ export function checkUserPassword(values) {
body: JSON.stringify(values), body: JSON.stringify(values),
}).then(res => res.json()); }).then(res => res.json());
} }
export function removeUserFromGroup({owner, name, groupId}) {
const formData = new FormData();
formData.append("owner", owner);
formData.append("name", name);
formData.append("groupId", groupId);
return fetch(`${Setting.ServerUrl}/api/remove-user-from-group`, {
method: "POST",
credentials: "include",
body: formData,
headers: {
"Accept-Language": Setting.getAcceptLanguage(),
},
}).then(res => res.json());
}

View File

@ -17,6 +17,8 @@ import i18next from "i18next";
import React from "react"; import React from "react";
export const PopconfirmModal = (props) => { export const PopconfirmModal = (props) => {
const text = props.text ? props.text : i18next.t("general:Delete");
const size = props.size ? props.size : "middle";
return ( return (
<Popconfirm <Popconfirm
title={props.title} title={props.title}
@ -25,7 +27,7 @@ export const PopconfirmModal = (props) => {
okText={i18next.t("general:OK")} okText={i18next.t("general:OK")}
cancelText={i18next.t("general:Cancel")} cancelText={i18next.t("general:Cancel")}
> >
<Button style={{marginBottom: "10px"}} disabled={props.disabled} type="primary" danger>{i18next.t("general:Delete")}</Button> <Button style={{...props.style}} size={size} disabled={props.disabled} type="primary" danger>{text}</Button>
</Popconfirm> </Popconfirm>
); );
}; };

View File

@ -188,6 +188,7 @@
"Close": "Schließen", "Close": "Schließen",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Erstellte Zeit", "Created time": "Erstellte Zeit",
"Custom": "Custom",
"Default application": "Standard Anwendung", "Default application": "Standard Anwendung",
"Default application - Tooltip": "Standard-Anwendung für Benutzer, die direkt von der Organisationsseite registriert wurden", "Default application - Tooltip": "Standard-Anwendung für Benutzer, die direkt von der Organisationsseite registriert wurden",
"Default avatar": "Standard-Avatar", "Default avatar": "Standard-Avatar",
@ -209,6 +210,7 @@
"Failed to delete": "Konnte nicht gelöscht werden", "Failed to delete": "Konnte nicht gelöscht werden",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Konnte nicht gespeichert werden", "Failed to save": "Konnte nicht gespeichert werden",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Abonnements", "Subscriptions": "Abonnements",
"Successfully added": "Erfolgreich hinzugefügt", "Successfully added": "Erfolgreich hinzugefügt",
"Successfully deleted": "Erfolgreich gelöscht", "Successfully deleted": "Erfolgreich gelöscht",
"Successfully removed": "Successfully removed",
"Successfully saved": "Erfolgreich gespeichert", "Successfully saved": "Erfolgreich gespeichert",
"Supported country codes": "Unterstützte Ländercodes", "Supported country codes": "Unterstützte Ländercodes",
"Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden", "Supported country codes - Tooltip": "Ländercodes, die von der Organisation unterstützt werden. Diese Codes können als Präfix ausgewählt werden, wenn SMS-Verifizierungscodes gesendet werden",
"Sure to delete": "Sicher zu löschen", "Sure to delete": "Sicher zu löschen",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Synchronisieren", "Sync": "Synchronisieren",
"Syncers": "Syncers", "Syncers": "Syncers",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "leere", "empty": "leere",
"remove": "remove",
"{total} in total": "Insgesamt {total}" "{total} in total": "Insgesamt {total}"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Close", "Close": "Close",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Created time", "Created time": "Created time",
"Custom": "Custom",
"Default application": "Default application", "Default application": "Default application",
"Default application - Tooltip": "Default application for users registered directly from the organization page", "Default application - Tooltip": "Default application for users registered directly from the organization page",
"Default avatar": "Default avatar", "Default avatar": "Default avatar",
@ -209,6 +210,7 @@
"Failed to delete": "Failed to delete", "Failed to delete": "Failed to delete",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Failed to save", "Failed to save": "Failed to save",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Subscriptions", "Subscriptions": "Subscriptions",
"Successfully added": "Successfully added", "Successfully added": "Successfully added",
"Successfully deleted": "Successfully deleted", "Successfully deleted": "Successfully deleted",
"Successfully removed": "Successfully removed",
"Successfully saved": "Successfully saved", "Successfully saved": "Successfully saved",
"Supported country codes": "Supported country codes", "Supported country codes": "Supported country codes",
"Supported country codes - Tooltip": "Country codes supported by the organization. These codes can be selected as a prefix when sending SMS verification codes", "Supported country codes - Tooltip": "Country codes supported by the organization. These codes can be selected as a prefix when sending SMS verification codes",
"Sure to delete": "Sure to delete", "Sure to delete": "Sure to delete",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sync", "Sync": "Sync",
"Syncers": "Syncers", "Syncers": "Syncers",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "empty", "empty": "empty",
"remove": "remove",
"{total} in total": "{total} in total" "{total} in total": "{total} in total"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Cerca", "Close": "Cerca",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Tiempo creado", "Created time": "Tiempo creado",
"Custom": "Custom",
"Default application": "Aplicación predeterminada", "Default application": "Aplicación predeterminada",
"Default application - Tooltip": "Aplicación predeterminada para usuarios registrados directamente desde la página de la organización", "Default application - Tooltip": "Aplicación predeterminada para usuarios registrados directamente desde la página de la organización",
"Default avatar": "Avatar predeterminado", "Default avatar": "Avatar predeterminado",
@ -209,6 +210,7 @@
"Failed to delete": "No se pudo eliminar", "Failed to delete": "No se pudo eliminar",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "No se pudo guardar", "Failed to save": "No se pudo guardar",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Suscripciones", "Subscriptions": "Suscripciones",
"Successfully added": "Éxito al agregar", "Successfully added": "Éxito al agregar",
"Successfully deleted": "Éxito en la eliminación", "Successfully deleted": "Éxito en la eliminación",
"Successfully removed": "Successfully removed",
"Successfully saved": "Guardado exitosamente", "Successfully saved": "Guardado exitosamente",
"Supported country codes": "Códigos de país admitidos", "Supported country codes": "Códigos de país admitidos",
"Supported country codes - Tooltip": "Códigos de país compatibles con la organización. Estos códigos se pueden seleccionar como prefijo al enviar códigos de verificación SMS", "Supported country codes - Tooltip": "Códigos de país compatibles con la organización. Estos códigos se pueden seleccionar como prefijo al enviar códigos de verificación SMS",
"Sure to delete": "Seguro que eliminar", "Sure to delete": "Seguro que eliminar",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sincronización", "Sync": "Sincronización",
"Syncers": "Sincronizadores", "Syncers": "Sincronizadores",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "vacío", "empty": "vacío",
"remove": "remove",
"{total} in total": "{total} en total" "{total} in total": "{total} en total"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Fermer", "Close": "Fermer",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Temps créé", "Created time": "Temps créé",
"Custom": "Custom",
"Default application": "Application par défaut", "Default application": "Application par défaut",
"Default application - Tooltip": "Application par défaut pour les utilisateurs enregistrés directement depuis la page de l'organisation", "Default application - Tooltip": "Application par défaut pour les utilisateurs enregistrés directement depuis la page de l'organisation",
"Default avatar": "Avatar par défaut", "Default avatar": "Avatar par défaut",
@ -209,6 +210,7 @@
"Failed to delete": "Échec de la suppression", "Failed to delete": "Échec de la suppression",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Échec de sauvegarde", "Failed to save": "Échec de sauvegarde",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Abonnements", "Subscriptions": "Abonnements",
"Successfully added": "Ajouté avec succès", "Successfully added": "Ajouté avec succès",
"Successfully deleted": "Supprimé avec succès", "Successfully deleted": "Supprimé avec succès",
"Successfully removed": "Successfully removed",
"Successfully saved": "Succès enregistré", "Successfully saved": "Succès enregistré",
"Supported country codes": "Codes de pays pris en charge", "Supported country codes": "Codes de pays pris en charge",
"Supported country codes - Tooltip": "Codes de pays pris en charge par l'organisation. Ces codes peuvent être sélectionnés comme préfixe lors de l'envoi de codes de vérification SMS", "Supported country codes - Tooltip": "Codes de pays pris en charge par l'organisation. Ces codes peuvent être sélectionnés comme préfixe lors de l'envoi de codes de vérification SMS",
"Sure to delete": "Sûr de supprimer", "Sure to delete": "Sûr de supprimer",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Synchronisation", "Sync": "Synchronisation",
"Syncers": "Synchroniseurs", "Syncers": "Synchroniseurs",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "vide", "empty": "vide",
"remove": "remove",
"{total} in total": "{total} au total" "{total} in total": "{total} au total"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Tutup", "Close": "Tutup",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Waktu dibuat", "Created time": "Waktu dibuat",
"Custom": "Custom",
"Default application": "Aplikasi default", "Default application": "Aplikasi default",
"Default application - Tooltip": "Aplikasi default untuk pengguna yang terdaftar langsung dari halaman organisasi", "Default application - Tooltip": "Aplikasi default untuk pengguna yang terdaftar langsung dari halaman organisasi",
"Default avatar": "Avatar default", "Default avatar": "Avatar default",
@ -209,6 +210,7 @@
"Failed to delete": "Gagal menghapus", "Failed to delete": "Gagal menghapus",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Gagal menyimpan", "Failed to save": "Gagal menyimpan",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Langganan", "Subscriptions": "Langganan",
"Successfully added": "Berhasil ditambahkan", "Successfully added": "Berhasil ditambahkan",
"Successfully deleted": "Berhasil dihapus", "Successfully deleted": "Berhasil dihapus",
"Successfully removed": "Successfully removed",
"Successfully saved": "Berhasil disimpan", "Successfully saved": "Berhasil disimpan",
"Supported country codes": "Kode negara yang didukung", "Supported country codes": "Kode negara yang didukung",
"Supported country codes - Tooltip": "Kode negara yang didukung oleh organisasi. Kode-kode ini dapat dipilih sebagai awalan saat mengirim kode verifikasi SMS", "Supported country codes - Tooltip": "Kode negara yang didukung oleh organisasi. Kode-kode ini dapat dipilih sebagai awalan saat mengirim kode verifikasi SMS",
"Sure to delete": "Pasti untuk menghapus", "Sure to delete": "Pasti untuk menghapus",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sinkronisasi", "Sync": "Sinkronisasi",
"Syncers": "Sinkronisasi", "Syncers": "Sinkronisasi",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "kosong", "empty": "kosong",
"remove": "remove",
"{total} in total": "{total} secara keseluruhan" "{total} in total": "{total} secara keseluruhan"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "閉じる", "Close": "閉じる",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "作成された時間", "Created time": "作成された時間",
"Custom": "Custom",
"Default application": "デフォルトアプリケーション", "Default application": "デフォルトアプリケーション",
"Default application - Tooltip": "組織ページから直接登録されたユーザーのデフォルトアプリケーション", "Default application - Tooltip": "組織ページから直接登録されたユーザーのデフォルトアプリケーション",
"Default avatar": "デフォルトのアバター", "Default avatar": "デフォルトのアバター",
@ -209,6 +210,7 @@
"Failed to delete": "削除に失敗しました", "Failed to delete": "削除に失敗しました",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "保存に失敗しました", "Failed to save": "保存に失敗しました",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "ファビコン", "Favicon": "ファビコン",
@ -301,10 +303,12 @@
"Subscriptions": "サブスクリプション", "Subscriptions": "サブスクリプション",
"Successfully added": "正常に追加されました", "Successfully added": "正常に追加されました",
"Successfully deleted": "正常に削除されました", "Successfully deleted": "正常に削除されました",
"Successfully removed": "Successfully removed",
"Successfully saved": "成功的に保存されました", "Successfully saved": "成功的に保存されました",
"Supported country codes": "サポートされている国コード", "Supported country codes": "サポートされている国コード",
"Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます", "Supported country codes - Tooltip": "組織でサポートされている国コード。これらのコードは、SMS認証コードのプレフィックスとして選択できます",
"Sure to delete": "削除することが確実です", "Sure to delete": "削除することが確実です",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "同期", "Sync": "同期",
"Syncers": "シンカーズ", "Syncers": "シンカーズ",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "空", "empty": "空",
"remove": "remove",
"{total} in total": "総計{total}" "{total} in total": "総計{total}"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "닫다", "Close": "닫다",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "작성한 시간", "Created time": "작성한 시간",
"Custom": "Custom",
"Default application": "기본 애플리케이션", "Default application": "기본 애플리케이션",
"Default application - Tooltip": "조직 페이지에서 직접 등록한 사용자의 기본 응용 프로그램", "Default application - Tooltip": "조직 페이지에서 직접 등록한 사용자의 기본 응용 프로그램",
"Default avatar": "기본 아바타", "Default avatar": "기본 아바타",
@ -209,6 +210,7 @@
"Failed to delete": "삭제에 실패했습니다", "Failed to delete": "삭제에 실패했습니다",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "저장에 실패했습니다", "Failed to save": "저장에 실패했습니다",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "파비콘", "Favicon": "파비콘",
@ -301,10 +303,12 @@
"Subscriptions": "구독", "Subscriptions": "구독",
"Successfully added": "성공적으로 추가되었습니다", "Successfully added": "성공적으로 추가되었습니다",
"Successfully deleted": "성공적으로 삭제되었습니다", "Successfully deleted": "성공적으로 삭제되었습니다",
"Successfully removed": "Successfully removed",
"Successfully saved": "성공적으로 저장되었습니다", "Successfully saved": "성공적으로 저장되었습니다",
"Supported country codes": "지원되는 국가 코드들", "Supported country codes": "지원되는 국가 코드들",
"Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다", "Supported country codes - Tooltip": "조직에서 지원하는 국가 코드입니다. 이 코드들은 SMS 인증 코드를 보낼 때 접두사로 선택할 수 있습니다",
"Sure to delete": "삭제하시겠습니까?", "Sure to delete": "삭제하시겠습니까?",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "싱크", "Sync": "싱크",
"Syncers": "싱크어스", "Syncers": "싱크어스",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "빈", "empty": "빈",
"remove": "remove",
"{total} in total": "총 {total}개" "{total} in total": "총 {total}개"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Fechar", "Close": "Fechar",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Hora de Criação", "Created time": "Hora de Criação",
"Custom": "Custom",
"Default application": "Aplicação padrão", "Default application": "Aplicação padrão",
"Default application - Tooltip": "Aplicação padrão para usuários registrados diretamente na página da organização", "Default application - Tooltip": "Aplicação padrão para usuários registrados diretamente na página da organização",
"Default avatar": "Avatar padrão", "Default avatar": "Avatar padrão",
@ -209,6 +210,7 @@
"Failed to delete": "Falha ao excluir", "Failed to delete": "Falha ao excluir",
"Failed to enable": "Falha ao habilitar", "Failed to enable": "Falha ao habilitar",
"Failed to get answer": "Falha ao obter resposta", "Failed to get answer": "Falha ao obter resposta",
"Failed to remove": "Failed to remove",
"Failed to save": "Falha ao salvar", "Failed to save": "Falha ao salvar",
"Failed to verify": "Falha ao verificar", "Failed to verify": "Falha ao verificar",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Đăng ký", "Subscriptions": "Đăng ký",
"Successfully added": "Adicionado com sucesso", "Successfully added": "Adicionado com sucesso",
"Successfully deleted": "Excluído com sucesso", "Successfully deleted": "Excluído com sucesso",
"Successfully removed": "Successfully removed",
"Successfully saved": "Salvo com sucesso", "Successfully saved": "Salvo com sucesso",
"Supported country codes": "Códigos de país suportados", "Supported country codes": "Códigos de país suportados",
"Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS", "Supported country codes - Tooltip": "Códigos de país suportados pela organização. Esses códigos podem ser selecionados como prefixo ao enviar códigos de verificação SMS",
"Sure to delete": "Tem certeza que deseja excluir", "Sure to delete": "Tem certeza que deseja excluir",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"Syncers": "Sincronizadores", "Syncers": "Sincronizadores",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "vazio", "empty": "vazio",
"remove": "remove",
"{total} in total": "{total} no total" "{total} in total": "{total} no total"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Близко", "Close": "Близко",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Созданное время", "Created time": "Созданное время",
"Custom": "Custom",
"Default application": "Приложение по умолчанию", "Default application": "Приложение по умолчанию",
"Default application - Tooltip": "По умолчанию приложение для пользователей, зарегистрированных непосредственно со страницы организации", "Default application - Tooltip": "По умолчанию приложение для пользователей, зарегистрированных непосредственно со страницы организации",
"Default avatar": "Стандартный аватар", "Default avatar": "Стандартный аватар",
@ -209,6 +210,7 @@
"Failed to delete": "Не удалось удалить", "Failed to delete": "Не удалось удалить",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Не удалось сохранить", "Failed to save": "Не удалось сохранить",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Фавикон", "Favicon": "Фавикон",
@ -301,10 +303,12 @@
"Subscriptions": "Подписки", "Subscriptions": "Подписки",
"Successfully added": "Успешно добавлено", "Successfully added": "Успешно добавлено",
"Successfully deleted": "Успешно удалено", "Successfully deleted": "Успешно удалено",
"Successfully removed": "Successfully removed",
"Successfully saved": "Успешно сохранено", "Successfully saved": "Успешно сохранено",
"Supported country codes": "Поддерживаемые коды стран", "Supported country codes": "Поддерживаемые коды стран",
"Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения", "Supported country codes - Tooltip": "Коды стран, поддерживаемые организацией. Эти коды могут быть выбраны в качестве префикса при отправке SMS-кодов подтверждения",
"Sure to delete": "Обязательное удаление", "Sure to delete": "Обязательное удаление",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Синхронизация", "Sync": "Синхронизация",
"Syncers": "Синкеры", "Syncers": "Синкеры",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "пустые", "empty": "пустые",
"remove": "remove",
"{total} in total": "{total} в общей сложности" "{total} in total": "{total} в общей сложности"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "Đóng lại", "Close": "Đóng lại",
"Confirm": "Confirm", "Confirm": "Confirm",
"Created time": "Thời gian tạo", "Created time": "Thời gian tạo",
"Custom": "Custom",
"Default application": "Ứng dụng mặc định", "Default application": "Ứng dụng mặc định",
"Default application - Tooltip": "Ứng dụng mặc định cho người dùng đăng ký trực tiếp từ trang tổ chức", "Default application - Tooltip": "Ứng dụng mặc định cho người dùng đăng ký trực tiếp từ trang tổ chức",
"Default avatar": "Hình đại diện mặc định", "Default avatar": "Hình đại diện mặc định",
@ -209,6 +210,7 @@
"Failed to delete": "Không thể xoá", "Failed to delete": "Không thể xoá",
"Failed to enable": "Failed to enable", "Failed to enable": "Failed to enable",
"Failed to get answer": "Failed to get answer", "Failed to get answer": "Failed to get answer",
"Failed to remove": "Failed to remove",
"Failed to save": "Không thể lưu được", "Failed to save": "Không thể lưu được",
"Failed to verify": "Failed to verify", "Failed to verify": "Failed to verify",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "Đăng ký", "Subscriptions": "Đăng ký",
"Successfully added": "Đã thêm thành công", "Successfully added": "Đã thêm thành công",
"Successfully deleted": "Đã xóa thành công", "Successfully deleted": "Đã xóa thành công",
"Successfully removed": "Successfully removed",
"Successfully saved": "Thành công đã được lưu lại", "Successfully saved": "Thành công đã được lưu lại",
"Supported country codes": "Các mã quốc gia được hỗ trợ", "Supported country codes": "Các mã quốc gia được hỗ trợ",
"Supported country codes - Tooltip": "Mã quốc gia được hỗ trợ bởi tổ chức. Những mã này có thể được chọn làm tiền tố khi gửi mã xác nhận SMS", "Supported country codes - Tooltip": "Mã quốc gia được hỗ trợ bởi tổ chức. Những mã này có thể được chọn làm tiền tố khi gửi mã xác nhận SMS",
"Sure to delete": "Chắc chắn muốn xóa", "Sure to delete": "Chắc chắn muốn xóa",
"Sure to remove": "Sure to remove",
"Swagger": "Swagger", "Swagger": "Swagger",
"Sync": "Đồng bộ hoá", "Sync": "Đồng bộ hoá",
"Syncers": "Đồng bộ hóa", "Syncers": "Đồng bộ hóa",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "You can only select one physical group", "You can only select one physical group": "You can only select one physical group",
"empty": "trống", "empty": "trống",
"remove": "remove",
"{total} in total": "Trong tổng số {total}" "{total} in total": "Trong tổng số {total}"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "Parent group", "Parent group": "Parent group",
"Parent group - Tooltip": "Parent group - Tooltip", "Parent group - Tooltip": "Parent group - Tooltip",
"Physical": "Physical", "Physical": "Physical",
"Show all": "Show all",
"Virtual": "Virtual" "Virtual": "Virtual"
}, },
"ldap": { "ldap": {

View File

@ -188,6 +188,7 @@
"Close": "关闭", "Close": "关闭",
"Confirm": "确认", "Confirm": "确认",
"Created time": "创建时间", "Created time": "创建时间",
"Custom": "自定义",
"Default application": "默认应用", "Default application": "默认应用",
"Default application - Tooltip": "直接从组织页面注册的用户默认所属的应用", "Default application - Tooltip": "直接从组织页面注册的用户默认所属的应用",
"Default avatar": "默认头像", "Default avatar": "默认头像",
@ -209,6 +210,7 @@
"Failed to delete": "删除失败", "Failed to delete": "删除失败",
"Failed to enable": "启用失败", "Failed to enable": "启用失败",
"Failed to get answer": "获取回答失败", "Failed to get answer": "获取回答失败",
"Failed to remove": "移除失败",
"Failed to save": "保存失败", "Failed to save": "保存失败",
"Failed to verify": "验证失败", "Failed to verify": "验证失败",
"Favicon": "Favicon", "Favicon": "Favicon",
@ -301,10 +303,12 @@
"Subscriptions": "订阅", "Subscriptions": "订阅",
"Successfully added": "添加成功", "Successfully added": "添加成功",
"Successfully deleted": "删除成功", "Successfully deleted": "删除成功",
"Successfully removed": "移除成功",
"Successfully saved": "保存成功", "Successfully saved": "保存成功",
"Supported country codes": "支持的国家代码", "Supported country codes": "支持的国家代码",
"Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀", "Supported country codes - Tooltip": "该组织所支持的国家代码,发送短信验证码时可以选择这些国家的代码前缀",
"Sure to delete": "确定删除", "Sure to delete": "确定删除",
"Sure to remove": "确定移除",
"Swagger": "API文档", "Swagger": "API文档",
"Sync": "同步", "Sync": "同步",
"Syncers": "同步器", "Syncers": "同步器",
@ -329,6 +333,7 @@
"Webhooks": "Webhooks", "Webhooks": "Webhooks",
"You can only select one physical group": "只能选择一个实体组", "You can only select one physical group": "只能选择一个实体组",
"empty": "无", "empty": "无",
"remove": "移除",
"{total} in total": "{total} 总计" "{total} in total": "{total} 总计"
}, },
"group": { "group": {
@ -337,6 +342,7 @@
"Parent group": "上级组", "Parent group": "上级组",
"Parent group - Tooltip": "上级组", "Parent group - Tooltip": "上级组",
"Physical": "物理组", "Physical": "物理组",
"Show all": "显示全部",
"Virtual": "虚拟组" "Virtual": "虚拟组"
}, },
"ldap": { "ldap": {