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);