PLT-2643 Fixed asynchronous autocomplete incorrectly replacing text (#3167)
* Allowed different suggestions to match different text. Added a Suggestion base component. Improved text replacement used when filling in suggestions * Fixed formatting
Этот коммит содержится в:
коммит произвёл
Joram Wilander
родитель
6e6257fcca
Коммит
12f6593727
@@ -1,20 +1,21 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Client from 'utils/web_client.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
const MaxUserSuggestions = 40;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class AtMentionSuggestion extends React.Component {
|
||||
class AtMentionSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
const {item, isSelection} = this.props;
|
||||
|
||||
let username;
|
||||
let description;
|
||||
@@ -56,7 +57,7 @@ class AtMentionSuggestion extends React.Component {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className='pull-left'>
|
||||
{icon}
|
||||
@@ -74,12 +75,6 @@ class AtMentionSuggestion extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
AtMentionSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class AtMentionProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/@([a-z0-9\-\._]*)$/i).exec(pretext);
|
||||
@@ -112,8 +107,7 @@ export default class AtMentionProvider {
|
||||
|
||||
const mentions = filtered.map((user) => '@' + user.username);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, captured[0]);
|
||||
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, mentions, filtered, AtMentionSuggestion, captured[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class CommandSuggestion extends React.Component {
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
class CommandSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
@@ -30,16 +32,10 @@ class CommandSuggestion extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
CommandSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class CommandProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
if (pretext.startsWith('/')) {
|
||||
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion);
|
||||
AsyncClient.getSuggestedCommands(pretext, suggestionId, CommandSuggestion, pretext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import React from 'react';
|
||||
|
||||
import * as Emoticons from 'utils/emoticons.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
const MAX_EMOTICON_SUGGESTIONS = 40;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EmoticonSuggestion extends React.Component {
|
||||
class EmoticonSuggestion extends Suggestion {
|
||||
render() {
|
||||
const text = this.props.term;
|
||||
const emoticon = this.props.item;
|
||||
@@ -39,13 +41,6 @@ class EmoticonSuggestion extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
EmoticonSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
term: React.PropTypes.string.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class EmoticonProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/(?:^|\s)(:([a-zA-Z0-9_+\-]*))$/g).exec(pretext);
|
||||
@@ -82,8 +77,7 @@ export default class EmoticonProvider {
|
||||
const terms = matched.map((emoticon) => ':' + emoticon.alias + ':');
|
||||
|
||||
if (terms.length > 0) {
|
||||
SuggestionStore.setMatchedPretext(suggestionId, text);
|
||||
SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, terms, matched, EmoticonSuggestion, text);
|
||||
|
||||
// force the selection to be cleared since the order of elements may have changed
|
||||
SuggestionStore.clearSelection(suggestionId);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
class SearchChannelSuggestion extends React.Component {
|
||||
class SearchChannelSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
@@ -27,12 +29,6 @@ class SearchChannelSuggestion extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SearchChannelSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class SearchChannelProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/\b(?:in|channel):\s*(\S*)$/i).exec(pretext);
|
||||
@@ -62,10 +58,8 @@ export default class SearchChannelProvider {
|
||||
privateChannels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
const privateChannelNames = privateChannels.map((channel) => channel.name);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, channelPrefix);
|
||||
|
||||
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, publicChannelNames, publicChannels, SearchChannelSuggestion, channelPrefix);
|
||||
SuggestionStore.addSuggestions(suggestionId, privateChannelNames, privateChannels, SearchChannelSuggestion, channelPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,8 +72,10 @@ export default class SearchSuggestionList extends SuggestionList {
|
||||
key={term}
|
||||
ref={term}
|
||||
item={item}
|
||||
term={term}
|
||||
matchedPretext={this.state.matchedPretext[i]}
|
||||
isSelection={isSelection}
|
||||
onClick={this.handleItemClick.bind(this, term)}
|
||||
onClick={this.handleItemClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import Client from 'utils/web_client.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SearchUserSuggestion extends React.Component {
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
class SearchUserSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection, onClick} = this.props;
|
||||
|
||||
@@ -31,12 +33,6 @@ class SearchUserSuggestion extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
SearchUserSuggestion.propTypes = {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default class SearchUserProvider {
|
||||
handlePretextChanged(suggestionId, pretext) {
|
||||
const captured = (/\bfrom:\s*(\S*)$/i).exec(pretext);
|
||||
@@ -58,8 +54,7 @@ export default class SearchUserProvider {
|
||||
|
||||
const usernames = filtered.map((user) => user.username);
|
||||
|
||||
SuggestionStore.setMatchedPretext(suggestionId, usernamePrefix);
|
||||
SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion);
|
||||
SuggestionStore.addSuggestions(suggestionId, usernames, filtered, SearchUserSuggestion, usernamePrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
webapp/components/suggestion/suggestion.jsx
Обычный файл
28
webapp/components/suggestion/suggestion.jsx
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Suggestion extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
item: React.PropTypes.object.isRequired,
|
||||
term: React.PropTypes.string.isRequired,
|
||||
matchedPretext: React.PropTypes.string.isRequired,
|
||||
isSelection: React.PropTypes.bool,
|
||||
onClick: React.PropTypes.func
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClick(this.props.term, this.props.matchedPretext);
|
||||
}
|
||||
}
|
||||
@@ -27,10 +27,10 @@ export default class SuggestionBox extends React.Component {
|
||||
this.handlePretextChanged = this.handlePretextChanged.bind(this);
|
||||
|
||||
this.suggestionId = Utils.generateId();
|
||||
SuggestionStore.registerSuggestionBox(this.suggestionId);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SuggestionStore.registerSuggestionBox(this.suggestionId);
|
||||
$(document).on('click', this.handleDocumentClick);
|
||||
|
||||
SuggestionStore.addCompleteWordListener(this.suggestionId, this.handleCompleteWord);
|
||||
@@ -81,12 +81,24 @@ export default class SuggestionBox extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCompleteWord(term) {
|
||||
handleCompleteWord(term, matchedPretext) {
|
||||
const textbox = ReactDOM.findDOMNode(this.refs.textbox);
|
||||
const caret = Utils.getCaretPosition(textbox);
|
||||
|
||||
const text = this.props.value;
|
||||
const prefix = text.substring(0, caret - SuggestionStore.getMatchedPretext(this.suggestionId).length);
|
||||
const pretext = text.substring(0, caret);
|
||||
|
||||
let prefix;
|
||||
if (pretext.endsWith(matchedPretext)) {
|
||||
prefix = pretext.substring(0, pretext.length - matchedPretext.length);
|
||||
} else {
|
||||
// the pretext has changed since we got a term to complete so see if the term still fits the pretext
|
||||
const termWithoutMatched = term.substring(matchedPretext.length);
|
||||
const overlap = SuggestionBox.findOverlap(pretext, termWithoutMatched);
|
||||
|
||||
prefix = pretext.substring(0, pretext.length - overlap.length - matchedPretext.length);
|
||||
}
|
||||
|
||||
const suffix = text.substring(caret);
|
||||
|
||||
if (this.props.onUserInput) {
|
||||
@@ -168,6 +180,20 @@ export default class SuggestionBox extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Finds the longest substring that's at both the end of b and the start of a. For example,
|
||||
// if a = "firepit" and b = "pitbull", findOverlap would return "pit".
|
||||
static findOverlap(a, b) {
|
||||
for (let i = b.length; i > 0; i--) {
|
||||
const substring = b.substring(0, i);
|
||||
|
||||
if (a.endsWith(substring)) {
|
||||
return substring;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
SuggestionBox.defaultProps = {
|
||||
|
||||
@@ -12,6 +12,8 @@ export default class SuggestionList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
|
||||
this.getContent = this.getContent.bind(this);
|
||||
|
||||
this.handleItemClick = this.handleItemClick.bind(this);
|
||||
@@ -19,11 +21,18 @@ export default class SuggestionList extends React.Component {
|
||||
|
||||
this.scrollToItem = this.scrollToItem.bind(this);
|
||||
|
||||
this.state = {
|
||||
items: [],
|
||||
terms: [],
|
||||
components: [],
|
||||
selection: ''
|
||||
this.state = this.getStateFromStores(props.suggestionId);
|
||||
}
|
||||
|
||||
getStateFromStores(suggestionId) {
|
||||
const suggestions = SuggestionStore.getSuggestions(suggestionId || this.props.suggestionId);
|
||||
|
||||
return {
|
||||
matchedPretext: suggestions.matchedPretext,
|
||||
items: suggestions.items,
|
||||
terms: suggestions.terms,
|
||||
components: suggestions.components,
|
||||
selection: suggestions.selection
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +40,12 @@ export default class SuggestionList extends React.Component {
|
||||
SuggestionStore.addSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.selection !== prevState.selection && this.state.selection) {
|
||||
this.scrollToItem(this.state.selection);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SuggestionStore.removeSuggestionsChangedListener(this.props.suggestionId, this.handleSuggestionsChanged);
|
||||
}
|
||||
@@ -39,25 +54,12 @@ export default class SuggestionList extends React.Component {
|
||||
return $(ReactDOM.findDOMNode(this.refs.content));
|
||||
}
|
||||
|
||||
handleItemClick(term, e) {
|
||||
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term);
|
||||
|
||||
e.preventDefault();
|
||||
handleItemClick(term, matchedPretext) {
|
||||
GlobalActions.emitCompleteWordSuggestion(this.props.suggestionId, term, matchedPretext);
|
||||
}
|
||||
|
||||
handleSuggestionsChanged() {
|
||||
const selection = SuggestionStore.getSelection(this.props.suggestionId);
|
||||
|
||||
this.setState({
|
||||
items: SuggestionStore.getItems(this.props.suggestionId),
|
||||
terms: SuggestionStore.getTerms(this.props.suggestionId),
|
||||
components: SuggestionStore.getComponents(this.props.suggestionId),
|
||||
selection
|
||||
});
|
||||
|
||||
if (selection) {
|
||||
window.requestAnimationFrame(() => this.scrollToItem(this.state.selection));
|
||||
}
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
|
||||
scrollToItem(term) {
|
||||
@@ -96,7 +98,6 @@ export default class SuggestionList extends React.Component {
|
||||
|
||||
const items = [];
|
||||
for (let i = 0; i < this.state.items.length; i++) {
|
||||
const item = this.state.items[i];
|
||||
const term = this.state.terms[i];
|
||||
const isSelection = term === this.state.selection;
|
||||
|
||||
@@ -107,10 +108,11 @@ export default class SuggestionList extends React.Component {
|
||||
<Component
|
||||
key={term}
|
||||
ref={term}
|
||||
item={item}
|
||||
item={this.state.items[i]}
|
||||
term={term}
|
||||
matchedPretext={this.state.matchedPretext[i]}
|
||||
isSelection={isSelection}
|
||||
onClick={this.handleItemClick.bind(this, term)}
|
||||
onClick={this.handleItemClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SuggestionStore extends EventEmitter {
|
||||
// this.suggestions stores the state of all SuggestionBoxes by mapping their unique identifier to an
|
||||
// object with the following fields:
|
||||
// pretext: the text before the cursor
|
||||
// matchedPretext: the text before the cursor that will be replaced if an autocomplete term is selected
|
||||
// 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
|
||||
// items: a list of objects backing the terms which may be used in rendering
|
||||
// components: a list of react components that can be used to render their corresponding item
|
||||
@@ -67,14 +67,14 @@ class SuggestionStore extends EventEmitter {
|
||||
removeCompleteWordListener(id, callback) {
|
||||
this.removeListener(COMPLETE_WORD_EVENT + id, callback);
|
||||
}
|
||||
emitCompleteWord(id, term) {
|
||||
this.emit(COMPLETE_WORD_EVENT + id, term);
|
||||
emitCompleteWord(id, term, matchedPretext) {
|
||||
this.emit(COMPLETE_WORD_EVENT + id, term, matchedPretext);
|
||||
}
|
||||
|
||||
registerSuggestionBox(id) {
|
||||
this.suggestions.set(id, {
|
||||
pretext: '',
|
||||
matchedPretext: '',
|
||||
matchedPretext: [],
|
||||
terms: [],
|
||||
items: [],
|
||||
components: [],
|
||||
@@ -89,7 +89,7 @@ class SuggestionStore extends EventEmitter {
|
||||
clearSuggestions(id) {
|
||||
const suggestion = this.suggestions.get(id);
|
||||
|
||||
suggestion.matchedPretext = '';
|
||||
suggestion.matchedPretext = [];
|
||||
suggestion.terms = [];
|
||||
suggestion.items = [];
|
||||
suggestion.components = [];
|
||||
@@ -111,21 +111,16 @@ class SuggestionStore extends EventEmitter {
|
||||
suggestion.pretext = pretext;
|
||||
}
|
||||
|
||||
setMatchedPretext(id, matchedPretext) {
|
||||
const suggestion = this.suggestions.get(id);
|
||||
|
||||
suggestion.matchedPretext = matchedPretext;
|
||||
}
|
||||
|
||||
addSuggestion(id, term, item, component) {
|
||||
addSuggestion(id, term, item, component, matchedPretext) {
|
||||
const suggestion = this.suggestions.get(id);
|
||||
|
||||
suggestion.terms.push(term);
|
||||
suggestion.items.push(item);
|
||||
suggestion.components.push(component);
|
||||
suggestion.matchedPretext.push(matchedPretext);
|
||||
}
|
||||
|
||||
addSuggestions(id, terms, items, component) {
|
||||
addSuggestions(id, terms, items, component, matchedPretext) {
|
||||
const suggestion = this.suggestions.get(id);
|
||||
|
||||
suggestion.terms.push(...terms);
|
||||
@@ -133,6 +128,7 @@ class SuggestionStore extends EventEmitter {
|
||||
|
||||
for (let i = 0; i < terms.length; i++) {
|
||||
suggestion.components.push(component);
|
||||
suggestion.matchedPretext.push(matchedPretext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +156,16 @@ class SuggestionStore extends EventEmitter {
|
||||
return this.suggestions.get(id).pretext;
|
||||
}
|
||||
|
||||
getMatchedPretext(id) {
|
||||
return this.suggestions.get(id).matchedPretext;
|
||||
getSelectedMatchedPretext(id) {
|
||||
const suggestion = this.suggestions.get(id);
|
||||
|
||||
for (let i = 0; i < suggestion.terms.length; i++) {
|
||||
if (suggestion.terms[i] === suggestion.selection) {
|
||||
return suggestion.matchedPretext[i];
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
getItems(id) {
|
||||
@@ -176,6 +180,10 @@ class SuggestionStore extends EventEmitter {
|
||||
return this.suggestions.get(id).components;
|
||||
}
|
||||
|
||||
getSuggestions(id) {
|
||||
return this.suggestions.get(id);
|
||||
}
|
||||
|
||||
getSelection(id) {
|
||||
return this.suggestions.get(id).selection;
|
||||
}
|
||||
@@ -223,15 +231,11 @@ class SuggestionStore extends EventEmitter {
|
||||
this.emitSuggestionsChanged(id);
|
||||
break;
|
||||
case ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS:
|
||||
if (this.getMatchedPretext(id) === '') {
|
||||
this.setMatchedPretext(id, other.matchedPretext);
|
||||
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
|
||||
this.addSuggestions(id, other.terms, other.items, other.component, other.matchedPretext);
|
||||
|
||||
// ensure the matched pretext hasn't changed so that we don't receive suggestions for outdated pretext
|
||||
this.addSuggestions(id, other.terms, other.items, other.component);
|
||||
|
||||
this.ensureSelectionExists(id);
|
||||
this.emitSuggestionsChanged(id);
|
||||
}
|
||||
this.ensureSelectionExists(id);
|
||||
this.emitSuggestionsChanged(id);
|
||||
break;
|
||||
case ActionTypes.SUGGESTION_CLEAR_SUGGESTIONS:
|
||||
this.clearSuggestions(id);
|
||||
@@ -247,7 +251,7 @@ class SuggestionStore extends EventEmitter {
|
||||
this.emitSuggestionsChanged(id);
|
||||
break;
|
||||
case ActionTypes.SUGGESTION_COMPLETE_WORD:
|
||||
this.emitCompleteWord(id, other.term || this.getSelection(id), this.getMatchedPretext(id));
|
||||
this.emitCompleteWord(id, other.term || this.getSelection(id), other.matchedPretext || this.getSelectedMatchedPretext(id));
|
||||
|
||||
this.setPretext(id, '');
|
||||
this.clearSuggestions(id);
|
||||
|
||||
20
webapp/tests/suggestion_box.test.jsx
Обычный файл
20
webapp/tests/suggestion_box.test.jsx
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import assert from 'assert';
|
||||
|
||||
import SuggestionBox from 'components/suggestion/suggestion_box.jsx';
|
||||
|
||||
describe('SuggestionBox', function() {
|
||||
it('findOverlap', function(done) {
|
||||
assert.equal(SuggestionBox.findOverlap('', 'blue'), '');
|
||||
assert.equal(SuggestionBox.findOverlap('red', ''), '');
|
||||
assert.equal(SuggestionBox.findOverlap('red', 'blue'), '');
|
||||
assert.equal(SuggestionBox.findOverlap('red', 'dog'), 'd');
|
||||
assert.equal(SuggestionBox.findOverlap('red', 'education'), 'ed');
|
||||
assert.equal(SuggestionBox.findOverlap('red', 'reduce'), 'red');
|
||||
assert.equal(SuggestionBox.findOverlap('black', 'ack'), 'ack');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
Ссылка в новой задаче
Block a user