Skip to content

Commit 4750fd8

Browse files
authored
DRYD-2144 Authority Fields > Predictive Search Result Messaging (#28)
1 parent ea3a5b2 commit 4750fd8

5 files changed

Lines changed: 127 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Change Log
22

3+
## 2.3.0
4+
5+
* Add formatNarrowResultsMessage prop to AutocompleteInput, shown in place of the match count when a partial term search returns a truncated result list
6+
37
## 2.2.0
48

59
* Resolve orphaned/missing form labels by forwarding id and aria attributes to rendered inputs, and adding label props to QuickSearchInput, RepeatingInput, TabularCompoundInput, and UploadInput

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cspace-input",
3-
"version": "2.2.0",
3+
"version": "2.3.0",
44
"description": "CollectionSpace input components",
55
"author": "Ray Lee <ray.lee@lyrasis.org>",
66
"license": "ECL-2.0",

src/components/AutocompleteInput.jsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const propTypes = {
3636
formatCloneOptionLabel: PropTypes.func,
3737
formatCreateNewOptionLabel: PropTypes.func,
3838
formatMoreCharsRequiredMessage: PropTypes.func,
39+
formatNarrowResultsMessage: PropTypes.func,
3940
formatSearchResultMessage: PropTypes.func,
4041
formatSourceName: PropTypes.func,
4142
matchFilter: PropTypes.func,
@@ -60,6 +61,7 @@ const defaultProps = {
6061
formatCloneOptionLabel: undefined,
6162
formatCreateNewOptionLabel: undefined,
6263
formatMoreCharsRequiredMessage: () => 'Continue typing to find matching terms',
64+
formatNarrowResultsMessage: () => 'Continue typing to narrow results',
6365
formatSearchResultMessage: (count) => {
6466
const matches = count === 1 ? 'matching term' : 'matching terms';
6567
const num = count === 0 ? 'No' : count;
@@ -167,6 +169,50 @@ const isPending = (sourceID, matches, partialTerm) => {
167169
return foundPending;
168170
};
169171

172+
const isSearchTruncated = (sourceID, matches, partialTerm) => {
173+
const sources = parseResourceID(sourceID);
174+
let foundTruncated = false;
175+
176+
if (matches) {
177+
const partialTermMatch = matches.get(partialTerm);
178+
179+
if (partialTermMatch) {
180+
sources.forEach((source) => {
181+
const {
182+
recordType,
183+
vocabulary,
184+
} = source;
185+
186+
const sourceMatch = partialTermMatch.getIn([recordType, vocabulary]);
187+
188+
if (sourceMatch) {
189+
const items = sourceMatch.get('items');
190+
const pageSize = sourceMatch.get('pageSize');
191+
const totalItems = sourceMatch.get('totalItems');
192+
193+
if (items) {
194+
// The search service reports at most one page of matches in totalItems, even when
195+
// more terms match.https://collectionspace.atlassian.net/browse/DRYD-2157
196+
// A source is considered truncated if more matches are reported
197+
// than were returned, or if a full page was returned, since in that case there are
198+
// likely more matches than the server is willing to report.
199+
// TODO: update condition when DRYD-2157 is fixed
200+
201+
if (
202+
(typeof totalItems === 'number' && totalItems > items.length)
203+
|| (typeof pageSize === 'number' && pageSize > 0 && items.length >= pageSize)
204+
) {
205+
foundTruncated = true;
206+
}
207+
}
208+
}
209+
});
210+
}
211+
}
212+
213+
return foundTruncated;
214+
};
215+
170216
const getNewTerm = (sourceID, matches, partialTerm) => {
171217
const sources = parseResourceID(sourceID);
172218

@@ -272,6 +318,7 @@ export default class AutocompleteInput extends Component {
272318

273319
if (!isPending(nextProps.source, nextProps.matches, partialTerm)) {
274320
nextState.options = getOptions(partialTerm, nextProps);
321+
nextState.optionsPartialTerm = partialTerm;
275322
}
276323

277324
this.setState(nextState);
@@ -351,6 +398,7 @@ export default class AutocompleteInput extends Component {
351398
commit(value, meta) {
352399
this.setState({
353400
options: [],
401+
optionsPartialTerm: null,
354402
partialTerm: null,
355403
value,
356404
});
@@ -404,6 +452,7 @@ export default class AutocompleteInput extends Component {
404452
}, findDelay);
405453
} else {
406454
newState.options = getOptions(partialTerm, this.props);
455+
newState.optionsPartialTerm = partialTerm;
407456
}
408457

409458
newState.isFindTimerActive = !!this.findTimer;
@@ -509,6 +558,7 @@ export default class AutocompleteInput extends Component {
509558
formatCloneOptionLabel,
510559
formatCreateNewOptionLabel,
511560
formatMoreCharsRequiredMessage,
561+
formatNarrowResultsMessage,
512562
formatSearchResultMessage,
513563
formatSourceName,
514564
matches,
@@ -527,6 +577,7 @@ export default class AutocompleteInput extends Component {
527577
const {
528578
isFindTimerActive,
529579
options,
580+
optionsPartialTerm,
530581
partialTerm,
531582
value,
532583
} = this.state;
@@ -541,9 +592,15 @@ export default class AutocompleteInput extends Component {
541592
&& removePartialTermOperators(partialTerm).length < minLength
542593
);
543594

544-
const formatStatusMessage = moreCharsRequired
545-
? formatMoreCharsRequiredMessage
546-
: formatSearchResultMessage;
595+
let formatStatusMessage;
596+
597+
if (moreCharsRequired) {
598+
formatStatusMessage = formatMoreCharsRequiredMessage;
599+
} else if (isSearchTruncated(source, matches, optionsPartialTerm)) {
600+
formatStatusMessage = formatNarrowResultsMessage;
601+
} else {
602+
formatStatusMessage = formatSearchResultMessage;
603+
}
547604

548605
const className = (isFindTimerActive || isPending(source, matches, partialTerm))
549606
? styles.searching

test/specs/components/AutocompleteInput.spec.jsx

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,66 @@ describe('AutocompleteInput', () => {
272272
});
273273
});
274274

275+
it('should show the search result message when all matching terms are returned', function test() {
276+
const matches = johMatches
277+
.setIn(['joh', 'person', 'local', 'pageSize'], 40)
278+
.setIn(['joh', 'person', 'local', 'totalItems'], 1);
279+
280+
render(
281+
<AutocompleteInput
282+
source="person/local"
283+
matches={matches}
284+
recordTypes={recordTypes}
285+
/>, this.container,
286+
);
287+
288+
const input = this.container.querySelector('input');
289+
290+
input.value = 'joh';
291+
292+
Simulate.change(input);
293+
294+
return new Promise((resolve) => {
295+
window.setTimeout(() => {
296+
const menuHeader = this.container.querySelector('.cspace-layout-Popup--common > header');
297+
298+
menuHeader.textContent.should.match(/^2 matching terms found/);
299+
300+
resolve();
301+
}, findTestDelay);
302+
});
303+
});
304+
305+
it('should show a continue typing to narrow results message when there are more matching terms than returned items', function test() {
306+
const matches = johMatches
307+
.setIn(['joh', 'person', 'local', 'pageSize'], 40)
308+
.setIn(['joh', 'person', 'local', 'totalItems'], 63);
309+
310+
render(
311+
<AutocompleteInput
312+
source="person/local"
313+
matches={matches}
314+
recordTypes={recordTypes}
315+
/>, this.container,
316+
);
317+
318+
const input = this.container.querySelector('input');
319+
320+
input.value = 'joh';
321+
322+
Simulate.change(input);
323+
324+
return new Promise((resolve) => {
325+
window.setTimeout(() => {
326+
const menuHeader = this.container.querySelector('.cspace-layout-Popup--common > header');
327+
328+
menuHeader.textContent.should.match(/^Continue typing to narrow results/);
329+
330+
resolve();
331+
}, findTestDelay);
332+
});
333+
});
334+
275335
it('should call findMatchingTerms when a partial term is entered that does not exist in matches', function test() {
276336
let findSource = null;
277337
let findPartialTerm = null;

0 commit comments

Comments
 (0)