Added server-side search to new members screen (#1582)

requires https://github.com/TryGhost/Ghost/pull/11854

- ties the search input on the members screen to a `?search` query param, debounced at 250ms to avoid unnecessary API requests and UI churn
- updated the members route's `model` hook to pass through the search param in the API request query parameters
This commit is contained in:
Kevin Ansfield 2020-05-28 10:15:17 +01:00 committed by GitHub
parent 76b93c3be7
commit ff33eb978b
4 changed files with 34 additions and 8 deletions

View File

@ -7,17 +7,19 @@ import {formatNumber} from 'ghost-admin/helpers/format-number';
import {pluralize} from 'ember-inflector'; import {pluralize} from 'ember-inflector';
import {inject as service} from '@ember/service'; import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators'; import {task} from 'ember-concurrency-decorators';
import {timeout} from 'ember-concurrency';
import {tracked} from '@glimmer/tracking'; import {tracked} from '@glimmer/tracking';
export default class MembersController extends Controller { export default class MembersController extends Controller {
@service feature; @service feature;
@service store; @service store;
queryParams = ['label']; queryParams = ['label', {searchParam: 'search'}];
@alias('model') members; @alias('model') members;
@tracked searchText = ''; @tracked searchText = '';
@tracked searchParam = '';
@tracked label = null; @tracked label = null;
@tracked modalLabel = null; @tracked modalLabel = null;
@tracked showLabelModal = false; @tracked showLabelModal = false;
@ -56,7 +58,7 @@ export default class MembersController extends Controller {
} }
get showingAll() { get showingAll() {
return !this.searchText && !this.label; return !this.searchParam && !this.label;
} }
get availableLabels() { get availableLabels() {
@ -88,6 +90,11 @@ export default class MembersController extends Controller {
// Actions ----------------------------------------------------------------- // Actions -----------------------------------------------------------------
@action
search(e) {
this.searchTask.perform(e.target.value);
}
@action @action
exportData() { exportData() {
let exportUrl = ghostPaths().url.api('members/csv'); let exportUrl = ghostPaths().url.api('members/csv');
@ -141,6 +148,12 @@ export default class MembersController extends Controller {
// Tasks ------------------------------------------------------------------- // Tasks -------------------------------------------------------------------
@task({restartable: true})
*searchTask(query) {
yield timeout(250); // debounce
this.searchParam = query;
}
@task @task
*fetchLabelsTask() { *fetchLabelsTask() {
if (!this._hasLoadedLabels) { if (!this._hasLoadedLabels) {
@ -149,4 +162,10 @@ export default class MembersController extends Controller {
}); });
} }
} }
// Internal ----------------------------------------------------------------
resetSearch() {
this.searchText = '';
}
} }

View File

@ -14,7 +14,8 @@ export const DEFAULT_QUERY_PARAMS = {
order: null order: null
}, },
'members.index': { 'members.index': {
label: null label: null,
searchParam: ''
} }
}; };

View File

@ -8,7 +8,8 @@ export default class MembersRoute extends AuthenticatedRoute {
@service store; @service store;
queryParams = { queryParams = {
label: {refreshModel: true} label: {refreshModel: true},
searchParam: {refreshModel: true, replace: true}
}; };
// redirect to posts screen if: // redirect to posts screen if:
@ -24,12 +25,17 @@ export default class MembersRoute extends AuthenticatedRoute {
} }
model(params) { model(params) {
if (!params.searchParam) {
this.controllerFor('members').resetSearch();
}
// use a fixed created_at date so that subsequent pages have a consistent index // use a fixed created_at date so that subsequent pages have a consistent index
let startDate = new Date(); let startDate = new Date();
// bypass the stale data shortcut if params change // bypass the stale data shortcut if params change
let forceReload = params.label !== this._lastLabel; let forceReload = params.label !== this._lastLabel || params.searchParam !== this._lastSearchParam;
this._lastLabel = params.label; this._lastLabel = params.label;
this._lastSearchParam = params.searchParam;
// unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old // unless we have a forced reload, do not re-fetch the members list unless it's more than a minute old
// keeps navigation between list->details->list snappy // keeps navigation between list->details->list snappy
@ -46,7 +52,8 @@ export default class MembersRoute extends AuthenticatedRoute {
limit: range.length, limit: range.length,
page: range.start / range.length, page: range.start / range.length,
order: 'created_at desc', order: 'created_at desc',
filter: `${labelFilter}created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'` filter: `${labelFilter}created_at:<='${moment.utc(this._startDate).format('YYYY-MM-DD HH:mm:ss')}'`,
search: params.searchParam
}, query); }, query);
return this.store.query('member', query).then((result) => { return this.store.query('member', query).then((result) => {

View File

@ -13,9 +13,8 @@
{{svg-jar "search" class="gh-input-search-icon"}} {{svg-jar "search" class="gh-input-search-icon"}}
<GhTextInput <GhTextInput
placeholder="Search members..." placeholder="Search members..."
@disabled={{true}}
@value={{this.searchText}} @value={{this.searchText}}
@input={{action (mut this.searchText) value="target.value"}} @input={{this.search}}
class="gh-members-list-searchfield {{if this.searchText "active"}}" /> class="gh-members-list-searchfield {{if this.searchText "active"}}" />
</div> </div>