fix(dashboard): a find result opens its job, and Back returns to the Catalog page

- Back into the first entry of a page opened at /catalog (or
  /catalog/<slug>) showed "Your own tools": that entry has no history
  state and no hash, and popstate fell back to the tools view. It now
  resolves the view from the path, as the first load does. A bug on main
  that the find flow walks into.
- A result opened from the Catalog page landed on its platform's shelf
  with the shelf's box filled with the row's name, which the always-ask
  box then filtered and searched by. It now opens the job: its
  comparison once the shelf has loaded (replacing the shelf's history
  entry, so Back returns to the answer), else the tool in the drawer; the
  shelf's box is never prefilled. openPlatform returns its load.
This commit is contained in:
SToneX
2026-09-30 23:47:39 +08:00
parent 07ade3018d
commit 9cae6ce47e
6 changed files with 67 additions and 13 deletions
+5
View File
@@ -102,6 +102,11 @@ this page keeps what no single file shows. The look follows the root `design.md`
name keeps filtering and gets its answer too), and while it reads, or when it is `none` or empty,
it is one line. Enter (or the suggestion row) asks for the full answer (`findFull`): shelves
unfiltered and lit where it landed.
- A find result opens its job (`findOpen`): the job's comparison on its platform when that shelf
compares it, else the tool in the drawer; from the Catalog page the shelf loads first and the
comparison replaces its history entry. Back returns to the Catalog page with the box, the answer
and the filtered shelves as they were: the first entry of a page opened at `/catalog` (or
`/catalog/<slug>`) has no history state, so popstate resolves it from the path.
- A shelf's own box answers from that shelf only. An empty answer there is one line, "Nothing in
<platform> for …", and "Search all tools" (`findEverywhere`) moves to the Catalog page with the box
prefilled and the same words asked unscoped. "Request it" appears only on an unscoped gap (and on
+15
View File
@@ -99,3 +99,18 @@ test('an empty answer on a shelf offers the whole catalog, and asks it with the
await expect(page.locator('.fa .ui-tbody .ui-tr')).toHaveCount(1)
expect(asked.at(-1)).toEqual({ q: 'verify an email before sending', platform: null })
})
test('a result opened from the Catalog page leads to its job, and Back returns to the Catalog page', async ({ page }) => {
const asked = await answer(page, { verdict: 'strong', rows: [row('hunter.people.email.find', 0.9)] })
await box(page).fill('find a work email')
await expect.poll(() => asked).toEqual(['find a work email'])
await page.locator('.fa .ui-tbody .ui-tr').first().click()
// the job's own comparison on its platform, every provider of it
await expect(page).toHaveURL(/\/catalog\/people\/[^/]+$/)
await expect(page.locator('h1')).toContainText('work email')
await page.goBack()
await expect(page).toHaveURL(/\/catalog$/)
await expect(page.locator('.pl.cat h1')).toContainText('tools for agents') // the Catalog page, not "Your own tools"
await expect(box(page)).toHaveValue('find a work email')
await expect(page.locator('.fa .ui-tbody .ui-tr')).toHaveCount(1) // the answer is still there
})
+4
View File
@@ -30,6 +30,10 @@ export default async function boot(){
if(d){ this.openDetail(d.kind, d.name, true); return; }
const rid=(e.state&&e.state.run)||this.runFromPath(location.pathname);
if(rid){ this.openRun(rid, true); return; }
// The first entry of a page opened at a public catalog URL (/catalog, /catalog/<slug>) carries no
// state and no hash: resolve it from the path, as the first load did, not as the tools view.
const cr=!(e.state&&e.state.view) && !location.hash && this.catalogFromPath(location.pathname);
if(cr){ this.openCatalogRoute(cr); return; }
let v=(e.state&&e.state.view)||(location.hash||'').replace('#','')||'tools';
if(v==='billing'){ this.orgTab='billing'; v='orgs'; }
if(['tools','orgs','activity','usage','admin','help','secrets','start','resources','catalog','connections','referrals','hub'].includes(v)) this.go(v, true);
+1 -1
View File
@@ -86,7 +86,7 @@ openPlatform(slug, fromPop, cap){ this.resetConfirms();
// Connected/not-connected is a member fact and the endpoint needs a session; a public
// visitor has none, so skip it rather than fire a guaranteed 401 on every shelf view.
if(!this.publicCatalog && !this.providers.length) this.loadConnections();
this.loadPlatform(); },
return this.loadPlatform(); }, // the shelf's data, for a caller that acts on it once loaded
// Where a provider name leads: the public provider page signed out, the provider's page in the app
// signed in. Both list every tool the provider serves.
provUrl(service){ return this.publicCatalog ? '/tools/'+encodeURIComponent(service) : '/app/marketplace/'+encodeURIComponent(service); },
+15 -12
View File
@@ -214,20 +214,23 @@ export default {
signed_in:!!this.authed, ...extra});
},
// Open the platform shelf the row lives on, its search box filtered to this job.
// On a shelf's own search the answer is already on that shelf: a job several providers do opens its
// comparison, anything else opens in the tool drawer.
findOpen(group, rank){
// Open the job a row is: its comparison on its platform when that shelf compares it (several
// providers), else the tool itself in the drawer. From another page the shelf is opened first and
// the job chosen once its data is in, the comparison replacing the shelf's history entry so Back
// returns to where the answer was. The shelf's own box is never filled with the row's name.
async findOpen(group, rank){
this.findTrackClick('job', group.platform, {provider:group.rows[0]?.provider, rank});
if(this.view==='platform' && group.platform===this.platSlug){
const cap=group.rows[0]?.capability;
const job=cap && this.platComparisons.find(j=>j.key===cap);
if(job) this.openComparison(job.slug);
else this.openTool(group.rows[0].id);
return;
const slug=group.platform, cap=group.rows[0]?.capability, id=group.rows[0]?.id;
const here=this.view==='platform' && slug===this.platSlug;
if(!here){
await this.openPlatform(slug);
if(this.view!=='platform' || this.platSlug!==slug) return; // the person moved on meanwhile
}
this.openPlatform(group.platform);
this.platQ=group.rows[0]?.name || group.label;
const job=cap && this.platComparisons.find(j=>j.key===cap);
if(!job){ this.openTool(id); return; }
if(here){ this.openComparison(job.slug); return; }
this.openPlatform(slug, true, job.slug);
history.replaceState({platform:slug}, '', this.platUrl(slug, job.slug));
},
// From /search into the dashboard: the platform's page. Signed
+27
View File
@@ -74,3 +74,30 @@ test('a shelf\'s answer asks the whole catalog with the same words', () => {
find.findEverywhere.call(vm)
expect(calls).toEqual([['exit'], ['go', 'catalog', 'verify emails'], ['run', 'verify emails', { scope: '' }]])
})
test('a result opens its job: the comparison when its shelf has one, else the tool; never the shelf\'s box', async () => {
const group = { platform: 'people', rows: [{ id: 'hunter.people.email.find', capability: 'people.email.find', provider: 'hunter' }] }
const calls: any[] = []
const replaced: any[] = []
const history0 = globalThis.history
;(globalThis as any).history = { replaceState: (...a: any[]) => replaced.push(a) }
const vm: any = { view: 'catalog', platSlug: '', platQ: '', platComparisons: [] as any[],
findTrackClick() {}, platUrl: (slug: string, key: string) => '/catalog/' + slug + '/' + key,
async openPlatform(slug: string, fromPop?: boolean, key?: string) {
calls.push(['platform', slug, fromPop ?? false, key ?? null]); this.view = 'platform'; this.platSlug = slug },
openComparison(key: string) { calls.push(['comparison', key]) }, openTool(id: string) { calls.push(['tool', id]) } }
try {
vm.platComparisons = [{ key: 'people.email.find', slug: 'email.find' }]
await find.findOpen.call(vm, group, 1) // from the Catalog page: shelf, then its job
expect(calls).toEqual([['platform', 'people', false, null], ['platform', 'people', true, 'email.find']])
expect(replaced).toEqual([[{ platform: 'people' }, '', '/catalog/people/email.find']])
calls.length = 0
await find.findOpen.call(vm, group, 1) // already on that shelf: the comparison directly
expect(calls).toEqual([['comparison', 'email.find']])
calls.length = 0
vm.platComparisons = []
await find.findOpen.call(vm, group, 1) // a job with no comparison: the tool
expect(calls).toEqual([['tool', 'hunter.people.email.find']])
expect(vm.platQ).toBe('')
} finally { (globalThis as any).history = history0 }
})