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) || '';