discourse/app/assets/javascripts/admin/addon/components/admin-search.gjs
Martin Brennan e26a1175d7
FEATURE: Initial version of experimental admin search (#31299)
This feature allows admins to find what they are
looking for in the admin interface via a search modal.
This replaces the admin sidebar filter
as the focus of the Ctrl+/ command, but the sidebar
filter can also still be used. Perhaps at some point
we may remove it or change the shortcut.

The search modal presents the following data for filtering:

* A list of all admin pages, the same as the sidebar,
   except also showing "third level" pages like
   "Email > Skipped"
* All site settings
* Themes
* Components
* Reports

Admins can also filter which types of items are shown in the modal,
for example hiding Settings if they know they are looking for a Page.

In this PR, I also have the following fixes:

* Site setting filters now clear when moving between
   filtered site setting pages, previously it was super
   sticky from Ember
* Many translations were moved around, instead of being
   in various namespaces for the sidebar links and the admin
   page titles and descriptions, now everything is under
   `admin.config` namespace, this makes it way easier to reuse
   this text for pages, search, and sidebar, and if you change it
   in one place then it is changed everywhere.

---------

Co-authored-by: Ella <ella.estigoy@gmail.com>
2025-02-21 11:59:24 +10:00

124 lines
3.5 KiB
Text
Vendored

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { TrackedObject } from "@ember-compat/tracked-built-ins";
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 autoFocus from "discourse/modifiers/auto-focus";
import AdminSearchFilters from "admin/components/admin-search-filters";
import { RESULT_TYPES } from "admin/services/admin-search-data-source";
export default class AdminSearch extends Component {
@service adminSearchDataSource;
@tracked filter = "";
@tracked searchResults = [];
@tracked showFilters = false;
@tracked loading = false;
typeFilters = new TrackedObject({
page: true,
setting: true,
theme: true,
component: true,
report: true,
});
constructor() {
super(...arguments);
this.adminSearchDataSource.buildMap();
}
get visibleTypes() {
return Object.keys(this.typeFilters).filter(
(type) => this.typeFilters[type]
);
}
get showLoadingSpinner() {
return !this.adminSearchDataSource.isLoaded || this.loading;
}
@action
toggleFilters() {
this.showFilters = !this.showFilters;
}
@action
toggleTypeFilter(type) {
this.typeFilters[type] = !this.typeFilters[type];
this.search();
}
@action
changeSearchTerm(event) {
this.searchResults = [];
this.filter = event.target.value;
this.loading = true;
this.search();
}
@action
search() {
discourseDebounce(this, this.#search, INPUT_DELAY);
}
#search() {
this.searchResults = this.adminSearchDataSource.search(this.filter, {
types: this.visibleTypes,
});
this.loading = false;
}
<template>
<div class="admin-search__input-container">
<div class="admin-search__input-group">
{{icon "magnifying-glass" class="admin-search__input-icon"}}
<input
type="text"
class="admin-search__input-field"
{{autoFocus}}
{{on "input" this.changeSearchTerm}}
/>
</div>
<DButton class="btn-flat" @icon="filter" @action={{this.toggleFilters}} />
</div>
{{#if this.showFilters}}
<AdminSearchFilters
@toggleTypeFilter={{this.toggleTypeFilter}}
@typeFilters={{this.typeFilters}}
@types={{RESULT_TYPES}}
/>
{{/if}}
<div class="admin-search__results">
<ConditionalLoadingSpinner @condition={{this.showLoadingSpinner}}>
{{#each this.searchResults as |result|}}
<div class="admin-search__result">
<a href={{result.url}}>
<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>
}