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:
Harshil Sharma
2025-07-22 14:57:37 +05:30
committed by GitHub
parent 7b41e17c7f
commit 2f886a2ec1
24 changed files with 633 additions and 11 deletions
+1
View File
@@ -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
+63
View File
@@ -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.
+5
View File
@@ -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 {
+87
View File
@@ -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"])
})
}
+29
View File
@@ -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)
})
}
+4
View File
@@ -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
+8
View File
@@ -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."
+30
View File
@@ -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,
});
@@ -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);
});
});
@@ -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: {},
});
+11
View File
@@ -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) {
+2
View File
@@ -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 = {
+1
View File
@@ -50,6 +50,7 @@ export type TeamsState = {
stats: RelationOneToOne<Team, TeamStats>;
groupsAssociatedToTeam: any;
totalCount: number;
contentFlaggingStatus: Record<Team['id'], boolean>;
};
export type TeamUnread = {