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..a19e42825b5 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 @@ -263,16 +263,16 @@ 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. + * Verify that public custom profile attributes with visibility set to always + * display their label in the profile popover even when they have no value. * * Precondition: * 1. A test server with valid license to support 'Custom Profile Attributes' * 2. Admin has created custom profile attributes - * 3. Other user has values set for custom profile attributes + * 3. Other user has no 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 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,14 @@ 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 - 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); - } + // * Verify the always-visible attribute label is displayed without a value + const popover = channelsPage.userProfilePopover.container; + await expect(popover.getByText('Title', {exact: false})).toBeVisible(); + await expect(popover.getByText(TEST_TITLE, {exact: false})).not.toBeVisible(); + + // * Verify custom attributes without values and default when_set visibility are not displayed + for (const attribute of customAttributes.filter((attribute) => attribute.name !== 'Title')) { + 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..89d8f1a61ca 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,260 @@ describe('components/ProfilePopoverCustomAttributes', () => { expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument(); }); + test('should render public 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.getByText('Text Attribute')).toBeInTheDocument(); + expect(screen.getByText('Phone Number')).toBeInTheDocument(); + }); + + test('should not render shared-only 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', + access_mode: 'shared_only', + }, + }, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument(); + expect(screen.getByText('Phone Number')).toBeInTheDocument(); + }); + + test('should render public 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.getByText('Select Attribute')).toBeInTheDocument(); + expect(screen.queryByText('filtered-option')).not.toBeInTheDocument(); + }); + + test('should not render shared-only 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', + access_mode: 'shared_only', + }, + }, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.queryByText('Select Attribute')).not.toBeInTheDocument(); + expect(screen.queryByText('filtered-option')).not.toBeInTheDocument(); + }); + + test('should hide only shared-only multiselect labels when values are not displayable', () => { + const publicMultiselectAttribute: UserPropertyField = { + ...selectAttribute, + id: 'public_multiselect_attribute_id', + name: 'Public Multiselect Attribute', + type: 'multiselect', + attrs: { + ...selectAttribute.attrs, + visibility: 'always', + }, + }; + const sharedOnlyMultiselectAttribute: UserPropertyField = { + ...selectAttribute, + id: 'shared_only_multiselect_attribute_id', + name: 'Shared Only Multiselect Attribute', + type: 'multiselect', + attrs: { + ...selectAttribute.attrs, + visibility: 'always', + access_mode: 'shared_only', + }, + }; + 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: { + public_multiselect_attribute_id: ['filtered-option'], + shared_only_multiselect_attribute_id: ['filtered-option'], + visible_multiselect_attribute_id: ['filtered-option', 'option1'], + }, + }), + }, + }, + general: { + ...baseState.entities.general, + customProfileAttributes: { + public_multiselect_attribute_id: publicMultiselectAttribute, + shared_only_multiselect_attribute_id: sharedOnlyMultiselectAttribute, + visible_multiselect_attribute_id: visibleMultiselectAttribute, + }, + }, + }, + }; + + const store = mockStore(state); + + renderWithContext( + + + , + ); + + expect(screen.getByText('Public Multiselect Attribute')).toBeInTheDocument(); + expect(screen.queryByText('Shared Only 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', () => { 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..036dc9da227 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,39 @@ type Props = { userID: string; hideStatus?: boolean; } + +const shouldRenderAttribute = (attribute: UserPropertyField, customProfileAttributes: Record): boolean => { + const attributeValue = customProfileAttributes[attribute.id]; + const visibility = attribute.attrs?.visibility || 'when_set'; + const shouldHideUnavailableValue = visibility === 'when_set' || attribute.attrs?.access_mode === 'shared_only'; + + if (Array.isArray(attributeValue)) { + if (attributeValue.length === 0) { + return !shouldHideUnavailableValue; + } + } else if (!attributeValue) { + return !shouldHideUnavailableValue; + } + + if (attribute.type === 'multiselect' || attribute.type === 'select') { + const options = attribute.attrs?.options; + if (!options?.length) { + return !shouldHideUnavailableValue; + } + + let hasDisplayableOption = false; + if (Array.isArray(attributeValue)) { + hasDisplayableOption = attributeValue.some((value) => options.some((option) => option.id === value)); + } else { + hasDisplayableOption = options.some((option) => option.id === attributeValue); + } + + return hasDisplayableOption || !shouldHideUnavailableValue; + } + + return true; +}; + const ProfilePopoverCustomAttributes = ({ userID, hideStatus = false, @@ -49,30 +82,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 (!shouldRenderAttribute(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) || '';