Skip to content

Commit 82105a9

Browse files
committed
Auto merge of #4382 - Turbo87:enhanced-search, r=pietroalbini
Add support for `keyword:cli` and `category:no-std` search filters Resolves #491 These filters can be used standalone, or combined with regular search terms. The syntax is inspired by https://api-docs.npms.io <table> <tr> <td><img width="767" alt="Bildschirmfoto 2021-12-29 um 00 16 37" src="https://user-images.githubusercontent.com/141300/147614047-ca323230-20f3-4fe5-9504-8e8c026e3f9d.png"> <td><img width="766" alt="Bildschirmfoto 2021-12-29 um 00 18 00" src="https://user-images.githubusercontent.com/141300/147614106-6b856e26-80a7-4d5d-8e48-17122d5c21dc.png">
2 parents f0c466e + 2144fca commit 82105a9

File tree

7 files changed

+145
-1
lines changed

7 files changed

+145
-1
lines changed

app/controllers/search.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { restartableTask } from 'ember-concurrency';
77
import { bool, reads } from 'macro-decorators';
88

99
import { pagination } from '../utils/pagination';
10+
import { CATEGORY_PREFIX, processSearchQuery } from '../utils/search';
1011

1112
export default class SearchController extends Controller {
1213
@service store;
@@ -48,6 +49,11 @@ export default class SearchController extends Controller {
4849

4950
@bool('totalItems') hasItems;
5051

52+
get hasMultiCategoryFilter() {
53+
let tokens = this.q.trim().split(/\s+/);
54+
return tokens.filter(token => token.startsWith(CATEGORY_PREFIX)).length > 1;
55+
}
56+
5157
@action fetchData() {
5258
this.dataTask.perform().catch(() => {
5359
// we ignore errors here because they are handled in the template already
@@ -61,6 +67,10 @@ export default class SearchController extends Controller {
6167
q = q.trim();
6268
}
6369

64-
return yield this.store.query('crate', { all_keywords, page, per_page, q, sort });
70+
let searchOptions = all_keywords
71+
? { page, per_page, sort, q, all_keywords }
72+
: { page, per_page, sort, ...processSearchQuery(q) };
73+
74+
return yield this.store.query('crate', searchOptions);
6575
}
6676
}

app/styles/application.module.css

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66
--grey200: hsl(200, 17%, 96%);
77
--green800: hsl(115, 31%, 31%);
88
--green900: hsl(115, 31%, 21%);
9+
10+
--orange-50: #fff7ed;
11+
--orange-100: #ffedd5;
12+
--orange-200: #fed7aa;
13+
--orange-300: #fdba74;
14+
--orange-400: #fb923c;
15+
--orange-500: #f97316;
16+
--orange-600: #ea580c;
17+
--orange-700: #c2410c;
18+
--orange-800: #9a3412;
19+
--orange-900: #7c2d12;
20+
921
--yellow500: hsl(44, 100%, 60%);
1022
--yellow700: hsl(44, 67%, 50%);
1123

app/styles/search.module.css

+9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
margin-bottom: 25px;
66
}
77

8+
.warning {
9+
margin: 0 0 16px;
10+
padding: 8px;
11+
color: var(--orange-700);
12+
background: var(--orange-100);
13+
border-left: solid var(--orange-400) 4px;
14+
border-radius: 2px;
15+
}
16+
817
.sort-by-label {
918
composes: small from './shared/typography.module.css';
1019
}

app/templates/search.hbs

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
data-test-header
99
/>
1010

11+
{{#if this.hasMultiCategoryFilter}}
12+
<div local-class="warning">
13+
Support for using multiple <code>category:</code> filters is not yet implemented.
14+
</div>
15+
{{/if}}
16+
1117
{{#if this.firstResultPending}}
1218
<h2>Loading search results...</h2>
1319
{{else if this.dataTask.lastComplete.error}}

app/utils/search.js

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
export const CATEGORY_PREFIX = 'category:';
2+
const KEYWORD_PREFIX = 'keyword:';
3+
4+
/**
5+
* Process a search query string and extract filters like `keyword:`.
6+
*
7+
* @param {string} query
8+
* @return {{ q: string, keyword?: string, all_keywords?: string, category?: string }}
9+
*/
10+
export function processSearchQuery(query) {
11+
let tokens = query.trim().split(/\s+/);
12+
13+
let queries = [];
14+
let keywords = [];
15+
let category = null;
16+
for (let token of tokens) {
17+
if (token.startsWith(CATEGORY_PREFIX)) {
18+
let value = token.slice(CATEGORY_PREFIX.length).trim();
19+
if (value) {
20+
category = value;
21+
}
22+
} else if (token.startsWith(KEYWORD_PREFIX)) {
23+
let value = token.slice(KEYWORD_PREFIX.length).trim();
24+
if (value) {
25+
keywords.push(value);
26+
}
27+
} else {
28+
queries.push(token);
29+
}
30+
}
31+
32+
let result = { q: queries.join(' ') };
33+
34+
if (keywords.length === 1) {
35+
result.keyword = keywords[0];
36+
} else if (keywords.length !== 0) {
37+
result.all_keywords = keywords.join(' ');
38+
}
39+
40+
if (category) {
41+
result.category = category;
42+
}
43+
44+
return result;
45+
}

tests/acceptance/search-test.js

+38
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,42 @@ module('Acceptance | search', function (hooks) {
191191
await visit('/search?q=rust&page=3&per_page=15&sort=new&all_keywords=fire ball');
192192
assert.verifySteps(['/api/v1/crates']);
193193
});
194+
195+
test('supports `keyword:bla` filters', async function (assert) {
196+
this.server.get('/api/v1/crates', function (schema, request) {
197+
assert.step('/api/v1/crates');
198+
199+
assert.deepEqual(request.queryParams, {
200+
all_keywords: 'fire ball',
201+
page: '3',
202+
per_page: '15',
203+
q: 'rust',
204+
sort: 'new',
205+
});
206+
207+
return { crates: [], meta: { total: 0 } };
208+
});
209+
210+
await visit('/search?q=rust keyword:fire keyword:ball&page=3&per_page=15&sort=new');
211+
assert.verifySteps(['/api/v1/crates']);
212+
});
213+
214+
test('`all_keywords` query parameter takes precedence over `keyword` filters', async function (assert) {
215+
this.server.get('/api/v1/crates', function (schema, request) {
216+
assert.step('/api/v1/crates');
217+
218+
assert.deepEqual(request.queryParams, {
219+
all_keywords: 'fire ball',
220+
page: '3',
221+
per_page: '15',
222+
q: 'rust keywords:foo',
223+
sort: 'new',
224+
});
225+
226+
return { crates: [], meta: { total: 0 } };
227+
});
228+
229+
await visit('/search?q=rust keywords:foo&page=3&per_page=15&sort=new&all_keywords=fire ball');
230+
assert.verifySteps(['/api/v1/crates']);
231+
});
194232
});

tests/utils/search-test.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { module, test } from 'qunit';
2+
3+
import { processSearchQuery } from '../../utils/search';
4+
5+
module('processSearchQuery()', function () {
6+
const TESTS = [
7+
['foo', { q: 'foo' }],
8+
[' foo bar ', { q: 'foo bar' }],
9+
['foo keyword:bar', { q: 'foo', keyword: 'bar' }],
10+
['foo keyword:', { q: 'foo' }],
11+
['keyword:bar foo', { q: 'foo', keyword: 'bar' }],
12+
['foo \t keyword:bar baz', { q: 'foo baz', keyword: 'bar' }],
13+
['foo keyword:bar keyword:baz', { q: 'foo', all_keywords: 'bar baz' }],
14+
['foo category:', { q: 'foo' }],
15+
['foo category:no-std', { q: 'foo', category: 'no-std' }],
16+
['foo category:no-std keyword:bar keyword:baz', { q: 'foo', all_keywords: 'bar baz', category: 'no-std' }],
17+
];
18+
19+
for (let [input, expectation] of TESTS) {
20+
test(input, function (assert) {
21+
assert.deepEqual(processSearchQuery(input), expectation);
22+
});
23+
}
24+
});

0 commit comments

Comments
 (0)