[MM-69587] Remove CustomProfileAttributes feature flag (#37389)

* [MM-69587] Remove CustomProfileAttributes feature flag

Remove the CustomProfileAttributes feature flag and all associated
conditional gating, leaving custom profile attributes permanently
enabled (still Enterprise-license gated).

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* [MM-69587] Update admin sidebar snapshots for permanent CPA

User Attributes now appears based solely on license tier since the
feature flag no longer gates it.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Update channel_header snapshot styled-components icon hash after master merge

Align pending-join-requests snapshot with the Icon-clPswv hash used by all
other ChannelHeader snapshots; CI received this after merging discoverable
channels from master.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* chore: retrigger CI after unrelated Postgres shard 1 flake

Server CI Postgres (shard 1) failed on TestCheckLdapUserPasswordConcurrency
(mock DoLogin panic under concurrent On/Called) and a one-shot
TestLicenseFromBytesEnvironmentMismatch mismatch that passed on re-run.
Neither touches CustomProfileAttributes removal; empty commit to retrigger.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
This commit is contained in:
cursor[bot]
2026-08-05 16:28:54 -03:00
committed by GitHub
co-authored by mattermost-code Cursor Agent
parent f9227b1a2f
commit 4eac9292c0
25 changed files with 283 additions and 175 deletions
@@ -805,7 +805,6 @@ const defaultServerConfig: AdminConfig = {
EnableExportDirectDownload: false,
MoveThreadsEnabled: false,
NotificationMonitoring: true,
CustomProfileAttributes: true,
AttributeValueMasking: false,
PermissionPolicies: false,
ChannelPermissionPolicies: false,
@@ -22,7 +22,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, team, user, userClient} = await pw.initSetup();
const suffix = pw.random.id();
const labels = [`First ${suffix}`, `Second ${suffix}`, `Third ${suffix}`];
@@ -64,7 +63,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, team, user, userClient} = await pw.initSetup();
const suffix = pw.random.id();
const attributes: CustomProfileAttribute[] = Array.from({length: 10}, (_, index) => ({
@@ -18,7 +18,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, user, userClient} = await pw.initSetup();
const displayName = '40_characters_allowed_000000000000000000';
const value = '64-characters-allowed-000000000000000000123456789012345678901234';
@@ -56,7 +55,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, user, userClient} = await pw.initSetup();
const suffix = pw.random.id();
const displayName = `Cancel Test ${suffix}`;
@@ -97,7 +95,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, user, userClient} = await pw.initSetup();
const displayName = `Favorite Color ${pw.random.id()}`;
const attributes: CustomProfileAttribute[] = [
@@ -63,11 +63,6 @@ test.describe('System Console - Admin User Profile Editing', () => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
// Fast-fail if CustomProfileAttributes feature flag is off — prevents a
// misleading 30 s timeout on the UI assertion and gives a clear skip reason.
// Note: default_config.ts sets this to true, so it should always pass in CI.
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
// Self-isolating setup — avoid pw.initSetup()'s destructive
// adminClient.updateConfig() full-config reset which wipes CPA fields mid-run
// for other concurrent tests in the same worker pool. Create a uniquely-named
@@ -35,7 +35,6 @@ test.describe('System Console - Ranked value picker', () => {
test.beforeEach(async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const clientInfo = await pw.getAdminClient();
adminClient = clientInfo.adminClient;
@@ -21,7 +21,6 @@ function matchingOrder(actual: string[], expected: string[]) {
test('MM-T5745 navigates to user attributes page and displays empty state', {tag: '@user_attributes'}, async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, adminUser} = await pw.initSetup();
// Custom profile attribute fields are global, so other specs may have left fields behind
@@ -69,7 +68,6 @@ test('MM-T5745 navigates to user attributes page and displays empty state', {tag
test('MM-T5746 adds, edits, and deletes a user attribute', {tag: '@user_attributes'}, async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, adminUser} = await pw.initSetup();
const {systemConsolePage} = await pw.testBrowser.login(adminUser);
const sp = systemConsolePage.systemProperties;
@@ -118,7 +116,6 @@ test(
async ({pw}) => {
await pw.ensureLicense();
await pw.skipIfNoLicense();
await pw.skipIfFeatureFlagNotSet('CustomProfileAttributes', true);
const {adminClient, adminUser, team, user} = await pw.initSetup();
const suffix = pw.random.id();
const labels = [`First ${suffix}`, `Second ${suffix}`, `Favorite Food ${suffix}`];
@@ -19,16 +19,14 @@ import (
)
func (api *API) InitCustomProfileAttributes() {
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributes.Handle("/group", api.APISessionRequired(getCPAGroup)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
}
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributes.Handle("/group", api.APISessionRequired(getCPAGroup)).Methods(http.MethodGet)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(patchCPAValuesForUser)).Methods(http.MethodPatch)
}
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -6,13 +6,11 @@ package api4
import "net/http"
func (api *API) InitCustomProfileAttributesLocal() {
if api.srv.Config().FeatureFlags.CustomProfileAttributes {
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(listCPAFields)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(createCPAField)).Methods(http.MethodPost)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(patchCPAField)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(listCPAValues)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APILocal(patchCPAValues)).Methods(http.MethodPatch)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(patchCPAValuesForUser)).Methods(http.MethodPatch)
}
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(listCPAFields)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(createCPAField)).Methods(http.MethodPost)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(patchCPAField)).Methods(http.MethodPatch)
api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(listCPAValues)).Methods(http.MethodGet)
api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APILocal(patchCPAValues)).Methods(http.MethodPatch)
api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APILocal(patchCPAValuesForUser)).Methods(http.MethodPatch)
}
@@ -27,9 +27,7 @@ func celSafeName() string {
func TestCreateCPAField(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
})
th := Setup(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
field := &model.PropertyField{Name: celSafeName(), Type: model.PropertyFieldTypeText}
@@ -139,9 +137,7 @@ func TestCreateCPAField(t *testing.T) {
func TestCPAFieldLimit(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
// Create 20 fields — the maximum allowed by FieldLimitHook.
@@ -185,9 +181,7 @@ func TestCPAFieldLimit(t *testing.T) {
func TestListCPAFields(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
})
th := Setup(t)
// License required for field creation (LicenseCheckHook)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -238,7 +232,6 @@ func TestListCPAFields(t *testing.T) {
func TestPatchCPAField(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
cfg.FeatureFlags.PropertyFieldRank = true
})
@@ -590,9 +583,7 @@ func TestPatchCPAField(t *testing.T) {
func TestDeleteCPAField(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
})
th := Setup(t)
th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
// Create a field with a license so we can test the license check on delete.
@@ -677,9 +668,7 @@ func TestDeleteCPAField(t *testing.T) {
func TestListCPAValues(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
// License required for field/value creation (LicenseCheckHook)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -774,9 +763,7 @@ func TestListCPAValues(t *testing.T) {
func TestPatchCPAValues(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
// License required for field creation (LicenseCheckHook)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1178,9 +1165,7 @@ func TestPatchCPAValues(t *testing.T) {
func TestPatchCPAValuesForUser(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
// License required for field creation (LicenseCheckHook)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1578,7 +1563,6 @@ func TestPatchCPAValuesForUser(t *testing.T) {
func TestCPANonAdminWriteOwnValueViaGenericAPI(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1640,7 +1624,6 @@ func TestCPANonAdminWriteOwnValueViaGenericAPI(t *testing.T) {
func TestCPANonAdminBlockedFromAdminManagedViaGenericAPI(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1705,7 +1688,6 @@ func TestCPANonAdminBlockedFromAdminManagedViaGenericAPI(t *testing.T) {
func TestCPACrossAPIFieldRoundtrip(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1824,7 +1806,6 @@ func TestCPACrossAPIFieldRoundtrip(t *testing.T) {
func TestCPABackwardCompatAfterRefactor(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
cfg.FeatureFlags.IntegratedBoards = true
}).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1919,9 +1900,7 @@ func TestCPABackwardCompatAfterRefactor(t *testing.T) {
func TestOwnerManagedCPAFieldHumanValueWrites(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -1985,9 +1964,7 @@ func TestOwnerManagedCPAFieldHumanValueWrites(t *testing.T) {
func TestSysadminManagesCPAFieldOwners(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
@@ -114,9 +114,7 @@ func TestGetCPAValue(t *testing.T) {
func TestDeleteCPAValues(t *testing.T) {
mainHelper.Parallel(t)
th := SetupConfig(t, func(cfg *model.Config) {
cfg.FeatureFlags.CustomProfileAttributes = true
}).InitBasic(t)
th := Setup(t).InitBasic(t)
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterprise))
cpaGroup, groupErr := th.App.GetPropertyGroup(request.TestContext(t), model.AccessControlPropertyGroupName)
-3
View File
@@ -36,8 +36,6 @@ type FeatureFlags struct {
NotificationMonitoring bool
CustomProfileAttributes bool
// Mask non-held attribute values in the policy editor for delegated admins.
AttributeValueMasking bool
@@ -167,7 +165,6 @@ func (f *FeatureFlags) SetDefaults() {
f.EnableExportDirectDownload = false
f.MoveThreadsEnabled = false
f.NotificationMonitoring = true
f.CustomProfileAttributes = true
f.AttributeValueMasking = true
f.PermissionPolicies = true
f.TeamMembershipAccessControl = true
@@ -895,9 +895,6 @@ describe('reconnect', () => {
license: {
SkuShortName: 'enterprise',
},
config: {
FeatureFlagCustomProfileAttributes: 'true',
},
},
},
},
@@ -911,9 +908,9 @@ describe('reconnect', () => {
});
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}) => {
{SkuShortName: 'starter'},
{SkuShortName: 'professional'},
])('should not reload custom profile attribute fields on reconnect without an Enterprise license', ({SkuShortName}) => {
const clonedMockState = cloneDeep(mockState);
mockState = mergeObjects(
@@ -924,9 +921,6 @@ describe('reconnect', () => {
license: {
SkuShortName,
},
config: {
FeatureFlagCustomProfileAttributes,
},
},
},
},
@@ -116,7 +116,7 @@ import {
hasAutotranslationBecomeEnabled,
} from 'mattermost-redux/selectors/entities/channels';
import {getIsUserStatusesConfigEnabled} from 'mattermost-redux/selectors/entities/common';
import {getConfig, getFeatureFlagValue, getLicense, isCustomProfileAttributesEnabled} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getFeatureFlagValue, getLicense} 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';
@@ -343,7 +343,7 @@ export function reconnect() {
});
// Refresh custom profile attributes on reconnect
if (isEnterpriseLicense(getLicense(state)) && isCustomProfileAttributesEnabled(state)) {
if (isEnterpriseLicense(getLicense(state))) {
dispatch(getCustomProfileAttributeFields());
}
@@ -639,10 +639,7 @@ const AdminDefinition: AdminDefinitionType = {
url: 'system_attributes/user_attributes',
title: defineMessage({id: 'admin.sidebar.user_attributes', defaultMessage: 'User Attributes'}),
searchableStrings: systemPropertiesSearchableStrings,
isHidden: it.not(it.all(
it.minLicenseTier(LicenseSkus.Enterprise),
it.configIsTrue('FeatureFlags', 'CustomProfileAttributes'),
)),
isHidden: it.not(it.minLicenseTier(LicenseSkus.Enterprise)),
schema: {
id: 'SystemProperties',
component: SystemProperties,
@@ -652,10 +649,7 @@ const AdminDefinition: AdminDefinitionType = {
url: 'system_attributes/user_attributes',
isDiscovery: true,
title: defineMessage({id: 'admin.sidebar.user_attributes', defaultMessage: 'User Attributes'}),
isHidden: it.any(
it.minLicenseTier(LicenseSkus.Enterprise),
it.configIsFalse('FeatureFlags', 'CustomProfileAttributes'),
),
isHidden: it.minLicenseTier(LicenseSkus.Enterprise),
schema: {
id: 'SystemProperties',
name: defineMessage({id: 'admin.sidebar.user_attributes', defaultMessage: 'User Attributes'}),
@@ -4803,10 +4797,7 @@ const AdminDefinition: AdminDefinitionType = {
type: 'custom',
key: 'SamlSettings.CustomProfileAttributes',
component: CustomProfileAttributes,
isHidden: it.not(it.all(
it.minLicenseTier(LicenseSkus.Enterprise),
it.configIsTrue('FeatureFlags', 'CustomProfileAttributes'),
)),
isHidden: it.not(it.minLicenseTier(LicenseSkus.Enterprise)),
},
{
type: 'text',
@@ -546,10 +546,7 @@ export const ldapWizardAdminDefinition: LDAPAdminDefinitionConfigSchemaSettings
type: 'custom',
key: 'LdapSettings.CustomProfileAttributes',
component: CustomProfileAttributes,
isHidden: it.not(it.all(
it.minLicenseTier(LicenseSkus.Enterprise),
it.configIsTrue('FeatureFlags', 'CustomProfileAttributes'),
)),
isHidden: it.not(it.minLicenseTier(LicenseSkus.Enterprise)),
},
],
},
@@ -832,6 +832,40 @@ exports[`components/AdminSidebar should match snapshot 1`] = `
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -6356,6 +6390,40 @@ exports[`components/AdminSidebar should match snapshot with license with profess
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -8154,6 +8222,40 @@ exports[`components/AdminSidebar should match snapshot with workspace optimizati
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -10042,6 +10144,40 @@ exports[`components/AdminSidebar should match snapshot, not prevent the console
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -11821,6 +11957,40 @@ exports[`components/AdminSidebar should match snapshot, render plugins without a
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -13570,6 +13740,40 @@ exports[`components/AdminSidebar should match snapshot, with license (with all f
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -15290,6 +15494,40 @@ exports[`components/AdminSidebar should match snapshot, with license (without an
class="sections"
data-testid="sidebar-category-sections"
>
<li
class="sidebar-section"
data-testid="system_attributes.user_attributes_feature_discovery"
>
<a
class="sidebar-section-title"
href="/admin_console/system_attributes/user_attributes"
id="system_attributes/user_attributes"
>
<span
class="sidebar-section-title__text"
>
User Attributes
</span>
<span
class="sidebar-section-indicator"
>
<span
class="RestrictedIndicator__icon-tooltip-container"
>
<div
class="RestrictedIndicator__content"
>
<i
class="RestrictedIndicator__icon-tooltip icon icon-key-variant"
/>
</div>
</span>
</span>
</a>
<ul
class="nav nav__sub-menu subsections"
/>
</li>
<li
class="sidebar-section"
data-testid="system_attributes.attribute_based_access_control_feature_discovery"
@@ -397,9 +397,7 @@ describe('components/AdminSidebar', () => {
Secret: 'office365Secret',
Scope: 'scope',
} as Office365Settings,
FeatureFlags: {
CustomProfileAttributes: true,
},
FeatureFlags: {},
},
adminDefinition: AdminDefinition,
buildEnterpriseReady: true,
@@ -529,9 +527,7 @@ describe('components/AdminSidebar', () => {
Secret: 'office365Secret',
Scope: 'scope',
} as Office365Settings,
FeatureFlags: {
CustomProfileAttributes: true,
},
FeatureFlags: {},
},
adminDefinition: AdminDefinition,
buildEnterpriseReady: true,
@@ -10,7 +10,7 @@ import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general'
import {getUserPreferences} from 'mattermost-redux/actions/preferences';
import {addUserToTeam} from 'mattermost-redux/actions/teams';
import {updateUserActive, updateUserAuth, getUser, patchUser, updateUserMfa, getCustomProfileAttributeValues, saveCustomProfileAttribute} from 'mattermost-redux/actions/users';
import {getConfig, getCustomProfileAttributes, getLicense, isCustomProfileAttributesEnabled} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getCustomProfileAttributes, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {setNavigationBlocked} from 'actions/admin_actions';
@@ -24,7 +24,7 @@ import SystemUserDetail from './system_user_detail';
function mapStateToProps(state: GlobalState) {
const license = getLicense(state);
const config = getConfig(state);
const customProfileAttributeEnabled = isEnterpriseLicense(license) && isCustomProfileAttributesEnabled(state);
const customProfileAttributeEnabled = isEnterpriseLicense(license);
const customProfileAttributeFields = customProfileAttributeEnabled ? getCustomProfileAttributes(state) : [];
const showManageUserSettings = getShowManageUserSettings(state);
@@ -62,9 +62,6 @@ describe('LoggedIn mapStateToProps', () => {
license: {
IsLicensed: 'false',
} as ClientLicense,
config: {
FeatureFlagCustomProfileAttributes: 'true',
},
},
},
});
@@ -74,7 +71,7 @@ describe('LoggedIn mapStateToProps', () => {
expect(props.customProfileAttributesEnabled).toBe(false);
});
it('should be false when Enterprise license but feature flag disabled', () => {
it('should be true when Enterprise license', () => {
const state = mergeObjects(baseState, {
entities: {
general: {
@@ -82,47 +79,6 @@ describe('LoggedIn mapStateToProps', () => {
IsLicensed: 'true',
SkuShortName: LicenseSkus.Enterprise,
} as ClientLicense,
config: {
FeatureFlagCustomProfileAttributes: 'false',
},
},
},
});
const props = mapStateToProps(state, baseProps);
expect(props.customProfileAttributesEnabled).toBe(false);
});
it('should be false when Enterprise license but feature flag missing', () => {
const state = mergeObjects(baseState, {
entities: {
general: {
license: {
IsLicensed: 'true',
SkuShortName: LicenseSkus.Enterprise,
} as ClientLicense,
config: {},
},
},
});
const props = mapStateToProps(state, baseProps);
expect(props.customProfileAttributesEnabled).toBe(false);
});
it('should be true when Enterprise license and feature flag enabled', () => {
const state = mergeObjects(baseState, {
entities: {
general: {
license: {
IsLicensed: 'true',
SkuShortName: LicenseSkus.Enterprise,
} as ClientLicense,
config: {
FeatureFlagCustomProfileAttributes: 'true',
},
},
},
});
@@ -137,9 +93,6 @@ describe('LoggedIn mapStateToProps', () => {
entities: {
general: {
license: {} as ClientLicense,
config: {
FeatureFlagCustomProfileAttributes: 'true',
},
},
},
});
@@ -159,9 +112,6 @@ describe('LoggedIn mapStateToProps', () => {
IsLicensed: 'true',
SkuShortName: LicenseSkus.Enterprise,
} as ClientLicense,
config: {
FeatureFlagCustomProfileAttributes: 'true',
},
},
},
});
@@ -9,7 +9,7 @@ import {updateApproximateViewTime} from 'mattermost-redux/actions/channels';
import {getCustomProfileAttributeFields} from 'mattermost-redux/actions/general';
import {autoUpdateTimezone} from 'mattermost-redux/actions/timezone';
import {getChannel, getCurrentChannelId, isManuallyUnread} from 'mattermost-redux/selectors/entities/channels';
import {getLicense, getConfig, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUser, shouldShowTermsOfService} from 'mattermost-redux/selectors/entities/users';
import {getChannelURL} from 'selectors/urls';
@@ -41,7 +41,7 @@ export function mapStateToProps(state: GlobalState, ownProps: Props) {
isCurrentChannelManuallyUnread: isManuallyUnread(state, currentChannelId),
mfaRequired: checkIfMFARequired(getCurrentUser(state), license, config, ownProps.match.url),
showTermsOfService,
customProfileAttributesEnabled: isEnterpriseLicense(license) && getFeatureFlagValue(state, 'CustomProfileAttributes') === 'true',
customProfileAttributesEnabled: isEnterpriseLicense(license),
};
}
@@ -599,7 +599,6 @@ describe('components/ProfilePopover', () => {
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
@@ -628,7 +627,6 @@ describe('components/ProfilePopover', () => {
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
@@ -661,7 +659,6 @@ describe('components/ProfilePopover', () => {
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
@@ -689,7 +686,6 @@ describe('components/ProfilePopover', () => {
test('should not display attributes if user attributes is null', async () => {
const [props, initialState] = getBasePropsAndState();
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
@@ -716,7 +712,6 @@ describe('components/ProfilePopover', () => {
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'false',
SkuShortName: '',
@@ -744,7 +739,6 @@ describe('components/ProfilePopover', () => {
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.license = {
IsLicensed: 'true',
SkuShortName: 'enterprise',
@@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getLicense, getFeatureFlagValue} from 'mattermost-redux/selectors/entities/general';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {getCurrentRelativeTeamUrl, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
import {getStatusForUserId, getUser} from 'mattermost-redux/selectors/entities/users';
@@ -82,7 +82,7 @@ const ProfilePopover = ({
const currentUserId = useSelector(getCurrentUserId);
const license = useSelector((state: GlobalState) => getLicense(state));
const isEnterprise = isEnterpriseLicense(license);
const enableCustomProfileAttributes = useSelector((state: GlobalState) => getFeatureFlagValue(state, 'CustomProfileAttributes') === 'true' && isEnterprise && !fromWebhook);
const enableCustomProfileAttributes = isEnterprise && !fromWebhook;
const [loadingDMChannel, setLoadingDMChannel] = useState<string>();
@@ -15,7 +15,7 @@ import {
getCustomProfileAttributeValues,
} from 'mattermost-redux/actions/users';
import {Permissions} from 'mattermost-redux/constants';
import {getConfig, getCustomProfileAttributes, getFeatureFlagValue, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getCustomProfileAttributes, getLicense} from 'mattermost-redux/selectors/entities/general';
import {haveISystemPermission} from 'mattermost-redux/selectors/entities/roles';
import {getIsMobileView} from 'selectors/views/browser';
@@ -46,7 +46,7 @@ function mapStateToProps(state: GlobalState) {
const license = getLicense(state);
const isEnterprise = isEnterpriseLicense(license);
const enableCustomProfileAttributes = isEnterprise && getFeatureFlagValue(state, 'CustomProfileAttributes') === 'true';
const enableCustomProfileAttributes = isEnterprise;
return {
isMobileView: getIsMobileView(state),
@@ -22,10 +22,6 @@ export function getFeatureFlagValue(state: GlobalState, key: keyof FeatureFlags)
return getConfig(state)?.[`FeatureFlag${key}` as keyof Partial<ClientConfig>];
}
export function isCustomProfileAttributesEnabled(state: GlobalState): boolean {
return getConfig(state).FeatureFlagCustomProfileAttributes === 'true';
}
export function isPostAttributesEnabled(state: GlobalState): boolean {
return getConfig(state).FeatureFlagPostAttributes === 'true';
}
-1
View File
@@ -129,7 +129,6 @@ export type ClientConfig = {
FileLevel: string;
FeatureFlagAppsEnabled: string;
FeatureFlagCallsEnabled: string;
FeatureFlagCustomProfileAttributes: string;
FeatureFlagTeamMembershipAccessControl: string;
FeatureFlagPermissionPolicies: string;
FeatureFlagChannelPermissionPolicies: string;