Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions public/components/search-command/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions public/components/search-command/search-command-panels.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<div class="section">
<div class="section-title">${i18n.section_presets}</div>
<div class="range-panel">
<div class="range-presets">
${presets.map((preset) => html`
<button
class="range-preset"
@click=${() => onApply(preset)}
>${i18n[`preset_${preset.id}`]}</button>
`)}
</div>
</div>
</div>
`;
}

/**
* @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props
*/
Expand Down
21 changes: 21 additions & 0 deletions public/components/search-command/search-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
FILTERS_NAME,
FILTER_HAS_HELPERS,
FILTER_MULTI_SELECT,
PRESETS,
computeMatches,
getHelperValues
} from "./filters.js";
Expand All @@ -18,6 +19,7 @@ import {
renderRangePanel,
renderListPanel,
renderFilterList,
renderPresets,
renderResults
} from "./search-command-panels.js";
import "./search-chip.js";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -463,13 +479,18 @@ class SearchCommand extends LitElement {

<div class="panel">
${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,
helperCount: helpers.length,
onFocus: (id) => this.#focusPackage(id)
})}
${isEmpty ? html`<div class="empty-state">${i18n.empty}</div>` : nothing}
${isEmptyAfterQuery ? html`<div class="empty-state">${this.#getEmptyQueryMessage()}</div>` : nothing}
</div>

<div class="search-footer">
Expand Down
63 changes: 63 additions & 0 deletions test/e2e/search-command.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Import Third-party Dependencies
import { test, expect } from "@playwright/test";

test.describe("[search-command] presets", () => {
let i18n;

test.beforeEach(async({ page }) => {
await page.goto("/");
await page.waitForSelector(`[data-menu="network--view"].active`);

i18n = await page.evaluate(() => {
const lang = document.getElementById("lang").dataset.lang;
const activeLang = lang in window.i18n ? lang : "english";

return window.i18n[activeLang].search_command;
});

await page.locator("body").click();
await page.keyboard.press("Control+k");

await expect(page.locator(".backdrop")).toBeVisible();
});

test("shows the Quick filters section on open", async({ page }) => {
await expect(page.locator(".section-title").filter({ hasText: i18n.section_presets })).toBeVisible();
});

test("renders all five preset buttons", async({ page }) => {
await expect(page.locator(".range-preset")).toHaveCount(5);
});

test("clicking a preset adds a chip and hides the presets section", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();

await expect(page.locator("search-chip")).toBeVisible();
await expect(page.locator(".section-title").filter({ hasText: i18n.section_presets })).not.toBeVisible();
});

test("shows contextual empty message for has-vulnerabilities preset", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();

await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
});

test("shows contextual empty message for deprecated preset", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_deprecated }).click();

await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_deprecated);
});

test("shows generic empty message when a manual filter yields no results", async({ page }) => {
await page.locator("#cmd-input").fill("flag:hasBannedFile");
await page.keyboard.press("Enter");

await expect(page.locator(".empty-state")).toHaveText(i18n.empty_after_filter);
});

test("pressing Escape closes the palette", async({ page }) => {
await page.keyboard.press("Escape");

await expect(page.locator(".backdrop")).not.toBeVisible();
});
});
Loading