LP-5613 #2

Open
dmitrii.pichenikin wants to merge 37 commits from LP-5613 into dev
13 changed files with 226 additions and 272 deletions
Showing only changes of commit ec89c1f115 - Show all commits

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {Badge} from '../../types/badges'; import {Badge} from '../../types/badges';
import RenderEmoji from '../utils/emoji'; import RenderEmoji from '../emoji/emoji';
import {IMAGE_TYPE_ABSOLUTE_URL, IMAGE_TYPE_EMOJI} from '../../constants'; import {IMAGE_TYPE_ABSOLUTE_URL, IMAGE_TYPE_EMOJI} from '../../constants';
type Props = { type Props = {

View File

@ -6,6 +6,8 @@ import {FormattedMessage, useIntl} from 'react-intl';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/common';
import {GlobalState} from 'mattermost-redux/types/store'; import {GlobalState} from 'mattermost-redux/types/store';
import RenderEmoji from 'components/emoji/emoji';
import {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors'; import {isCreateBadgeModalVisible, getEditBadgeModalData} from 'selectors';
import {closeCreateBadgeModal, closeEditBadgeModal, setRHSView} from 'actions/actions'; import {closeCreateBadgeModal, closeEditBadgeModal, setRHSView} from 'actions/actions';
import {RHS_STATE_ALL} from '../../constants'; import {RHS_STATE_ALL} from '../../constants';
@ -14,14 +16,14 @@ import Client from 'client/api';
import {getServerErrorId} from 'utils/helpers'; import {getServerErrorId} from 'utils/helpers';
import CloseIcon from 'components/icons/close_icon'; import CloseIcon from 'components/icons/close_icon';
import EmojiIcon from 'components/icons/emoji_icon'; import EmojiIcon from 'components/icons/emoji_icon';
import RenderEmoji from 'components/utils/emoji';
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
import EmojiPickerOverlay from './emoji_picker'; import EmojiPickerOverlay from './emoji_picker';
import InlineTypeForm from './inline_type_form'; import InlineTypeForm from './inline_type_form';
import TypeSelect from './type_select'; import TypeSelect from './type_select';
import './badge_modal.scss'; import './badge_modal.scss';
import ConfirmDialog from 'components/confirm_dialog/confirm_dialog';
const NEW_TYPE_VALUE = '__new__'; const NEW_TYPE_VALUE = '__new__';

View File

@ -13,7 +13,7 @@ import {AllBadgesBadge} from 'types/badges';
import Client from 'client/api'; import Client from 'client/api';
import {getServerErrorId, getUserDisplayName} from 'utils/helpers'; import {getServerErrorId, getUserDisplayName} from 'utils/helpers';
import CloseIcon from 'components/icons/close_icon'; import CloseIcon from 'components/icons/close_icon';
import RenderEmoji from 'components/utils/emoji'; import RenderEmoji from 'components/emoji/emoji';
type GrantFormData = { type GrantFormData = {
badgeId: string; badgeId: string;

View File

@ -72,7 +72,7 @@ const AllBadgesRow: React.FC<Props> = ({badge, onClick}: Props) => {
/> />
</span> </span>
{' '} {' '}
{badge.description ? markdown(badge.description) : ''} {badge.description ? markdown(badge.description) : '-'}
</div> </div>
<div className='badge-meta'> <div className='badge-meta'>
<FormattedMessage <FormattedMessage

View File

@ -9,7 +9,7 @@ import Client from '../../client/api';
import {RHSState} from '../../types/general'; import {RHSState} from '../../types/general';
import {RHS_STATE_MY, RHS_STATE_OTHER, RHS_STATE_TYPES} from '../../constants'; import {RHS_STATE_MY, RHS_STATE_OTHER, RHS_STATE_TYPES} from '../../constants';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../badge_image/badge_image';
import {markdown} from 'utils/markdown'; import {markdown} from 'utils/markdown';

View File

@ -5,7 +5,7 @@ import {FormattedMessage, useIntl} from 'react-intl';
import Client4 from 'mattermost-redux/client/client4'; import Client4 from 'mattermost-redux/client/client4';
import {UserBadge} from '../../types/badges'; import {UserBadge} from '../../types/badges';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../badge_image/badge_image';
import {markdown} from 'utils/markdown'; import {markdown} from 'utils/markdown';
import Client from '../../client/api'; import Client from '../../client/api';
import ConfirmDialog from '../confirm_dialog/confirm_dialog'; import ConfirmDialog from '../confirm_dialog/confirm_dialog';

View File

@ -1,256 +1,169 @@
import {UserProfile} from 'mattermost-redux/types/users'; import {UserProfile} from 'mattermost-redux/types/users';
import React from 'react'; import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, useIntl} from 'react-intl';
import {GlobalState} from 'mattermost-redux/types/store'; import {useDispatch, useSelector} from 'react-redux';
import {systemEmojis} from 'mattermost-redux/actions/emojis'; import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {BadgeID, UserBadge} from 'types/badges'; import {systemEmojis, getCustomEmojisByName} from 'mattermost-redux/actions/emojis';
import {UserBadge} from 'types/badges';
import Client from 'client/api'; import Client from 'client/api';
import BadgeImage from '../utils/badge_image'; import BadgeImage from '../badge_image/badge_image';
import TooltipWrapper from '../utils/tooltip_wrapper';
import {RHSState} from 'types/general';
import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants'; import {IMAGE_TYPE_EMOJI, RHS_STATE_DETAIL, RHS_STATE_MY, RHS_STATE_OTHER} from '../../constants';
import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions';
import {getShowRHS} from 'selectors';
import {groupBadges} from 'components/utils/badge_list_utils';
import BadgeTooltip from './badge_tooltip';
import TooltipWrapper from './tooltip_wrapper';
import './badge_list.scss'; import './badge_list.scss';
type Props = { type Props = {
intl: IntlShape;
debug: GlobalState;
user: UserProfile; user: UserProfile;
currentUserID: string;
openRHS: (() => void) | null;
hide: () => void; hide: () => void;
status?: string;
actions: {
setRHSView: (view: RHSState) => Promise<void>;
setRHSBadge: (id: BadgeID | null) => Promise<void>;
setRHSUser: (id: string | null) => Promise<void>;
openGrant: (user?: string, badge?: string) => Promise<void>;
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
};
}
type BadgeGroup = {
badge: UserBadge;
count: number;
}
type State = {
badges?: UserBadge[];
loaded?: boolean;
} }
const MAX_BADGES = 6; const MAX_BADGES = 6;
const BADGE_SIZE = 24; const BADGE_SIZE = 24;
class BadgeList extends React.PureComponent<Props, State> { const BadgeList: React.FC<Props> = ({user, hide}) => {
constructor(props: Props) { const intl = useIntl();
super(props); const dispatch = useDispatch();
const currentUserID = useSelector(getCurrentUserId);
const openRHS = useSelector(getShowRHS);
const [badges, setBadges] = useState<UserBadge[]>();
const [loaded, setLoaded] = useState(false);
this.state = {}; useEffect(() => {
}
componentDidMount() {
const c = new Client(); const c = new Client();
c.getUserBadges(this.props.user.id).then((badges) => { c.getUserBadges(user.id).then((result) => {
this.setState({badges, loaded: true}); setBadges(result);
Review

client

client
setLoaded(true);
}); });
} }, [user.id]);
componentDidUpdate(prevProps: Props, prevState: State) { const groups = useMemo(
if (this.state.badges !== prevState.badges && this.state.badges) { () => (badges ? groupBadges(badges) : []),
const groups = this.groupBadges(this.state.badges); [badges],
const toShow = Math.min(groups.length, MAX_BADGES); );
const names: string[] = [];
for (let i = 0; i < toShow; i++) {
const {badge} = groups[i];
if (badge.image_type === IMAGE_TYPE_EMOJI) {
names.push(badge.image);
}
}
const toLoad = names.filter((v) => !systemEmojis.has(v));
this.props.actions.getCustomEmojisByName(toLoad);
}
}
groupBadges = (badges: UserBadge[]): BadgeGroup[] => { useEffect(() => {
const map = new Map<BadgeID, BadgeGroup>(); if (!badges) {
for (const badge of badges) {
const existing = map.get(badge.id);
if (existing) {
existing.count++;
} else {
map.set(badge.id, {badge, count: 1});
}
}
return Array.from(map.values());
}
onMoreClick = () => {
if (!this.props.openRHS) {
return; return;
} }
const toShow = groups.slice(0, MAX_BADGES);
const names = toShow.
filter(({badge}) => badge.image_type === IMAGE_TYPE_EMOJI).
map(({badge}) => badge.image).
Review

как будто filter.map.filter можно заменить на reduce

как будто filter.map.filter можно заменить на reduce
filter((v) => !systemEmojis.has(v));
if (names.length > 0) {
dispatch(getCustomEmojisByName(names));
}
}, [badges, groups, dispatch]);
if (this.props.currentUserID === this.props.user.id) { const handleMoreClick = useCallback(() => {
this.props.actions.setRHSView(RHS_STATE_MY); if (!openRHS) {
this.props.openRHS();
this.props.hide();
return; return;
} }
if (currentUserID === user.id) {
dispatch(setRHSView(RHS_STATE_MY));
} else {
dispatch(setRHSUser(user.id));
dispatch(setRHSView(RHS_STATE_OTHER));
}
openRHS();
hide();
}, [openRHS, currentUserID, user.id, dispatch, hide]);
this.props.actions.setRHSUser(this.props.user.id); const handleBadgeClick = useCallback((badge: UserBadge) => {
this.props.actions.setRHSView(RHS_STATE_OTHER); if (!openRHS) {
this.props.openRHS();
this.props.hide();
}
onBadgeClick = (badge: UserBadge) => {
if (!this.props.openRHS) {
return; return;
} }
dispatch(setRHSBadge(badge.id));
dispatch(setRHSView(RHS_STATE_DETAIL));
openRHS();
hide();
}, [openRHS, dispatch, hide]);
this.props.actions.setRHSBadge(badge.id); const handleGrantClick = useCallback(() => {
this.props.actions.setRHSView(RHS_STATE_DETAIL); dispatch(openGrant(user.username));
this.props.openRHS(); hide();
this.props.hide(); }, [dispatch, user.username, hide]);
}
onGrantClick = () => { const visibleGroups = groups.slice(0, MAX_BADGES);
this.props.actions.openGrant(this.props.user.username); const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30;
this.props.hide();
}
render() { return (
const {intl} = this.props; <div id='badgePlugin'>
const groups = this.state.badges ? this.groupBadges(this.state.badges) : []; <div><b>
const nGroups = groups.length; <FormattedMessage
const toShow = Math.min(nGroups, MAX_BADGES); id='badges.popover.title'
defaultMessage='Достижения'
const nameLabel = intl.formatMessage( />
{id: 'badges.label.name', defaultMessage: 'Название:'}, </b></div>
); <div id='contentContainer'>
Review

немного поплыли стили

немного поплыли стили
const descLabel = intl.formatMessage( {visibleGroups.map(({badge, count}) => (
{id: 'badges.label.description', defaultMessage: 'Описание:'}, <TooltipWrapper
); key={badge.id}
tooltipContent={
const content: React.ReactNode[] = []; <BadgeTooltip
for (let i = 0; i < toShow; i++) {
const {badge, count} = groups[i];
let tooltipLines: string;
if (count > 1) {
const countLabel = intl.formatMessage(
{id: 'badges.label.count', defaultMessage: 'Количество: {count}'},
{count},
);
tooltipLines = [
nameLabel + ' ' + badge.name,
descLabel + ' ' + badge.description,
countLabel,
].join('\n');
} else {
const time = new Date(badge.time);
let reason: string | null = null;
if (badge.reason) {
reason = intl.formatMessage(
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
{reason: badge.reason},
);
}
const grantedBy = intl.formatMessage(
{id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'},
{username: badge.granted_by_name},
);
const grantedAt = intl.formatMessage(
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
);
tooltipLines = [
nameLabel + ' ' + badge.name,
descLabel + ' ' + badge.description,
reason,
grantedBy,
grantedAt,
].filter(Boolean).join('\n');
}
const badgeComponent = (
<TooltipWrapper tooltipContent={tooltipLines}>
<a onClick={() => this.onBadgeClick(badge)}>
<span className='badge-stacked'>
<BadgeImage
badge={badge} badge={badge}
size={BADGE_SIZE} count={count}
/> />
{count > 1 && ( }
<span className='badge-stack-count'>
{'×'}{count}
</span>
)}
</span>
</a>
</TooltipWrapper>
);
content.push(badgeComponent);
}
let andMore: React.ReactNode = null;
if (nGroups > MAX_BADGES) {
const andMoreText = intl.formatMessage(
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
{count: nGroups - MAX_BADGES},
);
andMore = (
<TooltipWrapper tooltipContent={andMoreText}>
<button
id='showMoreButton'
onClick={this.onMoreClick}
> >
<span className={'fa fa-angle-right'}/> <a onClick={() => handleBadgeClick(badge)}>
</button> <span className='badge-stacked'>
</TooltipWrapper> <BadgeImage
); badge={badge}
} size={BADGE_SIZE}
const maxWidth = (MAX_BADGES * BADGE_SIZE) + 30; />
let loading: React.ReactNode = null; {count > 1 && (
if (!this.state.loaded) { <span className='badge-stack-count'>
loading = ( {'×'}{count}
</span>
// Reserve enough height one row of badges and the "and more" button )}
</span>
</a>
</TooltipWrapper>
))}
{groups.length > MAX_BADGES && (
<TooltipWrapper
tooltipContent={intl.formatMessage(
{id: 'badges.and_more', defaultMessage: 'и ещё {count}. Нажмите, чтобы увидеть все.'},
{count: groups.length - MAX_BADGES},
)}
>
<button
id='showMoreButton'
onClick={handleMoreClick}
>
<span className={'fa fa-angle-right'}/>
</button>
</TooltipWrapper>
)}
</div>
{!loaded && (
<div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}> <div style={{height: BADGE_SIZE, minWidth: 66, maxWidth}}>
<div className='spinner'/> <div className='spinner'/>
</div> </div>
); )}
} <button
return ( id='grantBadgeButton'
<div id='badgePlugin'> onClick={handleGrantClick}
<div><b> >
<FormattedMessage <span className={'fa fa-plus-circle'}/>
id='badges.popover.title' <FormattedMessage
defaultMessage='Достижения' id='badges.grant_badge'
/> defaultMessage='Выдать достижение'
</b></div> />
<div id='contentContainer' > </button>
{content} <hr className='divider divider--expanded'/>
{andMore} </div>
</div> );
{loading} };
<button
id='grantBadgeButton'
onClick={this.onGrantClick}
>
<span className={'fa fa-plus-circle'}/>
<FormattedMessage
id='badges.grant_badge'
defaultMessage='Выдать достижение'
/>
</button>
<hr className='divider divider--expanded'/>
</div>
);
}
}
export default injectIntl(BadgeList); export default BadgeList;

View File

@ -0,0 +1,62 @@
import React from 'react';
import {useIntl} from 'react-intl';
import {UserBadge} from 'types/badges';
import {truncateText} from 'components/utils/badge_list_utils';
type Props = {
badge: UserBadge;
count: number;
}
const BadgeTooltip: React.FC<Props> = ({badge, count}) => {
const intl = useIntl();
const desc = badge.description ? truncateText(badge.description) : '—';
const nameRow = intl.formatMessage(
{id: 'badges.label.name', defaultMessage: 'Название:'},
) + ' ' + badge.name;
const descRow = intl.formatMessage(
{id: 'badges.label.description', defaultMessage: 'Описание:'},
) + ' ' + desc;
if (count > 1) {
const countRow = intl.formatMessage(
{id: 'badges.label.count', defaultMessage: 'Количество: {count}'},
{count},
);
return <>{nameRow}{'\n'}{descRow}{'\n'}{countRow}</>;
}
const time = new Date(badge.time);
const grantedBy = intl.formatMessage(
{id: 'badges.label.granted_by', defaultMessage: 'Выдал: {username}'},
{username: badge.granted_by_name},
);
const grantedAt = intl.formatMessage(
{id: 'badges.label.granted_at', defaultMessage: 'Выдан: {date}'},
{date: intl.formatDate(time, {day: '2-digit', month: '2-digit', year: 'numeric'})},
);
return (
<>
{nameRow}{'\n'}
Review

такой конструкции я еще не видел, жестка

такой конструкции я еще не видел, жестка
Review

Что тут происходит?) Согласен с Владмиром

Что тут происходит?) Согласен с Владмиром
{descRow}{'\n'}
{badge.reason && (
<>
{intl.formatMessage(
{id: 'badges.label.reason', defaultMessage: 'Причина: {reason}'},
{reason: badge.reason},
)}{'\n'}
</>
)}
{grantedBy}{'\n'}
{grantedAt}
</>
);
};
export default BadgeTooltip;

View File

@ -1,49 +1 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. export {default} from './badge_list';
// See License for license information.
import {connect} from 'react-redux';
import {ActionCreatorsMapObject, bindActionCreators, Dispatch} from 'redux';
import {GlobalState} from 'mattermost-redux/types/store';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/common';
import {getCustomEmojisByName} from 'mattermost-redux/actions/emojis';
import {setRHSView, setRHSBadge, setRHSUser, openGrant} from '../../actions/actions';
import {getShowRHS} from 'selectors';
import {RHSState} from 'types/general';
import {BadgeID} from 'types/badges';
import BadgeList from './badge_list';
function mapStateToProps(state: GlobalState) {
return {
openRHS: getShowRHS(state),
currentUserID: getCurrentUserId(state),
debug: state,
};
}
type Actions = {
setRHSView: (view: RHSState) => Promise<void>;
setRHSBadge: (id: BadgeID | null) => Promise<void>;
setRHSUser: (id: string | null) => Promise<void>;
openGrant: (user?: string, badge?: string) => Promise<void>;
getCustomEmojisByName: (names: string[]) => Promise<unknown>;
}
function mapDispatchToProps(dispatch: Dispatch) {
return {
actions: bindActionCreators<ActionCreatorsMapObject, Actions>({
setRHSView,
setRHSBadge,
setRHSUser,
openGrant,
getCustomEmojisByName,
}, dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(BadgeList);

View File

@ -0,0 +1,25 @@
import {BadgeID, UserBadge} from 'types/badges';
export type BadgeGroup = {
badge: UserBadge;
count: number;
}
const MAX_DESC_LENGTH = 40;
export function groupBadges(badges: UserBadge[]): BadgeGroup[] {
const map = new Map<BadgeID, BadgeGroup>();
for (const badge of badges) {
const existing = map.get(badge.id);
if (existing) {
existing.count++;
} else {
map.set(badge.id, {badge, count: 1});
}
}
return Array.from(map.values());
}
export function truncateText(text: string): string {
return text.length > MAX_DESC_LENGTH ? text.slice(0, MAX_DESC_LENGTH) + '...' : text;
}