mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-07 03:35:08 -05:00
Reporting config apis (#33378)
* Added enable/disable setting and feature flag * added rest of notifgication settings * Added backend for content flagging setting and populated notification values from server side defaults * WIP user selector * Added common reviewers UI * Added additonal reviewers section * WIP * WIP * Team table base * Added search in teams * Added search in teams * Added additional settings section * WIP * Inbtegrated reviewers settings * WIP * WIP * Added server side validation * cleanup * cleanup * [skip ci] * Some refactoring * type fixes * lint fix * test: add content flagging settings test file * test: add comprehensive unit tests for content flagging settings * enhanced tests * test: add test file for content flagging additional settings * test: add comprehensive unit tests for ContentFlaggingAdditionalSettingsSection * Added additoonal settings test * test: add empty test file for team reviewers section * test: add comprehensive unit tests for TeamReviewersSection component * test: update tests to handle async data fetching in team reviewers section * test: add empty test file for content reviewers component * feat: add comprehensive unit tests for ContentFlaggingContentReviewers component * Added ContentFlaggingContentReviewersContentFlaggingContentReviewers test * test: add notification settings test file for content flagging * test: add comprehensive unit tests for content flagging notification settings * Added ContentFlaggingNotificationSettingsSection tests * test: add user profile pill test file * test: add comprehensive unit tests for UserProfilePill component * refactor: Replace enzyme shallow with renderWithContext in user_profile_pill tests * Added UserProfilePill tests * test: add empty test file for content reviewers team option * test: add comprehensive unit tests for TeamOptionComponent * Added TeamOptionComponent tests * test: add empty test file for reason_option component * test: add comprehensive unit tests for ReasonOption component * Added ReasonOption tests * cleanup * Fixed i18n error * fixed e2e test lijnt issues * Updated test cases * Added snaoshot * Updated snaoshot * lint fix * lint fix * review fixes * updated snapshot * CI * Added base APIs * Fetched team status data on load and team switch * WIP * Review fixes * wip * WIP * Removed an test, updated comment * CI * Added tests * Added tests * Lint fix * Added API specs * Fixed types * CI fixes * API tests * lint fixes * Set env variable so API routes are regiustered * Test update * term renaming and disabling API tests on MySQL * typo * Updated store type definition * Minor tweaks * Updated tests and docs * finction rename * Updated tests * refactor * lint fix * Removed unnecesseery nil check * Updated error code order in API docs
This commit is contained in:
@@ -61,6 +61,7 @@ build-v4: node_modules playbooks
|
||||
@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/audit_logging.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/access_control.yaml >> $(V4_YAML)
|
||||
@cat $(V4_SRC)/content_flagging.yaml >> $(V4_YAML)
|
||||
@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
|
||||
@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
|
||||
@echo Extracting code samples
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/api/v4/content_flagging/flag/config:
|
||||
get:
|
||||
summary: Get content flagging configuration
|
||||
description: |
|
||||
Returns the configuration for content flagging, including the list of available reasons for flagging content. This data is used to gather details from the user when they flag content.
|
||||
An enterprise advanced license is required.
|
||||
tags:
|
||||
- Content Flagging
|
||||
responses:
|
||||
'200':
|
||||
description: Configuration retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
reasons:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of reasons for flagging content
|
||||
reporter_comment_required:
|
||||
type: boolean
|
||||
description: Indicates if a comment from the reporter is required when flagging content
|
||||
'404':
|
||||
description: Feature is disabled via the feature flag.
|
||||
'500':
|
||||
description: Internal server error.
|
||||
'501':
|
||||
description: Feature is disabled either via config or an Enterprise Advanced license is not available.
|
||||
/api/v4/content_flagging/team/{team_id}/status:
|
||||
get:
|
||||
summary: Get content flagging status for a team
|
||||
description: |
|
||||
Returns the content flagging status for a specific team, indicating whether content flagging is enabled on the specified team or not.
|
||||
tags:
|
||||
- Content Flagging
|
||||
parameters:
|
||||
- in: path
|
||||
name: team_id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the team to retrieve the content flagging status for
|
||||
responses:
|
||||
'200':
|
||||
description: Content flagging status retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Indicates if content flagging is enabled for the team
|
||||
'403':
|
||||
description: Forbidden - User does not have permission to access this team.
|
||||
'404':
|
||||
description: The specified team was not found or the feature is disabled via the feature flag.
|
||||
'500':
|
||||
description: Internal server error.
|
||||
'501':
|
||||
description: Feature is disabled either via config or an Enterprise Advanced license is not available.
|
||||
@@ -162,6 +162,8 @@ type Routes struct {
|
||||
|
||||
AccessControlPolicies *mux.Router // 'api/v4/access_control_policies'
|
||||
AccessControlPolicy *mux.Router // 'api/v4/access_control_policies/{policy_id:[A-Za-z0-9]+}'
|
||||
|
||||
ContentFlagging *mux.Router // 'api/v4/content_flagging'
|
||||
}
|
||||
|
||||
type API struct {
|
||||
@@ -310,6 +312,8 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.BaseRoutes.AccessControlPolicies = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies").Subrouter()
|
||||
api.BaseRoutes.AccessControlPolicy = api.BaseRoutes.APIRoot.PathPrefix("/access_control_policies/{policy_id:[A-Za-z0-9]+}").Subrouter()
|
||||
|
||||
api.BaseRoutes.ContentFlagging = api.BaseRoutes.APIRoot.PathPrefix("/content_flagging").Subrouter()
|
||||
|
||||
api.InitUser()
|
||||
api.InitBot()
|
||||
api.InitTeam()
|
||||
@@ -363,6 +367,7 @@ func Init(srv *app.Server) (*API, error) {
|
||||
api.InitCustomProfileAttributes()
|
||||
api.InitAuditLogging()
|
||||
api.InitAccessControlPolicy()
|
||||
api.InitContentFlagging()
|
||||
|
||||
// If we allow testing then listen for manual testing URL hits
|
||||
if *srv.Config().ServiceSettings.EnableTesting {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/mattermost/server/v8/channels/app"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||
)
|
||||
|
||||
func (api *API) InitContentFlagging() {
|
||||
if !api.srv.Config().FeatureFlags.ContentFlagging {
|
||||
return
|
||||
}
|
||||
|
||||
api.BaseRoutes.ContentFlagging.Handle("/flag/config", api.APISessionRequired(getFlaggingConfiguration)).Methods(http.MethodGet)
|
||||
api.BaseRoutes.ContentFlagging.Handle("/team/{team_id:[A-Za-z0-9]+}/status", api.APISessionRequired(getTeamPostFlaggingFeatureStatus)).Methods(http.MethodGet)
|
||||
}
|
||||
|
||||
func requireContentFlaggingEnabled(c *Context) {
|
||||
if !model.MinimumEnterpriseAdvancedLicense(c.App.License()) {
|
||||
c.Err = model.NewAppError("requireContentFlaggingEnabled", "api.content_flagging.error.license", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
contentFlaggingEnabled := c.App.Config().ContentFlaggingSettings.EnableContentFlagging
|
||||
if contentFlaggingEnabled == nil || !*contentFlaggingEnabled {
|
||||
c.Err = model.NewAppError("requireContentFlaggingEnabled", "api.content_flagging.error.disabled", nil, "", http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getFlaggingConfiguration(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
config := getFlaggingConfig(c.App.Config().ContentFlaggingSettings)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(config); err != nil {
|
||||
mlog.Error("failed to encode content flagging configuration to return API response", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getTeamPostFlaggingFeatureStatus(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
requireContentFlaggingEnabled(c)
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
teamID := c.Params.TeamId
|
||||
if !c.App.SessionHasPermissionToTeam(*c.AppContext.Session(), teamID, model.PermissionViewTeam) {
|
||||
c.SetPermissionError(model.PermissionViewTeam)
|
||||
return
|
||||
}
|
||||
|
||||
enabled := app.ContentFlaggingEnabledForTeam(c.App.Config(), teamID)
|
||||
|
||||
payload := map[string]bool{
|
||||
"enabled": enabled,
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
mlog.Error("failed to encode content flagging configuration to return API response", mlog.Err(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getFlaggingConfig(contentFlaggingSettings model.ContentFlaggingSettings) *model.ContentFlaggingReportingConfig {
|
||||
return &model.ContentFlaggingReportingConfig{
|
||||
Reasons: contentFlaggingSettings.AdditionalSettings.Reasons,
|
||||
ReporterCommentRequired: contentFlaggingSettings.AdditionalSettings.ReporterCommentRequired,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetFlaggingConfiguration(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
if *mainHelper.GetSQLSettings().DriverName == model.DatabaseDriverMysql {
|
||||
t.Skip("Content flagging tests are not supported on MySQL")
|
||||
}
|
||||
|
||||
os.Setenv("MM_FEATUREFLAGS_ContentFlagging", "true")
|
||||
th := Setup(t)
|
||||
defer func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ContentFlagging")
|
||||
}()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetFlaggingConfiguration(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetFlaggingConfiguration(context.Background())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetTeamPostReportingFeatureStatus(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
if *mainHelper.GetSQLSettings().DriverName == model.DatabaseDriverMysql {
|
||||
t.Skip("Content flagging tests are not supported on MySQL")
|
||||
}
|
||||
|
||||
os.Setenv("MM_FEATUREFLAGS_ContentFlagging", "true")
|
||||
th := Setup(t)
|
||||
defer func() {
|
||||
th.TearDown()
|
||||
os.Unsetenv("MM_FEATUREFLAGS_ContentFlagging")
|
||||
}()
|
||||
|
||||
client := th.Client
|
||||
|
||||
t.Run("Should return 501 when Enterprise Advanced license is not present even if feature is enabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return 501 when feature is disabled", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
})
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), model.NewId())
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
})
|
||||
|
||||
t.Run("Should return Forbidden error when calling for a team without the team membership", func(t *testing.T) {
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
th.App.UpdateConfig(func(config *model.Config) {
|
||||
config.ContentFlaggingSettings.EnableContentFlagging = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.SetDefaults()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewers = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewerIds = &[]string{"reviewer_user_id_1", "reviewer_user_id_2"}
|
||||
})
|
||||
|
||||
// using basic user because the default user is a system admin, and they have
|
||||
// access to all teams even without being an explicit team member
|
||||
th.LoginBasic()
|
||||
team := th.CreateTeam()
|
||||
// unlinking from the created team as by default the team's creator is
|
||||
// a team member, so we need to leave the team explicitly
|
||||
th.UnlinkUserFromTeam(th.BasicUser, team)
|
||||
|
||||
status, resp, err := client.GetTeamPostFlaggingFeatureStatus(context.Background(), team.Id)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
require.Nil(t, status)
|
||||
|
||||
// now we will join the team and that will allow us to call the endpoint without error
|
||||
th.LinkUserToTeam(th.BasicUser, team)
|
||||
status, resp, err = client.GetTeamPostFlaggingFeatureStatus(context.Background(), team.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.True(t, status["enabled"])
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import "github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
func ContentFlaggingEnabledForTeam(config *model.Config, teamId string) bool {
|
||||
reviewerSettings := config.ContentFlaggingSettings.ReviewerSettings
|
||||
|
||||
hasCommonReviewers := *reviewerSettings.CommonReviewers
|
||||
if hasCommonReviewers {
|
||||
return true
|
||||
}
|
||||
|
||||
teamSettings, exist := (*reviewerSettings.TeamReviewersSetting)[teamId]
|
||||
if !exist || (teamSettings.Enabled != nil && !*teamSettings.Enabled) {
|
||||
return false
|
||||
}
|
||||
|
||||
if teamSettings.ReviewerIds != nil && len(*teamSettings.ReviewerIds) > 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
hasAdditionalReviewers := (reviewerSettings.TeamAdminsAsReviewers != nil && *reviewerSettings.TeamAdminsAsReviewers) ||
|
||||
(reviewerSettings.SystemAdminsAsReviewers != nil && *reviewerSettings.SystemAdminsAsReviewers)
|
||||
|
||||
return hasAdditionalReviewers
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestContentFlaggingEnabledForTeam(t *testing.T) {
|
||||
getBaseConfig := func() *model.Config {
|
||||
contentFlaggingSettings := model.ContentFlaggingSettings{}
|
||||
contentFlaggingSettings.SetDefaults()
|
||||
|
||||
return &model.Config{
|
||||
ContentFlaggingSettings: contentFlaggingSettings,
|
||||
}
|
||||
}
|
||||
t.Run("should return true for common reviewers", func(t *testing.T) {
|
||||
config := getBaseConfig()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewers = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewerIds = &[]string{"reviewer_user_id_1", "reviewer_user_id_2"}
|
||||
|
||||
status := ContentFlaggingEnabledForTeam(config, "team1")
|
||||
require.True(t, status, "expected team post reporting feature to be enabled for common reviewers")
|
||||
})
|
||||
|
||||
t.Run("should return true when configured for specified team", func(t *testing.T) {
|
||||
config := getBaseConfig()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewers = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.TeamReviewersSetting = &map[string]model.TeamReviewerSetting{
|
||||
"team1": {
|
||||
Enabled: model.NewPointer(true),
|
||||
ReviewerIds: model.NewPointer([]string{"reviewer_user_id_1"}),
|
||||
},
|
||||
}
|
||||
|
||||
status := ContentFlaggingEnabledForTeam(config, "team1")
|
||||
require.True(t, status, "expected team post reporting feature to be disabled for team without reviewers")
|
||||
})
|
||||
|
||||
t.Run("should return true when using Additional Reviewers", func(t *testing.T) {
|
||||
config := getBaseConfig()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.CommonReviewers = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.TeamReviewersSetting = &map[string]model.TeamReviewerSetting{
|
||||
"team1": {
|
||||
Enabled: model.NewPointer(true),
|
||||
},
|
||||
}
|
||||
|
||||
status := ContentFlaggingEnabledForTeam(config, "team1")
|
||||
require.True(t, status)
|
||||
|
||||
config = getBaseConfig()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(false)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
|
||||
|
||||
status = ContentFlaggingEnabledForTeam(config, "team1")
|
||||
require.True(t, status)
|
||||
|
||||
config = getBaseConfig()
|
||||
config.ContentFlaggingSettings.ReviewerSettings.TeamAdminsAsReviewers = model.NewPointer(true)
|
||||
config.ContentFlaggingSettings.ReviewerSettings.SystemAdminsAsReviewers = model.NewPointer(true)
|
||||
|
||||
status = ContentFlaggingEnabledForTeam(config, "team1")
|
||||
require.True(t, status)
|
||||
})
|
||||
}
|
||||
@@ -239,6 +239,10 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
|
||||
props["MobilePreventScreenCapture"] = strconv.FormatBool(*c.NativeAppSettings.MobilePreventScreenCapture)
|
||||
props["MobileJailbreakProtection"] = strconv.FormatBool(*c.NativeAppSettings.MobileJailbreakProtection)
|
||||
}
|
||||
|
||||
if model.MinimumEnterpriseAdvancedLicense(license) {
|
||||
props["ContentFlaggingEnabled"] = strconv.FormatBool(c.FeatureFlags.ContentFlagging && *c.ContentFlaggingSettings.EnableContentFlagging)
|
||||
}
|
||||
}
|
||||
|
||||
return props
|
||||
|
||||
@@ -1737,6 +1737,14 @@
|
||||
"id": "api.config.update_config.translations.app_error",
|
||||
"translation": "Failed to update server translations."
|
||||
},
|
||||
{
|
||||
"id": "api.content_flagging.error.disabled",
|
||||
"translation": "Content flagging feature is disabled."
|
||||
},
|
||||
{
|
||||
"id": "api.content_flagging.error.license",
|
||||
"translation": "Your license does not support content flagging."
|
||||
},
|
||||
{
|
||||
"id": "api.context.404.app_error",
|
||||
"translation": "Sorry, we could not find the page."
|
||||
|
||||
@@ -294,6 +294,10 @@ func (c *Client4) postsRoute() string {
|
||||
return "/posts"
|
||||
}
|
||||
|
||||
func (c *Client4) contentFlaggingRoute() string {
|
||||
return "/content_flagging"
|
||||
}
|
||||
|
||||
func (c *Client4) postsEphemeralRoute() string {
|
||||
return "/posts/ephemeral"
|
||||
}
|
||||
@@ -721,6 +725,32 @@ func (c *Client4) DeleteScheduledPost(ctx context.Context, scheduledPostId strin
|
||||
return &deletedScheduledPost, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) GetFlaggingConfiguration(ctx context.Context) (*ContentFlaggingReportingConfig, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.contentFlaggingRoute()+"/flag/config", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
var config ContentFlaggingReportingConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
return nil, nil, NewAppError("GetFlaggingConfiguration", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return &config, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) GetTeamPostFlaggingFeatureStatus(ctx context.Context, teamId string) (map[string]bool, *Response, error) {
|
||||
r, err := c.DoAPIGet(ctx, c.contentFlaggingRoute()+"/team/"+teamId+"/status", "")
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
var status map[string]bool
|
||||
if err := json.NewDecoder(r.Body).Decode(&status); err != nil {
|
||||
return nil, nil, NewAppError("GetFlaggingConfiguration", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
return status, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
func (c *Client4) bookmarksRoute(channelId string) string {
|
||||
return c.channelRoute(channelId) + "/bookmarks"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ const (
|
||||
TargetReporter NotificationTarget = "reporter"
|
||||
)
|
||||
|
||||
var ContentFlaggingDefaultReasons = []string{
|
||||
"Inappropriate content",
|
||||
"Sensitive data",
|
||||
"Security concern",
|
||||
"Harassment or abuse",
|
||||
"Spam or phishing",
|
||||
}
|
||||
|
||||
type ContentFlaggingNotificationSettings struct {
|
||||
EventTargetMapping map[ContentFlaggingEvent][]NotificationTarget
|
||||
}
|
||||
@@ -124,9 +132,9 @@ func (rs *ReviewerSettings) IsValid() *AppError {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.content_flagging.common_reviewers_not_set.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// if additional reviewers are specified, no extra validation is needed in team specific settings as
|
||||
// if Additional Reviewers are specified, no extra validation is needed in team specific settings as
|
||||
// settings team reviewers keeping team feature disabled is valid, as well as
|
||||
// enabling team feature and not specified reviews is fine as well (since additional reviewers are set)
|
||||
// enabling team feature and not specified reviews is fine as well (since Additional Reviewers are set)
|
||||
if !additionalReviewersEnabled {
|
||||
for _, setting := range *rs.TeamReviewersSetting {
|
||||
if *setting.Enabled && (setting.ReviewerIds == nil || len(*setting.ReviewerIds) == 0) {
|
||||
@@ -147,13 +155,7 @@ type AdditionalContentFlaggingSettings struct {
|
||||
|
||||
func (acfs *AdditionalContentFlaggingSettings) SetDefaults() {
|
||||
if acfs.Reasons == nil {
|
||||
acfs.Reasons = &[]string{
|
||||
"Inappropriate content",
|
||||
"Sensitive data",
|
||||
"Security concern",
|
||||
"Harassment or abuse",
|
||||
"Spam or phishing",
|
||||
}
|
||||
acfs.Reasons = &ContentFlaggingDefaultReasons
|
||||
}
|
||||
|
||||
if acfs.ReporterCommentRequired == nil {
|
||||
@@ -223,3 +225,8 @@ func (cfs *ContentFlaggingSettings) IsValid() *AppError {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ContentFlaggingReportingConfig struct {
|
||||
Reasons *[]string `json:"reasons"`
|
||||
ReporterCommentRequired *bool `json:"reporter_comment_required"`
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func TestReviewerSettings_IsValid(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("should be valid when common reviewers enabled with additional reviewers", func(t *testing.T) {
|
||||
t.Run("should be valid when common reviewers enabled with Additional Reviewers", func(t *testing.T) {
|
||||
settings := &ReviewerSettings{
|
||||
CommonReviewers: NewPointer(true),
|
||||
CommonReviewerIds: &[]string{},
|
||||
@@ -204,7 +204,7 @@ func TestReviewerSettings_IsValid(t *testing.T) {
|
||||
require.Equal(t, "model.config.is_valid.content_flagging.team_reviewers_not_set.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("should be valid when team reviewers enabled but no reviewer IDs with additional reviewers", func(t *testing.T) {
|
||||
t.Run("should be valid when team reviewers enabled but no reviewer IDs with Additional Reviewers", func(t *testing.T) {
|
||||
settings := &ReviewerSettings{
|
||||
CommonReviewers: NewPointer(false),
|
||||
CommonReviewerIds: &[]string{},
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
|
||||
import iNoBounce from 'inobounce';
|
||||
import React, {lazy, memo, useEffect, useRef, useState} from 'react';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import {Route, Switch, useHistory, useParams} from 'react-router-dom';
|
||||
|
||||
import type {ServerError} from '@mattermost/types/errors';
|
||||
import type {Team} from '@mattermost/types/teams';
|
||||
|
||||
import {getTeamContentFlaggingStatus} from 'mattermost-redux/actions/content_flagging';
|
||||
import {
|
||||
contentFlaggingFeatureEnabled,
|
||||
} from 'mattermost-redux/selectors/entities/content_flagging';
|
||||
import type {ActionResult} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {reconnect} from 'actions/websocket_actions.jsx';
|
||||
@@ -42,6 +47,7 @@ declare global {
|
||||
type Props = PropsFromRedux & OwnProps;
|
||||
|
||||
function TeamController(props: Props) {
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const {team: teamNameParam} = useParams<Props['match']['params']>();
|
||||
|
||||
@@ -49,6 +55,8 @@ function TeamController(props: Props) {
|
||||
|
||||
const [team, setTeam] = useState<Team | null>(getTeamFromTeamList(props.teamsList, teamNameParam));
|
||||
|
||||
const contentFlaggingEnabled = useSelector(contentFlaggingFeatureEnabled);
|
||||
|
||||
const blurTime = useRef(Date.now());
|
||||
const lastTime = useRef(Date.now());
|
||||
|
||||
@@ -131,6 +139,13 @@ function TeamController(props: Props) {
|
||||
};
|
||||
}, [props.currentTeamId]);
|
||||
|
||||
// Load team content flagging status on team switch
|
||||
useEffect(() => {
|
||||
if (contentFlaggingEnabled && props.currentTeamId) {
|
||||
dispatch(getTeamContentFlaggingStatus(props.currentTeamId));
|
||||
}
|
||||
}, [contentFlaggingEnabled, dispatch, props.currentTeamId]);
|
||||
|
||||
// Effect runs on mount, adds active state to window
|
||||
useEffect(() => {
|
||||
const browserIsIosSafari = isIosSafari();
|
||||
|
||||
@@ -49,4 +49,6 @@ export default keyMirror({
|
||||
RECEIVED_TEAM_MEMBERS_MINUS_GROUP_MEMBERS: null,
|
||||
|
||||
RECEIVED_TOTAL_TEAM_COUNT: null,
|
||||
|
||||
RECEIVED_CONTENT_FLAGGING_STATUS: null,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import nock from 'nock';
|
||||
|
||||
import * as Actions from 'mattermost-redux/actions/content_flagging';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
|
||||
import TestHelper from 'packages/mattermost-redux/test/test_helper';
|
||||
import configureStore from 'packages/mattermost-redux/test/test_store';
|
||||
|
||||
describe('Actions.getTeamContentFlaggingStatus', () => {
|
||||
const store = configureStore();
|
||||
beforeAll(() => {
|
||||
TestHelper.initBasic(Client4);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
TestHelper.tearDown();
|
||||
});
|
||||
|
||||
it('should dispatch RECEIVED_CONTENT_FLAGGING_STATUS on success', async () => {
|
||||
nock(Client4.getContentFlaggingRoute()).
|
||||
get('/team/team_id/status').
|
||||
reply(200, {enabled: true});
|
||||
|
||||
await store.dispatch(Actions.getTeamContentFlaggingStatus('team_id'));
|
||||
|
||||
let enabled = store.getState().entities.teams.contentFlaggingStatus.team_id;
|
||||
expect(enabled).toEqual(true);
|
||||
|
||||
// Changing value for same team
|
||||
nock(Client4.getContentFlaggingRoute()).
|
||||
get('/team/team_id/status').
|
||||
reply(200, {enabled: false});
|
||||
|
||||
await store.dispatch(Actions.getTeamContentFlaggingStatus('team_id'));
|
||||
|
||||
enabled = store.getState().entities.teams.contentFlaggingStatus.team_id;
|
||||
expect(enabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {TeamTypes} from 'mattermost-redux/action_types';
|
||||
import {logError} from 'mattermost-redux/actions/errors';
|
||||
import {forceLogoutIfNecessary} from 'mattermost-redux/actions/helpers';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import type {ActionFuncAsync} from 'mattermost-redux/types/actions';
|
||||
|
||||
export function getTeamContentFlaggingStatus(teamId: string): ActionFuncAsync<{enabled: boolean}> {
|
||||
return async (dispatch, getState) => {
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await Client4.getTeamContentFlaggingStatus(teamId);
|
||||
|
||||
dispatch({
|
||||
type: TeamTypes.RECEIVED_CONTENT_FLAGGING_STATUS,
|
||||
data: {
|
||||
teamId,
|
||||
status: response.enabled,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(error, dispatch, getState);
|
||||
dispatch(logError(error));
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {data: response};
|
||||
};
|
||||
}
|
||||
@@ -494,6 +494,19 @@ function totalCount(state = 0, action: MMReduxAction) {
|
||||
}
|
||||
}
|
||||
|
||||
function contentFlaggingStatus(state = {}, action: MMReduxAction) {
|
||||
switch (action.type) {
|
||||
case TeamTypes.RECEIVED_CONTENT_FLAGGING_STATUS: {
|
||||
return {
|
||||
...state,
|
||||
[action.data.teamId]: action.data.status,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
|
||||
// the current selected team
|
||||
@@ -514,4 +527,6 @@ export default combineReducers({
|
||||
groupsAssociatedToTeam,
|
||||
|
||||
totalCount,
|
||||
|
||||
contentFlaggingStatus,
|
||||
});
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {DeepPartial} from 'redux';
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {contentFlaggingFeatureEnabled} from './content_flagging';
|
||||
|
||||
describe('Selectors.ContentFlagging', () => {
|
||||
test('should return true when config and feature flag both are set', () => {
|
||||
const state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ContentFlaggingEnabled: 'true',
|
||||
FeatureFlagContentFlagging: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(contentFlaggingFeatureEnabled(state as GlobalState)).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false when either config or feature flag are not set', () => {
|
||||
let state: DeepPartial<GlobalState> = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ContentFlaggingEnabled: 'false',
|
||||
FeatureFlagContentFlagging: 'true',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(contentFlaggingFeatureEnabled(state as GlobalState)).toBe(false);
|
||||
|
||||
state = {
|
||||
entities: {
|
||||
general: {
|
||||
config: {
|
||||
ContentFlaggingEnabled: 'true',
|
||||
FeatureFlagContentFlagging: 'false',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(contentFlaggingFeatureEnabled(state as GlobalState)).toBe(false);
|
||||
});
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type {GlobalState} from '@mattermost/types/store';
|
||||
|
||||
import {getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
|
||||
|
||||
export const contentFlaggingFeatureEnabled = (state: GlobalState): boolean => {
|
||||
const featureFlagEnabled = getFeatureFlagValue(state, 'ContentFlagging') === 'true';
|
||||
const featureEnabled = state.entities.general.config.ContentFlaggingEnabled === 'true';
|
||||
|
||||
return featureFlagEnabled && featureEnabled;
|
||||
};
|
||||
@@ -50,6 +50,7 @@ const state: GlobalState = {
|
||||
stats: {},
|
||||
groupsAssociatedToTeam: {},
|
||||
totalCount: 0,
|
||||
contentFlaggingStatus: {},
|
||||
},
|
||||
channels: {
|
||||
currentChannelId: '',
|
||||
|
||||
@@ -15,4 +15,5 @@ export const emptyTeams: () => TeamsState = () => ({
|
||||
stats: {},
|
||||
groupsAssociatedToTeam: {},
|
||||
totalCount: 0,
|
||||
contentFlaggingStatus: {},
|
||||
});
|
||||
|
||||
@@ -539,6 +539,10 @@ export default class Client4 {
|
||||
return `${this.getBaseRoute()}/client_perf`;
|
||||
}
|
||||
|
||||
getContentFlaggingRoute() {
|
||||
return `${this.getBaseRoute()}/content_flagging`;
|
||||
}
|
||||
|
||||
getCSRFFromCookie() {
|
||||
if (typeof document !== 'undefined' && typeof document.cookie !== 'undefined') {
|
||||
const cookies = document.cookie.split(';');
|
||||
@@ -4587,6 +4591,13 @@ export default class Client4 {
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamContentFlaggingStatus = (teamId: string) => {
|
||||
return this.doFetch<{enabled: boolean}>(
|
||||
`${this.getContentFlaggingRoute()}/team/${teamId}/status`,
|
||||
{method: 'get'},
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAndMergeNestedHeaders(originalHeaders: any) {
|
||||
|
||||
@@ -130,6 +130,7 @@ export type ClientConfig = {
|
||||
FeatureFlagCustomProfileAttributes: string;
|
||||
FeatureFlagAttributeBasedAccessControl: string;
|
||||
FeatureFlagWebSocketEventScope: string;
|
||||
FeatureFlagContentFlagging: string;
|
||||
ForgotPasswordLink: string;
|
||||
GiphySdkKey: string;
|
||||
GoogleDeveloperKey: string;
|
||||
@@ -225,6 +226,7 @@ export type ClientConfig = {
|
||||
YoutubeReferrerPolicy: 'true' | 'false';
|
||||
ScheduledPosts: string;
|
||||
DeleteAccountLink: string;
|
||||
ContentFlaggingEnabled: 'true' | 'false';
|
||||
};
|
||||
|
||||
export type License = {
|
||||
|
||||
@@ -50,6 +50,7 @@ export type TeamsState = {
|
||||
stats: RelationOneToOne<Team, TeamStats>;
|
||||
groupsAssociatedToTeam: any;
|
||||
totalCount: number;
|
||||
contentFlaggingStatus: Record<Team['id'], boolean>;
|
||||
};
|
||||
|
||||
export type TeamUnread = {
|
||||
|
||||
Reference in New Issue
Block a user