Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI for filter requests by folder, URI, method, query string #864

Merged
merged 11 commits into from
May 23, 2018
4 changes: 1 addition & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions packages/insomnia-app/app/common/__tests__/misc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,22 @@ describe('debounce()', () => {
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);
expect(misc.fuzzyMatch('test', 'testing').searchTermsMatched).toBeGreaterThan(0);

expect(misc.fuzzyMatch('tstg', 'testing').searchTermsMatched).toBeGreaterThan(0);
});

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);
expect(misc.fuzzyMatch('foo', undefined).searchTermsMatched).toEqual(0);
expect(misc.fuzzyMatch('foo', 'bar').searchTermsMatched).toEqual(0);
});
});

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);
Expand Down
Empty file.
66 changes: 52 additions & 14 deletions packages/insomnia-app/app/common/misc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow
import * as electron from 'electron';
import {Readable, Writable} from 'stream';
import fuzzysort from 'fuzzysort';
import uuid from 'uuid';
import zlib from 'zlib';
import {join as pathJoin} from 'path';
Expand Down Expand Up @@ -223,30 +224,67 @@ export function escapeRegex (str: string): string {
return str.replace(ESCAPE_REGEX_MATCH, '\\$&');
}

export function fuzzyMatch (searchString: string, text: string): boolean {
const lowercase = searchString.toLowerCase();
export function fuzzyMatch (searchString: string, text: string): {
searchTermsMatched: number,
indexes: number[]
} {
const searchTerms = searchString.trim().split(' ');
const emptyResults = {
searchTermsMatched: 0,
searchTermsCount: searchTerms.length,
indexes: []
};

// Split into individual chars, then escape the ones that need it.
const regexSearchString = lowercase.split('').map(v => escapeRegex(v)).join('.*');
if (!searchString || !searchString.trim() || !searchTerms || searchTerms.length === 0) {
return emptyResults;
}

let toMatch;
try {
toMatch = new RegExp(regexSearchString);
} catch (err) {
console.warn('Invalid regex', searchString, regexSearchString);
// Invalid regex somehow
return false;
const results = searchTerms.reduce((prevResult, nextTerm) => {
const nextResult = fuzzysort.single(nextTerm, text);

if (!nextResult) {
return prevResult;
}

if (!prevResult) {
return nextResult;
}

const sort = array => array.sort((a, b) => a - b);
const uniq = array => Array.from(new Set(array));

return {
...prevResult,
...nextResult,

searchTermsMatched: prevResult.searchTermsMatched + 1,

indexes: sort(uniq([
...prevResult.indexes,
...nextResult.indexes
]))
};
}, emptyResults);

if (results.indexes.length === 0) {
return emptyResults;
}

return toMatch.test((text || '').toLowerCase());
return results;
}

export function fuzzyMatchAll (searchString: string, allText: Array<string>): boolean {
if (!searchString || !searchString.trim()) {
return true;
}

return searchString
.split(' ')
.every(searchWord =>
.map(searchTerm => searchTerm.trim())
.filter(searchTerm => !!searchTerm)
.every(searchTerm =>
allText.some(text =>
fuzzyMatch(searchWord, text)));
fuzzyMatch(searchTerm, text).searchTermsMatched > 0));
}

export function getViewportSize (): string | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class Dropdown extends PureComponent {
const listItemTextWithoutSpaces = listItem.textContent.toLowerCase().replace(/[^\w_]*/g, '');
const filterWithoutSpaces = newFilter.toLowerCase().replace(/[^\w_]*/g, '');

if (!newFilter || fuzzyMatch(filterWithoutSpaces, listItemTextWithoutSpaces)) {
if (!newFilter || fuzzyMatch(filterWithoutSpaces, listItemTextWithoutSpaces).searchTermsMatched) {
const filterIndex = listItem.getAttribute('data-filter-index');
filterItems.push(parseInt(filterIndex, 10));
}
Expand Down
19 changes: 14 additions & 5 deletions packages/insomnia-app/app/ui/components/base/editable.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Editable extends PureComponent {
singleClick,
onEditStart, // eslint-disable-line no-unused-vars
className,
renderReadView,
...extra
} = this.props;
const {editing} = this.state;
Expand All @@ -83,12 +84,20 @@ class Editable extends PureComponent {
/>
);
} else {
const readViewProps = {
className: `editable ${className}`,
title: singleClick ? 'Click to edit' : 'Double click to edit',
onClick: this._handleSingleClickEditStart,
onDoubleClick: this._handleEditStart,
...extra
};

if (renderReadView) {
return renderReadView(value, readViewProps);
}

return (
<div {...extra}
className={`editable ${className}`}
title={singleClick ? 'Click to edit' : 'Double click to edit'}
onClick={this._handleSingleClickEditStart}
onDoubleClick={this._handleEditStart}>
<div {...readViewProps}>
{value}
</div>
);
Expand Down
36 changes: 36 additions & 0 deletions packages/insomnia-app/app/ui/components/base/highlight.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// @flow
import * as React from 'react';
import autobind from 'autobind-decorator';
import fuzzysort from 'fuzzysort';
import {fuzzyMatch} from '../../../common/misc';

type Props = {|
search: string,
text: string,
|};

@autobind
class Highlight extends React.PureComponent<Props> {
render () {
const {
search,
text,
...otherProps
} = this.props;

const results = fuzzyMatch(search, text);

if (results.searchTermsMatched === 0) {
return <span>{text}</span>;
}

return <span
{...otherProps}
dangerouslySetInnerHTML={{ __html: fuzzysort.highlight(results,
'<strong style="color: #695eb8; text-decoration: underline;">',
'</strong>') }}
/>;
}
}

export default Highlight;
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ class CookiesModal extends PureComponent<Props, State> {
visibleCookieIndexes = [];
for (let i = 0; i < renderedCookies.length; i++) {
const toSearch = JSON.stringify(renderedCookies[i]);
const matched = fuzzyMatch(filter, toSearch);
if (matched) {
const results = fuzzyMatch(filter, toSearch);
if (results.searchTermsMatched > 0) {
visibleCookieIndexes.push(i);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import * as React from 'react';
import autobind from 'autobind-decorator';
import classnames from 'classnames';
import {buildQueryStringFromParams, joinUrlAndQueryString} from 'insomnia-url';
import Button from '../base/button';
import Highlight from '../base/highlight';
import Modal from '../base/modal';
import ModalHeader from '../base/modal-header';
import ModalBody from '../base/modal-body';
import MethodTag from '../tags/method-tag';
import {fuzzyMatchAll} from '../../../common/misc';
import type {BaseModel} from '../../../models';
import * as models from '../../../models';
import {fuzzyMatchAll} from '../../../common/misc';
import type {RequestGroup} from '../../../models/request-group';
import type {Request} from '../../../models/request';
import type {Workspace} from '../../../models/workspace';
Expand Down Expand Up @@ -171,19 +173,17 @@ class RequestSwitcherModal extends React.PureComponent<Props, State> {

_isMatch (searchStrings: string): (Request) => boolean {
return (request: Request): boolean => {
// Disable URL filtering until we have proper UI to show this
// let finalUrl = request.url;
// if (request.parameters) {
// finalUrl = joinUrlAndQueryString(
// finalUrl,
// buildQueryStringFromParams(request.parameters));
// }

// Match request attributes
const matchesAttributes = fuzzyMatchAll(searchStrings, [
let finalUrl = request.url;
if (request.parameters) {
finalUrl = joinUrlAndQueryString(
finalUrl,
buildQueryStringFromParams(request.parameters));
}

let matchesAttributes = fuzzyMatchAll(searchStrings, [
request.name,
// finalUrl,
// request.method,
finalUrl,
request.method,
this._groupOf(request).join('/')
]);

Expand Down Expand Up @@ -305,22 +305,27 @@ class RequestSwitcherModal extends React.PureComponent<Props, State> {
{matchedRequests.map((r, i) => {
const requestGroup = requestGroups.find(rg => rg._id === r.parentId);
const buttonClasses = classnames(
'btn btn--super-compact wide text-left',
'btn btn--expandable-small wide text-left pad-bottom',
{focus: activeIndex === i}
);

return (
<li key={r._id}>
<Button onClick={this._activateRequest} value={r} className={buttonClasses}>
{requestGroup && (
<div className="pull-right faint italic">
{this._groupOf(r).join(' / ')}
&nbsp;&nbsp;
<i className="fa fa-folder-o"/>
</div>
)}
<MethodTag method={(r: any).method}/>
<strong>{(r: any).name}</strong>
<div>
{requestGroup ? (
<div className="pull-right faint italic">
<Highlight search={searchString} text={this._groupOf(r).join(' / ')} />
&nbsp;&nbsp;
<i className="fa fa-folder-o"/>
</div>
) : null}
<MethodTag method={(r: any).method}/>
<Highlight search={searchString} text={(r: any).name} />
</div>
<div className='margin-left-xs faint'>
<Highlight search={searchString} text={(r: any).url} />
</div>
</Button>
</li>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Props = {
moveDoc: Function,
childObjects: Array<Child>,
workspace: Workspace,
filter: string,

// Optional
activeRequest?: Request
Expand All @@ -36,6 +37,7 @@ type Props = {
class SidebarChildren extends React.PureComponent<Props> {
_renderChildren (children: Array<Child>) {
const {
filter,
handleCreateRequest,
handleCreateRequestGroup,
handleSetRequestGroupCollapsed,
Expand All @@ -61,6 +63,7 @@ class SidebarChildren extends React.PureComponent<Props> {
return (
<SidebarRequestRow
key={child.doc._id}
filter={filter || ''}
moveDoc={moveDoc}
handleActivateRequest={handleActivateRequest}
handleDuplicateRequest={handleDuplicateRequest}
Expand Down Expand Up @@ -97,6 +100,7 @@ class SidebarChildren extends React.PureComponent<Props> {
return (
<SidebarRequestGroupRow
key={requestGroup._id}
filter={filter || ''}
isActive={isActive}
moveDoc={moveDoc}
handleActivateRequest={handleActivateRequest}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import autobind from 'autobind-decorator';
import ReactDOM from 'react-dom';
import {DragSource, DropTarget} from 'react-dnd';
import classnames from 'classnames';
import Highlight from '../base/highlight';
import RequestGroupActionsDropdown from '../dropdowns/request-group-actions-dropdown';
import SidebarRequestRow from './sidebar-request-row';
import * as misc from '../../../common/misc';
Expand Down Expand Up @@ -41,6 +42,7 @@ class SidebarRequestGroupRow extends PureComponent {
const {
connectDragSource,
connectDropTarget,
filter,
moveDoc,
children,
requestGroup,
Expand Down Expand Up @@ -73,7 +75,7 @@ class SidebarRequestGroupRow extends PureComponent {
<button onClick={this._handleCollapse} onContextMenu={this._handleShowActions}>
<div className="sidebar__clickable">
<i className={'sidebar__item__icon fa ' + folderIconClass}/>
<span>{requestGroup.name}</span>
<Highlight search={filter} text={requestGroup.name}/>
</div>
</button>
));
Expand Down Expand Up @@ -130,6 +132,7 @@ SidebarRequestGroupRow.propTypes = {
handleCreateRequestGroup: PropTypes.func.isRequired,

// Other
filter: PropTypes.string.isRequired,
isActive: PropTypes.bool.isRequired,
isCollapsed: PropTypes.bool.isRequired,
workspace: PropTypes.object.isRequired,
Expand Down
Loading