From 5df67ff2937f0ad1df4758a3684ca1c2dcfbb9ef Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 21:56:13 +0000 Subject: [PATCH 1/4] MM-68957 Hide valueless profile attributes Co-authored-by: mattermost-code --- .../custom_attributes.spec.ts | 15 +--- ...profile_popover_custom_attributes.test.tsx | 89 +++++++++++++++++++ .../profile_popover_custom_attributes.tsx | 53 ++++++----- 3 files changed, 122 insertions(+), 35 deletions(-) diff --git a/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/custom_attributes.spec.ts b/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/custom_attributes.spec.ts index b57116b3435..1754589383c 100644 --- a/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/custom_attributes.spec.ts +++ b/e2e-tests/playwright/specs/functional/channels/custom_profile_attributes/custom_attributes.spec.ts @@ -264,7 +264,7 @@ test('MM-T5776 Hide custom profile attributes when visibility is set to hidden @ /** * Verify that custom profile attributes with visibility set to always - * are displayed in the profile popover even if they have no value. + * are not displayed in the profile popover if they have no value. * * Precondition: * 1. A test server with valid license to support 'Custom Profile Attributes' @@ -272,7 +272,7 @@ test('MM-T5776 Hide custom profile attributes when visibility is set to hidden @ * 3. Other user has values set for custom profile attributes * 4. Two user accounts exist and are members of the same channel */ -test('MM-T5777 Always display custom profile attributes with visibility set to always @custom_profile_attributes', async ({ +test('MM-T5777 Do not display valueless custom profile attributes with visibility set to always @custom_profile_attributes', async ({ pw, }) => { // 1. Update the visibility of the Title attribute to always @@ -289,16 +289,9 @@ test('MM-T5777 Always display custom profile attributes with visibility set to a const lastPost = await channelsPage.getLastPost(); await channelsPage.openProfilePopover(lastPost); - // * Verify custom attributes are displayed correctly + // * Verify custom attributes without values are not displayed for (const attribute of customAttributes) { - if (attribute.name === 'Title') { - // * Verify the Title attribute is displayed even though it has no value - const popover = channelsPage.userProfilePopover.container; - const nameElement = popover.getByText('Title', {exact: false}); - await expect(nameElement).toBeVisible(); - } else { - await verifyAttributeNotInPopover(channelsPage, attribute.name); - } + await verifyAttributeNotInPopover(channelsPage, attribute.name); } }); diff --git a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx index 589795dabf1..34295035a82 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx @@ -279,6 +279,95 @@ describe('components/ProfilePopoverCustomAttributes', () => { expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument(); }); + test('should not render always-visible attribute labels when the value is missing', () => { + const state = { + ...baseState, + entities: { + ...baseState.entities, + users: { + profiles: { + user_id: TestHelper.getUserMock({ + id: 'user_id', + custom_profile_attributes: { + phone_attribute_id: '+1 (555) 123-4567', + url_attribute_id: 'https://example.com', + select_attribute_id: 'option1', + }, + }), + }, + }, + general: { + ...baseState.entities.general, + customProfileAttributes: { + ...baseState.entities.general.customProfileAttributes, + text_attribute_id: { + ...textAttribute, + attrs: { + ...textAttribute.attrs, + visibility: 'always', + }, + }, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument(); + expect(screen.getByText('Phone Number')).toBeInTheDocument(); + }); + + test('should not render always-visible select labels when the value is not displayable', () => { + const state = { + ...baseState, + entities: { + ...baseState.entities, + users: { + profiles: { + user_id: TestHelper.getUserMock({ + id: 'user_id', + custom_profile_attributes: { + ...userProfile.custom_profile_attributes, + select_attribute_id: 'filtered-option', + }, + }), + }, + }, + general: { + ...baseState.entities.general, + customProfileAttributes: { + ...baseState.entities.general.customProfileAttributes, + select_attribute_id: { + ...selectAttribute, + attrs: { + ...selectAttribute.attrs, + visibility: 'always', + }, + }, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.queryByText('Select Attribute')).not.toBeInTheDocument(); + expect(screen.queryByText('filtered-option')).not.toBeInTheDocument(); + }); + test('should render display_name as the visible label when set', () => { const state = { ...baseState, diff --git a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx index d29a5ff9672..dc8a09279db 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx @@ -4,7 +4,7 @@ import React, {useEffect} from 'react'; import {useDispatch, useSelector} from 'react-redux'; -import type {UserPropertyValueType} from '@mattermost/types/properties'; +import type {UserPropertyField, UserPropertyValueType} from '@mattermost/types/properties'; import {getCustomProfileAttributeValues} from 'mattermost-redux/actions/users'; import {getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general'; @@ -23,6 +23,33 @@ type Props = { userID: string; hideStatus?: boolean; } + +const hasDisplayableAttributeValue = (attribute: UserPropertyField, customProfileAttributes: Record): boolean => { + const attributeValue = customProfileAttributes[attribute.id]; + if (Array.isArray(attributeValue)) { + if (attributeValue.length === 0) { + return false; + } + } else if (!attributeValue) { + return false; + } + + if (attribute.type === 'multiselect' || attribute.type === 'select') { + const options = attribute.attrs.options; + if (!options?.length) { + return false; + } + + if (Array.isArray(attributeValue)) { + return attributeValue.some((value) => options.some((option) => option.id === value)); + } + + return options.some((option) => option.id === attributeValue); + } + + return true; +}; + const ProfilePopoverCustomAttributes = ({ userID, hideStatus = false, @@ -49,30 +76,8 @@ const ProfilePopoverCustomAttributes = ({ return null; } - // Check if the attribute has a value - const hasValue = userProfile.custom_profile_attributes[attribute.id]?.length > 0; - - if (!hasValue && visibility === 'when_set') { + if (!hasDisplayableAttributeValue(attribute, userProfile.custom_profile_attributes)) { return null; - } else if (visibility === 'when_set' && (attribute.type === 'multiselect' || attribute.type === 'select')) { - const attributeValue = userProfile.custom_profile_attributes[attribute.id]; - - // make sure attribute contains legitimate values - if (Array.isArray(attributeValue)) { - // Handle multiselect - const options = attributeValue.map((value) => { - return attribute.attrs.options?.find((o) => o.id === value); - }).filter((o) => o != null); - if (options.length === 0) { - return null; - } - } else { - // Handle single select - const option = attribute.attrs.options?.find((o) => o.id === attributeValue); - if (option === undefined) { - return null; - } - } } const valueType = (attribute.attrs?.value_type as UserPropertyValueType) || ''; From 22d060f3eb35861c185d4ffe6c3e27f0a7abc5c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 22:00:56 +0000 Subject: [PATCH 2/4] MM-68957 Cover masked profile attributes Co-authored-by: mattermost-code --- ...profile_popover_custom_attributes.test.tsx | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx index 34295035a82..b35c4a8267e 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx @@ -279,7 +279,7 @@ describe('components/ProfilePopoverCustomAttributes', () => { expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument(); }); - test('should not render always-visible attribute labels when the value is missing', () => { + test('should not render shared-only always-visible attribute labels when the value is missing', () => { const state = { ...baseState, entities: { @@ -305,6 +305,7 @@ describe('components/ProfilePopoverCustomAttributes', () => { attrs: { ...textAttribute.attrs, visibility: 'always', + access_mode: 'shared_only', }, }, }, @@ -368,6 +369,65 @@ describe('components/ProfilePopoverCustomAttributes', () => { expect(screen.queryByText('filtered-option')).not.toBeInTheDocument(); }); + test('should render multiselect labels only when at least one value is displayable', () => { + const hiddenMultiselectAttribute: UserPropertyField = { + ...selectAttribute, + id: 'hidden_multiselect_attribute_id', + name: 'Hidden Multiselect Attribute', + type: 'multiselect', + attrs: { + ...selectAttribute.attrs, + visibility: 'always', + }, + }; + const visibleMultiselectAttribute: UserPropertyField = { + ...selectAttribute, + id: 'visible_multiselect_attribute_id', + name: 'Visible Multiselect Attribute', + type: 'multiselect', + attrs: { + ...selectAttribute.attrs, + visibility: 'always', + }, + }; + const state = { + ...baseState, + entities: { + ...baseState.entities, + users: { + profiles: { + user_id: TestHelper.getUserMock({ + id: 'user_id', + custom_profile_attributes: { + hidden_multiselect_attribute_id: ['filtered-option'], + visible_multiselect_attribute_id: ['filtered-option', 'option1'], + }, + }), + }, + }, + general: { + ...baseState.entities.general, + customProfileAttributes: { + hidden_multiselect_attribute_id: hiddenMultiselectAttribute, + visible_multiselect_attribute_id: visibleMultiselectAttribute, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.queryByText('Hidden Multiselect Attribute')).not.toBeInTheDocument(); + expect(screen.getByText('Visible Multiselect Attribute')).toBeInTheDocument(); + expect(screen.getByText('Option 1')).toBeInTheDocument(); + }); + test('should render display_name as the visible label when set', () => { const state = { ...baseState, From fd2a40028b9685ccfbf414889922fb3195257ef2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 22:03:45 +0000 Subject: [PATCH 3/4] MM-68957 Assert masked multiselect values stay hidden Co-authored-by: mattermost-code --- .../profile_popover/profile_popover_custom_attributes.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx index b35c4a8267e..50956c48508 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.test.tsx @@ -426,6 +426,7 @@ describe('components/ProfilePopoverCustomAttributes', () => { expect(screen.queryByText('Hidden Multiselect Attribute')).not.toBeInTheDocument(); expect(screen.getByText('Visible Multiselect Attribute')).toBeInTheDocument(); expect(screen.getByText('Option 1')).toBeInTheDocument(); + expect(screen.queryByText('filtered-option')).not.toBeInTheDocument(); }); test('should render display_name as the visible label when set', () => { From 6bbd385e7a6ce67e675b32917a1a8f89ac31ad41 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 May 2026 22:16:12 +0000 Subject: [PATCH 4/4] Guard select attrs before reading options in popover Use optional chaining when reading select/multiselect options so missing attribute attrs does not crash the profile popover. Co-authored-by: mattermost-code --- .../profile_popover/profile_popover_custom_attributes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx index dc8a09279db..f72009c33ca 100644 --- a/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx +++ b/webapp/channels/src/components/profile_popover/profile_popover_custom_attributes.tsx @@ -35,7 +35,7 @@ const hasDisplayableAttributeValue = (attribute: UserPropertyField, customProfil } if (attribute.type === 'multiselect' || attribute.type === 'select') { - const options = attribute.attrs.options; + const options = attribute.attrs?.options; if (!options?.length) { return false; }