MM-62698 Handles additional property types in the profile popover form. (#30318)

* fix for testing with server

* revert feature flag

* update profile popover for other property types

* lint fixes and test fixes

* src/components/user_settings/general/user_settings_general.tsx

* update from review

* lint fixes and type fixes

* review fixes

* fixes for property changes

* update properties

* fix tests

* fix tests

* update when_set and hidden

* update test for visiibility hidden

* add required fields to tests

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Scott Bishel
2025-04-02 17:41:06 -06:00
коммит произвёл GitHub
родитель c417ac1b57
Коммит 21ca303b5e
14 изменённых файлов: 907 добавлений и 14 удалений

Просмотреть файл

@@ -402,6 +402,66 @@ describe('components/ProfilePopover', () => {
expect(screen.queryByText('Base')).not.toBeInTheDocument();
});
test('should display select attribute values correctly', async () => {
const [props, initialState] = getBasePropsAndState();
(Client4.getUserCustomProfileAttributesValues as jest.Mock).mockImplementation(async () => {
return {
123: 'opt1',
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.customProfileAttributes = {
123: {
id: '123',
name: 'Department',
type: 'select',
attrs: {
options: [
{id: 'opt1', name: 'Engineering', color: ''},
{id: 'opt2', name: 'Sales', color: ''},
],
},
},
};
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
await act(async () => {
expect(await screen.findByText('Engineering')).toBeInTheDocument();
expect(screen.queryByText('opt1')).not.toBeInTheDocument();
});
});
test('should display multiselect attribute values correctly', async () => {
const [props, initialState] = getBasePropsAndState();
(Client4.getUserCustomProfileAttributesValues as jest.Mock).mockImplementation(async () => {
return {
123: ['opt1', 'opt2'],
};
});
initialState.entities!.general!.config!.FeatureFlagCustomProfileAttributes = 'true';
initialState.entities!.general!.customProfileAttributes = {
123: {
id: '123',
name: 'Skills',
type: 'multiselect',
attrs: {
options: [
{id: 'opt1', name: 'JavaScript', color: ''},
{id: 'opt2', name: 'Python', color: ''},
],
},
},
};
renderWithPluginReducers(<ProfilePopover {...props}/>, initialState);
await act(async () => {
expect(await screen.findByText(/JavaScript/)).toBeInTheDocument();
expect(await screen.findByText(/Python/)).toBeInTheDocument();
});
});
test('should not display attributes if user attributes is null', async () => {
const [props, initialState] = getBasePropsAndState();

Просмотреть файл

@@ -79,7 +79,7 @@ const ProfilePopover = ({
const status = useSelector((state: GlobalState) => getStatusForUserId(state, userId) || UserStatuses.OFFLINE);
const currentUserTimezone = useSelector(getCurrentTimezone);
const currentUserId = useSelector(getCurrentUserId);
const enableCustomProfileAttributes = useSelector((state: GlobalState) => getFeatureFlagValue(state, 'CustomProfileAttributes') === 'true');
const enableCustomProfileAttributes = useSelector((state: GlobalState) => getFeatureFlagValue(state, 'CustomProfileAttributes') === 'true' && !fromWebhook);
const [loadingDMChannel, setLoadingDMChannel] = useState<string>();
@@ -192,9 +192,10 @@ const ProfilePopover = ({
/>
</div>
{enableCustomProfileAttributes && (
{enableCustomProfileAttributes && !user.is_bot && (
<ProfilePopoverCustomAttributes
userID={userId}
hideStatus={hideStatus}
/>
)}
<ProfilePopoverTimezone

Просмотреть файл

@@ -0,0 +1,252 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import {Provider} from 'react-redux';
import configureStore from 'redux-mock-store';
import type {UserPropertyField, UserPropertyValueType} from '@mattermost/types/properties';
import {renderWithContext} from 'tests/react_testing_utils';
import ProfilePopoverCustomAttributes from './profile_popover_custom_attributes';
import {TestHelper} from '../../utils/test_helper';
jest.mock('mattermost-redux/actions/users', () => ({
getCustomProfileAttributeValues: jest.fn().mockReturnValue({type: 'GET_CUSTOM_PROFILE_ATTRIBUTE_VALUES'}),
}));
describe('components/ProfilePopoverCustomAttributes', () => {
const mockStore = configureStore();
const textAttribute: UserPropertyField = {
id: 'text_attribute_id',
name: 'Text Attribute',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: '' as UserPropertyValueType,
visibility: 'when_set',
sort_order: 0,
},
};
const phoneAttribute: UserPropertyField = {
id: 'phone_attribute_id',
name: 'Phone Number',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: 'phone' as UserPropertyValueType,
visibility: 'when_set',
sort_order: 1,
},
};
const urlAttribute: UserPropertyField = {
id: 'url_attribute_id',
name: 'Website',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: 'url' as UserPropertyValueType,
visibility: 'when_set',
sort_order: 2,
},
};
const selectAttribute: UserPropertyField = {
id: 'select_attribute_id',
name: 'Select Attribute',
type: 'select',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
options: [
{id: 'option1', name: 'Option 1', color: '#FF0000'},
{id: 'option2', name: 'Option 2', color: '#00FF00'},
],
visibility: 'when_set',
sort_order: 3,
value_type: '',
},
};
const userProfile = TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
text_attribute_id: 'text value',
phone_attribute_id: '+1 (555) 123-4567',
url_attribute_id: 'https://example.com',
select_attribute_id: 'option1',
},
});
const baseState = {
entities: {
general: {
config: {},
customProfileAttributes: {
text_attribute_id: textAttribute,
phone_attribute_id: phoneAttribute,
url_attribute_id: urlAttribute,
select_attribute_id: selectAttribute,
},
},
users: {
profiles: {
user_id: userProfile,
},
},
},
};
const baseProps = {
userID: 'user_id',
};
test('should render all attribute types', () => {
const store = mockStore(baseState);
renderWithContext(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
// Check that all attribute titles are rendered
expect(screen.getByText('Text Attribute')).toBeInTheDocument();
expect(screen.getByText('Phone Number')).toBeInTheDocument();
expect(screen.getByText('Website')).toBeInTheDocument();
expect(screen.getByText('Select Attribute')).toBeInTheDocument();
// Check that all attribute values are rendered
expect(screen.getByText('text value')).toBeInTheDocument();
expect(screen.getByText('+1 (555) 123-4567')).toBeInTheDocument();
expect(screen.getByText('https://example.com')).toBeInTheDocument();
expect(screen.getByText('Option 1')).toBeInTheDocument();
});
test('should fetch custom profile attributes if not available', () => {
const state = {
...baseState,
entities: {
...baseState.entities,
users: {
profiles: {
user_id: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: undefined,
}),
},
},
},
};
const store = mockStore(state);
const dispatchMock = jest.spyOn(store, 'dispatch');
renderWithContext(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
expect(dispatchMock).toHaveBeenCalledWith(expect.objectContaining({
type: 'GET_CUSTOM_PROFILE_ATTRIBUTE_VALUES',
}));
});
test('should respect visibility settings', () => {
const state = {
...baseState,
entities: {
...baseState.entities,
general: {
...baseState.entities.general,
customProfileAttributes: {
...baseState.entities.general.customProfileAttributes,
text_attribute_id: {
...textAttribute,
attrs: {
visibility: 'hidden',
},
},
},
},
},
};
const store = mockStore(state);
renderWithContext(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
// The attribute with 'hidden' visibility should not be rendered
expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument();
// Other attributes should still be rendered
expect(screen.getByText('Phone Number')).toBeInTheDocument();
expect(screen.getByText('Website')).toBeInTheDocument();
expect(screen.getByText('Select Attribute')).toBeInTheDocument();
});
test('should respect when_set visibility with empty values', () => {
const state = {
...baseState,
entities: {
...baseState.entities,
users: {
profiles: {
user_id: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
...userProfile.custom_profile_attributes,
text_attribute_id: '', // Empty value
},
}),
},
},
general: {
...baseState.entities.general,
customProfileAttributes: {
...baseState.entities.general.customProfileAttributes,
text_attribute_id: {
...textAttribute,
attrs: {
visibility: 'when_set',
},
},
},
},
},
};
const store = mockStore(state);
renderWithContext(
<Provider store={store}>
<ProfilePopoverCustomAttributes {...baseProps}/>
</Provider>,
);
// The attribute with empty value and 'when_set' visibility should not be rendered
expect(screen.queryByText('Text Attribute')).not.toBeInTheDocument();
});
});

Просмотреть файл

@@ -4,17 +4,26 @@
import React, {useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import type {UserPropertyValueType} from '@mattermost/types/properties';
import {getCustomProfileAttributeValues} from 'mattermost-redux/actions/users';
import {getCustomProfileAttributes} from 'mattermost-redux/selectors/entities/general';
import {getUser} from 'mattermost-redux/selectors/entities/users';
import type {GlobalState} from 'types/store';
import ProfilePopoverPhone from './profile_popover_phone';
import ProfilePopoverSelectAttribute from './profile_popover_select_attribute';
import ProfilePopoverTextAttribute from './profile_popover_text_attribute';
import ProfilePopoverUrl from './profile_popover_url';
type Props = {
userID: string;
hideStatus?: boolean;
}
const ProfilePopoverCustomAttributes = ({
userID,
hideStatus = false,
}: Props) => {
const dispatch = useDispatch();
const userProfile = useSelector((state: GlobalState) => getUser(state, userID));
@@ -25,12 +34,22 @@ const ProfilePopoverCustomAttributes = ({
dispatch(getCustomProfileAttributeValues(userID));
}
});
const attributeSections = customProfileAttributeFields.map((attribute) => {
if (userProfile.custom_profile_attributes) {
const value = userProfile.custom_profile_attributes[attribute.id];
if (!value) {
if (!hideStatus && userProfile.custom_profile_attributes) {
const visibility = attribute.attrs?.visibility || 'when_set';
if (visibility === 'hidden') {
return null;
}
// Check if the attribute has a value
const hasValue = userProfile.custom_profile_attributes[attribute.id]?.length > 0;
if (!hasValue && visibility === 'when_set') {
return null;
}
const valueType = (attribute.attrs?.value_type as UserPropertyValueType) || '';
return (
<div
key={'customAttribute_' + attribute.id}
@@ -42,12 +61,30 @@ const ProfilePopoverCustomAttributes = ({
>
{attribute.name}
</strong>
<p
aria-labelledby={`user-popover__custom_attributes-title-${attribute.id}`}
className='user-popover__subtitle-text'
>
{value}
</p>
{(attribute.type === 'multiselect' || attribute.type === 'select') && (
<ProfilePopoverSelectAttribute
attribute={attribute}
userProfile={userProfile}
/>
)}
{attribute.type === 'text' && valueType === 'phone' && (
<ProfilePopoverPhone
attribute={attribute}
userProfile={userProfile}
/>
)}
{attribute.type === 'text' && valueType === 'url' && (
<ProfilePopoverUrl
attribute={attribute}
userProfile={userProfile}
/>
)}
{attribute.type === 'text' && valueType === '' && (
<ProfilePopoverTextAttribute
attribute={attribute}
userProfile={userProfile}
/>
)}
</div>
);
}

Просмотреть файл

@@ -0,0 +1,95 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import {renderWithContext} from 'tests/react_testing_utils';
import ProfilePopoverPhone from './profile_popover_phone';
import {TestHelper} from '../../utils/test_helper';
describe('components/ProfilePopoverPhone', () => {
const attribute: UserPropertyField = {
id: 'phone_attribute_id',
name: 'Phone Number',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: 'phone',
visibility: 'when_set',
sort_order: 0,
},
};
const baseProps = {
attribute,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
phone_attribute_id: '+1 (555) 123-4567',
},
}),
};
test('should not render when phone is missing', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {},
}),
};
renderWithContext(<ProfilePopoverPhone {...props}/>);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
test('should not render when phone is empty', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
phone_attribute_id: '',
},
}),
};
renderWithContext(<ProfilePopoverPhone {...props}/>);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
test('should render phone with icon', () => {
renderWithContext(<ProfilePopoverPhone {...baseProps}/>);
const phone = '+1 (555) 123-4567';
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', 'tel:+1 (555) 123-4567');
expect(link).toHaveTextContent(phone);
expect(screen.getByTitle(phone)).toBeInTheDocument();
expect(screen.getByLabelText('phone icon')).toBeInTheDocument();
});
test('should handle international phone numbers', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
phone_attribute_id: '+44 20 7123 4567',
},
}),
};
renderWithContext(<ProfilePopoverPhone {...props}/>);
const phone = '+44 20 7123 4567';
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', 'tel:+44 20 7123 4567');
expect(link).toHaveTextContent(phone);
});
});

