Commit 80ebdad1 authored by Joram Wilander's avatar Joram Wilander Committed by Harrison Healey
Browse files

ABC-96 Add paging and server-side search to custom emoji management (#643)

* Add paging and server-side search to custom emoji management

* Remove unneeded action and update action usage slightly

* Fix import order

* Fixes for merge and paging

* Style fixes

* Temporary redux commit

* Updating UI for emoji list buttons

* Updates per feedback

* Fix style issue

* Point to latest mattermost-redux
parent 3fdf2c4b
......@@ -2,41 +2,14 @@
// See License.txt for license information.
import * as EmojiActions from 'mattermost-redux/actions/emojis';
import {getProfilesByIds} from 'mattermost-redux/actions/users';
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import store from 'stores/redux_store.jsx';
import UserStore from 'stores/user_store.jsx';
import {ActionTypes} from 'utils/constants.jsx';
const dispatch = store.dispatch;
const getState = store.getState;
export async function loadEmoji(getProfiles = true) {
const {data} = await EmojiActions.getAllCustomEmojis()(dispatch, getState);
if (data && getProfiles) {
loadProfilesForEmoji(data);
}
}
function loadProfilesForEmoji(emojiList) {
const profilesToLoad = {};
for (let i = 0; i < emojiList.length; i++) {
const emoji = emojiList[i];
if (!UserStore.hasProfile(emoji.creator_id)) {
profilesToLoad[emoji.creator_id] = true;
}
}
const list = Object.keys(profilesToLoad);
if (list.length === 0) {
return;
}
getProfilesByIds(list)(dispatch, getState);
}
export async function addEmoji(emoji, image, success, error) {
const {data, error: err} = await EmojiActions.createCustomEmoji(emoji, image)(dispatch, getState);
if (data && success) {
......
......@@ -9,8 +9,8 @@ import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import AnnouncementBar from 'components/announcement_bar';
import Integrations from 'components/integrations/components/integrations.jsx';
import Emoji from 'components/emoji/components/emoji_list';
import AddEmoji from 'components/emoji/components/add_emoji';
import Emoji from 'components/emoji';
import AddEmoji from 'components/emoji/add_emoji';
import InstalledIncomingWebhooks from 'components/integrations/components/installed_incoming_webhooks';
import AddIncomingWehook from 'components/integrations/components/add_incoming_webhook';
import EditIncomingWebhook from 'components/integrations/components/edit_incoming_webhook';
......
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import PropTypes from 'prop-types';
import {FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';
import * as EmojiActions from 'actions/emoji_actions.jsx';
import EmojiStore from 'stores/emoji_store.jsx';
import UserStore from 'stores/user_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {Emoji} from 'mattermost-redux/constants';
import {localizeMessage} from 'utils/utils.jsx';
import LoadingScreen from 'components/loading_screen.jsx';
import SaveButton from 'components/save_button.jsx';
import EmojiListItem from 'components/emoji/emoji_list_item';
import EmojiListItem from './emoji_list_item.jsx';
const EMOJI_PER_PAGE = 50;
const EMOJI_SEARCH_DELAY_MILLISECONDS = 200;
export default class EmojiList extends React.Component {
static get propTypes() {
return {
team: PropTypes.object,
user: PropTypes.object
};
static propTypes = {
/**
* Custom emojis on the system.
*/
emojiIds: PropTypes.arrayOf(PropTypes.string).isRequired,
actions: PropTypes.shape({
/**
* Get pages of custom emojis.
*/
getCustomEmojis: PropTypes.func.isRequired,
/**
* Search custom emojis.
*/
searchCustomEmojis: PropTypes.func.isRequired
}).isRequired
}
constructor(props) {
super(props);
this.updateTitle = this.updateTitle.bind(this);
this.handleEmojiChange = this.handleEmojiChange.bind(this);
this.handleUserChange = this.handleUserChange.bind(this);
this.deleteEmoji = this.deleteEmoji.bind(this);
this.updateFilter = this.updateFilter.bind(this);
this.searchTimeout = null;
this.state = {
emojis: EmojiStore.getCustomEmojiMap(),
loading: true,
filter: '',
users: UserStore.getProfiles()
page: 0,
nextLoading: false,
searchEmojis: null,
missingPages: true
};
}
componentDidMount() {
EmojiStore.addChangeListener(this.handleEmojiChange);
UserStore.addChangeListener(this.handleUserChange);
this.props.actions.getCustomEmojis(0, EMOJI_PER_PAGE + 1, Emoji.SORT_BY_NAME, true).then(({data}) => {
this.setState({loading: false});
if (data && data.length < EMOJI_PER_PAGE) {
this.setState({missingPages: false});
}
});
}
if (window.mm_config.EnableCustomEmoji === 'true') {
EmojiActions.loadEmoji().then(() => this.setState({loading: false}));
nextPage = (e) => {
if (e) {
e.preventDefault();
}
this.updateTitle();
const next = this.state.page + 1;
this.setState({nextLoading: true});
this.props.actions.getCustomEmojis(next, EMOJI_PER_PAGE, Emoji.SORT_BY_NAME, true).then(({data}) => {
this.setState({page: next, nextLoading: false});
if (data && data.length < EMOJI_PER_PAGE) {
this.setState({missingPages: false});
}
});
}
updateTitle() {
let currentSiteName = '';
if (global.window.mm_config.SiteName != null) {
currentSiteName = global.window.mm_config.SiteName;
previousPage = (e) => {
if (e) {
e.preventDefault();
}
document.title = Utils.localizeMessage('custom_emoji.header', 'Custom Emoji') + ' - ' + this.props.team.display_name + ' ' + currentSiteName;
this.setState({page: this.state.page - 1, nextLoading: false});
}
componentWillUnmount() {
EmojiStore.removeChangeListener(this.handleEmojiChange);
UserStore.removeChangeListener(this.handleUserChange);
}
onSearchChange = (e) => {
if (!e || !e.target) {
return;
}
handleEmojiChange() {
this.setState({
emojis: EmojiStore.getCustomEmojiMap()
});
}
const term = e.target.value || '';
handleUserChange() {
this.setState({users: UserStore.getProfiles()});
}
clearTimeout(this.searchTimeout);
updateFilter(e) {
this.setState({
filter: e.target.value
});
this.searchTimeout = setTimeout(async () => {
if (term.trim() === '') {
this.setState({searchEmojis: null, page: 0});
return;
}
this.setState({loading: true});
const {data} = await this.props.actions.searchCustomEmojis(term);
if (data) {
this.setState({searchEmojis: data.map((em) => em.id), loading: false});
} else {
this.setState({searchEmojis: [], loading: false});
}
}, EMOJI_SEARCH_DELAY_MILLISECONDS);
}
deleteEmoji(emoji) {
EmojiActions.deleteEmoji(emoji.id);
deleteFromSearch = (emojiId) => {
if (!this.state.searchEmojis) {
return;
}
const index = this.state.searchEmojis.indexOf(emojiId);
if (index < 0) {
return;
}
const newSearchEmojis = [...this.state.searchEmojis];
newSearchEmojis.splice(index, 1);
this.setState({searchEmojis: newSearchEmojis});
}
render() {
const filter = this.state.filter.toLowerCase();
const isSystemAdmin = Utils.isSystemAdmin(this.props.user.roles);
const searchEmojis = this.state.searchEmojis;
const emojis = [];
let nextButton;
let previousButton;
if (this.state.loading) {
emojis.push(
<tr
......@@ -101,7 +144,7 @@ export default class EmojiList extends React.Component {
</td>
</tr>
);
} else if (this.state.emojis.size === 0) {
} else if (this.props.emojiIds.length === 0 || (searchEmojis && searchEmojis.length === 0)) {
emojis.push(
<tr
key='empty'
......@@ -115,58 +158,80 @@ export default class EmojiList extends React.Component {
</td>
</tr>
);
} else if (searchEmojis) {
searchEmojis.forEach((emojiId) => {
emojis.push(
<EmojiListItem
key={'emoji_search_item' + emojiId}
emojiId={emojiId}
onDelete={this.deleteFromSearch}
/>
);
});
} else {
for (const [, emoji] of this.state.emojis) {
let onDelete = null;
if (isSystemAdmin || this.props.user.id === emoji.creator_id) {
onDelete = this.deleteEmoji;
}
const pageStart = this.state.page * EMOJI_PER_PAGE;
const pageEnd = pageStart + EMOJI_PER_PAGE;
const emojisToDisplay = this.props.emojiIds.slice(pageStart, pageEnd);
emojisToDisplay.forEach((emojiId) => {
emojis.push(
<EmojiListItem
key={emoji.id}
emoji={emoji}
onDelete={onDelete}
filter={filter}
creator={this.state.users[emoji.creator_id] || {}}
key={'emoji_list_item' + emojiId}
emojiId={emojiId}
/>
);
}
}
});
return (
<div className='backstage-content emoji-list'>
<div className='backstage-header'>
<h1>
if (this.state.missingPages) {
const buttonContents = (
<span>
<FormattedMessage
id='emoji_list.header'
defaultMessage='Custom Emoji'
id='filtered_user_list.next'
defaultMessage='Next'
/>
</h1>
<Link
className='add-link'
to={'/' + this.props.team.name + '/emoji/add'}
<i className='fa fa-chevron-right margin-left'/>
</span>
);
nextButton = (
<SaveButton
btnClass='btn-link'
extraClasses='pull-right'
onClick={this.nextPage}
saving={this.state.nextLoading}
disabled={this.state.nextLoading}
defaultMessage={buttonContents}
savingMessage={buttonContents}
/>
);
}
if (this.state.page > 0) {
previousButton = (
<button
className='btn btn-link'
onClick={this.previousPage}
>
<button
type='button'
className='btn btn-primary'
>
<FormattedMessage
id='emoji_list.add'
defaultMessage='Add Custom Emoji'
/>
</button>
</Link>
</div>
<i className='fa fa-chevron-left margin-right'/>
<FormattedMessage
id='filtered_user_list.prev'
defaultMessage='Previous'
/>
</button>
);
}
}
return (
<div>
<div className='backstage-filters'>
<div className='backstage-filter__search'>
<i className='fa fa-search'/>
<input
type='search'
className='form-control'
placeholder={Utils.localizeMessage('emoji_list.search', 'Search Custom Emoji')}
value={this.state.filter}
onChange={this.updateFilter}
placeholder={localizeMessage('emoji_list.search', 'Search Custom Emoji')}
onChange={this.onSearchChange}
style={style.search}
/>
</div>
......@@ -220,6 +285,10 @@ export default class EmojiList extends React.Component {
</tbody>
</table>
</div>
<div className='filter-controls padding-top x2'>
{previousButton}
{nextButton}
</div>
</div>
);
}
......
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getCustomEmojiIdsSortedByName} from 'mattermost-redux/selectors/entities/emojis';
import {getCustomEmojis, searchCustomEmojis} from 'mattermost-redux/actions/emojis';
import EmojiList from './emoji_list.jsx';
function mapStateToProps(state) {
return {
emojiIds: getCustomEmojiIdsSortedByName(state) || []
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getCustomEmojis,
searchCustomEmojis
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(EmojiList);
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import EmojiStore from 'stores/emoji_store.jsx';
import * as Utils from 'utils/utils.jsx';
import {Client4} from 'mattermost-redux/client';
import DeleteEmoji from './delete_emoji_modal.jsx';
import DeleteEmoji from 'components/emoji/delete_emoji_modal.jsx';
export default class EmojiListItem extends React.Component {
static get propTypes() {
return {
emoji: PropTypes.object.isRequired,
onDelete: PropTypes.func.isRequired,
filter: PropTypes.string,
creator: PropTypes.object.isRequired
};
static propTypes = {
/*
* Emoji to display.
*/
emoji: PropTypes.object.isRequired,
/*
* Logged in user's ID.
*/
currentUserId: PropTypes.string.isRequired,
/*
* Emoji creator name to display.
*/
creatorDisplayName: PropTypes.string.isRequired,
/*
* Emoji creator username to display if different from creatorDisplayName.
*/
creatorUsername: PropTypes.string,
/*
* Set if logged in user is system admin.
*/
isSystemAdmin: PropTypes.bool,
/*
* Function to call when emoji is deleted.
*/
onDelete: PropTypes.func,
actions: PropTypes.shape({
/**
* Delete a custom emoji.
*/
deleteCustomEmoji: PropTypes.func.isRequired
}).isRequired
}
constructor(props) {
super(props);
this.handleDelete = this.handleDelete.bind(this);
static defaultProps = {
emoji: {},
currentUserId: '',
creatorDisplayName: '',
isSystemAdmin: false
}
handleDelete() {
this.props.onDelete(this.props.emoji);
}
matchesFilter(emoji, creator, filter) {
if (!filter) {
return true;
}
if (emoji.name.toLowerCase().indexOf(filter) !== -1) {
return true;
}
if (creator && creator.username && creator.username.toLowerCase().indexOf(filter) !== -1) {
return true;
handleDelete = () => {
if (this.props.onDelete) {
this.props.onDelete(this.props.emoji.id);
}
return false;
this.props.actions.deleteCustomEmoji(this.props.emoji.id);
}
render() {
const emoji = this.props.emoji;
const creator = this.props.creator;
const filter = this.props.filter ? this.props.filter.toLowerCase() : '';
const creatorUsername = this.props.creatorUsername;
let creatorDisplayName = this.props.creatorDisplayName;
if (!this.matchesFilter(emoji, creator, filter)) {
return null;
}
let creatorName;
if (creator) {
creatorName = Utils.displayUsernameForUser(creator);
if (creatorName !== creator.username) {
creatorName += ' (@' + creator.username + ')';
}
} else {
creatorName = (
<FormattedMessage
id='emoji_list.somebody'
defaultMessage='Somebody on another team'
/>
);
if (creatorUsername && creatorUsername !== creatorDisplayName) {
creatorDisplayName += ' (@' + creatorUsername + ')';
}
let deleteButton = null;
if (this.props.onDelete) {
if (this.props.isSystemAdmin || emoji.creator_id === this.props.currentUserId) {
deleteButton = (
<DeleteEmoji onDelete={this.handleDelete}/>
);
......@@ -86,11 +89,11 @@ export default class EmojiListItem extends React.Component {
<td className='emoji-list__image'>
<span
className='emoticon'
style={{backgroundImage: 'url(' + EmojiStore.getEmojiImageUrl(emoji) + ')'}}
style={{backgroundImage: 'url(' + Client4.getCustomEmojiImageUrl(emoji.id) + ')'}}
/>
</td>
<td className='emoji-list__creator'>
{creatorName}
{creatorDisplayName}
</td>
<td className='emoji-list-item_actions'>
{deleteButton}
......
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {getUser, getCurrentUserId, isCurrentUserSystemAdmin} from 'mattermost-redux/selectors/entities/users';
import {deleteCustomEmoji} from 'mattermost-redux/actions/emojis';
import {displayUsernameForUser} from 'utils/utils.jsx';
import EmojiListItem from './emoji_list_item.jsx';
function mapStateToProps(state, ownProps) {
const emoji = state.entities.emojis.customEmoji[ownProps.emojiId];
const creator = getUser(state, emoji.creator_id) || {};
return {
emoji,
creatorDisplayName: displayUsernameForUser(creator),
creatorUsername: creator.username,
currentUserId: getCurrentUserId(state),
isSystemAdmin: isCurrentUserSystemAdmin(state)
};
}
function mapDispatchToProps(dispatch) {