diff --git a/app/components/addon-source-usages.js b/app/components/addon-source-usages.js index 599f862b..cfa4aacd 100644 --- a/app/components/addon-source-usages.js +++ b/app/components/addon-source-usages.js @@ -1,7 +1,7 @@ import Ember from 'ember'; import { task } from 'ember-concurrency'; -const { computed } = Ember; +const { computed, inject, isEmpty } = Ember; export default Ember.Component.extend({ visibleUsageCount: 25, @@ -12,7 +12,9 @@ export default Ember.Component.extend({ regex: false, - codeSearch: Ember.inject.service(), + fileFilter: null, + + codeSearch: inject.service(), visibleUsages: computed('visibleUsageCount', 'usages', function() { return this.get('usages').slice(0, this.get('visibleUsageCount')); @@ -24,7 +26,7 @@ export default Ember.Component.extend({ fetchUsages: task(function* () { let usages = yield this.get('codeSearch').usages(this.get('addon.name'), this.get('query'), this.get('regex')); - this.set('usages', usages); + this.set('usages', filterByFilePath(usages, this.get('fileFilter'))); }).drop(), actions: { @@ -41,3 +43,13 @@ export default Ember.Component.extend({ } } }); + +function filterByFilePath(usages, filterTerm) { + if (isEmpty(filterTerm)) { + return usages; + } + let filterRegex = new RegExp(filterTerm); + return usages.filter((usage) => { + return usage.filename.match(filterRegex); + }); +} diff --git a/app/components/code-search.js b/app/components/code-search.js index 8faf3d83..90e9212d 100644 --- a/app/components/code-search.js +++ b/app/components/code-search.js @@ -1,9 +1,10 @@ import Ember from 'ember'; -import { task } from 'ember-concurrency'; +import { task, timeout } from 'ember-concurrency'; +import config from 'ember-observer/config/environment'; -const { computed, inject } = Ember; +const { computed, inject, isEmpty } = Ember; -const PageSize = 50; +const PageSize = config.codeSearchPageSize; export default Ember.Component.extend({ metrics: inject.service(), @@ -11,18 +12,23 @@ export default Ember.Component.extend({ store: inject.service(), codeQuery: null, + sort: null, + fileFilter: null, + classNames: ['code-search'], focusNode: '#code-search-input', codeSearch: inject.service(), - usageCounts: computed.mapBy('results.rawResults', 'count'), + usageCounts: computed.mapBy('results.filteredResults', 'count'), totalUsageCount: computed.sum('usageCounts'), + isFilterApplied: computed.notEmpty('fileFilter'), + init() { this._super(...arguments); this.set('searchInput', this.get('codeQuery') || ''); @@ -50,16 +56,41 @@ export default Ember.Component.extend({ let results = yield this.get('codeSearch').addons(query, this.get('regex')); this.set('quotedLastSearch', quoteSearchTerm(query, this.get('regex'))); - let firstPageOfResults = yield this._fetchPageOfAddonResults(results, 1, this.get('sort')); + let filteredResults = filterByFilePath(results, this.get('fileFilter')); + + let firstPageOfResults = yield this._fetchPageOfAddonResults(filteredResults, 1, this.get('sort')); this.set('results', { displayingResults: firstPageOfResults, lastResultPageDisplaying: 1, rawResults: results, - length: results.length + length: results.length, + filteredResults: filteredResults, + filteredResultLength: filteredResults.length }); }).restartable(), + applyFileFilter: task(function*(fileFilter) { + yield timeout(500); + + this.set('fileFilter', fileFilter); + let filteredResults = filterByFilePath((this.get('results.rawResults')), fileFilter); + let firstPageOfFilteredResults = yield this._fetchPageOfAddonResults(filteredResults, 1, this.get('sort')); + this.set('results.displayingResults', firstPageOfFilteredResults); + this.set('results.lastResultPageDisplaying', 1); + this.set('results.filteredResults', filteredResults); + this.set('results.filteredResultLength', filteredResults.length); + }).restartable(), + + clearFileFilter: task(function*() { + this.set('fileFilter', null); + let firstPageOfResults = yield this._fetchPageOfAddonResults(this.get('results.rawResults'), 1, this.get('sort')); + this.set('results.displayingResults', firstPageOfResults); + this.set('results.lastResultPageDisplaying', 1); + this.set('results.filteredResults', this.get('results.rawResults')); + this.set('results.filteredResultLength', this.get('results.length')); + }), + _fetchPageOfAddonResults(results, page, sort) { if (!results || !results.length) { return Ember.RSVP.resolve(null); @@ -72,26 +103,27 @@ export default Ember.Component.extend({ return pageOfResults.map((result) => { return { addon: addons.findBy('name', result.addonName), - count: result.count + count: result.count, + files: result.files }; }); }); }, - canViewMore: computed('results.displayingResults.length', 'results.length', function() { - return this.get('results.displayingResults.length') < this.get('results.length'); + canViewMore: computed('results.displayingResults.length', 'results.filteredResultLength', function() { + return this.get('results.displayingResults.length') < this.get('results.filteredResultLength'); }), viewMore: task(function* () { let pageToFetch = this.get('results.lastResultPageDisplaying') + 1; - let moreAddons = yield this._fetchPageOfAddonResults(this.get('results.rawResults'), pageToFetch, this.get('sort')); + let moreAddons = yield this._fetchPageOfAddonResults(this.get('results.filteredResults'), pageToFetch, this.get('sort')); this.get('results.displayingResults').pushObjects(moreAddons); this.set('results.lastResultPageDisplaying', pageToFetch); }), sortBy: task(function* (key) { this.set('sort', key); - let sortedAddons = yield this._fetchPageOfAddonResults(this.get('results.rawResults'), 1, key); + let sortedAddons = yield this._fetchPageOfAddonResults(this.get('results.filteredResults'), 1, key); this.set('results.displayingResults', sortedAddons); this.set('results.lastResultPageDisplaying', 1); }), @@ -124,3 +156,25 @@ function quoteSearchTerm(searchTerm, isRegex) { let character = isRegex ? '/' : '"'; return `${character}${searchTerm}${character}`; } + +function filterByFilePath(results, filterTerm) { + if (isEmpty(filterTerm)) { + return results; + } + + let filteredList = []; + let filterRegex = new RegExp(filterTerm); + results.forEach((result) => { + let filteredFiles = result.files.filter((filePath) => { + return filePath.match(filterRegex); + }); + if (filteredFiles.length > 0) { + filteredList.push({ + addonName: result.addonName, + files: filteredFiles, + count: filteredFiles.length + }); + } + }); + return filteredList; +} diff --git a/app/controllers/code-search.js b/app/controllers/code-search.js index f21a0319..da6475de 100644 --- a/app/controllers/code-search.js +++ b/app/controllers/code-search.js @@ -1,8 +1,9 @@ import Ember from 'ember'; export default Ember.Controller.extend({ - queryParams: ['codeQuery', 'sort', 'regex'], + queryParams: ['codeQuery', 'sort', 'regex', 'fileFilter'], codeQuery: '', sort: 'name', - regex: false + regex: false, + fileFilter: null }); diff --git a/app/services/code-search.js b/app/services/code-search.js index d7ba0db1..a9309228 100644 --- a/app/services/code-search.js +++ b/app/services/code-search.js @@ -14,7 +14,7 @@ export default Ember.Service.extend({ } }).then((response) => { return response.results.map((item) => { - return { addonName: item.addon, count: item.count }; + return { addonName: item.addon, count: item.count, files: item.files }; }); }); }, diff --git a/app/styles/components/_code-search.scss b/app/styles/components/_code-search.scss index 807ef25f..36f67ad7 100644 --- a/app/styles/components/_code-search.scss +++ b/app/styles/components/_code-search.scss @@ -54,14 +54,56 @@ .result-details { @include clearfix; + h4 { + margin: .5rem 0; + } + h5 { - padding-top: 2.25rem; - float:left; + padding-top: .5rem; line-height: 2rem; } + } + + .result-controls { + padding: 0 .25rem; + border: 1px solid $light-gray; + border-width: 0 0 1px 0; + margin-top: 1rem; + @include clearfix(); + .sort-controls { float: right; + margin-right: .26rem; + margin-bottom: 1.5rem; + + .button-option { + margin-bottom: 0; + } + } + + .filter-controls { + float: left; + margin-bottom: 1.5rem; + + .filter-input { + position: relative; + width: 300px; + + input { + border: 1px solid $base-accent-color; + height: 36px; + width: 100%; + padding: .5rem; + } + + .icon-close { + right: .4rem; + color: $base-accent-color; + font-size: 1.25rem; + margin-top: -.7rem; + } + } } } diff --git a/app/templates/code-search.hbs b/app/templates/code-search.hbs index 32a29a54..155c2e77 100644 --- a/app/templates/code-search.hbs +++ b/app/templates/code-search.hbs @@ -1,3 +1,3 @@ {{#page-layout showCategories=true}} - {{code-search codeQuery=codeQuery sort=sort regex=regex}} + {{code-search codeQuery=codeQuery sort=sort regex=regex fileFilter=fileFilter}} {{/page-layout}} diff --git a/app/templates/components/code-search.hbs b/app/templates/components/code-search.hbs index 02aedcd2..bf8e1494 100644 --- a/app/templates/components/code-search.hbs +++ b/app/templates/components/code-search.hbs @@ -31,8 +31,25 @@
- Found {{pluralize-this results.length 'addon'}} ({{pluralize-this totalUsageCount 'usage'}}) + Found {{pluralize-this results.filteredResultLength 'addon'}} ({{pluralize-this totalUsageCount 'usage'}}) + {{#if isFilterApplied}} + in files matching /{{fileFilter}}/ + {{/if}}
+
+
+
+ Filter by file path: +
+ + {{#if isFilterApplied}} + + {{/if}} +
+
Sort by:
@@ -45,7 +62,13 @@ {{#each results.displayingResults as |result|}}
  • {{addon-details addon=result.addon}} - {{addon-source-usages addon=result.addon count=result.count query=codeQuery regex=regex}} + {{addon-source-usages + addon=result.addon + count=result.count + query=codeQuery + regex=regex + fileFilter=fileFilter + }}
  • {{/each}} {{#if canViewMore}} diff --git a/config/environment.js b/config/environment.js index 4a37fd10..2ee8b95e 100644 --- a/config/environment.js +++ b/config/environment.js @@ -10,6 +10,7 @@ module.exports = function(environment) { rootURL: '/', locationType: 'router-scroll', historySupportMiddleware: true, + codeSearchPageSize: 50, EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build @@ -60,6 +61,7 @@ module.exports = function(environment) { ENV.APP.LOG_VIEW_LOOKUPS = false; ENV.APP.rootElement = '#ember-testing'; + ENV.codeSearchPageSize = 3; } if (environment === 'production') { diff --git a/tests/acceptance/code-search-test.js b/tests/acceptance/code-search-test.js index 737bcf67..b1834ef8 100644 --- a/tests/acceptance/code-search-test.js +++ b/tests/acceptance/code-search-test.js @@ -275,7 +275,7 @@ test('searching when sort is set in query param', function(assert) { }); test('viewing more results', function(assert) { - let addons = server.createList('addon', 51); + let addons = server.createList('addon', 4); server.get('/search/addons', () => { return { @@ -288,18 +288,245 @@ test('viewing more results', function(assert) { click('.test-submit-search'); andThen(function() { - assert.equal(50, find('.test-addon-name').length, 'First 50 results show'); + assert.equal(3, find('.test-addon-name').length, 'First 50 results show'); assert.equal(1, find('.test-view-more').length, 'View more link shows'); }); click('.test-view-more'); andThen(function() { - assert.equal(51, find('.test-addon-name').length, 'All 51 results show'); + assert.equal(4, find('.test-addon-name').length, 'All 51 results show'); assert.equal(0, find('.test-view-more').length, 'View more link does not show'); }); }); +test('filtering search results by file path', function(assert) { + server.create('addon', { name: 'ember-try' }); + server.create('addon', { name: 'ember-blanket' }); + server.create('addon', { name: 'ember-foo' }); + + let filterTerm = 'index'; + + server.get('/search/addons', () => { + return { + results: [ + { + addon: 'ember-try', + count: 1, + files: ['app/controllers/index.js'] + }, + { + addon: 'ember-blanket', + count: 2, + files: ['app/components/blanket.js', 'app/templates/components/blanket.hbs'] + }, + { + addon: 'ember-foo', + count: 3, + files: ['app/controllers/index.js', 'app/controllers/index.js', 'app/services/current-foo.js'] + } + ] + }; + }); + + visit('/code-search'); + fillIn('#code-search-input', 'whatever'); + click('.test-submit-search'); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 3, 'shows all addons before filtering'); + }); + + fillIn('.test-file-filter-input', filterTerm); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 2, 'shows only matching addons after filtering'); + assert.contains('.test-result-info', '2 addons', 'filtered result count shows when filter is applied'); + assert.contains('.test-result-info', '3 usages', 'filtered usage count shows when filter is applied'); + }); + + click('.test-clear-file-filter'); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 3, 'shows all addons after clearing filter'); + }); + + let regexFilterTerm = 'components.*js'; + fillIn('.test-file-filter-input', regexFilterTerm); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 1, 'shows only matching addons after filtering'); + assert.contains('.test-result-info', '1 addon', 'filtered result count is correct after regex search'); + assert.contains('.test-result-info', '1 usage', 'filtered usage count is correct after regex search'); + }); +}); + +test('filtering addon source by file path', function(assert) { + server.create('addon', { name: 'no-match' }); + let addonWithFilteredFiles = server.create('addon', { name: 'has-match' }); + + let filterTerm = 'index'; + + server.get('/search/addons', () => { + return { + results: [ + { + addon: 'no-match', + count: 2, + files: ['app/components/no-match.js', 'app/templates/components/no-match.hbs'] + }, + { + addon: 'has-match', + count: 2, + files: ['app/controllers/index.js', 'app/services/no-match.js'] + } + ] + }; + }); + + server.get('/search/source', () => { + return { + /* eslint-disable camelcase */ + results: [ + { + line_number: 52, + filename: 'app/controllers/index.js', + lines: [ + { text: 'if (addonData) {', number: 51 } + ] + }, + { + line_number: 21, + filename: 'app/services/no-match.js', + lines: [ + { number: 20, text: '' } + ] + } + ] + /* eslint-disable camelcase */ + }; + }); + + visit('/code-search'); + fillIn('#code-search-input', 'whatever'); + click('.test-submit-search'); + + fillIn('.test-file-filter-input', filterTerm); + + click(`[data-id="${addonWithFilteredFiles.id}"] .test-usage-count`); + + andThen(function() { + assert.equal(find('.test-usage').length, 1, 'filtered down to 1 usage'); + assert.notExists('.test-usage:contains("app/services/no-match.js")', 'filtered out file does not show'); + assert.exists('.test-usage:contains("app/controllers/index.js")', 'file with matching name shows'); + }); + + click('.test-clear-file-filter'); + click(`[data-id="${addonWithFilteredFiles.id}"] .test-usage-count`); + + andThen(function() { + assert.equal(find('.test-usage').length, 2, 'all usages show'); + assert.exists('.test-usage:contains("app/services/no-match.js")', 'previously filtered out file now shows'); + }); +}); + +test('filtering works with sorting and pagination', function(assert) { + server.create('addon', { name: 'ember-try' }); + server.create('addon', { name: 'ember-blanket' }); + server.create('addon', { name: 'ember-foo' }); + server.create('addon', { name: 'ember-cli-thing' }); + server.create('addon', { name: 'ember-cli-other-thing' }); + server.create('addon', { name: 'ember-cli-matches' }); + + let filterTerm = 'index'; + + server.get('/search/addons', () => { + return { + results: [ + { + addon: 'ember-try', + count: 1, + files: ['app/controllers/index.js'] + }, + { + addon: 'ember-blanket', + count: 5, + files: ['app/components/blanket.js', + 'app/templates/components/blanket.hbs', + 'app/templates/blanket.hbs', + 'blah.js', + 'thing.js'] + }, + { + addon: 'ember-foo', + count: 3, + files: ['app/controllers/index.js', + 'app/controllers/index.js', + 'app/services/current-foo.js'] + }, + { + addon: 'ember-cli-thing', + count: 6, + files: ['app/controllers/index.js', + 'app/controllers/index.js', + 'app/services/current-foo.js', + 'app/templates/maybe.hbs', + 'app/templates/maybe.hbs', + 'app/templates/maybe.hbs'] + }, + { + addon: 'ember-cli-other-thing', + count: 1, + files: ['app/services/current-thing.js'] + }, + { + addon: 'ember-cli-matches', + count: 1, + files: ['app/controllers/index.js'] + } + ] + }; + }); + + visit('/code-search'); + fillIn('#code-search-input', 'whatever'); + click('.test-submit-search'); + click('.test-sort button:contains("Usages")'); + fillIn('.test-file-filter-input', filterTerm); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 3, 'shows one page worth of addons after filtering'); + assert.contains('.test-result-info', '4 addons', 'filtered addon count shows when filter is applied'); + assert.contains('.test-result-info', '6 usages', 'filtered usage count shows when filter is applied'); + assert.contains('.test-addon-name:eq(0)', 'ember-cli-thing', 'addons are sorted by usage count'); + }); + + click('.test-view-more'); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 4, 'adds additional addons that meet filter criteria'); + }); + + click('.test-sort button:contains("Name")'); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 3, 'resets to first page after sorting'); + assert.contains('.test-result-info', '4 addons', 'filtered addon count still shows after sorting'); + assert.contains('.test-result-info', '6 usages', 'filtered usage count still shows after sorting'); + assert.contains('.test-addon-name:eq(0)', 'ember-cli-matches', 'addons are sorted by name'); + }); + + click('.test-view-more'); + click('.test-clear-file-filter'); + + andThen(function() { + assert.equal(find('.test-addon-name').length, 3, 'shows first page of addons after clearing filter'); + assert.contains('.test-result-info', '6 addons', 'un-filtered addon count shows'); + assert.contains('.test-result-info', '17 usages', 'un-filtered usage count shows'); + assert.contains('.test-addon-name:eq(0)', 'ember-blanket', 'addons are sorted by name'); + }); +}); + function searchResults(addons) { return addons.map((addon) => { return {