From de6701fb99dad789de46453e2299e4b6de66459c Mon Sep 17 00:00:00 2001 From: Harshil Sharma <18575143+harshilsharma63@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:33:25 +0530 Subject: [PATCH] Scheduled post left gm fix (#29211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed a bug where GM member count shows up as 0 in scheduled posts * Fixed a bug where GM member count shows up as 0 in scheduled posts * Lint fix * Added wrap̛ntl to channels * lint fix * Used existing renderer * Removed ubnused store --- .../__snapshots__/draft_title.test.tsx.snap | 425 ++++-------------- .../drafts/draft_title/draft_title.test.tsx | 180 ++++---- .../drafts/draft_title/draft_title.tsx | 43 +- .../components/drafts/draft_title/index.ts | 4 +- .../mattermost-redux/src/actions/users.ts | 16 + webapp/channels/src/utils/test_intl.tsx | 14 + 6 files changed, 255 insertions(+), 427 deletions(-) create mode 100644 webapp/channels/src/utils/test_intl.tsx diff --git a/webapp/channels/src/components/drafts/draft_title/__snapshots__/draft_title.test.tsx.snap b/webapp/channels/src/components/drafts/draft_title/__snapshots__/draft_title.test.tsx.snap index 99b1d2ce1d..04a8d3e927 100644 --- a/webapp/channels/src/components/drafts/draft_title/__snapshots__/draft_title.test.tsx.snap +++ b/webapp/channels/src/components/drafts/draft_title/__snapshots__/draft_title.test.tsx.snap @@ -1,359 +1,130 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`components/drafts/draft_actions should fetch members when member count is 0 for GM 1`] = ` +
+ To: +
+ 0 +
+ + + Test Channel + +
+`; + exports[`components/drafts/draft_actions should match snapshot 1`] = ` - - + In: + - + + + Test Channel + + `; exports[`components/drafts/draft_actions should match snapshot for DM channel 1`] = ` - - + To: + user profile image - + + + Test Channel + + `; exports[`components/drafts/draft_actions should match snapshot for DM channel with teammate 1`] = ` - - + To: + username profile image - + + + Test Channel + + `; exports[`components/drafts/draft_actions should match snapshot for GM channel 1`] = ` - - - +
+ To: +
+ 5 +
+ + + Test Channel + +
`; exports[`components/drafts/draft_actions should match snapshot for open channel 1`] = ` - - + In: + - + + + Test Channel + + `; exports[`components/drafts/draft_actions should match snapshot for private channel 1`] = ` - - + In: + - + + + Test Channel + + `; exports[`components/drafts/draft_actions should match snapshot for self draft 1`] = ` - - + In: + - + + + Test Channel + +   + (you) + `; exports[`components/drafts/draft_actions should match snapshot for thread 1`] = ` - - + Thread in: + - + + + Test Channel + + `; diff --git a/webapp/channels/src/components/drafts/draft_title/draft_title.test.tsx b/webapp/channels/src/components/drafts/draft_title/draft_title.test.tsx index d2325cb6f8..d98668eb1d 100644 --- a/webapp/channels/src/components/drafts/draft_title/draft_title.test.tsx +++ b/webapp/channels/src/components/drafts/draft_title/draft_title.test.tsx @@ -1,102 +1,98 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {shallow} from 'enzyme'; import React from 'react'; -import {Provider} from 'react-redux'; import type {Channel} from '@mattermost/types/channels'; import type {UserProfile} from '@mattermost/types/users'; -import mockStore from 'tests/test_store'; +import {renderWithContext} from 'tests/react_testing_utils'; import Constants from 'utils/constants'; import DraftTitle from './draft_title'; describe('components/drafts/draft_actions', () => { + const channel = { + type: 'O', + display_name: 'Test Channel', + } as Channel; + const baseProps = { - channelType: '' as Channel['type'], - channelName: '', + channel, membersCount: 5, selfDraft: false, teammate: {} as UserProfile, teammateId: '', - type: '' as 'channel' | 'thread', + type: 'channel' as 'channel' | 'thread', }; it('should match snapshot', () => { - const store = mockStore(); - - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for self draft', () => { - const store = mockStore(); - const props = { ...baseProps, selfDraft: true, }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for private channel', () => { - const store = mockStore(); - + const channel = { + type: Constants.PRIVATE_CHANNEL, + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.PRIVATE_CHANNEL as Channel['type'], + channel, }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for DM channel', () => { - const store = mockStore(); - + const channel = { + type: Constants.DM_CHANNEL, + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.DM_CHANNEL as Channel['type'], + channel, }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for DM channel with teammate', () => { - const store = mockStore(); - + const channel = { + type: Constants.DM_CHANNEL, + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.DM_CHANNEL as Channel['type'], + channel, teammate: { username: 'username', id: 'id', @@ -104,69 +100,91 @@ describe('components/drafts/draft_actions', () => { } as UserProfile, }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for GM channel', () => { - const store = mockStore(); + const channel = { + type: 'G', + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.GM_CHANNEL as Channel['type'], + channel, }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for thread', () => { - const store = mockStore(); + const channel = { + type: Constants.OPEN_CHANNEL, + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.OPEN_CHANNEL as Channel['type'], + channel, type: 'thread' as 'channel' | 'thread', }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); }); it('should match snapshot for open channel', () => { - const store = mockStore(); + const channel = { + type: Constants.OPEN_CHANNEL, + display_name: 'Test Channel', + } as Channel; const props = { ...baseProps, - channelType: Constants.OPEN_CHANNEL as Channel['type'], + channel, type: 'channel' as 'channel' | 'thread', }; - const wrapper = shallow( - - - , + const {container} = renderWithContext( + , ); - expect(wrapper).toMatchSnapshot(); + expect(container).toMatchSnapshot(); + }); + + it('should fetch members when member count is 0 for GM', () => { + const channel = { + type: 'G', + display_name: 'Test Channel', + } as Channel; + + const props = { + ...baseProps, + channel, + membersCount: 0, + type: 'channel' as 'channel' | 'thread', + }; + + const {container} = renderWithContext( + , + ); + expect(container).toMatchSnapshot(); }); }); diff --git a/webapp/channels/src/components/drafts/draft_title/draft_title.tsx b/webapp/channels/src/components/drafts/draft_title/draft_title.tsx index a0299eb7ff..8f45d6033e 100644 --- a/webapp/channels/src/components/drafts/draft_title/draft_title.tsx +++ b/webapp/channels/src/components/drafts/draft_title/draft_title.tsx @@ -8,7 +8,7 @@ import {useDispatch} from 'react-redux'; import type {Channel} from '@mattermost/types/channels'; import type {UserProfile} from '@mattermost/types/users'; -import {getMissingProfilesByIds} from 'mattermost-redux/actions/users'; +import {batchGetProfilesInChannel, getMissingProfilesByIds} from 'mattermost-redux/actions/users'; import Avatar from 'components/widgets/users/avatar'; @@ -18,8 +18,7 @@ import {imageURLForUser} from 'utils/utils'; import './draft_title.scss'; type Props = { - channelType: Channel['type']; - channelName: string; + channel: Channel; membersCount?: number; selfDraft: boolean; teammate?: UserProfile; @@ -28,8 +27,7 @@ type Props = { } function DraftTitle({ - channelType, - channelName, + channel, membersCount, selfDraft, teammate, @@ -44,6 +42,19 @@ function DraftTitle({ } }, [teammate?.id, teammateId]); + useEffect(() => { + // if you have a scheduled post in a GM and you closed that GM, + // we don't fetch that GM's members by default. This causes the number of GM members + // in scheduled posts row header to show up as '0'. To fix this, + // we check if the channel is a GM and member count is 0 (could will at least be 1 as the current user + // is always a member) and if so, fetch the GM members. + // The action uses a data loader so it is safe to call do this for multiple + // scheduled posts for the same GM without causing any duplicate API calls. + if (channel.type === Constants.GM_CHANNEL && !membersCount) { + dispatch(batchGetProfilesInChannel(channel.id)); + } + }, [channel.id, channel.type, dispatch, membersCount]); + let you = null; let title = null; @@ -61,11 +72,11 @@ function DraftTitle({ let icon = ; - if (channelType === Constants.PRIVATE_CHANNEL) { + if (channel.type === Constants.PRIVATE_CHANNEL) { icon = ; } - if (channelType === Constants.DM_CHANNEL && teammate) { + if (channel.type === Constants.DM_CHANNEL && teammate) { icon = ( {membersCount} @@ -86,8 +97,8 @@ function DraftTitle({ if (type === 'thread') { if ( - channelType !== Constants.GM_CHANNEL && - channelType !== Constants.DM_CHANNEL + channel.type !== Constants.GM_CHANNEL && + channel.type !== Constants.DM_CHANNEL ) { title = ( {channelName}'} values={{ icon, - channelName, + channelName: channel.display_name, span: (chunks: React.ReactNode) => ({chunks}), }} /> @@ -107,15 +118,15 @@ function DraftTitle({ defaultMessage={'Thread to: {icon} {channelName}'} values={{ icon, - channelName, + channelName: channel.display_name, span: (chunks: React.ReactNode) => ({chunks}), }} /> ); } } else if ( - channelType !== Constants.GM_CHANNEL && - channelType !== Constants.DM_CHANNEL + channel.type !== Constants.GM_CHANNEL && + channel.type !== Constants.DM_CHANNEL ) { title = ( {channelName}'} values={{ icon, - channelName, + channelName: channel.display_name, span: (chunks: React.ReactNode) => ({chunks}), }} /> @@ -135,7 +146,7 @@ function DraftTitle({ defaultMessage={'To: {icon} {channelName}'} values={{ icon, - channelName, + channelName: channel.display_name, span: (chunks: React.ReactNode) => ({chunks}), }} /> diff --git a/webapp/channels/src/components/drafts/draft_title/index.ts b/webapp/channels/src/components/drafts/draft_title/index.ts index f823c08181..83b321c98f 100644 --- a/webapp/channels/src/components/drafts/draft_title/index.ts +++ b/webapp/channels/src/components/drafts/draft_title/index.ts @@ -25,7 +25,6 @@ function makeMapStateToProps() { return (state: GlobalState, ownProps: OwnProps) => { const {channel, userId} = ownProps; - const channelName = channel.display_name; let teammateId; let teammate; let membersCount; @@ -40,8 +39,7 @@ function makeMapStateToProps() { } return { - channelName, - channelType: channel.type, + channel, membersCount, selfDraft: teammateId === userId, teammate, diff --git a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts index 83e9232856..6a4df7a39b 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/actions/users.ts @@ -5,6 +5,7 @@ import type {AnyAction} from 'redux'; import {batchActions} from 'redux-batched-actions'; import type {UserAutocomplete} from '@mattermost/types/autocomplete'; +import type {Channel} from '@mattermost/types/channels'; import type {ServerError} from '@mattermost/types/errors'; import type {UserProfile, UserStatus, GetFilteredUsersStatsOpts, UsersStats, UserCustomStatus, UserAccessToken} from '@mattermost/types/users'; @@ -375,6 +376,21 @@ export function getProfilesInChannel(channelId: string, page: number, perPage: n }; } +export function batchGetProfilesInChannel(channelId: string): ActionFuncAsync> { + return async (dispatch, getState, {loaders}: any) => { + if (!loaders.profilesInChannelLoader) { + loaders.profilesInChannelLoader = new DelayedDataLoader({ + fetchBatch: (channelIds) => dispatch(getProfilesInChannel(channelIds[0], 0)), + maxBatchSize: 1, + wait: missingProfilesWait, + }); + } + + await loaders.profilesInChannelLoader.queueAndWait([channelId]); + return {}; + }; +} + export function getProfilesInGroupChannels(channelsIds: string[]): ActionFuncAsync { return async (dispatch, getState) => { let channelProfiles; diff --git a/webapp/channels/src/utils/test_intl.tsx b/webapp/channels/src/utils/test_intl.tsx new file mode 100644 index 0000000000..7ae98ffd5b --- /dev/null +++ b/webapp/channels/src/utils/test_intl.tsx @@ -0,0 +1,14 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import type {ReactNode} from 'react'; +import React from 'react'; +import {IntlProvider, createIntl} from 'react-intl'; + +export const defaultIntl = createIntl({ + locale: 'en', + defaultLocale: 'en', + messages: {}, +}); + +export const wrapIntl = (children?: ReactNode) => {children};