reduce usage of Utils.localizeMessage (#29282)

* reduce usage of Utils.localizeMessage

As we begin to transition to react-intl's `formatjs` for extraction, our custom wrappers like `Utils.localizeMessage` prevent an adoption roadblock.

This is the first in a series of PRs to begin to migrate away in favour of:
* `useIntl` when inside React functional component
* use `injectIntl` and use access the `intl` prop from class components
* use the new `getIntl`, leveraging a memoized `createIntl` and the store outside of React components

I'm pausing in this effort to get a feel from peers on both the substance of these changes and best practices in supporting them.

* prefer shallowWithIntl, renderWithContext, and local injectIntl wrapping

* revert unintentional whitespaces

* clarify getIntl, add minor unit test

* avoid triggering mmjstool

* update e2e tests

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Jesse Hallam
2024-12-13 09:49:28 -04:00
коммит произвёл GitHub
родитель bc22267829
Коммит 7179a9d1d6
31 изменённых файлов: 475 добавлений и 228 удалений

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

@@ -24,7 +24,6 @@ import Pluggable from 'plugins/pluggable';
import {createCallContext} from 'utils/apps';
import {Constants, Locations, ModalIdentifiers} from 'utils/constants';
import * as PostUtils from 'utils/post_utils';
import * as Utils from 'utils/utils';
import type {ModalData} from 'types/actions';
import type {HandleBindingClick, OpenAppsModal, PostEphemeralCallResponseForPost} from 'types/apps';
@@ -395,7 +394,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
key='more-actions-button'
ref={this.buttonRef}
id={`${this.props.location}_actions_button_${this.props.post.id}`}
aria-label={Utils.localizeMessage({id: 'post_info.actions.tooltip.actions', defaultMessage: 'Actions'}).toLowerCase()}
aria-label={formatMessage({id: 'post_info.actions.tooltip.actions', defaultMessage: 'Actions'}).toLowerCase()}
className={classNames('post-menu__item', {
'post-menu__item--active': this.props.isMenuOpen,
})}
@@ -409,7 +408,7 @@ export class ActionMenuClass extends React.PureComponent<Props, State> {
id={`${this.props.location}_actions_dropdown_${this.props.post.id}`}
openLeft={true}
openUp={this.state.openUp}
ariaLabel={Utils.localizeMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'})}
ariaLabel={formatMessage({id: 'post_info.menuAriaLabel', defaultMessage: 'Post extra options'})}
key={`${this.props.location}_actions_dropdown_${this.props.post.id}`}
>
{menuItems}

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

@@ -17,7 +17,7 @@ exports[`components/MarkdownImage should match snapshot 1`] = `
`;
exports[`components/MarkdownImage should match snapshot for SizeAwareImage dimensions 1`] = `
<SizeAwareImage
<injectIntl(SizeAwareImage)
alt="test image"
className="markdown-inline-img markdown-inline-img--hover markdown-inline-img--no-border"
dimensions={
@@ -62,7 +62,7 @@ exports[`components/MarkdownImage should provide image src as an alt text for Ma
imageKey="https://example.com/image.png"
postId="post_id"
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
alt=""
className="markdown-inline-img markdown-inline-img--hover cursor--pointer a11y--active"
dimensions={Object {}}
@@ -97,7 +97,7 @@ exports[`components/MarkdownImage should render a link if the source is unsafe 1
exports[`components/MarkdownImage should render an image with no preview if the source is safe and the image is a link 1`] = `
<div>
<SizeAwareImage
<injectIntl(SizeAwareImage)
alt="test image"
className="markdown-inline-img markdown-inline-img--hover markdown-inline-img--no-border"
dimensions={
@@ -122,7 +122,7 @@ exports[`components/MarkdownImage should render an image with no preview if the
exports[`components/MarkdownImage should render an image with preview modal if the source is safe 1`] = `
<div>
<SizeAwareImage
<injectIntl(SizeAwareImage)
alt="test image"
className="markdown-inline-img markdown-inline-img--hover cursor--pointer a11y--active"
dimensions={
@@ -152,7 +152,7 @@ exports[`components/MarkdownImage should render image with MarkdownImageExpand i
imageKey="https://example.com/image.png"
postId="post_id"
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
alt="test image"
className="markdown-inline-img markdown-inline-img--hover cursor--pointer a11y--active"
dimensions={Object {}}

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

@@ -26,7 +26,7 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
}
>
<button
aria-label="add reaction"
aria-label="Add Reaction"
className="post-menu__item post-menu__item--reactions"
data-testid="post-reaction-emoji-icon"
id="CENTER_reaction_post_id_1"

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

@@ -1,12 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {TestHelper} from 'utils/test_helper';
import PostReaction from './post_reaction';
import PostReaction, {type PostReaction as PostReactionComponent} from './post_reaction';
describe('components/post_view/PostReaction', () => {
const baseProps = {
@@ -23,13 +23,13 @@ describe('components/post_view/PostReaction', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<PostReaction {...baseProps}/>);
const wrapper = shallowWithIntl(<PostReaction {...baseProps}/>);
expect(wrapper).toMatchSnapshot();
});
test('should call toggleReaction and toggleEmojiPicker on handleToggleEmoji', () => {
const wrapper = shallow(<PostReaction {...baseProps}/>);
const instance = wrapper.instance() as PostReaction;
const wrapper = shallowWithIntl(<PostReaction {...baseProps}/>);
const instance = wrapper.instance() as PostReactionComponent;
instance.handleToggleEmoji(TestHelper.getCustomEmojiMock({name: 'smile'}));
expect(baseProps.actions.toggleReaction).toHaveBeenCalledTimes(1);

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

@@ -3,7 +3,8 @@
import classNames from 'classnames';
import React from 'react';
import {defineMessages} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {defineMessages, injectIntl} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis';
@@ -16,7 +17,6 @@ import EmojiIcon from 'components/widgets/icons/emoji_icon';
import WithTooltip from 'components/with_tooltip/with_tooltip_new';
import {Locations} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
const TOP_OFFSET = -7;
@@ -27,12 +27,12 @@ const messages = defineMessages({
},
});
export type Props = {
export type Props = WrappedComponentProps & {
channelId?: string;
postId: string;
teamId: string;
getDotMenuRef: () => HTMLDivElement | null;
location: keyof typeof Locations;
location?: keyof typeof Locations;
showEmojiPicker: boolean;
toggleEmojiPicker: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
actions: {
@@ -45,7 +45,7 @@ type State = {
showEmojiPicker: boolean;
}
export default class PostReaction extends React.PureComponent<Props, State> {
export class PostReaction extends React.PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
location: Locations.CENTER as 'CENTER',
showEmojiPicker: false,
@@ -65,6 +65,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
postId,
showEmojiPicker,
teamId,
intl,
} = this.props;
let spaceRequiredAbove;
@@ -96,7 +97,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
<button
data-testid='post-reaction-emoji-icon'
id={`${location}_reaction_${postId}`}
aria-label={localizeMessage({id: 'post_info.tooltip.add_reactions', defaultMessage: 'Add Reaction'}).toLowerCase()}
aria-label={intl.formatMessage({id: 'post_info.tooltip.add_reactions', defaultMessage: 'Add Reaction'})}
className={classNames('post-menu__item', 'post-menu__item--reactions', {
'post-menu__item--active': showEmojiPicker,
})}
@@ -110,3 +111,5 @@ export default class PostReaction extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(PostReaction);

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

@@ -36,7 +36,7 @@ exports[`components/sidebar should match snapshot 1`] = `
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(SidebarList)
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}
@@ -79,7 +79,7 @@ exports[`components/sidebar should match snapshot when direct channels modal is
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(SidebarList)
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}
@@ -126,7 +126,7 @@ exports[`components/sidebar should match snapshot when more channels modal is op
pluggableName="LeftSidebarHeader"
/>
</div>
<Connect(SidebarList)
<Connect(injectIntl(SidebarList))
handleOpenMoreDirectChannelsModal={[Function]}
onDragEnd={[Function]}
onDragStart={[Function]}

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

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match snapshot 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
ariaLabelPrefix="public channel"
channel={
Object {
@@ -34,7 +34,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match sn
`;
exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match snapshot when private channel 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
ariaLabelPrefix="private channel"
channel={
Object {
@@ -67,7 +67,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match sn
`;
exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match snapshot when shared channel 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
ariaLabelPrefix="public channel"
channel={
Object {
@@ -102,7 +102,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match sn
`;
exports[`components/sidebar/sidebar_channel/sidebar_base_channel should match snapshot when shared private channel 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
ariaLabelPrefix="private channel"
channel={
Object {

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

@@ -1,13 +1,15 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import type {ChannelType} from '@mattermost/types/channels';
import type {SidebarChannelLink as SidebarChannelLinkComponent} from 'components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link';
import SidebarChannelLink from 'components/sidebar/sidebar_channel/sidebar_channel_link/sidebar_channel_link';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
const baseProps = {
channel: {
@@ -49,7 +51,7 @@ describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarChannelLink {...baseProps}/>,
);
@@ -60,7 +62,7 @@ describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
const userAgentMock = jest.requireMock('utils/user_agent');
userAgentMock.isDesktopApp.mockImplementation(() => false);
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarChannelLink {...baseProps}/>,
);
@@ -68,7 +70,7 @@ describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
});
test('should match snapshot when tooltip is enabled', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarChannelLink {...baseProps}/>,
);
@@ -84,7 +86,7 @@ describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
ariaLabelPrefix: 'aria_label_prefix_',
};
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarChannelLink {...props}/>,
);
@@ -92,10 +94,10 @@ describe('components/sidebar/sidebar_channel/sidebar_channel_link', () => {
});
test('should enable tooltip when needed', () => {
const wrapper = shallow<SidebarChannelLink>(
const wrapper = shallowWithIntl(
<SidebarChannelLink {...baseProps}/>,
);
const instance = wrapper.instance();
const instance = wrapper.instance() as SidebarChannelLinkComponent;
instance.labelRef = {
current: {

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

@@ -3,6 +3,7 @@
import classNames from 'classnames';
import React from 'react';
import {type WrappedComponentProps, injectIntl} from 'react-intl';
import {Link} from 'react-router-dom';
import type {Channel} from '@mattermost/types/channels';
@@ -19,7 +20,6 @@ import Constants, {RHSStates} from 'utils/constants';
import {wrapEmojis} from 'utils/emoji_utils';
import {cmdOrCtrlPressed} from 'utils/keyboard';
import {Mark} from 'utils/performance_telemetry';
import {localizeMessage} from 'utils/utils';
import type {RhsState} from 'types/store/rhs';
@@ -28,7 +28,7 @@ import ChannelPencilIcon from '../channel_pencil_icon';
import SidebarChannelIcon from '../sidebar_channel_icon';
import SidebarChannelMenu from '../sidebar_channel_menu';
type Props = {
type Props = WrappedComponentProps & {
channel: Channel;
link: string;
label: string;
@@ -79,7 +79,7 @@ type State = {
showTooltip: boolean;
};
export default class SidebarChannelLink extends React.PureComponent<Props, State> {
export class SidebarChannelLink extends React.PureComponent<Props, State> {
labelRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) {
@@ -110,7 +110,7 @@ export default class SidebarChannelLink extends React.PureComponent<Props, State
};
getAriaLabel = (): string => {
const {label, ariaLabelPrefix, unreadMentions} = this.props;
const {label, ariaLabelPrefix, unreadMentions, intl} = this.props;
let ariaLabel = label;
@@ -119,13 +119,13 @@ export default class SidebarChannelLink extends React.PureComponent<Props, State
}
if (unreadMentions === 1) {
ariaLabel += ` ${unreadMentions} ${localizeMessage({id: 'accessibility.sidebar.types.mention', defaultMessage: 'mention'})}`;
ariaLabel += ` ${unreadMentions} ${intl.formatMessage({id: 'accessibility.sidebar.types.mention', defaultMessage: 'mention'})}`;
} else if (unreadMentions > 1) {
ariaLabel += ` ${unreadMentions} ${localizeMessage({id: 'accessibility.sidebar.types.mentions', defaultMessage: 'mentions'})}`;
ariaLabel += ` ${unreadMentions} ${intl.formatMessage({id: 'accessibility.sidebar.types.mentions', defaultMessage: 'mentions'})}`;
}
if (this.props.isUnread && unreadMentions === 0) {
ariaLabel += ` ${localizeMessage({id: 'accessibility.sidebar.types.unread', defaultMessage: 'unread'})}`;
ariaLabel += ` ${intl.formatMessage({id: 'accessibility.sidebar.types.unread', defaultMessage: 'unread'})}`;
}
return ariaLabel.toLowerCase();
@@ -298,3 +298,5 @@ export default class SidebarChannelLink extends React.PureComponent<Props, State
);
}
}
export default injectIntl(SidebarChannelLink);

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

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match snapshot 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
channel={
Object {
"create_at": 0,
@@ -39,7 +39,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
`;
exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match snapshot if DM is with bot with custom icon 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
channel={
Object {
"create_at": 0,
@@ -78,7 +78,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
`;
exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match snapshot if DM is with current user 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
channel={
Object {
"create_at": 0,
@@ -116,7 +116,7 @@ exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match
`;
exports[`components/sidebar/sidebar_channel/sidebar_direct_channel should match snapshot if DM is with deleted user 1`] = `
<Connect(SidebarChannelLink)
<Connect(injectIntl(SidebarChannelLink))
channel={
Object {
"create_at": 0,

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

@@ -11,10 +11,11 @@ import type {TeamType} from '@mattermost/types/teams';
import {CategoryTypes} from 'mattermost-redux/constants/channel_categories';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {DraggingStates, DraggingStateTypes} from 'utils/constants';
import {TestHelper} from 'utils/test_helper';
import SidebarList from './sidebar_list';
import SidebarList, {type SidebarList as SidebarListComponent} from './sidebar_list';
describe('SidebarList', () => {
const currentChannel = TestHelper.getChannelMock({
@@ -115,7 +116,7 @@ describe('SidebarList', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
@@ -130,7 +131,7 @@ describe('SidebarList', () => {
});
test('should close sidebar on mobile when channel is selected (ie. changed)', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
@@ -139,11 +140,12 @@ describe('SidebarList', () => {
});
test('should scroll to top when team changes', () => {
const wrapper = shallow<SidebarList>(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
const instance = wrapper.instance() as SidebarListComponent;
wrapper.instance().scrollbar = {
instance.scrollbar = {
current: {
scrollToTop: jest.fn(),
} as any,
@@ -155,14 +157,14 @@ describe('SidebarList', () => {
};
wrapper.setProps({currentTeam: newCurrentTeam});
expect(wrapper.instance().scrollbar.current!.scrollToTop).toHaveBeenCalled();
expect(instance.scrollbar.current!.scrollToTop).toHaveBeenCalled();
});
test('should display unread scroll indicator when channels appear outside visible area', () => {
const wrapper = shallow<SidebarList>(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
const instance = wrapper.instance();
const instance = wrapper.instance() as SidebarListComponent;
instance.scrollbar = {
current: {
@@ -189,10 +191,10 @@ describe('SidebarList', () => {
});
test('should scroll to correct position when scrolling to channel', () => {
const wrapper = shallow<SidebarList>(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
const instance = wrapper.instance();
const instance = wrapper.instance() as SidebarListComponent;
instance.scrollToPosition = jest.fn();
@@ -218,7 +220,7 @@ describe('SidebarList', () => {
style: {},
}]);
const wrapper = shallow<SidebarList>(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
@@ -232,7 +234,9 @@ describe('SidebarList', () => {
type: DraggingStateTypes.CATEGORY,
};
wrapper.instance().onBeforeCapture(categoryBefore);
const instance = wrapper.instance() as SidebarListComponent;
instance.onBeforeCapture(categoryBefore);
expect(baseProps.actions.setDraggingState).toHaveBeenCalledWith(expectedCategoryBefore);
const channelBefore = {
@@ -245,14 +249,15 @@ describe('SidebarList', () => {
type: DraggingStateTypes.CHANNEL,
};
wrapper.instance().onBeforeCapture(channelBefore);
instance.onBeforeCapture(channelBefore);
expect(baseProps.actions.setDraggingState).toHaveBeenCalledWith(expectedChannelBefore);
});
test('should call correct action on dropping item', () => {
const wrapper = shallow<SidebarList>(
const wrapper = shallowWithIntl(
<SidebarList {...baseProps}/>,
);
const instance = wrapper.instance() as SidebarListComponent;
const categoryResult: DropResult = {
reason: 'DROP',
@@ -269,7 +274,7 @@ describe('SidebarList', () => {
mode: 'SNAP' as MovementMode,
};
wrapper.instance().onDragEnd(categoryResult);
instance.onDragEnd(categoryResult);
expect(baseProps.actions.moveCategory).toHaveBeenCalledWith(baseProps.currentTeam.id, categoryResult.draggableId, categoryResult.destination!.index);
const channelResult: DropResult = {
@@ -287,7 +292,7 @@ describe('SidebarList', () => {
mode: 'SNAP' as MovementMode,
};
wrapper.instance().onDragEnd(channelResult);
instance.onDragEnd(channelResult);
expect(baseProps.actions.moveChannelsInSidebar).toHaveBeenCalledWith(channelResult.destination!.droppableId, channelResult.destination!.index, channelResult.draggableId);
});
});

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

@@ -8,7 +8,7 @@ import type {CSSProperties} from 'react';
import {DragDropContext, Droppable} from 'react-beautiful-dnd';
import type {DropResult, DragStart, BeforeCapture} from 'react-beautiful-dnd';
import Scrollbars from 'react-custom-scrollbars';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl';
import {SpringSystem} from 'rebound';
import type {Spring} from 'rebound';
@@ -26,7 +26,7 @@ import SidebarCategory from 'components/sidebar/sidebar_category';
import {findNextUnreadChannelId} from 'utils/channel_utils';
import {Constants, DraggingStates, DraggingStateTypes} from 'utils/constants';
import {isKeyPressed, cmdOrCtrlPressed} from 'utils/keyboard';
import {localizeMessage, mod} from 'utils/utils';
import {mod} from 'utils/utils';
import type {DraggingState} from 'types/store';
import type {StaticPage} from 'types/store/lhs';
@@ -74,7 +74,7 @@ export function renderThumbVertical(props: React.HTMLProps<HTMLDivElement>) {
const scrollbarStyles: CSSProperties = {position: 'absolute'};
type Props = {
type Props = WrappedComponentProps & {
currentTeam?: Team;
currentChannelId: string;
categories: ChannelCategory[];
@@ -122,7 +122,7 @@ const categoryHeaderHeight = 32;
// that the channel is not under the unread indicator.
const scrollMarginWithUnread = 55;
export default class SidebarList extends React.PureComponent<Props, State> {
export class SidebarList extends React.PureComponent<Props, State> {
channelRefs: Map<string, HTMLLIElement>;
scrollbar: React.RefObject<Scrollbars>;
animate: SpringSystem;
@@ -547,7 +547,7 @@ export default class SidebarList extends React.PureComponent<Props, State> {
/>
);
const ariaLabel = localizeMessage({id: 'accessibility.sections.lhsList', defaultMessage: 'channel sidebar region'});
const ariaLabel = this.props.intl.formatMessage({id: 'accessibility.sections.lhsList', defaultMessage: 'channel sidebar region'});
return (
@@ -602,3 +602,5 @@ export default class SidebarList extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(SidebarList);

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

@@ -33,7 +33,7 @@ exports[`components/SingleImageView permalink preview should render with permali
<div
className="image-permalink"
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className="image-permalink"
dimensions={
Object {
@@ -108,7 +108,7 @@ exports[`components/SingleImageView should match snapshot 1`] = `
<div
className=""
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className=""
dimensions={
Object {
@@ -183,7 +183,7 @@ exports[`components/SingleImageView should match snapshot 2`] = `
<div
className=""
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className=""
dimensions={
Object {
@@ -262,7 +262,7 @@ exports[`components/SingleImageView should match snapshot, SVG image 1`] = `
<div
className=""
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className=""
dimensions={
Object {
@@ -341,7 +341,7 @@ exports[`components/SingleImageView should match snapshot, SVG image 2`] = `
<div
className=""
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className=""
dimensions={
Object {
@@ -416,7 +416,7 @@ exports[`components/SingleImageView should set loaded state on callback of onIma
<div
className=""
>
<SizeAwareImage
<injectIntl(SizeAwareImage)
className=""
dimensions={
Object {

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

@@ -57,7 +57,7 @@ describe('components/SingleImageView', () => {
<SingleImageView {...baseProps}/>,
);
wrapper.find('SizeAwareImage').at(0).simulate('click', {preventDefault: () => {}});
wrapper.find(SizeAwareImage).at(0).simulate('click', {preventDefault: () => { }});
expect(baseProps.actions.openModal).toHaveBeenCalledTimes(1);
});

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

@@ -1,19 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount, shallow} from 'enzyme';
import React from 'react';
import {Provider} from 'react-redux';
import LoadingImagePreview from 'components/loading_image_preview';
import SizeAwareImage from 'components/size_aware_image';
import type {Props} from 'components/size_aware_image';
import SizeAwareImage, {SizeAwareImage as SizeAwareImageComponent} from 'components/size_aware_image';
import {shallowWithIntl, mountWithIntl} from 'tests/helpers/intl-test-helper';
import mockStore from 'tests/test_store';
import {TestHelper} from 'utils/test_helper';
describe('components/SizeAwareImage', () => {
const baseProps: Props = {
const baseProps = {
dimensions: {
height: 200,
width: 300,
@@ -41,17 +40,17 @@ describe('components/SizeAwareImage', () => {
});
test('should render an svg when first mounted with dimensions and img display set to none', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
// since download and copy icons use svgs now, attachment svg should be searched as a direct child of image-loading__container
const viewBox = wrapper.find(SizeAwareImage).find('.image-loading__container').children().filter('svg').prop('viewBox');
const viewBox = wrapper.find(SizeAwareImageComponent).find('.image-loading__container').children().filter('svg').prop('viewBox');
expect(viewBox).toEqual('0 0 300 200');
const style = wrapper.find('.file-preview__button').prop('style');
expect(style).toHaveProperty('display', 'none');
});
test('img should have inherited class name from prop', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...{...baseProps, className: 'imgClass'}}/></Provider>);
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...{...baseProps, className: 'imgClass'}}/></Provider>);
const className = wrapper.find('img').prop('className');
expect(className).toEqual('imgClass');
@@ -63,7 +62,7 @@ describe('components/SizeAwareImage', () => {
showLoader: true,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper.find(LoadingImagePreview).exists()).toEqual(true);
expect(wrapper).toMatchSnapshot();
});
@@ -78,17 +77,17 @@ describe('components/SizeAwareImage', () => {
}),
};
const wrapper = mount(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
wrapper.find(SizeAwareImage).setState({loaded: false, error: false});
wrapper.find(SizeAwareImageComponent).setState({loaded: false, error: false});
const src = wrapper.find('.image-loading__container img').prop('src');
expect(src).toEqual('data:mime_type;base64,mini_preview');
});
test('should have display set to initial in loaded state', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
wrapper.find(SizeAwareImage).setState({loaded: true, error: false});
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
wrapper.find(SizeAwareImageComponent).setState({loaded: true, error: false});
const style = wrapper.find('.file-preview__button').prop('style');
expect(style).toHaveProperty('display', 'inline-block');
@@ -98,9 +97,9 @@ describe('components/SizeAwareImage', () => {
const props = {...baseProps};
Reflect.deleteProperty(props, 'dimensions');
const wrapper = mount(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...props}/></Provider>);
wrapper.find(SizeAwareImage).setState({error: false});
wrapper.find(SizeAwareImageComponent).setState({error: false});
const src = wrapper.find('img').prop('src');
expect(src).toEqual(baseProps.src);
@@ -110,7 +109,7 @@ describe('components/SizeAwareImage', () => {
const height = 123;
const width = 1234;
const wrapper = shallow<SizeAwareImage>(<SizeAwareImage {...baseProps}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...baseProps}/>);
wrapper.find('img')?.prop('onLoad')?.({target: {naturalHeight: height, naturalWidth: width}} as unknown as React.SyntheticEvent<HTMLImageElement>);
expect(wrapper.state('loaded')).toBe(true);
@@ -118,18 +117,18 @@ describe('components/SizeAwareImage', () => {
});
test('should call onImageLoadFail when image load fails and should have svg', () => {
const wrapper = mount(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
const wrapper = mountWithIntl(<Provider store={store}><SizeAwareImage {...baseProps}/></Provider>);
const errorEvent = {
target: {},
currentTarget: {},
preventDefault: () => {},
stopPropagation: () => {},
preventDefault: () => { },
stopPropagation: () => { },
} as React.SyntheticEvent<HTMLImageElement>;
wrapper.find(SizeAwareImage).find('img').prop('onError')?.(errorEvent);
wrapper.find(SizeAwareImageComponent).find('img').prop('onError')?.(errorEvent);
expect(wrapper.find(SizeAwareImage).state('error')).toBe(true);
expect(wrapper.find(SizeAwareImage).find('svg').exists()).toEqual(true);
expect(wrapper.find(SizeAwareImage).find(LoadingImagePreview).exists()).toEqual(false);
expect(wrapper.find(SizeAwareImageComponent).state('error')).toBe(true);
expect(wrapper.find(SizeAwareImageComponent).find('svg').exists()).toEqual(true);
expect(wrapper.find(SizeAwareImageComponent).find(LoadingImagePreview).exists()).toEqual(false);
});
test('should match snapshot when handleSmallImageContainer prop is passed', () => {
@@ -138,7 +137,7 @@ describe('components/SizeAwareImage', () => {
handleSmallImageContainer: true,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper).toMatchSnapshot();
});
@@ -148,7 +147,7 @@ describe('components/SizeAwareImage', () => {
handleSmallImageContainer: true,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
wrapper.instance().setState({isSmallImage: true});
@@ -163,7 +162,7 @@ describe('components/SizeAwareImage', () => {
handleSmallImageContainer: true,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
wrapper.instance().setState({isSmallImage: true, imageWidth: 220});
expect(wrapper.find('div.small-image__container').prop('style')).
@@ -182,7 +181,7 @@ describe('components/SizeAwareImage', () => {
handleSmallImageContainer: true,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
wrapper.instance().setState({isSmallImage: true, imageWidth: 24});
@@ -195,7 +194,7 @@ describe('components/SizeAwareImage', () => {
...baseProps,
fileURL,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper).toMatchSnapshot();
});
@@ -205,7 +204,7 @@ describe('components/SizeAwareImage', () => {
...baseProps,
fileURL,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper.find('.size-aware-image__download').prop('href')).toBe(fileURL);
});
@@ -216,7 +215,7 @@ describe('components/SizeAwareImage', () => {
fileURL,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper.state('linkCopyInProgress')).toBe(false);
wrapper.find('.size-aware-image__copy_link').first().simulate('click');
expect(wrapper.state('linkCopyInProgress')).toBe(true);
@@ -228,7 +227,7 @@ describe('components/SizeAwareImage', () => {
enablePublicLink: false,
};
const wrapper = shallow(<SizeAwareImage {...props}/>);
const wrapper = shallowWithIntl(<SizeAwareImage {...props}/>);
expect(wrapper.find('button.size-aware-image__copy_link').exists()).toEqual(false);
});
});

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

@@ -6,7 +6,8 @@
import classNames from 'classnames';
import React from 'react';
import type {KeyboardEvent, MouseEvent, SyntheticEvent} from 'react';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {DownloadOutlineIcon, LinkVariantIcon, CheckIcon} from '@mattermost/compass-icons/components';
import type {FileInfo} from '@mattermost/types/files';
@@ -19,13 +20,13 @@ import LoadingImagePreview from 'components/loading_image_preview';
import WithTooltip from 'components/with_tooltip';
import {FileTypes} from 'utils/constants';
import {localizeMessage, copyToClipboard, getFileType} from 'utils/utils';
import {copyToClipboard, getFileType} from 'utils/utils';
const MIN_IMAGE_SIZE = 48;
const MIN_IMAGE_SIZE_FOR_INTERNAL_BUTTONS = 100;
const MAX_IMAGE_HEIGHT = 350;
export type Props = {
export type Props = WrappedComponentProps & {
/*
* The source URL of the image
@@ -86,7 +87,7 @@ export type Props = {
/**
* Action to fetch public link of an image from server.
*/
getFilePublicLink?: () => Promise<ActionResult<{link: string}>>;
getFilePublicLink?: () => Promise<ActionResult<{ link: string }>>;
/*
* Prevents display of utility buttons when image in a location that makes them inappropriate
@@ -105,10 +106,10 @@ type State = {
// SizeAwareImage is a component used for rendering images where the dimensions of the image are important for
// ensuring that the page is laid out correctly.
export default class SizeAwareImage extends React.PureComponent<Props, State> {
export class SizeAwareImage extends React.PureComponent<Props, State> {
public heightTimeout = 0;
public mounted = false;
public timeout: NodeJS.Timeout|null = null;
public timeout: NodeJS.Timeout | null = null;
constructor(props: Props) {
super(props);
@@ -199,6 +200,7 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
src,
fileURL,
enablePublicLink,
intl,
...props
} = this.props;
Reflect.deleteProperty(props, 'showLoader');
@@ -210,8 +212,9 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
Reflect.deleteProperty(props, 'onClick');
Reflect.deleteProperty(props, 'hideUtilities');
Reflect.deleteProperty(props, 'getFilePublicLink');
Reflect.deleteProperty(props, 'intl');
let ariaLabelImage = localizeMessage({id: 'file_attachment.thumbnail', defaultMessage: 'file thumbnail'});
let ariaLabelImage = intl.formatMessage({id: 'file_attachment.thumbnail', defaultMessage: 'file thumbnail'});
if (fileInfo) {
ariaLabelImage += ` ${fileInfo.name}`.toLowerCase();
}
@@ -269,7 +272,7 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
className={classNames('style--none', 'size-aware-image__copy_link', {
'size-aware-image__copy_link--recently_copied': this.state.linkCopiedRecently,
})}
aria-label={localizeMessage({id: 'single_image_view.copy_link_tooltip', defaultMessage: 'Copy link'})}
aria-label={intl.formatMessage({id: 'single_image_view.copy_link_tooltip', defaultMessage: 'Copy link'})}
onClick={this.copyLinkToAsset}
>
{this.state.linkCopiedRecently ? (
@@ -306,7 +309,7 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
className='style--none size-aware-image__download'
download={true}
role={this.isInternalImage ? 'button' : undefined}
aria-label={localizeMessage({id: 'single_image_view.download_tooltip', defaultMessage: 'Download'})}
aria-label={intl.formatMessage({id: 'single_image_view.download_tooltip', defaultMessage: 'Download'})}
>
<DownloadOutlineIcon
className={'style--none'}
@@ -396,7 +399,7 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
fileInfo,
} = this.props;
let ariaLabelImage = localizeMessage({id: 'file_attachment.thumbnail', defaultMessage: 'file thumbnail'});
let ariaLabelImage = this.props.intl.formatMessage({id: 'file_attachment.thumbnail', defaultMessage: 'file thumbnail'});
if (fileInfo) {
ariaLabelImage += ` ${fileInfo.name}`.toLowerCase();
}
@@ -500,3 +503,5 @@ export default class SizeAwareImage extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(SizeAwareImage);

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

@@ -112,7 +112,7 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -125,9 +125,7 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -190,7 +188,6 @@ exports[`components/StatusDropdown should match snapshot in default state 1`] =
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -439,7 +436,7 @@ exports[`components/StatusDropdown should match snapshot with custom status and
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -452,9 +449,7 @@ exports[`components/StatusDropdown should match snapshot with custom status and
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -517,7 +512,6 @@ exports[`components/StatusDropdown should match snapshot with custom status and
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -729,7 +723,7 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -742,9 +736,7 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -807,7 +799,6 @@ exports[`components/StatusDropdown should match snapshot with custom status enab
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -1019,7 +1010,7 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -1032,9 +1023,7 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -1097,7 +1086,6 @@ exports[`components/StatusDropdown should match snapshot with custom status expi
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -1310,7 +1298,7 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -1323,9 +1311,7 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -1388,7 +1374,6 @@ exports[`components/StatusDropdown should match snapshot with custom status puls
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -1576,7 +1561,7 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -1589,9 +1574,7 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -1654,7 +1637,6 @@ exports[`components/StatusDropdown should match snapshot with profile picture UR
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -1834,7 +1816,7 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -1847,9 +1829,7 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -1912,7 +1892,6 @@ exports[`components/StatusDropdown should match snapshot with status dropdown op
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -2124,7 +2103,7 @@ exports[`components/StatusDropdown should not show clear status button when cust
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -2137,9 +2116,7 @@ exports[`components/StatusDropdown should not show clear status button when cust
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -2202,7 +2179,6 @@ exports[`components/StatusDropdown should not show clear status button when cust
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction
@@ -2451,7 +2427,7 @@ exports[`components/StatusDropdown should show clear status button when custom s
show={true}
text="Away"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[Function]}
ariaLabel="Do not disturb. Disables all notifications"
direction="left"
@@ -2464,9 +2440,7 @@ exports[`components/StatusDropdown should show clear status button when custom s
}
id="status-menu-dnd"
openUp={false}
renderSelected={true}
rightDecorator={false}
show={true}
subMenu={
Array [
Object {
@@ -2529,7 +2503,6 @@ exports[`components/StatusDropdown should show clear status button when custom s
},
]
}
subMenuClass="pl-4"
text="Do not disturb"
/>
<MenuItemAction

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

@@ -3,7 +3,8 @@
import classNames from 'classnames';
import React from 'react';
import {defineMessages} from 'react-intl';
import {defineMessages, injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {connect, useSelector} from 'react-redux';
import type {Channel, ChannelMembership} from '@mattermost/types/channels';
@@ -59,6 +60,7 @@ import BotTag from 'components/widgets/tag/bot_tag';
import GuestTag from 'components/widgets/tag/guest_tag';
import {Constants, StoragePrefixes} from 'utils/constants';
import {getIntl} from 'utils/i18n';
import * as Utils from 'utils/utils';
import type {GlobalState} from 'types/store';
@@ -111,7 +113,7 @@ interface WrappedChannel {
unread_mentions?: number;
}
type Props = SuggestionProps<WrappedChannel> & {
type Props = SuggestionProps<WrappedChannel> & WrappedComponentProps & {
channelMember: ChannelMembership;
collapsedThreads: boolean;
dmChannelTeammate?: UserProfile;
@@ -220,7 +222,7 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLDivElement, Props>((props,
let deactivated = '';
if (teammate.delete_at) {
deactivated = (' - ' + Utils.localizeMessage({id: 'channel_switch_modal.deactivated', defaultMessage: 'Deactivated'}));
deactivated = (' - ' + props.intl.formatMessage({id: 'channel_switch_modal.deactivated', defaultMessage: 'Deactivated'}));
}
if (channel.display_name && !(teammate && teammate.is_bot)) {
@@ -228,7 +230,7 @@ const SwitchChannelSuggestion = React.forwardRef<HTMLDivElement, Props>((props,
} else {
name = teammate.username;
if (teammate.id === currentUserId) {
name += (' ' + Utils.localizeMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
name += (' ' + props.intl.formatMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
}
description = deactivated;
}
@@ -317,7 +319,7 @@ function mapStateToPropsForSwitchChannelSuggestion(state: GlobalState, ownProps:
};
}
const ConnectedSwitchChannelSuggestion = connect(mapStateToPropsForSwitchChannelSuggestion, null, null, {forwardRef: true})(SwitchChannelSuggestion);
const ConnectedSwitchChannelSuggestion = connect(mapStateToPropsForSwitchChannelSuggestion, null, null, {forwardRef: true})(injectIntl(SwitchChannelSuggestion));
let prefix = '';
@@ -528,6 +530,8 @@ export default class SwitchChannelProvider extends Provider {
}
userWrappedChannel(user: UserProfile, channel?: ChannelItem): WrappedChannel {
const intl = getIntl();
let displayName = '';
const currentUserId = getCurrentUserId(this.store.getState());
@@ -545,7 +549,7 @@ export default class SwitchChannelProvider extends Provider {
}
if (user.id === currentUserId && displayName) {
displayName += (' ' + Utils.localizeMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
displayName += (' ' + intl.formatMessage({id: 'suggestion.user.isCurrent', defaultMessage: '(you)'}));
}
return {

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

@@ -30,12 +30,22 @@ exports[`components/team_members_dropdown should match snapshot for a bot with g
id="removeFromTeam"
onClick={[Function]}
show={true}
text="Remove From Team"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove from Team"
id="team_members_dropdown.leave_team"
/>
}
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Member"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Make Team Member"
id="team_members_dropdown.makeMember"
/>
}
/>
</Menu>
</div>
@@ -72,12 +82,22 @@ exports[`components/team_members_dropdown should match snapshot for team_members
id="removeFromTeam"
onClick={[Function]}
show={true}
text="Remove From Team"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove from Team"
id="team_members_dropdown.leave_team"
/>
}
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Member"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Make Team Member"
id="team_members_dropdown.makeMember"
/>
}
/>
</Menu>
</div>
@@ -114,12 +134,22 @@ exports[`components/team_members_dropdown should match snapshot opening dropdown
id="removeFromTeam"
onClick={[Function]}
show={true}
text="Remove From Team"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Remove from Team"
id="team_members_dropdown.leave_team"
/>
}
/>
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Member"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Make Team Member"
id="team_members_dropdown.makeMember"
/>
}
/>
</Menu>
</div>
@@ -155,7 +185,12 @@ exports[`components/team_members_dropdown should match snapshot with group-const
<MenuItemAction
onClick={[Function]}
show={true}
text="Make Member"
text={
<Memo(MemoizedFormattedMessage)
defaultMessage="Make Team Member"
id="team_members_dropdown.makeMember"
/>
}
/>
</Menu>
</div>

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

@@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import TeamMembersDropdown from 'components/team_members_dropdown/team_members_dropdown';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import {TestHelper} from 'utils/test_helper';
describe('components/team_members_dropdown', () => {
@@ -40,14 +40,14 @@ describe('components/team_members_dropdown', () => {
};
test('should match snapshot for team_members_dropdown', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<TeamMembersDropdown {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
});
test('should match snapshot opening dropdown upwards', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<TeamMembersDropdown
{...baseProps}
index={4}
@@ -59,7 +59,7 @@ describe('components/team_members_dropdown', () => {
test('should match snapshot with group-constrained team', () => {
baseProps.currentTeam.group_constrained = true;
const wrapper = shallow(
const wrapper = shallowWithIntl(
<TeamMembersDropdown {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();
@@ -68,7 +68,7 @@ describe('components/team_members_dropdown', () => {
test('should match snapshot for a bot with group-constrained team', () => {
baseProps.currentTeam.group_constrained = true;
baseProps.user = bot;
const wrapper = shallow(
const wrapper = shallowWithIntl(
<TeamMembersDropdown {...baseProps}/>,
);
expect(wrapper).toMatchSnapshot();

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

@@ -2,7 +2,8 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import type {Team, TeamMembership} from '@mattermost/types/teams';
import type {UserProfile} from '@mattermost/types/users';
@@ -17,11 +18,10 @@ import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {getHistory} from 'utils/browser_history';
import * as Utils from 'utils/utils';
const ROWS_FROM_BOTTOM_TO_OPEN_UP = 3;
type Props = {
type Props = WrappedComponentProps & {
user: UserProfile;
currentUser: UserProfile;
teamMember: TeamMembership;
@@ -50,7 +50,7 @@ type State = {
role: string|null;
}
export default class TeamMembersDropdown extends React.PureComponent<Props, State> {
class TeamMembersDropdown extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
@@ -141,7 +141,7 @@ export default class TeamMembersDropdown extends React.PureComponent<Props, Stat
);
}
const {currentTeam, teamMember, user} = this.props;
const {currentTeam, teamMember, user, intl} = this.props;
let currentRoles = null;
@@ -253,19 +253,34 @@ export default class TeamMembersDropdown extends React.PureComponent<Props, Stat
<Menu.ItemAction
id='removeFromTeam'
onClick={this.handleRemoveFromTeam}
text={Utils.localizeMessage({id: 'team_members_dropdown.leave_team', defaultMessage: 'Remove From Team'})}
text={
<FormattedMessage
id='team_members_dropdown.leave_team'
defaultMessage='Remove from Team'
/>
}
/>
);
const menuMakeAdmin = (
<Menu.ItemAction
onClick={this.handleMakeAdmin}
text={Utils.localizeMessage({id: 'team_members_dropdown.makeAdmin', defaultMessage: 'Make Team Admin'})}
text={
<FormattedMessage
id='team_members_dropdown.makeAdmin'
defaultMessage='Make Team Admin'
/>
}
/>
);
const menuMakeMember = (
<Menu.ItemAction
onClick={this.handleMakeMember}
text={Utils.localizeMessage({id: 'team_members_dropdown.makeMember', defaultMessage: 'Make Member'})}
text={
<FormattedMessage
id='team_members_dropdown.makeMember'
defaultMessage='Make Team Member'
/>
}
/>
);
return (
@@ -283,7 +298,7 @@ export default class TeamMembersDropdown extends React.PureComponent<Props, Stat
<Menu
openLeft={true}
openUp={openUp}
ariaLabel={Utils.localizeMessage({id: 'team_members_dropdown.menuAriaLabel', defaultMessage: 'Change the role of a team member'})}
ariaLabel={intl.formatMessage({id: 'team_members_dropdown.menuAriaLabel', defaultMessage: 'Change the role of a team member'})}
>
{canRemoveFromTeam ? menuRemove : null}
{showMakeAdmin ? menuMakeAdmin : null}
@@ -296,3 +311,5 @@ export default class TeamMembersDropdown extends React.PureComponent<Props, Stat
);
}
}
export default injectIntl(TeamMembersDropdown);

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

@@ -6,7 +6,8 @@ import React from 'react';
import {DragDropContext, Droppable} from 'react-beautiful-dnd';
import type {DroppableProvided, DropResult} from 'react-beautiful-dnd';
import Scrollbars from 'react-custom-scrollbars';
import {FormattedMessage} from 'react-intl';
import {injectIntl, FormattedMessage} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import type {RouteComponentProps} from 'react-router-dom';
import type {Team} from '@mattermost/types/teams';
@@ -26,7 +27,7 @@ import * as Utils from 'utils/utils';
import type {PropsFromRedux} from './index';
export interface Props extends PropsFromRedux {
export interface Props extends PropsFromRedux, WrappedComponentProps {
location: RouteComponentProps['location'];
}
@@ -62,7 +63,7 @@ export function renderThumbVertical(props: Props) {
);
}
export default class TeamSidebar extends React.PureComponent<Props, State> {
export class TeamSidebar extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
@@ -202,6 +203,8 @@ export default class TeamSidebar extends React.PureComponent<Props, State> {
};
render() {
const {intl} = this.props;
const root: Element | null = document.querySelector('#root');
if (this.props.myTeams.length <= 1) {
root!.classList.remove('multi-teams');
@@ -246,7 +249,7 @@ export default class TeamSidebar extends React.PureComponent<Props, State> {
<i
className='icon icon-plus'
role={'img'}
aria-label={Utils.localizeMessage({id: 'sidebar.team_menu.button.plusIcon', defaultMessage: 'Plus Icon'})}
aria-label={intl.formatMessage({id: 'sidebar.team_menu.button.plusIcon', defaultMessage: 'Plus Icon'})}
/>
);
@@ -347,3 +350,5 @@ export default class TeamSidebar extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(TeamSidebar);

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {FormattedMessage} from 'react-intl';
import {FormattedMessage, useIntl} from 'react-intl';
import type {ListChildComponentProps} from 'react-window';
import {VariableSizeList} from 'react-window';
import InfiniteLoader from 'react-window-infinite-loader';
@@ -19,7 +19,6 @@ import Menu from 'components/widgets/menu/menu';
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
import {ModalIdentifiers} from 'utils/constants';
import * as Utils from 'utils/utils';
import type {ModalData} from 'types/actions';
@@ -58,6 +57,7 @@ const UserGroupsList = (props: Props) => {
const variableSizeListRef = useRef<VariableSizeList | null>(null);
const [hasMounted, setHasMounted] = useState(false);
const [overflowState, setOverflowState] = useState('overlay');
const {formatMessage} = useIntl();
useEffect(() => {
if (groups.length === 1) {
@@ -171,7 +171,7 @@ const UserGroupsList = (props: Props) => {
openLeft={true}
openUp={groupListOpenUp(index)}
className={'group-actions-menu'}
ariaLabel={Utils.localizeMessage({id: 'admin.user_item.menuAriaLabel', defaultMessage: 'User Actions Menu'})}
ariaLabel={formatMessage({id: 'admin.user_item.menuAriaLabel', defaultMessage: 'User Actions Menu'})}
>
<Menu.Group>
<Menu.ItemAction
@@ -179,7 +179,7 @@ const UserGroupsList = (props: Props) => {
goToViewGroupModal(group);
}}
icon={<i className='icon-account-multiple-outline'/>}
text={Utils.localizeMessage({id: 'user_groups_modal.viewGroup', defaultMessage: 'View Group'})}
text={formatMessage({id: 'user_groups_modal.viewGroup', defaultMessage: 'View Group'})}
disabled={false}
/>
</Menu.Group>
@@ -190,7 +190,7 @@ const UserGroupsList = (props: Props) => {
archiveGroup(group.id);
}}
icon={<i className='icon-archive-outline'/>}
text={Utils.localizeMessage({id: 'user_groups_modal.archiveGroup', defaultMessage: 'Archive Group'})}
text={formatMessage({id: 'user_groups_modal.archiveGroup', defaultMessage: 'Archive Group'})}
disabled={false}
isDangerous={true}
/>
@@ -200,7 +200,7 @@ const UserGroupsList = (props: Props) => {
restoreGroup(group.id);
}}
icon={<i className='icon-restore'/>}
text={Utils.localizeMessage({id: 'user_groups_modal.restoreGroup', defaultMessage: 'Restore Group'})}
text={formatMessage({id: 'user_groups_modal.restoreGroup', defaultMessage: 'Restore Group'})}
disabled={false}
/>
</Menu.Group>

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

@@ -5,6 +5,44 @@ exports[`components/widgets/menu/menu_items/submenu_item empty subMenu should ma
action={[MockFunction]}
direction="left"
id="1"
intl={
Object {
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": "span",
"timeZone": "Etc/UTC",
}
}
renderSelected={true}
root={true}
show={true}
@@ -59,6 +97,44 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
action={[MockFunction]}
direction="left"
id="1"
intl={
Object {
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": "span",
"timeZone": "Etc/UTC",
}
}
renderSelected={true}
root={true}
show={true}
@@ -124,6 +200,44 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
<SubMenuItem
direction="left"
id="A"
intl={
Object {
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": "span",
"timeZone": "Etc/UTC",
}
}
renderSelected={true}
root={false}
show={true}
@@ -180,6 +294,44 @@ exports[`components/widgets/menu/menu_items/submenu_item present subMenu should
<SubMenuItem
direction="left"
id="B"
intl={
Object {
"$t": [Function],
"defaultFormats": Object {},
"defaultLocale": "en",
"defaultRichTextElements": undefined,
"fallbackOnEmptyString": true,
"formatDate": [Function],
"formatDateTimeRange": [Function],
"formatDateToParts": [Function],
"formatDisplayName": [Function],
"formatList": [Function],
"formatListToParts": [Function],
"formatMessage": [Function],
"formatNumber": [Function],
"formatNumberToParts": [Function],
"formatPlural": [Function],
"formatRelativeTime": [Function],
"formatTime": [Function],
"formatTimeToParts": [Function],
"formats": Object {},
"formatters": Object {
"getDateTimeFormat": [Function],
"getDisplayNames": [Function],
"getListFormat": [Function],
"getMessageFormat": [Function],
"getNumberFormat": [Function],
"getPluralRules": [Function],
"getRelativeTimeFormat": [Function],
},
"locale": "en",
"messages": Object {},
"onError": [Function],
"onWarn": [Function],
"textComponent": "span",
"timeZone": "Etc/UTC",
}
}
renderSelected={true}
root={false}
show={true}

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

@@ -1,13 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {mount} from 'enzyme';
import React from 'react';
import {render, screen, userEvent} from 'tests/react_testing_utils';
import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import {screen, userEvent, renderWithContext} from 'tests/react_testing_utils';
import Constants from 'utils/constants';
import SubMenuItem from './submenu_item';
import SubMenuItem, {SubMenuItem as SubMenuItemClass} from './submenu_item';
jest.mock('../is_mobile_view_hack', () => ({
isMobile: jest.fn(() => false),
@@ -15,7 +15,7 @@ jest.mock('../is_mobile_view_hack', () => ({
describe('components/widgets/menu/menu_items/submenu_item', () => {
test('empty subMenu should match snapshot', () => {
const wrapper = mount(
const wrapper = mountWithIntl(
<SubMenuItem
key={'_pluginmenuitem'}
id={'1'}
@@ -30,7 +30,7 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
});
test('present subMenu should match snapshot with submenu', () => {
const wrapper = mount(
const wrapper = mountWithIntl(
<SubMenuItem
key={'_pluginmenuitem'}
id={'1'}
@@ -60,7 +60,7 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
const action2 = jest.fn();
const action3 = jest.fn();
render(
renderWithContext(
<SubMenuItem
key={'_pluginmenuitem'}
id={'Z'}
@@ -98,7 +98,7 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
});
test('should show/hide submenu based on keyboard commands', () => {
const wrapper = mount<SubMenuItem>(
const wrapper = mountWithIntl(
<SubMenuItem
key={'_pluginmenuitem'}
id={'1'}
@@ -109,16 +109,18 @@ describe('components/widgets/menu/menu_items/submenu_item', () => {
/>,
);
wrapper.instance().show = jest.fn();
wrapper.instance().hide = jest.fn();
const instance = wrapper.find(SubMenuItemClass).instance() as SubMenuItemClass;
wrapper.instance().handleKeyDown({keyCode: Constants.KeyCodes.ENTER[1]} as any);
expect(wrapper.instance().show).toHaveBeenCalled();
instance.show = jest.fn();
instance.hide = jest.fn();
wrapper.instance().handleKeyDown({keyCode: Constants.KeyCodes.LEFT[1]} as any);
expect(wrapper.instance().hide).toHaveBeenCalled();
instance.handleKeyDown({keyCode: Constants.KeyCodes.ENTER[1]} as any);
expect(instance.show).toHaveBeenCalled();
wrapper.instance().handleKeyDown({keyCode: Constants.KeyCodes.RIGHT[1]} as any);
expect(wrapper.instance().show).toHaveBeenCalled();
instance.handleKeyDown({keyCode: Constants.KeyCodes.LEFT[1]} as any);
expect(instance.hide).toHaveBeenCalled();
instance.handleKeyDown({keyCode: Constants.KeyCodes.RIGHT[1]} as any);
expect(instance.show).toHaveBeenCalled();
});
});

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

@@ -4,12 +4,13 @@
import classNames from 'classnames';
import React from 'react';
import type {CSSProperties} from 'react';
import {injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';
import {showMobileSubMenuModal} from 'actions/global_actions';
import Constants from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
import * as Utils from 'utils/utils';
import type {Menu} from 'types/store/plugins';
@@ -38,7 +39,7 @@ import './menu_item.scss';
// }
// Submenus can contain Submenus as well
export type Props = {
export type Props = WrappedComponentProps & {
id?: string;
postId?: string;
text: React.ReactNode;
@@ -68,12 +69,12 @@ type State = {
/**
* @deprecated Use the "webapp/channels/src/components/menu" instead.
*/
export default class SubMenuItem extends React.PureComponent<Props, State> {
export class SubMenuItem extends React.PureComponent<Props, State> {
private node: React.RefObject<HTMLLIElement>;
public static defaultProps = {
show: true,
direction: 'left',
direction: 'left' as const,
subMenuClass: 'pl-4',
renderSelected: true,
};
@@ -149,7 +150,7 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
};
public render() {
const {id, postId, text, selectedValueText, subMenu, icon, filter, ariaLabel, direction, styleSelectableItem, extraText, renderSelected, rightDecorator, tabIndex} = this.props;
const {id, postId, text, selectedValueText, subMenu, icon, filter, ariaLabel, direction, styleSelectableItem, extraText, renderSelected, rightDecorator, tabIndex, intl} = this.props;
const isMobile = isMobileViewHack();
if (filter && !filter(id)) {
@@ -191,7 +192,7 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
const hasDivider = s.id === 'ChannelMenu-moveToDivider';
let aria = ariaLabel;
if (s.action) {
aria = s.text === selectedValueText ? s.text + ' ' + Utils.localizeMessage({id: 'sidebar.menu.item.selected', defaultMessage: 'selected'}) : s.text + ' ' + Utils.localizeMessage({id: 'sidebar.menu.item.notSelected', defaultMessage: 'not selected'});
aria = s.text === selectedValueText ? s.text + ' ' + intl.formatMessage({id: 'sidebar.menu.item.selected', defaultMessage: 'selected'}) : s.text + ' ' + intl.formatMessage({id: 'sidebar.menu.item.notSelected', defaultMessage: 'not selected'});
}
return (
<span
@@ -213,6 +214,7 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
direction={s.direction}
isHeader={s.isHeader}
tabIndex={1}
intl={this.props.intl}
/>
{s.text === selectedValueText && <span className='sorting-menu-checkbox'>
<i className='icon-check'/>
@@ -248,7 +250,7 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
<span
id={'channelHeaderDropdownIconRight_' + id}
className={classNames([`fa fa-angle-right SubMenu__icon-right${hasSubmenu ? '' : '-empty'}`, {mobile: isMobile}])}
aria-label={Utils.localizeMessage({id: 'post_info.submenu.icon', defaultMessage: 'submenu icon'}).toLowerCase()}
aria-label={intl.formatMessage({id: 'post_info.submenu.icon', defaultMessage: 'submenu icon'}).toLowerCase()}
/>
}
</div>
@@ -259,3 +261,5 @@ export default class SubMenuItem extends React.PureComponent<Props, State> {
);
}
}
export default injectIntl(SubMenuItem);

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

@@ -44,25 +44,18 @@ exports[`components/submenu_modal should match snapshot 1`] = `
ariaLabel="mobile submenu"
openLeft={true}
>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[MockFunction]}
direction="left"
id="A"
key="A"
renderSelected={true}
root={false}
show={true}
subMenuClass="pl-4"
text="Text A"
/>
<SubMenuItem
<injectIntl(SubMenuItem)
action={[MockFunction]}
direction="left"
id="B"
key="B"
renderSelected={true}
root={false}
show={true}
subMenu={
Array [
Object {
@@ -73,7 +66,6 @@ exports[`components/submenu_modal should match snapshot 1`] = `
},
]
}
subMenuClass="pl-4"
text="Text B"
/>
</Menu>

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

@@ -6,8 +6,7 @@ import {shallow} from 'enzyme';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {withIntl} from 'tests/helpers/intl-test-helper';
import {render, screen, userEvent} from 'tests/react_testing_utils';
import {renderWithContext, screen, userEvent} from 'tests/react_testing_utils';
import SubMenuModal from './submenu_modal';
@@ -16,8 +15,8 @@ jest.mock('../../is_mobile_view_hack', () => ({
}));
(global as any).MutationObserver = class {
public disconnect() {}
public observe() {}
public disconnect() { }
public observe() { }
};
describe('components/submenu_modal', () => {
@@ -58,9 +57,9 @@ describe('components/submenu_modal', () => {
});
test('should hide on modal body click', async () => {
const view = render(withIntl(
const view = renderWithContext(
<SubMenuModal {...baseProps}/>,
));
);
screen.getByText('Text A');
screen.getByText('Text B');
@@ -78,7 +77,7 @@ describe('components/submenu_modal', () => {
...baseProps,
};
render(
renderWithContext(
<SubMenuModal {...props}/>,
);

29
webapp/channels/src/utils/i18n.test.tsx Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as i18nSelectors from 'selectors/i18n';
import {getIntl} from './i18n';
describe('i18n', () => {
jest.spyOn(i18nSelectors, 'getTranslations').mockReturnValue({
test_key: 'expected value',
});
jest.spyOn(i18nSelectors, 'getCurrentLocale').mockReturnValue('en');
it('getIntl.formatMessage should resolve translated string', () => {
const intl = getIntl();
const fm = intl.formatMessage; // avoid triggering mmjstool
const actual = fm({id: 'test_key', defaultMessage: 'not found'});
expect(actual).toBe('expected value');
});
it('getIntl.formatMessage should resolve unknown string to default message', () => {
const intl = getIntl();
const fm = intl.formatMessage; // avoid triggering mmjstool
const actual = fm({id: 'unknown_key', defaultMessage: 'not found'});
expect(actual).toBe('not found');
});
});

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

@@ -2,7 +2,25 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, type IntlShape, type MessageDescriptor} from 'react-intl';
import {FormattedMessage, createIntl, createIntlCache, type IntlShape, type MessageDescriptor} from 'react-intl';
import {getCurrentLocale, getTranslations} from 'selectors/i18n';
import store from 'stores/redux_store';
const cache = createIntlCache();
// getIntl returns an instance of IntlShape for the current locale.
// Prefer `useIntl` and `FormattedMessage` and only use this selectively when
// outside of React components.
export function getIntl(): IntlShape {
const state = store.getState();
const locale = getCurrentLocale(state);
return createIntl({
locale,
messages: getTranslations(state, locale),
}, cache);
}
export function isMessageDescriptor(descriptor: unknown): descriptor is MessageDescriptor {
return Boolean(descriptor && (descriptor as MessageDescriptor).id);