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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c417ac1b57
Коммит
21ca303b5e
@@ -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 & {
|
||||
|
||||
Ссылка в новой задаче
Block a user