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>
Этот коммит содержится в:
@@ -986,14 +986,19 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
|
||||
|
||||
// Include archived groups
|
||||
includeArchived := r.URL.Query().Get("include_archived") == "true"
|
||||
|
||||
opts := model.GroupSearchOpts{
|
||||
Q: c.Params.Q,
|
||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||
FilterAllowReference: c.Params.FilterAllowReference,
|
||||
FilterArchived: c.Params.FilterArchived,
|
||||
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
|
||||
Source: source,
|
||||
FilterHasMember: c.Params.FilterHasMember,
|
||||
IncludeTimezones: includeTimezones,
|
||||
IncludeArchived: includeArchived,
|
||||
}
|
||||
|
||||
if teamID != "" {
|
||||
@@ -1145,15 +1150,19 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
defer c.LogAuditRec(auditRec)
|
||||
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 {
|
||||
c.Err = err
|
||||
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()
|
||||
|
||||
ReturnStatusOK(w)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
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)
|
||||
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 {
|
||||
c.Err = err
|
||||
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) {
|
||||
@@ -1223,13 +1237,13 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
||||
if appErr != nil {
|
||||
appErr.Where = "Api4.deleteGroup"
|
||||
appErr.Where = "Api4.addGroupMembers"
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
@@ -1282,13 +1296,13 @@ func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
appErr = licensedAndConfiguredForGroupBySource(c.App, model.GroupSourceCustom)
|
||||
if appErr != nil {
|
||||
appErr.Where = "Api4.deleteGroup"
|
||||
appErr.Where = "Api4.deleteGroupMembers"
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1291,6 +1291,21 @@ func TestGetGroups(t *testing.T) {
|
||||
// make sure it returned th.Group,not group
|
||||
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
|
||||
groups, _, err = th.Client.GetGroups(context.Background(), opts)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -384,9 +384,11 @@ func (s *SqlGroupStore) Delete(groupID string) (*model.Group, error) {
|
||||
}
|
||||
|
||||
time := model.GetMillis()
|
||||
group.DeleteAt = time
|
||||
group.UpdateAt = time
|
||||
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
time := model.GetMillis()
|
||||
group.UpdateAt = model.GetMillis()
|
||||
group.DeleteAt = 0
|
||||
if _, err := s.GetMasterX().Exec(`UPDATE UserGroups
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1570,17 +1573,27 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts,
|
||||
}
|
||||
|
||||
groupsQuery = groupsQuery.
|
||||
From("UserGroups g").
|
||||
OrderBy("g.DisplayName")
|
||||
From("UserGroups g")
|
||||
|
||||
if opts.Since > 0 {
|
||||
groupsQuery = groupsQuery.Where(sq.Gt{
|
||||
"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")
|
||||
}
|
||||
|
||||
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 {
|
||||
groupsQuery = groupsQuery.
|
||||
Limit(uint64(perPage)).
|
||||
|
||||
@@ -3962,6 +3962,26 @@ func testGetGroups(t *testing.T, ss store.Store) {
|
||||
},
|
||||
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 {
|
||||
|
||||
@@ -83,6 +83,7 @@ type Params struct {
|
||||
IncludeTotalCount bool
|
||||
IncludeDeleted bool
|
||||
FilterAllowReference bool
|
||||
FilterArchived bool
|
||||
FilterParentTeamPermitted bool
|
||||
CategoryId string
|
||||
WarnMetricId string
|
||||
@@ -208,6 +209,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
||||
params.NotAssociatedToTeam = query.Get("not_associated_to_team")
|
||||
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
|
||||
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.IncludeChannelMemberCount = query.Get("include_channel_member_count")
|
||||
|
||||
|
||||
@@ -5510,7 +5510,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(ctx context.Context, teamI
|
||||
// GetGroups retrieves Mattermost Groups
|
||||
func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group, *Response, error) {
|
||||
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(),
|
||||
opts.IncludeMemberCount,
|
||||
opts.NotAssociatedToTeam,
|
||||
@@ -5521,6 +5521,8 @@ func (c *Client4) GetGroups(ctx context.Context, opts GroupSearchOpts) ([]*Group
|
||||
opts.Source,
|
||||
opts.IncludeChannelMemberCount,
|
||||
opts.IncludeTimezones,
|
||||
opts.IncludeArchived,
|
||||
opts.FilterArchived,
|
||||
)
|
||||
if opts.Since > 0 {
|
||||
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
|
||||
|
||||
@@ -133,6 +133,12 @@ type GroupSearchOpts struct {
|
||||
|
||||
IncludeChannelMemberCount string
|
||||
IncludeTimezones bool
|
||||
|
||||
// Include archived groups
|
||||
IncludeArchived bool
|
||||
|
||||
// Only return archived groups
|
||||
FilterArchived bool
|
||||
}
|
||||
|
||||
type GetGroupOpts struct {
|
||||
|
||||
Ссылка в новой задаче
Block a user