Skip to content

Commit e673342

Browse files
feat(interface): add preset filters to search command (#726)
1 parent ff2bbe3 commit e673342

6 files changed

Lines changed: 138 additions & 0 deletions

File tree

i18n/english.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ const ui = {
273273
placeholder: "Search packages...",
274274
placeholder_filter_hint: "or use",
275275
placeholder_refine: "Add another filter...",
276+
section_presets: "Quick filters",
277+
preset_has_vulnerabilities: "Has vulnerabilities",
278+
preset_has_scripts: "Has install scripts",
279+
preset_no_license: "No license",
280+
preset_deprecated: "Deprecated",
281+
preset_large: "Large (> 100kb)",
276282
section_filters: "Filters",
277283
section_flags: "Flags - click to toggle",
278284
section_size: "Size - select a preset or type above",
@@ -285,6 +291,12 @@ const ui = {
285291
hint_size: "e.g. >50kb, 10kb..200kb",
286292
hint_version: "e.g. ^1.0.0, >=2.0.0",
287293
empty: "No results found",
294+
empty_after_filter: "No packages match the active filters",
295+
preset_empty_has_vulnerabilities: "No package with known vulnerabilities",
296+
preset_empty_has_scripts: "No package with install scripts",
297+
preset_empty_no_license: "All packages have a license",
298+
preset_empty_deprecated: "No deprecated packages",
299+
preset_empty_large: "No package exceeds 100kb",
288300
nav_navigate: "navigate",
289301
nav_select: "select",
290302
nav_remove: "remove filter",

i18n/french.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,12 @@ const ui = {
273273
placeholder: "Rechercher des packages...",
274274
placeholder_filter_hint: "ou utiliser",
275275
placeholder_refine: "Ajouter un autre filtre...",
276+
section_presets: "Filtres rapides",
277+
preset_has_vulnerabilities: "Contient des vulnérabilités",
278+
preset_has_scripts: "Scripts d'installation",
279+
preset_no_license: "Sans licence",
280+
preset_deprecated: "Déprécié",
281+
preset_large: "Volumineux (> 100ko)",
276282
section_filters: "Filtres",
277283
section_flags: "Flags - cliquer pour activer",
278284
section_size: "Taille - choisir un préréglage ou saisir ci-dessus",
@@ -285,6 +291,12 @@ const ui = {
285291
hint_size: "ex. >50kb, 10kb..200kb",
286292
hint_version: "ex. ^1.0.0, >=2.0.0",
287293
empty: "Aucun résultat trouvé",
294+
empty_after_filter: "Aucun package ne correspond aux filtres actifs",
295+
preset_empty_has_vulnerabilities: "Aucun package avec des vulnérabilités connues",
296+
preset_empty_has_scripts: "Aucun package avec des scripts d'installation",
297+
preset_empty_no_license: "Tous les packages ont une licence",
298+
preset_empty_deprecated: "Aucun package déprécié",
299+
preset_empty_large: "Aucun package ne dépasse 100ko",
288300
nav_navigate: "naviguer",
289301
nav_select: "sélectionner",
290302
nav_remove: "supprimer le filtre",

public/components/search-command/filters.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ export const VERSION_PRESETS = [
2121
{ label: "< 1.0", value: "<1.0.0" }
2222
];
2323
export const FILTERS_NAME = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size"]);
24+
export const PRESETS = [
25+
{ id: "has_vulnerabilities", filter: "flag", value: "hasVulnerabilities" },
26+
{ id: "has_scripts", filter: "flag", value: "hasScript" },
27+
{ id: "no_license", filter: "flag", value: "hasNoLicense" },
28+
{ id: "deprecated", filter: "flag", value: "isDeprecated" },
29+
{ id: "large", filter: "size", value: ">100kb" }
30+
];
2431
// Filters that use a searchable text-based list (not a rich visual panel)
2532
export const FILTER_HAS_HELPERS = new Set(["license", "ext", "builtin", "author"]);
2633
// Filters where the mode persists after selection (multi-select)

public/components/search-command/search-command-panels.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,29 @@ export function renderFilterList({ helpers, selectedIndex, onSelect }) {
136136
`;
137137
}
138138

139+
/**
140+
* @param {{ presets: Array, onApply: Function }} props
141+
*/
142+
export function renderPresets({ presets, onApply }) {
143+
const i18n = window.i18n[currentLang()].search_command;
144+
145+
return html`
146+
<div class="section">
147+
<div class="section-title">${i18n.section_presets}</div>
148+
<div class="range-panel">
149+
<div class="range-presets">
150+
${presets.map((preset) => html`
151+
<button
152+
class="range-preset"
153+
@click=${() => onApply(preset)}
154+
>${i18n[`preset_${preset.id}`]}</button>
155+
`)}
156+
</div>
157+
</div>
158+
</div>
159+
`;
160+
}
161+
139162
/**
140163
* @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props
141164
*/

public/components/search-command/search-command.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
FILTERS_NAME,
1010
FILTER_HAS_HELPERS,
1111
FILTER_MULTI_SELECT,
12+
PRESETS,
1213
computeMatches,
1314
getHelperValues
1415
} from "./filters.js";
@@ -18,6 +19,7 @@ import {
1819
renderRangePanel,
1920
renderListPanel,
2021
renderFilterList,
22+
renderPresets,
2123
renderResults
2224
} from "./search-command-panels.js";
2325
import "./search-chip.js";
@@ -321,6 +323,19 @@ class SearchCommand extends LitElement {
321323
this.#close();
322324
}
323325

326+
#getEmptyQueryMessage() {
327+
const i18n = window.i18n[currentLang()].search_command;
328+
if (this.queries.length === 1) {
329+
const { filter, value } = this.queries[0];
330+
const preset = PRESETS.find((preset) => preset.filter === filter && preset.value === value);
331+
if (preset) {
332+
return i18n[`preset_empty_${preset.id}`] ?? i18n.empty_after_filter;
333+
}
334+
}
335+
336+
return i18n.empty_after_filter;
337+
}
338+
324339
#focusMultiplePackages(nodeIds) {
325340
window.navigation.setNavByName("network--view");
326341
this.#network.highlightMultipleNodes(nodeIds);
@@ -401,6 +416,7 @@ class SearchCommand extends LitElement {
401416
const helpers = this.#visibleHelpers;
402417
const isPanelMode = this.activeFilter !== null;
403418
const isEmpty = helpers.length === 0 && this.results.length === 0 && this.inputValue.length > 0;
419+
const isEmptyAfterQuery = this.queries.length > 0 && this.results.length === 0 && this.inputValue === "";
404420
const showRichPlaceholder = this.inputValue === "" && this.queries.length === 0;
405421
const showRefinePlaceholder = this.inputValue === "" && this.queries.length > 0;
406422
const helperPanel = helpers.length > 0
@@ -463,13 +479,18 @@ class SearchCommand extends LitElement {
463479
464480
<div class="panel">
465481
${isPanelMode ? this.#renderActiveFilterPanel(helpers) : helperPanel}
482+
${showRichPlaceholder ? renderPresets({
483+
presets: PRESETS,
484+
onApply: (preset) => this.#addQuery(preset.filter, preset.value)
485+
}) : nothing}
466486
${renderResults({
467487
results: this.results,
468488
selectedIndex: this.selectedIndex,
469489
helperCount: helpers.length,
470490
onFocus: (id) => this.#focusPackage(id)
471491
})}
472492
${isEmpty ? html`<div class="empty-state">${i18n.empty}</div>` : nothing}
493+
${isEmptyAfterQuery ? html`<div class="empty-state">${this.#getEmptyQueryMessage()}</div>` : nothing}
473494
</div>
474495
475496
<div class="search-footer">

test/e2e/search-command.spec.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Import Third-party Dependencies
2+
import { test, expect } from "@playwright/test";
3+
4+
test.describe("[search-command] presets", () => {
5+
let i18n;
6+
7+
test.beforeEach(async({ page }) => {
8+
await page.goto("/");
9+
await page.waitForSelector(`[data-menu="network--view"].active`);
10+
11+
i18n = await page.evaluate(() => {
12+
const lang = document.getElementById("lang").dataset.lang;
13+
const activeLang = lang in window.i18n ? lang : "english";
14+
15+
return window.i18n[activeLang].search_command;
16+
});
17+
18+
await page.locator("body").click();
19+
await page.keyboard.press("Control+k");
20+
21+
await expect(page.locator(".backdrop")).toBeVisible();
22+
});
23+
24+
test("shows the Quick filters section on open", async({ page }) => {
25+
await expect(page.locator(".section-title").filter({ hasText: i18n.section_presets })).toBeVisible();
26+
});
27+
28+
test("renders all five preset buttons", async({ page }) => {
29+
await expect(page.locator(".range-preset")).toHaveCount(5);
30+
});
31+
32+
test("clicking a preset adds a chip and hides the presets section", async({ page }) => {
33+
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();
34+
35+
await expect(page.locator("search-chip")).toBeVisible();
36+
await expect(page.locator(".section-title").filter({ hasText: i18n.section_presets })).not.toBeVisible();
37+
});
38+
39+
test("shows contextual empty message for has-vulnerabilities preset", async({ page }) => {
40+
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();
41+
42+
await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
43+
});
44+
45+
test("shows contextual empty message for deprecated preset", async({ page }) => {
46+
await page.locator(".range-preset").filter({ hasText: i18n.preset_deprecated }).click();
47+
48+
await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_deprecated);
49+
});
50+
51+
test("shows generic empty message when a manual filter yields no results", async({ page }) => {
52+
await page.locator("#cmd-input").fill("flag:hasBannedFile");
53+
await page.keyboard.press("Enter");
54+
55+
await expect(page.locator(".empty-state")).toHaveText(i18n.empty_after_filter);
56+
});
57+
58+
test("pressing Escape closes the palette", async({ page }) => {
59+
await page.keyboard.press("Escape");
60+
61+
await expect(page.locator(".backdrop")).not.toBeVisible();
62+
});
63+
});

0 commit comments

Comments
 (0)