diff --git a/webapp/channels/src/actions/global_actions.tsx b/webapp/channels/src/actions/global_actions.tsx index ee73c3f1e8d..377c32b9f0b 100644 --- a/webapp/channels/src/actions/global_actions.tsx +++ b/webapp/channels/src/actions/global_actions.tsx @@ -23,6 +23,7 @@ import {appsEnabled} from 'mattermost-redux/selectors/entities/apps'; import {getCurrentChannelStats, getCurrentChannelId, getMyChannelMember, getRedirectChannelNameForTeam, getChannelsNameMapInTeam, getAllDirectChannels, getChannelMessageCount} from 'mattermost-redux/selectors/entities/channels'; import {getConfig, isPerformanceDebuggingEnabled} from 'mattermost-redux/selectors/entities/general'; import {getBool, getIsOnboardingFlowEnabled, isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; +import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts'; import {getCurrentTeamId, getMyTeams, getTeam, getMyTeamMember, getTeamMemberships, getActiveTeamsList} from 'mattermost-redux/selectors/entities/teams'; import {getCurrentUser, getCurrentUserId, isFirstAdmin} from 'mattermost-redux/selectors/entities/users'; import {calculateUnreadCount} from 'mattermost-redux/utils/channel_utils'; @@ -411,7 +412,9 @@ export async function redirectUserToDefaultTeam(searchParams?: URLSearchParams) if (team && team.delete_at === 0) { const channel = await getTeamRedirectChannelIfIsAccesible(user, team); if (channel) { - dispatch(fetchTeamScheduledPosts(team.id, true)); + if (isScheduledPostsEnabled(state)) { + dispatch(fetchTeamScheduledPosts(team.id, true)); + } dispatch(selectChannel(channel.id)); historyPushWithQueryParams(`/${team.name}/channels/${channel.name}`, searchParams); return; diff --git a/webapp/channels/src/actions/websocket_actions.test.jsx b/webapp/channels/src/actions/websocket_actions.test.jsx index 9837ad85b97..88d7554b1a3 100644 --- a/webapp/channels/src/actions/websocket_actions.test.jsx +++ b/webapp/channels/src/actions/websocket_actions.test.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import cloneDeep from 'lodash/cloneDeep'; + import {WebSocketEvents} from '@mattermost/client'; import {CloudTypes} from 'mattermost-redux/action_types'; @@ -705,8 +707,58 @@ describe('reconnect', () => { }); test('should reload custom profile attribute fields on reconnect', () => { + const clonedMockState = cloneDeep(mockState); + + mockState = mergeObjects( + mockState, + { + entities: { + general: { + license: { + SkuShortName: 'enterprise', + }, + config: { + FeatureFlagCustomProfileAttributes: 'true', + }, + }, + }, + }, + ); + reconnect(); expect(getCustomProfileAttributeFields).toHaveBeenCalled(); + + // Restore mock state + mockState = clonedMockState; + }); + + test.each([ + {SkuShortName: 'starter', FeatureFlagCustomProfileAttributes: 'true'}, + {SkuShortName: 'enterprise', FeatureFlagCustomProfileAttributes: 'false'}, + ])("should not reload custom profile attribute fields on reconnect if feature isn't available", ({SkuShortName, FeatureFlagCustomProfileAttributes}) => { + const clonedMockState = cloneDeep(mockState); + + mockState = mergeObjects( + mockState, + { + entities: { + general: { + license: { + SkuShortName, + }, + config: { + FeatureFlagCustomProfileAttributes, + }, + }, + }, + }, + ); + + reconnect(); + expect(getCustomProfileAttributeFields).not.toHaveBeenCalled(); + + // Restore mock state + mockState = clonedMockState; }); }); diff --git a/webapp/channels/src/actions/websocket_actions.ts b/webapp/channels/src/actions/websocket_actions.ts index 6b6675c1c89..1604bd2752d 100644 --- a/webapp/channels/src/actions/websocket_actions.ts +++ b/webapp/channels/src/actions/websocket_actions.ts @@ -107,11 +107,12 @@ import { hasAutotranslationBecomeEnabled, } from 'mattermost-redux/selectors/entities/channels'; import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common'; -import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general'; +import {getConfig, getLicense, isCustomProfileAttributesEnabled} from 'mattermost-redux/selectors/entities/general'; import {getGroup} from 'mattermost-redux/selectors/entities/groups'; import {getPost, getMostRecentPostIdInChannel, getTeamIdFromPost} from 'mattermost-redux/selectors/entities/posts'; import {isCollapsedThreadsEnabled} from 'mattermost-redux/selectors/entities/preferences'; import {haveISystemPermission, haveITeamPermission} from 'mattermost-redux/selectors/entities/roles'; +import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts'; import { getTeamIdByChannelId, getMyTeams, @@ -157,6 +158,7 @@ import {loadPlugin, loadPluginsIfNecessary, removePlugin} from 'plugins'; import {getHistory} from 'utils/browser_history'; import {ActionTypes, Constants, AnnouncementBarMessages, SocketEvents, UserStatuses, ModalIdentifiers, PageLoadContext} from 'utils/constants'; import {getIntl} from 'utils/i18n'; +import {isEnterpriseLicense} from 'utils/license_utils'; import {isChannelPopoutWindow} from 'utils/popouts/popout_windows'; import {getSiteURL} from 'utils/url'; @@ -274,7 +276,9 @@ export function reconnect() { } dispatch(fetchAllMyTeamsChannels()); - dispatch(fetchTeamScheduledPosts(currentTeamId, true, true)); + if (isScheduledPostsEnabled(state)) { + dispatch(fetchTeamScheduledPosts(currentTeamId, true, true)); + } dispatch(fetchAllMyChannelMembers()); dispatch(fetchMyCategories(currentTeamId)); loadProfilesForSidebar(); @@ -320,7 +324,9 @@ export function reconnect() { }); // Refresh custom profile attributes on reconnect - dispatch(getCustomProfileAttributeFields()); + if (isEnterpriseLicense(getLicense(state)) && isCustomProfileAttributesEnabled(state)) { + dispatch(getCustomProfileAttributeFields()); + } if (state.websocket.lastDisconnectAt) { dispatch(checkForModifiedUsers()); diff --git a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap index 5118f0ba203..8e4018cf372 100644 --- a/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap +++ b/webapp/channels/src/components/admin_console/system_user_detail/__snapshots__/system_user_detail.test.tsx.snap @@ -494,6 +494,253 @@ exports[`SystemUserDetail should match snapshot if MFA is enabled 1`] = ` `; +exports[`SystemUserDetail should not fetch CPA data if disabled 1`] = ` +
+
+ +
+
+
+
+ + +
+ User ID: user_id +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+

+ Team Membership +

+
+ Teams to which this user belongs +
+
+
+
+ +
+
+
+
+
+
+
+ No teams found +
+
+
+
+
+
+
+
+ +
+ +
+
+`; + exports[`SystemUserDetail should not show manage user settings button when user doesn't have permission 1`] = `
{ showManageUserSettings: false, showLockedManageUserSettings: false, mfaEnabled: false, + customProfileAttributeEnabled: true, customProfileAttributeFields: [], patchUser: jest.fn(), updateUserAuth: jest.fn(), @@ -135,6 +136,26 @@ describe('SystemUserDetail', () => { expect(container).toMatchSnapshot(); }); + test('should not fetch CPA data if disabled', async () => { + const getCustomProfileAttributeFields = jest.fn().mockResolvedValue({data: []}); + const getCustomProfileAttributeValues = jest.fn().mockResolvedValue({data: {}}); + + const props = { + ...defaultProps, + customProfileAttributeEnabled: false, + getCustomProfileAttributeFields, + getCustomProfileAttributeValues, + }; + const {container} = renderWithContext(); + + await waitForLoadingToFinish(); + + expect(getCustomProfileAttributeFields).not.toHaveBeenCalled(); + expect(getCustomProfileAttributeValues).not.toHaveBeenCalled(); + + expect(container).toMatchSnapshot(); + }); + describe('change detection', () => { test('should detect email changes and enable save', async () => { const userEventInstance = userEvent.setup(); diff --git a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx index 6f000154b7a..bc1ad52c21b 100644 --- a/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx +++ b/webapp/channels/src/components/admin_console/system_user_detail/system_user_detail.tsx @@ -180,13 +180,12 @@ export class SystemUserDetail extends PureComponent { try { // Fetch user data and CPA values in parallel - const [userResult, cpaResult] = await Promise.all([ + const [userResult, cpaValues] = await Promise.all([ this.props.getUser(userId) as ActionResult, - this.props.getCustomProfileAttributeValues(userId), + this.props.customProfileAttributeEnabled ? this.getCustomProfileAttributeValues(userId) : {}, ]); if (userResult.data) { - const cpaValues = (cpaResult as {data?: Record}).data || {}; this.setState({ user: userResult.data, emailField: userResult.data.email, // Set emailField to the email of the user for editing purposes @@ -211,6 +210,11 @@ export class SystemUserDetail extends PureComponent { } }; + getCustomProfileAttributeValues = async (userId: UserProfile['id']) => { + return this.props.getCustomProfileAttributeValues(userId). + then((result: { data?: Record }) => result.data || {}); + }; + componentDidMount() { const userId = this.props.match.params.user_id ?? ''; if (userId) { @@ -219,7 +223,7 @@ export class SystemUserDetail extends PureComponent { } // Fetch CPA field definitions if not already available - if (this.props.customProfileAttributeFields.length === 0) { + if (this.props.customProfileAttributeEnabled && this.props.customProfileAttributeFields.length === 0) { this.props.getCustomProfileAttributeFields(); } } @@ -232,6 +236,24 @@ export class SystemUserDetail extends PureComponent { if (hasChanges !== hadChanges) { this.props.setNavigationBlocked(hasChanges); } + + // Fetch CPA field definitions if CPA has been enabled + const hasCpaBeenEnabled = !prevProps.customProfileAttributeEnabled && this.props.customProfileAttributeEnabled; + if (hasCpaBeenEnabled) { + if (this.state.user) { + this.getCustomProfileAttributeValues(this.state.user.id). + then((cpaValues) => { + this.setState({ + customProfileAttributeValues: cpaValues, + originalCpaValues: {...cpaValues}, // Deep copy for change tracking + }); + }); + } + + if (this.props.customProfileAttributeFields.length === 0) { + this.props.getCustomProfileAttributeFields(); + } + } } private hasUnsavedChanges = (state: State = this.state): boolean => { @@ -248,7 +270,12 @@ export class SystemUserDetail extends PureComponent { }; private hasCpaChanges = (state: State = this.state): boolean => { - const {customProfileAttributeFields} = this.props; + const {customProfileAttributeEnabled, customProfileAttributeFields} = this.props; + + if (!customProfileAttributeEnabled) { + return false; + } + for (const field of customProfileAttributeFields) { const currentValue = state.customProfileAttributeValues[field.id]; const originalValue = state.originalCpaValues[field.id]; diff --git a/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx b/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx index 769365d1dac..9116d42bcb3 100644 --- a/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx +++ b/webapp/channels/src/components/drafts/drafts_link/drafts_link.test.tsx @@ -4,6 +4,7 @@ import React from 'react'; import {MemoryRouter, Route} from 'react-router-dom'; +import type {GeneralState} from '@mattermost/types/general'; import type {DeepPartial} from '@mattermost/types/utilities'; import {renderWithContext, screen, waitFor} from 'tests/react_testing_utils'; @@ -201,6 +202,27 @@ describe('components/drafts/drafts_link', () => { }); }); + it.each>([ + {config: {ScheduledPosts: 'false'}}, + {license: {IsLicensed: 'false'}}, + ])('should not fetch scheduled posts when component mounts if disabled', async (partialConf) => { + const fetchTeamScheduledPosts = require('mattermost-redux/actions/scheduled_posts').fetchTeamScheduledPosts; + const state: DeepPartial = { + ...baseState, + entities: { + ...baseState.entities, + general: { + ...baseState.entities?.general, + ...partialConf, + }, + }, + }; + + renderWithRouter(state); + + expect(fetchTeamScheduledPosts).not.toHaveBeenCalled(); + }); + it('should be active when on drafts route', () => { const state: DeepPartial = { ...baseState, diff --git a/webapp/channels/src/components/thread_popout/thread_popout.tsx b/webapp/channels/src/components/thread_popout/thread_popout.tsx index f87c2bec5d0..fb2c567b87c 100644 --- a/webapp/channels/src/components/thread_popout/thread_popout.tsx +++ b/webapp/channels/src/components/thread_popout/thread_popout.tsx @@ -16,6 +16,7 @@ import {selectTeam} from 'mattermost-redux/actions/teams'; import {getThread} from 'mattermost-redux/actions/threads'; import {getProfilesByIds} from 'mattermost-redux/actions/users'; import {getChannel, getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; +import {isScheduledPostsEnabled} from 'mattermost-redux/selectors/entities/scheduled_posts'; import {getTeamByName} from 'mattermost-redux/selectors/entities/teams'; import {makeGetThreadOrSynthetic} from 'mattermost-redux/selectors/entities/threads'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; @@ -72,6 +73,7 @@ export default function ThreadPopout() { } return getThreadOrSynthetic(state, post); }); + const isScheduledPostEnabled = useSelector(isScheduledPostsEnabled); usePopoutTitle(getThreadPopoutTitle(channel)); @@ -92,10 +94,12 @@ export default function ThreadPopout() { useEffect(() => { if (teamId) { dispatch(fetchChannelsAndMembers(teamId)); - dispatch(fetchTeamScheduledPosts(teamId, true)); + if (isScheduledPostEnabled) { + dispatch(fetchTeamScheduledPosts(teamId, true)); + } dispatch(selectTeam(teamId)); } - }, [dispatch, teamId]); + }, [dispatch, teamId, isScheduledPostEnabled]); useEffect(() => { if (teamId) { diff --git a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts index db3518523ab..abaf67168e5 100644 --- a/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts +++ b/webapp/channels/src/packages/mattermost-redux/src/selectors/entities/general.ts @@ -22,6 +22,10 @@ export function getFeatureFlagValue(state: GlobalState, key: keyof FeatureFlags) return getConfig(state)?.[`FeatureFlag${key}` as keyof Partial]; } +export function isCustomProfileAttributesEnabled(state: GlobalState): boolean { + return getConfig(state).FeatureFlagCustomProfileAttributes === 'true'; +} + export type PasswordConfig = { minimumLength: number; requireLowercase: boolean;