MM-57119 Migrate various components to use WithTooltip (#26458)

* MM-57119 Migrate MultiSelectCard to use WithTooltip

* MM-57119 Migrate SeatsCalculator to use WithTooltip

* MM-57119 Migrate BillingSummary and RenewalCard to WithTooltip

* Add getEmojiName helper function

* Add emojiStyle=large option for WithTooltip

* MM-57119 Migrate PostRecentReactions to use WithTooltip

* MM-57119 Migrate Reaction to use WithTooltip

* MM-57119 Migrate PostReaction to use WithTooltip

* MM-57119 Migrate ReactionList to use WithTooltip

* Change how WithTooltip's shortcut prop works to take a sequence of keys

* Convert TeamButton to functional component

* MM-57119 Migrate TeamButton to use WithTooltip

* MM-57119 Migrate ChannelFilter to use WithTooltip

* Revert unintentional change

* Finish removing values prop from KeyboardShortcutSequence

* Address feedback

* Add margin between emoji and text
Этот коммит содержится в:
Harrison Healey
2024-03-18 14:39:09 -04:00
коммит произвёл GitHub
родитель 8c3ab07d49
Коммит 297135c5b4
38 изменённых файлов: 772 добавлений и 929 удалений

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

@@ -7,6 +7,7 @@ import {Preferences as ReduxPreferences} from 'mattermost-redux/constants';
import {getCustomEmojisByName as selectCustomEmojisByName, getCustomEmojisEnabled} from 'mattermost-redux/selectors/entities/emojis';
import {get} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiMap, getRecentEmojisData, getRecentEmojisNames, isCustomEmojiEnabled} from 'selectors/emojis';
import {isCustomStatusEnabled, makeGetCustomStatus} from 'selectors/views/custom_status';
@@ -68,16 +69,13 @@ export function addRecentEmojis(aliases) {
let updatedRecentEmojis = [...recentEmojis];
for (const alias of aliases) {
let name;
const emoji = emojiMap.get(alias);
if (!emoji) {
continue;
} else if (emoji.short_name) {
name = emoji.short_name;
} else {
name = emoji.name;
}
const name = getEmojiName(emoji);
const currentEmojiIndexInRecentList = updatedRecentEmojis.findIndex((recentEmoji) => recentEmoji.name === name);
if (currentEmojiIndexInRecentList > -1) {
const currentEmojiInRecentList = updatedRecentEmojis[currentEmojiIndexInRecentList];

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

@@ -11,10 +11,6 @@
}
}
#BillingSubscriptions__seatOverageTooltip {
margin-left: -5px;
}
.BillingSubscriptions__tooltip.tooltip {
font-family: 'Open Sans', sans-serif;
@@ -67,18 +63,6 @@
}
}
.BillingSubscriptions__tooltipTitle {
font-size: 16px;
font-weight: 600;
line-height: 24px;
}
.BillingSubscriptions__tooltipMessage {
margin-top: 8px;
font-size: 14px;
line-height: 20px;
}
.UpgradeMattermostCloud {
text-align: center;
}

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedDate, FormattedMessage, FormattedNumber} from 'react-intl';
import {FormattedDate, FormattedMessage, FormattedNumber, defineMessages} from 'react-intl';
import {useDispatch} from 'react-redux';
import {CheckCircleOutlineIcon, CheckIcon, ClockOutlineIcon} from '@mattermost/compass-icons/components';
@@ -18,11 +18,21 @@ import CloudInvoicePreview from 'components/cloud_invoice_preview';
import EmptyBillingHistorySvg from 'components/common/svg_images_components/empty_billing_history_svg';
import UpgradeSvg from 'components/common/svg_images_components/upgrade_svg';
import ExternalLink from 'components/external_link';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import WithTooltip from 'components/with_tooltip';
import {BillingSchemes, CloudLinks, TrialPeriodDays, ModalIdentifiers} from 'utils/constants';
const messages = defineMessages({
partialChargesTooltipTitle: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges',
defaultMessage: 'What are partial charges?',
},
partialChargesTooltipText: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message',
defaultMessage: 'Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.',
},
});
export const noBillingHistory = (
<div className='BillingSummary__noBillingHistory'>
<EmptyBillingHistorySvg
@@ -180,6 +190,7 @@ type InvoiceInfoProps = {
export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasMore, willRenew}: InvoiceInfoProps) => {
const dispatch = useDispatch();
const isUpcomingInvoice = invoice?.status.toLowerCase() === 'upcoming';
const openInvoicePreview = () => {
dispatch(
@@ -298,32 +309,14 @@ export const InvoiceInfo = ({invoice, product, fullCharges, partialCharges, hasM
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
defaultMessage='Partial charges'
/>
<OverlayTrigger
delayShow={500}
<WithTooltip
id='BillingSubscriptions__seatOverageTooltip'
title={messages.partialChargesTooltipTitle}
hint={messages.partialChargesTooltipText}
placement='bottom'
overlay={
<Tooltip
id='BillingSubscriptions__seatOverageTooltip'
className='BillingSubscriptions__tooltip BillingSubscriptions__tooltip-right'
positionLeft={390}
>
<div className='BillingSubscriptions__tooltipTitle'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges'
defaultMessage='What are partial charges?'
/>
</div>
<div className='BillingSubscriptions__tooltipMessage'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message'
defaultMessage='Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.'
/>
</div>
</Tooltip>
}
>
<i className='icon-information-outline'/>
</OverlayTrigger>
</WithTooltip>
</div>
{partialCharges.map((charge: any) => (
<div

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

@@ -238,7 +238,7 @@ describe('components/AdvancedCreateComment', () => {
(wrapper.instance() as any).textboxRef.current = {getInputBox: jest.fn(mockImpl), getBoundingClientRect: jest.fn(), focus: jest.fn()};
wrapper.instance().handleEmojiClick({name: 'smile'} as any);
wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
expect(onUpdateCommentDraft).toHaveBeenCalled();
@@ -252,7 +252,7 @@ describe('components/AdvancedCreateComment', () => {
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test', uploadsInProgress: [], fileInfos: []}),
caretPosition: 'test'.length, // cursor is at the end
});
wrapper.instance().handleEmojiClick({name: 'smile'} as any);
wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
// Message with no space at the end
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);
@@ -264,7 +264,7 @@ describe('components/AdvancedCreateComment', () => {
wrapper.setState({draft: TestHelper.getPostDraftMock({message: 'test ', uploadsInProgress: [], fileInfos: []}),
caretPosition: 'test '.length, // cursor is at the end
});
wrapper.instance().handleEmojiClick({name: 'smile'} as any);
wrapper.instance().handleEmojiClick(TestHelper.getCustomEmojiMock({name: 'smile'}));
// Message with space at the end
jest.advanceTimersByTime(Constants.SAVE_DRAFT_TIMEOUT);

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

@@ -15,6 +15,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {Posts} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions';
@@ -471,7 +472,7 @@ class AdvancedCreateComment extends React.PureComponent<Props, State> {
};
handleEmojiClick = (emoji: Emoji) => {
const emojiAlias = ('short_name' in emoji && emoji.short_name) || emoji.name;
const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) {
//Oops... There went something wrong

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

@@ -18,6 +18,7 @@ import type {PreferenceType} from '@mattermost/types/preferences';
import {Posts} from 'mattermost-redux/constants';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import {sortFileInfos} from 'mattermost-redux/utils/file_utils';
import * as GlobalActions from 'actions/global_actions';
@@ -1202,7 +1203,7 @@ class AdvancedCreatePost extends React.PureComponent<Props, State> {
};
handleEmojiClick = (emoji: Emoji) => {
const emojiAlias = ('short_names' in emoji && emoji.short_names && emoji.short_names[0]) || emoji.name;
const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) {
//Oops.. There went something wrong

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

@@ -4,9 +4,9 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import WithTooltip from 'components/with_tooltip';
import './multi_select_card.scss';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
export type Props = {
onClick: () => void;
@@ -49,21 +49,13 @@ const MultiSelectCard = (props: Props) => {
if (props.tooltip) {
button = (
<OverlayTrigger
className='hidden-xs'
delayShow={500}
<WithTooltip
id={props.id}
placement='top'
overlay={
<Tooltip
id={props.tooltip}
className='hidden-xs'
>
{props.tooltip}
</Tooltip>
}
title={props.tooltip}
>
{button}
</OverlayTrigger>
</WithTooltip>
);
}

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

@@ -6,10 +6,11 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {EmoticonPlusOutlineIcon} from '@mattermost/compass-icons/components';
import type {Emoji, SystemEmoji} from '@mattermost/types/emojis';
import type {Emoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts';
import type {ActionResult} from 'mattermost-redux/types/actions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import DeletePostModal from 'components/delete_post_modal';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
@@ -390,8 +391,11 @@ const EditPost = ({editingPost, actions, canEditPost, config, channelId, draft,
};
const handleEmojiClick = (emoji?: Emoji) => {
const emojiAlias = emoji && (((emoji as SystemEmoji).short_names && (emoji as SystemEmoji).short_names[0]) || emoji.name);
if (!emoji) {
return;
}
const emojiAlias = getEmojiName(emoji);
if (!emojiAlias) {
//Oops.. There went something wrong
return;

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

@@ -9,7 +9,7 @@ import type InfiniteLoader from 'react-window-infinite-loader';
import type {Emoji, EmojiCategory} from '@mattermost/types/emojis';
import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerCategories from 'components/emoji_picker/components/emoji_picker_categories';
import EmojiPickerCurrentResults from 'components/emoji_picker/components/emoji_picker_current_results';
@@ -358,7 +358,7 @@ const EmojiPicker = ({
return '';
}
const name = isSystemEmoji(emoji) ? emoji.short_name : emoji.name;
const name = getEmojiName(emoji);
return name.replace(/_/g, ' ');
}, [cursor.emojiId]);

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

@@ -1,64 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/shortcuts/KeyboardShortcutsSequence should create sequence with order 1`] = `
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Ctrl|Alt|{order}",
"id": "team.button.tooltip",
},
"mac": Object {
"defaultMessage": "⌘|⌥|{order}",
"id": "team.button.tooltip.mac",
},
}
}
values={
Object {
"order": 3,
}
}
>
<div
className="shortcut-line"
>
<ShortcutKey
key="Ctrl"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
Ctrl
</mark>
</ShortcutKey>
<ShortcutKey
key="Alt"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
Alt
</mark>
</ShortcutKey>
<ShortcutKey
key="3"
variant="tooltip"
>
<mark
className="shortcut-key shortcut-key--tooltip"
>
3
</mark>
</ShortcutKey>
</div>
</Memo(KeyboardShortcutSequence)>
`;
exports[`components/shortcuts/KeyboardShortcutsSequence should match snapshot when used for modal with description 1`] = `
<Memo(KeyboardShortcutSequence)
shortcut={

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

@@ -9,12 +9,6 @@ export type KeyboardShortcutDescriptor =
| MessageDescriptor
| {default: MessageDescriptor; mac?: MessageDescriptor};
export function isMessageDescriptor(
descriptor: KeyboardShortcutDescriptor,
): descriptor is MessageDescriptor {
return Boolean((descriptor as MessageDescriptor).id);
}
const callsKBShortcuts = {
global: {
callsJoinCall: {
@@ -171,16 +165,6 @@ export const KEYBOARD_SHORTCUTS = {
defaultMessage: 'Navigate to a specific team:\t⌘|⌥|[1-9]',
},
},
teamNavigation: {
default: {
id: t('team.button.tooltip'),
defaultMessage: 'Ctrl|Alt|{order}',
},
mac: {
id: t('team.button.tooltip.mac'),
defaultMessage: '⌘|⌥|{order}',
},
},
navSwitcher: {
default: {
id: t('shortcuts.nav.switcher'),

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

@@ -7,8 +7,6 @@ import {mountWithIntl} from 'tests/helpers/intl-test-helper';
import KeyboardShortcutsSequence from './keyboard_shortcuts_sequence';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from './index';
describe('components/shortcuts/KeyboardShortcutsSequence', () => {
test('should match snapshot when used for modal with description', () => {
const wrapper = mountWithIntl(
@@ -95,21 +93,4 @@ describe('components/shortcuts/KeyboardShortcutsSequence', () => {
expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(2);
expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0);
});
test('should create sequence with order', () => {
const order = 3;
const wrapper = mountWithIntl(
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.teamNavigation}
values={{order}}
hideDescription={true}
isInsideTooltip={true}
/>,
);
expect(wrapper).toMatchSnapshot();
const tag = <span>{'Keyboard shortcuts'}</span>;
expect(wrapper.contains(tag)).toEqual(false);
expect(wrapper.find('.shortcut-key--tooltip')).toHaveLength(3);
expect(wrapper.find('.shortcut-key--shortcut-modal')).toHaveLength(0);
});
});

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

@@ -1,22 +1,20 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {FormatXMLElementFn, PrimitiveType} from 'intl-messageformat';
import React, {memo} from 'react';
import {useIntl} from 'react-intl';
import {ShortcutKeyVariant, ShortcutKey} from 'components/shortcut_key';
import {isMessageDescriptor} from 'utils/i18n';
import {isMac} from 'utils/user_agent';
import {isMessageDescriptor} from './keyboard_shortcuts';
import type {KeyboardShortcutDescriptor} from './keyboard_shortcuts';
import './keyboard_shortcuts_sequence.scss';
type Props = {
shortcut: KeyboardShortcutDescriptor;
values?: Record<string, PrimitiveType | FormatXMLElementFn<string, string>>;
hideDescription?: boolean;
hoistDescription?: boolean;
isInsideTooltip?: boolean;
@@ -32,9 +30,9 @@ function normalizeShortcutDescriptor(shortcut: KeyboardShortcutDescriptor) {
const KEY_SEPARATOR = '|';
function KeyboardShortcutSequence({shortcut, values, hideDescription, hoistDescription, isInsideTooltip}: Props) {
function KeyboardShortcutSequence({shortcut, hideDescription, hoistDescription, isInsideTooltip}: Props) {
const {formatMessage} = useIntl();
const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut), values);
const shortcutText = formatMessage(normalizeShortcutDescriptor(shortcut));
const splitShortcut = shortcutText.split('\t');
let description = '';

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

@@ -17,27 +17,14 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
target={[MockFunction]}
topOffset={-7}
/>
<OverlayTrigger
className="hidden-xs"
defaultOverlayShown={false}
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="reaction-icon-tooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add Reaction"
id="post_info.tooltip.add_reactions"
/>
</Tooltip>
}
<WithTooltip
id="reaction-icon-tooltip"
placement="top"
trigger={
Array [
"hover",
"focus",
]
title={
Object {
"defaultMessage": "Add Reaction",
"id": "post_info.tooltip.add_reactions",
}
}
>
<button
@@ -51,6 +38,6 @@ exports[`components/post_view/PostReaction should match snapshot 1`] = `
className="icon icon--small"
/>
</button>
</OverlayTrigger>
</WithTooltip>
</Connect(Component)>
`;

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

@@ -4,9 +4,9 @@
import {shallow} from 'enzyme';
import React from 'react';
import type {Emoji} from '@mattermost/types/emojis';
import {TestHelper} from 'utils/test_helper';
import PostReaction from 'components/post_view/post_reaction/post_reaction';
import PostReaction from './post_reaction';
describe('components/post_view/PostReaction', () => {
const baseProps = {
@@ -31,7 +31,7 @@ describe('components/post_view/PostReaction', () => {
const wrapper = shallow(<PostReaction {...baseProps}/>);
const instance = wrapper.instance() as PostReaction;
instance.handleToggleEmoji({name: 'smile'} as Emoji);
instance.handleToggleEmoji(TestHelper.getCustomEmojiMock({name: 'smile'}));
expect(baseProps.actions.toggleReaction).toHaveBeenCalledTimes(1);
expect(baseProps.actions.toggleReaction).toHaveBeenCalledWith('post_id_1', 'smile');
expect(baseProps.toggleEmojiPicker).toHaveBeenCalledTimes(1);

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

@@ -3,23 +3,30 @@
import classNames from 'classnames';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {defineMessages} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis';
import Permissions from 'mattermost-redux/constants/permissions';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Tooltip from 'components/tooltip';
import EmojiIcon from 'components/widgets/icons/emoji_icon';
import WithTooltip from 'components/with_tooltip';
import {Locations} from 'utils/constants';
import {localizeMessage} from 'utils/utils';
const TOP_OFFSET = -7;
const messages = defineMessages({
addReaction: {
id: 'post_info.tooltip.add_reactions',
defaultMessage: 'Add Reaction',
},
});
export type Props = {
channelId?: string;
postId: string;
@@ -46,7 +53,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
handleToggleEmoji = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false});
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name;
const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.postId, emojiName);
this.props.toggleEmojiPicker();
};
@@ -83,21 +90,10 @@ export default class PostReaction extends React.PureComponent<Props, State> {
spaceRequiredAbove={spaceRequiredAbove}
spaceRequiredBelow={spaceRequiredBelow}
/>
<OverlayTrigger
className='hidden-xs'
delayShow={500}
<WithTooltip
id='reaction-icon-tooltip'
title={messages.addReaction}
placement='top'
overlay={
<Tooltip
id='reaction-icon-tooltip'
className='hidden-xs'
>
<FormattedMessage
id='post_info.tooltip.add_reactions'
defaultMessage='Add Reaction'
/>
</Tooltip>
}
>
<button
data-testid='post-reaction-emoji-icon'
@@ -110,7 +106,7 @@ export default class PostReaction extends React.PureComponent<Props, State> {
>
<EmojiIcon className='icon icon--small'/>
</button>
</OverlayTrigger>
</WithTooltip>
</React.Fragment>
</ChannelPermissionGate>
);

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

@@ -6,11 +6,10 @@ import React from 'react';
import type {Emoji} from '@mattermost/types/emojis';
import Permissions from 'mattermost-redux/constants/permissions';
import {getEmojiImageUrl} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Tooltip from 'components/tooltip';
import WithTooltip from 'components/with_tooltip';
import {Locations} from 'utils/constants';
@@ -43,7 +42,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
};
handleToggleEmoji = (emoji: Emoji): void => {
const emojiName = 'short_name' in emoji ? emoji.short_name : emoji.name;
const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.postId, emojiName);
};
@@ -70,7 +69,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
function capitalizeFirstLetter(s: string) {
return s[0].toLocaleUpperCase(locale) + s.slice(1);
}
const name = 'short_name' in emoji ? emoji.short_name : emoji.name;
const name = getEmojiName(emoji);
return capitalizeFirstLetter(name.replace(/_/g, ' '));
};
@@ -92,25 +91,12 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
teamId={teamId}
permissions={[Permissions.ADD_REACTION]}
>
<OverlayTrigger
className='hidden-xs'
delayShow={500}
<WithTooltip
id='post_info.emoji.tooltip'
title={this.emojiName(emoji, this.props.locale)}
emoji={getEmojiName(emoji)}
emojiStyle='large'
placement='top'
overlay={
<Tooltip
id='post_info.emoji.tooltip'
className='hidden-xs'
>
<div>
<img
className='Reaction__emoji Reaction__emoji--large'
src={getEmojiImageUrl(emoji)}
width={48}
/>
</div>
{this.emojiName(emoji, this.props.locale)}
</Tooltip>
}
>
<div>
<React.Fragment>
@@ -121,7 +107,7 @@ export default class PostRecentReactions extends React.PureComponent<Props, Stat
/>
</React.Fragment>
</div>
</OverlayTrigger>
</WithTooltip>
</ChannelPermissionGate>
),
);

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

@@ -7,7 +7,7 @@ import {useIntl} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis';
import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiImageUrl, getEmojiName} from 'mattermost-redux/utils/emoji_utils';
type Props = {
emoji: Emoji;
@@ -24,7 +24,7 @@ const EmojiItem = ({emoji, onItemClick, order}: Props) => {
const itemClassName = 'post-menu__item';
const emojiName = isSystemEmoji(emoji) ? emoji.short_name ?? emoji.name : emoji.name;
const emojiName = getEmojiName(emoji);
return (
<div

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

@@ -1,50 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to add reaction 1`] = `
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
onEnter={[Function]}
overlay={
<Tooltip
id="post_id-smile-reaction"
>
<Memo(Connect(ReactionTooltip))
canAddReactions={false}
canRemoveReactions={true}
currentUserReacted={false}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
<Connect(ReactionTooltip)
canAddReactions={false}
canRemoveReactions={true}
currentUserReacted={false}
emojiName="smile"
id="post_id-smile-reaction"
onShow={[Function]}
reactions={
Array [
"hover",
"focus",
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
@@ -87,54 +64,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
</span>
</span>
</button>
</OverlayTrigger>
</Connect(ReactionTooltip)>
`;
exports[`components/post_view/Reaction should apply read-only class if user does not have permission to remove reaction 1`] = `
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
onEnter={[Function]}
overlay={
<Tooltip
id="post_id-smile-reaction"
>
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={false}
currentUserReacted={true}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={false}
currentUserReacted={true}
emojiName="smile"
id="post_id-smile-reaction"
onShow={[Function]}
reactions={
Array [
"hover",
"focus",
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
@@ -177,54 +131,31 @@ exports[`components/post_view/Reaction should apply read-only class if user does
</span>
</span>
</button>
</OverlayTrigger>
</Connect(ReactionTooltip)>
`;
exports[`components/post_view/Reaction should match snapshot 1`] = `
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
onEnter={[Function]}
overlay={
<Tooltip
id="post_id-smile-reaction"
>
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={false}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={false}
emojiName="smile"
id="post_id-smile-reaction"
onShow={[Function]}
reactions={
Array [
"hover",
"focus",
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_2",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
@@ -267,54 +198,31 @@ exports[`components/post_view/Reaction should match snapshot 1`] = `
</span>
</span>
</button>
</OverlayTrigger>
</Connect(ReactionTooltip)>
`;
exports[`components/post_view/Reaction should match snapshot when a current user reacted to a post 1`] = `
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
onEnter={[Function]}
overlay={
<Tooltip
id="post_id-smile-reaction"
>
<Memo(Connect(ReactionTooltip))
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={true}
emojiIcon={
<img
className="Reaction__emoji emoticon"
src="emoji_image_url"
/>
}
emojiName="smile"
reactions={
Array [
Object {
"create_at": 0,
"emoji_name": ":cry:",
"post_id": "post_id",
"user_id": "user_id_1",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
/>
</Tooltip>
}
placement="top"
shouldUpdatePosition={true}
trigger={
<Connect(ReactionTooltip)
canAddReactions={true}
canRemoveReactions={true}
currentUserReacted={true}
emojiName="smile"
id="post_id-smile-reaction"
onShow={[Function]}
reactions={
Array [
"hover",
"focus",
Object {
"create_at": 0,
"emoji_name": ":cry:",
"post_id": "post_id",
"user_id": "user_id_1",
},
Object {
"create_at": 0,
"emoji_name": ":smile:",
"post_id": "post_id",
"user_id": "user_id_3",
},
]
}
>
@@ -357,7 +265,7 @@ exports[`components/post_view/Reaction should match snapshot when a current user
</span>
</span>
</button>
</OverlayTrigger>
</Connect(ReactionTooltip)>
`;
exports[`components/post_view/Reaction should return null/empty if no emojiImageUrl 1`] = `""`;

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

@@ -71,14 +71,6 @@
margin: 0 2px 0 0;
object-fit: contain;
vertical-align: middle;
&--large {
width: 48px;
max-width: none;
height: 48px;
max-height: none;
margin: 4px 0;
}
}
&__emoji--post-menu {

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

@@ -6,9 +6,6 @@ import React from 'react';
import type {Post} from '@mattermost/types/posts';
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import * as Utils from 'utils/utils';
import ReactionTooltip from './reaction_tooltip';
@@ -218,23 +215,14 @@ export default class Reaction extends React.PureComponent<Props, State> {
);
return (
<OverlayTrigger
delayShow={500}
placement='top'
shouldUpdatePosition={true}
overlay={
<Tooltip id={`${this.props.post.id}-${this.props.emojiName}-reaction`}>
<ReactionTooltip
canAddReactions={canAddReactions}
canRemoveReactions={canRemoveReactions}
currentUserReacted={currentUserReacted}
emojiName={emojiName}
emojiIcon={emojiIcon}
reactions={reactions}
/>
</Tooltip>
}
onEnter={this.loadMissingProfiles}
<ReactionTooltip
id={`${this.props.post.id}-${this.props.emojiName}-reaction`}
canAddReactions={canAddReactions}
canRemoveReactions={canRemoveReactions}
currentUserReacted={currentUserReacted}
emojiName={emojiName}
reactions={reactions}
onShow={this.loadMissingProfiles}
>
<button
id={`postReaction-${this.props.post.id}-${this.props.emojiName}`}
@@ -262,7 +250,7 @@ export default class Reaction extends React.PureComponent<Props, State> {
</span>
</span>
</button>
</OverlayTrigger>
</ReactionTooltip>
);
}
}

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

@@ -2,16 +2,20 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useIntl} from 'react-intl';
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import WithTooltip from 'components/with_tooltip';
type Props = {
canAddReactions: boolean;
canRemoveReactions: boolean;
children: React.ReactNode;
currentUserReacted: boolean;
emojiName: string;
emojiIcon: React.ReactNode;
id: string;
onShow: () => void;
reactions: ReactionType[];
users: string[];
};
@@ -20,123 +24,120 @@ const ReactionTooltip: React.FC<Props> = (props: Props) => {
const {
canAddReactions,
canRemoveReactions,
children,
currentUserReacted,
emojiIcon,
emojiName,
id,
onShow,
reactions,
users,
} = props;
const intl = useIntl();
const otherUsersCount = reactions.length - users.length;
let names: React.ReactNode;
let names;
if (otherUsersCount > 0) {
if (users.length > 0) {
names = (
<FormattedMessage
id='reaction.usersAndOthersReacted'
defaultMessage='{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}'
values={{
users: users.join(', '),
otherUsers: otherUsersCount,
}}
/>
names = intl.formatMessage(
{
id: 'reaction.usersAndOthersReacted',
defaultMessage: '{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}',
},
{
users: users.join(', '),
otherUsers: otherUsersCount,
},
);
} else {
names = (
<FormattedMessage
id='reaction.othersReacted'
defaultMessage='{otherUsers, number} {otherUsers, plural, one {user} other {users}}'
values={{
otherUsers: otherUsersCount,
}}
/>
names = intl.formatMessage(
{
id: 'reaction.othersReacted',
defaultMessage: '{otherUsers, number} {otherUsers, plural, one {user} other {users}}',
},
{
otherUsers: otherUsersCount,
},
);
}
} else if (users.length > 1) {
names = (
<FormattedMessage
id='reaction.usersReacted'
defaultMessage='{users} and {lastUser}'
values={{
users: users.slice(0, -1).join(', '),
lastUser: users[users.length - 1],
}}
/>
names = intl.formatMessage(
{
id: 'reaction.usersReacted',
defaultMessage: '{users} and {lastUser}',
},
{
users: users.slice(0, -1).join(', '),
lastUser: users[users.length - 1],
},
);
} else {
names = users[0];
}
let reactionVerb: React.ReactNode;
let reactionVerb;
if (users.length + otherUsersCount > 1) {
if (currentUserReacted) {
reactionVerb = (
<FormattedMessage
id='reaction.reactionVerb.youAndUsers'
defaultMessage='reacted'
/>
);
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.youAndUsers',
defaultMessage: 'reacted',
});
} else {
reactionVerb = (
<FormattedMessage
id='reaction.reactionVerb.users'
defaultMessage='reacted'
/>
);
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.users',
defaultMessage: 'reacted',
});
}
} else if (currentUserReacted) {
reactionVerb = (
<FormattedMessage
id='reaction.reactionVerb.you'
defaultMessage='reacted'
/>
);
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.you',
defaultMessage: 'reacted',
});
} else {
reactionVerb = (
<FormattedMessage
id='reaction.reactionVerb.user'
defaultMessage='reacted'
/>
);
reactionVerb = intl.formatMessage({
id: 'reaction.reactionVerb.user',
defaultMessage: 'reacted',
});
}
const tooltip = (
<FormattedMessage
id='reaction.reacted'
defaultMessage='{users} {reactionVerb} with {emoji}'
values={{
users: <b>{names}</b>,
reactionVerb,
emoji: <b>{':' + emojiName + ':'}</b>,
}}
/>
const tooltip = intl.formatMessage(
{
id: 'reaction.reacted',
defaultMessage: '{users} {reactionVerb} with {emoji}',
},
{
users: names,
reactionVerb,
emoji: ':' + emojiName + ':',
},
);
let clickTooltip: React.ReactNode;
let clickTooltip;
if (currentUserReacted && canRemoveReactions) {
clickTooltip = (
<FormattedMessage
id='reaction.clickToRemove'
defaultMessage='(click to remove)'
/>
);
clickTooltip = intl.formatMessage({
id: 'reaction.clickToRemove',
defaultMessage: '(click to remove)',
});
} else if (!currentUserReacted && canAddReactions) {
clickTooltip = (
<FormattedMessage
id='reaction.clickToAdd'
defaultMessage='(click to add)'
/>
);
clickTooltip = intl.formatMessage({
id: 'reaction.clickToAdd',
defaultMessage: '(click to add)',
});
}
return (
<>
<div className='reaction-emoji--large'>{emojiIcon}</div>
{tooltip}
<br/>
{clickTooltip}
</>
<WithTooltip
id={id}
emoji={emojiName}
emojiStyle='large'
placement='top'
title={tooltip}
hint={clickTooltip}
onShow={onShow}
>
{children}
</WithTooltip>
);
};

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

@@ -71,25 +71,14 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
}
teamId="teamId"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={400}
overlay={
<Tooltip
id="addReactionTooltip"
>
<Memo(MemoizedFormattedMessage)
defaultMessage="Add a reaction"
id="reaction_list.addReactionTooltip"
/>
</Tooltip>
}
<WithTooltip
id="addReactionTooltip"
placement="top"
trigger={
Array [
"hover",
"focus",
]
title={
Object {
"defaultMessage": "Add a reaction",
"id": "reaction_list.addReactionTooltip",
}
}
>
<button
@@ -104,7 +93,7 @@ exports[`components/ReactionList should render when there are reactions 1`] = `
<AddReactionIcon />
</span>
</button>
</OverlayTrigger>
</WithTooltip>
</Connect(Component)>
</span>
</div>

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

@@ -2,28 +2,33 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {defineMessages} from 'react-intl';
import type {Emoji} from '@mattermost/types/emojis';
import type {Post} from '@mattermost/types/posts';
import type {Reaction as ReactionType} from '@mattermost/types/reactions';
import Permissions from 'mattermost-redux/constants/permissions';
import {isSystemEmoji} from 'mattermost-redux/utils/emoji_utils';
import {getEmojiName} from 'mattermost-redux/utils/emoji_utils';
import EmojiPickerOverlay from 'components/emoji_picker/emoji_picker_overlay';
import OverlayTrigger from 'components/overlay_trigger';
import ChannelPermissionGate from 'components/permissions_gates/channel_permission_gate';
import Reaction from 'components/post_view/reaction';
import Tooltip from 'components/tooltip';
import AddReactionIcon from 'components/widgets/icons/add_reaction_icon';
import WithTooltip from 'components/with_tooltip';
import Constants from 'utils/constants';
import {localizeMessage} from 'utils/utils';
const DEFAULT_EMOJI_PICKER_RIGHT_OFFSET = 15;
const EMOJI_PICKER_WIDTH_OFFSET = 260;
const messages = defineMessages({
addAReaction: {
id: 'reaction_list.addReactionTooltip',
defaultMessage: 'Add a reaction',
},
});
type Props = {
/**
@@ -90,7 +95,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
handleEmojiClick = (emoji: Emoji): void => {
this.setState({showEmojiPicker: false});
const emojiName = isSystemEmoji(emoji) ? emoji.short_names[0] : emoji.name;
const emojiName = getEmojiName(emoji);
this.props.actions.toggleReaction(this.props.post.id, emojiName);
};
@@ -148,15 +153,6 @@ export default class ReactionList extends React.PureComponent<Props, State> {
let emojiPicker = null;
if (this.props.canAddReactions) {
const addReactionTooltip = (
<Tooltip id='addReactionTooltip'>
<FormattedMessage
id='reaction_list.addReactionTooltip'
defaultMessage='Add a reaction'
/>
</Tooltip>
);
emojiPicker = (
<span className='emoji-picker__container'>
<EmojiPickerOverlay
@@ -172,10 +168,10 @@ export default class ReactionList extends React.PureComponent<Props, State> {
teamId={this.props.teamId}
permissions={[Permissions.ADD_REACTION]}
>
<OverlayTrigger
<WithTooltip
id='addReactionTooltip'
title={messages.addAReaction}
placement='top'
delayShow={Constants.OVERLAY_TIME_DELAY}
overlay={addReactionTooltip}
>
<button
aria-label={localizeMessage('reaction.add.ariaLabel', 'Add a reaction')}
@@ -190,7 +186,7 @@ export default class ReactionList extends React.PureComponent<Props, State> {
<AddReactionIcon/>
</span>
</button>
</OverlayTrigger>
</WithTooltip>
</ChannelPermissionGate>
</span>
);

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

@@ -545,16 +545,3 @@
right: 12px;
}
}
.tooltipTitle {
font-weight: 700;
}
.tooltipText {
color: #9a9a9a !important;
text-align: left;
}
.proratedTooltip > .tooltip-inner {
min-width: 300px;
}

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

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage, FormattedNumber, FormattedDate} from 'react-intl';
import {FormattedMessage, FormattedNumber, FormattedDate, defineMessages} from 'react-intl';
import {useDispatch} from 'react-redux';
import type {Invoice, InvoiceLineItem, Product} from '@mattermost/types/cloud';
@@ -14,16 +14,26 @@ import {openModal} from 'actions/views/modals';
import {getPaymentStatus} from 'components/admin_console/billing/billing_summary/billing_summary';
import CloudInvoicePreview from 'components/cloud_invoice_preview';
import OverlayTrigger from 'components/overlay_trigger';
import type {Seats} from 'components/seats_calculator';
import SeatsCalculator from 'components/seats_calculator';
import Tooltip from 'components/tooltip';
import EllipsisHorizontalIcon from 'components/widgets/icons/ellipsis_h_icon';
import WithTooltip from 'components/with_tooltip';
import {BillingSchemes, ModalIdentifiers, TELEMETRY_CATEGORIES, CloudLinks} from 'utils/constants';
import './renewal_card.scss';
const messages = defineMessages({
partialChargesTooltipTitle: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges',
defaultMessage: 'What are partial charges?',
},
partialChargesTooltipText: {
id: 'admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message',
defaultMessage: 'Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.',
},
});
type RenewalCardProps = {
invoice: Invoice;
product?: Product;
@@ -39,6 +49,7 @@ type RenewalCardProps = {
export default function RenewalCard({invoice, product, hasMore, fullCharges, partialCharges, seats, onSeatChange, existingUsers, buttonDisabled, onButtonClick}: RenewalCardProps) {
const dispatch = useDispatch();
const openInvoicePreview = () => {
dispatch(
openModal({
@@ -178,32 +189,14 @@ export default function RenewalCard({invoice, product, hasMore, fullCharges, par
id='admin.billing.subscriptions.billing_summary.lastInvoice.partialCharges'
defaultMessage='Partial charges'
/>
<OverlayTrigger
delayShow={500}
<WithTooltip
id='BillingSubscriptions__seatOverageTooltip'
title={messages.partialChargesTooltipTitle}
hint={messages.partialChargesTooltipText}
placement='bottom'
overlay={
<Tooltip
id='BillingSubscriptions__seatOverageTooltip'
className='BillingSubscriptions__tooltip BillingSubscriptions__tooltip-right'
positionLeft={390}
>
<div className='BillingSubscriptions__tooltipTitle'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges'
defaultMessage='What are partial charges?'
/>
</div>
<div className='BillingSubscriptions__tooltipMessage'>
<FormattedMessage
id='admin.billing.subscriptions.billing_summary.lastInvoice.whatArePartialCharges.message'
defaultMessage='Users who have not been enabled for the full duration of the month are charged at a prorated monthly rate.'
/>
</div>
</Tooltip>
}
>
<i className='icon-information-outline'/>
</OverlayTrigger>
</WithTooltip>
</div>
{partialCharges.map((charge: any) => (
<div

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

@@ -2,18 +2,28 @@
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {useIntl, FormattedMessage, FormattedNumber} from 'react-intl';
import {useIntl, FormattedMessage, FormattedNumber, defineMessages} from 'react-intl';
import {InformationOutlineIcon} from '@mattermost/compass-icons/components';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import Input from 'components/widgets/inputs/input/input';
import WithTooltip from 'components/with_tooltip';
import {Constants, ItemStatus} from 'utils/constants';
import {ItemStatus} from 'utils/constants';
import './seats_calculator.scss';
const messages = defineMessages({
tooltipText: {
id: 'admin.billing.subscription.userCount.tooltipTitle',
defaultMessage: 'Current User Count',
},
tooltipTitle: {
id: 'admin.billing.subscription.userCount.tooltipText',
defaultMessage: 'You must purchase at least the current number of active users.',
},
});
interface Props {
price: number;
seats: Seats;
@@ -154,25 +164,6 @@ export default function SeatsCalculator(props: Props) {
const maxSeats = calculateMaxUsers(annualPricePerSeat);
const total = '$' + intl.formatNumber((parseFloat(props.seats.quantity) || 0) * annualPricePerSeat, {maximumFractionDigits: 2});
const userCountTooltip = (
<Tooltip
id='userCount__tooltip'
className='self-hosted-user-count-tooltip'
>
<div className='tooltipTitle'>
<FormattedMessage
defaultMessage={'Current User Count'}
id={'admin.billing.subscription.userCount.tooltipTitle'}
/>
</div>
<div className='tooltipText'>
<FormattedMessage
defaultMessage={'You must purchase at least the current number of active users.'}
id={'admin.billing.subscription.userCount.tooltipText'}
/>
</div>
</Tooltip>
);
return (
<div className='SeatsCalculator'>
@@ -198,16 +189,17 @@ export default function SeatsCalculator(props: Props) {
</div>
<div className='SeatsCalculator__seats-tooltip'>
<div className='icon'>
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
<WithTooltip
id='userCount__tooltip'
title={messages.tooltipTitle}
hint={messages.tooltipText}
placement='right'
overlay={userCountTooltip}
>
<InformationOutlineIcon
size={18}
color={'rgba(var(--center-channel-text-rgb), 0.75)'}
/>
</OverlayTrigger>
</WithTooltip>
</div>
</div>
</div>
@@ -242,6 +234,5 @@ export default function SeatsCalculator(props: Props) {
)}
</div>
</div>
);
}

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

@@ -4,39 +4,37 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
<div
className="SidebarFilters"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="new-group-tooltip"
>
Filter by unread
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Toggle unread/all channels: Ctrl|Shift|U",
"id": "shortcuts.nav.toggle_unreads",
},
"mac": Object {
"defaultMessage": "Toggle unread/all channels: ⌘|Shift|U",
"id": "shortcuts.nav.toggle_unreads.mac",
},
}
}
/>
</Tooltip>
}
<WithTooltip
id="channel-filter-tooltip"
placement="right"
trigger={
Array [
"hover",
"focus",
]
shortcut={
Object {
"default": Array [
Object {
"defaultMessage": "Ctrl",
"id": "shortcuts.generic.ctrl",
},
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
"mac": Array [
"⌘",
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
}
}
title={
Object {
"defaultMessage": "Filter by unread",
"id": "sidebar_left.channel_filter.filterByUnread",
}
}
>
<a
@@ -49,7 +47,7 @@ exports[`components/sidebar/channel_filter should match snapshot 1`] = `
className="icon icon-filter-variant"
/>
</a>
</OverlayTrigger>
</WithTooltip>
</div>
`;
@@ -57,39 +55,37 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
<div
className="SidebarFilters"
>
<OverlayTrigger
defaultOverlayShown={false}
delayShow={500}
overlay={
<Tooltip
className="hidden-xs"
id="new-group-tooltip"
>
Show all channels
<Memo(KeyboardShortcutSequence)
hideDescription={true}
isInsideTooltip={true}
shortcut={
Object {
"default": Object {
"defaultMessage": "Toggle unread/all channels: Ctrl|Shift|U",
"id": "shortcuts.nav.toggle_unreads",
},
"mac": Object {
"defaultMessage": "Toggle unread/all channels: ⌘|Shift|U",
"id": "shortcuts.nav.toggle_unreads.mac",
},
}
}
/>
</Tooltip>
}
<WithTooltip
id="channel-filter-tooltip"
placement="right"
trigger={
Array [
"hover",
"focus",
]
shortcut={
Object {
"default": Array [
Object {
"defaultMessage": "Ctrl",
"id": "shortcuts.generic.ctrl",
},
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
"mac": Array [
"⌘",
Object {
"defaultMessage": "Shift",
"id": "shortcuts.generic.shift",
},
"U",
],
}
}
title={
Object {
"defaultMessage": "Show all channels",
"id": "sidebar_left.channel_filter.showAllChannels",
}
}
>
<a
@@ -102,6 +98,6 @@ exports[`components/sidebar/channel_filter should match snapshot if the unread f
className="icon icon-filter-variant"
/>
</a>
</OverlayTrigger>
</WithTooltip>
</div>
`;

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

@@ -3,18 +3,33 @@
import classNames from 'classnames';
import React from 'react';
import {injectIntl} from 'react-intl';
import {defineMessages, injectIntl} from 'react-intl';
import type {IntlShape} from 'react-intl';
import {trackEvent} from 'actions/telemetry_actions';
import KeyboardShortcutSequence, {KEYBOARD_SHORTCUTS} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import WithTooltip from 'components/with_tooltip';
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
import Constants from 'utils/constants';
import * as Keyboard from 'utils/keyboard';
const messages = defineMessages({
disableTooltip: {
id: 'sidebar_left.channel_filter.showAllChannels',
defaultMessage: 'Show all channels',
},
enableTooltip: {
id: 'sidebar_left.channel_filter.filterByUnread',
defaultMessage: 'Filter by unread',
},
});
const shortcut = {
default: [ShortcutKeys.ctrl, ShortcutKeys.shift, 'U'],
mac: [ShortcutKeys.cmd, ShortcutKeys.shift, 'U'],
};
type Props = {
intl: IntlShape;
hasMultipleTeams: boolean;
@@ -62,34 +77,15 @@ export class ChannelFilter extends React.PureComponent<Props> {
render() {
const {intl, unreadFilterEnabled, hasMultipleTeams} = this.props;
let tooltipMessage = intl.formatMessage({id: 'sidebar_left.channel_filter.filterByUnread', defaultMessage: 'Filter by unread'});
if (unreadFilterEnabled) {
tooltipMessage = intl.formatMessage({id: 'sidebar_left.channel_filter.showAllChannels', defaultMessage: 'Show all channels'});
}
const unreadsAriaLabel = intl.formatMessage({id: 'sidebar_left.channel_filter.filterUnreadAria', defaultMessage: 'unreads filter'});
const tooltip = (
<Tooltip
id='new-group-tooltip'
className='hidden-xs'
>
{tooltipMessage}
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.navToggleUnreads}
hideDescription={true}
isInsideTooltip={true}
/>
</Tooltip>
);
return (
<div className='SidebarFilters'>
<OverlayTrigger
delayShow={500}
<WithTooltip
id='channel-filter-tooltip'
title={unreadFilterEnabled ? messages.disableTooltip : messages.enableTooltip}
shortcut={shortcut}
placement={hasMultipleTeams ? 'top' : 'right'}
overlay={tooltip}
>
<a
href='#'
@@ -101,7 +97,7 @@ export class ChannelFilter extends React.PureComponent<Props> {
>
<i className='icon icon-filter-variant'/>
</a>
</OverlayTrigger>
</WithTooltip>
</div>
);
}

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

@@ -2,25 +2,26 @@
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React from 'react';
import React, {useCallback, useMemo} from 'react';
import {Draggable} from 'react-beautiful-dnd';
import {injectIntl} from 'react-intl';
import type {IntlShape} from 'react-intl';
import {defineMessages, useIntl} from 'react-intl';
import {Link} from 'react-router-dom';
import {mark, trackEvent} from 'actions/telemetry_actions.jsx';
import CopyUrlContextMenu from 'components/copy_url_context_menu';
import KeyboardShortcutSequence, {
KEYBOARD_SHORTCUTS,
} from 'components/keyboard_shortcuts/keyboard_shortcuts_sequence';
import OverlayTrigger from 'components/overlay_trigger';
import Tooltip from 'components/tooltip';
import TeamIcon from 'components/widgets/team_icon/team_icon';
import WithTooltip from 'components/with_tooltip';
import {ShortcutKeys} from 'components/with_tooltip/shortcut';
import Constants from 'utils/constants';
import {isDesktopApp} from 'utils/user_agent';
import {localizeMessage} from 'utils/utils';
const messages = defineMessages({
nameUndefined: {
id: 'team.button.name_undefined',
defaultMessage: 'This team does not have a name',
},
});
interface Props {
btnClass?: string;
@@ -36,7 +37,6 @@ interface Props {
placement?: 'left' | 'right' | 'top' | 'bottom';
teamIconUrl?: string | null;
switchTeam: (url: string) => void;
intl: IntlShape;
isDraggable?: boolean;
teamIndex?: number;
teamId?: string;
@@ -44,178 +44,201 @@ interface Props {
hasUrgent?: boolean;
}
class TeamButton extends React.PureComponent<Props> {
handleSwitch = (e: React.MouseEvent) => {
export default function TeamButton({
btnClass,
url,
displayName,
order,
unread,
mentions,
teamIconUrl,
isDraggable = false,
switchTeam,
teamIndex,
teamId,
tip,
...otherProps
}: Props) {
const {formatMessage} = useIntl();
const handleSwitch = useCallback((e: React.MouseEvent) => {
mark('TeamLink#click');
e.preventDefault();
this.props.switchTeam(this.props.url);
switchTeam(url);
setTimeout(() => {
trackEvent('ui', 'ui_team_sidebar_switch_team');
}, 0);
};
}, [switchTeam, url]);
render() {
const {teamIconUrl, displayName, btnClass, mentions, unread, isDraggable = false, teamIndex, teamId, order} = this.props;
const {formatMessage} = this.props.intl;
let teamClass: string = otherProps.active ? 'active' : '';
const isNotCreateTeamButton: boolean = !url.endsWith('create_team') && !url.endsWith('select_team');
let teamClass: string = this.props.active ? 'active' : '';
const isNotCreateTeamButton: boolean = !this.props.url.endsWith('create_team') && !this.props.url.endsWith('select_team');
let badge: JSX.Element | undefined;
let badge: JSX.Element | undefined;
let ariaLabel = formatMessage({
id: 'team.button.ariaLabel',
defaultMessage: '{teamName} team',
},
{
teamName: displayName,
});
let ariaLabel = formatMessage({
id: 'team.button.ariaLabel',
defaultMessage: '{teamName} team',
if (!teamClass) {
if (unread && !otherProps.isInProduct) {
teamClass = 'unread';
badge = (
<span className={'unread-badge'}/>
);
} else if (isNotCreateTeamButton) {
teamClass = '';
} else {
teamClass = 'special';
}
ariaLabel = formatMessage({
id: 'team.button.unread.ariaLabel',
defaultMessage: '{teamName} team unread',
},
{
teamName: displayName,
});
if (!teamClass) {
if (unread && !this.props.isInProduct) {
teamClass = 'unread';
badge = (
<span className={'unread-badge'}/>
);
} else if (isNotCreateTeamButton) {
teamClass = '';
} else {
teamClass = 'special';
}
if (mentions) {
ariaLabel = formatMessage({
id: 'team.button.unread.ariaLabel',
defaultMessage: '{teamName} team unread',
id: 'team.button.mentions.ariaLabel',
defaultMessage: '{teamName} team, {mentionCount} mentions',
},
{
teamName: displayName,
mentionCount: mentions,
});
if (mentions) {
ariaLabel = formatMessage({
id: 'team.button.mentions.ariaLabel',
defaultMessage: '{teamName} team, {mentionCount} mentions',
},
{
teamName: displayName,
mentionCount: mentions,
});
badge = (
<span className={classNames('badge badge-max-number pull-right small', {urgent: this.props.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
);
}
}
ariaLabel = ariaLabel.toLowerCase();
const content = (
<TeamIcon
className={teamClass}
withHover={true}
content={this.props.content || displayName || ''}
url={teamIconUrl}
/>
);
let toolTip = this.props.tip || localizeMessage('team.button.name_undefined', 'This team does not have a name');
let orderIndicator: JSX.Element | undefined;
if (typeof this.props.order !== 'undefined' && this.props.order < 10) {
toolTip = (
<>
{toolTip}
<KeyboardShortcutSequence
shortcut={KEYBOARD_SHORTCUTS.teamNavigation}
values={{order}}
hideDescription={true}
isInsideTooltip={true}
/>
</>
badge = (
<span className={classNames('badge badge-max-number pull-right small', {urgent: otherProps.hasUrgent})}>{mentions > 99 ? '99+' : mentions}</span>
);
}
}
if (this.props.showOrder) {
orderIndicator = (
<div className='order-indicator'>
{order}
ariaLabel = ariaLabel.toLowerCase();
const content = (
<TeamIcon
className={teamClass}
withHover={true}
content={otherProps.content || displayName || ''}
url={teamIconUrl}
/>
);
let orderIndicator: JSX.Element | undefined;
if (typeof order !== 'undefined' && order < 10) {
if (otherProps.showOrder) {
orderIndicator = (
<div className='order-indicator'>
{order}
</div>
);
}
}
const btn = (
<WithTeamTooltip
order={order}
tip={tip}
url={url}
>
<div className={'team-btn ' + btnClass}>
{!otherProps.isInProduct && badge}
{content}
</div>
</WithTeamTooltip>
);
let teamButton = (
<Link
id={`${url.slice(1)}TeamButton`}
aria-label={ariaLabel}
to={url}
onClick={handleSwitch}
>
{btn}
</Link>
);
if (isDesktopApp()) {
// if this is not a "special" team button, give it a context menu
if (isNotCreateTeamButton) {
teamButton = (
<CopyUrlContextMenu
link={url}
menuId={url}
>
{teamButton}
</CopyUrlContextMenu>
);
}
}
return isDraggable ? (
<Draggable
draggableId={teamId!}
index={teamIndex!}
>
{(provided, snapshot) => {
return (
<div
className='draggable-team-container'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className={classNames([`team-container ${teamClass}`, {isDragging: snapshot.isDragging}])}
>
{teamButton}
{orderIndicator}
</div>
</div>
);
}
}
const btn = (
<OverlayTrigger
delayShow={Constants.OVERLAY_TIME_DELAY}
placement={this.props.placement}
overlay={
<Tooltip id={`tooltip-${this.props.url}`}>
{toolTip}
</Tooltip>
}
>
<div className={'team-btn ' + btnClass}>
{!this.props.isInProduct && badge}
{content}
</div>
</OverlayTrigger>
);
let teamButton = (
<Link
id={`${this.props.url.slice(1)}TeamButton`}
aria-label={ariaLabel}
to={this.props.url}
onClick={this.handleSwitch}
>
{btn}
</Link>
);
if (isDesktopApp()) {
// if this is not a "special" team button, give it a context menu
if (isNotCreateTeamButton) {
teamButton = (
<CopyUrlContextMenu
link={this.props.url}
menuId={this.props.url}
>
{teamButton}
</CopyUrlContextMenu>
);
}
}
return isDraggable ? (
<Draggable
draggableId={teamId!}
index={teamIndex!}
>
{(provided, snapshot) => {
return (
<div
className='draggable-team-container'
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div
className={classNames([`team-container ${teamClass}`, {isDragging: snapshot.isDragging}])}
>
{teamButton}
{orderIndicator}
</div>
</div>
);
}}
</Draggable>
) : (
<div className={`team-container ${teamClass}`}>
{teamButton}
{orderIndicator}
</div>
);
}
}}
</Draggable>
) : (
<div className={`team-container ${teamClass}`}>
{teamButton}
{orderIndicator}
</div>
);
}
export default injectIntl(TeamButton);
function WithTeamTooltip({
order,
tip,
url,
children,
}: React.PropsWithChildren<Pick<Props, 'order' | 'tip' | 'url'>>) {
const intl = useIntl();
const shortcut = useMemo(() => {
if (!order || order >= 10) {
return undefined;
}
return {
default: [ShortcutKeys.ctrl, ShortcutKeys.alt, order.toString()],
mac: [ShortcutKeys.cmd, ShortcutKeys.option, order.toString()],
};
}, [order]);
return (
<WithTooltip
id={`tooltip-${url}`}
title={tip || intl.formatMessage(messages.nameUndefined)}
shortcut={shortcut}
placement='right'
>
{children}
</WithTooltip>
);
}

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

@@ -3,55 +3,97 @@
import React from 'react';
import type {ComponentProps} from 'react';
import type {MessageDescriptor} from 'react-intl';
import {type MessageDescriptor} from 'react-intl';
import RenderEmoji from 'components/emoji/render_emoji';
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
import Tooltip from 'components/tooltip';
import {getStringOrDescriptorComponent} from './utils';
import {TooltipShortcutSequence, type ShortcutDefinition} from './shortcut';
import {getAsFormattedMessage} from './utils';
type EmojiStyle = 'inline' | 'large' | undefined;
export type CommonTooltipProps = {
id: string;
title: string | MessageDescriptor;
hint?: string | MessageDescriptor;
shortcut?: string[];
title: string | MessageDescriptor | React.ReactElement;
hint?: string | MessageDescriptor | React.ReactElement;
shortcut?: ShortcutDefinition;
emoji?: string;
emojiStyle?: EmojiStyle;
}
export function createTooltip(commonTooltipProps: CommonTooltipProps) {
return (props: Omit<ComponentProps<typeof Tooltip>, 'children' | 'id'>) => {
const title = getStringOrDescriptorComponent(commonTooltipProps.title);
const hint = getStringOrDescriptorComponent(commonTooltipProps.hint);
const contents = [];
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle === 'large') {
contents.push(
<div
key='emoji'
className='tooltip-large-emoji'
>
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={48}
/>
</div>,
);
}
const title = getAsFormattedMessage(commonTooltipProps.title);
if (commonTooltipProps.emoji && commonTooltipProps.emojiStyle !== 'large') {
contents.push(
<div
key='title'
className={'tooltip-title'}
>
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={16}
/>
{title}
</div>,
);
} else {
contents.push(
<div
key='title'
className={'tooltip-title'}
>
{title}
</div>,
);
}
if (commonTooltipProps.shortcut) {
contents.push(
<div
key='shortcut'
className={'tooltip-shortcuts-container'}
>
<TooltipShortcutSequence shortcut={commonTooltipProps.shortcut}/>
</div>,
);
}
const hint = getAsFormattedMessage(commonTooltipProps.hint);
if (commonTooltipProps.hint) {
contents.push(
<div
key='hint'
className={'tooltip-hint'}
>
{hint}
</div>,
);
}
const emoji = commonTooltipProps.emoji && (
<RenderEmoji
emojiName={commonTooltipProps.emoji}
size={12}
/>
);
return (
<Tooltip
{...props}
id={commonTooltipProps.id}
>
<div className={'tooltip-title'}>
{emoji}
{title}
</div>
{commonTooltipProps.shortcut && (
<div className={'tooltip-shortcuts-container'}>
{commonTooltipProps.shortcut.map((v) => (
<ShortcutKey
key={v}
variant={ShortcutKeyVariant.Tooltip}
>
{v}
</ShortcutKey>
))}
</div>
)}
{commonTooltipProps.hint && (<div className={'tooltip-hint'}>{hint}</div>)}
{contents}
</Tooltip>
);
};

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

@@ -16,29 +16,34 @@ type OverlayTriggerProps = ComponentProps<typeof OverlayTrigger>;
type WithTooltipProps = {
children: OverlayTriggerProps['children'];
placement: OverlayTriggerProps['placement'];
onShow?: () => void;
} & CommonTooltipProps;
const WithTooltip = ({
id,
title,
emoji,
emojiStyle,
hint,
shortcut,
placement,
onShow,
children,
}: WithTooltipProps) => {
const ThisTooltip = useMemo(() => createTooltip({
id,
title,
emoji,
emojiStyle,
hint,
shortcut,
}), [id, title, emoji, hint, shortcut]);
}), [id, title, emoji, emojiStyle, hint, shortcut]);
return (
<OverlayTrigger
delay={Constants.OVERLAY_TIME_DELAY}
overlay={<ThisTooltip/>}
placement={placement}
onEnter={onShow}
>
{children}
</OverlayTrigger>

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

@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage, defineMessage} from 'react-intl';
import {ShortcutKey, ShortcutKeyVariant} from 'components/shortcut_key';
import {isMessageDescriptor} from 'utils/i18n';
import {isMac} from 'utils/user_agent';
export type ShortcutDefinition = {
default: ShortcutKeyDescriptor[];
mac?: ShortcutKeyDescriptor[];
}
export type ShortcutKeyDescriptor = string | MessageDescriptor;
export const ShortcutKeys = {
alt: defineMessage({
id: 'shortcuts.generic.alt',
defaultMessage: 'Alt',
}),
cmd: '⌘',
ctrl: defineMessage({
id: 'shortcuts.generic.ctrl',
defaultMessage: 'Ctrl',
}),
option: '⌥',
shift: defineMessage({
id: 'shortcuts.generic.shift',
defaultMessage: 'Shift',
}),
};
type Props = {
shortcut: ShortcutDefinition;
}
export function TooltipShortcutSequence(props: Props) {
let shortcut = props.shortcut.default;
if (props.shortcut.mac && isMac()) {
shortcut = props.shortcut.mac;
}
return (
<>
{shortcut.map((v) => {
let key;
let content;
if (isMessageDescriptor(v)) {
key = v.id;
content = <FormattedMessage {...v}/>;
} else {
key = v;
content = v;
}
return (
<ShortcutKey
key={key}
variant={ShortcutKeyVariant.Tooltip}
>
{content}
</ShortcutKey>
);
})}
</>
);
}

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

@@ -6,19 +6,21 @@ import type {ComponentProps} from 'react';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage} from 'react-intl';
export function getStringOrDescriptorComponent(v: string | MessageDescriptor | undefined, values?: ComponentProps<typeof FormattedMessage>['values']) {
import {isMessageDescriptor} from 'utils/i18n';
export function getAsFormattedMessage(v: string | MessageDescriptor | React.ReactElement | undefined, values?: ComponentProps<typeof FormattedMessage>['values']) {
if (!v) {
return undefined;
}
if (typeof v === 'string') {
return v;
if (isMessageDescriptor(v)) {
return (
<FormattedMessage
{...v}
values={values}
/>
);
}
return (
<FormattedMessage
{...v}
values={values}
/>
);
return v;
}

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

@@ -4953,6 +4953,9 @@
"shortcuts.files.header": "Files",
"shortcuts.files.upload": "Upload files:\tCtrl|U",
"shortcuts.files.upload.mac": "Upload files:\t⌘|U",
"shortcuts.generic.alt": "Alt",
"shortcuts.generic.ctrl": "Ctrl",
"shortcuts.generic.shift": "Shift",
"shortcuts.header": "Keyboard shortcuts\tCtrl|/",
"shortcuts.header.mac": "Keyboard shortcuts\t⌘|/",
"shortcuts.info": "Begin a message with / for a list of all the available slash commands.",
@@ -5274,8 +5277,6 @@
"team.button.ariaLabel": "{teamName} team",
"team.button.mentions.ariaLabel": "{teamName} team, {mentionCount} mentions",
"team.button.name_undefined": "This team does not have a name",
"team.button.tooltip": "Ctrl|Alt|{order}",
"team.button.tooltip.mac": "⌘|⌥|{order}",
"team.button.unread.ariaLabel": "{teamName} team unread",
"terms_of_service.agreeButton": "I Agree",
"terms_of_service.api_error": "Unable to complete the request. If this issue persists, contact your System Administrator.",

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

@@ -29,6 +29,10 @@ export function getEmojiImageUrl(emoji: Emoji): string {
return Client4.getEmojiRoute(emoji.id) + '/image';
}
export function getEmojiName(emoji: Emoji): string {
return isSystemEmoji(emoji) ? emoji.short_name : emoji.name;
}
export function parseEmojiNamesFromText(text: string): string[] {
if (!text.includes(':')) {
return [];

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

@@ -50,9 +50,17 @@
color: rgba(255, 255, 255, 0.75);
}
.tooltip-large-emoji {
margin-bottom: 2px;
}
.tooltip-title {
font-weight: 600;
line-height: 15px;
.emoticon {
margin-right: 6px;
}
}
.tooltip-hint {
@@ -69,7 +77,6 @@
}
.emoticon {
margin-right: 6px;
vertical-align: center;
}
}

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

@@ -1,6 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {MessageDescriptor} from 'react-intl';
export function isMessageDescriptor(descriptor: unknown): descriptor is MessageDescriptor {
return Boolean(descriptor && (descriptor as MessageDescriptor).id);
}
export function getMonthLong(locale: string): 'short' | 'long' {
if (locale === 'ko') {
// Long and short are equivalent in Korean except long has a bug on IE11/Windows 7