Просмотреть файл

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import type {UserProfile} from '@mattermost/types/users';
type Props = {
attribute: UserPropertyField;
userProfile: UserProfile;
}
const ProfilePopoverPhone = ({attribute, userProfile}: Props) => {
const phone = userProfile.custom_profile_attributes?.[attribute.id] as string;
if (!phone) {
return null;
}
return (
<div
title={phone}
className='user-profile-popover__phone'
>
<i
className='icon icon-phone-outline'
aria-hidden='true'
aria-label='phone icon'
/>
<a
href={'tel:' + phone}
>
{phone}
</a>
</div>
);
};
export default ProfilePopoverPhone;

Просмотреть файл

@@ -0,0 +1,117 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import type {PropertyFieldOption, UserPropertyField, UserPropertyFieldType} from '@mattermost/types/properties';
import {renderWithContext} from 'tests/react_testing_utils';
import ProfilePopoverSelectAttribute from './profile_popover_select_attribute';
import {TestHelper} from '../../utils/test_helper';
describe('components/ProfilePopoverSelectAttribute', () => {
const options: PropertyFieldOption[] = [
{id: 'option1', name: 'Option 1', color: '#FF0000'},
{id: 'option2', name: 'Option 2', color: '#00FF00'},
{id: 'option3', name: 'Option 3', color: '#0000FF'},
];
const attribute: UserPropertyField = {
id: 'select_attribute_id',
name: 'Select Attribute',
type: 'select' as UserPropertyFieldType,
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
options,
visibility: 'when_set',
sort_order: 0,
value_type: '',
},
};
const baseProps = {
attribute,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
select_attribute_id: 'option1',
},
}),
};
test('should render select option name', () => {
renderWithContext(<ProfilePopoverSelectAttribute {...baseProps}/>);
const textElement = screen.getByText('Option 1');
expect(textElement).toBeInTheDocument();
expect(textElement).toHaveClass('user-popover__subtitle-text');
expect(textElement).toHaveAttribute('aria-labelledby', 'user-popover__custom_attributes-title-select_attribute_id');
});
test('should render multiple selected options for multiselect', () => {
const props = {
...baseProps,
attribute: {
...attribute,
type: 'multiselect' as UserPropertyFieldType,
},
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
select_attribute_id: ['option1', 'option2'],
},
}),
};
renderWithContext(<ProfilePopoverSelectAttribute {...props}/>);
const textElement = screen.getByText('Option 1, Option 2');
expect(textElement).toBeInTheDocument();
});
test('should not render when attribute value is missing', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {},
}),
};
const {container} = renderWithContext(<ProfilePopoverSelectAttribute {...props}/>);
expect(container.firstChild).toBeNull();
});
test('should not render when options are missing', () => {
const props = {
...baseProps,
attribute: {
...attribute,
attrs: {
...attribute.attrs,
options: [],
},
},
};
const {container} = renderWithContext(<ProfilePopoverSelectAttribute {...props}/>);
expect(container.firstChild).toBeNull();
});
test('should not render when option is not found', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
select_attribute_id: 'non_existent_option',
},
}),
};
const {container} = renderWithContext(<ProfilePopoverSelectAttribute {...props}/>);
expect(container.firstChild).toBeNull();
});
});

