Skip to content

Commit 118e144

Browse files
feat(interface): add dep filter to command palette (#747)
1 parent 1971ef4 commit 118e144

7 files changed

Lines changed: 363 additions & 17 deletions

File tree

i18n/english.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ const ui = {
299299
section_extensions: "File extensions",
300300
section_builtins: "Node.js core modules",
301301
section_authors: "Authors",
302+
section_dep: "Packages depending on",
302303
hint_size: "e.g. >50kb, 10kb..200kb",
303304
hint_version: "e.g. ^1.0.0, >=2.0.0",
304305
empty: "No results found",
@@ -323,7 +324,8 @@ const ui = {
323324
ext: "file extension",
324325
builtin: "node.js module",
325326
size: "e.g. >50kb",
326-
highlighted: "all"
327+
highlighted: "all",
328+
dep: "package name"
327329
}
328330
},
329331
legend: {

i18n/french.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ const ui = {
299299
section_extensions: "Extensions de fichiers",
300300
section_builtins: "Modules Node.js natifs",
301301
section_authors: "Auteurs",
302+
section_dep: "Packages dépendant de",
302303
hint_size: "ex. >50kb, 10kb..200kb",
303304
hint_version: "ex. ^1.0.0, >=2.0.0",
304305
empty: "Aucun résultat trouvé",
@@ -323,7 +324,8 @@ const ui = {
323324
ext: "extension de fichier",
324325
builtin: "module node.js",
325326
size: "ex. >50kb",
326-
highlighted: "all"
327+
highlighted: "all",
328+
dep: "nom du package"
327329
}
328330
},
329331
legend: {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const kListTitleKeys = {
1818
license: "section_licenses",
1919
ext: "section_extensions",
2020
builtin: "section_builtins",
21-
author: "section_authors"
21+
author: "section_authors",
22+
dep: "section_dep"
2223
};
2324

2425
/**

public/components/command-palette/filters.js

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,18 @@ export const VERSION_PRESETS = [
2020
{ label: "≥ 1.0", value: ">=1.0.0" },
2121
{ label: "< 1.0", value: "<1.0.0" }
2222
];
23-
export const FILTERS_NAME = new Set(["package", "version", "flag", "license", "author", "ext", "builtin", "size", "highlighted"]);
23+
export const FILTERS_NAME = new Set([
24+
"package",
25+
"version",
26+
"flag",
27+
"license",
28+
"author",
29+
"ext",
30+
"builtin",
31+
"size",
32+
"highlighted",
33+
"dep"
34+
]);
2435
export const PRESETS = [
2536
{ id: "has_vulnerabilities", filter: "flag", value: "hasVulnerabilities" },
2637
{ id: "has_scripts", filter: "flag", value: "hasScript" },
@@ -29,7 +40,7 @@ export const PRESETS = [
2940
{ id: "large", filter: "size", value: ">100kb" }
3041
];
3142
// Filters that use a searchable text-based list (not a rich visual panel)
32-
export const FILTER_HAS_HELPERS = new Set(["license", "ext", "builtin", "author"]);
43+
export const FILTER_HAS_HELPERS = new Set(["license", "ext", "builtin", "author", "dep"]);
3344
// Filters where the mode persists after selection (multi-select)
3445
export const FILTER_MULTI_SELECT = new Set(["flag"]);
3546
// Filters that auto-confirm immediately on selection (no text input needed)
@@ -60,6 +71,18 @@ export function getFlagCounts(linker) {
6071
* @returns {Map<string, number>}
6172
*/
6273
export function getFilterValueCounts(linker, filterName) {
74+
if (filterName === "dep") {
75+
const counts = new Map();
76+
for (const opt of linker.values()) {
77+
const dependentCount = Object.keys(opt.usedBy).length;
78+
if (dependentCount > 0) {
79+
counts.set(opt.name, (counts.get(opt.name) ?? 0) + dependentCount);
80+
}
81+
}
82+
83+
return counts;
84+
}
85+
6386
const counts = new Map();
6487
for (const opt of linker.values()) {
6588
for (const value of getValuesForCount(opt, filterName)) {
@@ -155,6 +178,16 @@ export function getHelperValues(linker, filterName) {
155178
return { display: name, value: name };
156179
});
157180
}
181+
case "dep": {
182+
const items = new Set();
183+
for (const { name } of linker.values()) {
184+
items.add(name);
185+
}
186+
187+
return [...items].sort().map((name) => {
188+
return { display: name, value: name };
189+
});
190+
}
158191
default:
159192
return [];
160193
}
@@ -169,6 +202,10 @@ export function getHelperValues(linker, filterName) {
169202
* @returns {Set<string>}
170203
*/
171204
export function computeMatches(linker, filterName, inputValue) {
205+
if (filterName === "dep") {
206+
return computeDepMatches(linker, inputValue);
207+
}
208+
172209
const matchingIds = new Set();
173210

174211
for (const [id, opt] of linker) {
@@ -180,6 +217,41 @@ export function computeMatches(linker, filterName, inputValue) {
180217
return matchingIds;
181218
}
182219

220+
/**
221+
* Collect packages that depend on package matching inputValue
222+
*
223+
* @param {Map<number, object>} linker
224+
* @param {string} inputValue
225+
* @returns {Set<string>}
226+
*/
227+
function computeDepMatches(linker, inputValue) {
228+
const matchingIds = new Set();
229+
230+
try {
231+
const regex = new RegExp(inputValue, "i");
232+
233+
const dependentNames = new Set();
234+
for (const opt of linker.values()) {
235+
if (regex.test(opt.name)) {
236+
for (const dependency of Object.keys(opt.usedBy)) {
237+
dependentNames.add(dependency);
238+
}
239+
}
240+
}
241+
242+
for (const [id, opt] of linker) {
243+
if (dependentNames.has(opt.name)) {
244+
matchingIds.add(String(id));
245+
}
246+
}
247+
}
248+
catch {
249+
// invalid regex
250+
}
251+
252+
return matchingIds;
253+
}
254+
183255
function matchesFilter(opt, filterName, inputValue) {
184256
switch (filterName) {
185257
case "package": {

test/e2e/command-palette.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,88 @@ test.describe("[command-palette] presets and actions", () => {
152152
});
153153
});
154154

155+
test.describe("[command-palette] dep filter", () => {
156+
let i18n;
157+
158+
test.beforeEach(async({ page }) => {
159+
await page.goto("/");
160+
await page.waitForSelector(`[data-menu="network--view"].active`);
161+
162+
i18n = await page.evaluate(() => {
163+
const lang = document.getElementById("lang").dataset.lang;
164+
const activeLang = lang in window.i18n ? lang : "english";
165+
166+
return window.i18n[activeLang].search_command;
167+
});
168+
169+
await page.locator("body").click();
170+
await page.keyboard.press("Control+k");
171+
172+
await expect(page.locator(".backdrop")).toBeVisible();
173+
});
174+
175+
test("dep appears in the filter hint list", async({ page }) => {
176+
const filterRow = page
177+
.locator(".helper-item")
178+
.filter({ hasText: "dep:" });
179+
180+
await expect(filterRow).toBeVisible();
181+
await expect(filterRow).toContainText(i18n.filter_hints.dep);
182+
});
183+
184+
test("typing dep: activates the filter and shows the dep panel", async({ page }) => {
185+
await page.locator("#cmd-input").fill("dep:");
186+
187+
await expect(
188+
page
189+
.locator(".section-title").filter({ hasText: i18n.section_dep })).toBeVisible();
190+
});
191+
192+
test("dep list panel shows all package names as helpers", async({ page }) => {
193+
await page.locator("#cmd-input").fill("dep:");
194+
195+
await expect(page.locator(".list-item").filter({ hasText: "debug" })).toBeVisible();
196+
await expect(page.locator(".list-item").filter({ hasText: "ms" })).toBeVisible();
197+
});
198+
199+
test("typing dep:ms and pressing Enter adds a chip and shows debug as result", async({ page }) => {
200+
await page.locator("#cmd-input").fill("dep:ms");
201+
await page.keyboard.press("Enter");
202+
203+
await expect(page.locator("search-chip")).toBeVisible();
204+
await expect(page.locator(".result-item")).toHaveCount(1);
205+
await expect(page.locator(".result-item")).toContainText("debug");
206+
});
207+
208+
test("typing dep:debug and pressing Enter shows empty results", async({ page }) => {
209+
await page.locator("#cmd-input").fill("dep:debug");
210+
await page.keyboard.press("Enter");
211+
212+
await expect(page.locator("search-chip")).toBeVisible();
213+
await expect(page.locator(".dialog .empty-state")).toHaveText(i18n.empty_after_filter);
214+
});
215+
216+
test("dep chip label shows filter and value", async({ page }) => {
217+
await page.locator("#cmd-input").fill("dep:ms");
218+
await page.keyboard.press("Enter");
219+
220+
const chip = page.locator("search-chip");
221+
await expect(chip).toHaveAttribute("filter", "dep");
222+
await expect(chip).toHaveAttribute("value", "ms");
223+
});
224+
225+
test("removing the dep chip clears the results", async({ page }) => {
226+
await page.locator("#cmd-input").fill("dep:ms");
227+
await page.keyboard.press("Enter");
228+
229+
await expect(page.locator("search-chip")).toBeVisible();
230+
231+
await page.keyboard.press("Backspace");
232+
233+
await expect(page.locator("search-chip")).not.toBeVisible();
234+
});
235+
});
236+
155237
test.describe("[command-palette] ignore flags and warnings", () => {
156238
let i18n;
157239

0 commit comments

Comments
 (0)