Skip to content

Commit 6f14360

Browse files
feat(interface): add reset view, copy package & export payload actions to command palette (#744)
1 parent 90219db commit 6f14360

5 files changed

Lines changed: 143 additions & 16 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ reports
7777

7878
# playwright test results
7979
test-results
80+
81+
# IDEs
82+
.vscode
83+
.idea

i18n/english.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ const ui = {
281281
section_actions: "Actions",
282282
action_toggle_theme_to_dark: "Switch to dark theme",
283283
action_toggle_theme_to_light: "Switch to light theme",
284+
action_reset_view: "Reset view",
285+
action_copy_packages: "Copy packages",
286+
action_export_payload: "Export payload",
284287
section_presets: "Quick filters",
285288
preset_has_vulnerabilities: "Has vulnerabilities",
286289
preset_has_scripts: "Has install scripts",

i18n/french.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,9 @@ const ui = {
281281
section_actions: "Actions",
282282
action_toggle_theme_to_dark: "Passer en thème sombre",
283283
action_toggle_theme_to_light: "Passer en thème clair",
284+
action_reset_view: "Réinitialiser la vue",
285+
action_copy_packages: "Copier les packages",
286+
action_export_payload: "Exporter le payload",
284287
section_presets: "Filtres rapides",
285288
preset_has_vulnerabilities: "Contient des vulnérabilités",
286289
preset_has_scripts: "Scripts d'installation",

public/components/command-palette/command-palette.js

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ import "./search-chip.js";
3030

3131
// CONSTANTS
3232
const kActions = [
33-
{ id: "toggle_theme", shortcut: "t" }
33+
{ id: "toggle_theme", shortcut: "t" },
34+
{ id: "reset_view", shortcut: "r" },
35+
{ id: "copy_packages", shortcut: "c" },
36+
{ id: "export_payload", shortcut: "e" }
3437
];
3538
const kWarningItems = Object.keys(warnings)
3639
.map((id) => {
@@ -364,12 +367,50 @@ class CommandPalette extends LitElement {
364367
this.#close();
365368
}
366369

367-
#executeAction(action) {
368-
if (action.id === "toggle_theme") {
369-
const nextTheme = window.settings.config.theme === "dark" ? "light" : "dark";
370-
window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
371-
detail: { ...window.settings.config, theme: nextTheme }
372-
}));
370+
async #executeAction(action) {
371+
switch (action.id) {
372+
case "toggle_theme": {
373+
const nextTheme = window.settings.config.theme === "dark" ? "light" : "dark";
374+
window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
375+
detail: { ...window.settings.config, theme: nextTheme }
376+
}));
377+
break;
378+
}
379+
case "reset_view":
380+
this.#network.network.emit("click", { nodes: [], edges: [] });
381+
this.#network.network.focus(0, {
382+
animation: true,
383+
scale: 0.35,
384+
offset: { x: 150, y: 0 }
385+
});
386+
break;
387+
case "copy_packages": {
388+
const packages = this.results.length > 0 ? this.results : this.#packages;
389+
const text = packages.map((pkg) => `${pkg.name}@${pkg.version}`).join("\n");
390+
try {
391+
await navigator.clipboard.writeText(text);
392+
}
393+
catch (error) {
394+
console.error(error);
395+
}
396+
break;
397+
}
398+
case "export_payload": {
399+
try {
400+
const res = await fetch("/data");
401+
const blob = await res.blob();
402+
const url = URL.createObjectURL(blob);
403+
const anchor = document.createElement("a");
404+
anchor.href = url;
405+
anchor.download = "nsecure-result.json";
406+
anchor.click();
407+
URL.revokeObjectURL(url);
408+
}
409+
catch (error) {
410+
console.error(error);
411+
}
412+
break;
413+
}
373414
}
374415

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