Просмотреть файл

@@ -0,0 +1,52 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {PropertyFieldOption, UserPropertyField} from '@mattermost/types/properties';
import type {UserProfile} from '@mattermost/types/users';
type Props = {
attribute: UserPropertyField;
userProfile: UserProfile;
}
const ProfilePopoverSelectAttribute = ({attribute, userProfile}: Props) => {
const attributeValue = userProfile.custom_profile_attributes?.[attribute.id];
if (!attributeValue) {
return null;
}
const options = attribute.attrs?.options as PropertyFieldOption[];
if (!options) {
return null;
}
let displayValue = '';
if (Array.isArray(attributeValue)) {
// Handle multiselect
displayValue = attributeValue.map((value) => {
const option = options.find((o) => o.id === value);
return option?.name;
}).filter(Boolean).join(', ');
} else {
// Handle single select
const option = options.find((o) => o.id === attributeValue);
displayValue = option?.name || '';
}
if (!displayValue) {
return null;
}
return (
<p
aria-labelledby={`user-popover__custom_attributes-title-${attribute.id}`}
className='user-popover__subtitle-text'
>
{displayValue}
</p>
);
};
export default ProfilePopoverSelectAttribute;

Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import {renderWithContext} from 'tests/react_testing_utils';
import ProfilePopoverTextAttribute from './profile_popover_text_attribute';
import {TestHelper} from '../../utils/test_helper';
describe('components/ProfilePopoverTextAttribute', () => {
const attribute: UserPropertyField = {
id: 'text_attribute_id',
name: 'Text Attribute',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: 'phone',
visibility: 'when_set',
sort_order: 0,
},
};
const baseProps = {
attribute,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
text_attribute_id: 'text value',
},
}),
};
test('should render text attribute value', () => {
renderWithContext(<ProfilePopoverTextAttribute {...baseProps}/>);
const textElement = screen.getByText('text value');
expect(textElement).toBeInTheDocument();
expect(textElement).toHaveClass('user-popover__subtitle-text');
expect(textElement).toHaveAttribute('aria-labelledby', 'user-popover__custom_attributes-title-text_attribute_id');
});
test('should not render when attribute value is missing', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {},
}),
};
const {container} = renderWithContext(<ProfilePopoverTextAttribute {...props}/>);
expect(container.firstChild).toBeNull();
});
test('should not render when attribute value is empty', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
text_attribute_id: '',
},
}),
};
const {container} = renderWithContext(<ProfilePopoverTextAttribute {...props}/>);
expect(container.firstChild).toBeNull();
});
});

