From 364048cc48020230546d7572cc51e81bba6b127e Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Thu, 20 Sep 2018 17:27:27 -0400 Subject: [PATCH 01/20] Highlight notes in note assistant while hovering in search results --- src/notes/NoteAssistant.jsx | 16 ++++++++++++---- src/notes/NoteAssistant.scss | 4 ++++ src/patientControl/NotesIndexer.js | 3 ++- src/patientControl/PatientSearch.jsx | 5 ++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 7b06511a1d..107e346240 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -22,7 +22,8 @@ export default class NoteAssistant extends Component { // insertingTemplate indicates whether a shortcut or template is being inserted // false indicates a shortcut is being inserted // true indicates a template is being inserted - insertingTemplate: false + insertingTemplate: false, + searchResultNoteId: null }; // On creating of NoteAssistant, check if the note viewer is editable if (this.props.isNoteViewerEditable) { @@ -220,13 +221,20 @@ export default class NoteAssistant extends Component { this.props.deleteSelectedNote(); } + onSearchSuggestionHighlighted = (suggestion) => { + this.setState({ + searchResultNoteId: suggestion.note.entryInfo.entryId + }); + this.refs[suggestion.note.entryInfo.entryId].scrollIntoView(); + } + // Render the content for the Note Assistant panel renderNoteAssistantContent(noteAssistantMode) { const allNotes = this.props.patient.getNotes(); const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); this.props.searchIndex.removeDataBySection('Clinical Notes'); - notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex); + notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted); switch (noteAssistantMode) { case "poc": return ( @@ -383,14 +391,14 @@ export default class NoteAssistant extends Component { // For each clinical note, render the note image with the text renderClinicalNote(item, i) { let selected = Lang.isEqual(this.props.selectedNote, item); + let searchedFor = item.entryInfo.entryId === this.state.searchResultNoteId; // if we have closed the note, selected = false if (Lang.isEqual(this.props.noteClosed, true)) { selected = false; } return ( -
{ - +
{ this.openNote(false, item) }}>
{item.signedOn}
diff --git a/src/notes/NoteAssistant.scss b/src/notes/NoteAssistant.scss index ba46c426df..6d72b77be9 100644 --- a/src/notes/NoteAssistant.scss +++ b/src/notes/NoteAssistant.scss @@ -3,6 +3,7 @@ $note-assitant-width-1: 175px; $note-assitant-width-2: 170px; $note-assitant-width-3: 160px; +$highlight-yellow: rgb(255, 255, 70); /* Wrapper */ .note-assistant-wrapper { @@ -108,6 +109,9 @@ $note-assitant-width-3: 160px; border-width: 2px; border-color: $interface-blue; } + &.search-result { + background-color: $highlight-yellow; + } &.existing-note { .existing-note-date { padding-bottom: 5px; diff --git a/src/patientControl/NotesIndexer.js b/src/patientControl/NotesIndexer.js index 02207b85b8..a51f482554 100644 --- a/src/patientControl/NotesIndexer.js +++ b/src/patientControl/NotesIndexer.js @@ -1,7 +1,7 @@ import BaseIndexer from './BaseIndexer'; class NotesIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { + indexData(section, subsection, data, searchIndex, onHighlight) { searchIndex.addSearchableData({ section, subsection: '', @@ -40,6 +40,7 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Content`, value: note.content, + onHighlight }) searchIndex.addSearchableData({ diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index 773efe435b..02c1c3a550 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -69,6 +69,7 @@ class PatientSearch extends React.Component { valueTitle: obj.valueTitle, contentSnapshot: obj.value, source: "clinicalNote", + onHighlight: obj.onHighlight }; // If searching content of note, remove styling from content before executing regex @@ -166,7 +167,9 @@ class PatientSearch extends React.Component { onSuggestionHighlighted = ({suggestion}) => { if (!Lang.isNull(suggestion)) { - if (suggestion.source !== "clinicalNote") { + if (suggestion.onHighlight) { + suggestion.onHighlight(suggestion); + } else if (suggestion.source !== "clinicalNote") { this.props.moveTargetedDataPanelToSubsection(suggestion.section, suggestion.subsection); } } From 846409d24cd561de126cb2e3c9c3584065a4aa45 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Fri, 21 Sep 2018 09:19:10 -0400 Subject: [PATCH 02/20] Added handlers for all indexers --- src/notes/NoteAssistant.jsx | 6 ++++- src/panels/TargetedDataPanel.jsx | 5 +++++ src/patientControl/BaseIndexer.js | 5 +++-- src/patientControl/ClusterPointsIndexer.js | 4 ++-- src/patientControl/ColumnsIndexer.js | 13 ++++++----- .../DiseaseStatusValuesIndexer.js | 10 +++++---- src/patientControl/EventsIndexer.js | 7 +++--- src/patientControl/MedicationsIndexer.js | 22 ++++++++++++------- src/patientControl/NameValuePairsIndexer.js | 7 +++--- src/patientControl/NotesIndexer.js | 17 ++++++++++++-- src/patientControl/PatientSearch.jsx | 18 ++++++--------- src/patientControl/ValueOverTimeIndexer.js | 4 ++-- src/summary/TargetedDataSection.jsx | 5 +++-- src/summary/TargetedDataSubpanel.jsx | 1 + 14 files changed, 79 insertions(+), 45 deletions(-) diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 107e346240..1841ab2b81 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -228,13 +228,17 @@ export default class NoteAssistant extends Component { this.refs[suggestion.note.entryInfo.entryId].scrollIntoView(); } + onSearchSuggestionClicked = (suggestion) => { + this.openNote(!suggestion.note.signed, suggestion.note); + } + // Render the content for the Note Assistant panel renderNoteAssistantContent(noteAssistantMode) { const allNotes = this.props.patient.getNotes(); const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); this.props.searchIndex.removeDataBySection('Clinical Notes'); - notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted); + notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, this.onSearchSuggestionClicked); switch (noteAssistantMode) { case "poc": return ( diff --git a/src/panels/TargetedDataPanel.jsx b/src/panels/TargetedDataPanel.jsx index a10314a779..f6cceda6c5 100644 --- a/src/panels/TargetedDataPanel.jsx +++ b/src/panels/TargetedDataPanel.jsx @@ -21,6 +21,10 @@ export default class TargetedDataPanel extends Component { return this.minimap.moveToSubsection(sectionName, subsectionName); } + moveToSubsectionFromSearch(suggestion) { + this.moveToSubsection(suggestion.section, suggestion.subsection) + } + render () { // The css data attribute associated with the minimap const minimapAttribute = 'data-test-summary-section'; @@ -51,6 +55,7 @@ export default class TargetedDataPanel extends Component { setForceRefresh={this.props.setForceRefresh} summaryMetadata={this.props.summaryMetadata.getMetadata()} searchIndex={this.props.searchIndex} + moveToSubsectionFromSearch={this.moveToSubsectionFromSearch.bind(this)} />
diff --git a/src/patientControl/BaseIndexer.js b/src/patientControl/BaseIndexer.js index 9609d21421..e7fa937197 100644 --- a/src/patientControl/BaseIndexer.js +++ b/src/patientControl/BaseIndexer.js @@ -1,13 +1,14 @@ import { Component } from 'react'; class BaseIndexer extends Component { - indexData(section, subsection, data, searchIndex) { + indexData(section, subsection, data, searchIndex, onHighlight) { if (subsection) { searchIndex.addSearchableData({ section, subsection, valueTitle: "Subsection", - value: subsection + value: subsection, + onHighlight }); } } diff --git a/src/patientControl/ClusterPointsIndexer.js b/src/patientControl/ClusterPointsIndexer.js index 50097ed410..c7d1f3b4f9 100644 --- a/src/patientControl/ClusterPointsIndexer.js +++ b/src/patientControl/ClusterPointsIndexer.js @@ -1,8 +1,8 @@ import BaseIndexer from './BaseIndexer'; class ClusterPointsIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); } } diff --git a/src/patientControl/ColumnsIndexer.js b/src/patientControl/ColumnsIndexer.js index adf0114fd5..59886c309c 100644 --- a/src/patientControl/ColumnsIndexer.js +++ b/src/patientControl/ColumnsIndexer.js @@ -1,8 +1,8 @@ import BaseIndexer from './BaseIndexer'; class ColumnIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex, subsectionDescriptor) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight, subsectionDescriptor) { + super.indexData(section, subsection, data, searchIndex, onHighlight); if (subsectionDescriptor.headings) { // true tabular where each item is a column of data // add each column value using title of the column heading @@ -13,7 +13,8 @@ class ColumnIndexer extends BaseIndexer { section, subsection, valueTitle: `${vtPrefix}${subsectionDescriptor.headings[columnNumber]}`, - value: col.value || "Missing Data" + value: col.value || "Missing Data", + onHighlight }); }); }); @@ -27,7 +28,8 @@ class ColumnIndexer extends BaseIndexer { section, subsection, valueTitle: subsection, - value: valueObject.value || "Missing Data" + value: valueObject.value || "Missing Data", + onHighlight }); } else { const [ title, valueObject ] = row; @@ -35,7 +37,8 @@ class ColumnIndexer extends BaseIndexer { section, subsection, valueTitle: title.value, - value: valueObject.value || "Missing Data" + value: valueObject.value || "Missing Data", + onHighlight }); } }); diff --git a/src/patientControl/DiseaseStatusValuesIndexer.js b/src/patientControl/DiseaseStatusValuesIndexer.js index 161182235e..f158017dfa 100644 --- a/src/patientControl/DiseaseStatusValuesIndexer.js +++ b/src/patientControl/DiseaseStatusValuesIndexer.js @@ -1,14 +1,15 @@ import BaseIndexer from './BaseIndexer'; class DiseaseStatusValuesIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); data.potentialDiagnosisDates.forEach(item => { searchIndex.addSearchableData({ section, subsection: "", valueTitle: item.label, - value: item.date + value: item.date, + onHighlight }); }); @@ -17,7 +18,8 @@ class DiseaseStatusValuesIndexer extends BaseIndexer { section, subsection: "", valueTitle: progression.start_time, - value: progression.disease_status_string + value: progression.disease_status_string, + onHighlight }); }); } diff --git a/src/patientControl/EventsIndexer.js b/src/patientControl/EventsIndexer.js index ecb523793f..976a83a61a 100644 --- a/src/patientControl/EventsIndexer.js +++ b/src/patientControl/EventsIndexer.js @@ -1,14 +1,15 @@ import BaseIndexer from './BaseIndexer'; class EventsIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); data.forEach(item => { searchIndex.addSearchableData({ section, subsection, valueTitle: "", - value: subsection === "Procedures" ? item.hoverText : `${item.hoverTitle}: ${item.hoverText}` + value: subsection === "Procedures" ? item.hoverText : `${item.hoverTitle}: ${item.hoverText}`, + onHighlight }); }); } diff --git a/src/patientControl/MedicationsIndexer.js b/src/patientControl/MedicationsIndexer.js index c160818d73..c27137c69a 100644 --- a/src/patientControl/MedicationsIndexer.js +++ b/src/patientControl/MedicationsIndexer.js @@ -1,49 +1,55 @@ import BaseIndexer from './BaseIndexer'; class MedicationsIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); data.forEach(item => { searchIndex.addSearchableData({ section, subsection: "", valueTitle: `${item.medication.medication} Dosage`, - value: `${item.medication.amountPerDose.value} ${item.medication.amountPerDose.units}` + value: `${item.medication.amountPerDose.value} ${item.medication.amountPerDose.units}`, + onHighlight }); searchIndex.addSearchableData({ section, subsection: "", valueTitle: "Medication", - value: item.medication.medication + value: item.medication.medication, + onHighlight }); searchIndex.addSearchableData({ section, subsection: "", valueTitle: `${item.medication.medication} Timing`, - value: item.medication.timingOfDoses.value + value: item.medication.timingOfDoses.value, + onHighlight }); searchIndex.addSearchableData({ section, subsection: "", valueTitle: `${item.medication.medication} Prescribed`, - value: item.medication.whenPrescribed + value: item.medication.whenPrescribed, + onHighlight }); searchIndex.addSearchableData({ section, subsection: "", valueTitle: `${item.medication.medication} Route`, - value: item.medication.routeIntoBody + value: item.medication.routeIntoBody, + onHighlight }); searchIndex.addSearchableData({ section, subsection: "", valueTitle: `${item.medication.medication} Prescribed By`, - value: item.medication.prescribedBy + value: item.medication.prescribedBy, + onHighlight }); }); } diff --git a/src/patientControl/NameValuePairsIndexer.js b/src/patientControl/NameValuePairsIndexer.js index e03aea9b10..4ad8f282ce 100644 --- a/src/patientControl/NameValuePairsIndexer.js +++ b/src/patientControl/NameValuePairsIndexer.js @@ -1,14 +1,15 @@ import BaseIndexer from './BaseIndexer'; class NameValuePairsIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); data.forEach(obj => { searchIndex.addSearchableData({ section, subsection: "", valueTitle: obj.name, - value: obj.value || "Missing Data" + value: obj.value || "Missing Data", + onHighlight }); }) } diff --git a/src/patientControl/NotesIndexer.js b/src/patientControl/NotesIndexer.js index a51f482554..46084a286a 100644 --- a/src/patientControl/NotesIndexer.js +++ b/src/patientControl/NotesIndexer.js @@ -1,7 +1,7 @@ import BaseIndexer from './BaseIndexer'; class NotesIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex, onHighlight) { + indexData(section, subsection, data, searchIndex, onHighlight, onClick) { searchIndex.addSearchableData({ section, subsection: '', @@ -32,6 +32,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: 'Title', value: note.subject, + onHighlight, + onClick }); searchIndex.addSearchableData({ @@ -40,7 +42,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Content`, value: note.content, - onHighlight + onHighlight, + onClick }) searchIndex.addSearchableData({ @@ -49,6 +52,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Source`, value: note.hospital, + onHighlight, + onClick }); if (note.signed) { @@ -58,6 +63,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Signed on`, value: note.signedOn, + onHighlight, + onClick }); searchIndex.addSearchableData({ @@ -66,6 +73,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Signed by`, value: note.signedBy, + onHighlight, + onClick }); } else { searchIndex.addSearchableData({ @@ -74,6 +83,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Created on`, value: note.createdOn, + onHighlight, + onClick }); searchIndex.addSearchableData({ @@ -82,6 +93,8 @@ class NotesIndexer extends BaseIndexer { subsection: notesSubsection, valueTitle: `Created by`, value: note.createdBy, + onHighlight, + onClick }); } }); diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index 02c1c3a550..7b4f4d2dfa 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -69,7 +69,8 @@ class PatientSearch extends React.Component { valueTitle: obj.valueTitle, contentSnapshot: obj.value, source: "clinicalNote", - onHighlight: obj.onHighlight + onHighlight: obj.onHighlight, + onClick: obj.onClick }; // If searching content of note, remove styling from content before executing regex @@ -103,6 +104,7 @@ class PatientSearch extends React.Component { inputValue, matchedOn: "", source: 'structuredData', + onHighlight: obj.onHighlight } const contentMatches = regex.exec(obj.value); @@ -156,22 +158,16 @@ class PatientSearch extends React.Component { // Will be called every time suggestion is selected via mouse or keyboard. onSuggestionSelected = (event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => { - if (suggestion.note) { - this.props.setSearchSelectedItem(suggestion.note) - } else { - this.props.moveTargetedDataPanelToSubsection(suggestion.section, suggestion.subsection); + if (suggestion.onClick) { + suggestion.onClick(suggestion); } this.refs.autosuggest.input.blur(); } onSuggestionHighlighted = ({suggestion}) => { - if (!Lang.isNull(suggestion)) { - if (suggestion.onHighlight) { - suggestion.onHighlight(suggestion); - } else if (suggestion.source !== "clinicalNote") { - this.props.moveTargetedDataPanelToSubsection(suggestion.section, suggestion.subsection); - } + if (!Lang.isNull(suggestion) && suggestion.onHighlight) { + suggestion.onHighlight(suggestion); } } diff --git a/src/patientControl/ValueOverTimeIndexer.js b/src/patientControl/ValueOverTimeIndexer.js index 069e54a4e5..7203b2af11 100644 --- a/src/patientControl/ValueOverTimeIndexer.js +++ b/src/patientControl/ValueOverTimeIndexer.js @@ -1,8 +1,8 @@ import BaseIndexer from './BaseIndexer'; class ValueOverTimeIndexer extends BaseIndexer { - indexData(section, subsection, data, searchIndex) { - super.indexData(section, subsection, data, searchIndex); + indexData(section, subsection, data, searchIndex, onHighlight) { + super.indexData(section, subsection, data, searchIndex, onHighlight); data.forEach(item => { searchIndex.addSearchableData({ section, diff --git a/src/summary/TargetedDataSection.jsx b/src/summary/TargetedDataSection.jsx index 74db00dd15..f153a20573 100644 --- a/src/summary/TargetedDataSection.jsx +++ b/src/summary/TargetedDataSection.jsx @@ -173,9 +173,10 @@ export default class TargetedDataSection extends Component { section: section.name, subsection: "", valueTitle: "Section", - value: section.name + value: section.name, + onHighlight: this.props.moveToSubsectionFromSearch }); - indexer.indexData(section.name, subsection.name, list, searchIndex, newSubsection); + indexer.indexData(section.name, subsection.name, list, searchIndex, this.props.moveToSubsectionFromSearch, newSubsection); } }) diff --git a/src/summary/TargetedDataSubpanel.jsx b/src/summary/TargetedDataSubpanel.jsx index 7172497661..42beda6240 100644 --- a/src/summary/TargetedDataSubpanel.jsx +++ b/src/summary/TargetedDataSubpanel.jsx @@ -165,6 +165,7 @@ export default class TargetedDataSubpanel extends Component { actions={actions} loginUser={this.props.loginUser} searchIndex={this.props.searchIndex} + moveToSubsectionFromSearch={this.props.moveToSubsectionFromSearch} /> {i < conditionMetadata.sections.length - 1 ? : null} From eab9e5cf05be21521bd2f59293c360daba49d2ce Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Fri, 21 Sep 2018 17:44:56 -0400 Subject: [PATCH 03/20] Index note content separately --- src/notes/FluxNotesEditor.jsx | 12 ++++ src/notes/NoteAssistant.jsx | 4 +- src/panels/NotesPanel.jsx | 1 + src/patientControl/NoteContentIndexer.js | 81 ++++++++++++++++++++++++ src/patientControl/NotesIndexer.js | 78 +---------------------- 5 files changed, 100 insertions(+), 76 deletions(-) create mode 100644 src/patientControl/NoteContentIndexer.js diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index 01552ded41..b3be8f62ff 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -22,6 +22,7 @@ import NLPHashtagPlugin from './NLPHashtagPlugin' import NoteParser from '../noteparser/NoteParser'; import './FluxNotesEditor.css'; import { setTimeout } from 'timers'; +import NoteContentIndexer from '../patientControl/NoteContentIndexer'; // This forces the initial block to be inline instead of a paragraph. When insert structured field, prevents adding new lines const initialState = Slate.Plain.deserialize(''); @@ -89,6 +90,8 @@ class FluxNotesEditor extends React.Component { this.plugins = []; this.previousState = {}; + this.noteContentIndexer = new NoteContentIndexer(); + // Set the initial state when the app is first constructed. this.resetEditorState(); @@ -737,6 +740,15 @@ class FluxNotesEditor extends React.Component { this.props.setNoteViewerEditable(true); } } + + this.props.searchIndex.removeDataBySection('Open Note'); + this.props.searchIndex.addSearchableData({ + section: 'Open Note', + subsection: '', + valueTitle: 'Section', + value: 'Open Note' + }); + this.noteContentIndexer.indexData("Open Note", '', nextProps.updatedEditorNote, this.props.searchIndex, null, null); } // Check if the current view mode changes diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 1841ab2b81..df35af00c4 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -238,7 +238,9 @@ export default class NoteAssistant extends Component { const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); this.props.searchIndex.removeDataBySection('Clinical Notes'); - notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, this.onSearchSuggestionClicked); + + // Temporarily disabling opening source note on click + notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked switch (noteAssistantMode) { case "poc": return ( diff --git a/src/panels/NotesPanel.jsx b/src/panels/NotesPanel.jsx index 110f48a42c..faf041ab47 100644 --- a/src/panels/NotesPanel.jsx +++ b/src/panels/NotesPanel.jsx @@ -396,6 +396,7 @@ export default class NotesPanel extends Component { changeShortcutType={this.changeShortcutType} openSourceNoteEntryId={this.props.openSourceNoteEntryId} setOpenSourceNoteEntryId={this.props.setOpenSourceNoteEntryId} + searchIndex={this.props.searchIndex} /> ); diff --git a/src/patientControl/NoteContentIndexer.js b/src/patientControl/NoteContentIndexer.js new file mode 100644 index 0000000000..a4bc5f85cd --- /dev/null +++ b/src/patientControl/NoteContentIndexer.js @@ -0,0 +1,81 @@ +import BaseIndexer from './BaseIndexer' + +class NoteContentIndexer extends BaseIndexer { + indexData(section, subsection, note, searchIndex, onHighlight, onClick) { + const notesSubsection = note.signed ? 'Signed Notes' : 'In Progress Notes'; + console.log(searchIndex); + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: 'Title', + value: note.subject, + onHighlight, + onClick + }); + + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Content`, + value: note.content, + onHighlight, + onClick + }) + + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Source`, + value: note.hospital, + onHighlight, + onClick + }); + + if (note.signed) { + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Signed on`, + value: note.signedOn, + onHighlight, + onClick + }); + + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Signed by`, + value: note.signedBy, + onHighlight, + onClick + }); + } else { + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Created on`, + value: note.createdOn, + onHighlight, + onClick + }); + + searchIndex.addSearchableData({ + note, + section, + subsection: notesSubsection, + valueTitle: `Created by`, + value: note.createdBy, + onHighlight, + onClick + }); + } + } +} + +export default NoteContentIndexer; \ No newline at end of file diff --git a/src/patientControl/NotesIndexer.js b/src/patientControl/NotesIndexer.js index 46084a286a..0db979a749 100644 --- a/src/patientControl/NotesIndexer.js +++ b/src/patientControl/NotesIndexer.js @@ -1,6 +1,6 @@ -import BaseIndexer from './BaseIndexer'; +import NoteContentIndexer from './NoteContentIndexer'; -class NotesIndexer extends BaseIndexer { +class NotesIndexer extends NoteContentIndexer { indexData(section, subsection, data, searchIndex, onHighlight, onClick) { searchIndex.addSearchableData({ section, @@ -24,79 +24,7 @@ class NotesIndexer extends BaseIndexer { }); data.forEach(note => { - const notesSubsection = note.signed ? 'Signed Notes' : 'In Progress Notes'; - - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: 'Title', - value: note.subject, - onHighlight, - onClick - }); - - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Content`, - value: note.content, - onHighlight, - onClick - }) - - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Source`, - value: note.hospital, - onHighlight, - onClick - }); - - if (note.signed) { - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Signed on`, - value: note.signedOn, - onHighlight, - onClick - }); - - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Signed by`, - value: note.signedBy, - onHighlight, - onClick - }); - } else { - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Created on`, - value: note.createdOn, - onHighlight, - onClick - }); - - searchIndex.addSearchableData({ - note, - section, - subsection: notesSubsection, - valueTitle: `Created by`, - value: note.createdBy, - onHighlight, - onClick - }); - } + super.indexData(section, subsection, note, searchIndex, onHighlight, onClick); }); } } From 28baea8ec28aaab8f468f9be142ab3c2fe20e521 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Fri, 21 Sep 2018 17:55:16 -0400 Subject: [PATCH 04/20] Remove Open Note indexed data when note is closed or delted --- src/notes/FluxNotesEditor.jsx | 2 +- src/notes/NoteAssistant.jsx | 1 + src/patientControl/NoteContentIndexer.js | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index b3be8f62ff..b8fef51297 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -514,6 +514,7 @@ class FluxNotesEditor extends React.Component { } closeNote = () => { + this.props.searchIndex.removeDataBySection('Open Note'); if (this.props.noteAssistantMode === 'pick-list-options-panel') { const documentText = this.getNoteText(this.previousState); this.revertTemplate(); @@ -740,7 +741,6 @@ class FluxNotesEditor extends React.Component { this.props.setNoteViewerEditable(true); } } - this.props.searchIndex.removeDataBySection('Open Note'); this.props.searchIndex.addSearchableData({ section: 'Open Note', diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index df35af00c4..05dbf2fece 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -218,6 +218,7 @@ export default class NoteAssistant extends Component { } deleteSelectedNote = () => { + this.props.searchIndex.removeDataBySection('Open Note'); this.props.deleteSelectedNote(); } diff --git a/src/patientControl/NoteContentIndexer.js b/src/patientControl/NoteContentIndexer.js index a4bc5f85cd..2505ba6999 100644 --- a/src/patientControl/NoteContentIndexer.js +++ b/src/patientControl/NoteContentIndexer.js @@ -3,7 +3,6 @@ import BaseIndexer from './BaseIndexer' class NoteContentIndexer extends BaseIndexer { indexData(section, subsection, note, searchIndex, onHighlight, onClick) { const notesSubsection = note.signed ? 'Signed Notes' : 'In Progress Notes'; - console.log(searchIndex); searchIndex.addSearchableData({ note, section, From c5e4447913c7ac9463156f437f47613078ad2df4 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Fri, 21 Sep 2018 17:58:32 -0400 Subject: [PATCH 05/20] Changed 'notes' to 'record' in search bar --- src/patientControl/PatientSearch.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index 7b4f4d2dfa..cbc7402c45 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -195,7 +195,7 @@ class PatientSearch extends React.Component { const { value, suggestions } = this.state; const inputProps = { - placeholder: `Search ${this.firstName}'s notes`, + placeholder: `Search ${this.firstName}'s record`, value, onChange: this.onChange }; From 596084a62581b8371a060928f7affafd72093171 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 24 Sep 2018 15:24:08 -0400 Subject: [PATCH 06/20] Added unique IDs to indexed data --- src/notes/FluxNotesEditor.jsx | 5 ++++- src/patientControl/BaseIndexer.js | 7 +++++++ src/patientControl/ColumnsIndexer.js | 8 +++++++- src/patientControl/DiseaseStatusValuesIndexer.js | 5 +++++ src/patientControl/EventsIndexer.js | 6 +++++- src/patientControl/MedicationsIndexer.js | 8 ++++++++ src/patientControl/NameValuePairsIndexer.js | 2 ++ src/patientControl/NoteContentIndexer.js | 10 ++++++++++ src/patientControl/NotesIndexer.js | 4 ++++ src/patientControl/ValueOverTimeIndexer.js | 4 ++++ src/summary/TargetedDataSection.jsx | 2 ++ 11 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index b8fef51297..a5b2144cea 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -743,10 +743,13 @@ class FluxNotesEditor extends React.Component { } this.props.searchIndex.removeDataBySection('Open Note'); this.props.searchIndex.addSearchableData({ + id: 'open_note_section', section: 'Open Note', subsection: '', valueTitle: 'Section', - value: 'Open Note' + value: 'Open Note', + onHighlight: null, + onClick: null }); this.noteContentIndexer.indexData("Open Note", '', nextProps.updatedEditorNote, this.props.searchIndex, null, null); } diff --git a/src/patientControl/BaseIndexer.js b/src/patientControl/BaseIndexer.js index e7fa937197..edea2df743 100644 --- a/src/patientControl/BaseIndexer.js +++ b/src/patientControl/BaseIndexer.js @@ -3,7 +3,10 @@ import { Component } from 'react'; class BaseIndexer extends Component { indexData(section, subsection, data, searchIndex, onHighlight) { if (subsection) { + const sectionId = this.getStringForId(section); + const subsectionId = this.getStringForId(subsection); searchIndex.addSearchableData({ + id: `${sectionId}_${subsectionId}_subsection`, section, subsection, valueTitle: "Subsection", @@ -12,6 +15,10 @@ class BaseIndexer extends Component { }); } } + + getStringForId(s) { + return s.toLowerCase().replace(/[.,#!$%&;:{}=\-_`~()]/g,"").replace(/ /g, '_'); + } } export default BaseIndexer; \ No newline at end of file diff --git a/src/patientControl/ColumnsIndexer.js b/src/patientControl/ColumnsIndexer.js index 59886c309c..4392fc454b 100644 --- a/src/patientControl/ColumnsIndexer.js +++ b/src/patientControl/ColumnsIndexer.js @@ -3,13 +3,16 @@ import BaseIndexer from './BaseIndexer'; class ColumnIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight, subsectionDescriptor) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); + const subsectionId = super.getStringForId(subsection); if (subsectionDescriptor.headings) { // true tabular where each item is a column of data // add each column value using title of the column heading - data.forEach((row) => { + data.forEach((row, rowNumber) => { row.forEach((col, columnNumber) => { const vtPrefix = columnNumber === 0 ? '' : `${row[0].value} `; searchIndex.addSearchableData({ + id: `${sectionId}_${subsectionId}_${rowNumber}_${columnNumber}`, section, subsection, valueTitle: `${vtPrefix}${subsectionDescriptor.headings[columnNumber]}`, @@ -25,6 +28,7 @@ class ColumnIndexer extends BaseIndexer { if (row.length < 2) { const [ valueObject ] = row; searchIndex.addSearchableData({ + id: `${sectionId}_${subsectionId}_${subsectionId}`, section, subsection, valueTitle: subsection, @@ -33,7 +37,9 @@ class ColumnIndexer extends BaseIndexer { }); } else { const [ title, valueObject ] = row; + const valueTitleId = super.getStringForId(title.value); searchIndex.addSearchableData({ + id: `${sectionId}_${valueTitleId}`, section, subsection, valueTitle: title.value, diff --git a/src/patientControl/DiseaseStatusValuesIndexer.js b/src/patientControl/DiseaseStatusValuesIndexer.js index f158017dfa..53b3347ade 100644 --- a/src/patientControl/DiseaseStatusValuesIndexer.js +++ b/src/patientControl/DiseaseStatusValuesIndexer.js @@ -3,8 +3,11 @@ import BaseIndexer from './BaseIndexer'; class DiseaseStatusValuesIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); data.potentialDiagnosisDates.forEach(item => { + const valueTitleId = super.getStringForId(item.label); searchIndex.addSearchableData({ + id: `${sectionId}_${valueTitleId}`, section, subsection: "", valueTitle: item.label, @@ -14,7 +17,9 @@ class DiseaseStatusValuesIndexer extends BaseIndexer { }); data.progressions.forEach(progression => { + const valueTitleId = super.getStringForId(progression.start_time); searchIndex.addSearchableData({ + id: `${sectionId}_${valueTitleId}`, section, subsection: "", valueTitle: progression.start_time, diff --git a/src/patientControl/EventsIndexer.js b/src/patientControl/EventsIndexer.js index 976a83a61a..e162f14319 100644 --- a/src/patientControl/EventsIndexer.js +++ b/src/patientControl/EventsIndexer.js @@ -3,12 +3,16 @@ import BaseIndexer from './BaseIndexer'; class EventsIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); + const subsectionId = super.getStringForId(sectionId); data.forEach(item => { + const value = subsection === "Procedures" ? item.hoverText : `${item.hoverTitle}: ${item.hoverText}`; searchIndex.addSearchableData({ + id: `${sectionId}_${subsectionId}_${super.getStringForId(value)}`, section, subsection, valueTitle: "", - value: subsection === "Procedures" ? item.hoverText : `${item.hoverTitle}: ${item.hoverText}`, + value, onHighlight }); }); diff --git a/src/patientControl/MedicationsIndexer.js b/src/patientControl/MedicationsIndexer.js index c27137c69a..3120c1753e 100644 --- a/src/patientControl/MedicationsIndexer.js +++ b/src/patientControl/MedicationsIndexer.js @@ -3,8 +3,11 @@ import BaseIndexer from './BaseIndexer'; class MedicationsIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); data.forEach(item => { + const medicationId = super.getStringForId(item.medication.medication); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_dosage`, section, subsection: "", valueTitle: `${item.medication.medication} Dosage`, @@ -13,6 +16,7 @@ class MedicationsIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_medication`, section, subsection: "", valueTitle: "Medication", @@ -21,6 +25,7 @@ class MedicationsIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_timing`, section, subsection: "", valueTitle: `${item.medication.medication} Timing`, @@ -29,6 +34,7 @@ class MedicationsIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_perscribed`, section, subsection: "", valueTitle: `${item.medication.medication} Prescribed`, @@ -37,6 +43,7 @@ class MedicationsIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_route`, section, subsection: "", valueTitle: `${item.medication.medication} Route`, @@ -45,6 +52,7 @@ class MedicationsIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${sectionId}_${medicationId}_perscribed_by`, section, subsection: "", valueTitle: `${item.medication.medication} Prescribed By`, diff --git a/src/patientControl/NameValuePairsIndexer.js b/src/patientControl/NameValuePairsIndexer.js index 4ad8f282ce..7f3e545bad 100644 --- a/src/patientControl/NameValuePairsIndexer.js +++ b/src/patientControl/NameValuePairsIndexer.js @@ -3,8 +3,10 @@ import BaseIndexer from './BaseIndexer'; class NameValuePairsIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); data.forEach(obj => { searchIndex.addSearchableData({ + id: `${sectionId}_${super.getStringForId(obj.name)}`, section, subsection: "", valueTitle: obj.name, diff --git a/src/patientControl/NoteContentIndexer.js b/src/patientControl/NoteContentIndexer.js index 2505ba6999..f694df5883 100644 --- a/src/patientControl/NoteContentIndexer.js +++ b/src/patientControl/NoteContentIndexer.js @@ -3,7 +3,11 @@ import BaseIndexer from './BaseIndexer' class NoteContentIndexer extends BaseIndexer { indexData(section, subsection, note, searchIndex, onHighlight, onClick) { const notesSubsection = note.signed ? 'Signed Notes' : 'In Progress Notes'; + const notesSectionId = section.toLowerCase().replace(' ', '_'); + const notesSubsectionId = notesSubsection.toLowerCase().replace(' ', '_'); + const {entryId} = note.entryInfo; searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_title_${entryId}`, note, section, subsection: notesSubsection, @@ -14,6 +18,7 @@ class NoteContentIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_content_${entryId}`, note, section, subsection: notesSubsection, @@ -24,6 +29,7 @@ class NoteContentIndexer extends BaseIndexer { }) searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_source_${entryId}`, note, section, subsection: notesSubsection, @@ -35,6 +41,7 @@ class NoteContentIndexer extends BaseIndexer { if (note.signed) { searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_signed_on_${entryId}`, note, section, subsection: notesSubsection, @@ -45,6 +52,7 @@ class NoteContentIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_signed_by_${entryId}`, note, section, subsection: notesSubsection, @@ -55,6 +63,7 @@ class NoteContentIndexer extends BaseIndexer { }); } else { searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_created_on_${entryId}`, note, section, subsection: notesSubsection, @@ -65,6 +74,7 @@ class NoteContentIndexer extends BaseIndexer { }); searchIndex.addSearchableData({ + id: `${notesSectionId}_${notesSubsectionId}_created_by_${entryId}`, note, section, subsection: notesSubsection, diff --git a/src/patientControl/NotesIndexer.js b/src/patientControl/NotesIndexer.js index 0db979a749..f526cb069b 100644 --- a/src/patientControl/NotesIndexer.js +++ b/src/patientControl/NotesIndexer.js @@ -2,7 +2,9 @@ import NoteContentIndexer from './NoteContentIndexer'; class NotesIndexer extends NoteContentIndexer { indexData(section, subsection, data, searchIndex, onHighlight, onClick) { + const noteSectionId = super.getStringForId(section); searchIndex.addSearchableData({ + id: noteSectionId, section, subsection: '', valueTitle: 'Section', @@ -10,6 +12,7 @@ class NotesIndexer extends NoteContentIndexer { }); searchIndex.addSearchableData({ + id: `${noteSectionId}_signed_notes}`, section, subsection: 'Signed Notes', valueTitle: 'Subsection', @@ -17,6 +20,7 @@ class NotesIndexer extends NoteContentIndexer { }); searchIndex.addSearchableData({ + id: `${noteSectionId}_in_progress_notes}`, section, subsection: 'In Progress Notes', valueTitle: 'Subsection', diff --git a/src/patientControl/ValueOverTimeIndexer.js b/src/patientControl/ValueOverTimeIndexer.js index 7203b2af11..a62b074f2c 100644 --- a/src/patientControl/ValueOverTimeIndexer.js +++ b/src/patientControl/ValueOverTimeIndexer.js @@ -3,8 +3,12 @@ import BaseIndexer from './BaseIndexer'; class ValueOverTimeIndexer extends BaseIndexer { indexData(section, subsection, data, searchIndex, onHighlight) { super.indexData(section, subsection, data, searchIndex, onHighlight); + const sectionId = super.getStringForId(section); + const subsectionId = super.getStringForId(subsection); data.forEach(item => { + const value = `${item.start_time}: ${item[subsection]} ${item.unit}`; searchIndex.addSearchableData({ + id: `${sectionId}_${subsectionId}_${super.getStringForId(value)}`, section, subsection, valueTitle: subsection, diff --git a/src/summary/TargetedDataSection.jsx b/src/summary/TargetedDataSection.jsx index f153a20573..a724450a87 100644 --- a/src/summary/TargetedDataSection.jsx +++ b/src/summary/TargetedDataSection.jsx @@ -169,7 +169,9 @@ export default class TargetedDataSection extends Component { const indexer = this.props.visualizerManager.getIndexer(typeToIndex); if (!Lang.isUndefined(subsection.nameFunction)) subsection.name = subsection.nameFunction(); if (indexer) { + const sectionId = section.name.toLowerCase().replace(/ /g, '_'); searchIndex.addSearchableData({ + id: `${sectionId}_${sectionId}`, section: section.name, subsection: "", valueTitle: "Section", From 393656bdac3b8c3f25dd60e45f5f5dac8384298f Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 24 Sep 2018 15:25:26 -0400 Subject: [PATCH 07/20] Integrated elasticlunr for indexed queries --- package.json | 1 + src/notes/NoteAssistant.jsx | 6 +- src/patientControl/PatientSearch.jsx | 72 +++++----------- src/patientControl/SearchIndex.js | 42 ++++++++- src/patientControl/SearchSuggestion.jsx | 108 +++++------------------- src/summary/TargetedDataSection.jsx | 28 ++++-- yarn.lock | 4 + 7 files changed, 109 insertions(+), 152 deletions(-) diff --git a/package.json b/package.json index 817692ece6..a1157a1325 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "d3-scale": "^1.0.7", "d3-time": "^1.0.8", "downshift": "^1.30.1", + "elasticlunr": "^0.9.5", "es6-shim": "0.35.3", "fhirclient": "^0.1.12", "flat": "^4.1.0", diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 05dbf2fece..51222ea43a 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -226,7 +226,10 @@ export default class NoteAssistant extends Component { this.setState({ searchResultNoteId: suggestion.note.entryInfo.entryId }); - this.refs[suggestion.note.entryInfo.entryId].scrollIntoView(); + const domNodeRef = this.refs[suggestion.note.entryInfo.entryId]; + if (domNodeRef && domNodeRef.scrollIntoView) { + domNodeRef.scrollIntoView(); + } } onSearchSuggestionClicked = (suggestion) => { @@ -238,7 +241,6 @@ export default class NoteAssistant extends Component { const allNotes = this.props.patient.getNotes(); const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); - this.props.searchIndex.removeDataBySection('Clinical Notes'); // Temporarily disabling opening source note on click notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index cbc7402c45..8e10357dee 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -54,71 +54,37 @@ class PatientSearch extends React.Component { // Used by AutoSuggest to get a list of suggestions based on the current search's InputValue getSuggestions = (inputValue) => { let suggestions = []; - const regex = new RegExp(escapeRegExp(inputValue), "gi"); - - this.props.searchIndex.searchableData.forEach(obj => { + let results = this.props.searchIndex.search(inputValue); + results.forEach(result => { let suggestion; - - // obj.note means this should be a clinicalNote suggestion - if (obj.note) { + if (result.note) { suggestion = { - date: obj.note.signedOn || obj.note.createdOn, - subject: obj.note.subject, + date: result.note.signedOn || result.note.createdOn, + subject: result.note.subject, inputValue: inputValue, - note: obj.note, - valueTitle: obj.valueTitle, - contentSnapshot: obj.value, + note: result.note, + valueTitle: result.valueTitle, + contentSnapshot: this.getNoteContentWithoutStyle(result.value), source: "clinicalNote", - onHighlight: obj.onHighlight, - onClick: obj.onClick - }; - - // If searching content of note, remove styling from content before executing regex - if (obj.note.content === obj.value) { - const noteContentWithoutStyle = this.getNoteContentWithoutStyle(obj.value); - const noteRegex = this.createRegexForSearching(inputValue); - let contentMatches = noteRegex.exec(noteContentWithoutStyle); - - while (contentMatches) { - // Want a snapshot of text; use index and continue 100 chars - suggestion.contentSnapshot = noteContentWithoutStyle.slice(contentMatches.index, contentMatches.index + 100) - suggestion.matchedOn = "contentSnapshot"; - // Clone the object - suggestions.push(Lang.clone(suggestion)); - contentMatches = noteRegex.exec(noteContentWithoutStyle); - } - } else { - const contentMatches = regex.exec(obj.value); - - if (contentMatches) { - suggestion.matchedOn = 'contentSnapshot'; - suggestions.push(suggestion); - } + matchedOn: result.valueTitle === "Content" ? "contentSnapshot" : "valueTitle", + onHighlight: result.onHighlight, + onClick: result.onClick, + score: result.score } } else { suggestion = { - section: obj.section, - subsection: obj.subsection, - contentSnapshot: obj.value, - valueTitle: obj.valueTitle, + section: result.section, + subsection: result.subsection, + contentSnapshot: result.value, + valueTitle: result.valueTitle, inputValue, matchedOn: "", source: 'structuredData', - onHighlight: obj.onHighlight - } - const contentMatches = regex.exec(obj.value); - - if (contentMatches) { - suggestion.matchedOn = "contentSnapshot"; - suggestions.push(suggestion); + onHighlight: result.onHighlight, + score: result.score } } - - const contentMatches = regex.exec(obj.valueTitle); - if (!suggestion.matchOn && contentMatches) { - suggestion.matchedOn = "valueTitle"; - suggestions.push(suggestion); - } + suggestions.push(suggestion); }); return suggestions; diff --git a/src/patientControl/SearchIndex.js b/src/patientControl/SearchIndex.js index 266843074f..a4357057e1 100644 --- a/src/patientControl/SearchIndex.js +++ b/src/patientControl/SearchIndex.js @@ -1,8 +1,18 @@ import Lang from 'lodash'; +import elasticlunr from 'elasticlunr'; class SearchIndex { constructor() { this._searchableData = []; + elasticlunr.clearStopWords(); + this._index = elasticlunr(function() { + this.addField('section'); + this.addField('subsection'); + this.addField('valueTitle'); + this.addField('value'); + this.setRef('id'); + this.saveDocument(true); + }); } get searchableData() { @@ -10,11 +20,39 @@ class SearchIndex { } addSearchableData(data) { - this._searchableData.push(data); + const existingDocument = this._index.documentStore.getDoc(data.id); + if (Lang.isNull(existingDocument) || !Lang.isEqual(Lang.omit(data, ['onHighlight', 'onClick']), Lang.omit(existingDocument, ['onHighlight', 'onClick']))) { + this._index.addDoc({...data}); + } } removeDataBySection(section) { - Lang.remove(this._searchableData, data => data.section === section); + for(let id in this._index.documentStore.docs) { + if (this._index.documentStore.getDoc(id).section === section) { + this.removeDataByRef(id); + } + } + } + + removeDataByRef(ref) { + this._index.removeDocByRef(ref); + } + + search(query) { + return this._index.search(query, { + fields: { + valueTitle: { + expand: true + }, + value: { + expand: true + } + } + }).map(result => { + let doc = this._index.documentStore.getDoc(result.ref); + doc.score = result.score; + return doc; + }); } } diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 392d2a0658..b5cf1766e5 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -8,106 +8,40 @@ class SearchSuggestion extends React.Component { renderSuggestionText() { const { suggestion } = this.props; - const fullSnapshot = suggestion.contentSnapshot; - const inputValue = suggestion.inputValue; - // Lowercase versions for index finding - const fullSnapshotLowerCase = fullSnapshot.toLowerCase(); + const {inputValue, valueTitle, contentSnapshot} = suggestion; const inputValueLowerCase = inputValue.toLowerCase(); - let indexOfMatch = fullSnapshotLowerCase.indexOf(inputValueLowerCase); - // Slice original copies for highlighting - let preText = fullSnapshot.slice(0, indexOfMatch); - let highlightedText = fullSnapshot.slice(indexOfMatch, preText.length + inputValue.length) - let postText = fullSnapshot.slice(preText.length + inputValue.length, fullSnapshot.length); - - let suggestionText = ''; - if (suggestion.matchedOn === "contentSnapshot") { - let title = suggestion.valueTitle.length > 0 ? `${suggestion.valueTitle}: ` : ''; - suggestionText = ( -
- {title} - {preText} - {highlightedText} - {postText} -
- ) - } else if (suggestion.matchedOn === "valueTitle") { - let valueTitleLowerCase = suggestion.valueTitle.toLowerCase(); - indexOfMatch = valueTitleLowerCase.indexOf(inputValueLowerCase); - preText = suggestion.valueTitle.slice(0, indexOfMatch); - highlightedText = suggestion.valueTitle.slice(indexOfMatch, preText.length + inputValue.length); - postText = suggestion.valueTitle.slice(preText.length + inputValue.length, suggestion.valueTitle.length); - suggestionText = ( -
- {preText} - {highlightedText} - {postText} - {': '} - {suggestion.contentSnapshot} -
- ) - } else if (suggestion.matchedOn === "section" || suggestion.matchedOn === "subsection") { - let title = suggestion.valueTitle.length > 0 ? `${suggestion.valueTitle}: ` : ''; - suggestionText = ( -
- {title + suggestion.contentSnapshot}
- ); + const fullText = `${valueTitle ? valueTitle + ': ' : ''}${contentSnapshot}`; + const fullTextLowerCase = fullText.toLowerCase(); + const indexOfMatch = fullTextLowerCase.indexOf(inputValueLowerCase); + let preText = '', highlightedText = '', postText = ''; + if (indexOfMatch !== -1) { + preText = fullText.slice(0, indexOfMatch); + highlightedText = fullText.slice(indexOfMatch, preText.length + inputValue.length) + postText = fullText.slice(preText.length + inputValue.length, fullText.length); } else { - suggestionText = ( -
{suggestion.contentSnapshot}
- ); + postText = fullText; } - return suggestionText; + + return ( +
+ {preText} + {highlightedText} + {postText} +
+ ) } renderLabel = () => { const { suggestion } = this.props; - const inputValue = suggestion.inputValue; - const inputValueLowerCase = inputValue.toLowerCase(); let suggestionLabel = ''; if (suggestion.source === 'structuredData') { - - if(suggestion.matchedOn === "section") { - const sectionLowerCase = suggestion.section.toLowerCase(); - const indexOfMatch = sectionLowerCase.indexOf(inputValueLowerCase); - const preText = suggestion.section.slice(0, indexOfMatch); - const highlightedText = suggestion.section.slice(indexOfMatch, preText.length + inputValue.length); - const postText = suggestion.section.slice(preText.length + inputValue.length, suggestion.section.length); - const subsection = suggestion.subsection.length > 0 ? ` > ${suggestion.subsection}` : ''; - suggestionLabel = ( -
- - {preText} - {highlightedText} - {postText} - {subsection} -
- ); - } - else if(suggestion.matchedOn === "subsection") { - const subsectionLowerCase = suggestion.subsection.toLowerCase(); - const indexOfMatch = subsectionLowerCase.indexOf(inputValueLowerCase); - const preText = suggestion.subsection.slice(0, indexOfMatch); - const highlightedText = suggestion.subsection.slice(indexOfMatch, preText.length + inputValue.length); - const postText = suggestion.subsection.slice(preText.length + inputValue.length, suggestion.subsection.length); - suggestionLabel = ( -
- {suggestion.section + ` > `} - {preText} - {highlightedText} - {postText} - -
- ); - } else { - const subsection = suggestion.subsection.length > 0 ? ` > ${suggestion.subsection}` : ''; - suggestionLabel = ( + const subsection = suggestion.subsection.length > 0 ? ` > ${suggestion.subsection}` : ''; + suggestionLabel = (
{suggestion.section + subsection}
- ); - } - + ); } else if (suggestion.source === 'clinicalNote') { const date = {suggestion.date + ` `}; const subject = {suggestion.subject + ` `}; diff --git a/src/summary/TargetedDataSection.jsx b/src/summary/TargetedDataSection.jsx index a724450a87..0c8dfabc87 100644 --- a/src/summary/TargetedDataSection.jsx +++ b/src/summary/TargetedDataSection.jsx @@ -66,6 +66,8 @@ export default class TargetedDataSection extends Component { } handleViewChange = (chosenVisualizer) => { + this.props.searchIndex.removeDataBySection(this.props.section.name); + this.indexSectionData(this.props.section); this.setState({ chosenVisualizer }); } @@ -119,19 +121,16 @@ export default class TargetedDataSection extends Component { } } - // renderSection checks the type of data that is being passed and chooses the correct component to render the data - // TODO: Add a List type and a tabular renderer for it for Procedures section. case where left column is data - // and not just a label - renderSection = (section) => { - const { patient, condition, allowItemClick, isWide, type, loginUser, actions, searchIndex } = this.props; + indexSectionData(section) { + const { patient, condition, type, loginUser, searchIndex } = this.props; const visualization = this.checkVisualization(); const viz = this.props.visualizerManager.getVisualizer(type, visualization); + if (Lang.isNull(viz)) return null; const sectionTransform = viz.transform; - const Visualizer = viz.visualizer; if (section.resetData) section.resetData(); - searchIndex.removeDataBySection(section.name); + // searchIndex.removeDataBySection(section.name); const subsections = patient === null || condition === null || section === null ? [] : section.data; subsections.forEach(subsection => { @@ -180,7 +179,20 @@ export default class TargetedDataSection extends Component { }); indexer.indexData(section.name, subsection.name, list, searchIndex, this.props.moveToSubsectionFromSearch, newSubsection); } - }) + }); + return viz; + } + + // renderSection checks the type of data that is being passed and chooses the correct component to render the data + // TODO: Add a List type and a tabular renderer for it for Procedures section. case where left column is data + // and not just a label + renderSection = (section) => { + const { patient, condition, allowItemClick, isWide, loginUser, actions, searchIndex } = this.props; + + const viz = this.indexSectionData(section); + if (Lang.isNull(viz)) return null; + const Visualizer = viz.visualizer; + const sectionTransform = viz.transform; return ( Date: Mon, 24 Sep 2018 16:06:26 -0400 Subject: [PATCH 08/20] Fixed search result highlihgting --- src/patientControl/SearchSuggestion.jsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index b5cf1766e5..1a1365df6c 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -12,12 +12,19 @@ class SearchSuggestion extends React.Component { const inputValueLowerCase = inputValue.toLowerCase(); const fullText = `${valueTitle ? valueTitle + ': ' : ''}${contentSnapshot}`; const fullTextLowerCase = fullText.toLowerCase(); - const indexOfMatch = fullTextLowerCase.indexOf(inputValueLowerCase); + const regex = new RegExp(inputValueLowerCase, "g"); + const matchesTitle = regex.exec(valueTitle.toLowerCase()); + const matchesContent = regex.exec(contentSnapshot.toLowerCase()); let preText = '', highlightedText = '', postText = ''; - if (indexOfMatch !== -1) { - preText = fullText.slice(0, indexOfMatch); - highlightedText = fullText.slice(indexOfMatch, preText.length + inputValue.length) - postText = fullText.slice(preText.length + inputValue.length, fullText.length); + + if (matchesTitle) { + preText = ''; + highlightedText = valueTitle; + postText = `: ${contentSnapshot}`; + } else if (matchesContent) { + preText = valueTitle ? valueTitle + ': ' : '' + contentSnapshot.slice(0, matchesContent.index); + highlightedText = contentSnapshot.slice(matchesContent.index, matchesContent.index + inputValue.length); + postText = contentSnapshot.slice(matchesContent.index + inputValue.length, contentSnapshot.length); } else { postText = fullText; } From 92b06196c65848986f382e0418fa1e75782e2a64 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 24 Sep 2018 18:11:45 -0400 Subject: [PATCH 09/20] Fixed error with search highlighting referring to unmounted noteassistant --- src/notes/NoteAssistant.jsx | 15 ++++++++------- src/patientControl/SearchIndex.js | 2 +- src/patientControl/SearchSuggestion.jsx | 1 - 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 51222ea43a..86ff2435d1 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -223,13 +223,15 @@ export default class NoteAssistant extends Component { } onSearchSuggestionHighlighted = (suggestion) => { + this.toggleView('clinical-notes'); this.setState({ searchResultNoteId: suggestion.note.entryInfo.entryId + }, () => { + const domNodeRef = this.refs[suggestion.note.entryInfo.entryId]; + if (domNodeRef && domNodeRef.scrollIntoView) { + domNodeRef.scrollIntoView(); + } }); - const domNodeRef = this.refs[suggestion.note.entryInfo.entryId]; - if (domNodeRef && domNodeRef.scrollIntoView) { - domNodeRef.scrollIntoView(); - } } onSearchSuggestionClicked = (suggestion) => { @@ -241,9 +243,6 @@ export default class NoteAssistant extends Component { const allNotes = this.props.patient.getNotes(); const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); - - // Temporarily disabling opening source note on click - notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked switch (noteAssistantMode) { case "poc": return ( @@ -270,6 +269,8 @@ export default class NoteAssistant extends Component { // Render the clinical notes view which includes new note button, resume note button, // number of previous notes label, sort selection, and preview of previous notes case "clinical-notes": + // Temporarily disabling opening source note on click + notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked return (
{this.renderNewNote()} diff --git a/src/patientControl/SearchIndex.js b/src/patientControl/SearchIndex.js index a4357057e1..7e50acff97 100644 --- a/src/patientControl/SearchIndex.js +++ b/src/patientControl/SearchIndex.js @@ -21,7 +21,7 @@ class SearchIndex { addSearchableData(data) { const existingDocument = this._index.documentStore.getDoc(data.id); - if (Lang.isNull(existingDocument) || !Lang.isEqual(Lang.omit(data, ['onHighlight', 'onClick']), Lang.omit(existingDocument, ['onHighlight', 'onClick']))) { + if (Lang.isNull(existingDocument) || !Lang.isEqual(data, existingDocument)) { this._index.addDoc({...data}); } } diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 1a1365df6c..275e461703 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -11,7 +11,6 @@ class SearchSuggestion extends React.Component { const {inputValue, valueTitle, contentSnapshot} = suggestion; const inputValueLowerCase = inputValue.toLowerCase(); const fullText = `${valueTitle ? valueTitle + ': ' : ''}${contentSnapshot}`; - const fullTextLowerCase = fullText.toLowerCase(); const regex = new RegExp(inputValueLowerCase, "g"); const matchesTitle = regex.exec(valueTitle.toLowerCase()); const matchesContent = regex.exec(contentSnapshot.toLowerCase()); From 3120d148b7cc1cc6eea2af5824b23e6003675877 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Tue, 25 Sep 2018 17:10:12 -0400 Subject: [PATCH 10/20] Rename scrollToShortcut to scrollToData for generalized use --- src/notes/FluxNotesEditor.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index a5b2144cea..66e808f333 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -630,7 +630,7 @@ class FluxNotesEditor extends React.Component { return transform; } - scrollToShortcut = (document, shortcutKey) => { + scrollToData = (document, shortcutKey) => { const node = document.getNode(shortcutKey); try { @@ -648,7 +648,7 @@ class FluxNotesEditor extends React.Component { if (nextProps.shouldUpdateShortcutType) { let transform = this.state.state.transform(); let state = transform.setNodeByKey(nextProps.shortcutKey, nextProps.shortcutType).apply(); - this.scrollToShortcut(state.document, nextProps.shortcutKey); + this.scrollToData(state.document, nextProps.shortcutKey); this.setState({ state }); } nextProps.selectedPickListOptions.forEach(picklist => { @@ -682,7 +682,7 @@ class FluxNotesEditor extends React.Component { transform = this.resetShortcutData(shortcut, transform); let state = transform.apply(); this.setState({ state }, () => { - this.scrollToShortcut(state.document, shortcut.getKey()); + this.scrollToData(state.document, shortcut.getKey()); }); } } @@ -784,7 +784,7 @@ class FluxNotesEditor extends React.Component { const shortcutKey = this.structuredFieldMapManager.getKeyFromEntryId(nextProps.openSourceNoteEntryId); if (shortcutKey) { - this.scrollToShortcut(this.state.state.document, shortcutKey); + this.scrollToData(this.state.state.document, shortcutKey); this.props.setOpenSourceNoteEntryId(null); } } @@ -1141,7 +1141,7 @@ class FluxNotesEditor extends React.Component { if (shortcutKey) { setTimeout(() => { - this.scrollToShortcut(state.document, shortcutKey) + this.scrollToData(state.document, shortcutKey) this.props.setOpenSourceNoteEntryId(null); }, 0); } From e06e7d8756861d59c697f70daf97584254a13b2d Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Tue, 25 Sep 2018 17:11:28 -0400 Subject: [PATCH 11/20] Added separate full text indexer for note content to allow search results to appear multiple times with the index of the match specified --- package.json | 1 + src/notes/NoteAssistant.jsx | 4 +-- src/panels/NotesPanel.jsx | 6 ++++ src/patientControl/NoteContentIndexer.js | 19 +++++++++-- src/patientControl/PatientSearch.jsx | 4 ++- src/patientControl/SearchIndex.js | 43 ++++++++++++++++++++++-- src/patientControl/SearchSuggestion.jsx | 8 +++-- yarn.lock | 4 +++ 8 files changed, 78 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index a1157a1325..2715feaa8b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "flat": "^4.1.0", "flux_notes_treatment_options_rest_client": "^1.0.0", "font-awesome": "4.7.0", + "fuse.js": "^3.2.1", "guid": "0.0.12", "highcharts": "^6.1.1", "history": "^4.7.2", diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 86ff2435d1..5986510cb5 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -243,6 +243,8 @@ export default class NoteAssistant extends Component { const allNotes = this.props.patient.getNotes(); const numberOfPreviousSignedNotes = Lang.filter(allNotes, o => o.signed).length; const notesIndexer = new NotesIndexer(); + // Temporarily disabling opening source note on click + notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked switch (noteAssistantMode) { case "poc": return ( @@ -269,8 +271,6 @@ export default class NoteAssistant extends Component { // Render the clinical notes view which includes new note button, resume note button, // number of previous notes label, sort selection, and preview of previous notes case "clinical-notes": - // Temporarily disabling opening source note on click - notesIndexer.indexData('Clinical Notes', '', allNotes, this.props.searchIndex, this.onSearchSuggestionHighlighted, null); //this.onSearchSuggestionClicked return (
{this.renderNewNote()} diff --git a/src/panels/NotesPanel.jsx b/src/panels/NotesPanel.jsx index faf041ab47..1b21574d0b 100644 --- a/src/panels/NotesPanel.jsx +++ b/src/panels/NotesPanel.jsx @@ -254,6 +254,12 @@ export default class NotesPanel extends Component { this.props.patient.reAddEntryToPatient(b); }); + this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_created_by_${tempNote.entryInfo.entryId}`); + this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_created_on_${tempNote.entryInfo.entryId}`); + this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_title_${tempNote.entryInfo.entryId}`); + this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_content_${tempNote.entryInfo.entryId}`); + this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_source_${tempNote.entryInfo.entryId}`); + // Close the current note this.closeNote(); } diff --git a/src/patientControl/NoteContentIndexer.js b/src/patientControl/NoteContentIndexer.js index f694df5883..84ec9419b8 100644 --- a/src/patientControl/NoteContentIndexer.js +++ b/src/patientControl/NoteContentIndexer.js @@ -3,8 +3,8 @@ import BaseIndexer from './BaseIndexer' class NoteContentIndexer extends BaseIndexer { indexData(section, subsection, note, searchIndex, onHighlight, onClick) { const notesSubsection = note.signed ? 'Signed Notes' : 'In Progress Notes'; - const notesSectionId = section.toLowerCase().replace(' ', '_'); - const notesSubsectionId = notesSubsection.toLowerCase().replace(' ', '_'); + const notesSectionId = super.getStringForId(section); + const notesSubsectionId = super.getStringForId(notesSubsection); const {entryId} = note.entryInfo; searchIndex.addSearchableData({ id: `${notesSectionId}_${notesSubsectionId}_title_${entryId}`, @@ -23,7 +23,7 @@ class NoteContentIndexer extends BaseIndexer { section, subsection: notesSubsection, valueTitle: `Content`, - value: note.content, + value: this.getNoteContentWithoutStyle(note.content), onHighlight, onClick }) @@ -85,6 +85,19 @@ class NoteContentIndexer extends BaseIndexer { }); } } + + getNoteContentWithoutStyle = (noteContent) => { + let noteContentWithoutStyle = noteContent; + + // Remove HTML tags + noteContentWithoutStyle = noteContentWithoutStyle.replace(/<(div|\/div|strong|\/strong|em|\/em|u|\/u|ul|\/ul|ol|\/ol|li|\/li){0,}>/g, ""); + // Remove brackets from @ structured phrases + noteContentWithoutStyle = noteContentWithoutStyle.replace(/@(.*?)\[\[(.*?)\]\]/g, (match, g1, g2) => g2); + // Removed brackets from # structured phrases + noteContentWithoutStyle = noteContentWithoutStyle.replace(/#(.*?)\[\[(.*?)\]\]/g, (match, g1, g2) => `#${g1}`); + + return noteContentWithoutStyle; + } } export default NoteContentIndexer; \ No newline at end of file diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index 8e10357dee..b070f2e0aa 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -66,10 +66,12 @@ class PatientSearch extends React.Component { valueTitle: result.valueTitle, contentSnapshot: this.getNoteContentWithoutStyle(result.value), source: "clinicalNote", + section: result.section, matchedOn: result.valueTitle === "Content" ? "contentSnapshot" : "valueTitle", onHighlight: result.onHighlight, onClick: result.onClick, - score: result.score + score: result.score, + indices: result.indices } } else { suggestion = { diff --git a/src/patientControl/SearchIndex.js b/src/patientControl/SearchIndex.js index 7e50acff97..65f61ad056 100644 --- a/src/patientControl/SearchIndex.js +++ b/src/patientControl/SearchIndex.js @@ -1,5 +1,6 @@ import Lang from 'lodash'; import elasticlunr from 'elasticlunr'; +import Fuse from 'fuse.js'; class SearchIndex { constructor() { @@ -13,6 +14,11 @@ class SearchIndex { this.setRef('id'); this.saveDocument(true); }); + this._options = { + keys: ['section', 'subsection', 'valueTitle', 'value'], + id: 'id' + }; + this._fuse = null; } get searchableData() { @@ -22,6 +28,21 @@ class SearchIndex { addSearchableData(data) { const existingDocument = this._index.documentStore.getDoc(data.id); if (Lang.isNull(existingDocument) || !Lang.isEqual(data, existingDocument)) { + if (data.section === "Open Note" && data.valueTitle === "Content") { + this._fuse = new Fuse([{ + id: data.id, + content: data.value + }], { + id: 'id', + keys: ['content'], + shouldSort: true, + includeScore: true, + includeMatches: true, + findAllMatches: true, + threshold: 0.6, + distance: data.value.length + }); + } this._index.addDoc({...data}); } } @@ -39,7 +60,8 @@ class SearchIndex { } search(query) { - return this._index.search(query, { + let suggestions = []; + this._index.search(query, { fields: { valueTitle: { expand: true @@ -48,11 +70,26 @@ class SearchIndex { expand: true } } - }).map(result => { + }).forEach(result => { let doc = this._index.documentStore.getDoc(result.ref); doc.score = result.score; - return doc; + if (doc.section === "Open Note") { + const regex = new RegExp(query, "g"); + let contentMatches = this._fuse.search(query); + if (contentMatches.length > 0 && contentMatches[0].matches.length > 0) { + contentMatches[0].matches[0].indices.forEach(([from, to]) => { + if (regex.exec(doc.value.substring(from, to+1).toLowerCase())) { + let tempDoc = Lang.cloneDeep(doc); + tempDoc.indices = [from, to]; + suggestions.unshift(tempDoc); + } + }) + } + } else { + suggestions.push(doc); + } }); + return suggestions; } } diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 275e461703..2b147a7b78 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -8,7 +8,7 @@ class SearchSuggestion extends React.Component { renderSuggestionText() { const { suggestion } = this.props; - const {inputValue, valueTitle, contentSnapshot} = suggestion; + const {inputValue, valueTitle, contentSnapshot, indices} = suggestion; const inputValueLowerCase = inputValue.toLowerCase(); const fullText = `${valueTitle ? valueTitle + ': ' : ''}${contentSnapshot}`; const regex = new RegExp(inputValueLowerCase, "g"); @@ -16,7 +16,11 @@ class SearchSuggestion extends React.Component { const matchesContent = regex.exec(contentSnapshot.toLowerCase()); let preText = '', highlightedText = '', postText = ''; - if (matchesTitle) { + if (indices) { + preText = valueTitle ? valueTitle + ': ' : '' + contentSnapshot.slice(0, indices[0]); + highlightedText = contentSnapshot.slice(indices[0], indices[1]+1); + postText = contentSnapshot.slice(indices[1]+1); + } else if (matchesTitle) { preText = ''; highlightedText = valueTitle; postText = `: ${contentSnapshot}`; diff --git a/yarn.lock b/yarn.lock index c7feea2658..e30a2560ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3781,6 +3781,10 @@ function.prototype.name@^1.0.3: function-bind "^1.1.0" is-callable "^1.1.3" +fuse.js@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.2.1.tgz#6320cb94ce56ec9755c89ade775bcdbb0358d425" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" From 45f841943257ae92314dcc8675e84239b9157d5d Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Tue, 25 Sep 2018 17:12:27 -0400 Subject: [PATCH 12/20] Added basic scrolling on highlight of open note content search results --- src/notes/FluxNotesEditor.jsx | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index 66e808f333..fcb1aec631 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -751,7 +751,7 @@ class FluxNotesEditor extends React.Component { onHighlight: null, onClick: null }); - this.noteContentIndexer.indexData("Open Note", '', nextProps.updatedEditorNote, this.props.searchIndex, null, null); + this.noteContentIndexer.indexData("Open Note", '', nextProps.updatedEditorNote, this.props.searchIndex, this.onOpenNoteSearchResultHighlight, null); } // Check if the current view mode changes @@ -790,6 +790,38 @@ class FluxNotesEditor extends React.Component { } } + onOpenNoteSearchResultHighlight = (suggestion) => { + const {document} = this.state.state; + let startIndex = suggestion.indices[0]; + let foundNode; + document.toJSON().nodes.find(node => { + const nodeLength = this.getLengthOfNode(node); + if (startIndex <= nodeLength) { + foundNode = node; + return true; + } else { + startIndex -= nodeLength; + return false; + } + }); + if (foundNode) this.scrollToData(document, foundNode.key); + } + + getLengthOfNode = (node) => { + let length = 0; + if (node.type === 'line') { + node.nodes.forEach(node => { + length += this.getLengthOfNode(node); + }); + } else if (node.characters) { + length += node.characters.length; + } else if (node.type === 'structured_field') { + let shortcut = node.data.shortcut; + length += shortcut.getText().length; + } + return length; + } + revertTemplate = () => { this.props.changeShortcutType(null, false, null); this.setState({ state: this.previousState }, () => { From ad4de47b39b4dd48f13e57892966ab6f015aabe7 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Tue, 25 Sep 2018 17:12:48 -0400 Subject: [PATCH 13/20] Fixed bug where in progress notes were note scrolled into view properly --- src/notes/NoteAssistant.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 5986510cb5..7c0ae69657 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -333,13 +333,14 @@ export default class NoteAssistant extends Component { renderInProgressNote(note, i) { let selected = Lang.isEqual(this.props.selectedNote, note); + let searchedFor = note.entryInfo.entryId === this.state.searchResultNoteId; // if we have closed the note, selected = false if (Lang.isEqual(this.props.noteClosed, true)) { selected = false; } return ( -
{ +
{ this.openNote(true, note) }}>
In progress note
From a64a532de499dff8997ad64f9aeea0f19eb3ac02 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Tue, 25 Sep 2018 18:52:07 -0400 Subject: [PATCH 14/20] Escape regex --- src/patientControl/SearchIndex.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/patientControl/SearchIndex.js b/src/patientControl/SearchIndex.js index 65f61ad056..0d52efe9b5 100644 --- a/src/patientControl/SearchIndex.js +++ b/src/patientControl/SearchIndex.js @@ -74,7 +74,7 @@ class SearchIndex { let doc = this._index.documentStore.getDoc(result.ref); doc.score = result.score; if (doc.section === "Open Note") { - const regex = new RegExp(query, "g"); + const regex = new RegExp(this.escapeRegExp(query), "g"); let contentMatches = this._fuse.search(query); if (contentMatches.length > 0 && contentMatches[0].matches.length > 0) { contentMatches[0].matches[0].indices.forEach(([from, to]) => { @@ -91,6 +91,10 @@ class SearchIndex { }); return suggestions; } + + escapeRegExp(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + } } export default SearchIndex; \ No newline at end of file From 92cb39eeabd5e904386ea8b39a60ed3408e22968 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Thu, 27 Sep 2018 10:23:06 -0400 Subject: [PATCH 15/20] Fixed test to properly add clinical note to a patient and pass the searchInex as a prop to FluxNotesEditor --- .../patientControl/PatientControl.test.js | 43 ++++++++++--------- test/backend/views/FullApp.test.js | 38 +++++++++++----- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/test/backend/patientControl/PatientControl.test.js b/test/backend/patientControl/PatientControl.test.js index c93704884f..5cbfa9a4fb 100644 --- a/test/backend/patientControl/PatientControl.test.js +++ b/test/backend/patientControl/PatientControl.test.js @@ -94,28 +94,29 @@ describe('PatientSearch', function () { .to.have.lengthOf.at.least(expectedLength) }); - it('Should fire setFullAppState when you click on a suggestions ', function () { + // Since we are no longer opening source notes on clicking search results, I am disabling this test for now. We might need it later. + // it('Should fire setFullAppState when you click on a suggestions ', function () { // We'll expect this to be true after clicking on a suggestion - didSetFullAppStateFunctionTrigger = false; - const wrapper = mount(); - const inputField = wrapper.find('input.react-autosuggest__input') - inputField.simulate('change', { target: {value: inputValue}}); - inputField.simulate('focus'); - // Assumes we're testing against TestPatient - expect(testPatientObj.shrId) - .to.equal(testPatientShrId) + // didSetFullAppStateFunctionTrigger = false; + // const wrapper = mount(); + // const inputField = wrapper.find('input.react-autosuggest__input') + // inputField.simulate('change', { target: {value: inputValue}}); + // inputField.simulate('focus'); + // // Assumes we're testing against TestPatient + // expect(testPatientObj.shrId) + // .to.equal(testPatientShrId) - const suggestionList = wrapper.find('li.react-autosuggest__suggestion'); - const firstSuggestion = suggestionList.first() - firstSuggestion.simulate('click'); + // const suggestionList = wrapper.find('li.react-autosuggest__suggestion'); + // const firstSuggestion = suggestionList.first() + // firstSuggestion.simulate('click'); - // After clicking this should be true - expect(didSetFullAppStateFunctionTrigger) - .to.be.true - }); + // // After clicking this should be true + // expect(didSetFullAppStateFunctionTrigger) + // .to.be.true + // }); }); \ No newline at end of file diff --git a/test/backend/views/FullApp.test.js b/test/backend/views/FullApp.test.js index f87f77b2b6..4e7d1c95df 100644 --- a/test/backend/views/FullApp.test.js +++ b/test/backend/views/FullApp.test.js @@ -304,6 +304,7 @@ describe('6 FluxNotesEditor', function() { const contextManager = new ContextManager(patient, () => {}); const shortcutManager = new ShortcutManager(); const structuredFieldMapManager = new StructuredFieldMapManager(); + const searchIndex = new SearchIndex(); // Mock function to create a new shortcut and set text on shortcut. Allows Editor to update correctly. let mockNewCurrentShortcut = (shortcutC, shortcutType, shortcutData, updatePatient = true) => { @@ -321,6 +322,7 @@ describe('6 FluxNotesEditor', function() { structuredFieldMapManager={structuredFieldMapManager} newCurrentShortcut={mockNewCurrentShortcut} updatedEditorNote={null} + searchIndex={searchIndex} handleUpdateEditorWithNote={jest.fn()} isNoteViewerVisible={true} isNoteViewerEditable={true} @@ -346,9 +348,9 @@ describe('6 FluxNotesEditor', function() { />); expect(wrapper).to.exist; // wrapper.find('.editor-content').simulate('click'); //goes into on change - let noteContent = '@name[[Test Name]] is a @age[[49]] year old @gender[[Female]] coming in for follow up.'; - const updatedEditorNote = { content: noteContent }; + const entryId = patient.addClinicalNote('', '', '', '', '', noteContent, false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. wrapper.setProps({ updatedEditorNote }); @@ -488,8 +490,10 @@ describe('6 FluxNotesEditor', function() { { attachTo: document.body }); expect(notesPanelWrapper).to.have.lengthOf(1); expect(notesPanelWrapper.find(FluxNotesEditor)).to.have.lengthOf(1); - const note = new FluxClinicalNote({ EntryId: "7000", content: '@name' }); - notesPanelWrapper.setState({ updatedEditorNote: note }); + + const entryId = patient.addClinicalNote('', '', '', '', '', '@name', false); + const updatedEditorNote = patient.getEntryById(entryId); + notesPanelWrapper.setState({ updatedEditorNote }); expect(notesPanelWrapper.find('.structured-field-inserter')).to.have.length(1); expect(notesPanelWrapper.find('.structured-field-inserter').text()).to.contain(patient.getName()); @@ -565,6 +569,7 @@ describe('6 FluxNotesEditor', function() { const contextManager = new ContextManager(patient, () => {}); const shortcutManager = new ShortcutManager(); const structuredFieldMapManager = new StructuredFieldMapManager(); + const searchIndex = new SearchIndex(); // Mock function to create a new shortcut and set text on shortcut. Allows Editor to update correctly. let mockNewCurrentShortcut = (shortcutC, shortcutType, shortcutData, updatePatient = true) => { @@ -581,6 +586,7 @@ describe('6 FluxNotesEditor', function() { structuredFieldMapManager={structuredFieldMapManager} newCurrentShortcut={mockNewCurrentShortcut} updatedEditorNote={null} + searchIndex={searchIndex} handleUpdateEditorWithNote={jest.fn()} isNoteViewerVisible={true} isNoteViewerEditable={true} @@ -609,7 +615,8 @@ describe('6 FluxNotesEditor', function() { // let noteContent = '#imaging '; const arrayOfStructuredDataToEnter = ["#imaging "] - const updatedEditorNote = { content: arrayOfStructuredDataToEnter.join(' ') }; + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfStructuredDataToEnter.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. wrapper.instance().onFocus(); wrapper.setProps({ updatedEditorNote }); @@ -629,6 +636,7 @@ describe('6 FluxNotesEditor', function() { const contextManager = new ContextManager(patient, () => {}); const shortcutManager = new ShortcutManager(); const structuredFieldMapManager = new StructuredFieldMapManager(); + const searchIndex = new SearchIndex(); // Mock function to create a new shortcut and set text on shortcut. Allows Editor to update correctly. let mockNewCurrentShortcut = (shortcutC, shortcutType, shortcutData, updatePatient = true) => { @@ -645,6 +653,7 @@ describe('6 FluxNotesEditor', function() { structuredFieldMapManager={structuredFieldMapManager} newCurrentShortcut={mockNewCurrentShortcut} updatedEditorNote={null} + searchIndex={searchIndex} handleUpdateEditorWithNote={jest.fn()} isNoteViewerVisible={true} isNoteViewerEditable={true} @@ -675,7 +684,8 @@ describe('6 FluxNotesEditor', function() { const arrayOfStructuredDataToEnter = ["@condition[[Invasive ductal carcinoma of breast]] ", "#staging ", "t2 ", "n2 ", "m1 "]; const arrayOfExpectedStructuredDataInserters = ["Invasive ductal carcinoma of breast "]; const arrayOfExpectedStructuredDataCreators = ["staging ", "t2 ", "n2 ", "m1 "]; - const updatedEditorNote = { content: arrayOfStructuredDataToEnter.join(' ') }; + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfStructuredDataToEnter.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. wrapper.setProps({ updatedEditorNote }); @@ -707,6 +717,7 @@ describe('6 FluxNotesEditor', function() { const contextManager = new ContextManager(patient, () => {}); const shortcutManager = new ShortcutManager(); const structuredFieldMapManager = new StructuredFieldMapManager(); + const searchIndex = new SearchIndex(); // Mock function to create a new shortcut and set text on shortcut. Allows Editor to update correctly. let mockNewCurrentShortcut = (shortcutC, shortcutType, shortcutData, updatePatient = true) => { @@ -723,6 +734,7 @@ describe('6 FluxNotesEditor', function() { structuredFieldMapManager={structuredFieldMapManager} newCurrentShortcut={mockNewCurrentShortcut} updatedEditorNote={null} + searchIndex={searchIndex} handleUpdateEditorWithNote={jest.fn()} isNoteViewerVisible={true} isNoteViewerEditable={true} @@ -752,7 +764,8 @@ describe('6 FluxNotesEditor', function() { // let noteContent = ' #staging t2 n2 m1'; const arrayOfStructuredDataToEnter = ["#12/20/2015 "]; const arrayOfExpectedStructuredData = ["12/20/2015 "]; - const updatedEditorNote = { content: arrayOfStructuredDataToEnter.join(' ') }; + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfStructuredDataToEnter.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. wrapper.setProps({ updatedEditorNote }); @@ -819,7 +832,8 @@ describe('6 FluxNotesEditor', function() { expect(notesPanelWrapper.find(NoteAssistant)).to.have.lengthOf(1); const arrayOfStructuredDataToEnter = ["#deceased "]; - const updatedEditorNote = new FluxClinicalNote({ EntryId: "7000",content: arrayOfStructuredDataToEnter.join(' ') }); + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfStructuredDataToEnter.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. fluxNotesEditor.instance().onFocus(); //fluxNotesEditor.setProps({ updatedEditorNote }); @@ -885,7 +899,8 @@ describe('6 FluxNotesEditor', function() { const arrayOfStructuredDataToEnter = ["@condition[[Invasive ductal carcinoma of breast]] ", "#PR ", "#Positive "]; const arrayOfExpectedStructuredDataInserter = ["Invasive ductal carcinoma of breast "] const arrayOfExpectedStructuredDataCreator = ["PR ", "Positive "] - const updatedEditorNote = new FluxClinicalNote({ EntryId: "7000", content: arrayOfStructuredDataToEnter.join(' ') }); + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfStructuredDataToEnter.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. fluxNotesEditor.instance().onFocus(); //fluxNotesEditor.setProps({ updatedEditorNote }); @@ -1053,6 +1068,7 @@ describe('6 FluxNotesEditor', function() { const contextManager = new ContextManager(patient, () => {}); const shortcutManager = new ShortcutManager(); const structuredFieldMapManager = new StructuredFieldMapManager(); + const searchIndex = new SearchIndex(); // Mock function to create a new shortcut and set text on shortcut. Allows Editor to update correctly. let mockNewCurrentShortcut = (shortcutC, shortcutType, shortcutData, updatePatient = true) => { @@ -1069,6 +1085,7 @@ describe('6 FluxNotesEditor', function() { structuredFieldMapManager={structuredFieldMapManager} newCurrentShortcut={mockNewCurrentShortcut} updatedEditorNote={null} + searchIndex={searchIndex} handleUpdateEditorWithNote={jest.fn()} isNoteViewerVisible={true} isNoteViewerEditable={true} @@ -1099,7 +1116,8 @@ describe('6 FluxNotesEditor', function() { const arrayOfShortcutText = ["@condition[[Invasive ductal carcinoma of breast]] ", "#toxicity ", "#nausea ", "#disease status ", "#imaging "]; const arrayOfParsedShortcutTextInserter = ["Invasive ductal carcinoma of breast "] const arrayOfParsedShortcutTextCreator = ["toxicity ", "nausea ", "disease status ", "imaging "] - const updatedEditorNote = { content: arrayOfShortcutText.join(' ') }; + const entryId = patient.addClinicalNote('', '', '', '', '', arrayOfShortcutText.join(' '), false); + const updatedEditorNote = patient.getEntryById(entryId); // Set updatedEditorNote props because this triggers that a change is coming in to the editor and inserts text with structured phrases. wrapper.setProps({ updatedEditorNote }); From f10cbdb585e7c515cd8fa68c04ac8b9eb7f64695 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 1 Oct 2018 09:10:57 -0400 Subject: [PATCH 16/20] Added open note prefix to search suggestions that are for an open clinical note --- src/patientControl/SearchSuggestion.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 2b147a7b78..8b06a05e68 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -16,6 +16,7 @@ class SearchSuggestion extends React.Component { const matchesContent = regex.exec(contentSnapshot.toLowerCase()); let preText = '', highlightedText = '', postText = ''; + // If there are indices, we matched on an open note if (indices) { preText = valueTitle ? valueTitle + ': ' : '' + contentSnapshot.slice(0, indices[0]); highlightedText = contentSnapshot.slice(indices[0], indices[1]+1); @@ -59,6 +60,7 @@ class SearchSuggestion extends React.Component { suggestionLabel = (
+ {suggestion.section === "Open Note" ? "Open Note > " : ''} {subject} {date} From 32fc0265f72b9f646d727c50ce8ee39b8bcdeafe Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 1 Oct 2018 10:15:30 -0400 Subject: [PATCH 17/20] Don't index note assistant content that is in an open note --- src/notes/FluxNotesEditor.jsx | 2 ++ src/panels/NotesPanel.jsx | 1 + src/patientControl/NotesIndexer.js | 6 ++++-- src/patientControl/SearchIndex.js | 4 ++++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/notes/FluxNotesEditor.jsx b/src/notes/FluxNotesEditor.jsx index fcb1aec631..e280b7cdb8 100644 --- a/src/notes/FluxNotesEditor.jsx +++ b/src/notes/FluxNotesEditor.jsx @@ -742,6 +742,7 @@ class FluxNotesEditor extends React.Component { } } this.props.searchIndex.removeDataBySection('Open Note'); + const sectionId = nextProps.updatedEditorNote.signed ? 'signed_notes' : 'in_progress_notes'; this.props.searchIndex.addSearchableData({ id: 'open_note_section', section: 'Open Note', @@ -752,6 +753,7 @@ class FluxNotesEditor extends React.Component { onClick: null }); this.noteContentIndexer.indexData("Open Note", '', nextProps.updatedEditorNote, this.props.searchIndex, this.onOpenNoteSearchResultHighlight, null); + this.props.searchIndex.removeDataByRef(`clinical_notes_${sectionId}_content_${nextProps.updatedEditorNote.entryInfo.entryId}`); } // Check if the current view mode changes diff --git a/src/panels/NotesPanel.jsx b/src/panels/NotesPanel.jsx index 1b21574d0b..0b5d7550a0 100644 --- a/src/panels/NotesPanel.jsx +++ b/src/panels/NotesPanel.jsx @@ -254,6 +254,7 @@ export default class NotesPanel extends Component { this.props.patient.reAddEntryToPatient(b); }); + this.props.searchIndex.removeDataBySection('Open Note'); this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_created_by_${tempNote.entryInfo.entryId}`); this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_created_on_${tempNote.entryInfo.entryId}`); this.props.searchIndex.removeDataByRef(`clinical_notes_in_progress_notes_title_${tempNote.entryInfo.entryId}`); diff --git a/src/patientControl/NotesIndexer.js b/src/patientControl/NotesIndexer.js index f526cb069b..efdb7f6dbd 100644 --- a/src/patientControl/NotesIndexer.js +++ b/src/patientControl/NotesIndexer.js @@ -12,7 +12,7 @@ class NotesIndexer extends NoteContentIndexer { }); searchIndex.addSearchableData({ - id: `${noteSectionId}_signed_notes}`, + id: `${noteSectionId}_signed_notes`, section, subsection: 'Signed Notes', valueTitle: 'Subsection', @@ -20,7 +20,7 @@ class NotesIndexer extends NoteContentIndexer { }); searchIndex.addSearchableData({ - id: `${noteSectionId}_in_progress_notes}`, + id: `${noteSectionId}_in_progress_notes`, section, subsection: 'In Progress Notes', valueTitle: 'Subsection', @@ -28,6 +28,8 @@ class NotesIndexer extends NoteContentIndexer { }); data.forEach(note => { + const subsectionId = note.signed ? 'signed_notes' : 'in_progress_notes'; + if (searchIndex.hasDocument(`open_note_${subsectionId}_content_${note.entryInfo.entryId}`)) return; super.indexData(section, subsection, note, searchIndex, onHighlight, onClick); }); } diff --git a/src/patientControl/SearchIndex.js b/src/patientControl/SearchIndex.js index 0d52efe9b5..63287f4fea 100644 --- a/src/patientControl/SearchIndex.js +++ b/src/patientControl/SearchIndex.js @@ -59,6 +59,10 @@ class SearchIndex { this._index.removeDocByRef(ref); } + hasDocument(ref) { + return this._index.documentStore.hasDoc(ref); + } + search(query) { let suggestions = []; this._index.search(query, { From f1cc01f8caa48467c6c439968f1c558ad92c98e0 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 1 Oct 2018 11:37:51 -0400 Subject: [PATCH 18/20] Added mouseleave event to unhighlight note in noteassistant when necessary --- .gitignore | 1 + src/notes/NoteAssistant.jsx | 24 +++++++----- src/patientControl/PatientSearch.scss | 2 +- src/patientControl/SearchSuggestion.css | 44 --------------------- src/patientControl/SearchSuggestion.jsx | 9 ++++- src/patientControl/SearchSuggestion.scss | 49 ++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 56 deletions(-) delete mode 100644 src/patientControl/SearchSuggestion.css create mode 100644 src/patientControl/SearchSuggestion.scss diff --git a/.gitignore b/.gitignore index bafa5c4eb9..da31e69462 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* /src/summary/NarrativeNameValuePairsVisualizer.css /src/timeline/Timeline.css /src/timeline/TimelineEventsVisualizer.css +/src/patientControl/SearchSuggestion.css #Visual Paradigm Project Files *.vpp.bak* diff --git a/src/notes/NoteAssistant.jsx b/src/notes/NoteAssistant.jsx index 7c0ae69657..c438b74def 100644 --- a/src/notes/NoteAssistant.jsx +++ b/src/notes/NoteAssistant.jsx @@ -222,16 +222,22 @@ export default class NoteAssistant extends Component { this.props.deleteSelectedNote(); } - onSearchSuggestionHighlighted = (suggestion) => { + onSearchSuggestionHighlighted = (suggestion, shouldReset=false) => { this.toggleView('clinical-notes'); - this.setState({ - searchResultNoteId: suggestion.note.entryInfo.entryId - }, () => { - const domNodeRef = this.refs[suggestion.note.entryInfo.entryId]; - if (domNodeRef && domNodeRef.scrollIntoView) { - domNodeRef.scrollIntoView(); - } - }); + if(shouldReset) { + this.setState({ + searchResultNoteId: null + }); + } else { + this.setState({ + searchResultNoteId: suggestion.note.entryInfo.entryId + }, () => { + const domNodeRef = this.refs[suggestion.note.entryInfo.entryId]; + if (domNodeRef && domNodeRef.scrollIntoView) { + domNodeRef.scrollIntoView(); + } + }); + } } onSearchSuggestionClicked = (suggestion) => { diff --git a/src/patientControl/PatientSearch.scss b/src/patientControl/PatientSearch.scss index 25cb9b6246..96b56eecd6 100644 --- a/src/patientControl/PatientSearch.scss +++ b/src/patientControl/PatientSearch.scss @@ -90,7 +90,7 @@ .react-autosuggest__suggestion { cursor: pointer; - padding: 10px; + padding: 0px !important; } .react-autosuggest__suggestion--highlighted { diff --git a/src/patientControl/SearchSuggestion.css b/src/patientControl/SearchSuggestion.css deleted file mode 100644 index 4fd915df7f..0000000000 --- a/src/patientControl/SearchSuggestion.css +++ /dev/null @@ -1,44 +0,0 @@ -.suggestion-item { - display: block; - align-items: center; -} - -.suggestion-label { - min-width: 80px; - display: block; - font-size: 10px; - width: 80px; - padding: 5px auto; -} - -.suggestion-label .label-content { - margin: 0; - display: inline-block; - white-space: nowrap; - line-height: 1.2rem; - text-overflow: ellipsis; -} - -.dividing-line:after { - background: #b1b1b1; - width: 1px; - content: ""; - display: inline-block; - top: 0; - bottom: 0; - right: 0; - min-height: 25px; - margin: -5px 10px -5px 10px; -} - -.suggestion-text { - font-size: 14px; - display: block; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.highlightedInputValue{ - font-weight: 600; -} \ No newline at end of file diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 8b06a05e68..8aa62c449a 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -6,6 +6,11 @@ import './SearchSuggestion.css'; class SearchSuggestion extends React.Component { + onMouseLeave = () => { + const {suggestion} = this.props; + if (suggestion.onHighlight) suggestion.onHighlight(suggestion, true); + } + renderSuggestionText() { const { suggestion } = this.props; const {inputValue, valueTitle, contentSnapshot, indices} = suggestion; @@ -36,7 +41,7 @@ class SearchSuggestion extends React.Component { return (
{preText} - {highlightedText} + {highlightedText} {postText}
) @@ -73,7 +78,7 @@ class SearchSuggestion extends React.Component { render() { return ( -
+
{this.renderLabel()} {this.renderSuggestionText()}
diff --git a/src/patientControl/SearchSuggestion.scss b/src/patientControl/SearchSuggestion.scss new file mode 100644 index 0000000000..8de5f6c6f5 --- /dev/null +++ b/src/patientControl/SearchSuggestion.scss @@ -0,0 +1,49 @@ +@import '../styles/variables'; + +.suggestion { + &-item { + display: block; + align-items: center; + padding: 10px; + } + + &-label { + min-width: 80px; + display: block; + font-size: 10px; + width: 80px; + padding: 5px auto; + + & .label-content { + margin: 0; + display: inline-block; + white-space: nowrap; + line-height: 1.2rem; + text-overflow: ellipsis; + } + } + + &-text { + font-size: 14px; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.highlighted-input-value { + font-weight: 600; +} + +.dividing-line:after { + background: #b1b1b1; + width: 1px; + content: ""; + display: inline-block; + top: 0; + bottom: 0; + right: 0; + min-height: 25px; + margin: -5px 10px -5px 10px; +} \ No newline at end of file From 12a539b037ded20e26dc8cd07213afefa80ac4c6 Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 1 Oct 2018 12:10:26 -0400 Subject: [PATCH 19/20] Unhighlight the previous suggestion after new suggestion is highlighted --- src/patientControl/PatientSearch.jsx | 9 ++++++++- src/patientControl/SearchSuggestion.jsx | 7 +------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index b070f2e0aa..45a933d381 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -21,7 +21,8 @@ class PatientSearch extends React.Component { // and they are initially empty because the Autosuggest is closed. this.state = { suggestions: [], - value: '' + value: '', + previousSuggestion: null }; } @@ -134,9 +135,15 @@ class PatientSearch extends React.Component { } onSuggestionHighlighted = ({suggestion}) => { + const {previousSuggestion} = this.state; + if (previousSuggestion && previousSuggestion.onHighlight) previousSuggestion.onHighlight(previousSuggestion, true); if (!Lang.isNull(suggestion) && suggestion.onHighlight) { suggestion.onHighlight(suggestion); } + + this.setState({ + previousSuggestion: suggestion + }); } // When the input is focused, Autosuggest will consult this function when to render suggestions diff --git a/src/patientControl/SearchSuggestion.jsx b/src/patientControl/SearchSuggestion.jsx index 8aa62c449a..ccbacbfb8e 100644 --- a/src/patientControl/SearchSuggestion.jsx +++ b/src/patientControl/SearchSuggestion.jsx @@ -6,11 +6,6 @@ import './SearchSuggestion.css'; class SearchSuggestion extends React.Component { - onMouseLeave = () => { - const {suggestion} = this.props; - if (suggestion.onHighlight) suggestion.onHighlight(suggestion, true); - } - renderSuggestionText() { const { suggestion } = this.props; const {inputValue, valueTitle, contentSnapshot, indices} = suggestion; @@ -78,7 +73,7 @@ class SearchSuggestion extends React.Component { render() { return ( -
+
{this.renderLabel()} {this.renderSuggestionText()}
From ad9bec675e05c3bd303e8ee941c6f43ceaf228bd Mon Sep 17 00:00:00 2001 From: Matt Gramigna Date: Mon, 1 Oct 2018 13:42:46 -0400 Subject: [PATCH 20/20] Persist note highlighting --- src/patientControl/PatientSearch.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/patientControl/PatientSearch.jsx b/src/patientControl/PatientSearch.jsx index 45a933d381..5f6c99df06 100644 --- a/src/patientControl/PatientSearch.jsx +++ b/src/patientControl/PatientSearch.jsx @@ -131,6 +131,7 @@ class PatientSearch extends React.Component { suggestion.onClick(suggestion); } + this.setState({ previousSuggestion: null }); this.refs.autosuggest.input.blur(); }