From cc0a011fae9fb1b03ef331401d21457766451978 Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 19 Sep 2024 13:51:19 -0700 Subject: [PATCH] frontend: change logic of when doc links are shown in search results The crate version page uses a fallback to handle crates that don't define `documentation` keys in their manifests: if docs.rs has documentation for that crate version, that those docs are used. The search results do not implement the same logic, so crates that don't have `documentation` keys don't get the handy documentation link in the crate row. This copies the logic from the version model into the crate model, using the default crate version as the version to look for on docs.rs. I strongly suspect there is a better way to do this than copying a bunch of code, and would welcome suggestions from those more familiar with Ember. This would eventually fix #1484. --- app/components/crate-row.hbs | 6 +++--- app/controllers/search.js | 21 ++++++++++++++++-- app/models/crate.js | 42 ++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) diff --git a/app/components/crate-row.hbs b/app/components/crate-row.hbs index 9b864218530..383cfdd82e6 100644 --- a/app/components/crate-row.hbs +++ b/app/components/crate-row.hbs @@ -61,12 +61,12 @@ {{#if @crate.homepage}}
  • Homepage
  • {{/if}} - {{#if @crate.documentation}} -
  • Documentation
  • + {{#if @crate.documentationLink}} +
  • Documentation
  • {{/if}} {{#if @crate.repository}}
  • Repository
  • {{/if}} - \ No newline at end of file + diff --git a/app/controllers/search.js b/app/controllers/search.js index 65c393b0fb8..a00ea45ae8e 100644 --- a/app/controllers/search.js +++ b/app/controllers/search.js @@ -3,9 +3,10 @@ import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import { restartableTask } from 'ember-concurrency'; +import { all, restartableTask } from 'ember-concurrency'; import { bool, reads } from 'macro-decorators'; +import { AjaxError } from '../utils/ajax'; import { pagination } from '../utils/pagination'; import { CATEGORY_PREFIX, processSearchQuery } from '../utils/search'; @@ -73,6 +74,22 @@ export default class SearchController extends Controller { ? { page, per_page, sort, q: query, all_keywords } : { page, per_page, sort, ...processSearchQuery(query) }; - return await this.store.query('crate', searchOptions); + const crates = await this.store.query('crate', searchOptions); + + // Prime the docs for the most recent versions of each crate. + const docTasks = []; + for (const crate of crates) { + docTasks.push(crate.loadDocsStatusTask.perform()); + } + try { + await all(docTasks); + } catch (e) { + // report unexpected errors to Sentry and ignore `ajax()` errors + if (!didCancel(error) && !(error instanceof AjaxError)) { + this.sentry.captureException(error); + } + } + + return crates; }); } diff --git a/app/models/crate.js b/app/models/crate.js index 92864803e83..b5b6870af73 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -1,9 +1,12 @@ import Model, { attr, hasMany } from '@ember-data/model'; import { waitForPromise } from '@ember/test-waiters'; +import { task } from 'ember-concurrency'; import { apiAction } from '@mainmatter/ember-api-actions'; import { cached } from 'tracked-toolbox'; +import ajax from '../utils/ajax'; + export default class Crate extends Model { @attr name; @attr downloads; @@ -42,6 +45,45 @@ export default class Crate extends Model { } } + get documentationLink() { + let crateDocsLink = this.documentation; + + // if this is *not* a docs.rs link we'll return it directly + if (crateDocsLink && !crateDocsLink.startsWith('https://docs.rs/')) { + return crateDocsLink; + } + + // if we know about a successful docs.rs build, we'll return a link to that + let { docsRsLink } = this; + if (docsRsLink) { + return docsRsLink; + } + + // finally, we'll return the specified documentation link, whatever it is + if (crateDocsLink) { + return crateDocsLink; + } + + return null; + } + + loadDocsStatusTask = task(async () => { + if (!this.documentation) { + return await ajax(`https://docs.rs/crate/${this.name}/=${this.defaultVersion}/status.json`); + } + }); + + get hasDocsRsLink() { + let docsStatus = this.loadDocsStatusTask.lastSuccessful?.value; + return docsStatus?.doc_status === true; + } + + get docsRsLink() { + if (this.hasDocsRsLink) { + return `https://docs.rs/${this.name}`; + } + } + @cached get versionIdsBySemver() { let versions = this.versions.toArray() ?? []; return versions.sort(compareVersionBySemver).map(v => v.id);