Skip to content

Commit a299d76

Browse files
feat(interface): add flag and warning ignore toggles to command palette (#731)
1 parent 15af02d commit a299d76

7 files changed

Lines changed: 237 additions & 18 deletions

File tree

i18n/english.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ const ui = {
300300
preset_empty_no_license: "All packages have a license",
301301
preset_empty_deprecated: "No deprecated packages",
302302
preset_empty_large: "No package exceeds 100kb",
303+
section_ignore_flags: "Ignore flags",
304+
section_ignore_warnings: "Ignore warnings",
303305
nav_navigate: "navigate",
304306
nav_select: "select",
305307
nav_remove: "remove filter",

i18n/french.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ const ui = {
300300
preset_empty_no_license: "Tous les packages ont une licence",
301301
preset_empty_deprecated: "Aucun package déprécié",
302302
preset_empty_large: "Aucun package ne dépasse 100ko",
303+
section_ignore_flags: "Ignorer les flags",
304+
section_ignore_warnings: "Ignorer les avertissements",
303305
nav_navigate: "naviguer",
304306
nav_select: "sélectionner",
305307
nav_remove: "supprimer le filtre",

public/common/flags.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Import Third-party Dependencies
2+
import { getManifest } from "@nodesecure/flags/web";
3+
4+
// CONSTANTS
5+
export const IGNORABLE_FLAGS = new Set([
6+
"hasManyPublishers",
7+
"hasIndirectDependencies",
8+
"hasMissingOrUnusedDependency",
9+
"isDead",
10+
"isOutdated",
11+
"hasDuplicate"
12+
]);
13+
export const FLAG_IGNORE_ITEMS = Object.values(getManifest())
14+
.filter(({ title }) => IGNORABLE_FLAGS.has(title))
15+
.map(({ title, emoji }) => {
16+
return { value: title, label: title, emoji };
17+
});

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,33 @@ export function renderActions({ actions, onExecute }) {
185185
`;
186186
}
187187

188+
/**
189+
* @param {{ title: string, items: Array<{value: string, label: string, emoji?: string}>, ignored: Set<string>, onToggle: Function }} props
190+
*/
191+
export function renderIgnorePanel({ title, items, ignored, onToggle }) {
192+
return html`
193+
<div class="section">
194+
<div class="section-title">${title}</div>
195+
<div class="flag-grid">
196+
${repeat(items, (item) => item.value, (item) => {
197+
const isIgnored = ignored.has(item.value);
198+
199+
return html`
200+
<div
201+
class=${classMap({ "flag-chip": true, "flag-active": isIgnored })}
202+
title=${item.value}
203+
@click=${() => onToggle(item.value)}
204+
>
205+
${item.emoji ? html`<span class="flag-emoji">${item.emoji}</span>` : nothing}
206+
<span class="flag-name">${item.label}</span>
207+
</div>
208+
`;
209+
})}
210+
</div>
211+
</div>
212+
`;
213+
}
214+
188215
/**
189216
* @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props
190217
*/

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Import Third-party Dependencies
22
import { LitElement, html, nothing } from "lit";
33
import { repeat } from "lit/directives/repeat.js";
4+
import { warnings } from "@nodesecure/js-x-ray/warnings";
45

56
// Import Internal Dependencies
67
import { currentLang, vec2Distance } from "../../common/utils.js";
@@ -13,6 +14,7 @@ import {
1314
computeMatches,
1415
getHelperValues
1516
} from "./filters.js";
17+
import { FLAG_IGNORE_ITEMS } from "../../common/flags.js";
1618
import { commandPaletteStyles } from "./command-palette-styles.js";
1719
import {
1820
renderFlagPanel,
@@ -21,6 +23,7 @@ import {
2123
renderFilterList,
2224
renderPresets,
2325
renderActions,
26+
renderIgnorePanel,
2427
renderResults
2528
} from "./command-palette-panels.js";
2629
import "./search-chip.js";
@@ -29,6 +32,10 @@ import "./search-chip.js";
2932
const kActions = [
3033
{ id: "toggle_theme", shortcut: "t" }
3134
];
35+
const kWarningItems = Object.keys(warnings)
36+
.map((id) => {
37+
return { value: id, label: id.replaceAll("-", " ") };
38+
});
3239

3340
function resolveKbd(shortcut) {
3441
if (!shortcut) {
@@ -107,15 +114,23 @@ class CommandPalette extends LitElement {
107114
this.results = [];
108115
}
109116

117+
#onSettingsSaved = () => {
118+
if (this.open) {
119+
this.requestUpdate();
120+
}
121+
};
122+
110123
connectedCallback() {
111124
super.connectedCallback();
112125
document.addEventListener("keydown", this.#handleKeydown);
113126
window.addEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init);
127+
window.addEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
114128
}
115129

116130
disconnectedCallback() {
117131
document.removeEventListener("keydown", this.#handleKeydown);
118132
window.removeEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init);
133+
window.removeEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
119134
super.disconnectedCallback();
120135
}
121136

@@ -360,6 +375,41 @@ class CommandPalette extends LitElement {
360375
this.#close();
361376
}
362377

378+
#toggleIgnore(type, value) {
379+
const ignoreSet = window.settings.config.ignore[type];
380+
if (ignoreSet.has(value)) {
381+
ignoreSet.delete(value);
382+
}
383+
else {
384+
ignoreSet.add(value);
385+
}
386+
387+
const config = window.settings.config;
388+
fetch("/config", {
389+
method: "put",
390+
body: JSON.stringify({
391+
...config,
392+
ignore: {
393+
warnings: [...config.ignore.warnings],
394+
flags: [...config.ignore.flags]
395+
}
396+
}),
397+
headers: { "content-type": "application/json" }
398+
}).catch(console.error);
399+
400+
window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, {
401+
detail: {
402+
...config,
403+
ignore: {
404+
warnings: config.ignore.warnings,
405+
flags: config.ignore.flags
406+
}
407+
}
408+
}));
409+
410+
this.requestUpdate();
411+
}
412+
363413
#getEmptyQueryMessage() {
364414
const i18n = window.i18n[currentLang()].search_command;
365415
if (this.queries.length === 1) {
@@ -538,6 +588,18 @@ class CommandPalette extends LitElement {
538588
actions: this.#resolveActions(),
539589
onExecute: (action) => this.#executeAction(action)
540590
}) : nothing}
591+
${showRichPlaceholder ? renderIgnorePanel({
592+
title: i18n.section_ignore_flags,
593+
items: FLAG_IGNORE_ITEMS,
594+
ignored: window.settings?.config?.ignore?.flags ?? new Set(),
595+
onToggle: (value) => this.#toggleIgnore("flags", value)
596+
}) : nothing}
597+
${showRichPlaceholder ? renderIgnorePanel({
598+
title: i18n.section_ignore_warnings,
599+
items: kWarningItems,
600+
ignored: window.settings?.config?.ignore?.warnings ?? new Set(),
601+
onToggle: (value) => this.#toggleIgnore("warnings", value)
602+
}) : nothing}
541603
${renderResults({
542604
results: this.results,
543605
selectedIndex: this.selectedIndex,

public/components/views/settings/settings.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import { LitElement, html, css, nothing } from "lit";
33
import { getJSON } from "@nodesecure/vis-network";
44
import { warnings } from "@nodesecure/js-x-ray/warnings";
5-
import { getManifest } from "@nodesecure/flags/web";
65

76
// Import Internal Dependencies
87
import { EVENTS } from "../../../core/events.js";
98
import { currentLang } from "../../../common/utils.js";
9+
import { FLAG_IGNORE_ITEMS } from "../../../common/flags.js";
1010

1111
// CONSTANTS
1212
const kAllowedHotKeys = new Set([
@@ -25,19 +25,6 @@ const kDefaultHotKeys = {
2525
warnings: "A"
2626
};
2727
const kShortcutInputTargetIds = new Set(Object.keys(kDefaultHotKeys));
28-
const kIgnorableFlags = new Set([
29-
"hasManyPublishers",
30-
"hasIndirectDependencies",
31-
"hasMissingOrUnusedDependency",
32-
"isDead",
33-
"isOutdated",
34-
"hasDuplicate"
35-
]);
36-
const kFlags = Object.values(getManifest())
37-
.filter(({ title }) => kIgnorableFlags.has(title))
38-
.map(({ title, emoji }) => {
39-
return { value: title, emoji };
40-
});
4128
const kShortcuts = [
4229
{ id: "home", labelKey: "goto", viewKey: "home" },
4330
{ id: "network", labelKey: "goto", viewKey: "network" },
@@ -304,6 +291,21 @@ export class SettingsView extends LitElement {
304291
}
305292
}
306293

294+
#onSettingsSaved = (event) => {
295+
this.setNewConfig(event.detail);
296+
this._saveEnabled = false;
297+
};
298+
299+
connectedCallback() {
300+
super.connectedCallback();
301+
window.addEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
302+
}
303+
304+
disconnectedCallback() {
305+
window.removeEventListener(EVENTS.SETTINGS_SAVED, this.#onSettingsSaved);
306+
super.disconnectedCallback();
307+
}
308+
307309
firstUpdated() {
308310
this.updateNavigationHotKey(this._hotkeys);
309311
}
@@ -464,7 +466,7 @@ export class SettingsView extends LitElement {
464466
}
465467

466468
#renderFlagCheckboxes() {
467-
return kFlags.map(({ value, emoji }) => html`
469+
return FLAG_IGNORE_ITEMS.map(({ value, emoji }) => html`
468470
<div>
469471
<input
470472
type="checkbox"

test/e2e/command-palette.spec.js

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ test.describe("[command-palette] presets and actions", () => {
4545
test("shows contextual empty message for has-vulnerabilities preset", async({ page }) => {
4646
await page.locator(".range-preset").filter({ hasText: i18n.preset_has_vulnerabilities }).click();
4747

48-
await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
48+
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.preset_empty_has_vulnerabilities);
4949
});
5050

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

54-
await expect(page.locator(".empty-state")).toHaveText(i18n.preset_empty_deprecated);
54+
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.preset_empty_deprecated);
5555
});
5656

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

61-
await expect(page.locator(".empty-state")).toHaveText(i18n.empty_after_filter);
61+
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.empty_after_filter);
6262
});
6363

6464
test("clicking the theme action closes the palette and toggles the theme", async({ page }) => {
@@ -90,3 +90,110 @@ test.describe("[command-palette] presets and actions", () => {
9090
await expect(page.locator(".backdrop")).not.toBeVisible();
9191
});
9292
});
93+
94+
test.describe("[command-palette] ignore flags and warnings", () => {
95+
let i18n;
96+
97+
test.beforeEach(async({ page }) => {
98+
await page.goto("/");
99+
await page.waitForSelector(`[data-menu="network--view"].active`);
100+
101+
i18n = await page.evaluate(() => {
102+
const lang = document.getElementById("lang").dataset.lang;
103+
const activeLang = lang in window.i18n ? lang : "english";
104+
105+
return window.i18n[activeLang].search_command;
106+
});
107+
108+
await page.locator("body").click();
109+
await page.keyboard.press("Control+k");
110+
111+
await expect(page.locator(".backdrop")).toBeVisible();
112+
});
113+
114+
test("renders the ignore flags section", async({ page }) => {
115+
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_flags })).toBeVisible();
116+
});
117+
118+
test("renders the ignore warnings section", async({ page }) => {
119+
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_warnings })).toBeVisible();
120+
});
121+
122+
test("renders all six ignorable flag chips", async({ page }) => {
123+
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });
124+
125+
await expect(ignoreFlagsSection.locator(".flag-chip")).toHaveCount(6);
126+
});
127+
128+
test("renders at least one warning chip", async({ page }) => {
129+
const ignoreWarningsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_warnings });
130+
131+
expect(await ignoreWarningsSection.locator(".flag-chip").count()).toBeGreaterThan(0);
132+
});
133+
134+
test("clicking a flag chip marks it as ignored", async({ page }) => {
135+
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });
136+
const chip = ignoreFlagsSection.locator(".flag-chip[title='isOutdated']");
137+
138+
const isInitiallyIgnored = await page.evaluate(
139+
() => window.settings.config.ignore.flags.has("isOutdated")
140+
);
141+
if (isInitiallyIgnored) {
142+
await chip.click();
143+
await expect(chip).not.toContainClass("flag-active");
144+
}
145+
146+
await chip.click();
147+
148+
await expect(chip).toContainClass("flag-active");
149+
const ignoredFlags = await page.evaluate(() => [...window.settings.config.ignore.flags]);
150+
expect(ignoredFlags).toContain("isOutdated");
151+
});
152+
153+
test("clicking an active flag chip removes it from ignored", async({ page }) => {
154+
const ignoreFlagsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_flags });
155+
const chip = ignoreFlagsSection.locator(".flag-chip[title='isOutdated']");
156+
157+
const isInitiallyIgnored = await page.evaluate(
158+
() => window.settings.config.ignore.flags.has("isOutdated")
159+
);
160+
if (!isInitiallyIgnored) {
161+
await chip.click();
162+
await expect(chip).toContainClass("flag-active");
163+
}
164+
165+
await chip.click();
166+
167+
await expect(chip).not.toContainClass("flag-active");
168+
const ignoredFlags = await page.evaluate(() => [...window.settings.config.ignore.flags]);
169+
expect(ignoredFlags).not.toContain("isOutdated");
170+
});
171+
172+
test("clicking a warning chip marks it as ignored", async({ page }) => {
173+
const ignoreWarningsSection = page.locator(".section").filter({ hasText: i18n.section_ignore_warnings });
174+
const chip = ignoreWarningsSection.locator(".flag-chip").first();
175+
const warningValue = await chip.getAttribute("title");
176+
177+
const isInitiallyIgnored = await page.evaluate(
178+
(id) => window.settings.config.ignore.warnings.has(id),
179+
warningValue
180+
);
181+
if (isInitiallyIgnored) {
182+
await chip.click();
183+
await expect(chip).not.toContainClass("flag-active");
184+
}
185+
186+
await chip.click();
187+
188+
await expect(chip).toContainClass("flag-active");
189+
const ignoredWarnings = await page.evaluate(() => [...window.settings.config.ignore.warnings]);
190+
expect(ignoredWarnings).toContain(warningValue);
191+
});
192+
193+
test("ignore sections are hidden when a filter is active", async({ page }) => {
194+
await page.locator("#cmd-input").fill("flag:");
195+
196+
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_flags })).not.toBeVisible();
197+
await expect(page.locator(".section").filter({ hasText: i18n.section_ignore_warnings })).not.toBeVisible();
198+
});
199+
});

0 commit comments

Comments
 (0)