From bee9973839b504f3fd818d0defd1e7e6cf86aa29 Mon Sep 17 00:00:00 2001 From: Shawn Axsom Date: Wed, 28 Mar 2018 16:57:05 -0400 Subject: [PATCH] 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 --- .../app/common/__tests__/misc.test.js | 34 ++++++++++ packages/insomnia-app/app/common/misc.js | 10 ++- .../modals/request-switcher-modal.js | 67 +++++++++++++++---- .../ui/components/sidebar/sidebar-filter.js | 2 +- .../insomnia-app/app/ui/redux/selectors.js | 26 +++---- 5 files changed, 108 insertions(+), 31 deletions(-) diff --git a/packages/insomnia-app/app/common/__tests__/misc.test.js b/packages/insomnia-app/app/common/__tests__/misc.test.js index d5abb7cf5a..0329c91ca6 100644 --- a/packages/insomnia-app/app/common/__tests__/misc.test.js +++ b/packages/insomnia-app/app/common/__tests__/misc.test.js @@ -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); + }); +}); + diff --git a/packages/insomnia-app/app/common/misc.js b/packages/insomnia-app/app/common/misc.js index 709156a4ed..d9997f7ddb 100644 --- a/packages/insomnia-app/app/common/misc.js +++ b/packages/insomnia-app/app/common/misc.js @@ -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): boolean { + return searchString + .split(' ') + .every(searchWord => + allText.some(text => + fuzzyMatch(searchWord, text))); } export function getViewportSize (): string | null { diff --git a/packages/insomnia-app/app/ui/components/modals/request-switcher-modal.js b/packages/insomnia-app/app/ui/components/modals/request-switcher-modal.js index b12a43a23d..591077bd39 100644 --- a/packages/insomnia-app/app/ui/components/modals/request-switcher-modal.js +++ b/packages/insomnia-app/app/ui/components/modals/request-switcher-modal.js @@ -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 {
diff --git a/packages/insomnia-app/app/ui/redux/selectors.js b/packages/insomnia-app/app/ui/redux/selectors.js index d4ad219459..c042c173f9 100644 --- a/packages/insomnia-app/app/ui/redux/selectors.js +++ b/packages/insomnia-app/app/ui/redux/selectors.js @@ -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; }