Filter requests by folder, URI, method, query string (#797)

* Quick Switch matching for Request URL and Method

Previously only Request Name was searched for in Quick Switch window.
This adds support for searching Request URL and Method as well.
A fuzzyMatchAll function has been added to be able to search different
fields in any
order, space delimited.

* Include request parameters in searchable fields

* Allow searching requests by folder paths

* More descriptive placeholder for Quick Switch modal search input

* Update sidebar filter to match Quick Switch, allowing URL and Query String matching

* More descriptive placeholder for sidebar search

* Unit tests for fuzzyMatch and fuzzyMatchAll

* More unit tests for fuzzyMatch and fuzzyMatchAll

* minor refactorings
This commit is contained in:
Shawn Axsom
2018-03-28 16:57:05 -04:00
committed by Gregory Schier
parent dc0695de23
commit bee9973839
5 changed files with 108 additions and 31 deletions

View File

@@ -106,3 +106,37 @@ describe('debounce()', () => {
expect(resultList).toEqual([['foo', 'bar3']]);
});
});
describe('fuzzyMatch()', () => {
beforeEach(globalBeforeEach);
it('can get a positive fuzzy match on a single field', () => {
expect(misc.fuzzyMatch('', undefined)).toEqual(true);
expect(misc.fuzzyMatch('', 'testing')).toEqual(true);
expect(misc.fuzzyMatch('test', 'testing')).toEqual(true);
expect(misc.fuzzyMatch('tstg', 'testing')).toEqual(true);
});
it('can get a negative fuzzy match on a single field', () => {
expect(misc.fuzzyMatch('foo', undefined)).toEqual(false);
expect(misc.fuzzyMatch('foo', 'bar')).toEqual(false);
});
});
describe('fuzzyMatchAll()', () => {
beforeEach(globalBeforeEach);
it('can get a positive fuzzy match on multiple fields', () => {
expect(misc.fuzzyMatchAll('', [undefined])).toEqual(true);
expect(misc.fuzzyMatchAll('', ['testing'])).toEqual(true);
expect(misc.fuzzyMatchAll(' ', ['testing'])).toEqual(true);
expect(misc.fuzzyMatchAll('test', ['testing'])).toEqual(true);
expect(misc.fuzzyMatchAll('tstg', ['testing'])).toEqual(true);
expect(misc.fuzzyMatchAll('tstg this ou', ['testing', 'this', 'out'])).toEqual(true);
});
it('can get a negative fuzzy match on multiple fields', () => {
expect(misc.fuzzyMatchAll('foo', [undefined])).toEqual(false);
expect(misc.fuzzyMatchAll('foo', ['bar'])).toEqual(false);
expect(misc.fuzzyMatchAll('wrong this ou', ['testing', 'this', 'out'])).toEqual(false);
});
});

View File

