generated from scale-tone/react-ts-basic
/
DetailsDialogState.ts
190 lines (136 loc) · 5.74 KB
/
DetailsDialogState.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
import { observable, computed } from 'mobx'
import axios from 'axios';
import { ErrorMessageState } from './ErrorMessageState';
import { SearchResult } from './SearchResult';
const BackendUri = process.env.REACT_APP_BACKEND_BASE_URI as string;
// Enum describing tabs on the Details dialog
export enum DetailsTabEnum {
Transcript = 0,
Metadata,
Map
}
// A pair of positions in a text
interface ITextInterval {
start: number;
stop: number;
}
// Represents a fragment inside document's text
interface ITextFragment {
readonly text: ITextInterval;
readonly textBefore?: ITextInterval;
readonly textAfter?: ITextInterval;
}
// Num of symbols to take before and after the search keyword
const TextFragmentLength = 100;
// State of the Details dialog
export class DetailsDialogState extends ErrorMessageState {
// Tab currently selected
@observable
selectedTab: DetailsTabEnum = DetailsTabEnum.Transcript;
// Raw text split into fragments like <some-text><search-keyword><some-more-text><another-search-keyword><etc>
@computed
get textFragments(): ITextFragment[] {
if (this.searchWords.length <= 0) {
return [{ text: { start: 0, stop: this._text.length } }];
}
const results: ITextFragment[] = []
var prevIndex = 0;
// searching for any of search keywords...
const regex = new RegExp(this.searchWords.join('|'), 'gi');
var match: RegExpExecArray | null;
while (!!(match = regex.exec(this._text))) {
const keyword = { start: match.index, stop: match.index + match[0].length };
if (keyword.start > prevIndex) {
results.push({ text: { start: prevIndex, stop: keyword.start } });
}
// A fragment with textBefore and textAfter denotes a keyword (which is to be highlighted by markup)
results.push({
textBefore: { start: keyword.start - TextFragmentLength, stop: keyword.start },
text: keyword,
textAfter: { start: keyword.stop, stop: keyword.stop + TextFragmentLength }
});
prevIndex = keyword.stop;
}
if (this._text.length > prevIndex) {
results.push({ text: { start: prevIndex, stop: this._text.length } });
}
return results;
}
// Progress flag
@computed
get inProgress(): boolean { return this._inProgress; }
// Document's display name
@computed
get name(): string { return this._searchResult.name; }
// Document's coordinates
@computed
get coordinates(): number[] { return !!this._details && this._details[this._geoLocationFieldName]?.coordinates; }
// All document's properties
@computed
get details(): any { return this._details; }
// Search query split into words (for highlighting)
readonly searchWords: string[];
constructor(searchQuery: string, private _searchResult: SearchResult, private _geoLocationFieldName: string, transcriptFieldNames: string) {
super();
this.searchWords = this.extractSearchWords(searchQuery, this._searchResult);
axios.get(`${BackendUri}/lookup/${_searchResult.key}`).then(lookupResponse => {
this._details = lookupResponse.data;
// Aggregating all document fields to display them in Transcript view
this._text = this.collectAllTextFields(this._details, transcriptFieldNames);
}, err => {
this.ShowError(`Failed to load details. ${err}`);
}).finally(() => {
this._inProgress = false;
});
}
// Returns a piece of text within specified boundaries
getPieceOfText(interval: ITextInterval): string {
const start = interval.start > 0 ? interval.start : 0;
const stop = interval.stop > this._text.length ? this._text.length : interval.stop;
return this._text.slice(start, stop);
}
@observable
private _details: any;
@observable
private _inProgress: boolean = true;
private _text: string = '';
private extractSearchWords(searchQuery: string, searchResult: SearchResult): string[] {
const results: string[] = [];
// Also adding highlighted words returned by Cognitive Search, if any
for (const highlightedWord of searchResult.highlightedWords) {
if (!results.includes[highlightedWord]) {
results.push(highlightedWord);
}
}
// Skipping search query operators
const queryOperators = ["and", "or"];
const regex = /\w+/gi
var match: RegExpExecArray | null;
while (!!(match = regex.exec(searchQuery))) {
const word = match[0];
if (!queryOperators.includes(word.toLowerCase()) && !results.includes(word)) {
results.push(word);
}
}
return results;
}
private collectAllTextFields(details: any, transcriptFieldNames: string): string {
var result = '';
// If CognitiveSearchTranscriptFields is defined, then using it.
// Otherwise just aggregating all fields that look like string.
if (!transcriptFieldNames) {
for (const fieldName in details) {
const fieldValue = details[fieldName];
if (typeof fieldValue === 'string' && !fieldValue.includes('$metadata#docs')) {
result += fieldValue + '\n';
}
}
} else {
for (const fieldName of transcriptFieldNames.split(',')) {
const fieldValue = details[fieldName];
result += (typeof fieldValue === 'string' ? fieldValue : JSON.stringify(fieldValue)) + '\n';
}
}
return result;
}
}