Commit e8046fa9 authored by Joram Wilander's avatar Joram Wilander Committed by JoramWilander
Browse files

MM-12398/MM-13153 Fix typing quickly in autocomplete selecting wrong item and...

MM-12398/MM-13153 Fix typing quickly in autocomplete selecting wrong item and fix channel switcher (#2078)

* Fix typing quickly in autocomplete selecting wrong item and fix channel switcher

* Fix opening GMs and add tests

* Fix import
parent 43102c58
......@@ -13,7 +13,7 @@ import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
import {getLastViewedChannelName} from 'selectors/local_storage';
import {browserHistory} from 'utils/browser_history';
import {ActionTypes} from 'utils/constants.jsx';
import {Constants, ActionTypes} from 'utils/constants.jsx';
import {isMobile} from 'utils/utils.jsx';
export function checkAndSetMobileView() {
......@@ -58,6 +58,9 @@ export function switchToChannel(channel) {
return {error: true};
}
browserHistory.push(`${teamUrl}/messages/@${channel.name}`);
} else if (channel.type === Constants.GM_CHANNEL) {
const gmChannel = getChannel(state, channel.id);
browserHistory.push(`${teamUrl}/channels/${gmChannel.name}`);
} else {
browserHistory.push(`${teamUrl}/channels/${channel.name}`);
}
......
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {browserHistory} from 'utils/browser_history';
import * as Actions from 'actions/views/channel';
import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
const mockStore = configureStore([thunk]);
jest.mock('utils/browser_history', () => ({
browserHistory: {
push: jest.fn(),
},
}));
jest.mock('actions/channel_actions.jsx', () => ({
openDirectChannelToUserId: jest.fn(() => {
return {type: ''};
}),
}));
describe('channel view actions', () => {
const channel1 = {id: 'channelid1', name: 'channel1', display_name: 'Channel 1', type: 'O'};
const gmChannel = {id: 'gmchannelid', name: 'gmchannel', display_name: 'GM Channel 1', type: 'G'};
const team1 = {id: 'teamid1', name: 'team1'};
const initialState = {
entities: {
users: {
currentUserId: 'userid1',
profiles: {userid1: {id: 'userid1', username: 'username1'}, userid2: {id: 'userid2', username: 'username2'}},
profilesInChannel: {},
},
teams: {
currentTeamId: 'teamid1',
teams: {teamid1: team1},
},
channels: {
channels: {channelid1: channel1, gmchannelid: gmChannel},
myMembers: {gmchannelid: {channel_id: 'gmchannelid', user_id: 'userid1'}},
},
general: {
config: {},
},
preferences: {
myPreferences: {},
},
},
};
let store;
beforeEach(() => {
store = mockStore(initialState);
});
describe('switchToChannel', () => {
test('switch to public channel', () => {
store.dispatch(Actions.switchToChannel(channel1));
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}/channels/${channel1.name}`);
});
test('switch to fake direct channel', async () => {
await store.dispatch(Actions.switchToChannel({fake: true, userId: 'userid2', name: 'username2'}));
expect(openDirectChannelToUserId).toHaveBeenCalledWith('userid2');
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}/messages/@username2`);
});
test('switch to gm channel', async () => {
await store.dispatch(Actions.switchToChannel(gmChannel));
expect(browserHistory.push).toHaveBeenCalledWith(`/${team1.name}/channels/${gmChannel.name}`);
});
});
});
......@@ -4,7 +4,7 @@
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {switchToChannelById} from 'actions/views/channel';
import {switchToChannel} from 'actions/views/channel';
import QuickSwitchModal from './quick_switch_modal.jsx';
......@@ -17,7 +17,7 @@ function mapStateToProps() {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
switchToChannelById,
switchToChannel,
}, dispatch),
};
}
......
......@@ -33,7 +33,7 @@ export default class QuickSwitchModal extends React.PureComponent {
showTeamSwitcher: PropTypes.bool,
actions: PropTypes.shape({
switchToChannelById: PropTypes.func.isRequired,
switchToChannel: PropTypes.func.isRequired,
}).isRequired,
}
......@@ -111,7 +111,7 @@ export default class QuickSwitchModal extends React.PureComponent {
if (this.state.mode === CHANNEL_MODE) {
const selectedChannel = selected.channel;
this.props.actions.switchToChannelById(selectedChannel.id).then((result) => {
this.props.actions.switchToChannel(selectedChannel).then((result) => {
if (result.data) {
this.onHide();
}
......
......@@ -13,7 +13,7 @@ describe('components/QuickSwitchModal', () => {
onHide: jest.fn(),
showTeamSwitcher: false,
actions: {
switchToChannelById: jest.fn().mockImplementation(() => {
switchToChannel: jest.fn().mockImplementation(() => {
const error = {
message: 'Failed',
};
......@@ -40,7 +40,7 @@ describe('components/QuickSwitchModal', () => {
wrapper.instance().handleSubmit();
expect(baseProps.onHide).not.toBeCalled();
expect(props.actions.switchToChannelById).not.toBeCalled();
expect(props.actions.switchToChannel).not.toBeCalled();
});
it('should fail to switch to a channel', (done) => {
......@@ -50,7 +50,7 @@ describe('components/QuickSwitchModal', () => {
const channel = {id: 'channel_id', userId: 'user_id', type: Constants.DM_CHANNEL};
wrapper.instance().handleSubmit({channel});
expect(baseProps.actions.switchToChannelById).toBeCalledWith(channel.id);
expect(baseProps.actions.switchToChannel).toBeCalledWith(channel);
process.nextTick(() => {
expect(baseProps.onHide).not.toBeCalled();
done();
......@@ -61,7 +61,7 @@ describe('components/QuickSwitchModal', () => {
const props = {
...baseProps,
actions: {
switchToChannelById: jest.fn().mockImplementation(() => {
switchToChannel: jest.fn().mockImplementation(() => {
const data = true;
return Promise.resolve({data});
}),
......@@ -74,7 +74,7 @@ describe('components/QuickSwitchModal', () => {
const channel = {id: 'channel_id', userId: 'user_id', type: Constants.DM_CHANNEL};
wrapper.instance().handleSubmit({channel});
expect(props.actions.switchToChannelById).toBeCalledWith(channel.id);
expect(props.actions.switchToChannel).toBeCalledWith(channel);
process.nextTick(() => {
expect(baseProps.onHide).toBeCalled();
done();
......
......@@ -4,8 +4,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import {debounce} from 'mattermost-redux/actions/helpers';
import QuickInput from 'components/quick_input.jsx';
import Constants from 'utils/constants.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
......@@ -147,6 +145,9 @@ export default class SuggestionBox extends React.Component {
// dividers work on a per-provider basis so this wouldn't be necessary.
this.allowDividers = true;
// Used for debouncing pretext changes
this.timeoutId = '';
// pretext: the text before the cursor
// matchedPretext: a list of the text before the cursor that will be replaced if the corresponding autocomplete term is selected
// terms: a list of strings which the previously typed text may be replaced by
......@@ -427,7 +428,16 @@ export default class SuggestionBox extends React.Component {
matchedPretext = this.state.matchedPretext[i];
}
}
this.handleCompleteWord(this.state.selection, matchedPretext);
// If these don't match, the user typed quickly and pressed enter before we could
// update the pretext, so update the pretext before completing
if (this.pretext.endsWith(matchedPretext)) {
this.handleCompleteWord(this.state.selection, matchedPretext);
} else {
clearTimeout(this.timeoutId);
this.nonDebouncedPretextChanged(this.pretext, true);
}
if (this.props.onKeyDown) {
this.props.onKeyDown(e);
}
......@@ -472,13 +482,26 @@ export default class SuggestionBox extends React.Component {
components: newComponents,
matchedPretext: newPretext,
});
return {selection, matchedPretext: suggestions.matchedPretext};
}
handlePretextChanged = debounce((pretext) => {
handleReceivedSuggestionsAndComplete = (suggestions) => {
const {selection, matchedPretext} = this.handleReceivedSuggestions(suggestions);
if (selection) {
this.handleCompleteWord(selection, matchedPretext);
}
}
nonDebouncedPretextChanged = (pretext, complete = false) => {
this.pretext = pretext;
let handled = false;
let callback = this.handleReceivedSuggestions;
if (complete) {
callback = this.handleReceivedSuggestionsAndComplete;
}
for (const provider of this.props.providers) {
handled = provider.handlePretextChanged(pretext, this.handleReceivedSuggestions) || handled;
handled = provider.handlePretextChanged(pretext, callback) || handled;
if (handled) {
if (provider.constructor.name === 'SearchDateProvider') {
......@@ -499,7 +522,17 @@ export default class SuggestionBox extends React.Component {
if (!handled) {
this.clear();
}
}, Constants.SEARCH_TIMEOUT_MILLISECONDS)
}
debouncedPretextChanged = (pretext) => {
clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => this.nonDebouncedPretextChanged(pretext), Constants.SEARCH_TIMEOUT_MILLISECONDS);
};
handlePretextChanged = (pretext) => {
this.pretext = pretext;
this.debouncedPretextChanged(pretext);
}
blur = () => {
this.refs.input.blur();
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment