This commit is contained in:
cursor[bot] 2026-05-25 05:45:20 +02:00 committed by GitHub
commit efcd305f3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 183 additions and 35 deletions

View file

@ -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);
}
});

View file

@ -279,6 +279,156 @@ describe('components/ProfilePopoverCustomAttributes', () => {
expect(screen.queryByText('Text Attribute')).not.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(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
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(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
expect(screen.queryByText('Select Attribute')).not.toBeInTheDocument();
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(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
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', () => {
const state = {
...baseState,

View file

@ -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<string, string | string[]>): 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) || '';