mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Restore previously archived groups (#22597)
* add ability to restore groups from the user group modal * factory selector for groups to reduce number of renders across the app * react window and infinite scroll for user groups * adding archive groups to dropdown * restore user group from the view modal * component cleanup * lint * adding websocket for archiveGroup * updating tests * adding some tests and fixing types * lint * fixing broken test * fixing snapshot * fixing infinitescroll * lint * increasing max-height and updating snapshots * fixing PR comments * fixing case for button * snapshot and translation * fixing PR comments * tiding up repition and creating new hook * fixing tests * add additional parammeter for call to getGroups() * make sure popup is visible for all rows * update text for admin console * update css for lint * fix edge cases found in review * revert package-lock.json * revert adding query to GetGroupsParam * fixing lint * change include_archived to false in team_controller --------- Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MacBook-Pro.local> Co-authored-by: Scott Bishel <scott.bishel@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
32512d35fb
commit
2c6179a0a6
@ -986,14 +986,19 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
|
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
|
||||||
|
|
||||||
|
// Include archived groups
|
||||||
|
includeArchived := r.URL.Query().Get("include_archived") == "true"
|
||||||
|
|
||||||
opts := model.GroupSearchOpts{
|
opts := model.GroupSearchOpts{
|
||||||
Q: c.Params.Q,
|
Q: c.Params.Q,
|
||||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||||
FilterAllowReference: c.Params.FilterAllowReference,
|
FilterAllowReference: c.Params.FilterAllowReference,
|
||||||
|
FilterArchived: c.Params.FilterArchived,
|
||||||
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
|
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
|
||||||
Source: source,
|
Source: source,
|
||||||
FilterHasMember: c.Params.FilterHasMember,
|
FilterHasMember: c.Params.FilterHasMember,
|
||||||
IncludeTimezones: includeTimezones,
|
IncludeTimezones: includeTimezones,
|
||||||
|
IncludeArchived: includeArchived,
|
||||||
}
|
}
|
||||||
|
|
||||||
if teamID != "" {
|
if teamID != "" {
|
||||||
@ -1145,15 +1150,19 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
defer c.LogAuditRec(auditRec)
|
defer c.LogAuditRec(auditRec)
|
||||||
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
|
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
|
||||||
|
|
||||||
_, err = c.App.DeleteGroup(c.Params.GroupId)
|
group, err = c.App.DeleteGroup(c.Params.GroupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b, jsonErr := json.Marshal(group)
|
||||||
|
if jsonErr != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.deleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
|
w.Write(b)
|
||||||
ReturnStatusOK(w)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@ -1194,15 +1203,20 @@ func restoreGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
defer c.LogAuditRec(auditRec)
|
defer c.LogAuditRec(auditRec)
|
||||||
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
|
audit.AddEventParameter(auditRec, "group_id", c.Params.GroupId)
|
||||||
|
|
||||||
_, err = c.App.RestoreGroup(c.Params.GroupId)
|
restoredGroup, err := c.App.RestoreGroup(c.Params.GroupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
auditRec.Success()
|
b, jsonErr := json.Marshal(restoredGroup)
|
||||||
|
if jsonErr != nil {
|
||||||
|
c.Err = model.NewAppError("Api4.restoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(jsonErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ReturnStatusOK(w)
|
auditRec.Success()
|
||||||
|
w.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@ -1223,13 +1237,13 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if group.Source != model.GroupSourceCustom {
|
if group.Source != model.GroupSourceCustom {
|
||||||
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
|
c.Err = model.NewAppError("Api4.addGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
appErr.Where = "Api4.deleteGroup"
|
appErr.Where = "Api4.addGroupMembers"
|
||||||
c.Err = appErr
|
c.Err = appErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -1282,13 +1296,13 @@ func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if group.Source != model.GroupSourceCustom {
|
if group.Source != model.GroupSourceCustom {
|
||||||
c.Err = model.NewAppError("Api4.deleteGroup", "app.group.crud_permission", nil, "", http.StatusBadRequest)
|
c.Err = model.NewAppError("Api4.deleteGroupMembers", "app.group.crud_permission", nil, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
appErr.Where = "Api4.deleteGroup"
|
appErr.Where = "Api4.deleteGroupMembers"
|
||||||
c.Err = appErr
|
c.Err = appErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1291,6 +1291,21 @@ func TestGetGroups(t *testing.T) {
|
|||||||
// make sure it returned th.Group,not group
|
// make sure it returned th.Group,not group
|
||||||
assert.Equal(t, groups[0].Id, th.Group.Id)
|
assert.Equal(t, groups[0].Id, th.Group.Id)
|
||||||
|
|
||||||
|
// Test include_archived parameter
|
||||||
|
opts.IncludeArchived = true
|
||||||
|
groups, _, err = th.Client.GetGroups(context.Background(), opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, groups, 2)
|
||||||
|
opts.IncludeArchived = false
|
||||||
|
|
||||||
|
// Test returning only archived groups
|
||||||
|
opts.FilterArchived = true
|
||||||
|
groups, _, err = th.Client.GetGroups(context.Background(), opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, groups, 1)
|
||||||
|
assert.Equal(t, groups[0].Id, group.Id)
|
||||||
|
opts.FilterArchived = false
|
||||||
|
|
||||||
opts.Source = model.GroupSourceCustom
|
opts.Source = model.GroupSourceCustom
|
||||||
groups, _, err = th.Client.GetGroups(context.Background(), opts)
|
groups, _, err = th.Client.GetGroups(context.Background(), opts)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -213,6 +213,22 @@ func (a *App) DeleteGroup(groupID string) (*model.Group, *model.AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
count, err := a.Srv().Store().Group().GetMemberCount(groupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("DeleteGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedGroup.MemberCount = model.NewInt(int(count))
|
||||||
|
|
||||||
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
|
||||||
|
|
||||||
|
groupJSON, err := json.Marshal(deletedGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("DeleteGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
messageWs.Add("group", string(groupJSON))
|
||||||
|
a.Publish(messageWs)
|
||||||
|
|
||||||
return deletedGroup, nil
|
return deletedGroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,6 +244,22 @@ func (a *App) RestoreGroup(groupID string) (*model.Group, *model.AppError) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
count, err := a.Srv().Store().Group().GetMemberCount(groupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("RestoreGroup", "app.group.id.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredGroup.MemberCount = model.NewInt(int(count))
|
||||||
|
|
||||||
|
messageWs := model.NewWebSocketEvent(model.WebsocketEventReceivedGroup, "", "", "", nil, "")
|
||||||
|
|
||||||
|
groupJSON, err := json.Marshal(restoredGroup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, model.NewAppError("RestoreGroup", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
messageWs.Add("group", string(groupJSON))
|
||||||
|
a.Publish(messageWs)
|
||||||
|
|
||||||
return restoredGroup, nil
|
return restoredGroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,9 +384,11 @@ func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
time := model.GetMillis()
|
time := model.GetMillis()
|
||||||
|
group.DeleteAt = time
|
||||||
|
group.UpdateAt = time
|
||||||
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
||||||
SET DeleteAt=?, UpdateAt=?
|
SET DeleteAt=?, UpdateAt=?
|
||||||
WHERE Id=? AND DeleteAt=0`, time, time, groupID); err != nil {
|
WHERE Id=? AND DeleteAt=0`, group.DeleteAt, group.UpdateAt, groupID); err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
|
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -410,10 +412,11 @@ func (s *SqlGroupStore) Restore(groupID string) (*model.Group, error) {
|
|||||||
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
|
return nil, errors.Wrapf(err, "failed to get Group with id=%s", groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
time := model.GetMillis()
|
group.UpdateAt = model.GetMillis()
|
||||||
|
group.DeleteAt = 0
|
||||||
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
||||||
SET DeleteAt=0, UpdateAt=?
|
SET DeleteAt=0, UpdateAt=?
|
||||||
WHERE Id=? AND DeleteAt!=0`, time, groupID); err != nil {
|
WHERE Id=? AND DeleteAt!=0`, group.UpdateAt, groupID); err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
|
return nil, errors.Wrapf(err, "failed to update Group with id=%s", groupID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1570,17 +1573,27 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts,
|
|||||||
}
|
}
|
||||||
|
|
||||||
groupsQuery = groupsQuery.
|
groupsQuery = groupsQuery.
|
||||||
From("UserGroups g").
|
From("UserGroups g")
|
||||||
OrderBy("g.DisplayName")
|
|
||||||
|
|
||||||
if opts.Since > 0 {
|
if opts.Since > 0 {
|
||||||
groupsQuery = groupsQuery.Where(sq.Gt{
|
groupsQuery = groupsQuery.Where(sq.Gt{
|
||||||
"g.UpdateAt": opts.Since,
|
"g.UpdateAt": opts.Since,
|
||||||
})
|
})
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if opts.FilterArchived {
|
||||||
|
groupsQuery = groupsQuery.Where("g.DeleteAt > 0")
|
||||||
|
} else if !opts.IncludeArchived && opts.Since <= 0 {
|
||||||
|
// Mobile needs to return archived groups when the since parameter is set, will need to keep this for backwards compatibility
|
||||||
groupsQuery = groupsQuery.Where("g.DeleteAt = 0")
|
groupsQuery = groupsQuery.Where("g.DeleteAt = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.IncludeArchived {
|
||||||
|
groupsQuery = groupsQuery.OrderBy("CASE WHEN g.DeleteAt = 0 THEN g.DisplayName end, CASE WHEN g.DeleteAt != 0 THEN g.DisplayName END")
|
||||||
|
} else {
|
||||||
|
groupsQuery = groupsQuery.OrderBy("g.DisplayName")
|
||||||
|
}
|
||||||
|
|
||||||
if perPage != 0 {
|
if perPage != 0 {
|
||||||
groupsQuery = groupsQuery.
|
groupsQuery = groupsQuery.
|
||||||
Limit(uint64(perPage)).
|
Limit(uint64(perPage)).
|
||||||
|
@ -3962,6 +3962,26 @@ func testGetGroups(t *testing.T, ss store.Store) {
|
|||||||
},
|
},
|
||||||
Restrictions: nil,
|
Restrictions: nil,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "Include archived groups",
|
||||||
|
Opts: model.GroupSearchOpts{IncludeArchived: true, Q: "group-deleted"},
|
||||||
|
Page: 0,
|
||||||
|
PerPage: 1,
|
||||||
|
Resultf: func(groups []*model.Group) bool {
|
||||||
|
return len(groups) == 1
|
||||||
|
},
|
||||||
|
Restrictions: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Only return archived groups",
|
||||||
|
Opts: model.GroupSearchOpts{FilterArchived: true, Q: "group-1"},
|
||||||
|
Page: 0,
|
||||||
|
PerPage: 1,
|
||||||
|
Resultf: func(groups []*model.Group) bool {
|
||||||
|
return len(groups) == 0
|
||||||
|
},
|
||||||
|
Restrictions: nil,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -83,6 +83,7 @@ type Params struct {
|
|||||||
IncludeTotalCount bool
|
IncludeTotalCount bool
|
||||||
IncludeDeleted bool
|
IncludeDeleted bool
|
||||||
FilterAllowReference bool
|
FilterAllowReference bool
|
||||||
|
FilterArchived bool
|
||||||
FilterParentTeamPermitted bool
|
FilterParentTeamPermitted bool
|
||||||
CategoryId string
|
CategoryId string
|
||||||
WarnMetricId string
|
WarnMetricId string
|
||||||
@ -208,6 +209,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
|||||||
params.NotAssociatedToTeam = query.Get("not_associated_to_team")
|
params.NotAssociatedToTeam = query.Get("not_associated_to_team")
|
||||||
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
|
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
|
||||||
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
|
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
|
||||||
|
params.FilterArchived, _ = strconv.ParseBool(query.Get("filter_archived"))
|
||||||
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
|
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
|
||||||
params.IncludeChannelMemberCount = query.Get("include_channel_member_count")
|
params.IncludeChannelMemberCount = query.Get("include_channel_member_count")
|
||||||
|
|
||||||
|
@ -5510,7 +5510,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(ctx context.Context, teamI
|
|||||||
// GetGroups retrieves Mattermost Groups
|
// GetGroups retrieves Mattermost Groups
|
||||||
func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group, *Response, error) {
|
func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group, *Response, error) {
|
||||||
path := fmt.Sprintf(
|
path := fmt.Sprintf(
|
||||||
"%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v",
|
"%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v&include_archived=%v&filter_archived=%v",
|
||||||
c.groupsRoute(),
|
c.groupsRoute(),
|
||||||
opts.IncludeMemberCount,
|
opts.IncludeMemberCount,
|
||||||
opts.NotAssociatedToTeam,
|
opts.NotAssociatedToTeam,
|
||||||
@ -5521,6 +5521,8 @@ func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group
|
|||||||
opts.Source,
|
opts.Source,
|
||||||
opts.IncludeChannelMemberCount,
|
opts.IncludeChannelMemberCount,
|
||||||
opts.IncludeTimezones,
|
opts.IncludeTimezones,
|
||||||
|
opts.IncludeArchived,
|
||||||
|
opts.FilterArchived,
|
||||||
)
|
)
|
||||||
if opts.Since > 0 {
|
if opts.Since > 0 {
|
||||||
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
|
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
|
||||||
|
@ -133,6 +133,12 @@ type GroupSearchOpts struct {
|
|||||||
|
|
||||||
IncludeChannelMemberCount string
|
IncludeChannelMemberCount string
|
||||||
IncludeTimezones bool
|
IncludeTimezones bool
|
||||||
|
|
||||||
|
// Include archived groups
|
||||||
|
IncludeArchived bool
|
||||||
|
|
||||||
|
// Only return archived groups
|
||||||
|
FilterArchived bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetGroupOpts struct {
|
type GetGroupOpts struct {
|
||||||
|
@ -117,10 +117,13 @@ exports[`components/global/product_switcher_menu should match snapshot with id 1
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@ -312,10 +315,13 @@ exports[`components/global/product_switcher_menu should match snapshot with most
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@ -399,10 +405,13 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@ -428,10 +437,13 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
@ -470,10 +482,13 @@ exports[`components/global/product_switcher_menu should match userGroups snapsho
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
@ -624,10 +639,13 @@ exports[`components/global/product_switcher_menu should show integrations should
|
|||||||
}
|
}
|
||||||
dialogType={
|
dialogType={
|
||||||
Object {
|
Object {
|
||||||
|
"$$typeof": Symbol(react.forward_ref),
|
||||||
|
"WrappedComponent": Object {
|
||||||
"$$typeof": Symbol(react.memo),
|
"$$typeof": Symbol(react.memo),
|
||||||
"WrappedComponent": [Function],
|
|
||||||
"compare": null,
|
"compare": null,
|
||||||
"type": [Function],
|
"type": [Function],
|
||||||
|
},
|
||||||
|
"render": [Function],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
|
@ -37,6 +37,7 @@ const iconMap: {[key in NoResultsVariant]: React.ReactNode } = {
|
|||||||
[NoResultsVariant.ChannelFilesFiltered]: <i className='icon icon-file-text-outline no-results__icon'/>,
|
[NoResultsVariant.ChannelFilesFiltered]: <i className='icon icon-file-text-outline no-results__icon'/>,
|
||||||
[NoResultsVariant.UserGroups]: <i className='icon icon-account-multiple-outline no-results__icon'/>,
|
[NoResultsVariant.UserGroups]: <i className='icon icon-account-multiple-outline no-results__icon'/>,
|
||||||
[NoResultsVariant.UserGroupMembers]: <i className='icon icon-account-outline no-results__icon'/>,
|
[NoResultsVariant.UserGroupMembers]: <i className='icon icon-account-outline no-results__icon'/>,
|
||||||
|
[NoResultsVariant.UserGroupsArchived]: <i className='icon icon-account-multiple-outline no-results__icon'/>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
const titleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
||||||
@ -64,6 +65,9 @@ const titleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
|||||||
[NoResultsVariant.UserGroupMembers]: {
|
[NoResultsVariant.UserGroupMembers]: {
|
||||||
id: t('no_results.user_group_members.title'),
|
id: t('no_results.user_group_members.title'),
|
||||||
},
|
},
|
||||||
|
[NoResultsVariant.UserGroupsArchived]: {
|
||||||
|
id: t('no_results.user_groups.archived.title'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const subtitleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
const subtitleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
||||||
@ -91,6 +95,9 @@ const subtitleMap: {[key in NoResultsVariant]: MessageDescriptor} = {
|
|||||||
[NoResultsVariant.UserGroupMembers]: {
|
[NoResultsVariant.UserGroupMembers]: {
|
||||||
id: t('no_results.user_group_members.subtitle'),
|
id: t('no_results.user_group_members.subtitle'),
|
||||||
},
|
},
|
||||||
|
[NoResultsVariant.UserGroupsArchived]: {
|
||||||
|
id: t('no_results.user_groups.archived.subtitle'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
import './no_results_indicator.scss';
|
import './no_results_indicator.scss';
|
||||||
|
@ -9,6 +9,7 @@ export enum NoResultsVariant {
|
|||||||
ChannelFiles = 'ChannelFiles',
|
ChannelFiles = 'ChannelFiles',
|
||||||
ChannelFilesFiltered = 'ChannelFilesFiltered',
|
ChannelFilesFiltered = 'ChannelFilesFiltered',
|
||||||
UserGroups = 'UserGroups',
|
UserGroups = 'UserGroups',
|
||||||
|
UserGroupsArchived = 'UserGroupsArchived',
|
||||||
UserGroupMembers = 'UserGroupMembers',
|
UserGroupMembers = 'UserGroupMembers',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export function makeGetMentionKeysForPost(): (
|
|||||||
getCurrentUserMentionKeys,
|
getCurrentUserMentionKeys,
|
||||||
(state: GlobalState, post?: Post) => post,
|
(state: GlobalState, post?: Post) => post,
|
||||||
(state: GlobalState, post?: Post, channel?: Channel) =>
|
(state: GlobalState, post?: Post, channel?: Channel) =>
|
||||||
(channel ? getMyGroupMentionKeysForChannel(state, channel.team_id, channel.id) : getMyGroupMentionKeys(state)),
|
(channel ? getMyGroupMentionKeysForChannel(state, channel.team_id, channel.id) : getMyGroupMentionKeys(state, false)),
|
||||||
(mentionKeysWithoutGroups, post, groupMentionKeys) => {
|
(mentionKeysWithoutGroups, post, groupMentionKeys) => {
|
||||||
let mentionKeys = mentionKeysWithoutGroups;
|
let mentionKeys = mentionKeysWithoutGroups;
|
||||||
if (!post?.props?.disable_group_highlight) {
|
if (!post?.props?.disable_group_highlight) {
|
||||||
|
@ -21,6 +21,7 @@ import LocalStorageStore from 'stores/local_storage_store';
|
|||||||
|
|
||||||
import {Team} from '@mattermost/types/teams';
|
import {Team} from '@mattermost/types/teams';
|
||||||
import {ServerError} from '@mattermost/types/errors';
|
import {ServerError} from '@mattermost/types/errors';
|
||||||
|
import {GetGroupsForUserParams, GetGroupsParams} from '@mattermost/types/groups';
|
||||||
|
|
||||||
export function initializeTeam(team: Team): ActionFunc<Team, ServerError> {
|
export function initializeTeam(team: Team): ActionFunc<Team, ServerError> {
|
||||||
return async (dispatch, getState) => {
|
return async (dispatch, getState) => {
|
||||||
@ -50,8 +51,20 @@ export function initializeTeam(team: Team): ActionFunc<Team, ServerError> {
|
|||||||
if (license &&
|
if (license &&
|
||||||
license.IsLicensed === 'true' &&
|
license.IsLicensed === 'true' &&
|
||||||
(license.LDAPGroups === 'true' || customGroupEnabled)) {
|
(license.LDAPGroups === 'true' || customGroupEnabled)) {
|
||||||
|
const groupsParams: GetGroupsParams = {
|
||||||
|
filter_allow_reference: false,
|
||||||
|
page: 0,
|
||||||
|
per_page: 60,
|
||||||
|
include_member_count: true,
|
||||||
|
include_archived: false,
|
||||||
|
};
|
||||||
|
const myGroupsParams: GetGroupsForUserParams = {
|
||||||
|
...groupsParams,
|
||||||
|
filter_has_member: currentUser.id,
|
||||||
|
};
|
||||||
|
|
||||||
if (currentUser) {
|
if (currentUser) {
|
||||||
dispatch(getGroupsByUserIdPaginated(currentUser.id, false, 0, 60, true));
|
dispatch(getGroupsByUserIdPaginated(myGroupsParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (license.LDAPGroups === 'true') {
|
if (license.LDAPGroups === 'true') {
|
||||||
@ -61,7 +74,7 @@ export function initializeTeam(team: Team): ActionFunc<Team, ServerError> {
|
|||||||
if (team.group_constrained && license.LDAPGroups === 'true') {
|
if (team.group_constrained && license.LDAPGroups === 'true') {
|
||||||
dispatch(getAllGroupsAssociatedToTeam(team.id, true));
|
dispatch(getAllGroupsAssociatedToTeam(team.id, true));
|
||||||
} else {
|
} else {
|
||||||
dispatch(getGroups('', false, 0, 60, true));
|
dispatch(getGroups(groupsParams));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,49 +56,10 @@ exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Memo(UserGroupsFilter)
|
||||||
className="more-modal__dropdown"
|
getGroups={[Function]}
|
||||||
>
|
selectedFilter="all"
|
||||||
<MenuWrapper
|
|
||||||
animationComponent={[Function]}
|
|
||||||
className=""
|
|
||||||
id="groupsFilterDropdown"
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<span>
|
|
||||||
Show: All Groups
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="icon icon-chevron-down"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
|
||||||
<Menu
|
|
||||||
ariaLabel="Groups Filter Menu"
|
|
||||||
openLeft={false}
|
|
||||||
>
|
|
||||||
<MenuItemAction
|
|
||||||
buttonClass="groups-filter-btn"
|
|
||||||
id="groupsDropdownAll"
|
|
||||||
onClick={[Function]}
|
|
||||||
rightDecorator={
|
|
||||||
<i
|
|
||||||
className="icon icon-check"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
show={true}
|
|
||||||
text="All Groups"
|
|
||||||
/>
|
|
||||||
<MenuItemAction
|
|
||||||
buttonClass="groups-filter-btn"
|
|
||||||
id="groupsDropdownMy"
|
|
||||||
onClick={[Function]}
|
|
||||||
rightDecorator={false}
|
|
||||||
show={true}
|
|
||||||
text="My Groups"
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
|
||||||
<Connect(Component)
|
<Connect(Component)
|
||||||
backButtonAction={[MockFunction]}
|
backButtonAction={[MockFunction]}
|
||||||
groups={
|
groups={
|
||||||
@ -150,16 +111,17 @@ exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
loading={true}
|
hasNextPage={true}
|
||||||
|
loadMoreGroups={[Function]}
|
||||||
|
loading={false}
|
||||||
onExited={[MockFunction]}
|
onExited={[MockFunction]}
|
||||||
onScroll={[Function]}
|
|
||||||
searchTerm=""
|
searchTerm=""
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot with groups, myGroups selected 1`] = `
|
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
||||||
<Modal
|
<Modal
|
||||||
animation={true}
|
animation={true}
|
||||||
aria-labelledby="userGroupsModalLabel"
|
aria-labelledby="userGroupsModalLabel"
|
||||||
@ -215,122 +177,14 @@ exports[`component/user_groups_modal should match snapshot with groups, myGroups
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Memo(UserGroupsFilter)
|
||||||
className="more-modal__dropdown"
|
getGroups={[Function]}
|
||||||
>
|
selectedFilter="all"
|
||||||
<MenuWrapper
|
|
||||||
animationComponent={[Function]}
|
|
||||||
className=""
|
|
||||||
id="groupsFilterDropdown"
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<span>
|
|
||||||
Show: My Groups
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="icon icon-chevron-down"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
|
||||||
<Menu
|
|
||||||
ariaLabel="Groups Filter Menu"
|
|
||||||
openLeft={false}
|
|
||||||
>
|
|
||||||
<MenuItemAction
|
|
||||||
buttonClass="groups-filter-btn"
|
|
||||||
id="groupsDropdownAll"
|
|
||||||
onClick={[Function]}
|
|
||||||
rightDecorator={false}
|
|
||||||
show={true}
|
|
||||||
text="All Groups"
|
|
||||||
/>
|
|
||||||
<MenuItemAction
|
|
||||||
buttonClass="groups-filter-btn"
|
|
||||||
id="groupsDropdownMy"
|
|
||||||
onClick={[Function]}
|
|
||||||
rightDecorator={
|
|
||||||
<i
|
|
||||||
className="icon icon-check"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
show={true}
|
|
||||||
text="My Groups"
|
|
||||||
/>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
|
||||||
<Connect(Component)
|
|
||||||
backButtonAction={[MockFunction]}
|
|
||||||
groups={
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"allow_reference": true,
|
|
||||||
"create_at": 1637349374137,
|
|
||||||
"delete_at": 0,
|
|
||||||
"description": "Group 0 description",
|
|
||||||
"display_name": "Group 0",
|
|
||||||
"has_syncables": false,
|
|
||||||
"id": "group0",
|
|
||||||
"member_count": 1,
|
|
||||||
"name": "group0",
|
|
||||||
"remote_id": null,
|
|
||||||
"scheme_admin": false,
|
|
||||||
"source": "custom",
|
|
||||||
"update_at": 1637349374137,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
loading={true}
|
|
||||||
onExited={[MockFunction]}
|
|
||||||
onScroll={[Function]}
|
|
||||||
searchTerm=""
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
</Modal>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
|
||||||
<Modal
|
|
||||||
animation={true}
|
|
||||||
aria-labelledby="userGroupsModalLabel"
|
|
||||||
autoFocus={true}
|
|
||||||
backdrop={true}
|
|
||||||
bsClass="modal"
|
|
||||||
dialogClassName="a11y__modal user-groups-modal"
|
|
||||||
dialogComponentClass={[Function]}
|
|
||||||
enforceFocus={true}
|
|
||||||
id="userGroupsModal"
|
|
||||||
keyboard={true}
|
|
||||||
manager={
|
|
||||||
ModalManager {
|
|
||||||
"add": [Function],
|
|
||||||
"containers": Array [],
|
|
||||||
"data": Array [],
|
|
||||||
"handleContainerOverflow": true,
|
|
||||||
"hideSiblingNodes": true,
|
|
||||||
"isTopModal": [Function],
|
|
||||||
"modals": Array [],
|
|
||||||
"remove": [Function],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onExited={[MockFunction]}
|
|
||||||
onHide={[Function]}
|
|
||||||
renderBackdrop={[Function]}
|
|
||||||
restoreFocus={true}
|
|
||||||
role="dialog"
|
|
||||||
show={true}
|
|
||||||
>
|
|
||||||
<Connect(Component)
|
|
||||||
backButtonAction={[MockFunction]}
|
|
||||||
onExited={[MockFunction]}
|
|
||||||
/>
|
|
||||||
<ModalBody
|
|
||||||
bsClass="modal-body"
|
|
||||||
componentClass="div"
|
|
||||||
>
|
|
||||||
<NoResultsIndicator
|
<NoResultsIndicator
|
||||||
variant="UserGroups"
|
variant="UserGroups"
|
||||||
/>
|
/>
|
||||||
<ADLDAPUpsellBanner />
|
<Memo(ADLDAPUpsellBanner) />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</Modal>
|
</Modal>
|
||||||
`;
|
`;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {memo, useEffect, useState} from 'react';
|
||||||
import {useDispatch, useSelector} from 'react-redux';
|
import {useDispatch, useSelector} from 'react-redux';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
@ -146,4 +146,4 @@ function ADLDAPUpsellBanner() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ADLDAPUpsellBanner;
|
export default memo(ADLDAPUpsellBanner);
|
||||||
|
20
webapp/channels/src/components/user_groups_modal/hooks.ts
Normal file
20
webapp/channels/src/components/user_groups_modal/hooks.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useState} from 'react';
|
||||||
|
|
||||||
|
export function usePagingMeta(groupType: string): [number, (page: number) => void] {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [myGroupsPage, setMyGroupsPage] = useState(0);
|
||||||
|
const [archivedGroupsPage, setArchivedGroupsPage] = useState(0);
|
||||||
|
if (groupType === 'all') {
|
||||||
|
return [page, setPage];
|
||||||
|
} else if (groupType === 'my') {
|
||||||
|
return [myGroupsPage, setMyGroupsPage];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
archivedGroupsPage,
|
||||||
|
setArchivedGroupsPage,
|
||||||
|
];
|
||||||
|
}
|
@ -9,9 +9,9 @@ import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
|
|||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getAllAssociatedGroupsForReference, getMyAllowReferencedGroups, searchAllowReferencedGroups, searchMyAllowReferencedGroups} from 'mattermost-redux/selectors/entities/groups';
|
import {makeGetAllAssociatedGroupsForReference, makeGetMyAllowReferencedGroups, searchAllowReferencedGroups, searchMyAllowReferencedGroups, searchArchivedGroups, getArchivedGroups} from 'mattermost-redux/selectors/entities/groups';
|
||||||
import {getGroups, getGroupsByUserIdPaginated, searchGroups} from 'mattermost-redux/actions/groups';
|
import {getGroups, getGroupsByUserIdPaginated, searchGroups} from 'mattermost-redux/actions/groups';
|
||||||
import {Group, GroupSearachParams} from '@mattermost/types/groups';
|
import {GetGroupsForUserParams, GetGroupsParams, Group, GroupSearchParams} from '@mattermost/types/groups';
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
import {isModalOpen} from 'selectors/views/modals';
|
import {isModalOpen} from 'selectors/views/modals';
|
||||||
import {setModalSearchTerm} from 'actions/views/search';
|
import {setModalSearchTerm} from 'actions/views/search';
|
||||||
@ -20,35 +20,35 @@ import UserGroupsModal from './user_groups_modal';
|
|||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
getGroups: (
|
getGroups: (
|
||||||
filterAllowReference?: boolean,
|
groupsParams: GetGroupsParams,
|
||||||
page?: number,
|
|
||||||
perPage?: number,
|
|
||||||
includeMemberCount?: boolean
|
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
setModalSearchTerm: (term: string) => void;
|
setModalSearchTerm: (term: string) => void;
|
||||||
getGroupsByUserIdPaginated: (
|
getGroupsByUserIdPaginated: (
|
||||||
userId: string,
|
opts: GetGroupsForUserParams,
|
||||||
filterAllowReference?: boolean,
|
|
||||||
page?: number,
|
|
||||||
perPage?: number,
|
|
||||||
includeMemberCount?: boolean
|
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
searchGroups: (
|
searchGroups: (
|
||||||
params: GroupSearachParams,
|
params: GroupSearchParams,
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function makeMapStateToProps() {
|
||||||
|
const getAllAssociatedGroupsForReference = makeGetAllAssociatedGroupsForReference();
|
||||||
|
const getMyAllowReferencedGroups = makeGetMyAllowReferencedGroups();
|
||||||
|
|
||||||
|
return function mapStateToProps(state: GlobalState) {
|
||||||
const searchTerm = state.views.search.modalSearch;
|
const searchTerm = state.views.search.modalSearch;
|
||||||
|
|
||||||
let groups: Group[] = [];
|
let groups: Group[] = [];
|
||||||
let myGroups: Group[] = [];
|
let myGroups: Group[] = [];
|
||||||
|
let archivedGroups: Group[] = [];
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
groups = searchAllowReferencedGroups(state, searchTerm);
|
groups = searchAllowReferencedGroups(state, searchTerm, true);
|
||||||
myGroups = searchMyAllowReferencedGroups(state, searchTerm);
|
myGroups = searchMyAllowReferencedGroups(state, searchTerm, true);
|
||||||
|
archivedGroups = searchArchivedGroups(state, searchTerm);
|
||||||
} else {
|
} else {
|
||||||
groups = getAllAssociatedGroupsForReference(state);
|
groups = getAllAssociatedGroupsForReference(state, true);
|
||||||
myGroups = getMyAllowReferencedGroups(state);
|
myGroups = getMyAllowReferencedGroups(state, true);
|
||||||
|
archivedGroups = getArchivedGroups(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -56,8 +56,10 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
groups,
|
groups,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
myGroups,
|
myGroups,
|
||||||
|
archivedGroups,
|
||||||
currentUserId: getCurrentUserId(state),
|
currentUserId: getCurrentUserId(state),
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
@ -71,4 +73,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(UserGroupsModal);
|
export default connect(makeMapStateToProps, mapDispatchToProps, null, {forwardRef: true})(UserGroupsModal);
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {useCallback} from 'react';
|
||||||
|
import {useIntl} from 'react-intl';
|
||||||
|
|
||||||
|
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||||
|
import Menu from 'components/widgets/menu/menu';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedFilter: string;
|
||||||
|
getGroups: (page: number, groupType: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserGroupsFilter = (props: Props) => {
|
||||||
|
const {
|
||||||
|
selectedFilter,
|
||||||
|
getGroups,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const allGroupsOnClick = useCallback(() => {
|
||||||
|
getGroups(0, 'all');
|
||||||
|
}, [getGroups]);
|
||||||
|
|
||||||
|
const myGroupsOnClick = useCallback(() => {
|
||||||
|
getGroups(0, 'my');
|
||||||
|
}, [getGroups]);
|
||||||
|
|
||||||
|
const archivedGroupsOnClick = useCallback(() => {
|
||||||
|
getGroups(0, 'archived');
|
||||||
|
}, [getGroups]);
|
||||||
|
|
||||||
|
const filterLabel = useCallback(() => {
|
||||||
|
if (selectedFilter === 'all') {
|
||||||
|
return intl.formatMessage({id: 'user_groups_modal.showAllGroups', defaultMessage: 'Show: All Groups'});
|
||||||
|
} else if (selectedFilter === 'my') {
|
||||||
|
return intl.formatMessage({id: 'user_groups_modal.showMyGroups', defaultMessage: 'Show: My Groups'});
|
||||||
|
} else if (selectedFilter === 'archived') {
|
||||||
|
return intl.formatMessage({id: 'user_groups_modal.showArchivedGroups', defaultMessage: 'Show: Archived Groups'});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}, [selectedFilter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='more-modal__dropdown'>
|
||||||
|
<MenuWrapper id='groupsFilterDropdown'>
|
||||||
|
<a>
|
||||||
|
<span>{filterLabel()}</span>
|
||||||
|
<span className='icon icon-chevron-down'/>
|
||||||
|
</a>
|
||||||
|
<Menu
|
||||||
|
openLeft={false}
|
||||||
|
ariaLabel={intl.formatMessage({id: 'user_groups_modal.filterAriaLabel', defaultMessage: 'Groups Filter Menu'})}
|
||||||
|
>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.ItemAction
|
||||||
|
id='groupsDropdownAll'
|
||||||
|
buttonClass='groups-filter-btn'
|
||||||
|
onClick={allGroupsOnClick}
|
||||||
|
text={intl.formatMessage({id: 'user_groups_modal.allGroups', defaultMessage: 'All Groups'})}
|
||||||
|
rightDecorator={selectedFilter === 'all' && <i className='icon icon-check'/>}
|
||||||
|
/>
|
||||||
|
<Menu.ItemAction
|
||||||
|
id='groupsDropdownMy'
|
||||||
|
buttonClass='groups-filter-btn'
|
||||||
|
onClick={myGroupsOnClick}
|
||||||
|
text={intl.formatMessage({id: 'user_groups_modal.myGroups', defaultMessage: 'My Groups'})}
|
||||||
|
rightDecorator={selectedFilter === 'my' && <i className='icon icon-check'/>}
|
||||||
|
/>
|
||||||
|
</Menu.Group>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.ItemAction
|
||||||
|
id='groupsDropdownArchived'
|
||||||
|
buttonClass='groups-filter-btn'
|
||||||
|
onClick={archivedGroupsOnClick}
|
||||||
|
text={intl.formatMessage({id: 'user_groups_modal.archivedGroups', defaultMessage: 'Archived Groups'})}
|
||||||
|
rightDecorator={selectedFilter === 'archived' && <i className='icon icon-check'/>}
|
||||||
|
/>
|
||||||
|
</Menu.Group>
|
||||||
|
</Menu>
|
||||||
|
</MenuWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(UserGroupsFilter);
|
@ -3,276 +3,39 @@
|
|||||||
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
exports[`component/user_groups_modal should match snapshot with groups 1`] = `
|
||||||
<div
|
<div
|
||||||
className="user-groups-modal__content user-groups-list"
|
className="user-groups-modal__content user-groups-list"
|
||||||
onScroll={[MockFunction]}
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"overflow": "overlay",
|
"overflow": "overlay",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<InfiniteLoader
|
||||||
className="group-row"
|
isItemLoaded={[Function]}
|
||||||
key="group0"
|
itemCount={100000}
|
||||||
onClick={[Function]}
|
loadMoreItems={[MockFunction]}
|
||||||
>
|
>
|
||||||
<span
|
<Component />
|
||||||
className="group-display-name"
|
</InfiniteLoader>
|
||||||
>
|
<Memo(ADLDAPUpsellBanner) />
|
||||||
Group 0
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="group-name"
|
|
||||||
>
|
|
||||||
@
|
|
||||||
group0
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className="group-member-count"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
|
||||||
id="user_groups_modal.memberCount"
|
|
||||||
values={
|
|
||||||
Object {
|
|
||||||
"member_count": 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="group-action"
|
|
||||||
>
|
|
||||||
<MenuWrapper
|
|
||||||
animationComponent={[Function]}
|
|
||||||
className=""
|
|
||||||
id="customWrapper-group0"
|
|
||||||
isDisabled={false}
|
|
||||||
stopPropagationOnToggle={true}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="action-wrapper"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="icon icon-dots-vertical"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Menu
|
|
||||||
ariaLabel="User Actions Menu"
|
|
||||||
className="group-actions-menu"
|
|
||||||
openLeft={true}
|
|
||||||
openUp={false}
|
|
||||||
>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-account-multiple-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="View Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-archive-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isDangerous={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="Archive Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="group-row"
|
|
||||||
key="group1"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="group-display-name"
|
|
||||||
>
|
|
||||||
Group 1
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="group-name"
|
|
||||||
>
|
|
||||||
@
|
|
||||||
group1
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className="group-member-count"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
|
||||||
id="user_groups_modal.memberCount"
|
|
||||||
values={
|
|
||||||
Object {
|
|
||||||
"member_count": 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="group-action"
|
|
||||||
>
|
|
||||||
<MenuWrapper
|
|
||||||
animationComponent={[Function]}
|
|
||||||
className=""
|
|
||||||
id="customWrapper-group1"
|
|
||||||
isDisabled={false}
|
|
||||||
stopPropagationOnToggle={true}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="action-wrapper"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="icon icon-dots-vertical"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Menu
|
|
||||||
ariaLabel="User Actions Menu"
|
|
||||||
className="group-actions-menu"
|
|
||||||
openLeft={true}
|
|
||||||
openUp={true}
|
|
||||||
>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-account-multiple-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="View Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-archive-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isDangerous={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="Archive Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="group-row"
|
|
||||||
key="group2"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="group-display-name"
|
|
||||||
>
|
|
||||||
Group 2
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className="group-name"
|
|
||||||
>
|
|
||||||
@
|
|
||||||
group2
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className="group-member-count"
|
|
||||||
>
|
|
||||||
<MemoizedFormattedMessage
|
|
||||||
defaultMessage="{member_count} {member_count, plural, one {member} other {members}}"
|
|
||||||
id="user_groups_modal.memberCount"
|
|
||||||
values={
|
|
||||||
Object {
|
|
||||||
"member_count": 3,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="group-action"
|
|
||||||
>
|
|
||||||
<MenuWrapper
|
|
||||||
animationComponent={[Function]}
|
|
||||||
className=""
|
|
||||||
id="customWrapper-group2"
|
|
||||||
isDisabled={false}
|
|
||||||
stopPropagationOnToggle={true}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="action-wrapper"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className="icon icon-dots-vertical"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<Menu
|
|
||||||
ariaLabel="User Actions Menu"
|
|
||||||
className="group-actions-menu"
|
|
||||||
openLeft={true}
|
|
||||||
openUp={true}
|
|
||||||
>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-account-multiple-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="View Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
<MenuGroup>
|
|
||||||
<MenuItemAction
|
|
||||||
disabled={false}
|
|
||||||
icon={
|
|
||||||
<i
|
|
||||||
className="icon-archive-outline"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isDangerous={true}
|
|
||||||
onClick={[Function]}
|
|
||||||
show={true}
|
|
||||||
text="Archive Group"
|
|
||||||
/>
|
|
||||||
</MenuGroup>
|
|
||||||
</Menu>
|
|
||||||
</MenuWrapper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ADLDAPUpsellBanner />
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
exports[`component/user_groups_modal should match snapshot without groups 1`] = `
|
||||||
<div
|
<div
|
||||||
className="user-groups-modal__content user-groups-list"
|
className="user-groups-modal__content user-groups-list"
|
||||||
onScroll={[MockFunction]}
|
|
||||||
style={
|
style={
|
||||||
Object {
|
Object {
|
||||||
"overflow": "overlay",
|
"overflow": "overlay",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ADLDAPUpsellBanner />
|
<InfiniteLoader
|
||||||
|
isItemLoaded={[Function]}
|
||||||
|
itemCount={100000}
|
||||||
|
loadMoreItems={[MockFunction]}
|
||||||
|
>
|
||||||
|
<Component />
|
||||||
|
</InfiniteLoader>
|
||||||
|
<Memo(ADLDAPUpsellBanner) />
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -8,7 +8,7 @@ import {ActionFunc, ActionResult, GenericAction} from 'mattermost-redux/types/ac
|
|||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import {archiveGroup} from 'mattermost-redux/actions/groups';
|
import {archiveGroup, restoreGroup} from 'mattermost-redux/actions/groups';
|
||||||
import {ModalData} from 'types/actions';
|
import {ModalData} from 'types/actions';
|
||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
import {getGroupListPermissions} from 'mattermost-redux/selectors/entities/roles';
|
import {getGroupListPermissions} from 'mattermost-redux/selectors/entities/roles';
|
||||||
@ -18,6 +18,7 @@ import UserGroupsList from './user_groups_list';
|
|||||||
type Actions = {
|
type Actions = {
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
|
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state: GlobalState) {
|
function mapStateToProps(state: GlobalState) {
|
||||||
@ -32,6 +33,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
actions: bindActionCreators<ActionCreatorsMapObject<ActionFunc | GenericAction>, Actions>({
|
||||||
openModal,
|
openModal,
|
||||||
archiveGroup,
|
archiveGroup,
|
||||||
|
restoreGroup,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,12 @@ describe('component/user_groups_modal', () => {
|
|||||||
backButtonAction: jest.fn(),
|
backButtonAction: jest.fn(),
|
||||||
groupPermissionsMap: {},
|
groupPermissionsMap: {},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
loadMoreGroups: jest.fn(),
|
||||||
|
hasNextPage: false,
|
||||||
actions: {
|
actions: {
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
archiveGroup: jest.fn(),
|
archiveGroup: jest.fn(),
|
||||||
|
restoreGroup: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,6 +56,7 @@ describe('component/user_groups_modal', () => {
|
|||||||
groupPermissionsMap[g.id] = {
|
groupPermissionsMap[g.id] = {
|
||||||
can_delete: true,
|
can_delete: true,
|
||||||
can_manage_members: true,
|
can_manage_members: true,
|
||||||
|
can_restore: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useState} from 'react';
|
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||||
|
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
import {VariableSizeList, ListChildComponentProps} from 'react-window';
|
||||||
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
|
|
||||||
import NoResultsIndicator from 'components/no_results_indicator';
|
import NoResultsIndicator from 'components/no_results_indicator';
|
||||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||||
@ -25,27 +27,33 @@ export type Props = {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
groupPermissionsMap: Record<string, GroupPermissions>;
|
groupPermissionsMap: Record<string, GroupPermissions>;
|
||||||
onScroll: () => void;
|
loadMoreGroups: () => void;
|
||||||
onExited: () => void;
|
onExited: () => void;
|
||||||
backButtonAction: () => void;
|
backButtonAction: () => void;
|
||||||
|
hasNextPage: boolean;
|
||||||
actions: {
|
actions: {
|
||||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
|
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivElement>) => {
|
const UserGroupsList = (props: Props) => {
|
||||||
const {
|
const {
|
||||||
groups,
|
groups,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
loading,
|
loading,
|
||||||
groupPermissionsMap,
|
groupPermissionsMap,
|
||||||
onScroll,
|
hasNextPage,
|
||||||
|
loadMoreGroups,
|
||||||
backButtonAction,
|
backButtonAction,
|
||||||
onExited,
|
onExited,
|
||||||
actions,
|
actions,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const infiniteLoaderRef = useRef<InfiniteLoader | null>(null);
|
||||||
|
const variableSizeListRef = useRef<VariableSizeList | null>(null);
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
const [overflowState, setOverflowState] = useState('overlay');
|
const [overflowState, setOverflowState] = useState('overlay');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -54,10 +62,34 @@ const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivEl
|
|||||||
}
|
}
|
||||||
}, [groups]);
|
}, [groups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMounted) {
|
||||||
|
if (infiniteLoaderRef.current) {
|
||||||
|
infiniteLoaderRef.current.resetloadMoreItemsCache();
|
||||||
|
}
|
||||||
|
if (variableSizeListRef.current) {
|
||||||
|
variableSizeListRef.current.resetAfterIndex(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHasMounted(true);
|
||||||
|
}, [searchTerm, groups.length, hasMounted]);
|
||||||
|
|
||||||
|
const itemCount = hasNextPage ? groups.length + 1 : groups.length;
|
||||||
|
|
||||||
|
const loadMoreItems = loading ? () => {} : loadMoreGroups;
|
||||||
|
|
||||||
|
const isItemLoaded = (index: number) => {
|
||||||
|
return !hasNextPage || index < groups.length;
|
||||||
|
};
|
||||||
|
|
||||||
const archiveGroup = useCallback(async (groupId: string) => {
|
const archiveGroup = useCallback(async (groupId: string) => {
|
||||||
await actions.archiveGroup(groupId);
|
await actions.archiveGroup(groupId);
|
||||||
}, [actions.archiveGroup]);
|
}, [actions.archiveGroup]);
|
||||||
|
|
||||||
|
const restoreGroup = useCallback(async (groupId: string) => {
|
||||||
|
await actions.restoreGroup(groupId);
|
||||||
|
}, [actions.restoreGroup]);
|
||||||
|
|
||||||
const goToViewGroupModal = useCallback((group: Group) => {
|
const goToViewGroupModal = useCallback((group: Group) => {
|
||||||
actions.openModal({
|
actions.openModal({
|
||||||
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
modalId: ModalIdentifiers.VIEW_USER_GROUP,
|
||||||
@ -74,36 +106,41 @@ const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivEl
|
|||||||
}, [actions.openModal, onExited, backButtonAction]);
|
}, [actions.openModal, onExited, backButtonAction]);
|
||||||
|
|
||||||
const groupListOpenUp = (groupListItemIndex: number): boolean => {
|
const groupListOpenUp = (groupListItemIndex: number): boolean => {
|
||||||
if (groups.length > 1 && groupListItemIndex === 0) {
|
if (groupListItemIndex === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Item = ({index, style}: ListChildComponentProps) => {
|
||||||
|
if (groups.length === 0 && searchTerm) {
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
className='user-groups-modal__content user-groups-list'
|
|
||||||
onScroll={onScroll}
|
|
||||||
ref={ref}
|
|
||||||
style={{overflow: overflowState}}
|
|
||||||
>
|
|
||||||
{(groups.length === 0 && searchTerm) &&
|
|
||||||
<NoResultsIndicator
|
<NoResultsIndicator
|
||||||
variant={NoResultsVariant.ChannelSearch}
|
variant={NoResultsVariant.ChannelSearch}
|
||||||
titleValues={{channelName: `"${searchTerm}"`}}
|
titleValues={{channelName: `"${searchTerm}"`}}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{groups.map((group, i) => {
|
if (isItemLoaded(index)) {
|
||||||
|
const group = groups[index] as Group;
|
||||||
|
if (!group) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='group-row'
|
className='group-row'
|
||||||
|
style={style}
|
||||||
key={group.id}
|
key={group.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
goToViewGroupModal(group);
|
goToViewGroupModal(group);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className='group-display-name'>
|
<span className='group-display-name'>
|
||||||
|
{
|
||||||
|
group.delete_at > 0 &&
|
||||||
|
<i className='icon icon-archive-outline'/>
|
||||||
|
}
|
||||||
{group.display_name}
|
{group.display_name}
|
||||||
</span>
|
</span>
|
||||||
<span className='group-name'>
|
<span className='group-name'>
|
||||||
@ -129,7 +166,7 @@ const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivEl
|
|||||||
</button>
|
</button>
|
||||||
<Menu
|
<Menu
|
||||||
openLeft={true}
|
openLeft={true}
|
||||||
openUp={groupListOpenUp(i)}
|
openUp={groupListOpenUp(index)}
|
||||||
className={'group-actions-menu'}
|
className={'group-actions-menu'}
|
||||||
ariaLabel={Utils.localizeMessage('admin.user_item.menuAriaLabel', 'User Actions Menu')}
|
ariaLabel={Utils.localizeMessage('admin.user_item.menuAriaLabel', 'User Actions Menu')}
|
||||||
>
|
>
|
||||||
@ -154,20 +191,54 @@ const UserGroupsList = React.forwardRef((props: Props, ref?: React.Ref<HTMLDivEl
|
|||||||
disabled={false}
|
disabled={false}
|
||||||
isDangerous={true}
|
isDangerous={true}
|
||||||
/>
|
/>
|
||||||
|
<Menu.ItemAction
|
||||||
|
show={groupPermissionsMap[group.id].can_restore}
|
||||||
|
onClick={() => {
|
||||||
|
restoreGroup(group.id);
|
||||||
|
}}
|
||||||
|
icon={<i className='icon-restore'/>}
|
||||||
|
text={Utils.localizeMessage('user_groups_modal.restoreGroup', 'Restore Group')}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
</Menu.Group>
|
</Menu.Group>
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
|
||||||
{
|
|
||||||
(loading) &&
|
|
||||||
<LoadingScreen/>
|
|
||||||
}
|
}
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingScreen/>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='user-groups-modal__content user-groups-list'
|
||||||
|
style={{overflow: overflowState}}
|
||||||
|
>
|
||||||
|
<InfiniteLoader
|
||||||
|
ref={infiniteLoaderRef}
|
||||||
|
isItemLoaded={isItemLoaded}
|
||||||
|
itemCount={100000}
|
||||||
|
loadMoreItems={loadMoreItems}
|
||||||
|
>
|
||||||
|
{({onItemsRendered, ref}) => (
|
||||||
|
<VariableSizeList
|
||||||
|
itemCount={itemCount}
|
||||||
|
onItemsRendered={onItemsRendered}
|
||||||
|
ref={ref}
|
||||||
|
itemSize={() => 52}
|
||||||
|
height={groups.length >= 8 ? 416 : Math.max(groups.length, 3) * 52}
|
||||||
|
width={'100%'}
|
||||||
|
>
|
||||||
|
{Item}
|
||||||
|
</VariableSizeList>)}
|
||||||
|
</InfiniteLoader>
|
||||||
<ADLDAPUpsellBanner/>
|
<ADLDAPUpsellBanner/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
export default React.memo(UserGroupsList);
|
export default React.memo(UserGroupsList);
|
||||||
|
@ -162,11 +162,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
overflow: hidden;
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.no-results__wrapper {
|
.no-results__wrapper {
|
||||||
|
max-width: 350px;
|
||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,7 +242,7 @@
|
|||||||
|
|
||||||
&.user-groups-list {
|
&.user-groups-list {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: 450px;
|
max-height: 460px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
overflow-y: scroll; // for Firefox and browsers that doesn't support overflow-y:overlay property
|
overflow-y: scroll; // for Firefox and browsers that doesn't support overflow-y:overlay property
|
||||||
@ -284,6 +284,11 @@
|
|||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-name {
|
.group-name {
|
||||||
@ -305,6 +310,13 @@
|
|||||||
.MenuWrapper {
|
.MenuWrapper {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
|
.Menu {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-actions-menu {
|
.group-actions-menu {
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import {Group} from '@mattermost/types/groups';
|
import {Group} from '@mattermost/types/groups';
|
||||||
@ -14,6 +13,7 @@ describe('component/user_groups_modal', () => {
|
|||||||
onExited: jest.fn(),
|
onExited: jest.fn(),
|
||||||
groups: [],
|
groups: [],
|
||||||
myGroups: [],
|
myGroups: [],
|
||||||
|
archivedGroups: [],
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
currentUserId: '',
|
currentUserId: '',
|
||||||
backButtonAction: jest.fn(),
|
backButtonAction: jest.fn(),
|
||||||
@ -70,52 +70,4 @@ describe('component/user_groups_modal', () => {
|
|||||||
);
|
);
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should match snapshot with groups, myGroups selected', () => {
|
|
||||||
const groups = getGroups(3);
|
|
||||||
const myGroups = getGroups(1);
|
|
||||||
|
|
||||||
const wrapper = shallow(
|
|
||||||
<UserGroupsModal
|
|
||||||
{...baseProps}
|
|
||||||
groups={groups}
|
|
||||||
myGroups={myGroups}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
wrapper.setState({selectedFilter: 'my'});
|
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should match snapshot with groups, search group1', () => {
|
|
||||||
const groups = getGroups(3);
|
|
||||||
const myGroups = getGroups(1);
|
|
||||||
|
|
||||||
const wrapper = shallow(
|
|
||||||
<UserGroupsModal
|
|
||||||
{...baseProps}
|
|
||||||
groups={groups}
|
|
||||||
myGroups={myGroups}
|
|
||||||
searchTerm='group1'
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const instance = wrapper.instance() as UserGroupsModal;
|
|
||||||
|
|
||||||
const e = {
|
|
||||||
target: {
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(1);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith('');
|
|
||||||
|
|
||||||
e.target.value = 'group1';
|
|
||||||
instance.handleSearch(e as React.ChangeEvent<HTMLInputElement>);
|
|
||||||
expect(wrapper.state('loading')).toEqual(true);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toHaveBeenCalledTimes(2);
|
|
||||||
expect(baseProps.actions.setModalSearchTerm).toBeCalledWith(e.target.value);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {createRef, RefObject} from 'react';
|
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||||
|
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
|
|
||||||
import Constants from 'utils/constants';
|
import Constants from 'utils/constants';
|
||||||
|
|
||||||
import * as Utils from 'utils/utils';
|
import * as Utils from 'utils/utils';
|
||||||
import {Group, GroupSearachParams} from '@mattermost/types/groups';
|
import {GetGroupsForUserParams, GetGroupsParams, Group, GroupSearchParams} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import './user_groups_modal.scss';
|
import './user_groups_modal.scss';
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|
||||||
import Menu from 'components/widgets/menu/menu';
|
|
||||||
import {debounce} from 'mattermost-redux/actions/helpers';
|
|
||||||
import Input from 'components/widgets/inputs/input/input';
|
import Input from 'components/widgets/inputs/input/input';
|
||||||
import NoResultsIndicator from 'components/no_results_indicator';
|
import NoResultsIndicator from 'components/no_results_indicator';
|
||||||
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
import {NoResultsVariant} from 'components/no_results_indicator/types';
|
||||||
|
|
||||||
import UserGroupsList from './user_groups_list';
|
import UserGroupsList from './user_groups_list';
|
||||||
|
import UserGroupsFilter from './user_groups_filter/user_groups_filter';
|
||||||
import UserGroupsModalHeader from './user_groups_modal_header';
|
import UserGroupsModalHeader from './user_groups_modal_header';
|
||||||
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
import ADLDAPUpsellBanner from './ad_ldap_upsell_banner';
|
||||||
|
import {usePagingMeta} from './hooks';
|
||||||
|
|
||||||
const GROUPS_PER_PAGE = 60;
|
const GROUPS_PER_PAGE = 60;
|
||||||
|
|
||||||
@ -28,269 +27,208 @@ export type Props = {
|
|||||||
onExited: () => void;
|
onExited: () => void;
|
||||||
groups: Group[];
|
groups: Group[];
|
||||||
myGroups: Group[];
|
myGroups: Group[];
|
||||||
|
archivedGroups: Group[];
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
currentUserId: string;
|
currentUserId: string;
|
||||||
backButtonAction: () => void;
|
backButtonAction: () => void;
|
||||||
actions: {
|
actions: {
|
||||||
getGroups: (
|
getGroups: (
|
||||||
filterAllowReference?: boolean,
|
opts: GetGroupsParams,
|
||||||
page?: number,
|
|
||||||
perPage?: number,
|
|
||||||
includeMemberCount?: boolean
|
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
setModalSearchTerm: (term: string) => void;
|
setModalSearchTerm: (term: string) => void;
|
||||||
getGroupsByUserIdPaginated: (
|
getGroupsByUserIdPaginated: (
|
||||||
userId: string,
|
opts: GetGroupsForUserParams,
|
||||||
filterAllowReference?: boolean,
|
|
||||||
page?: number,
|
|
||||||
perPage?: number,
|
|
||||||
includeMemberCount?: boolean
|
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
searchGroups: (
|
searchGroups: (
|
||||||
params: GroupSearachParams,
|
params: GroupSearchParams,
|
||||||
) => Promise<{data: Group[]}>;
|
) => Promise<{data: Group[]}>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
const UserGroupsModal = (props: Props) => {
|
||||||
page: number;
|
const [searchTimeoutId, setSearchTimeoutId] = useState(0);
|
||||||
myGroupsPage: number;
|
const [loading, setLoading] = useState(false);
|
||||||
loading: boolean;
|
const [show, setShow] = useState(true);
|
||||||
show: boolean;
|
const [selectedFilter, setSelectedFilter] = useState('all');
|
||||||
selectedFilter: string;
|
const [groupsFull, setGroupsFull] = useState(false);
|
||||||
allGroupsFull: boolean;
|
const [groups, setGroups] = useState(props.groups);
|
||||||
myGroupsFull: boolean;
|
|
||||||
|
const [page, setPage] = usePagingMeta(selectedFilter);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFilter === 'all') {
|
||||||
|
setGroups(props.groups);
|
||||||
}
|
}
|
||||||
|
if (selectedFilter === 'my') {
|
||||||
export default class UserGroupsModal extends React.PureComponent<Props, State> {
|
setGroups(props.myGroups);
|
||||||
divScrollRef: RefObject<HTMLDivElement>;
|
|
||||||
private searchTimeoutId: number;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.divScrollRef = createRef();
|
|
||||||
this.searchTimeoutId = 0;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
page: 0,
|
|
||||||
myGroupsPage: 0,
|
|
||||||
loading: true,
|
|
||||||
show: true,
|
|
||||||
selectedFilter: 'all',
|
|
||||||
allGroupsFull: false,
|
|
||||||
myGroupsFull: false,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
if (selectedFilter === 'archived') {
|
||||||
|
setGroups(props.archivedGroups);
|
||||||
|
}
|
||||||
|
}, [selectedFilter, props.groups, props.myGroups]);
|
||||||
|
|
||||||
doHide = () => {
|
const doHide = () => {
|
||||||
this.setState({show: false});
|
setShow(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
const getGroups = useCallback(async (page: number, groupType: string) => {
|
||||||
const {
|
const {actions, currentUserId} = props;
|
||||||
actions,
|
setLoading(true);
|
||||||
} = this.props;
|
const groupsParams: GetGroupsParams = {
|
||||||
await Promise.all([
|
filter_allow_reference: false,
|
||||||
actions.getGroups(false, this.state.page, GROUPS_PER_PAGE, true),
|
page,
|
||||||
actions.getGroupsByUserIdPaginated(this.props.currentUserId, false, this.state.myGroupsPage, GROUPS_PER_PAGE, true),
|
|
||||||
]);
|
|
||||||
this.loadComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.actions.setModalSearchTerm('');
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
if (prevProps.searchTerm !== this.props.searchTerm) {
|
|
||||||
clearTimeout(this.searchTimeoutId);
|
|
||||||
const searchTerm = this.props.searchTerm;
|
|
||||||
|
|
||||||
if (searchTerm === '') {
|
|
||||||
this.loadComplete();
|
|
||||||
this.searchTimeoutId = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTimeoutId = window.setTimeout(
|
|
||||||
async () => {
|
|
||||||
const params: GroupSearachParams = {
|
|
||||||
q: searchTerm,
|
|
||||||
filter_allow_reference: true,
|
|
||||||
page: this.state.page,
|
|
||||||
per_page: GROUPS_PER_PAGE,
|
per_page: GROUPS_PER_PAGE,
|
||||||
include_member_count: true,
|
include_member_count: true,
|
||||||
};
|
};
|
||||||
if (this.state.selectedFilter === 'all') {
|
let data: {data: Group[]} = {data: []};
|
||||||
await prevProps.actions.searchGroups(params);
|
|
||||||
|
if (groupType === 'all') {
|
||||||
|
groupsParams.include_archived = true;
|
||||||
|
data = await actions.getGroups(groupsParams);
|
||||||
|
} else if (groupType === 'my') {
|
||||||
|
const groupsUserParams = {
|
||||||
|
...groupsParams,
|
||||||
|
filter_has_member: currentUserId,
|
||||||
|
include_archived: true,
|
||||||
|
} as GetGroupsForUserParams;
|
||||||
|
data = await actions.getGroupsByUserIdPaginated(groupsUserParams);
|
||||||
|
} else if (groupType === 'archived') {
|
||||||
|
groupsParams.filter_archived = true;
|
||||||
|
data = await actions.getGroups(groupsParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.data.length === 0) {
|
||||||
|
setGroupsFull(true);
|
||||||
} else {
|
} else {
|
||||||
params.user_id = this.props.currentUserId;
|
setGroupsFull(false);
|
||||||
await prevProps.actions.searchGroups(params);
|
}
|
||||||
|
setLoading(false);
|
||||||
|
setSelectedFilter(groupType);
|
||||||
|
}, [props.actions.getGroups, props.actions.getGroupsByUserIdPaginated, props.currentUserId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getGroups(0, 'all');
|
||||||
|
return () => {
|
||||||
|
props.actions.setModalSearchTerm('');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
clearTimeout(searchTimeoutId);
|
||||||
|
const searchTerm = props.searchTerm;
|
||||||
|
|
||||||
|
if (searchTerm === '') {
|
||||||
|
setLoading(false);
|
||||||
|
setSearchTimeoutId(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = window.setTimeout(
|
||||||
|
async () => {
|
||||||
|
const params: GroupSearchParams = {
|
||||||
|
q: searchTerm,
|
||||||
|
filter_allow_reference: true,
|
||||||
|
page,
|
||||||
|
per_page: GROUPS_PER_PAGE,
|
||||||
|
include_archived: true,
|
||||||
|
include_member_count: true,
|
||||||
|
};
|
||||||
|
if (selectedFilter === 'all') {
|
||||||
|
await props.actions.searchGroups(params);
|
||||||
|
} else if (selectedFilter === 'my') {
|
||||||
|
params.filter_has_member = props.currentUserId;
|
||||||
|
await props.actions.searchGroups(params);
|
||||||
|
} else if (selectedFilter === 'archived') {
|
||||||
|
params.filter_archived = true;
|
||||||
|
await props.actions.searchGroups(params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
Constants.SEARCH_TIMEOUT_MILLISECONDS,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.searchTimeoutId = searchTimeoutId;
|
setSearchTimeoutId(timeoutId);
|
||||||
}
|
}, [props.searchTerm, setSearchTimeoutId]);
|
||||||
}
|
|
||||||
|
|
||||||
startLoad = () => {
|
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.setState({loading: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
loadComplete = () => {
|
|
||||||
this.setState({loading: false});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
this.props.actions.setModalSearchTerm(term);
|
props.actions.setModalSearchTerm(term);
|
||||||
};
|
}, [props.actions.setModalSearchTerm]);
|
||||||
|
|
||||||
scrollGetGroups = debounce(
|
const loadMoreGroups = useCallback(() => {
|
||||||
async () => {
|
|
||||||
const {page} = this.state;
|
|
||||||
const newPage = page + 1;
|
const newPage = page + 1;
|
||||||
|
setPage(newPage);
|
||||||
this.setState({page: newPage});
|
if (selectedFilter === 'all' && !loading) {
|
||||||
this.getGroups(newPage);
|
getGroups(newPage, 'all');
|
||||||
},
|
|
||||||
500,
|
|
||||||
false,
|
|
||||||
(): void => {},
|
|
||||||
);
|
|
||||||
scrollGetMyGroups = debounce(
|
|
||||||
async () => {
|
|
||||||
const {myGroupsPage} = this.state;
|
|
||||||
const newPage = myGroupsPage + 1;
|
|
||||||
|
|
||||||
this.setState({myGroupsPage: newPage});
|
|
||||||
this.getMyGroups(newPage);
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
false,
|
|
||||||
(): void => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
onScroll = () => {
|
|
||||||
const scrollHeight = this.divScrollRef.current?.scrollHeight || 0;
|
|
||||||
const scrollTop = this.divScrollRef.current?.scrollTop || 0;
|
|
||||||
const clientHeight = this.divScrollRef.current?.clientHeight || 0;
|
|
||||||
|
|
||||||
if ((scrollTop + clientHeight + 30) >= scrollHeight) {
|
|
||||||
if (this.state.selectedFilter === 'all' && this.state.loading === false && !this.state.allGroupsFull) {
|
|
||||||
this.scrollGetGroups();
|
|
||||||
}
|
}
|
||||||
if (this.state.selectedFilter !== 'all' && this.props.myGroups.length % GROUPS_PER_PAGE === 0 && this.state.loading === false) {
|
if (selectedFilter === 'my' && !loading) {
|
||||||
this.scrollGetMyGroups();
|
getGroups(newPage, 'my');
|
||||||
}
|
}
|
||||||
|
if (selectedFilter === 'archived' && !loading) {
|
||||||
|
getGroups(newPage, 'archived');
|
||||||
}
|
}
|
||||||
};
|
}, [selectedFilter, page, getGroups, loading]);
|
||||||
|
|
||||||
getMyGroups = async (page: number) => {
|
const inputPrefix = useMemo(() => {
|
||||||
const {actions} = this.props;
|
return <i className={'icon icon-magnify'}/>;
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.startLoad();
|
const noResultsType = useMemo(() => {
|
||||||
const data = await actions.getGroupsByUserIdPaginated(this.props.currentUserId, false, page, GROUPS_PER_PAGE, true);
|
if (selectedFilter === 'archived') {
|
||||||
if (data.data.length === 0) {
|
return NoResultsVariant.UserGroupsArchived;
|
||||||
this.setState({myGroupsFull: true});
|
|
||||||
}
|
}
|
||||||
this.loadComplete();
|
return NoResultsVariant.UserGroups;
|
||||||
this.setState({selectedFilter: 'my'});
|
}, [selectedFilter]);
|
||||||
};
|
|
||||||
|
|
||||||
getGroups = async (page: number) => {
|
|
||||||
const {actions} = this.props;
|
|
||||||
|
|
||||||
this.startLoad();
|
|
||||||
const data = await actions.getGroups(false, page, GROUPS_PER_PAGE, true);
|
|
||||||
if (data.data.length === 0) {
|
|
||||||
this.setState({allGroupsFull: true});
|
|
||||||
}
|
|
||||||
this.loadComplete();
|
|
||||||
this.setState({selectedFilter: 'all'});
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const groups = this.state.selectedFilter === 'all' ? this.props.groups : this.props.myGroups;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
dialogClassName='a11y__modal user-groups-modal'
|
dialogClassName='a11y__modal user-groups-modal'
|
||||||
show={this.state.show}
|
show={show}
|
||||||
onHide={this.doHide}
|
onHide={doHide}
|
||||||
onExited={this.props.onExited}
|
onExited={props.onExited}
|
||||||
role='dialog'
|
role='dialog'
|
||||||
aria-labelledby='userGroupsModalLabel'
|
aria-labelledby='userGroupsModalLabel'
|
||||||
id='userGroupsModal'
|
id='userGroupsModal'
|
||||||
>
|
>
|
||||||
<UserGroupsModalHeader
|
<UserGroupsModalHeader
|
||||||
onExited={this.props.onExited}
|
onExited={props.onExited}
|
||||||
backButtonAction={this.props.backButtonAction}
|
backButtonAction={props.backButtonAction}
|
||||||
/>
|
/>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{(groups.length === 0 && !this.props.searchTerm) ? <>
|
|
||||||
<NoResultsIndicator
|
|
||||||
variant={NoResultsVariant.UserGroups}
|
|
||||||
/>
|
|
||||||
<ADLDAPUpsellBanner/>
|
|
||||||
</> : <>
|
|
||||||
<div className='user-groups-search'>
|
<div className='user-groups-search'>
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={Utils.localizeMessage('user_groups_modal.searchGroups', 'Search Groups')}
|
placeholder={Utils.localizeMessage('user_groups_modal.searchGroups', 'Search Groups')}
|
||||||
onChange={this.handleSearch}
|
onChange={handleSearch}
|
||||||
value={this.props.searchTerm}
|
value={props.searchTerm}
|
||||||
data-testid='searchInput'
|
data-testid='searchInput'
|
||||||
className={'user-group-search-input'}
|
className={'user-group-search-input'}
|
||||||
inputPrefix={<i className={'icon icon-magnify'}/>}
|
inputPrefix={inputPrefix}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='more-modal__dropdown'>
|
<UserGroupsFilter
|
||||||
<MenuWrapper id='groupsFilterDropdown'>
|
selectedFilter={selectedFilter}
|
||||||
<a>
|
getGroups={getGroups}
|
||||||
<span>{this.state.selectedFilter === 'all' ? Utils.localizeMessage('user_groups_modal.showAllGroups', 'Show: All Groups') : Utils.localizeMessage('user_groups_modal.showMyGroups', 'Show: My Groups')}</span>
|
|
||||||
<span className='icon icon-chevron-down'/>
|
|
||||||
</a>
|
|
||||||
<Menu
|
|
||||||
openLeft={false}
|
|
||||||
ariaLabel={Utils.localizeMessage('user_groups_modal.filterAriaLabel', 'Groups Filter Menu')}
|
|
||||||
>
|
|
||||||
<Menu.ItemAction
|
|
||||||
id='groupsDropdownAll'
|
|
||||||
buttonClass='groups-filter-btn'
|
|
||||||
onClick={() => {
|
|
||||||
this.getGroups(0);
|
|
||||||
}}
|
|
||||||
text={Utils.localizeMessage('user_groups_modal.allGroups', 'All Groups')}
|
|
||||||
rightDecorator={this.state.selectedFilter === 'all' && <i className='icon icon-check'/>}
|
|
||||||
/>
|
/>
|
||||||
<Menu.ItemAction
|
{(groups.length === 0 && !props.searchTerm) ? <>
|
||||||
id='groupsDropdownMy'
|
<NoResultsIndicator
|
||||||
buttonClass='groups-filter-btn'
|
variant={noResultsType}
|
||||||
onClick={() => {
|
|
||||||
this.getMyGroups(0);
|
|
||||||
}}
|
|
||||||
text={Utils.localizeMessage('user_groups_modal.myGroups', 'My Groups')}
|
|
||||||
rightDecorator={this.state.selectedFilter !== 'all' && <i className='icon icon-check'/>}
|
|
||||||
/>
|
/>
|
||||||
</Menu>
|
<ADLDAPUpsellBanner/>
|
||||||
</MenuWrapper>
|
</> : <>
|
||||||
</div>
|
|
||||||
<UserGroupsList
|
<UserGroupsList
|
||||||
groups={groups}
|
groups={groups}
|
||||||
searchTerm={this.props.searchTerm}
|
searchTerm={props.searchTerm}
|
||||||
loading={this.state.loading}
|
loading={loading}
|
||||||
onScroll={this.onScroll}
|
hasNextPage={!groupsFull}
|
||||||
ref={this.divScrollRef}
|
loadMoreGroups={loadMoreGroups}
|
||||||
onExited={this.props.onExited}
|
onExited={props.onExited}
|
||||||
backButtonAction={this.props.backButtonAction}
|
backButtonAction={props.backButtonAction}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
export default React.memo(UserGroupsModal);
|
||||||
|
@ -5,6 +5,11 @@
|
|||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
margin-left: 46px;
|
margin-left: 46px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
margin-left: 5px;
|
||||||
|
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.user-groups-create {
|
button.user-groups-create {
|
||||||
|
@ -10,9 +10,8 @@ import {GlobalState} from 'types/store';
|
|||||||
import {ModalData} from 'types/actions';
|
import {ModalData} from 'types/actions';
|
||||||
import {openModal} from 'actions/views/modals';
|
import {openModal} from 'actions/views/modals';
|
||||||
import {getGroup as getGroupById, isMyGroup} from 'mattermost-redux/selectors/entities/groups';
|
import {getGroup as getGroupById, isMyGroup} from 'mattermost-redux/selectors/entities/groups';
|
||||||
import {addUsersToGroup, archiveGroup, removeUsersFromGroup} from 'mattermost-redux/actions/groups';
|
import {addUsersToGroup, archiveGroup, removeUsersFromGroup, restoreGroup} from 'mattermost-redux/actions/groups';
|
||||||
import {haveIGroupPermission} from 'mattermost-redux/selectors/entities/roles';
|
import {haveIGroupPermission} from 'mattermost-redux/selectors/entities/roles';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
|
||||||
import {Permissions} from 'mattermost-redux/constants';
|
import {Permissions} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
import ViewUserGroupModalHeader from './view_user_group_modal_header';
|
import ViewUserGroupModalHeader from './view_user_group_modal_header';
|
||||||
@ -22,6 +21,7 @@ type Actions = {
|
|||||||
removeUsersFromGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
removeUsersFromGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
addUsersToGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
addUsersToGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
|
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -36,15 +36,16 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
const permissionToJoinGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS);
|
const permissionToJoinGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS);
|
||||||
const permissionToLeaveGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS);
|
const permissionToLeaveGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS);
|
||||||
const permissionToArchiveGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.DELETE_CUSTOM_GROUP);
|
const permissionToArchiveGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.DELETE_CUSTOM_GROUP);
|
||||||
|
const permissionToRestoreGroup = haveIGroupPermission(state, ownProps.groupId, Permissions.RESTORE_CUSTOM_GROUP);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
permissionToEditGroup,
|
permissionToEditGroup,
|
||||||
permissionToJoinGroup,
|
permissionToJoinGroup,
|
||||||
permissionToLeaveGroup,
|
permissionToLeaveGroup,
|
||||||
permissionToArchiveGroup,
|
permissionToArchiveGroup,
|
||||||
|
permissionToRestoreGroup,
|
||||||
isGroupMember,
|
isGroupMember,
|
||||||
group,
|
group,
|
||||||
currentUserId: getCurrentUserId(state),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
removeUsersFromGroup,
|
removeUsersFromGroup,
|
||||||
addUsersToGroup,
|
addUsersToGroup,
|
||||||
archiveGroup,
|
archiveGroup,
|
||||||
|
restoreGroup,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useCallback} from 'react';
|
||||||
import {Modal} from 'react-bootstrap';
|
import {Modal} from 'react-bootstrap';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
@ -24,8 +24,8 @@ export type Props = {
|
|||||||
permissionToJoinGroup: boolean;
|
permissionToJoinGroup: boolean;
|
||||||
permissionToLeaveGroup: boolean;
|
permissionToLeaveGroup: boolean;
|
||||||
permissionToArchiveGroup: boolean;
|
permissionToArchiveGroup: boolean;
|
||||||
|
permissionToRestoreGroup: boolean;
|
||||||
isGroupMember: boolean;
|
isGroupMember: boolean;
|
||||||
currentUserId: string;
|
|
||||||
incrementMemberCount: () => void;
|
incrementMemberCount: () => void;
|
||||||
decrementMemberCount: () => void;
|
decrementMemberCount: () => void;
|
||||||
actions: {
|
actions: {
|
||||||
@ -33,39 +33,50 @@ export type Props = {
|
|||||||
removeUsersFromGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
removeUsersFromGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
addUsersToGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
addUsersToGroup: (groupId: string, userIds: string[]) => Promise<ActionResult>;
|
||||||
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
archiveGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
|
restoreGroup: (groupId: string) => Promise<ActionResult>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewUserGroupModalHeader = (props: Props) => {
|
const ViewUserGroupModalHeader = ({
|
||||||
const goToAddPeopleModal = () => {
|
groupId,
|
||||||
const {actions, groupId} = props;
|
group,
|
||||||
|
onExited,
|
||||||
|
backButtonCallback,
|
||||||
|
backButtonAction,
|
||||||
|
permissionToEditGroup,
|
||||||
|
permissionToJoinGroup,
|
||||||
|
permissionToLeaveGroup,
|
||||||
|
permissionToArchiveGroup,
|
||||||
|
permissionToRestoreGroup,
|
||||||
|
isGroupMember,
|
||||||
|
incrementMemberCount,
|
||||||
|
decrementMemberCount,
|
||||||
|
actions,
|
||||||
|
}: Props) => {
|
||||||
|
const goToAddPeopleModal = useCallback(() => {
|
||||||
actions.openModal({
|
actions.openModal({
|
||||||
modalId: ModalIdentifiers.ADD_USERS_TO_GROUP,
|
modalId: ModalIdentifiers.ADD_USERS_TO_GROUP,
|
||||||
dialogType: AddUsersToGroupModal,
|
dialogType: AddUsersToGroupModal,
|
||||||
dialogProps: {
|
dialogProps: {
|
||||||
groupId,
|
groupId,
|
||||||
backButtonCallback: props.backButtonAction,
|
backButtonCallback: backButtonAction,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
props.onExited();
|
onExited();
|
||||||
};
|
}, [actions.openModal, groupId, onExited, backButtonAction]);
|
||||||
|
|
||||||
const showSubMenu = (source: string) => {
|
const restoreGroup = useCallback(async () => {
|
||||||
const {permissionToEditGroup, permissionToJoinGroup, permissionToLeaveGroup, permissionToArchiveGroup} = props;
|
await actions.restoreGroup(groupId);
|
||||||
|
}, [actions.restoreGroup, groupId]);
|
||||||
|
|
||||||
return source.toLowerCase() !== 'ldap' &&
|
const showSubMenu = useCallback(() => {
|
||||||
(
|
return permissionToEditGroup ||
|
||||||
permissionToEditGroup ||
|
|
||||||
permissionToJoinGroup ||
|
permissionToJoinGroup ||
|
||||||
permissionToLeaveGroup ||
|
permissionToLeaveGroup ||
|
||||||
permissionToArchiveGroup
|
permissionToArchiveGroup;
|
||||||
);
|
}, [permissionToEditGroup, permissionToJoinGroup, permissionToLeaveGroup, permissionToArchiveGroup]);
|
||||||
};
|
|
||||||
|
|
||||||
const modalTitle = () => {
|
|
||||||
const {group} = props;
|
|
||||||
|
|
||||||
|
const modalTitle = useCallback(() => {
|
||||||
if (group) {
|
if (group) {
|
||||||
return (
|
return (
|
||||||
<Modal.Title
|
<Modal.Title
|
||||||
@ -73,16 +84,18 @@ const ViewUserGroupModalHeader = (props: Props) => {
|
|||||||
id='userGroupsModalLabel'
|
id='userGroupsModalLabel'
|
||||||
>
|
>
|
||||||
{group.display_name}
|
{group.display_name}
|
||||||
|
{
|
||||||
|
group.delete_at > 0 &&
|
||||||
|
<i className='icon icon-archive-outline'/>
|
||||||
|
}
|
||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (<></>);
|
return (<></>);
|
||||||
};
|
}, [group]);
|
||||||
|
|
||||||
const addPeopleButton = () => {
|
const addPeopleButton = useCallback(() => {
|
||||||
const {group, permissionToJoinGroup} = props;
|
if (permissionToJoinGroup) {
|
||||||
|
|
||||||
if (group?.source.toLowerCase() !== 'ldap' && permissionToJoinGroup) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className='user-groups-create btn btn-md btn-primary'
|
className='user-groups-create btn btn-md btn-primary'
|
||||||
@ -90,47 +103,64 @@ const ViewUserGroupModalHeader = (props: Props) => {
|
|||||||
>
|
>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='user_groups_modal.addPeople'
|
id='user_groups_modal.addPeople'
|
||||||
defaultMessage='Add People'
|
defaultMessage='Add people'
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (<></>);
|
return (<></>);
|
||||||
};
|
}, [permissionToJoinGroup, goToAddPeopleModal]);
|
||||||
|
|
||||||
|
const restoreGroupButton = useCallback(() => {
|
||||||
|
if (permissionToRestoreGroup) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className='user-groups-create btn btn-md btn-primary'
|
||||||
|
onClick={restoreGroup}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user_groups_modal.button.restoreGroup'
|
||||||
|
defaultMessage='Restore Group'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (<></>);
|
||||||
|
}, [permissionToRestoreGroup, restoreGroup]);
|
||||||
|
|
||||||
const subMenuButton = () => {
|
const subMenuButton = () => {
|
||||||
const {group} = props;
|
if (group && showSubMenu()) {
|
||||||
|
|
||||||
if (group && showSubMenu(group?.source)) {
|
|
||||||
return (
|
return (
|
||||||
<ViewUserGroupHeaderSubMenu
|
<ViewUserGroupHeaderSubMenu
|
||||||
group={group}
|
group={group}
|
||||||
isGroupMember={props.isGroupMember}
|
isGroupMember={isGroupMember}
|
||||||
decrementMemberCount={props.decrementMemberCount}
|
decrementMemberCount={decrementMemberCount}
|
||||||
incrementMemberCount={props.incrementMemberCount}
|
incrementMemberCount={incrementMemberCount}
|
||||||
backButtonCallback={props.backButtonCallback}
|
backButtonCallback={backButtonCallback}
|
||||||
backButtonAction={props.backButtonAction}
|
backButtonAction={backButtonAction}
|
||||||
onExited={props.onExited}
|
onExited={onExited}
|
||||||
permissionToEditGroup={props.permissionToEditGroup}
|
permissionToEditGroup={permissionToEditGroup}
|
||||||
permissionToJoinGroup={props.permissionToJoinGroup}
|
permissionToJoinGroup={permissionToJoinGroup}
|
||||||
permissionToLeaveGroup={props.permissionToLeaveGroup}
|
permissionToLeaveGroup={permissionToLeaveGroup}
|
||||||
permissionToArchiveGroup={props.permissionToArchiveGroup}
|
permissionToArchiveGroup={permissionToArchiveGroup}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goBack = useCallback(() => {
|
||||||
|
backButtonCallback();
|
||||||
|
onExited();
|
||||||
|
}, [backButtonCallback, onExited]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header closeButton={true}>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='modal-header-back-button btn-icon'
|
className='modal-header-back-button btn-icon'
|
||||||
aria-label='Close'
|
aria-label='Close'
|
||||||
onClick={() => {
|
onClick={goBack}
|
||||||
props.backButtonCallback();
|
|
||||||
props.onExited();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<LocalizedIcon
|
<LocalizedIcon
|
||||||
className='icon icon-arrow-left'
|
className='icon icon-arrow-left'
|
||||||
@ -139,6 +169,7 @@ const ViewUserGroupModalHeader = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
{modalTitle()}
|
{modalTitle()}
|
||||||
{addPeopleButton()}
|
{addPeopleButton()}
|
||||||
|
{restoreGroupButton()}
|
||||||
{subMenuButton()}
|
{subMenuButton()}
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
);
|
);
|
||||||
|
@ -1665,7 +1665,7 @@
|
|||||||
"admin.permissions.permission.read_user_access_token.name": "Read user access token",
|
"admin.permissions.permission.read_user_access_token.name": "Read user access token",
|
||||||
"admin.permissions.permission.remove_user_from_team.description": "Remove user from team",
|
"admin.permissions.permission.remove_user_from_team.description": "Remove user from team",
|
||||||
"admin.permissions.permission.remove_user_from_team.name": "Remove user from team",
|
"admin.permissions.permission.remove_user_from_team.name": "Remove user from team",
|
||||||
"admin.permissions.permission.restore_custom_group.description": "Restore deleted user groups.",
|
"admin.permissions.permission.restore_custom_group.description": "Restore archived user groups.",
|
||||||
"admin.permissions.permission.restore_custom_group.name": "Restore",
|
"admin.permissions.permission.restore_custom_group.name": "Restore",
|
||||||
"admin.permissions.permission.revoke_user_access_token.description": "Revoke user access token",
|
"admin.permissions.permission.revoke_user_access_token.description": "Revoke user access token",
|
||||||
"admin.permissions.permission.revoke_user_access_token.name": "Revoke user access token",
|
"admin.permissions.permission.revoke_user_access_token.name": "Revoke user access token",
|
||||||
@ -4133,6 +4133,8 @@
|
|||||||
"no_results.pinned_posts.title": "No pinned posts yet",
|
"no_results.pinned_posts.title": "No pinned posts yet",
|
||||||
"no_results.user_group_members.subtitle": "There are currently no members in this group, please add one.",
|
"no_results.user_group_members.subtitle": "There are currently no members in this group, please add one.",
|
||||||
"no_results.user_group_members.title": "No members yet",
|
"no_results.user_group_members.title": "No members yet",
|
||||||
|
"no_results.user_groups.archived.subtitle": "Groups that are no longer relevant or are not being used can be archived",
|
||||||
|
"no_results.user_groups.archived.title": "No archived groups",
|
||||||
"no_results.user_groups.subtitle": "Groups are a custom collection of users that can be used for mentions and invites.",
|
"no_results.user_groups.subtitle": "Groups are a custom collection of users that can be used for mentions and invites.",
|
||||||
"no_results.user_groups.title": "No groups yet",
|
"no_results.user_groups.title": "No groups yet",
|
||||||
"notification.crt": "Reply in {title}",
|
"notification.crt": "Reply in {title}",
|
||||||
@ -5131,10 +5133,12 @@
|
|||||||
"user_group_popover.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
"user_group_popover.memberCount": "{member_count} {member_count, plural, one {Member} other {Members}}",
|
||||||
"user_group_popover.openGroupModal": "View full group info",
|
"user_group_popover.openGroupModal": "View full group info",
|
||||||
"user_group_popover.searchGroupMembers": "Search members",
|
"user_group_popover.searchGroupMembers": "Search members",
|
||||||
"user_groups_modal.addPeople": "Add People",
|
"user_groups_modal.addPeople": "Add people",
|
||||||
"user_groups_modal.addPeopleTitle": "Add people to {group}",
|
"user_groups_modal.addPeopleTitle": "Add people to {group}",
|
||||||
"user_groups_modal.allGroups": "All Groups",
|
"user_groups_modal.allGroups": "All Groups",
|
||||||
|
"user_groups_modal.archivedGroups": "Archived Groups",
|
||||||
"user_groups_modal.archiveGroup": "Archive Group",
|
"user_groups_modal.archiveGroup": "Archive Group",
|
||||||
|
"user_groups_modal.button.restoreGroup": "Restore Group",
|
||||||
"user_groups_modal.createNew": "Create Group",
|
"user_groups_modal.createNew": "Create Group",
|
||||||
"user_groups_modal.createTitle": "Create Group",
|
"user_groups_modal.createTitle": "Create Group",
|
||||||
"user_groups_modal.editDetails": "Edit Details",
|
"user_groups_modal.editDetails": "Edit Details",
|
||||||
@ -5153,8 +5157,10 @@
|
|||||||
"user_groups_modal.myGroups": "My Groups",
|
"user_groups_modal.myGroups": "My Groups",
|
||||||
"user_groups_modal.name": "Name",
|
"user_groups_modal.name": "Name",
|
||||||
"user_groups_modal.nameIsEmpty": "Name is a required field.",
|
"user_groups_modal.nameIsEmpty": "Name is a required field.",
|
||||||
|
"user_groups_modal.restoreGroup": "Restore Group",
|
||||||
"user_groups_modal.searchGroups": "Search Groups",
|
"user_groups_modal.searchGroups": "Search Groups",
|
||||||
"user_groups_modal.showAllGroups": "Show: All Groups",
|
"user_groups_modal.showAllGroups": "Show: All Groups",
|
||||||
|
"user_groups_modal.showArchivedGroups": "Show: Archived Groups",
|
||||||
"user_groups_modal.showMyGroups": "Show: My Groups",
|
"user_groups_modal.showMyGroups": "Show: My Groups",
|
||||||
"user_groups_modal.title": "User Groups",
|
"user_groups_modal.title": "User Groups",
|
||||||
"user_groups_modal.unknownError": "An unknown error has occurred.",
|
"user_groups_modal.unknownError": "An unknown error has occurred.",
|
||||||
|
@ -51,4 +51,6 @@ export default keyMirror({
|
|||||||
ARCHIVED_GROUP: null,
|
ARCHIVED_GROUP: null,
|
||||||
|
|
||||||
CREATED_GROUP_TEAMS_AND_CHANNELS: null,
|
CREATED_GROUP_TEAMS_AND_CHANNELS: null,
|
||||||
|
|
||||||
|
RESTORED_GROUP: null,
|
||||||
});
|
});
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
|
||||||
import {SyncableType} from '@mattermost/types/groups';
|
import {GetGroupsParams, SyncableType} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import * as Actions from 'mattermost-redux/actions/groups';
|
import * as Actions from 'mattermost-redux/actions/groups';
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@ -275,7 +275,12 @@ describe('Actions.Groups', () => {
|
|||||||
get('/groups?filter_allow_reference=true&page=0&per_page=0').
|
get('/groups?filter_allow_reference=true&page=0&per_page=0').
|
||||||
reply(200, response1.groups);
|
reply(200, response1.groups);
|
||||||
|
|
||||||
await Actions.getGroups('', true, 0, 0)(store.dispatch, store.getState);
|
const groupParams: GetGroupsParams = {
|
||||||
|
filter_allow_reference: true,
|
||||||
|
page: 0,
|
||||||
|
per_page: 0,
|
||||||
|
};
|
||||||
|
await Actions.getGroups(groupParams)(store.dispatch, store.getState);
|
||||||
|
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {General} from 'mattermost-redux/constants';
|
|||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
|
|
||||||
import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
import {ActionFunc, DispatchFunc, GetStateFunc} from 'mattermost-redux/types/actions';
|
||||||
import {GroupPatch, SyncableType, SyncablePatch, GroupCreateWithUserIds, CustomGroupPatch, GroupSearachParams, GroupSource} from '@mattermost/types/groups';
|
import {GroupPatch, SyncableType, SyncablePatch, GroupCreateWithUserIds, CustomGroupPatch, GroupSearchParams, GroupSource, GetGroupsParams, GetGroupsForUserParams} from '@mattermost/types/groups';
|
||||||
|
|
||||||
import {logError} from './errors';
|
import {logError} from './errors';
|
||||||
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
import {bindClientFunc, forceLogoutIfNecessary} from './helpers';
|
||||||
@ -156,19 +156,15 @@ export function getGroup(id: string, includeMemberCount = false): ActionFunc {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGroups(q = '', filterAllowReference = false, page = 0, perPage = 10, includeMemberCount = false): ActionFunc {
|
export function getGroups(opts: GetGroupsParams): ActionFunc {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: async (param1, param2, param3, param4, param5) => {
|
clientFunc: async (opts) => {
|
||||||
const result = await Client4.getGroups(param1, param2, param3, param4, param5);
|
const result = await Client4.getGroups(opts);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
onSuccess: [GroupTypes.RECEIVED_GROUPS],
|
onSuccess: [GroupTypes.RECEIVED_GROUPS],
|
||||||
params: [
|
params: [
|
||||||
q,
|
opts,
|
||||||
filterAllowReference,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
includeMemberCount,
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -303,19 +299,15 @@ export function getGroupsByUserId(userID: string): ActionFunc {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGroupsByUserIdPaginated(userId: string, filterAllowReference = false, page = 0, perPage: number = General.PAGE_SIZE_DEFAULT, includeMemberCount = false): ActionFunc {
|
export function getGroupsByUserIdPaginated(opts: GetGroupsForUserParams): ActionFunc {
|
||||||
return bindClientFunc({
|
return bindClientFunc({
|
||||||
clientFunc: async (param1, param2, param3, param4, param5) => {
|
clientFunc: async (opts) => {
|
||||||
const result = await Client4.getGroups(param1, param2, param3, param4, param5);
|
const result = await Client4.getGroups(opts);
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
onSuccess: [GroupTypes.RECEIVED_MY_GROUPS, GroupTypes.RECEIVED_GROUPS],
|
onSuccess: [GroupTypes.RECEIVED_MY_GROUPS, GroupTypes.RECEIVED_GROUPS],
|
||||||
params: [
|
params: [
|
||||||
filterAllowReference,
|
opts,
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
includeMemberCount,
|
|
||||||
userId,
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -392,7 +384,7 @@ export function removeUsersFromGroup(groupId: string, userIds: string[]): Action
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchGroups(params: GroupSearachParams): ActionFunc {
|
export function searchGroups(params: GroupSearchParams): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
@ -405,7 +397,7 @@ export function searchGroups(params: GroupSearachParams): ActionFunc {
|
|||||||
|
|
||||||
const dispatches: AnyAction[] = [{type: GroupTypes.RECEIVED_GROUPS, data}];
|
const dispatches: AnyAction[] = [{type: GroupTypes.RECEIVED_GROUPS, data}];
|
||||||
|
|
||||||
if (params.user_id) {
|
if (params.filter_has_member) {
|
||||||
dispatches.push({type: GroupTypes.RECEIVED_MY_GROUPS, data});
|
dispatches.push({type: GroupTypes.RECEIVED_MY_GROUPS, data});
|
||||||
}
|
}
|
||||||
if (params.include_channel_member_count) {
|
if (params.include_channel_member_count) {
|
||||||
@ -431,6 +423,29 @@ export function archiveGroup(groupId: string): ActionFunc {
|
|||||||
{
|
{
|
||||||
type: GroupTypes.ARCHIVED_GROUP,
|
type: GroupTypes.ARCHIVED_GROUP,
|
||||||
id: groupId,
|
id: groupId,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {data};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreGroup(groupId: string): ActionFunc {
|
||||||
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = await Client4.restoreGroup(groupId);
|
||||||
|
} catch (error) {
|
||||||
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
{
|
||||||
|
type: GroupTypes.RESTORED_GROUP,
|
||||||
|
id: groupId,
|
||||||
|
data,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ import {isCombinedUserActivityPost} from 'mattermost-redux/utils/post_list';
|
|||||||
|
|
||||||
import {General, Preferences, Posts} from 'mattermost-redux/constants';
|
import {General, Preferences, Posts} from 'mattermost-redux/constants';
|
||||||
|
|
||||||
import {getGroups} from 'mattermost-redux/actions/groups';
|
import {searchGroups} from 'mattermost-redux/actions/groups';
|
||||||
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
import {getProfilesByIds, getProfilesByUsernames, getStatusesByIds} from 'mattermost-redux/actions/users';
|
||||||
import {
|
import {
|
||||||
deletePreferences,
|
deletePreferences,
|
||||||
@ -1127,7 +1127,16 @@ export async function getMentionsAndStatusesForPosts(postsArrayOrMap: Post[]|Pos
|
|||||||
const loadedProfiles = new Set<string>((data || []).map((p) => p.username));
|
const loadedProfiles = new Set<string>((data || []).map((p) => p.username));
|
||||||
const groupsToCheck = Array.from(usernamesAndGroupsToLoad).filter((name) => !loadedProfiles.has(name));
|
const groupsToCheck = Array.from(usernamesAndGroupsToLoad).filter((name) => !loadedProfiles.has(name));
|
||||||
|
|
||||||
groupsToCheck.forEach((name) => promises.push(getGroups(name)(dispatch, getState)));
|
groupsToCheck.forEach((name) => {
|
||||||
|
const groupParams = {
|
||||||
|
q: name,
|
||||||
|
filter_allow_reference: true,
|
||||||
|
page: 0,
|
||||||
|
per_page: 60,
|
||||||
|
include_member_count: true,
|
||||||
|
};
|
||||||
|
promises.push(searchGroups(groupParams)(dispatch, getState));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
|
@ -164,8 +164,7 @@ function myGroups(state: string[] = [], action: GenericAction) {
|
|||||||
|
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
case GroupTypes.REMOVE_MY_GROUP:
|
case GroupTypes.REMOVE_MY_GROUP: {
|
||||||
case GroupTypes.ARCHIVED_GROUP: {
|
|
||||||
const groupId = action.id;
|
const groupId = action.id;
|
||||||
const index = state.indexOf(groupId);
|
const index = state.indexOf(groupId);
|
||||||
|
|
||||||
@ -203,6 +202,8 @@ function groups(state: Record<string, Group> = {}, action: GenericAction) {
|
|||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case GroupTypes.CREATE_GROUP_SUCCESS:
|
case GroupTypes.CREATE_GROUP_SUCCESS:
|
||||||
case GroupTypes.PATCHED_GROUP:
|
case GroupTypes.PATCHED_GROUP:
|
||||||
|
case GroupTypes.RESTORED_GROUP:
|
||||||
|
case GroupTypes.ARCHIVED_GROUP:
|
||||||
case GroupTypes.RECEIVED_GROUP: {
|
case GroupTypes.RECEIVED_GROUP: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -241,11 +242,6 @@ function groups(state: Record<string, Group> = {}, action: GenericAction) {
|
|||||||
|
|
||||||
return nextState;
|
return nextState;
|
||||||
}
|
}
|
||||||
case GroupTypes.ARCHIVED_GROUP: {
|
|
||||||
const nextState = {...state};
|
|
||||||
Reflect.deleteProperty(nextState, action.id);
|
|
||||||
return nextState;
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -208,13 +208,14 @@ describe('Selectors.Groups', () => {
|
|||||||
expect(Selectors.getGroupsAssociatedToChannelForReference(testState, channelID)).toEqual(expected);
|
expect(Selectors.getGroupsAssociatedToChannelForReference(testState, channelID)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAllAssociatedGroupsForReference', () => {
|
it('makeGetAllAssociatedGroupsForReference', () => {
|
||||||
const expected = [
|
const expected = [
|
||||||
group1,
|
group1,
|
||||||
group4,
|
group4,
|
||||||
group5,
|
group5,
|
||||||
];
|
];
|
||||||
expect(Selectors.getAllAssociatedGroupsForReference(testState)).toEqual(expected);
|
const getAllAssociatedGroupsForReference = Selectors.makeGetAllAssociatedGroupsForReference();
|
||||||
|
expect(getAllAssociatedGroupsForReference(testState, false)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getMyGroupMentionKeys', () => {
|
it('getMyGroupMentionKeys', () => {
|
||||||
@ -226,7 +227,7 @@ describe('Selectors.Groups', () => {
|
|||||||
key: `@${group4.name}`,
|
key: `@${group4.name}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(Selectors.getMyGroupMentionKeys(testState)).toEqual(expected);
|
expect(Selectors.getMyGroupMentionKeys(testState, false)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getMyGroupMentionKeysForChannel', () => {
|
it('getMyGroupMentionKeysForChannel', () => {
|
||||||
|
@ -130,7 +130,7 @@ export function getAssociatedGroupsForReference(state: GlobalState, teamId: stri
|
|||||||
} else if (channel && channel.group_constrained) {
|
} else if (channel && channel.group_constrained) {
|
||||||
groupsForReference = getGroupsAssociatedToChannelForReference(state, channelId);
|
groupsForReference = getGroupsAssociatedToChannelForReference(state, channelId);
|
||||||
} else {
|
} else {
|
||||||
groupsForReference = getAllAssociatedGroupsForReference(state);
|
groupsForReference = getAllAssociatedGroupsForReference(state, false);
|
||||||
}
|
}
|
||||||
return groupsForReference;
|
return groupsForReference;
|
||||||
}
|
}
|
||||||
@ -221,20 +221,29 @@ export const getGroupsAssociatedToChannelForReference: (state: GlobalState, chan
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getAllAssociatedGroupsForReference: (state: GlobalState) => Group[] = createSelector(
|
export const makeGetAllAssociatedGroupsForReference = () => {
|
||||||
'getAllAssociatedGroupsForReference',
|
return createSelector(
|
||||||
getAllGroups,
|
'makeGetAllAssociatedGroupsForReference',
|
||||||
getCurrentUserLocale,
|
(state: GlobalState) => getAllGroups(state),
|
||||||
(allGroups, locale) => {
|
(state: GlobalState) => getCurrentUserLocale(state),
|
||||||
const groups = Object.entries(allGroups).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0)).map((entry) => entry[1]);
|
(_: GlobalState, includeArchived: boolean) => includeArchived,
|
||||||
|
(allGroups, locale, includeArchived) => {
|
||||||
|
const groups = Object.entries(allGroups).filter((entry) => {
|
||||||
|
if (includeArchived) {
|
||||||
|
return entry[1].allow_reference;
|
||||||
|
}
|
||||||
|
return entry[1].allow_reference && entry[1].delete_at === 0;
|
||||||
|
}).map((entry) => entry[1]);
|
||||||
return sortGroups(groups, locale);
|
return sortGroups(groups, locale);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllAssociatedGroupsForReference = makeGetAllAssociatedGroupsForReference();
|
||||||
|
|
||||||
export const getAllGroupsForReferenceByName: (state: GlobalState) => Record<string, Group> = createSelector(
|
export const getAllGroupsForReferenceByName: (state: GlobalState) => Record<string, Group> = createSelector(
|
||||||
'getAllGroupsForReferenceByName',
|
'getAllGroupsForReferenceByName',
|
||||||
getAllAssociatedGroupsForReference,
|
(state: GlobalState) => getAllAssociatedGroupsForReference(state, false),
|
||||||
(groups) => {
|
(groups) => {
|
||||||
const groupsByName: Record<string, Group> = {};
|
const groupsByName: Record<string, Group> = {};
|
||||||
|
|
||||||
@ -249,16 +258,25 @@ export const getAllGroupsForReferenceByName: (state: GlobalState) => Record<stri
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getMyAllowReferencedGroups: (state: GlobalState) => Group[] = createSelector(
|
export const makeGetMyAllowReferencedGroups = () => {
|
||||||
'getMyAllowReferencedGroups',
|
return createSelector(
|
||||||
getMyGroups,
|
'makeGetMyAllowReferencedGroups',
|
||||||
getCurrentUserLocale,
|
(state: GlobalState) => getMyGroups(state),
|
||||||
(myGroups, locale) => {
|
(state: GlobalState) => getCurrentUserLocale(state),
|
||||||
const groups = myGroups.filter((group) => group.allow_reference && group.delete_at === 0);
|
(_: GlobalState, includeArchived: boolean) => includeArchived,
|
||||||
|
(myGroups, locale, includeArchived) => {
|
||||||
|
const groups = myGroups.filter((group) => {
|
||||||
|
if (includeArchived) {
|
||||||
|
return group.allow_reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
return group.allow_reference && group.delete_at === 0;
|
||||||
|
});
|
||||||
|
|
||||||
return sortGroups(groups, locale);
|
return sortGroups(groups, locale);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getMyGroupsAssociatedToChannelForReference: (state: GlobalState, teamId: string, channelId: string) => Group[] = createSelector(
|
export const getMyGroupsAssociatedToChannelForReference: (state: GlobalState, teamId: string, channelId: string) => Group[] = createSelector(
|
||||||
'getMyGroupsAssociatedToChannelForReference',
|
'getMyGroupsAssociatedToChannelForReference',
|
||||||
@ -269,9 +287,11 @@ export const getMyGroupsAssociatedToChannelForReference: (state: GlobalState, te
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getMyGroupMentionKeys: (state: GlobalState) => UserMentionKey[] = createSelector(
|
const getMyAllowReferencedGroups = makeGetMyAllowReferencedGroups();
|
||||||
|
|
||||||
|
export const getMyGroupMentionKeys: (state: GlobalState, includeArchived: boolean) => UserMentionKey[] = createSelector(
|
||||||
'getMyGroupMentionKeys',
|
'getMyGroupMentionKeys',
|
||||||
getMyAllowReferencedGroups,
|
(state: GlobalState, includeArchived: boolean) => getMyAllowReferencedGroups(state, includeArchived),
|
||||||
(groups: Group[]) => {
|
(groups: Group[]) => {
|
||||||
const keys: UserMentionKey[] = [];
|
const keys: UserMentionKey[] = [];
|
||||||
groups.forEach((group) => keys.push({key: `@${group.name}`}));
|
groups.forEach((group) => keys.push({key: `@${group.name}`}));
|
||||||
@ -289,20 +309,24 @@ export const getMyGroupMentionKeysForChannel: (state: GlobalState, teamId: strin
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const searchAllowReferencedGroups: (state: GlobalState, term: string) => Group[] = createSelector(
|
const searchGetAllAssociatedGroupsForReference = makeGetAllAssociatedGroupsForReference();
|
||||||
|
|
||||||
|
export const searchAllowReferencedGroups: (state: GlobalState, term: string, includeArchived: boolean) => Group[] = createSelector(
|
||||||
'searchAllowReferencedGroups',
|
'searchAllowReferencedGroups',
|
||||||
getAllAssociatedGroupsForReference,
|
|
||||||
(state: GlobalState, term: string) => term,
|
(state: GlobalState, term: string) => term,
|
||||||
(groups, term) => {
|
(state: GlobalState, term: string, includeArchived: boolean) => searchGetAllAssociatedGroupsForReference(state, includeArchived),
|
||||||
|
(term, groups) => {
|
||||||
return filterGroupsMatchingTerm(groups, term);
|
return filterGroupsMatchingTerm(groups, term);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const searchMyAllowReferencedGroups: (state: GlobalState, term: string) => Group[] = createSelector(
|
const searchGetMyAllowReferencedGroups = makeGetMyAllowReferencedGroups();
|
||||||
|
|
||||||
|
export const searchMyAllowReferencedGroups: (state: GlobalState, term: string, includeArchived: boolean) => Group[] = createSelector(
|
||||||
'searchMyAllowReferencedGroups',
|
'searchMyAllowReferencedGroups',
|
||||||
getMyAllowReferencedGroups,
|
|
||||||
(state: GlobalState, term: string) => term,
|
(state: GlobalState, term: string) => term,
|
||||||
(groups, term) => {
|
(state: GlobalState, term: string, includeArchived: boolean) => searchGetMyAllowReferencedGroups(state, includeArchived),
|
||||||
|
(term, groups) => {
|
||||||
return filterGroupsMatchingTerm(groups, term);
|
return filterGroupsMatchingTerm(groups, term);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -321,3 +345,24 @@ export const isMyGroup: (state: GlobalState, groupId: string) => boolean = creat
|
|||||||
return isMyGroup;
|
return isMyGroup;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getArchivedGroups: (state: GlobalState) => Group[] = createSelector(
|
||||||
|
'getArchivedGroups',
|
||||||
|
(state: GlobalState) => getAllGroups(state),
|
||||||
|
(state: GlobalState) => getCurrentUserLocale(state),
|
||||||
|
(allGroups, locale) => {
|
||||||
|
const groups = Object.entries(allGroups).filter((entry) => {
|
||||||
|
return entry[1].allow_reference && entry[1].delete_at > 0;
|
||||||
|
}).map((entry) => entry[1]);
|
||||||
|
return sortGroups(groups, locale);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const searchArchivedGroups: (state: GlobalState, term: string) => Group[] = createSelector(
|
||||||
|
'searchArchivedGroups',
|
||||||
|
getArchivedGroups,
|
||||||
|
(state: GlobalState, term: string) => term,
|
||||||
|
(groups, term) => {
|
||||||
|
return filterGroupsMatchingTerm(groups, term);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
@ -93,7 +93,7 @@ describe('Selectors.Roles', () => {
|
|||||||
test_channel_b_role2: {permissions: ['channel_b_role2']},
|
test_channel_b_role2: {permissions: ['channel_b_role2']},
|
||||||
test_channel_c_role1: {permissions: ['channel_c_role1']},
|
test_channel_c_role1: {permissions: ['channel_c_role1']},
|
||||||
test_channel_c_role2: {permissions: ['channel_c_role2']},
|
test_channel_c_role2: {permissions: ['channel_c_role2']},
|
||||||
test_user_role2: {permissions: ['user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP]},
|
test_user_role2: {permissions: ['user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP, Permissions.RESTORE_CUSTOM_GROUP]},
|
||||||
custom_group_user: {permissions: ['custom_group_user']},
|
custom_group_user: {permissions: ['custom_group_user']},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -102,6 +102,8 @@ describe('Selectors.Roles', () => {
|
|||||||
const group3 = TestHelper.fakeGroup('group3', 'custom');
|
const group3 = TestHelper.fakeGroup('group3', 'custom');
|
||||||
const group4 = TestHelper.fakeGroup('group4', 'custom');
|
const group4 = TestHelper.fakeGroup('group4', 'custom');
|
||||||
const group5 = TestHelper.fakeGroup('group5');
|
const group5 = TestHelper.fakeGroup('group5');
|
||||||
|
const group6 = TestHelper.fakeGroup('group6', 'custom');
|
||||||
|
group6.delete_at = 10000;
|
||||||
|
|
||||||
const groups: Record<string, Group> = {};
|
const groups: Record<string, Group> = {};
|
||||||
groups.group1 = group1;
|
groups.group1 = group1;
|
||||||
@ -109,6 +111,7 @@ describe('Selectors.Roles', () => {
|
|||||||
groups.group3 = group3;
|
groups.group3 = group3;
|
||||||
groups.group4 = group4;
|
groups.group4 = group4;
|
||||||
groups.group5 = group5;
|
groups.group5 = group5;
|
||||||
|
groups.group6 = group6;
|
||||||
|
|
||||||
const testState = deepFreezeAndThrowOnMutation({
|
const testState = deepFreezeAndThrowOnMutation({
|
||||||
entities: {
|
entities: {
|
||||||
@ -164,7 +167,7 @@ describe('Selectors.Roles', () => {
|
|||||||
test_channel_b_role2: {permissions: ['channel_b_role2']},
|
test_channel_b_role2: {permissions: ['channel_b_role2']},
|
||||||
test_channel_c_role1: {permissions: ['channel_c_role1']},
|
test_channel_c_role1: {permissions: ['channel_c_role1']},
|
||||||
test_channel_c_role2: {permissions: ['channel_c_role2']},
|
test_channel_c_role2: {permissions: ['channel_c_role2']},
|
||||||
test_user_role2: {permissions: ['user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP]},
|
test_user_role2: {permissions: ['user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP, Permissions.RESTORE_CUSTOM_GROUP]},
|
||||||
custom_group_user: {permissions: ['custom_group_user']},
|
custom_group_user: {permissions: ['custom_group_user']},
|
||||||
};
|
};
|
||||||
expect(getRoles(testState)).toEqual(loadedRoles);
|
expect(getRoles(testState)).toEqual(loadedRoles);
|
||||||
@ -172,7 +175,7 @@ describe('Selectors.Roles', () => {
|
|||||||
|
|
||||||
it('should return my system permission on getMySystemPermissions', () => {
|
it('should return my system permission on getMySystemPermissions', () => {
|
||||||
expect(getMySystemPermissions(testState)).toEqual(new Set([
|
expect(getMySystemPermissions(testState)).toEqual(new Set([
|
||||||
'user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP,
|
'user_role2', Permissions.EDIT_CUSTOM_GROUP, Permissions.CREATE_CUSTOM_GROUP, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS, Permissions.DELETE_CUSTOM_GROUP, Permissions.RESTORE_CUSTOM_GROUP,
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,15 +273,17 @@ describe('Selectors.Roles', () => {
|
|||||||
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.CREATE_CUSTOM_GROUP)).toEqual(true);
|
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.CREATE_CUSTOM_GROUP)).toEqual(true);
|
||||||
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS)).toEqual(true);
|
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.MANAGE_CUSTOM_GROUP_MEMBERS)).toEqual(true);
|
||||||
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.DELETE_CUSTOM_GROUP)).toEqual(true);
|
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.DELETE_CUSTOM_GROUP)).toEqual(true);
|
||||||
|
expect(Selectors.haveIGroupPermission(newState, group2.id, Permissions.RESTORE_CUSTOM_GROUP)).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return group set with permissions on getGroupListPermissions', () => {
|
it('should return group set with permissions on getGroupListPermissions', () => {
|
||||||
expect(Selectors.getGroupListPermissions(testState)).toEqual({
|
expect(Selectors.getGroupListPermissions(testState)).toEqual({
|
||||||
[group1.id]: {can_delete: true, can_manage_members: true},
|
[group1.id]: {can_delete: true, can_manage_members: true, can_restore: false},
|
||||||
[group2.id]: {can_delete: true, can_manage_members: true},
|
[group2.id]: {can_delete: true, can_manage_members: true, can_restore: false},
|
||||||
[group3.id]: {can_delete: true, can_manage_members: true},
|
[group3.id]: {can_delete: true, can_manage_members: true, can_restore: false},
|
||||||
[group4.id]: {can_delete: true, can_manage_members: true},
|
[group4.id]: {can_delete: true, can_manage_members: true, can_restore: false},
|
||||||
[group5.id]: {can_delete: false, can_manage_members: false},
|
[group5.id]: {can_delete: false, can_manage_members: false, can_restore: false},
|
||||||
|
[group6.id]: {can_delete: false, can_manage_members: false, can_restore: true},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -60,7 +60,7 @@ export const getGroupListPermissions: (state: GlobalState) => Record<string, Gro
|
|||||||
getMySystemPermissions,
|
getMySystemPermissions,
|
||||||
(state) => state.entities.groups.groups,
|
(state) => state.entities.groups.groups,
|
||||||
(myGroupRoles, roles, systemPermissions, allGroups) => {
|
(myGroupRoles, roles, systemPermissions, allGroups) => {
|
||||||
const groups = Object.entries(allGroups).filter((entry) => (entry[1].allow_reference && entry[1].delete_at === 0)).map((entry) => entry[1]);
|
const groups = Object.entries(allGroups).filter((entry) => (entry[1].allow_reference)).map((entry) => entry[1]);
|
||||||
|
|
||||||
const permissions = new Set<string>();
|
const permissions = new Set<string>();
|
||||||
groups.forEach((group) => {
|
groups.forEach((group) => {
|
||||||
@ -83,8 +83,9 @@ export const getGroupListPermissions: (state: GlobalState) => Record<string, Gro
|
|||||||
const groupPermissionsMap: Record<string, GroupPermissions> = {};
|
const groupPermissionsMap: Record<string, GroupPermissions> = {};
|
||||||
groups.forEach((g) => {
|
groups.forEach((g) => {
|
||||||
groupPermissionsMap[g.id] = {
|
groupPermissionsMap[g.id] = {
|
||||||
can_delete: permissions.has(Permissions.DELETE_CUSTOM_GROUP) && g.source.toLowerCase() !== 'ldap',
|
can_delete: permissions.has(Permissions.DELETE_CUSTOM_GROUP) && g.source.toLowerCase() !== 'ldap' && g.delete_at === 0,
|
||||||
can_manage_members: permissions.has(Permissions.MANAGE_CUSTOM_GROUP_MEMBERS) && g.source.toLowerCase() !== 'ldap',
|
can_manage_members: permissions.has(Permissions.MANAGE_CUSTOM_GROUP_MEMBERS) && g.source.toLowerCase() !== 'ldap' && g.delete_at === 0,
|
||||||
|
can_restore: permissions.has(Permissions.RESTORE_CUSTOM_GROUP) && g.source.toLowerCase() !== 'ldap' && g.delete_at !== 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return groupPermissionsMap;
|
return groupPermissionsMap;
|
||||||
@ -175,12 +176,34 @@ export function haveITeamPermission(state: GlobalState, teamId: string, permissi
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function haveIGroupPermission(state: GlobalState, groupID: string, permission: string): boolean {
|
export const haveIGroupPermission: (state: GlobalState, groupID: string, permission: string) => boolean = createSelector(
|
||||||
return (
|
'haveIGroupPermission',
|
||||||
getMySystemPermissions(state).has(permission) ||
|
getMySystemPermissions,
|
||||||
(getMyPermissionsByGroup(state)[groupID] ? getMyPermissionsByGroup(state)[groupID].has(permission) : false)
|
getMyPermissionsByGroup,
|
||||||
);
|
(state: GlobalState, groupID: string) => state.entities.groups.groups[groupID],
|
||||||
|
(state: GlobalState, groupID: string, permission: string) => permission,
|
||||||
|
(systemPermissions, permissionGroups, group, permission) => {
|
||||||
|
if (permission === Permissions.RESTORE_CUSTOM_GROUP) {
|
||||||
|
if ((group.source !== 'ldap' && group.delete_at !== 0) && (systemPermissions.has(permission) || (permissionGroups[group.id] && permissionGroups[group.id].has(permission)))) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.source === 'ldap' || group.delete_at !== 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemPermissions.has(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGroups[group.id] && permissionGroups[group.id].has(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function haveIChannelPermission(state: GlobalState, teamId: string, channelId: string, permission: string): boolean {
|
export function haveIChannelPermission(state: GlobalState, teamId: string, channelId: string, permission: string): boolean {
|
||||||
return (
|
return (
|
||||||
|
@ -20,7 +20,7 @@ export const getCurrentSearchForCurrentTeam: (state: GlobalState) => string = cr
|
|||||||
export const getAllUserMentionKeys: (state: GlobalState) => UserMentionKey[] = createSelector(
|
export const getAllUserMentionKeys: (state: GlobalState) => UserMentionKey[] = createSelector(
|
||||||
'getAllUserMentionKeys',
|
'getAllUserMentionKeys',
|
||||||
getCurrentUserMentionKeys,
|
getCurrentUserMentionKeys,
|
||||||
getMyGroupMentionKeys,
|
(state: GlobalState) => getMyGroupMentionKeys(state, false),
|
||||||
(userMentionKeys, groupMentionKeys) => {
|
(userMentionKeys, groupMentionKeys) => {
|
||||||
return userMentionKeys.concat(groupMentionKeys);
|
return userMentionKeys.concat(groupMentionKeys);
|
||||||
},
|
},
|
||||||
|
@ -34,6 +34,16 @@ export function filterGroupsMatchingTerm(groups: Group[], term: string): Group[]
|
|||||||
|
|
||||||
export function sortGroups(groups: Group[] = [], locale: string = General.DEFAULT_LOCALE): Group[] {
|
export function sortGroups(groups: Group[] = [], locale: string = General.DEFAULT_LOCALE): Group[] {
|
||||||
return groups.sort((a, b) => {
|
return groups.sort((a, b) => {
|
||||||
|
if ((a.delete_at === 0 && b.delete_at === 0) || (a.delete_at > 0 && b.delete_at > 0)) {
|
||||||
return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
|
return a.display_name.localeCompare(b.display_name, locale, {numeric: true});
|
||||||
|
}
|
||||||
|
if (a.delete_at < b.delete_at) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (a.delete_at > b.delete_at) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -74,8 +74,10 @@ import {
|
|||||||
UsersWithGroupsAndCount,
|
UsersWithGroupsAndCount,
|
||||||
GroupsWithCount,
|
GroupsWithCount,
|
||||||
GroupCreateWithUserIds,
|
GroupCreateWithUserIds,
|
||||||
GroupSearachParams,
|
GroupSearchParams,
|
||||||
CustomGroupPatch,
|
CustomGroupPatch,
|
||||||
|
GetGroupsParams,
|
||||||
|
GetGroupsForUserParams,
|
||||||
} from '@mattermost/types/groups';
|
} from '@mattermost/types/groups';
|
||||||
import {PostActionResponse} from '@mattermost/types/integration_actions';
|
import {PostActionResponse} from '@mattermost/types/integration_actions';
|
||||||
import {
|
import {
|
||||||
@ -3502,20 +3504,9 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getGroups = (q = '', filterAllowReference = false, page = 0, perPage = 10, includeMemberCount = false, hasFilterMember = false) => {
|
getGroups = (opts: GetGroupsForUserParams | GetGroupsParams) => {
|
||||||
const qs: any = {
|
|
||||||
q,
|
|
||||||
filter_allow_reference: filterAllowReference,
|
|
||||||
page,
|
|
||||||
per_page: perPage,
|
|
||||||
include_member_count: includeMemberCount,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (hasFilterMember) {
|
|
||||||
qs.filter_has_member = hasFilterMember;
|
|
||||||
}
|
|
||||||
return this.doFetch<Group[]>(
|
return this.doFetch<Group[]>(
|
||||||
`${this.getGroupsRoute()}${buildQueryString(qs)}`,
|
`${this.getGroupsRoute()}${buildQueryString(opts)}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -3573,7 +3564,7 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchGroups = (params: GroupSearachParams) => {
|
searchGroups = (params: GroupSearchParams) => {
|
||||||
return this.doFetch<Group[]>(
|
return this.doFetch<Group[]>(
|
||||||
`${this.getGroupsRoute()}${buildQueryString(params)}`,
|
`${this.getGroupsRoute()}${buildQueryString(params)}`,
|
||||||
{method: 'get'},
|
{method: 'get'},
|
||||||
@ -3676,6 +3667,13 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreGroup = (groupId: string) => {
|
||||||
|
return this.doFetch<Group>(
|
||||||
|
`${this.getGroupRoute(groupId)}/restore`,
|
||||||
|
{method: 'post'},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
createGroupTeamsAndChannels = (userID: string) => {
|
createGroupTeamsAndChannels = (userID: string) => {
|
||||||
return this.doFetch<Group>(
|
return this.doFetch<Group>(
|
||||||
`${this.getBaseRoute()}/ldap/users/${userID}/group_sync_memberships`,
|
`${this.getBaseRoute()}/ldap/users/${userID}/group_sync_memberships`,
|
||||||
|
@ -150,13 +150,22 @@ export type GroupCreateWithUserIds = {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupSearachParams = {
|
export type GetGroupsParams = {
|
||||||
|
filter_allow_reference?: boolean;
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
include_member_count?: boolean;
|
||||||
|
include_archived?: boolean;
|
||||||
|
filter_archived?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GetGroupsForUserParams = GetGroupsParams & {
|
||||||
|
filter_has_member: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GroupSearchParams = GetGroupsParams & {
|
||||||
q: string;
|
q: string;
|
||||||
filter_allow_reference: boolean;
|
filter_has_member?: string;
|
||||||
page: number;
|
|
||||||
per_page: number;
|
|
||||||
include_member_count: boolean;
|
|
||||||
user_id?: string;
|
|
||||||
include_timezones?: string;
|
include_timezones?: string;
|
||||||
include_channel_member_count?: string;
|
include_channel_member_count?: string;
|
||||||
}
|
}
|
||||||
@ -169,4 +178,5 @@ export type GroupMembership = {
|
|||||||
export type GroupPermissions = {
|
export type GroupPermissions = {
|
||||||
can_delete: boolean;
|
can_delete: boolean;
|
||||||
can_manage_members: boolean;
|
can_manage_members: boolean;
|
||||||
|
can_restore: boolean;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user