Deleted removed post from content flagging redux store (#36803) (#36882)

Automatic Merge
This commit is contained in:
Harshil Sharma
2026-06-04 08:54:05 +02:00
committed by GitHub
parent deb266789b
commit 48b5e9a919
10 changed files with 465 additions and 15 deletions
@@ -166,6 +166,18 @@ export default class ContentReviewPage {
await expect(this.reportCard!.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
}
async verifyFlaggedPostMessageInRHS(expected: string) {
await expect(this.rhsCard.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
}
async verifyFlaggedPostMessageInCenter(postID: string, expected: string) {
const centerCard = this.page
.getByTestId('channel_view')
.locator('div.DataSpillageReport')
.filter({has: this.page.locator(`#postMessageText_${postID}`)});
await expect(centerCard.locator('.row:has-text("Message") .post-message__text')).toHaveText(expected);
}
async clickKeepMessage() {
await this.keepMessageButton.scrollIntoViewIfNeeded();
await this.keepMessageButton.click();
@@ -78,6 +78,15 @@ test('Verify Removed Flagged posts show appropriate status and do not show the p
await secondContentReviewPage.confirmRemovePermanently();
await setupContentFlagging(adminClient, [adminUser.id, secondUserID, thirdUserID]);
// After the remove action succeeds, the reviewer's own view of the flagged
// post should be replaced with the moderation placeholder rather than the
// original message that is now cleared from the redux store (MM-69043).
// The placeholder appears both in the RHS detail view and in the report
// card shown in the @content-review center channel — assert each scope
// separately so a regression in either location is caught.
await secondContentReviewPage.verifyFlaggedPostMessageInRHS(contentModerationMessage);
await secondContentReviewPage.verifyFlaggedPostMessageInCenter(post.id, contentModerationMessage);
const {channelsPage: channelsPageThird, contentReviewPage: contentReviewPageThird} =
await pw.testBrowser.login(thirdUser);
await verifyAuthorNotification(
+11 -11
View File
@@ -1,6 +1,6 @@
module github.com/mattermost/mattermost/tools/sharedchannel-test
go 1.26.2
go 1.26.3
require github.com/mattermost/mattermost/server/public v0.1.12
@@ -18,14 +18,14 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.7.0 // indirect
github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.22 // indirect
github.com/mattermost/mattermost/server/v8 v8.0.0-20251014075701-833e0125320d // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/oklog/run v1.2.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
@@ -33,18 +33,18 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/grpc v1.79.3 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect
google.golang.org/grpc v1.81.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
+10
View File
@@ -81,6 +81,7 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=
github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/go-plugin v1.8.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -111,6 +112,7 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -173,6 +175,7 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cb
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
@@ -204,6 +207,7 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -211,6 +215,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -223,6 +228,7 @@ golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -248,6 +254,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -255,6 +262,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -279,6 +287,7 @@ google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRn
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff h1:A90eA31Wq6HOMIQlLfzFwzqGKBTuaVztYu/g8sn+8Zc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251007200510-49b9836ed3ff/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -286,6 +295,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
@@ -4,7 +4,9 @@
import {screen, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import type {MockStoreEnhanced} from 'redux-mock-store';
import {ContentFlaggingTypes} from 'mattermost-redux/action_types';
import {Client4} from 'mattermost-redux/client';
import {renderWithContext} from 'tests/react_testing_utils';
@@ -517,6 +519,101 @@ describe('KeepRemoveFlaggedMessageConfirmationModal', () => {
});
});
describe('store cleanup after remove', () => {
test('dispatches FLAGGED_POST_REMOVED after a successful removeFlaggedPost call', async () => {
const {store} = renderWithContext(
<KeepRemoveFlaggedMessageConfirmationModal
action='remove'
onExited={onExited}
flaggedPost={flaggedPost}
reportingUser={reportingUser}
/>,
{},
{useMockedStore: true},
);
await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
await waitFor(() => {
expect(screen.getByTestId('generated-section')).toBeVisible();
});
await userEvent.click(screen.getByRole('button', {name: 'Remove permanently'}));
await waitFor(() => {
expect(Client4.removeFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
});
await waitFor(() => {
expect((store as unknown as MockStoreEnhanced).getActions()).toContainEqual({
type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
data: {postId: flaggedPost.id},
});
});
});
test('does not dispatch FLAGGED_POST_REMOVED when removeFlaggedPost fails', async () => {
Client4.removeFlaggedPost = jest.fn().mockRejectedValue({message: 'boom'});
const {store} = renderWithContext(
<KeepRemoveFlaggedMessageConfirmationModal
action='remove'
onExited={onExited}
flaggedPost={flaggedPost}
reportingUser={reportingUser}
/>,
{},
{useMockedStore: true},
);
await userEvent.click(screen.getByTestId('download-report-checkbox'));
await userEvent.click(screen.getByRole('button', {name: 'Remove message'}));
await waitFor(() => {
expect(screen.getByTestId('skip-confirm-body')).toBeVisible();
});
await userEvent.click(screen.getByRole('button', {name: 'Remove without report'}));
await waitFor(() => {
expect(Client4.removeFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
});
expect((store as unknown as MockStoreEnhanced).getActions()).not.toContainEqual(
expect.objectContaining({type: ContentFlaggingTypes.FLAGGED_POST_REMOVED}),
);
});
test('does not dispatch FLAGGED_POST_REMOVED for keep action', async () => {
const {store} = renderWithContext(
<KeepRemoveFlaggedMessageConfirmationModal
action='keep'
onExited={onExited}
flaggedPost={flaggedPost}
reportingUser={reportingUser}
/>,
{},
{useMockedStore: true},
);
await userEvent.click(screen.getByRole('button', {name: 'Continue'}));
await waitFor(() => {
expect(screen.getByTestId('generated-section')).toBeVisible();
});
await userEvent.click(screen.getByRole('button', {name: 'Keep permanently'}));
await waitFor(() => {
expect(Client4.keepFlaggedPost).toHaveBeenCalledWith(flaggedPost.id, '');
});
expect((store as unknown as MockStoreEnhanced).getActions()).not.toContainEqual(
expect.objectContaining({type: ContentFlaggingTypes.FLAGGED_POST_REMOVED}),
);
});
});
describe('error handling', () => {
test('should show request error when API call fails', async () => {
const errorMessage = 'Failed to remove flagged post';
@@ -3,12 +3,14 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {useDispatch} from 'react-redux';
import {GenericModal} from '@mattermost/components';
import type {ServerError} from '@mattermost/types/errors';
import type {Post} from '@mattermost/types/posts';
import type {UserProfile} from '@mattermost/types/users';
import {removeContentFlaggingPost} from 'mattermost-redux/actions/content_flagging';
import {Client4} from 'mattermost-redux/client';
import {useChannel} from 'components/common/hooks/useChannel';
@@ -39,6 +41,7 @@ type Step = 'form' | 'skip_confirm' | 'generating' | 'generated' | 'error';
export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExited, flaggedPost, reportingUser}: Props) {
const {formatMessage} = useIntl();
const dispatch = useDispatch();
const flaggedPostChannel = useChannel(flaggedPost.channel_id);
const contentFlaggingConfig = useContentFlaggingConfig(flaggedPostChannel?.team_id || '');
@@ -91,6 +94,9 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
setSubmitting(true);
setRequestError('');
await actionFunc(flaggedPost.id, comment);
if (action === 'remove') {
dispatch(removeContentFlaggingPost(flaggedPost.id));
}
handleClose();
} catch (error) {
// eslint-disable-next-line no-console
@@ -99,7 +105,7 @@ export default function KeepRemoveFlaggedMessageConfirmationModal({action, onExi
} finally {
setSubmitting(false);
}
}, [action, comment, flaggedPost.id, handleClose]);
}, [action, comment, dispatch, flaggedPost.id, handleClose]);
const handleFormPrimary = useCallback(() => {
if (validateForm()) {
@@ -11,4 +11,5 @@ export default keyMirror({
RECEIVED_FLAGGED_POST: null,
RECEIVED_CONTENT_FLAGGING_CHANNEL: null,
RECEIVED_CONTENT_FLAGGING_TEAM: null,
FLAGGED_POST_REMOVED: null,
});
@@ -11,7 +11,7 @@ import {TeamTypes, ContentFlaggingTypes} 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';
import type {ActionFuncAsync, ActionFunc} from 'mattermost-redux/types/actions';
import {DelayedDataLoader} from 'mattermost-redux/utils/data_loader';
export type ContentFlaggingChannelRequestIdentifier = {
@@ -264,3 +264,14 @@ export function getPostContentFlaggingValues(postId: string): ActionFuncAsync<Ar
return {data: response};
};
}
export function removeContentFlaggingPost(postId: string): ActionFunc<void> {
return (dispatch) => {
dispatch({
type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
data: {postId},
});
return {};
};
}
@@ -0,0 +1,274 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {PropertyValue} from '@mattermost/types/properties';
import {ContentFlaggingTypes} from 'mattermost-redux/action_types';
import contentFlaggingReducer from './content_flagging';
function makeValue(overrides: Partial<PropertyValue<unknown>> = {}): PropertyValue<unknown> {
return {
id: 'value-1',
target_id: 'post-1',
target_type: 'post',
group_id: 'content_flagging',
field_id: 'field-1',
value: 'test',
create_at: 1000,
update_at: 1000,
delete_at: 0,
created_by: 'user-1',
updated_by: 'user-1',
...overrides,
};
}
function valuesByFieldId(values: Array<PropertyValue<unknown>>) {
return Object.fromEntries(values.map((value) => [value.field_id, value]));
}
describe('Reducers.ContentFlagging', () => {
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED stores values for posts without cached values', () => {
const value = makeValue({field_id: 'review_status', value: 'removed'});
const state = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: JSON.stringify([value]),
},
});
expect(state.postValues['post-1']).toEqual([value]);
});
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED merges updated values with cached values', () => {
const existingValue = makeValue({field_id: 'review_status', value: 'pending'});
const retainedValue = makeValue({id: 'value-2', field_id: 'reason', value: 'data_spillage'});
const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [existingValue, retainedValue],
},
});
const state = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: JSON.stringify([updatedValue]),
},
});
expect(state.postValues['post-1']).toHaveLength(2);
expect(valuesByFieldId(state.postValues['post-1'])).toEqual({
review_status: updatedValue,
reason: retainedValue,
});
});
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED merges multiple values and preserves other posts', () => {
const existingValue = makeValue({field_id: 'review_status', value: 'pending'});
const retainedValue = makeValue({id: 'value-2', field_id: 'reason', value: 'data_spillage'});
const otherPostValue = makeValue({id: 'value-3', target_id: 'post-2', field_id: 'review_status', value: 'pending'});
const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
const newValue = makeValue({id: 'value-4', field_id: 'removal_type', value: 'permanent'});
const stateWithPostOne = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [existingValue, retainedValue],
},
});
const stateWithValues = contentFlaggingReducer(stateWithPostOne, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-2',
values: [otherPostValue],
},
});
const state = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: JSON.stringify([updatedValue, newValue]),
},
});
expect(state.postValues['post-1']).toHaveLength(3);
expect(valuesByFieldId(state.postValues['post-1'])).toEqual({
review_status: updatedValue,
reason: retainedValue,
removal_type: newValue,
});
expect(state.postValues['post-2']).toEqual([otherPostValue]);
});
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED ignores malformed property values', () => {
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [makeValue({field_id: 'review_status', value: 'pending'})],
},
});
expect(() => contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: '{',
},
})).not.toThrow();
const state = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: '{',
},
});
expect(state).toBe(stateWithValues);
});
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED ignores valid non-array property values', () => {
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [makeValue({field_id: 'review_status', value: 'pending'})],
},
});
const state = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: '{}',
},
});
expect(state).toBe(stateWithValues);
});
test('CONTENT_FLAGGING_REPORT_VALUE_UPDATED tolerates non-array cached values', () => {
const updatedValue = makeValue({field_id: 'review_status', value: 'removed'});
const invalidState = {
settings: {},
fields: {},
postValues: {
'post-1': {
invalid: true,
},
},
flaggedPosts: {},
channels: {},
teams: {},
};
const state = contentFlaggingReducer(invalidState as unknown as Parameters<typeof contentFlaggingReducer>[0], {
type: ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED,
data: {
target_id: 'post-1',
property_values: JSON.stringify([updatedValue]),
},
});
expect(state.postValues['post-1']).toEqual([updatedValue]);
});
describe('FLAGGED_POST_REMOVED', () => {
test('clears the flagged post and its property values for the given post id', () => {
const value = makeValue({field_id: 'review_status', value: 'pending'});
const otherValue = makeValue({id: 'value-2', target_id: 'post-2', field_id: 'review_status', value: 'pending'});
const flaggedPost = {id: 'post-1'} as any;
const otherFlaggedPost = {id: 'post-2'} as any;
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [value],
},
});
const stateWithOtherValues = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-2',
values: [otherValue],
},
});
const stateWithFlaggedPost = contentFlaggingReducer(stateWithOtherValues, {
type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
data: flaggedPost,
});
const stateWithBothFlaggedPosts = contentFlaggingReducer(stateWithFlaggedPost, {
type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
data: otherFlaggedPost,
});
const state = contentFlaggingReducer(stateWithBothFlaggedPosts, {
type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
data: {postId: 'post-1'},
});
expect(state.postValues['post-1']).toBeUndefined();
expect(state.flaggedPosts['post-1']).toBeUndefined();
expect(state.postValues['post-2']).toEqual([otherValue]);
expect(state.flaggedPosts['post-2']).toEqual(otherFlaggedPost);
});
test('returns the same state reference when the post id is not present', () => {
const value = makeValue({field_id: 'review_status', value: 'pending'});
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [value],
},
});
const state = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
data: {postId: 'unknown-post'},
});
expect(state.postValues).toBe(stateWithValues.postValues);
expect(state.flaggedPosts).toBe(stateWithValues.flaggedPosts);
});
test('is a no-op when postId is missing from the action data', () => {
const value = makeValue({field_id: 'review_status', value: 'pending'});
const flaggedPost = {id: 'post-1'} as any;
const stateWithValues = contentFlaggingReducer(undefined, {
type: ContentFlaggingTypes.RECEIVED_POST_CONTENT_FLAGGING_VALUES,
data: {
postId: 'post-1',
values: [value],
},
});
const stateWithFlaggedPost = contentFlaggingReducer(stateWithValues, {
type: ContentFlaggingTypes.RECEIVED_FLAGGED_POST,
data: flaggedPost,
});
const state = contentFlaggingReducer(stateWithFlaggedPost, {
type: ContentFlaggingTypes.FLAGGED_POST_REMOVED,
data: {},
});
expect(state.postValues['post-1']).toEqual([value]);
expect(state.flaggedPosts['post-1']).toEqual(flaggedPost);
});
});
});
@@ -15,6 +15,15 @@ import type {
import type {MMReduxAction} from 'mattermost-redux/action_types';
import {ContentFlaggingTypes, UserTypes} from 'mattermost-redux/action_types';
function parsePropertyValues(propertyValues: string): Array<PropertyValue<unknown>> | null {
try {
const parsedPropertyValues = JSON.parse(propertyValues);
return Array.isArray(parsedPropertyValues) ? parsedPropertyValues : null;
} catch {
return null;
}
}
function settings(state: ContentFlaggingState['settings'] = {} as ContentFlaggingConfig, action: MMReduxAction) {
switch (action.type) {
case ContentFlaggingTypes.RECEIVED_CONTENT_FLAGGING_CONFIG: {
@@ -55,8 +64,11 @@ function postValues(state: ContentFlaggingState['postValues'] = {}, action: MMRe
}
case ContentFlaggingTypes.CONTENT_FLAGGING_REPORT_VALUE_UPDATED: {
const postId = action.data.target_id as string;
const existingPropertyValues = state[postId] || {};
const updatedPropertyValues = JSON.parse(action.data.property_values);
const existingPropertyValues = Array.isArray(state[postId]) ? state[postId] : [];
const updatedPropertyValues = parsePropertyValues(action.data.property_values);
if (!updatedPropertyValues) {
return state;
}
const valuesByFieldId = {} as Record<string, PropertyValue<unknown>>;
existingPropertyValues.forEach((property: PropertyValue<unknown>) => {
@@ -71,6 +83,15 @@ function postValues(state: ContentFlaggingState['postValues'] = {}, action: MMRe
[postId]: Object.values(valuesByFieldId),
};
}
case ContentFlaggingTypes.FLAGGED_POST_REMOVED: {
const postId = action.data?.postId as string | undefined;
if (!postId || !(postId in state)) {
return state;
}
const nextState = {...state};
Reflect.deleteProperty(nextState, postId);
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
@@ -86,6 +107,15 @@ function flaggedPosts(state: ContentFlaggingState['flaggedPosts'] = {}, action:
[action.data.id]: action.data,
};
}
case ContentFlaggingTypes.FLAGGED_POST_REMOVED: {
const postId = action.data?.postId as string | undefined;
if (!postId || !(postId in state)) {
return state;
}
const nextState = {...state};
Reflect.deleteProperty(nextState, postId);
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default: