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>
Этот коммит содержится в:
Ben Cooke
2023-08-31 10:07:51 -04:00
коммит произвёл GitHub
родитель 32512d35fb
Коммит 2c6179a0a6
44 изменённых файлов: 1031 добавлений и 1019 удалений

Просмотреть файл

@@ -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&not_associated_to_team=%v&not_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&not_associated_to_team=%v&not_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 {