mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-24 13:26:45 +08:00
Instead of simple match keywords, search login was a bit enhanced by calculating score. 20 points if item label starts with searched phrase. This is to prioritize automation when auto is typed; 10 points for each exact word matches keywords. This is to prioritize discourse-ai when ai is typed; 5 points if matches fallback to original phrase match. In addition, stop showing loading sign when modal is opened.
247 lines
6.8 KiB
Text
Vendored
247 lines
6.8 KiB
Text
Vendored
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { hash } from "@ember/helper";
|
|
import { on } from "@ember/modifier";
|
|
import { action } from "@ember/object";
|
|
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
|
import { LinkTo } from "@ember/routing";
|
|
import { service } from "@ember/service";
|
|
import { htmlSafe } from "@ember/template";
|
|
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
|
import { and, not } from "truth-helpers";
|
|
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
|
import DButton from "discourse/components/d-button";
|
|
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";
|
|
import AdminSearchFilters from "admin/components/admin-search-filters";
|
|
import { ADMIN_SEARCH_RESULT_TYPES } from "admin/lib/constants";
|
|
|
|
const ADMIN_SEARCH_FILTERS = "admin_search_filters";
|
|
|
|
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;
|
|
typeFilters = new TrackedObject({
|
|
page: true,
|
|
setting: true,
|
|
theme: true,
|
|
component: true,
|
|
report: true,
|
|
});
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
|
|
if (this.keyValueStore.getItem(ADMIN_SEARCH_FILTERS)) {
|
|
this.typeFilters = new TrackedObject(
|
|
JSON.parse(this.keyValueStore.getItem(ADMIN_SEARCH_FILTERS))
|
|
);
|
|
}
|
|
|
|
this.adminSearchDataSource.buildMap().then(() => {
|
|
if (this.filter !== "") {
|
|
this.loading = true;
|
|
this.runSearch();
|
|
}
|
|
});
|
|
}
|
|
|
|
get visibleTypes() {
|
|
return Object.keys(this.typeFilters).filter(
|
|
(type) => this.typeFilters[type]
|
|
);
|
|
}
|
|
|
|
get noResultsDescription() {
|
|
return i18n("admin.search.no_results", {
|
|
filter: escapeExpression(this.filter),
|
|
});
|
|
}
|
|
|
|
@action
|
|
toggleFilters() {
|
|
this.showFilters = !this.showFilters;
|
|
}
|
|
|
|
@action
|
|
toggleTypeFilter(type) {
|
|
this.typeFilters[type] = !this.typeFilters[type];
|
|
|
|
const allFiltersShowing = Object.values(this.typeFilters).every(
|
|
(value) => value
|
|
);
|
|
|
|
if (!allFiltersShowing) {
|
|
this.keyValueStore.setItem(
|
|
ADMIN_SEARCH_FILTERS,
|
|
JSON.stringify(this.typeFilters)
|
|
);
|
|
} else {
|
|
this.keyValueStore.removeItem(ADMIN_SEARCH_FILTERS);
|
|
}
|
|
|
|
this.search();
|
|
}
|
|
|
|
@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, {
|
|
types: this.visibleTypes,
|
|
});
|
|
this.loading = false;
|
|
}
|
|
|
|
<template>
|
|
<div
|
|
class="admin-search__input-container"
|
|
{{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>
|
|
<DButton class="btn-flat" @icon="filter" @action={{this.toggleFilters}} />
|
|
</div>
|
|
{{#if @fullPageLink}}
|
|
<LinkTo
|
|
@route="adminSearch"
|
|
@query={{hash filter=this.filter}}
|
|
class="admin-search__full-page-link"
|
|
>
|
|
{{i18n "admin.search.full_page_link"}}
|
|
</LinkTo>
|
|
{{/if}}
|
|
|
|
{{#if this.showFilters}}
|
|
<AdminSearchFilters
|
|
@toggleTypeFilter={{this.toggleTypeFilter}}
|
|
@typeFilters={{this.typeFilters}}
|
|
@types={{ADMIN_SEARCH_RESULT_TYPES}}
|
|
/>
|
|
{{/if}}
|
|
|
|
<div class="admin-search__results">
|
|
<ConditionalLoadingSpinner @condition={{this.loading}}>
|
|
{{#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}}
|
|
{{#if (and (not this.searchResults) this.filter)}}
|
|
<p class="admin-search__no-results">
|
|
{{this.noResultsDescription}}
|
|
</p>
|
|
{{/if}}
|
|
</ConditionalLoadingSpinner>
|
|
</div>
|
|
</template>
|
|
}
|