mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 13:17:29 -05:00
Automatic Merge
This commit is contained in:
@@ -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();
|
||||
|
||||
+9
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+97
@@ -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';
|
||||
|
||||
+7
-1
@@ -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 {};
|
||||
};
|
||||
}
|
||||
|
||||
+274
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+32
-2
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user