Просмотреть файл

@@ -0,0 +1,30 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import type {UserProfile} from '@mattermost/types/users';
type Props = {
attribute: UserPropertyField;
userProfile: UserProfile;
}
const ProfilePopoverTextAttribute = ({attribute, userProfile}: Props) => {
const attributeValue = userProfile.custom_profile_attributes?.[attribute.id];
if (!attributeValue) {
return null;
}
return (
<p
aria-labelledby={`user-popover__custom_attributes-title-${attribute.id}`}
className='user-popover__subtitle-text'
>
{attributeValue}
</p>
);
};
export default ProfilePopoverTextAttribute;

Просмотреть файл

@@ -0,0 +1,94 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {screen} from '@testing-library/react';
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import {renderWithContext} from 'tests/react_testing_utils';
import ProfilePopoverUrl from './profile_popover_url';
import {TestHelper} from '../../utils/test_helper';
describe('components/ProfilePopoverUrl', () => {
const attribute: UserPropertyField = {
id: 'url_attribute_id',
name: 'Website',
type: 'text',
group_id: 'custom_profile_attributes',
create_at: 0,
update_at: 0,
delete_at: 0,
attrs: {
value_type: 'url',
visibility: 'when_set',
sort_order: 0,
},
};
const baseProps = {
attribute,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
url_attribute_id: 'https://example.com',
},
}),
};
test('should not render when url is missing', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {},
}),
};
renderWithContext(<ProfilePopoverUrl {...props}/>);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
test('should not render when url is empty', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
url_attribute_id: '',
},
}),
};
renderWithContext(<ProfilePopoverUrl {...props}/>);
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
test('should render url with icon', () => {
renderWithContext(<ProfilePopoverUrl {...baseProps}/>);
const url = 'https://example.com';
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', url);
expect(link).toHaveTextContent(url);
expect(screen.getByTestId('url-icon')).toBeInTheDocument();
});
test('should render long url correctly', () => {
const props = {
...baseProps,
userProfile: TestHelper.getUserMock({
id: 'user_id',
custom_profile_attributes: {
url_attribute_id: 'https://really-long-subdomain.example.com/path/to/resource?param=value',
},
}),
};
renderWithContext(<ProfilePopoverUrl {...props}/>);
const url = 'https://really-long-subdomain.example.com/path/to/resource?param=value';
const container = screen.getByTitle(url);
expect(container).toBeInTheDocument();
expect(screen.getByRole('link')).toHaveTextContent(url);
});
});

