mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-16 16:36:10 +08:00
What is the problem? When we construct the admin search component, we fire off a call to get data from the back-end. When this is done, there's an async callback that re-does the search. This can result in confusing flickering of the results. This was compounded by a separate issue that caused the data endpoint to be extremely slow if we had two specific plugins installed at the same time. That issue has already been resolved. The first time we construct the component (and fetch the data) we initialize it with a flag to indicate that data is still loading. Until the data is loaded, we will show the regular loading spinner if the admin starts typing. On Meta the endpoint takes ~600ms. That countdown starts when you click or use the shortcut, then you need to start typing as well, so this delay shouldn't be a big issue.
208 lines
5.7 KiB
Text
208 lines
5.7 KiB
Text
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
|
import { service } from "@ember/service";
|
|
import { htmlSafe } from "@ember/template";
|
|
import { and, not } from "truth-helpers";
|
|
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
|
import icon from "discourse/helpers/d-icon";
|
|
import discourseDebounce from "discourse/lib/debounce";
|
|
import { INPUT_DELAY } from "discourse/lib/environment";
|
|
import { escapeExpression } from "discourse/lib/utilities";
|
|
import autoFocus from "discourse/modifiers/auto-focus";
|
|
import { i18n } from "discourse-i18n";
|
|
|
|
export default class AdminSearch extends Component {
|
|
@service adminSearchDataSource;
|
|
@service keyValueStore;
|
|
@service router;
|
|
|
|
@tracked filter = this.args.initialFilter ?? "";
|
|
@tracked searchResults = [];
|
|
@tracked showFilters = true;
|
|
@tracked loading = false;
|
|
@tracked dataReady = false;
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
|
|
this.adminSearchDataSource.buildMap().then(() => {
|
|
this.dataReady = true;
|
|
if (this.filter !== "") {
|
|
this.loading = true;
|
|
this.runSearch();
|
|
}
|
|
});
|
|
}
|
|
|
|
get noResultsDescription() {
|
|
return i18n("admin.search.no_results", {
|
|
filter: escapeExpression(this.filter),
|
|
});
|
|
}
|
|
|
|
@action
|
|
changeSearchTerm(event) {
|
|
this.searchResults = [];
|
|
this.filter = event.target.value;
|
|
if (this.filter.length > 0) {
|
|
this.runSearch();
|
|
}
|
|
}
|
|
|
|
@action
|
|
search() {
|
|
discourseDebounce(this, this.#search, INPUT_DELAY);
|
|
}
|
|
|
|
// TODO (martin) Maybe we can move ListHandler / iterate-list from chat into
|
|
// core so we can use it here too.
|
|
@action
|
|
handleResultKeyDown(event) {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.target.click();
|
|
}
|
|
|
|
if (event.key === "ArrowUp") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const previousResult = event.target.parentElement.previousElementSibling;
|
|
if (previousResult) {
|
|
previousResult.firstElementChild?.focus();
|
|
} else {
|
|
document.querySelector(".admin-search__input-field").focus();
|
|
}
|
|
}
|
|
|
|
if (event.key === "ArrowDown") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.target.parentElement.nextElementSibling.firstElementChild?.focus();
|
|
}
|
|
}
|
|
|
|
@action
|
|
handleSearchKeyDown(event) {
|
|
if (event.key === "ArrowDown") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
document
|
|
.querySelector(".admin-search__result .admin-search__result-link")
|
|
.focus();
|
|
}
|
|
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
this.router.transitionTo("adminSearch.index", {
|
|
queryParams: { filter: this.filter },
|
|
});
|
|
}
|
|
}
|
|
|
|
@action
|
|
initialFilterUpdated() {
|
|
this.filter = this.args.initialFilter;
|
|
this.runSearch();
|
|
}
|
|
|
|
@action
|
|
runSearch() {
|
|
this.loading = true;
|
|
this.search();
|
|
}
|
|
|
|
#search() {
|
|
this.searchResults = this.adminSearchDataSource.search(this.filter);
|
|
this.loading = false;
|
|
}
|
|
|
|
get showLoadingSpinner() {
|
|
return this.filter !== "" && (this.loading || !this.dataReady);
|
|
}
|
|
|
|
<template>
|
|
<div
|
|
class="admin-search__input-container
|
|
{{if this.searchResults '--has-results'}}
|
|
"
|
|
{{didUpdate this.initialFilterUpdated @initialFilter}}
|
|
>
|
|
<div class="admin-search__input-group">
|
|
{{icon "magnifying-glass" class="admin-search__input-icon"}}
|
|
<input
|
|
type="text"
|
|
class="admin-search__input-field"
|
|
value={{this.filter}}
|
|
{{autoFocus}}
|
|
{{on "input" this.changeSearchTerm}}
|
|
{{on "keydown" this.handleSearchKeyDown}}
|
|
placeholder={{i18n "admin.search.instructions"}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="sr-only" aria-live="polite" role="status">
|
|
{{#if this.searchResults}}
|
|
{{i18n
|
|
"admin.search.result_count"
|
|
count=this.searchResults.length
|
|
filter=this.filter
|
|
}}
|
|
{{/if}}
|
|
{{#if
|
|
(and
|
|
this.filter
|
|
(not this.searchResults.length)
|
|
(not this.showLoadingSpinner)
|
|
)
|
|
}}
|
|
{{this.noResultsDescription}}
|
|
{{/if}}
|
|
</div>
|
|
{{#if
|
|
(and
|
|
this.filter
|
|
(not this.searchResults.length)
|
|
(not this.showLoadingSpinner)
|
|
)
|
|
}}
|
|
<p class="admin-search__no-results" aria-live="polite" role="status">
|
|
{{this.noResultsDescription}}
|
|
</p>
|
|
{{/if}}
|
|
<div
|
|
class="admin-search__results {{if this.searchResults '--has-results'}}"
|
|
>
|
|
<ConditionalLoadingSpinner @condition={{this.showLoadingSpinner}}>
|
|
{{#each this.searchResults as |result|}}
|
|
<div class="admin-search__result" data-result-type={{result.type}}>
|
|
<a
|
|
href={{result.url}}
|
|
{{on "keydown" this.handleResultKeyDown}}
|
|
class="admin-search__result-link"
|
|
tabindex="0"
|
|
>
|
|
<div class="admin-search__result-name">
|
|
{{#if result.icon}}
|
|
{{icon result.icon}}
|
|
{{/if}}
|
|
<span
|
|
class="admin-search__result-name-label"
|
|
>{{result.label}}</span>
|
|
</div>
|
|
{{#if result.description}}
|
|
<div class="admin-search__result-description">{{htmlSafe
|
|
result.description
|
|
}}</div>
|
|
{{/if}}
|
|
</a>
|
|
</div>
|
|
{{/each}}
|
|
</ConditionalLoadingSpinner>
|
|
</div>
|
|
</template>
|
|
}
|