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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ reports

# playwright test results
test-results

# IDEs
.vscode
.idea
3 changes: 3 additions & 0 deletions i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ const ui = {
section_actions: "Actions",
action_toggle_theme_to_dark: "Switch to dark theme",
action_toggle_theme_to_light: "Switch to light theme",
action_reset_view: "Reset view",
action_copy_packages: "Copy packages",
action_export_payload: "Export payload",
section_presets: "Quick filters",
preset_has_vulnerabilities: "Has vulnerabilities",
preset_has_scripts: "Has install scripts",
Expand Down
3 changes: 3 additions & 0 deletions i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,9 @@ const ui = {
section_actions: "Actions",
action_toggle_theme_to_dark: "Passer en thème sombre",
action_toggle_theme_to_light: "Passer en thème clair",
action_reset_view: "Réinitialiser la vue",
action_copy_packages: "Copier les packages",
action_export_payload: "Exporter le payload",
section_presets: "Filtres rapides",
preset_has_vulnerabilities: "Contient des vulnérabilités",
preset_has_scripts: "Scripts d'installation",
Expand Down
82 changes: 69 additions & 13 deletions public/components/command-palette/command-palette.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import "./search-chip.js";

// CONSTANTS
const kActions = [
{ id: "toggle_theme", shortcut: "t" }
{ id: "toggle_theme", shortcut: "t" },
{ id: "reset_view", shortcut: "r" },
{ id: "copy_packages", shortcut: "c" },
{ id: "export_payload", shortcut: "e" }
];
const kWarningItems = Object.keys(warnings)
.map((id) => {
Expand Down Expand Up @@ -364,12 +367,50 @@ class CommandPalette extends LitElement {
this.#close();
}

#executeAction(action) {
if (action.id === "toggle_theme") {
const nextTheme = window.settings.config.theme === "dark" ? "light" : "dark";
window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
detail: { ...window.settings.config, theme: nextTheme }
}));
async #executeAction(action) {
switch (action.id) {
case "toggle_theme": {
const nextTheme = window.settings.config.theme === "dark" ? "light" : "dark";
window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
detail: { ...window.settings.config, theme: nextTheme }
}));
break;
}
case "reset_view":
this.#network.network.emit("click", { nodes: [], edges: [] });
this.#network.network.focus(0, {
animation: true,
scale: 0.35,
offset: { x: 150, y: 0 }
});
break;
case "copy_packages": {
const packages = this.results.length > 0 ? this.results : this.#packages;
const text = packages.map((pkg) => `${pkg.name}@${pkg.version}`).join("\n");
try {
await navigator.clipboard.writeText(text);
}
catch (error) {
console.error(error);
}
break;
}
case "export_payload": {
try {
const res = await fetch("/data");
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = "nsecure-result.json";
anchor.click();
URL.revokeObjectURL(url);
}
catch (error) {
console.error(error);
}
break;
}
}

this.#close();
Expand Down Expand Up @@ -498,13 +539,28 @@ class CommandPalette extends LitElement {
const i18n = window.i18n[currentLang()].search_command;
const currentTheme = window.settings?.config?.theme ?? "light";
const targetTheme = currentTheme === "dark" ? "light" : "dark";
const copyCount = this.results.length > 0 ? this.results.length : this.#packages.length;

return kActions.map((action) => {
return {
...action,
label: i18n[`action_${action.id}_to_${targetTheme}`],
kbd: resolveKbd(action.shortcut)
};
let label;
switch (action.id) {
case "toggle_theme":
label = i18n[`action_toggle_theme_to_${targetTheme}`];
break;
case "reset_view":
label = i18n.action_reset_view;
break;
case "copy_packages":
label = `${i18n.action_copy_packages} (${copyCount})`;
break;
case "export_payload":
label = i18n.action_export_payload;
break;
default:
label = action.id;
}

return { ...action, label, kbd: resolveKbd(action.shortcut) };
});
}

Expand Down Expand Up @@ -584,7 +640,7 @@ class CommandPalette extends LitElement {
presets: PRESETS,
onApply: (preset) => this.#addQuery(preset.filter, preset.value)
}) : nothing}
${showRichPlaceholder ? renderActions({
${(showRichPlaceholder || showRefinePlaceholder) ? renderActions({
actions: this.#resolveActions(),
onExecute: (action) => this.#executeAction(action)
}) : nothing}
Expand Down
67 changes: 64 additions & 3 deletions test/e2e/command-palette.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ test.describe("[command-palette] presets and actions", () => {
await expect(presetsSection.locator(".range-preset")).toHaveCount(5);
});

test("renders the theme toggle action button", async({ page }) => {
test("renders all four action buttons", async({ page }) => {
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
await expect(actionsSection.locator(".range-preset")).toHaveCount(1);
await expect(actionsSection.locator(".range-preset")).toHaveCount(4);
});

test("clicking a preset adds a chip and hides the presets section", async({ page }) => {
Expand Down Expand Up @@ -66,7 +66,8 @@ test.describe("[command-palette] presets and actions", () => {
const expectedTheme = initialTheme === "dark" ? "light" : "dark";

const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
await actionsSection.locator(".range-preset").click();
const toggleLabel = i18n[`action_toggle_theme_to_${expectedTheme}`];
await actionsSection.locator(".range-preset").filter({ hasText: toggleLabel }).click();

await expect(page.locator(".backdrop")).not.toBeVisible();
const newTheme = await page.evaluate(() => window.settings.config.theme);
Expand All @@ -89,6 +90,66 @@ test.describe("[command-palette] presets and actions", () => {

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

test("actions section remains visible after a filter chip is applied", async({ page }) => {
await page.locator(".range-preset").filter({ hasText: i18n.preset_deprecated }).click();

await expect(page.locator(".section").filter({ hasText: i18n.section_actions })).toBeVisible();
});

test("clicking reset view closes the palette", async({ page }) => {
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
await actionsSection.locator(".range-preset").filter({ hasText: i18n.action_reset_view }).click();

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

test("Alt+R triggers reset view and closes the palette", async({ page }) => {
await page.keyboard.press("Alt+r");

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

test("clicking copy packages closes the palette and writes specs to clipboard", async({ page }) => {
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);

const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
await actionsSection.locator(".range-preset").filter({ hasText: i18n.action_copy_packages }).click();

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

const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
expect(clipboardText.length).toBeGreaterThan(0);
expect(clipboardText).toContain("@");
});

test("Alt+C triggers copy packages and closes the palette", async({ page }) => {
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await page.keyboard.press("Alt+c");

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

test("clicking export payload closes the palette and triggers a download", async({ page }) => {
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
const [download] = await Promise.all([
page.waitForEvent("download"),
actionsSection.locator(".range-preset").filter({ hasText: i18n.action_export_payload }).click()
]);

await expect(page.locator(".backdrop")).not.toBeVisible();
expect(download.suggestedFilename()).toBe("nsecure-result.json");
});

test("Alt+E triggers export payload and closes the palette", async({ page }) => {
const [download] = await Promise.all([
page.waitForEvent("download"),
page.keyboard.press("Alt+e")
]);

await expect(page.locator(".backdrop")).not.toBeVisible();
expect(download.suggestedFilename()).toBe("nsecure-result.json");
});
});

test.describe("[command-palette] ignore flags and warnings", () => {
Expand Down
Loading