Просмотреть файл

@@ -0,0 +1,40 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {UserPropertyField} from '@mattermost/types/properties';
import type {UserProfile} from '@mattermost/types/users';
type Props = {
attribute: UserPropertyField;
userProfile: UserProfile;
}
const ProfilePopoverUrl = ({attribute, userProfile}: Props) => {
const url = userProfile.custom_profile_attributes?.[attribute.id] as string;
if (!url) {
return null;
}
return (
<div
title={url}
className='user-profile-popover__url'
>
<i
className='icon icon-url-outline'
aria-hidden='true'
data-testid='url-icon'
/>
<a
href={url}
>
{url}
</a>
</div>
);
};
export default ProfilePopoverUrl;

Просмотреть файл

@@ -152,8 +152,8 @@ export type Props = {
sendVerificationEmail: (email: string) => Promise<ActionResult>;
setDefaultProfileImage: (id: string) => void;
uploadProfileImage: (id: string, file: File) => Promise<ActionResult>;
getCustomProfileAttributeValues: (userID: string) => Promise<ActionResult<Record<string, string | string[]>>>;
saveCustomProfileAttribute: (userID: string, attributeID: string, attributeValue: string | string[]) => Promise<ActionResult<Record<string, string | string[]>>>;
getCustomProfileAttributeValues: (userID: string) => Promise<ActionResult<Record<string, string>>>;
};
requireEmailVerification?: boolean;
ldapFirstNameAttributeSet?: boolean;
@@ -472,7 +472,7 @@ export class UserSettingsGeneralTab extends PureComponent<Props, State> {
this.setState({sectionIsSaving: true});
this.props.actions.saveCustomProfileAttribute(this.props.user.id, attributeID, attributeValue).
this.props.actions.saveCustomProfileAttribute(this.props.user.id, attributeID, attributeValue as string).
then(({data, error: err}) => {
if (data) {
this.updateSection('');

Просмотреть файл

@@ -60,8 +60,8 @@ export type UserProfile = {
terms_of_service_create_at: number;
remote_id?: string;
status?: string;
failed_attempts?: number;
custom_profile_attributes?: Record<string, string | string[]>;
failed_attempts?: number;
};
export type UserProfileWithLastViewAt = UserProfile & {