From dfb2006eb9021feadcee38f32d0357b9c6bd70c4 Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Tue, 31 Mar 2026 21:44:59 +0200 Subject: [PATCH] feat(interface): add preset filters to search command --- i18n/english.js | 12 ++++ i18n/french.js | 12 ++++ public/components/search-command/filters.js | 7 +++ .../search-command/search-command-panels.js | 23 +++++++ .../search-command/search-command.js | 21 +++++++ test/e2e/search-command.spec.js | 63 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 test/e2e/search-command.spec.js diff --git a/i18n/english.js b/i18n/english.js index 686b2b5e..64a7784c 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -273,6 +273,12 @@ const ui = { placeholder: "Search packages...", placeholder_filter_hint: "or use", placeholder_refine: "Add another filter...", + section_presets: "Quick filters", + preset_has_vulnerabilities: "Has vulnerabilities", + preset_has_scripts: "Has install scripts", + preset_no_license: "No license", + preset_deprecated: "Deprecated", + preset_large: "Large (> 100kb)", section_filters: "Filters", section_flags: "Flags - click to toggle", section_size: "Size - select a preset or type above", @@ -285,6 +291,12 @@ const ui = { hint_size: "e.g. >50kb, 10kb..200kb", hint_version: "e.g. ^1.0.0, >=2.0.0", empty: "No results found", + empty_after_filter: "No packages match the active filters", + preset_empty_has_vulnerabilities: "No package with known vulnerabilities", + preset_empty_has_scripts: "No package with install scripts", + preset_empty_no_license: "All packages have a license", + preset_empty_deprecated: "No deprecated packages", + preset_empty_large: "No package exceeds 100kb", nav_navigate: "navigate", nav_select: "select", nav_remove: "remove filter", diff --git a/i18n/french.js b/i18n/french.js index 22c0a738..6e3acc1b 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -273,6 +273,12 @@ const ui = { placeholder: "Rechercher des packages...", placeholder_filter_hint: "ou utiliser", placeholder_refine: "Ajouter un autre filtre...", + section_presets: "Filtres rapides", + preset_has_vulnerabilities: "Contient des vulnérabilités", + preset_has_scripts: "Scripts d'installation", + preset_no_license: "Sans licence", + preset_deprecated: "Déprécié", + preset_large: "Volumineux (> 100ko)", section_filters: "Filtres", section_flags: "Flags - cliquer pour activer", section_size: "Taille - choisir un préréglage ou saisir ci-dessus", @@ -285,6 +291,12 @@ const ui = { hint_size: "ex. >50kb, 10kb..200kb", hint_version: "ex. ^1.0.0, >=2.0.0", empty: "Aucun résultat trouvé", + empty_after_filter: "Aucun package ne correspond aux filtres actifs", + preset_empty_has_vulnerabilities: "Aucun package avec des vulnérabilités connues", + preset_empty_has_scripts: "Aucun package avec des scripts d'installation", + preset_empty_no_license: "Tous les packages ont une licence", + preset_empty_deprecated: "Aucun package déprécié", + preset_empty_large: "Aucun package ne dépasse 100ko", nav_navigate: "naviguer", nav_select: "sélectionner", nav_remove: "supprimer le filtre", diff --git a/public/components/search-command/filters.js b/public/components/search-command/filters.js index 6e05e27f..36e77e20 100644 --- a/public/components/search-command/filters.js +++ b/public/components/search-command/filters.js @@ -21,6 +21,13 @@ export const VERSION_PRESETS = [ { label: "< 1.0", value: "<1.0.0" } ]; export const FILTERS_NAME = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size"]); +export const PRESETS = [ + { id: "has_vulnerabilities", filter: "flag", value: "hasVulnerabilities" }, + { id: "has_scripts", filter: "flag", value: "hasScript" }, + { id: "no_license", filter: "flag", value: "hasNoLicense" }, + { id: "deprecated", filter: "flag", value: "isDeprecated" }, + { id: "large", filter: "size", value: ">100kb" } +]; // Filters that use a searchable text-based list (not a rich visual panel) export const FILTER_HAS_HELPERS = new Set(["license", "ext", "builtin", "author"]); // Filters where the mode persists after selection (multi-select) diff --git a/public/components/search-command/search-command-panels.js b/public/components/search-command/search-command-panels.js index 86394207..918e3650 100644 --- a/public/components/search-command/search-command-panels.js +++ b/public/components/search-command/search-command-panels.js @@ -136,6 +136,29 @@ export function renderFilterList({ helpers, selectedIndex, onSelect }) { `; } +/** + * @param {{ presets: Array, onApply: Function }} props + */ +export function renderPresets({ presets, onApply }) { + const i18n = window.i18n[currentLang()].search_command; + + return html` +
+
${i18n.section_presets}
+
+
+ ${presets.map((preset) => html` + + `)} +
+
+
+ `; +} + /** * @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props */ diff --git a/public/components/search-command/search-command.js b/public/components/search-command/search-command.js index 3ca97266..3dc3d7bc 100644 --- a/public/components/search-command/search-command.js +++ b/public/components/search-command/search-command.js @@ -9,6 +9,7 @@ import { FILTERS_NAME, FILTER_HAS_HELPERS, FILTER_MULTI_SELECT, + PRESETS, computeMatches, getHelperValues } from "./filters.js"; @@ -18,6 +19,7 @@ import { renderRangePanel, renderListPanel, renderFilterList, + renderPresets, renderResults } from "./search-command-panels.js"; import "./search-chip.js"; @@ -321,6 +323,19 @@ class SearchCommand extends LitElement { this.#close(); } + #getEmptyQueryMessage() { + const i18n = window.i18n[currentLang()].search_command; + if (this.queries.length === 1) { + const { filter, value } = this.queries[0]; + const preset = PRESETS.find((preset) => preset.filter === filter && preset.value === value); + if (preset) { + return i18n[`preset_empty_${preset.id}`] ?? i18n.empty_after_filter; + } + } + + return i18n.empty_after_filter; + } + #focusMultiplePackages(nodeIds) { window.navigation.setNavByName("network--view"); this.#network.highlightMultipleNodes(nodeIds); @@ -401,6 +416,7 @@ class SearchCommand extends LitElement { const helpers = this.#visibleHelpers; const isPanelMode = this.activeFilter !== null; const isEmpty = helpers.length === 0 && this.results.length === 0 && this.inputValue.length > 0; + const isEmptyAfterQuery = this.queries.length > 0 && this.results.length === 0 && this.inputValue === ""; const showRichPlaceholder = this.inputValue === "" && this.queries.length === 0; const showRefinePlaceholder = this.inputValue === "" && this.queries.length > 0; const helperPanel = helpers.length > 0 @@ -463,6 +479,10 @@ class SearchCommand extends LitElement {
${isPanelMode ? this.#renderActiveFilterPanel(helpers) : helperPanel} + ${showRichPlaceholder ? renderPresets({ + presets: PRESETS, + onApply: (preset) => this.#addQuery(preset.filter, preset.value) + }) : nothing} ${renderResults({ results: this.results, selectedIndex: this.selectedIndex, @@ -470,6 +490,7 @@ class SearchCommand extends LitElement { onFocus: (id) => this.#focusPackage(id) })} ${isEmpty ? html`
${i18n.empty}
` : nothing} + ${isEmptyAfterQuery ? html`
${this.#getEmptyQueryMessage()}
` : nothing}