@@ -251,7 +251,15 @@ export function fuzzyMatch (searchString: string, text: string): boolean {
return false;
}
return toMatch.test(text.toLowerCase());
return toMatch.test((text || '').toLowerCase());
}
export function fuzzyMatchAll (searchString: string, allText: Array<string>): boolean {
return searchString
.split(' ')
.every(searchWord =>
allText.some(text =>
fuzzyMatch(searchWord, text)));
}
export function getViewportSize (): string | null {

View File

@@ -2,13 +2,14 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import {buildQueryStringFromParams, joinUrlAndQueryString} from 'insomnia-url';
import Button from '../base/button';
import Modal from '../base/modal';
import ModalHeader from '../base/modal-header';
import ModalBody from '../base/modal-body';
import MethodTag from '../tags/method-tag';
import * as models from '../../../models';
import {fuzzyMatch} from '../../../common/misc';
import {fuzzyMatchAll} from '../../../common/misc';
@autobind
class RequestSwitcherModal extends PureComponent {
@@ -122,24 +123,62 @@ class RequestSwitcherModal extends PureComponent {
this._handleChangeValue(e.target.value);
}
/**
* Appends path of ancestor groups, delimited by forward slashes
* E.g. Folder1/Folder2/Folder3
*/
_groupOf (requestOrRequestGroup) {
const {workspaceChildren} = this.props;
const requestGroups = workspaceChildren.filter(d => d.type === models.requestGroup.type);
const matchedGroups = requestGroups.filter(g => g._id === requestOrRequestGroup.parentId);
const currentGroupName = requestOrRequestGroup.type === models.requestGroup.type && requestOrRequestGroup.name ? `${requestOrRequestGroup.name}` : '';
if (matchedGroups.length === 0) {
return currentGroupName;
}
const parentGroup = this._groupOf(matchedGroups[0]);
const parentGroupText = parentGroup ? `${parentGroup}/` : '';
const group = `${parentGroupText}${currentGroupName}`;
return group;
}
_isMatch (searchStrings) {
return (request) => {
let finalUrl = request.url;
if (request.parameters) {
finalUrl = joinUrlAndQueryString(
finalUrl,
buildQueryStringFromParams(request.parameters));
}
// Match request attributes
const matchesAttributes = fuzzyMatchAll(searchStrings,
[
request.name,
finalUrl,
request.method,
this._groupOf(request)
]);
// Match exact Id
const matchesId = request._id === searchStrings;
return matchesAttributes || matchesId;
};
}
async _handleChangeValue (searchString) {
const {workspaceChildren, workspaces} = this.props;
const {workspaceId, activeRequestParentId} = this.props;
// OPTIMIZATION: This only filters if we have a filter
let matchedRequests = workspaceChildren.filter(d => d.type === models.request.type);
let matchedRequests = workspaceChildren
.filter(d => d.type === models.request.type);
if (searchString) {
matchedRequests = matchedRequests.filter(r => {
const name = r.name.toLowerCase();
// Fuzzy match searchString to name
const matchesName = fuzzyMatch(searchString, name);
// Match exact Id
const matchesId = r._id === searchString;
return matchesName || matchesId;
});
matchedRequests = matchedRequests.filter(this._isMatch(searchString));
}
matchedRequests = matchedRequests.sort((a, b) => {
@@ -225,7 +264,7 @@ class RequestSwitcherModal extends PureComponent {
<div className="form-control form-control--outlined no-margin">
<input
type="text"
placeholder="Search"
placeholder="Fuzzy filter by request name, folder, url, method, or query parameters"
ref={this._setInputRef}
value={searchString}
onChange={this._handleChange}

View File

@@ -52,7 +52,7 @@ class SidebarFilter extends PureComponent {
<input
ref={this._setInputRef}
type="text"
placeholder="Filter"
placeholder="Filter by name, folder, url"
defaultValue={this.props.filter}
onChange={this._handleOnChange}
/>

View File

@@ -1,5 +1,5 @@
import {createSelector} from 'reselect';
import {fuzzyMatch} from '../../common/misc';
import {fuzzyMatchAll} from '../../common/misc';
// ~~~~~~~~~ //
// Selectors //
@@ -112,23 +112,19 @@ export const selectSidebarChildren = createSelector(
const hasMatchedChildren = child.children.find(c => c.hidden === false);
// Build the monster string to match on
const method = child.doc.method || '';
const name = child.doc.name;
const toMatch = `${method}${name}${parentNames.join('❅')}`;
// Try to match request attributes
const {name, url, method, parameters} = child.doc;
// Try to match name
let hasMatchedName = true;
for (const token of sidebarFilter.trim().split(' ')) {
// Filter failed. Don't render children
if (!fuzzyMatch(token, toMatch)) {
hasMatchedName = false;
break;
}
}
const hasMatchedAttributes = fuzzyMatchAll(sidebarFilter, [
name,
url,
method,
...(parameters ? parameters.map(p => `${p.name}=${p.value}`) : []),
...parentNames
]);
// Update hidden state depending on whether it matched
const matched = hasMatchedChildren || hasMatchedName;
const matched = hasMatchedChildren || hasMatchedAttributes;
child.hidden = !matched;
}