Skip to content

Commit

Permalink
Merge pull request #471 from FluxNotes/prioritize-suggestions-portal-…
Browse files Browse the repository at this point in the history
…options

Suggestion portals should prioritize options based on context and matching score
  • Loading branch information
nicoleng12 authored Oct 2, 2018
2 parents 9749eb8 + 9b76f3f commit 32a31d0
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 55 deletions.
52 changes: 43 additions & 9 deletions src/context/ContextManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,19 @@ class ContextManager {
this.contexts = []; // patient context is kept separately
this.activeContexts = []; // patient context is always active
this.onContextUpdate = onContextUpdate;
this.subscribers = [];
}

subscribe = (subscriber, callback) => {
const isAlreadyASubscriber = Collection.includes(this.subscribers, subscriber);
if (!isAlreadyASubscriber) {
this.subscribers.push({
subscriber,
callback,
})
}
}

endNonGlobalContexts() {
let contextsToKeep = [];
this.contexts.forEach((item, i) => {
Expand Down Expand Up @@ -52,18 +63,36 @@ class ContextManager {
}, [])
}

// Returns objects corresponding to all currently valid shortcuts,
// Ordering them from the most recently active context down to the patient context
// ShortcutObjects have an 'id': a string associated to their unique shortcut id
// and they have a 'parentId': a string associated with their parent context, if they have one other than patientContext
getCurrentlyValidShortcuts(shortcutManager) {
let result = shortcutManager.getValidChildShortcutsInContext(this.patientContext, true);
let childResults;
let result = [];
let childIds = [];
this.activeContexts.forEach((shortcut) => {
// GQ changed the recurse argument (2nd) to false below. Don't want it to get child shortcuts of each context unless
// they are in context too. If a shortcut is in context, it will be a separate entry in the active contexts list
// so we'll get the correct shortcuts that way
childResults = shortcutManager.getValidChildShortcutsInContext(shortcut, false);
childResults.forEach((child) => {
if (!result.includes(child)) result.push(child);
childIds = shortcutManager.getValidChildShortcutsInContext(shortcut, false);
const shortcutId = shortcut.getId();
childIds.forEach((childId) => {
// DP added this to the loop
if (Lang.isUndefined(result.find((shortcutObject) => shortcutObject.id === childId))) {
const childObj = {
id: childId,
parentId: shortcutId,
}
result.push(childObj);
}
});
});
// Make sure we add all the patientContext shortcuts
result = result.concat(shortcutManager.getValidChildShortcutsInContext(this.patientContext, true).map((shortcutId) => {
return {
id: shortcutId
}
}));
return result;
}

Expand All @@ -79,16 +108,21 @@ class ContextManager {
}

// returns undefined if not found
getActiveContextOfType(contextType) {
getActiveContextOfType = (contextType) => {
let context = Collection.find(this.activeContexts, (item) => {
return (item.getShortcutType() === contextType);
});

return context;
}

contextUpdated() {
contextUpdated = () => {
this.onContextUpdate();
// After updating, update all subscribers
for (const subscriberObj of this.subscribers) {
const { callback } = subscriberObj;
callback(this);
}
}

adjustActiveContexts(shouldContextBeActive) {
Expand Down Expand Up @@ -127,7 +161,7 @@ class ContextManager {
//when adding a new shortcut to context, we assume cursor ends up after it so its active
this.activeContexts.unshift(shortcut);
if (!shortcut.needToSelectValueFromMultipleOptions()) {
if (this.onContextUpdate) { this.onContextUpdate(); }
if (this.onContextUpdate) { this.contextUpdated(); }
}
}

Expand Down Expand Up @@ -172,7 +206,7 @@ class ContextManager {
clearContexts() {
this.contexts = [];
this.activeContexts = [];
this.onContextUpdate();
this.contextUpdated();
}

// Clears all non active contexts from this.contexts
Expand Down
3 changes: 2 additions & 1 deletion src/lib/slate-suggestions-dist/suggestion-portal.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,9 @@ class SuggestionPortal extends React.Component {

render = () => {
const filteredSuggestions = this.getFilteredSuggestions();
this.setCallbackSuggestion(filteredSuggestions, this.state.selectedIndex);

this.setCallbackSuggestion(filteredSuggestions, this.state.selectedIndex);

return (
<Portal isOpened closeOnEsc closeOnOutsideClick onOpen={this.openPortal}>
<div className="suggestion-portal" ref="suggestionPortal">
Expand Down
65 changes: 20 additions & 45 deletions src/notes/FluxNotesEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import Slate from '../lib/slate';
import Lang from 'lodash';
import FontAwesome from 'react-fontawesome';
import ContextPortal from '../context/ContextPortal';
import SuggestionPortalShortcutSearchIndex from './SuggestionPortalShortcutSearchIndex'
import SuggestionPortalPlaceholderSearchIndex from './SuggestionPortalPlaceholderSearchIndex'
// versions 0.20.3-0.20.7 of Slate seem to have an issue.
// when we change the selection and give focus in our key handlers, Slate changes the selection including
// focus and then immediately takes focus away. Not an issue in 0.20.2 and older. package.json currently
Expand Down Expand Up @@ -136,29 +138,41 @@ class FluxNotesEditor extends React.Component {
this.NLPHashtagPlugin = NLPHashtagPlugin(NLPHashtagPluginOptions);
this.plugins.push(this.NLPHashtagPlugin)

// Track all the indexes needed for suggestions portals
this.suggestionPortalSearchIndexes = [];

// setup creator suggestions plugin (autocomplete)
const creatorSuggestionPortalSearchIndex = new SuggestionPortalShortcutSearchIndex([], '#', this.props.shortcutManager);
this.contextManager.subscribe(creatorSuggestionPortalSearchIndex, creatorSuggestionPortalSearchIndex.updateIndex)
this.suggestionPortalSearchIndexes.push(creatorSuggestionPortalSearchIndex)
this.suggestionsPluginCreators = SuggestionsPlugin({
capture: /#([\w\s\-,]*)/,
onEnter: this.choseSuggestedShortcut.bind(this),
suggestions: this.suggestionFunction.bind(this, '#'),
suggestions: creatorSuggestionPortalSearchIndex.search,
trigger: '#',
});
this.plugins.push(this.suggestionsPluginCreators)

// setup inserter suggestions plugin (autocomplete)
const inserterSuggestionPortalSearchIndex = new SuggestionPortalShortcutSearchIndex([], '@', this.props.shortcutManager);
this.contextManager.subscribe(inserterSuggestionPortalSearchIndex, inserterSuggestionPortalSearchIndex.updateIndex)
this.suggestionPortalSearchIndexes.push(inserterSuggestionPortalSearchIndex)
this.suggestionsPluginInserters = SuggestionsPlugin({
capture: /@([\w\s\-,]*)/,
onEnter: this.choseSuggestedShortcut.bind(this),
suggestions: this.suggestionFunction.bind(this, '@'),
suggestions: inserterSuggestionPortalSearchIndex.search,
trigger: '@',
});
this.plugins.push(this.suggestionsPluginInserters)

// Setup suggestions plugin
const placeholderSuggestionPortalSearchIndex = new SuggestionPortalPlaceholderSearchIndex([], '<', this.props.shortcutManager);
this.contextManager.subscribe(placeholderSuggestionPortalSearchIndex, placeholderSuggestionPortalSearchIndex.updateIndex)
this.suggestionPortalSearchIndexes.push(placeholderSuggestionPortalSearchIndex)
this.suggestionsPluginPlaceholders = SuggestionsPlugin({
capture: /<([\w\s\-,>]*)/,
onEnter: this.choseSuggestedPlaceholder.bind(this),
suggestions: this.suggestionFunction.bind(this, '<'),
suggestions: placeholderSuggestionPortalSearchIndex.search,
trigger: '<',
});
this.plugins.push(this.suggestionsPluginPlaceholders);
Expand Down Expand Up @@ -268,46 +282,6 @@ class FluxNotesEditor extends React.Component {
this.contextManager.clearContexts();
}

suggestionFunction(initialChar, text) {
if (Lang.isUndefined(text)) return [];

const { shortcutManager } = this.props;
const shortcuts = this.contextManager.getCurrentlyValidShortcuts(shortcutManager);
const suggestionsShortcuts = [];
const textLowercase = text.toLowerCase();

shortcuts.forEach((shortcut) => {
const triggers = shortcutManager.getTriggersForShortcut(shortcut);
triggers.forEach((trigger) => {
const triggerNoPrefix = trigger.name.substring(1);
if (trigger.name.substring(0, 1) === initialChar && triggerNoPrefix.toLowerCase().includes(textLowercase)) {
suggestionsShortcuts.push({
key: triggerNoPrefix,
value: trigger,
suggestion: triggerNoPrefix,
});
}
});
});
const placeHolderShortcuts = shortcutManager.getAllPlaceholderShortcuts();

placeHolderShortcuts.forEach((shortcut) => {
const triggers = shortcutManager.getTriggersForShortcut(shortcut.id);
triggers.forEach((trigger) => {
const triggerNoPrefix = trigger.name.substring(1);
if (initialChar === '<' && triggerNoPrefix.toLowerCase().includes(textLowercase)) {
suggestionsShortcuts.push({
key: triggerNoPrefix,
value: `${initialChar}${triggerNoPrefix}>`,
suggestion: triggerNoPrefix,
});
}
});
});

return suggestionsShortcuts.slice(0, 10);
}

choseSuggestedShortcut(suggestion) {
const {state} = this.state;
const shortcut = this.props.newCurrentShortcut(null, suggestion.value.name, undefined, true, "auto-complete");
Expand Down Expand Up @@ -1264,8 +1238,9 @@ class FluxNotesEditor extends React.Component {
const shortcuts = this.contextManager.getCurrentlyValidShortcuts(this.props.shortcutManager);

// Check if shortcutTrigger is a shortcut trigger in the list of currently valid shortcuts
return shortcuts.some((shortcut) => {
const triggers = this.props.shortcutManager.getTriggersForShortcut(shortcut);
return shortcuts.some((shortcutObj) => {
const shortcutId = shortcutObj.id
const triggers = this.props.shortcutManager.getTriggersForShortcut(shortcutId);
return triggers.some((trigger) => {
return trigger.name.toLowerCase() === shortcutTrigger.toLowerCase();
});
Expand Down
33 changes: 33 additions & 0 deletions src/notes/SuggestionPortalPlaceholderSearchIndex.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import SuggestionPortalSearchIndex from './SuggestionPortalSearchIndex';
import Fuse from 'fuse.js';
import Lang from 'lodash';

class SuggestionPortalPlaceholderSearchIndex extends SuggestionPortalSearchIndex {
constructor(list, initialChar, shortcutManager) {
super(list, initialChar, shortcutManager)
this.currentlyValidPlaceholders = [];
}
updateIndex = (contextManager) => {
const placeholders = this.shortcutManager.getAllPlaceholderShortcuts();
// If shortcuts haven't updated, we don't need to update our fuse index
if (Lang.isEqual(placeholders, this.currentlyValidPlaceholders)) return
this.currentlyValidPlaceholders = placeholders;
const relevantShortcuts = [];

placeholders.forEach((placeholder) => {
const triggers = this.shortcutManager.getTriggersForShortcut(placeholder.id);
triggers.forEach((trigger) => {
const triggerNoPrefix = trigger.name.substring(1);
relevantShortcuts.push({
key: triggerNoPrefix,
value: `${this.initialChar}${triggerNoPrefix}>`,
suggestion: triggerNoPrefix,
});
});
});

this.shortcutsFuse = new Fuse(relevantShortcuts, this.fuseOptions);
}
};

export default SuggestionPortalPlaceholderSearchIndex;
82 changes: 82 additions & 0 deletions src/notes/SuggestionPortalSearchIndex.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Fuse from 'fuse.js';
import Lang from 'lodash';

class SuggestionPortalSearchIndex {
constructor(list, initialChar, shortcutManager) {
this.initialChar = initialChar;
this.shortcutManager = shortcutManager;
// Metdata common to all suggestionSearchIndexs
this.fuseOptions = {
includeScore: true,
includeMatches: true,
threshold: 0.3,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"suggestion"
]
}
this.shortcutsFuse = new Fuse([], this.fuseOptions);
}

// Takes a contextmanager and uses the current context to update it's current shortcutsFuse Index
updateIndex = (contextManager) => {
// Every kind of suggestion portal index is going to be different, so we don't have a common way of building an index.
}

sortSuggestionsAlphabetically = (a, b) => {
if(a.data.score > b.data.score) {
return 1;
}
if(a.data.score < b.data.score){
return -1;
}
if(a.suggestion.toLowerCase() > b.suggestion.toLowerCase()){
return 1;
}
if(a.suggestion.toLowerCase() < b.suggestion.toLowerCase()){
return -1;
}
return 0;
}

search = (searchText) => {
if (Lang.isUndefined(searchText)) return [];

const maxLength = 25;
const searchTextLowercase = searchText.toLowerCase();
let results = this.shortcutsFuse.search(searchTextLowercase);

// If there are no results, if the searchText is empty, and if the list being searched on is nonempty
// return a list of shortcutsFuseOptions formatted with this extra data field
if (results.length === 0 && Lang.isEmpty(searchText)) {
return this.shortcutsFuse.list.slice(0, maxLength).map((suggestionObj) => {
suggestionObj.data = {
score: 0.1,
matches: [],
}
return suggestionObj
});
}


const resultFormatted = results.map((result) => {
return {
key: result.item.key,
value: result.item.value,
suggestion: result.item.suggestion,
data: {
// Use the bonus score to drag the most recent shortcuts to the top and weight the older ones to the bottom
score: result.score + result.item.scoreBonusBasedOnContext,
matches: result.matches,
},
};
}).sort(this.sortSuggestionsAlphabetically).slice(0,maxLength);;

return resultFormatted
}
}

export default SuggestionPortalSearchIndex;
Loading

0 comments on commit 32a31d0

Please sign in to comment.