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`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
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;