502544
return kActions.map((action) => {
503-
return {
504-
...action,
505-
label: i18n[`action_${action.id}_to_${targetTheme}`],
506-
kbd: resolveKbd(action.shortcut)
507-
};
545+
let label;
546+
switch (action.id) {
547+
case "toggle_theme":
548+
label = i18n[`action_toggle_theme_to_${targetTheme}`];
549+
break;
550+
case "reset_view":
551+
label = i18n.action_reset_view;
552+
break;
553+
case "copy_packages":
554+
label = `${i18n.action_copy_packages} (${copyCount})`;
555+
break;
556+
case "export_payload":
557+
label = i18n.action_export_payload;
558+
break;
559+
default:
560+
label = action.id;
561+
}
562+
563+
return { ...action, label, kbd: resolveKbd(action.shortcut) };
508564
});
509565
}
510566

@@ -584,7 +640,7 @@ class CommandPalette extends LitElement {
584640
presets: PRESETS,
585641
onApply: (preset) => this.#addQuery(preset.filter, preset.value)
586642
}) : nothing}
587-
${showRichPlaceholder ? renderActions({
643+
${(showRichPlaceholder || showRefinePlaceholder) ? renderActions({
588644
actions: this.#resolveActions(),
589645
onExecute: (action) => this.#executeAction(action)
590646
}) : nothing}

test/e2e/command-palette.spec.js

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ test.describe("[command-palette] presets and actions", () => {
3030
await expect(presetsSection.locator(".range-preset")).toHaveCount(5);
3131
});
3232

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

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

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

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

9091
await expect(page.locator(".backdrop")).not.toBeVisible();
9192
});
93+
94+
test("actions section remains visible after a filter chip is applied", async({ page }) => {
95+
await page.locator(".range-preset").filter({ hasText: i18n.preset_deprecated }).click();
96+
97+
await expect(page.locator(".section").filter({ hasText: i18n.section_actions })).toBeVisible();
98+
});
99+
100+
test("clicking reset view closes the palette", async({ page }) => {
101+
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
102+
await actionsSection.locator(".range-preset").filter({ hasText: i18n.action_reset_view }).click();
103+
104+
await expect(page.locator(".backdrop")).not.toBeVisible();
105+
});
106+
107+
test("Alt+R triggers reset view and closes the palette", async({ page }) => {
108+
await page.keyboard.press("Alt+r");
109+
110+
await expect(page.locator(".backdrop")).not.toBeVisible();
111+
});
112+
113+
test("clicking copy packages closes the palette and writes specs to clipboard", async({ page }) => {
114+
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
115+
116+
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
117+
await actionsSection.locator(".range-preset").filter({ hasText: i18n.action_copy_packages }).click();
118+
119+
await expect(page.locator(".backdrop")).not.toBeVisible();
120+
121+
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
122+
expect(clipboardText.length).toBeGreaterThan(0);
123+
expect(clipboardText).toContain("@");
124+
});
125+
126+
test("Alt+C triggers copy packages and closes the palette", async({ page }) => {
127+
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
128+
await page.keyboard.press("Alt+c");
129+
130+
await expect(page.locator(".backdrop")).not.toBeVisible();
131+
});
132+
133+
test("clicking export payload closes the palette and triggers a download", async({ page }) => {
134+
const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions });
135+
const [download] = await Promise.all([
136+
page.waitForEvent("download"),
137+
actionsSection.locator(".range-preset").filter({ hasText: i18n.action_export_payload }).click()
138+
]);
139+
140+
await expect(page.locator(".backdrop")).not.toBeVisible();
141+
expect(download.suggestedFilename()).toBe("nsecure-result.json");
142+
});
143+
144+
test("Alt+E triggers export payload and closes the palette", async({ page }) => {
145+
const [download] = await Promise.all([
146+
page.waitForEvent("download"),
147+
page.keyboard.press("Alt+e")
148+
]);
149+
150+
await expect(page.locator(".backdrop")).not.toBeVisible();
151+
expect(download.suggestedFilename()).toBe("nsecure-result.json");
152+
});
92153
});
93154

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

0 commit comments

Comments
 (0)