Public: anyone can read this; the crew can change it.

When /explore finds nothing, it names what was searched and offers each term as a link that drops it #5972

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/5959-when-explore-finds-nothing-it-names-what → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/5959-when-explore-finds-nothing-it-names-what was merged into main as 61aad53.

Review

Sign in to review this change.
3 files changed +61 −4
src/web/studio/explore.ts +33 −3
@@ -56,13 +56,43 @@ function rowHtml(row: ExploreRow): Html {
5656 });
5757 }
5858
59 /** A button label that cannot push the page wider; the whole term is its title. */
60 const shorten = (term: string): string =>
61 term.length > 24 ? `${term.slice(0, 23)}…` : term;
62
63 /** Each term of a missed line, with the query that is the line without it. */
64 function terms(query: ExploreQuery): { term: string; without: ExploreQuery }[] {
65 const out: { term: string; without: ExploreQuery }[] = [];
66 if (query.kind !== "all")
67 out.push({ term: `kind:${query.kind}`, without: { ...query, kind: "all" } });
68 if (query.sort !== "score")
69 out.push({ term: `sort:${query.sort}`, without: { ...query, sort: "score" } });
70 if (query.archived)
71 out.push({ term: "archived:yes", without: { ...query, archived: false } });
72 const words = query.text.split(/\s+/).filter((w) => w !== "");
73 words.forEach((word, i) =>
74 out.push({
75 term: word,
76 without: { ...query, text: words.filter((_, j) => j !== i).join(" ") },
77 }),
78 );
79 return out;
80 }
81
5982 /** Nothing to list: a bare line means nothing is public yet, not a miss. */
6083 function emptyState(signedIn: boolean, query: ExploreQuery): Html {
61 if (exploreLine(query) !== "")
84 const line = exploreLine(query);
85 if (line !== "")
outdated

class="row" is the list-row component, not a button group: it is flex without wrap, so many terms overflow at 360px. Use a wrapping group from the design system instead.

6286 return empty({
6387 icon: "search",
64 title: "Nothing matches",
65 note: "Clear the line to see everything you can.",
88 title: `Nothing matches ${line}`,
89 note: "Drop a term to step back:",
90 action: html`<div class="form-actions">
91 ${terms(query).map(
92 (t) => btn(shorten(t.term), { href: href(t.without), size: "sm", title: `Drop ${t.term}` }),
93 )}
94 ${btn("Clear all", { href: "/explore", size: "sm" })}
95 </div>`,
6696 });
6797 return empty({
6898 icon: "search",
src/web/style/components.ts +1
@@ -840,6 +840,7 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); }
840840 .empty .ico { color: var(--faint); width: 20px; height: 20px; }
841841 .empty h2, .empty .empty-title {
842842 font-size: 15px; font-weight: 600; color: var(--fg); text-wrap: balance; margin: 0;
843 overflow-wrap: anywhere;
843844 }
844845 .empty p { font-size: 13px; max-width: 320px; text-wrap: pretty; margin: 0; }
845846 .empty .btn { margin-top: 8px; }
test/explore.test.ts +27 −1
@@ -326,8 +326,34 @@ describe("explore on a forge with nothing public", () => {
326326 for (const signedIn of [false, true]) {
327327 const out = await get(`/explore?q=${encodeURIComponent("zzz-no-such")}`, signedIn);
328328 expect(out).toContain("Nothing matches");
329 expect(out).toContain("Clear the line");
329 expect(out).toContain("Nothing matches zzz-no-such");
330 expect(out).toContain("Clear all");
330331 expect(out).not.toContain("Nothing public yet");
331332 }
332333 });
334
335 test("a miss names the line and offers each term as a link that drops it", async () => {
336 const out = await get(`/explore?q=${encodeURIComponent("kind:app zzz-no-such")}`, false);
337 expect(out).toContain("Nothing matches kind:app zzz-no-such");
338 expect(out).toContain(`href="/explore?q=${encodeURIComponent("zzz-no-such")}"`);
339 expect(out).toContain(`href="/explore?q=${encodeURIComponent("kind:app")}"`);
340 expect(out).toContain('href="/explore"');
341 });
342
343 test("many words and a long one sit in a wrapping group, not a list row", async () => {
344 const words = Array.from({ length: 10 }, (_, i) => `w${i}`).join(" ");
345 const long = "x".repeat(60);
346 const out = await get(`/explore?q=${encodeURIComponent(`${words} ${long}`)}`, false);
347 const group = out.slice(out.indexOf("Drop a term"));
348 expect(group).toContain('class="form-actions"');
349 expect(group.slice(0, group.indexOf("Clear all"))).not.toContain('class="row"');
350 expect(group).toContain(`>${"x".repeat(23)}…</a`);
351 expect(group).toContain(`title="Drop ${long}"`);
352 });
353
354 test("a hostile word is escaped in the title and the links", async () => {
355 const out = await get(`/explore?q=${encodeURIComponent("<script>x</script>")}`, false);
356 expect(out).not.toContain("<script>x");
357 expect(out).toContain("Nothing matches &lt;script&gt;x&lt;/script&gt;");
358 });
333359 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs