diff --git a/.ember-cli b/.ember-cli index ee64cfed2a8..46c316d1747 100644 --- a/.ember-cli +++ b/.ember-cli @@ -5,5 +5,17 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false + "disableAnalytics": false, + + /** + Setting `componentAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS files for the component and the component rendering test. "loose" is the default. + */ + "componentAuthoringFormat": "strict", + + /** + Setting `routeAuthoringFormat` to "strict" will force the blueprint generators to generate GJS + or GTS templates for routes. "loose" is the default + */ + "routeAuthoringFormat": "strict" } diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000000..193a59d88ca --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +a0efcd29f514b404d568478423ca2c7074287a56 +da9ad2f8964594b6cbd75504fc43ae43e141081c diff --git a/app/components/color-scheme-menu.gjs b/app/components/color-scheme-menu.gjs new file mode 100644 index 00000000000..9d561e2152f --- /dev/null +++ b/app/components/color-scheme-menu.gjs @@ -0,0 +1,49 @@ +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import eq from 'ember-truth-helpers/helpers/eq'; + +import Dropdown from 'crates-io/components/dropdown'; + +export default class Header extends Component { + /** @type {import("../services/dark-mode").default} */ + @service colorScheme; + + colorSchemes = [ + { mode: 'light', svg: 'sun' }, + { mode: 'dark', svg: 'moon' }, + { mode: 'system', svg: 'color-mode' }, + ]; + + get icon() { + return this.colorSchemes.find(({ mode }) => mode === this.colorScheme.scheme)?.svg; + } + + +} diff --git a/app/components/color-scheme-menu.hbs b/app/components/color-scheme-menu.hbs deleted file mode 100644 index 4f6843ec22f..00000000000 --- a/app/components/color-scheme-menu.hbs +++ /dev/null @@ -1,20 +0,0 @@ - - - {{svg-jar this.icon class=(scoped-class "icon")}} - Change color scheme - - - - {{#each this.colorSchemes as |colorScheme|}} - - - - {{/each}} - - \ No newline at end of file diff --git a/app/components/color-scheme-menu.js b/app/components/color-scheme-menu.js deleted file mode 100644 index 3d86a373f97..00000000000 --- a/app/components/color-scheme-menu.js +++ /dev/null @@ -1,17 +0,0 @@ -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -export default class Header extends Component { - /** @type {import("../services/dark-mode").default} */ - @service colorScheme; - - colorSchemes = [ - { mode: 'light', svg: 'sun' }, - { mode: 'dark', svg: 'moon' }, - { mode: 'system', svg: 'color-mode' }, - ]; - - get icon() { - return this.colorSchemes.find(({ mode }) => mode === this.colorScheme.scheme)?.svg; - } -} diff --git a/app/components/copy-button.js b/app/components/copy-button.gjs similarity index 68% rename from app/components/copy-button.js rename to app/components/copy-button.gjs index 184d8521730..d002c923e2f 100644 --- a/app/components/copy-button.js +++ b/app/components/copy-button.gjs @@ -1,7 +1,9 @@ +import { on } from '@ember/modifier'; import { service } from '@ember/service'; import Component from '@glimmer/component'; import { restartableTask } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; export default class CrateTomlCopy extends Component { @service notifications; @@ -15,4 +17,10 @@ export default class CrateTomlCopy extends Component { this.notifications.error('Copy to clipboard failed!'); } }); + + } diff --git a/app/components/copy-button.hbs b/app/components/copy-button.hbs deleted file mode 100644 index cae9419a9ae..00000000000 --- a/app/components/copy-button.hbs +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/app/components/crate-downloads-list.gjs b/app/components/crate-downloads-list.gjs new file mode 100644 index 00000000000..b4b38fc5ded --- /dev/null +++ b/app/components/crate-downloads-list.gjs @@ -0,0 +1,21 @@ +import { LinkTo } from '@ember/routing'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +import formatNum from 'crates-io/helpers/format-num'; + + diff --git a/app/components/crate-downloads-list.hbs b/app/components/crate-downloads-list.hbs deleted file mode 100644 index a526b42c274..00000000000 --- a/app/components/crate-downloads-list.hbs +++ /dev/null @@ -1,11 +0,0 @@ - diff --git a/app/components/crate-header.gjs b/app/components/crate-header.gjs new file mode 100644 index 00000000000..878940d4199 --- /dev/null +++ b/app/components/crate-header.gjs @@ -0,0 +1,116 @@ +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import { task } from 'ember-concurrency'; +import pluralize from 'ember-inflector/helpers/pluralize'; +import link_ from 'ember-link/helpers/link'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import { alias } from 'macro-decorators'; + +import FollowButton from 'crates-io/components/follow-button'; +import NavTabs from 'crates-io/components/nav-tabs'; +import PageHeader from 'crates-io/components/page-header'; +import Tooltip from 'crates-io/components/tooltip'; + +export default class CrateHeader extends Component { + @service session; + + @alias('loadKeywordsTask.last.value') keywords; + + constructor() { + super(...arguments); + + this.loadKeywordsTask.perform().catch(() => { + // ignore all errors and just don't display keywords if the request fails + }); + } + + get isOwner() { + let userId = this.session.currentUser?.id; + return this.args.crate?.hasOwnerUser(userId) ?? false; + } + + loadKeywordsTask = task(async () => { + return (await this.args.crate?.keywords) ?? []; + }); + + +} diff --git a/app/components/crate-header.hbs b/app/components/crate-header.hbs deleted file mode 100644 index 4b010ad3661..00000000000 --- a/app/components/crate-header.hbs +++ /dev/null @@ -1,80 +0,0 @@ - -

- {{@crate.name}} - {{#if @version}} - v{{@version.num}} - - {{#if @version.yanked}} - - {{svg-jar "trash"}} - Yanked - - - This crate has been yanked, but it is still available for download for other crates that - may be depending on it. - - - {{/if}} - {{/if}} -

- - {{#if @crate.description}} -
- {{@crate.description}} -
- {{/if}} - - {{#if this.keywords}} - - {{/if}} - - {{#if this.session.currentUser}} - - {{/if}} -
- - - - Readme - - - - {{pluralize @crate.num_versions "Version"}} - - - - Dependencies - - - - Dependents - - - {{#if this.isOwner}} - - Settings - - {{/if}} - \ No newline at end of file diff --git a/app/components/crate-header.js b/app/components/crate-header.js deleted file mode 100644 index b5d76c9d1dc..00000000000 --- a/app/components/crate-header.js +++ /dev/null @@ -1,28 +0,0 @@ -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -import { task } from 'ember-concurrency'; -import { alias } from 'macro-decorators'; - -export default class CrateHeader extends Component { - @service session; - - @alias('loadKeywordsTask.last.value') keywords; - - constructor() { - super(...arguments); - - this.loadKeywordsTask.perform().catch(() => { - // ignore all errors and just don't display keywords if the request fails - }); - } - - get isOwner() { - let userId = this.session.currentUser?.id; - return this.args.crate?.hasOwnerUser(userId) ?? false; - } - - loadKeywordsTask = task(async () => { - return (await this.args.crate?.keywords) ?? []; - }); -} diff --git a/app/components/crate-list.gjs b/app/components/crate-list.gjs new file mode 100644 index 00000000000..22b9f0d2524 --- /dev/null +++ b/app/components/crate-list.gjs @@ -0,0 +1,14 @@ +import CrateRow from 'crates-io/components/crate-row'; + + diff --git a/app/components/crate-list.hbs b/app/components/crate-list.hbs deleted file mode 100644 index a060264f8ce..00000000000 --- a/app/components/crate-list.hbs +++ /dev/null @@ -1,10 +0,0 @@ -
- {{!-- The extra div wrapper is needed for specificity issues with `margin` --}} -
    - {{#each @crates as |crate index|}} -
  1. - -
  2. - {{/each}} -
-
\ No newline at end of file diff --git a/app/components/crate-row.gjs b/app/components/crate-row.gjs new file mode 100644 index 00000000000..5d9ed428417 --- /dev/null +++ b/app/components/crate-row.gjs @@ -0,0 +1,89 @@ +import { on } from '@ember/modifier'; + +import link_ from 'ember-link/helpers/link'; +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import and from 'ember-truth-helpers/helpers/and'; +import not from 'ember-truth-helpers/helpers/not'; + +import CopyButton from 'crates-io/components/copy-button'; +import Tooltip from 'crates-io/components/tooltip'; +import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; +import dateFormatIso from 'crates-io/helpers/date-format-iso'; +import formatNum from 'crates-io/helpers/format-num'; +import truncateText from 'crates-io/helpers/truncate-text'; + + diff --git a/app/components/crate-row.hbs b/app/components/crate-row.hbs deleted file mode 100644 index 15693fef103..00000000000 --- a/app/components/crate-row.hbs +++ /dev/null @@ -1,72 +0,0 @@ -
-
-
- {{#let (link "crate" @crate.id) as |l|}} - - {{@crate.name}} - - {{/let}} - {{#if (and @crate.default_version (not @crate.yanked))}} - v{{@crate.default_version}} - - {{svg-jar "copy" alt="Copy Cargo.toml snippet to clipboard"}} - - {{/if}} -
-
- {{ truncate-text @crate.description }} -
-
-
-
- {{svg-jar "download" class=(scoped-class "download-icon")}} - - - All-Time: - - - {{ format-num @crate.downloads }} - -
-
- {{svg-jar "download" class=(scoped-class "download-icon")}} - - - Recent: - - - {{ format-num @crate.recent_downloads }} - -
-
- {{svg-jar "latest-updates" height="32" width="32"}} - - - Updated: - - - - -
-
- - -
\ No newline at end of file diff --git a/app/components/crate-sidebar.gjs b/app/components/crate-sidebar.gjs new file mode 100644 index 00000000000..8ab8277a038 --- /dev/null +++ b/app/components/crate-sidebar.gjs @@ -0,0 +1,221 @@ +import { hash } from '@ember/helper'; +import { action } from '@ember/object'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import { didCancel } from 'ember-concurrency'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import eq from 'ember-truth-helpers/helpers/eq'; +import not from 'ember-truth-helpers/helpers/not'; +import or from 'ember-truth-helpers/helpers/or'; + +import CopyButton from 'crates-io/components/copy-button'; +import InstallInstructions from 'crates-io/components/crate-sidebar/install-instructions'; +import Link from 'crates-io/components/crate-sidebar/link'; +import Edition from 'crates-io/components/edition'; +import LicenseExpression from 'crates-io/components/license-expression'; +import Msrv from 'crates-io/components/msrv'; +import OwnersList from 'crates-io/components/owners-list'; +import Tooltip from 'crates-io/components/tooltip'; +import dateFormat from 'crates-io/helpers/date-format'; +import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; +import dateFormatIso from 'crates-io/helpers/date-format-iso'; +import prettyBytes from 'crates-io/helpers/pretty-bytes'; + +import { simplifyUrl } from './crate-sidebar/link'; + +export default class CrateSidebar extends Component { + @service notifications; + @service playground; + @service sentry; + + get showHomepage() { + let { repository, homepage } = this.args.crate; + return homepage && (!repository || simplifyUrl(repository) !== simplifyUrl(homepage)); + } + + get playgroundLink() { + let playgroundCrates = this.playground.crates; + if (!playgroundCrates) return; + + let playgroundCrate = playgroundCrates.find(it => it.name === this.args.crate.name); + if (!playgroundCrate) return; + + return `https://play.rust-lang.org/?edition=2021&code=use%20${playgroundCrate.id}%3B%0A%0Afn%20main()%20%7B%0A%20%20%20%20%2F%2F%20try%20using%20the%20%60${playgroundCrate.id}%60%20crate%20here%0A%7D`; + } + + get canHover() { + return window?.matchMedia('(hover: hover)').matches; + } + + constructor() { + super(...arguments); + + // load Rust Playground crates list, if necessary + this.playground.loadCrates().catch(error => { + if (!(didCancel(error) || error.isServerError || error.isNetworkError)) { + // report unexpected errors to Sentry + this.sentry.captureException(error); + } + }); + } + + @action + async copyToClipboard(text) { + try { + await navigator.clipboard.writeText(text); + this.notifications.success('Copied to clipboard!'); + } catch { + this.notifications.error('Copy to clipboard failed!'); + } + } + + +} diff --git a/app/components/crate-sidebar.hbs b/app/components/crate-sidebar.hbs deleted file mode 100644 index 16f67154c08..00000000000 --- a/app/components/crate-sidebar.hbs +++ /dev/null @@ -1,165 +0,0 @@ - \ No newline at end of file diff --git a/app/components/crate-sidebar.js b/app/components/crate-sidebar.js deleted file mode 100644 index f0cd38545ae..00000000000 --- a/app/components/crate-sidebar.js +++ /dev/null @@ -1,54 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -import { didCancel } from 'ember-concurrency'; - -import { simplifyUrl } from './crate-sidebar/link'; - -export default class CrateSidebar extends Component { - @service notifications; - @service playground; - @service sentry; - - get showHomepage() { - let { repository, homepage } = this.args.crate; - return homepage && (!repository || simplifyUrl(repository) !== simplifyUrl(homepage)); - } - - get playgroundLink() { - let playgroundCrates = this.playground.crates; - if (!playgroundCrates) return; - - let playgroundCrate = playgroundCrates.find(it => it.name === this.args.crate.name); - if (!playgroundCrate) return; - - return `https://play.rust-lang.org/?edition=2021&code=use%20${playgroundCrate.id}%3B%0A%0Afn%20main()%20%7B%0A%20%20%20%20%2F%2F%20try%20using%20the%20%60${playgroundCrate.id}%60%20crate%20here%0A%7D`; - } - - get canHover() { - return window?.matchMedia('(hover: hover)').matches; - } - - constructor() { - super(...arguments); - - // load Rust Playground crates list, if necessary - this.playground.loadCrates().catch(error => { - if (!(didCancel(error) || error.isServerError || error.isNetworkError)) { - // report unexpected errors to Sentry - this.sentry.captureException(error); - } - }); - } - - @action - async copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - this.notifications.success('Copied to clipboard!'); - } catch { - this.notifications.error('Copy to clipboard failed!'); - } - } -} diff --git a/app/components/crate-sidebar/install-instructions.gjs b/app/components/crate-sidebar/install-instructions.gjs new file mode 100644 index 00000000000..dfe41232cbb --- /dev/null +++ b/app/components/crate-sidebar/install-instructions.gjs @@ -0,0 +1,106 @@ +import { get } from '@ember/helper'; +import Component from '@glimmer/component'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import and from 'ember-truth-helpers/helpers/and'; +import eq from 'ember-truth-helpers/helpers/eq'; + +import CopyButton from 'crates-io/components/copy-button'; +import isClipboardSupported from 'crates-io/helpers/is-clipboard-supported'; +import sum from 'crates-io/helpers/sum'; + +export default class InstallInstructions extends Component { + get cargoInstallCommand() { + return this.args.exactVersion + ? `cargo install ${this.args.crate}@${this.args.version}` + : `cargo install ${this.args.crate}`; + } + + get cargoAddCommand() { + return this.args.exactVersion + ? `cargo add ${this.args.crate}@=${this.args.version}` + : `cargo add ${this.args.crate}`; + } + + get tomlSnippet() { + let version = this.args.version.split('+')[0]; + let exact = this.args.exactVersion ? '=' : ''; + return `${this.args.crate} = "${exact}${version}"`; + } + + +} diff --git a/app/components/crate-sidebar/install-instructions.hbs b/app/components/crate-sidebar/install-instructions.hbs deleted file mode 100644 index 4bb63a8c06e..00000000000 --- a/app/components/crate-sidebar/install-instructions.hbs +++ /dev/null @@ -1,82 +0,0 @@ -{{#if @binNames}} - {{#if (is-clipboard-supported)}} - - {{this.cargoInstallCommand}} - {{svg-jar "copy" aria-hidden="true" class=(scoped-class "copy-icon")}} - - {{else}} - - {{this.cargoInstallCommand}} - - {{/if}} - -

- {{#if (eq @binNames.length 1)}} - Running the above command will globally install the - {{get @binNames 0}} - binary. - {{else if (eq @binNames.length 2)}} - Running the above command will globally install the - {{get @binNames 0}} - and - {{get @binNames 1}} - binaries. - {{else}} - Running the above command will globally install these binaries: - {{#each @binNames as |binName index|~}} - {{~#if (eq index 0)~}} - {{binName}} - {{~else if (eq index (sum @binNames.length -1))}} - and {{binName}} - {{~else~}} - , {{binName}} - {{~/if}} - {{~/each}} - {{/if}} -

- -{{/if}} - -{{#if (and @hasLib @binNames)}} -

Install as library

-{{/if}} - -{{#if @hasLib}} -

Run the following Cargo command in your project directory:

- - {{#if (is-clipboard-supported)}} - - {{this.cargoAddCommand}} - {{svg-jar "copy" aria-hidden="true" class=(scoped-class "copy-icon")}} - - {{else}} - - {{this.cargoAddCommand}} - - {{/if}} - -

Or add the following line to your Cargo.toml:

- - {{#if (is-clipboard-supported)}} - - {{this.tomlSnippet}} - {{svg-jar "copy" aria-hidden="true" class=(scoped-class "copy-icon")}} - - {{else}} - - {{this.tomlSnippet}} - - {{/if}} -{{/if}} \ No newline at end of file diff --git a/app/components/crate-sidebar/install-instructions.js b/app/components/crate-sidebar/install-instructions.js deleted file mode 100644 index e6a8ec185f4..00000000000 --- a/app/components/crate-sidebar/install-instructions.js +++ /dev/null @@ -1,21 +0,0 @@ -import Component from '@glimmer/component'; - -export default class InstallInstructions extends Component { - get cargoInstallCommand() { - return this.args.exactVersion - ? `cargo install ${this.args.crate}@${this.args.version}` - : `cargo install ${this.args.crate}`; - } - - get cargoAddCommand() { - return this.args.exactVersion - ? `cargo add ${this.args.crate}@=${this.args.version}` - : `cargo add ${this.args.crate}`; - } - - get tomlSnippet() { - let version = this.args.version.split('+')[0]; - let exact = this.args.exactVersion ? '=' : ''; - return `${this.args.crate} = "${exact}${version}"`; - } -} diff --git a/app/components/crate-sidebar/link.gjs b/app/components/crate-sidebar/link.gjs new file mode 100644 index 00000000000..35a8ed375a4 --- /dev/null +++ b/app/components/crate-sidebar/link.gjs @@ -0,0 +1,55 @@ +import Component from '@glimmer/component'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +export default class CrateSidebarLink extends Component { + get text() { + let { url } = this.args; + return simplifyUrl(url); + } + + get isDocsRs() { + return this.text.startsWith('docs.rs/'); + } + + get isGitHub() { + return this.text.startsWith('github.com/'); + } + + +} + +export function simplifyUrl(url) { + if (url.startsWith('https://')) { + url = url.slice('https://'.length); + } + if (url.startsWith('www.')) { + url = url.slice('www.'.length); + } + if (url.endsWith('/')) { + url = url.slice(0, -1); + } + if (url.startsWith('github.com/') && url.endsWith('.git')) { + url = url.slice(0, -4); + } + + return url; +} diff --git a/app/components/crate-sidebar/link.hbs b/app/components/crate-sidebar/link.hbs deleted file mode 100644 index 69c15822c5e..00000000000 --- a/app/components/crate-sidebar/link.hbs +++ /dev/null @@ -1,16 +0,0 @@ -
-

{{@title}}

-
- {{#if this.isDocsRs}} - {{svg-jar "docs-rs" class=(scoped-class "icon") data-test-icon="docs-rs"}} - {{else if this.isGitHub}} - {{svg-jar "github" class=(scoped-class "icon") data-test-icon="github"}} - {{else}} - {{svg-jar "link" class=(scoped-class "icon") data-test-icon="link"}} - {{/if}} - - - {{this.text}} - -
-
\ No newline at end of file diff --git a/app/components/crate-sidebar/link.js b/app/components/crate-sidebar/link.js deleted file mode 100644 index 687d1c6148b..00000000000 --- a/app/components/crate-sidebar/link.js +++ /dev/null @@ -1,33 +0,0 @@ -import Component from '@glimmer/component'; - -export default class CrateSidebarLink extends Component { - get text() { - let { url } = this.args; - return simplifyUrl(url); - } - - get isDocsRs() { - return this.text.startsWith('docs.rs/'); - } - - get isGitHub() { - return this.text.startsWith('github.com/'); - } -} - -export function simplifyUrl(url) { - if (url.startsWith('https://')) { - url = url.slice('https://'.length); - } - if (url.startsWith('www.')) { - url = url.slice('www.'.length); - } - if (url.endsWith('/')) { - url = url.slice(0, -1); - } - if (url.startsWith('github.com/') && url.endsWith('.git')) { - url = url.slice(0, -4); - } - - return url; -} diff --git a/app/components/dependency-list/row.gjs b/app/components/dependency-list/row.gjs new file mode 100644 index 00000000000..ad4523c11b1 --- /dev/null +++ b/app/components/dependency-list/row.gjs @@ -0,0 +1,123 @@ +import { array, fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { task } from 'ember-concurrency'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import or from 'ember-truth-helpers/helpers/or'; + +import Placeholder from 'crates-io/components/placeholder'; +import Tooltip from 'crates-io/components/tooltip'; +import formatReq from 'crates-io/helpers/format-req'; + +export default class VersionRow extends Component { + @service store; + + @tracked focused = false; + + @action setFocused(value) { + this.focused = value; + } + + constructor() { + super(...arguments); + + this.loadCrateTask.perform().catch(() => { + // ignore all errors and just don't display a description if the request fails + }); + } + + get description() { + return this.loadCrateTask.lastSuccessful?.value?.description; + } + + get featuresDescription() { + let { default_features: defaultFeatures, features } = this.args.dependency; + let numFeatures = features.length; + + if (numFeatures !== 0) { + return defaultFeatures + ? `${numFeatures} extra feature${numFeatures > 1 ? 's' : ''}` + : `only ${numFeatures} feature${numFeatures > 1 ? 's' : ''}`; + } else if (!defaultFeatures) { + return 'no default features'; + } + } + + loadCrateTask = task(async () => { + let { dependency } = this.args; + return await this.store.findRecord('crate', dependency.crate_id); + }); + + +} diff --git a/app/components/dependency-list/row.hbs b/app/components/dependency-list/row.hbs deleted file mode 100644 index 5189dc3a131..00000000000 --- a/app/components/dependency-list/row.hbs +++ /dev/null @@ -1,67 +0,0 @@ -
- - {{format-req @dependency.req}} - - -
- - - {{#if (or this.description this.loadCrateTask.isRunning)}} -
- {{#if this.loadCrateTask.isRunning}} - - {{else}} - {{this.description}} - {{/if}} -
- {{/if}} -
-
\ No newline at end of file diff --git a/app/components/dependency-list/row.js b/app/components/dependency-list/row.js deleted file mode 100644 index f9c2243c924..00000000000 --- a/app/components/dependency-list/row.js +++ /dev/null @@ -1,46 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -import { task } from 'ember-concurrency'; - -export default class VersionRow extends Component { - @service store; - - @tracked focused = false; - - @action setFocused(value) { - this.focused = value; - } - - constructor() { - super(...arguments); - - this.loadCrateTask.perform().catch(() => { - // ignore all errors and just don't display a description if the request fails - }); - } - - get description() { - return this.loadCrateTask.lastSuccessful?.value?.description; - } - - get featuresDescription() { - let { default_features: defaultFeatures, features } = this.args.dependency; - let numFeatures = features.length; - - if (numFeatures !== 0) { - return defaultFeatures - ? `${numFeatures} extra feature${numFeatures > 1 ? 's' : ''}` - : `only ${numFeatures} feature${numFeatures > 1 ? 's' : ''}`; - } else if (!defaultFeatures) { - return 'no default features'; - } - } - - loadCrateTask = task(async () => { - let { dependency } = this.args; - return await this.store.findRecord('crate', dependency.crate_id); - }); -} diff --git a/app/components/download-graph.js b/app/components/download-graph.gjs similarity index 83% rename from app/components/download-graph.js rename to app/components/download-graph.gjs index 2e30d02706d..d64d10db319 100644 --- a/app/components/download-graph.js +++ b/app/components/download-graph.gjs @@ -1,4 +1,9 @@ +/* eslint-disable ember/no-at-ember-render-modifiers */ +import { on } from '@ember/modifier'; import { action } from '@ember/object'; +import didInsert from '@ember/render-modifiers/modifiers/did-insert'; +import didUpdate from '@ember/render-modifiers/modifiers/did-update'; +import willDestroy from '@ember/render-modifiers/modifiers/will-destroy'; import { service } from '@ember/service'; import { waitForPromise } from '@ember/test-waiters'; import Component from '@glimmer/component'; @@ -8,6 +13,8 @@ import window from 'ember-window-mock'; import semverSort from 'semver/functions/sort'; // Colors by http://colorbrewer2.org/#type=diverging&scheme=RdBu&n=10 +import LoadingSpinner from 'crates-io/components/loading-spinner'; + const COLORS = ['#67001f', '#b2182b', '#d6604d', '#f4a582', '#92c5de', '#4393c3', '#2166ac', '#053061']; const BG_COLORS = ['#d3b5bc', '#eabdc0', '#f3d0ca', '#fce4d9', '#deedf5', '#c9deed', '#2166ac', '#053061']; @@ -116,6 +123,30 @@ export default class DownloadGraph extends Component { get data() { return toChartData(this.args.data, this.args.versions); } + + } export function toChartData(data) { diff --git a/app/components/download-graph.hbs b/app/components/download-graph.hbs deleted file mode 100644 index 31dc60fadb7..00000000000 --- a/app/components/download-graph.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{!-- template-lint-disable no-at-ember-render-modifiers --}} -
- {{#if this.chartjs.loadTask.isRunning}} - - {{else if this.chartjs.loadTask.lastSuccessful.value}} - - {{else}} -
-

Sorry, there was a problem loading the graphing code.

- -
- {{/if}} -
\ No newline at end of file diff --git a/app/components/dropdown.gjs b/app/components/dropdown.gjs new file mode 100644 index 00000000000..1e074c1bce3 --- /dev/null +++ b/app/components/dropdown.gjs @@ -0,0 +1,37 @@ +import { fn, hash } from '@ember/helper'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import onClickOutside from 'ember-click-outside/modifiers/on-click-outside'; +import onKey from 'ember-keyboard/modifiers/on-key'; + +import DropdownContent from 'crates-io/components/dropdown/content'; +import DropdownMenu from 'crates-io/components/dropdown/menu'; +import DropdownTrigger from 'crates-io/components/dropdown/trigger'; + +export default class Dropdown extends Component { + @tracked dropdownExpanded = false; + + @action + toggleDropdown() { + this.dropdownExpanded = !this.dropdownExpanded; + } + + +} diff --git a/app/components/dropdown.hbs b/app/components/dropdown.hbs deleted file mode 100644 index be7676065dc..00000000000 --- a/app/components/dropdown.hbs +++ /dev/null @@ -1,12 +0,0 @@ -
- {{yield (hash - Trigger=(component "dropdown/trigger" toggle=this.toggleDropdown) - Content=(component "dropdown/content" isExpanded=this.dropdownExpanded) - Menu=(component "dropdown/menu" Content=(component "dropdown/content" isExpanded=this.dropdownExpanded)) - )}} -
\ No newline at end of file diff --git a/app/components/dropdown.js b/app/components/dropdown.js deleted file mode 100644 index 43a1a08ac89..00000000000 --- a/app/components/dropdown.js +++ /dev/null @@ -1,12 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -export default class Dropdown extends Component { - @tracked dropdownExpanded = false; - - @action - toggleDropdown() { - this.dropdownExpanded = !this.dropdownExpanded; - } -} diff --git a/app/components/dropdown/content.gjs b/app/components/dropdown/content.gjs new file mode 100644 index 00000000000..d13cf865b01 --- /dev/null +++ b/app/components/dropdown/content.gjs @@ -0,0 +1,3 @@ + diff --git a/app/components/dropdown/content.hbs b/app/components/dropdown/content.hbs deleted file mode 100644 index c7c87da0137..00000000000 --- a/app/components/dropdown/content.hbs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/components/dropdown/menu-item.gjs b/app/components/dropdown/menu-item.gjs new file mode 100644 index 00000000000..04dbbd48110 --- /dev/null +++ b/app/components/dropdown/menu-item.gjs @@ -0,0 +1,3 @@ + diff --git a/app/components/dropdown/menu-item.hbs b/app/components/dropdown/menu-item.hbs deleted file mode 100644 index bc005561414..00000000000 --- a/app/components/dropdown/menu-item.hbs +++ /dev/null @@ -1 +0,0 @@ -
  • {{yield}}
  • \ No newline at end of file diff --git a/app/components/dropdown/menu.gjs b/app/components/dropdown/menu.gjs new file mode 100644 index 00000000000..736b9bb7e38 --- /dev/null +++ b/app/components/dropdown/menu.gjs @@ -0,0 +1,11 @@ +import { hash } from '@ember/helper'; + +import DropdownMenuItem from 'crates-io/components/dropdown/menu-item'; + + diff --git a/app/components/dropdown/menu.hbs b/app/components/dropdown/menu.hbs deleted file mode 100644 index a3a5cb95b35..00000000000 --- a/app/components/dropdown/menu.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<@Content ...attributes> - - \ No newline at end of file diff --git a/app/components/dropdown/trigger.gjs b/app/components/dropdown/trigger.gjs new file mode 100644 index 00000000000..c0bc5b4df6c --- /dev/null +++ b/app/components/dropdown/trigger.gjs @@ -0,0 +1,10 @@ +import { on } from '@ember/modifier'; + + diff --git a/app/components/dropdown/trigger.hbs b/app/components/dropdown/trigger.hbs deleted file mode 100644 index 9446793054e..00000000000 --- a/app/components/dropdown/trigger.hbs +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/app/components/edition.gjs b/app/components/edition.gjs new file mode 100644 index 00000000000..f7f3759101a --- /dev/null +++ b/app/components/edition.gjs @@ -0,0 +1,20 @@ +import Tooltip from 'crates-io/components/tooltip'; + + diff --git a/app/components/edition.hbs b/app/components/edition.hbs deleted file mode 100644 index 839bbac3bd3..00000000000 --- a/app/components/edition.hbs +++ /dev/null @@ -1,14 +0,0 @@ - - {{@version.edition}} edition - - - This crate version does not declare a Minimum Supported Rust Version, but - does require the {{@version.edition}} Rust Edition. - -
    - {{@version.editionMsrv}} was the first version of Rust in this edition, - but this crate may require features that were added in later versions of - Rust. -
    -
    -
    \ No newline at end of file diff --git a/app/components/email-input.gjs b/app/components/email-input.gjs new file mode 100644 index 00000000000..85cf20a7fd3 --- /dev/null +++ b/app/components/email-input.gjs @@ -0,0 +1,160 @@ +import { Input } from '@ember/component'; +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { task } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; +import preventDefault from 'ember-event-helpers/helpers/prevent-default'; +import and from 'ember-truth-helpers/helpers/and'; +import not from 'ember-truth-helpers/helpers/not'; + +export default class EmailInput extends Component { + @service notifications; + + @tracked value; + @tracked isEditing = false; + @tracked disableResend = false; + + resendEmailTask = task(async () => { + try { + await this.args.user.resendVerificationEmail(); + this.disableResend = true; + } catch (error) { + let detail = error.errors?.[0]?.detail; + if (detail && !detail.startsWith('{')) { + this.notifications.error(`Error in resending message: ${detail}`); + } else { + this.notifications.error('Unknown error in resending message'); + } + } + }); + + @action + editEmail() { + this.value = this.args.user.email; + this.isEditing = true; + } + + saveEmailTask = task(async () => { + let userEmail = this.value; + let user = this.args.user; + + try { + await user.changeEmail(userEmail); + + this.isEditing = false; + this.disableResend = false; + } catch (error) { + let detail = error.errors?.[0]?.detail; + + let msg = + detail && !detail.startsWith('{') + ? `An error occurred while saving this email, ${detail}` + : 'An unknown error occurred while saving this email.'; + + this.notifications.error(`Error in saving email: ${msg}`); + } + }); + + +} diff --git a/app/components/email-input.hbs b/app/components/email-input.hbs deleted file mode 100644 index 8f73dbd13bb..00000000000 --- a/app/components/email-input.hbs +++ /dev/null @@ -1,100 +0,0 @@ -
    - {{#unless @user.email}} -
    -

    - Please add your email address. We will only use - it to contact you about your account. We promise we'll never share it! -

    -
    - {{/unless}} - - {{#if this.isEditing }} -
    -
    - -
    - -
    - {{else}} -
    -
    -
    Email
    -
    - -
    - -
    -
    - {{#if (and @user.email (not @user.email_verified))}} -
    -
    - {{#if @user.email_verification_sent}} -

    We have sent a verification email to your address.

    - {{/if}} -

    Your email has not yet been verified.

    -
    -
    - -
    -
    - {{/if}} - {{/if}} - -
    \ No newline at end of file diff --git a/app/components/email-input.js b/app/components/email-input.js deleted file mode 100644 index 993958d9bef..00000000000 --- a/app/components/email-input.js +++ /dev/null @@ -1,55 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -import { task } from 'ember-concurrency'; - -export default class EmailInput extends Component { - @service notifications; - - @tracked value; - @tracked isEditing = false; - @tracked disableResend = false; - - resendEmailTask = task(async () => { - try { - await this.args.user.resendVerificationEmail(); - this.disableResend = true; - } catch (error) { - let detail = error.errors?.[0]?.detail; - if (detail && !detail.startsWith('{')) { - this.notifications.error(`Error in resending message: ${detail}`); - } else { - this.notifications.error('Unknown error in resending message'); - } - } - }); - - @action - editEmail() { - this.value = this.args.user.email; - this.isEditing = true; - } - - saveEmailTask = task(async () => { - let userEmail = this.value; - let user = this.args.user; - - try { - await user.changeEmail(userEmail); - - this.isEditing = false; - this.disableResend = false; - } catch (error) { - let detail = error.errors?.[0]?.detail; - - let msg = - detail && !detail.startsWith('{') - ? `An error occurred while saving this email, ${detail}` - : 'An unknown error occurred while saving this email.'; - - this.notifications.error(`Error in saving email: ${msg}`); - } - }); -} diff --git a/app/components/follow-button.js b/app/components/follow-button.gjs similarity index 60% rename from app/components/follow-button.js rename to app/components/follow-button.gjs index ce9b8ecd9f0..3cd6494d923 100644 --- a/app/components/follow-button.js +++ b/app/components/follow-button.gjs @@ -1,12 +1,37 @@ +import { on } from '@ember/modifier'; import { service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { didCancel, dropTask, task } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; +import or from 'ember-truth-helpers/helpers/or'; + +import LoadingSpinner from 'crates-io/components/loading-spinner'; import ajax from '../utils/ajax'; export default class extends Component { + @service notifications; @tracked following = false; diff --git a/app/components/follow-button.hbs b/app/components/follow-button.hbs deleted file mode 100644 index 9a94fe98bb2..00000000000 --- a/app/components/follow-button.hbs +++ /dev/null @@ -1,22 +0,0 @@ - \ No newline at end of file diff --git a/app/components/footer.gjs b/app/components/footer.gjs new file mode 100644 index 00000000000..324dbd8cce5 --- /dev/null +++ b/app/components/footer.gjs @@ -0,0 +1,60 @@ +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +export default class Footer extends Component { + @service pristineQuery; + + get pristineSupportQuery() { + let params = this.pristineQuery.paramsFor('support'); + return params; + } + + +} diff --git a/app/components/footer.hbs b/app/components/footer.hbs deleted file mode 100644 index d2fcb080ef3..00000000000 --- a/app/components/footer.hbs +++ /dev/null @@ -1,42 +0,0 @@ - \ No newline at end of file diff --git a/app/components/footer.js b/app/components/footer.js deleted file mode 100644 index f2639df2d28..00000000000 --- a/app/components/footer.js +++ /dev/null @@ -1,11 +0,0 @@ -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -export default class Footer extends Component { - @service pristineQuery; - - get pristineSupportQuery() { - let params = this.pristineQuery.paramsFor('support'); - return params; - } -} diff --git a/app/components/front-page-list/item.gjs b/app/components/front-page-list/item.gjs new file mode 100644 index 00000000000..03f0e6484e0 --- /dev/null +++ b/app/components/front-page-list/item.gjs @@ -0,0 +1,14 @@ +import { on } from '@ember/modifier'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + + diff --git a/app/components/front-page-list/item.hbs b/app/components/front-page-list/item.hbs deleted file mode 100644 index f8b437c3a7a..00000000000 --- a/app/components/front-page-list/item.hbs +++ /dev/null @@ -1,7 +0,0 @@ - -
    -
    {{@title}}
    - {{#if @subtitle}}
    {{@subtitle}}
    {{/if}} -
    - {{svg-jar "chevron-right" class=(scoped-class "right")}} -
    \ No newline at end of file diff --git a/app/components/front-page-list/item/placeholder.gjs b/app/components/front-page-list/item/placeholder.gjs new file mode 100644 index 00000000000..604200fe451 --- /dev/null +++ b/app/components/front-page-list/item/placeholder.gjs @@ -0,0 +1,14 @@ +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +import Placeholder from 'crates-io/components/placeholder'; + + diff --git a/app/components/front-page-list/item/placeholder.hbs b/app/components/front-page-list/item/placeholder.hbs deleted file mode 100644 index 5d134acfbd5..00000000000 --- a/app/components/front-page-list/item/placeholder.hbs +++ /dev/null @@ -1,7 +0,0 @@ - \ No newline at end of file diff --git a/app/components/header.gjs b/app/components/header.gjs new file mode 100644 index 00000000000..c3756bfdfd0 --- /dev/null +++ b/app/components/header.gjs @@ -0,0 +1,178 @@ +import { hash } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +// Six hours. +import perform from 'ember-concurrency/helpers/perform'; +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +import ColorSchemeMenu from 'crates-io/components/color-scheme-menu'; +import Dropdown from 'crates-io/components/dropdown'; +import LoadingSpinner from 'crates-io/components/loading-spinner'; +import SearchForm from 'crates-io/components/search-form'; +import UserAvatar from 'crates-io/components/user-avatar'; +import dateFormat from 'crates-io/helpers/date-format'; + +const SUDO_SESSION_DURATION_MS = 6 * 60 * 60 * 1000; + +export default class Header extends Component { + /** @type {import("../services/session").default} */ + @service session; + + @action + enableSudo() { + this.session.setSudo(SUDO_SESSION_DURATION_MS); + } + + @action + disableSudo() { + this.session.setSudo(0); + } + + +} diff --git a/app/components/header.hbs b/app/components/header.hbs deleted file mode 100644 index abf847d1069..00000000000 --- a/app/components/header.hbs +++ /dev/null @@ -1,130 +0,0 @@ -
    -
    - - -

    crates.io

    -
    - -
    -

    - The Rust community’s crate registry -

    - - -
    - - - - -
    -
    \ No newline at end of file diff --git a/app/components/header.js b/app/components/header.js deleted file mode 100644 index 96efa8a535a..00000000000 --- a/app/components/header.js +++ /dev/null @@ -1,21 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -// Six hours. -const SUDO_SESSION_DURATION_MS = 6 * 60 * 60 * 1000; - -export default class Header extends Component { - /** @type {import("../services/session").default} */ - @service session; - - @action - enableSudo() { - this.session.setSudo(SUDO_SESSION_DURATION_MS); - } - - @action - disableSudo() { - this.session.setSudo(0); - } -} diff --git a/app/components/license-expression.gjs b/app/components/license-expression.gjs new file mode 100644 index 00000000000..46da68b7124 --- /dev/null +++ b/app/components/license-expression.gjs @@ -0,0 +1,15 @@ +import parseLicense from 'crates-io/helpers/parse-license'; + + diff --git a/app/components/license-expression.hbs b/app/components/license-expression.hbs deleted file mode 100644 index c5f96437c7a..00000000000 --- a/app/components/license-expression.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#each (parse-license @license) as |part|}} - {{#if part.isKeyword}} - {{part.text}} - {{else if part.link}} - - {{part.text}} - - {{else}} - {{part.text}} - {{/if}} -{{/each}} \ No newline at end of file diff --git a/app/components/loading-spinner.gjs b/app/components/loading-spinner.gjs new file mode 100644 index 00000000000..07c02adf5a3 --- /dev/null +++ b/app/components/loading-spinner.gjs @@ -0,0 +1,7 @@ +import eq from 'ember-truth-helpers/helpers/eq'; + + diff --git a/app/components/loading-spinner.hbs b/app/components/loading-spinner.hbs deleted file mode 100644 index 324a54586d7..00000000000 --- a/app/components/loading-spinner.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
    - Loading… -
    \ No newline at end of file diff --git a/app/components/msrv.gjs b/app/components/msrv.gjs new file mode 100644 index 00000000000..c6092124eb0 --- /dev/null +++ b/app/components/msrv.gjs @@ -0,0 +1,14 @@ +import Tooltip from 'crates-io/components/tooltip'; + + diff --git a/app/components/msrv.hbs b/app/components/msrv.hbs deleted file mode 100644 index fd14d11491f..00000000000 --- a/app/components/msrv.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - v{{@version.msrv}} - - - "Minimum Supported Rust Version" - {{#if @version.edition}} -
    requires Rust Edition {{@version.edition}}
    - {{/if}} -
    -
    \ No newline at end of file diff --git a/app/components/nav-tabs.gjs b/app/components/nav-tabs.gjs new file mode 100644 index 00000000000..f681410e529 --- /dev/null +++ b/app/components/nav-tabs.gjs @@ -0,0 +1,11 @@ +import { hash } from '@ember/helper'; + +import NavTabsTab from 'crates-io/components/nav-tabs/tab'; + + diff --git a/app/components/nav-tabs.hbs b/app/components/nav-tabs.hbs deleted file mode 100644 index bae81817d69..00000000000 --- a/app/components/nav-tabs.hbs +++ /dev/null @@ -1,5 +0,0 @@ - \ No newline at end of file diff --git a/app/components/nav-tabs/tab.gjs b/app/components/nav-tabs/tab.gjs new file mode 100644 index 00000000000..9aa64683cfe --- /dev/null +++ b/app/components/nav-tabs/tab.gjs @@ -0,0 +1,14 @@ +import { on } from '@ember/modifier'; + + diff --git a/app/components/nav-tabs/tab.hbs b/app/components/nav-tabs/tab.hbs deleted file mode 100644 index 06ff03a555f..00000000000 --- a/app/components/nav-tabs/tab.hbs +++ /dev/null @@ -1,10 +0,0 @@ -
  • - - {{yield}} - -
  • \ No newline at end of file diff --git a/app/components/owned-crate-row.gjs b/app/components/owned-crate-row.gjs new file mode 100644 index 00000000000..ad557790288 --- /dev/null +++ b/app/components/owned-crate-row.gjs @@ -0,0 +1,37 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; + +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +export default class OwnedCrateRow extends Component { + @action setEmailNotifications(event) { + let { checked } = event.target; + this.args.ownedCrate.set('email_notifications', checked); + } + + +} diff --git a/app/components/owned-crate-row.hbs b/app/components/owned-crate-row.hbs deleted file mode 100644 index e0e8677336b..00000000000 --- a/app/components/owned-crate-row.hbs +++ /dev/null @@ -1,22 +0,0 @@ - \ No newline at end of file diff --git a/app/components/owned-crate-row.js b/app/components/owned-crate-row.js deleted file mode 100644 index 6c312ae24c6..00000000000 --- a/app/components/owned-crate-row.js +++ /dev/null @@ -1,9 +0,0 @@ -import { action } from '@ember/object'; -import Component from '@glimmer/component'; - -export default class OwnedCrateRow extends Component { - @action setEmailNotifications(event) { - let { checked } = event.target; - this.args.ownedCrate.set('email_notifications', checked); - } -} diff --git a/app/components/owners-list.gjs b/app/components/owners-list.gjs new file mode 100644 index 00000000000..1ca36e19b9b --- /dev/null +++ b/app/components/owners-list.gjs @@ -0,0 +1,34 @@ +import { LinkTo } from '@ember/routing'; +import Component from '@glimmer/component'; + +import eq from 'ember-truth-helpers/helpers/eq'; +import or from 'ember-truth-helpers/helpers/or'; + +import UserAvatar from 'crates-io/components/user-avatar'; + +export default class VersionRow extends Component { + get showDetailedList() { + return this.args.owners.length <= 5; + } + + +} diff --git a/app/components/owners-list.hbs b/app/components/owners-list.hbs deleted file mode 100644 index 27fc0b14824..00000000000 --- a/app/components/owners-list.hbs +++ /dev/null @@ -1,19 +0,0 @@ - \ No newline at end of file diff --git a/app/components/owners-list.js b/app/components/owners-list.js deleted file mode 100644 index b00cce81c75..00000000000 --- a/app/components/owners-list.js +++ /dev/null @@ -1,7 +0,0 @@ -import Component from '@glimmer/component'; - -export default class VersionRow extends Component { - get showDetailedList() { - return this.args.owners.length <= 5; - } -} diff --git a/app/components/page-header.gjs b/app/components/page-header.gjs new file mode 100644 index 00000000000..55648c5b8fb --- /dev/null +++ b/app/components/page-header.gjs @@ -0,0 +1,19 @@ +import LoadingSpinner from 'crates-io/components/loading-spinner'; + + diff --git a/app/components/page-header.hbs b/app/components/page-header.hbs deleted file mode 100644 index d615a9b7727..00000000000 --- a/app/components/page-header.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
    - {{#if (has-block)}} - {{yield}} - {{else}} -

    - {{@title}} - {{#if @suffix}} - {{@suffix}} - {{/if}} - {{#if @showSpinner}} - - {{/if}} -

    - {{/if}} -
    \ No newline at end of file diff --git a/app/components/pagination.gjs b/app/components/pagination.gjs new file mode 100644 index 00000000000..f6326444eb7 --- /dev/null +++ b/app/components/pagination.gjs @@ -0,0 +1,36 @@ +import { concat, hash } from '@ember/helper'; +import { LinkTo } from '@ember/routing'; + +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + + diff --git a/app/components/pagination.hbs b/app/components/pagination.hbs deleted file mode 100644 index 33c707b636d..00000000000 --- a/app/components/pagination.hbs +++ /dev/null @@ -1,17 +0,0 @@ - \ No newline at end of file diff --git a/app/components/pending-owner-invite-row.gjs b/app/components/pending-owner-invite-row.gjs new file mode 100644 index 00000000000..24be4abff07 --- /dev/null +++ b/app/components/pending-owner-invite-row.gjs @@ -0,0 +1,96 @@ +import { on } from '@ember/modifier'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { task } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; + +import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; + +export default class PendingOwnerInviteRow extends Component { + @service notifications; + + @tracked isAccepted = false; + @tracked isDeclined = false; + + acceptInvitationTask = task(async () => { + this.args.invite.set('accepted', true); + + try { + await this.args.invite.save(); + this.isAccepted = true; + } catch (error) { + let detail = error.errors?.[0]?.detail; + if (detail && !detail.startsWith('{')) { + this.notifications.error(`Error in accepting invite: ${detail}`); + } else { + this.notifications.error('Error in accepting invite'); + } + } + }); + + declineInvitationTask = task(async () => { + this.args.invite.set('accepted', false); + + try { + await this.args.invite.save(); + this.isDeclined = true; + } catch (error) { + let detail = error.errors?.[0]?.detail; + if (detail && !detail.startsWith('{')) { + this.notifications.error(`Error in declining invite: ${detail}`); + } else { + this.notifications.error('Error in declining invite'); + } + } + }); + + +} diff --git a/app/components/pending-owner-invite-row.hbs b/app/components/pending-owner-invite-row.hbs deleted file mode 100644 index 43482ae7270..00000000000 --- a/app/components/pending-owner-invite-row.hbs +++ /dev/null @@ -1,34 +0,0 @@ -{{#if this.isAccepted }} -

    - Success! You've been added as an owner of crate - {{@invite.crate_name}}. -

    -{{else if this.isDeclined}} -

    - Declined. You have not been added as an owner of crate - {{@invite.crate_name}}. -

    -{{else}} -
    -
    -

    - - {{@invite.crate_name}} - -

    -
    -
    - Invited by: - - {{@invite.inviter.login}} - -
    -
    - {{date-format-distance-to-now @invite.created_at addSuffix=true}} -
    -
    - - -
    -
    -{{/if}} \ No newline at end of file diff --git a/app/components/pending-owner-invite-row.js b/app/components/pending-owner-invite-row.js deleted file mode 100644 index 3893ab2178f..00000000000 --- a/app/components/pending-owner-invite-row.js +++ /dev/null @@ -1,44 +0,0 @@ -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -import { task } from 'ember-concurrency'; - -export default class PendingOwnerInviteRow extends Component { - @service notifications; - - @tracked isAccepted = false; - @tracked isDeclined = false; - - acceptInvitationTask = task(async () => { - this.args.invite.set('accepted', true); - - try { - await this.args.invite.save(); - this.isAccepted = true; - } catch (error) { - let detail = error.errors?.[0]?.detail; - if (detail && !detail.startsWith('{')) { - this.notifications.error(`Error in accepting invite: ${detail}`); - } else { - this.notifications.error('Error in accepting invite'); - } - } - }); - - declineInvitationTask = task(async () => { - this.args.invite.set('accepted', false); - - try { - await this.args.invite.save(); - this.isDeclined = true; - } catch (error) { - let detail = error.errors?.[0]?.detail; - if (detail && !detail.startsWith('{')) { - this.notifications.error(`Error in declining invite: ${detail}`); - } else { - this.notifications.error('Error in declining invite'); - } - } - }); -} diff --git a/app/components/placeholder.gjs b/app/components/placeholder.gjs new file mode 100644 index 00000000000..c6f791ed8bf --- /dev/null +++ b/app/components/placeholder.gjs @@ -0,0 +1,3 @@ + diff --git a/app/components/placeholder.hbs b/app/components/placeholder.hbs deleted file mode 100644 index 041d8b1f3b6..00000000000 --- a/app/components/placeholder.hbs +++ /dev/null @@ -1 +0,0 @@ -
    \ No newline at end of file diff --git a/app/components/privileged-action.js b/app/components/privileged-action.gjs similarity index 70% rename from app/components/privileged-action.js rename to app/components/privileged-action.gjs index 814de90ebc4..a1f6feac949 100644 --- a/app/components/privileged-action.js +++ b/app/components/privileged-action.gjs @@ -29,6 +29,7 @@ import Component from '@glimmer/component'; * Note that all blocks will be output with a wrapping `
    ` for technical * reasons, so be sure to style accordingly if necessary. */ +import Tooltip from 'crates-io/components/tooltip'; export default class PrivilegedAction extends Component { /** @type {import("../services/session").default} */ @service session; @@ -42,4 +43,33 @@ export default class PrivilegedAction extends Component { get canBePrivileged() { return !this.args.userAuthorised && this.session.currentUser?.is_admin && !this.session.isSudoEnabled; } + + } diff --git a/app/components/privileged-action.hbs b/app/components/privileged-action.hbs deleted file mode 100644 index 8dc0c98b0d2..00000000000 --- a/app/components/privileged-action.hbs +++ /dev/null @@ -1,26 +0,0 @@ -{{#if this.isPrivileged}} -
    - {{yield}} -
    -{{else if this.canBePrivileged}} - {{#if (has-block 'placeholder')}} -
    - {{yield to='placeholder'}} -
    - {{else}} -
    -
    - {{yield}} -
    - - You must enable admin actions before you can perform this operation. - -
    - {{/if}} -{{else}} -
    - {{#if (has-block 'unprivileged')}} - {{yield to='unprivileged'}} - {{/if}} -
    -{{/if}} \ No newline at end of file diff --git a/app/components/progress-bar.js b/app/components/progress-bar.gjs similarity index 61% rename from app/components/progress-bar.js rename to app/components/progress-bar.gjs index 7460a24d892..1ef36a9a087 100644 --- a/app/components/progress-bar.js +++ b/app/components/progress-bar.gjs @@ -2,5 +2,8 @@ import { service } from '@ember/service'; import Component from '@glimmer/component'; export default class extends Component { + @service progress; } diff --git a/app/components/progress-bar.hbs b/app/components/progress-bar.hbs deleted file mode 100644 index 306159404fa..00000000000 --- a/app/components/progress-bar.hbs +++ /dev/null @@ -1 +0,0 @@ -
    \ No newline at end of file diff --git a/app/components/rendered-html.gjs b/app/components/rendered-html.gjs new file mode 100644 index 00000000000..17b1e552762 --- /dev/null +++ b/app/components/rendered-html.gjs @@ -0,0 +1,26 @@ +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import TextContent from 'crates-io/components/text-content'; +import htmlSafe from 'crates-io/helpers/html-safe'; +import highlightSyntax from 'crates-io/modifiers/highlight-syntax'; +import renderMermaids from 'crates-io/modifiers/render-mermaids'; +import updateSourceMedia from 'crates-io/modifiers/update-source-media'; + +export default class extends Component { + + @service colorScheme; +} diff --git a/app/components/rendered-html.hbs b/app/components/rendered-html.hbs deleted file mode 100644 index c95f066316a..00000000000 --- a/app/components/rendered-html.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{!-- - This component renders raw HTML. Be very careful with this since it - can enable cross-site scripting attacks! ---}} - - {{html-safe @html}} - \ No newline at end of file diff --git a/app/components/rendered-html.js b/app/components/rendered-html.js deleted file mode 100644 index f6784d89d72..00000000000 --- a/app/components/rendered-html.js +++ /dev/null @@ -1,6 +0,0 @@ -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -export default class extends Component { - @service colorScheme; -} diff --git a/app/components/results-count.gjs b/app/components/results-count.gjs new file mode 100644 index 00000000000..900593c5154 --- /dev/null +++ b/app/components/results-count.gjs @@ -0,0 +1,9 @@ + diff --git a/app/components/results-count.hbs b/app/components/results-count.hbs deleted file mode 100644 index f1387bf8dd6..00000000000 --- a/app/components/results-count.hbs +++ /dev/null @@ -1,5 +0,0 @@ - - Displaying - {{@start}}-{{@end}} - of {{@total}} {{if @name @name "total results"}} - diff --git a/app/components/rev-dep-row.gjs b/app/components/rev-dep-row.gjs new file mode 100644 index 00000000000..43ed906e3fc --- /dev/null +++ b/app/components/rev-dep-row.gjs @@ -0,0 +1,79 @@ +import { fn } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import { task } from 'ember-concurrency'; +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import or from 'ember-truth-helpers/helpers/or'; + +import Placeholder from 'crates-io/components/placeholder'; +import formatNum from 'crates-io/helpers/format-num'; + +export default class VersionRow extends Component { + @service store; + + @tracked focused = false; + + @action setFocused(value) { + this.focused = value; + } + + constructor() { + super(...arguments); + + this.loadCrateTask.perform().catch(() => { + // ignore all errors and just don't display a description if the request fails + }); + } + + get description() { + return this.loadCrateTask.lastSuccessful?.value?.description; + } + + loadCrateTask = task(async () => { + let { dependency } = this.args; + return await this.store.findRecord('crate', dependency.version.crateName); + }); + + +} diff --git a/app/components/rev-dep-row.hbs b/app/components/rev-dep-row.hbs deleted file mode 100644 index 157b9c92ab6..00000000000 --- a/app/components/rev-dep-row.hbs +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    - - {{@dependency.version.crateName}} - - - depends on {{@dependency.req}} - -
    -
    - {{svg-jar "download-arrow" class=(scoped-class "download-icon")}} - {{format-num @dependency.downloads}} -
    -
    - - {{#if (or this.description this.loadCrateTask.isRunning)}} -
    - {{#if this.loadCrateTask.isRunning}} - - {{else}} - {{this.description}} - {{/if}} -
    - {{/if}} -
    diff --git a/app/components/rev-dep-row.js b/app/components/rev-dep-row.js deleted file mode 100644 index b5c727144df..00000000000 --- a/app/components/rev-dep-row.js +++ /dev/null @@ -1,33 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; -import { tracked } from '@glimmer/tracking'; - -import { task } from 'ember-concurrency'; - -export default class VersionRow extends Component { - @service store; - - @tracked focused = false; - - @action setFocused(value) { - this.focused = value; - } - - constructor() { - super(...arguments); - - this.loadCrateTask.perform().catch(() => { - // ignore all errors and just don't display a description if the request fails - }); - } - - get description() { - return this.loadCrateTask.lastSuccessful?.value?.description; - } - - loadCrateTask = task(async () => { - let { dependency } = this.args; - return await this.store.findRecord('crate', dependency.version.crateName); - }); -} diff --git a/app/components/search-form.gjs b/app/components/search-form.gjs new file mode 100644 index 00000000000..d394d592969 --- /dev/null +++ b/app/components/search-form.gjs @@ -0,0 +1,91 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import preventDefault from 'ember-event-helpers/helpers/prevent-default'; +import onKey from 'ember-keyboard/helpers/on-key'; +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import eq from 'ember-truth-helpers/helpers/eq'; + +import focus from 'crates-io/helpers/focus'; + +export default class Header extends Component { + @service header; + @service router; + + @action updateSearchValue(event) { + let { value } = event.target; + this.header.searchValue = value; + } + + @action search() { + this.router.transitionTo('search', { + queryParams: { + q: this.header.searchValue, + page: 1, + }, + }); + } + + +} diff --git a/app/components/search-form.hbs b/app/components/search-form.hbs deleted file mode 100644 index d8dd2253837..00000000000 --- a/app/components/search-form.hbs +++ /dev/null @@ -1,57 +0,0 @@ - diff --git a/app/components/search-form.js b/app/components/search-form.js deleted file mode 100644 index 4b3aa5343f9..00000000000 --- a/app/components/search-form.js +++ /dev/null @@ -1,22 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -export default class Header extends Component { - @service header; - @service router; - - @action updateSearchValue(event) { - let { value } = event.target; - this.header.searchValue = value; - } - - @action search() { - this.router.transitionTo('search', { - queryParams: { - q: this.header.searchValue, - page: 1, - }, - }); - } -} diff --git a/app/components/settings-page.gjs b/app/components/settings-page.gjs new file mode 100644 index 00000000000..74940588b09 --- /dev/null +++ b/app/components/settings-page.gjs @@ -0,0 +1,16 @@ +import link_ from 'ember-link/helpers/link'; + +import SideMenu from 'crates-io/components/side-menu'; + + diff --git a/app/components/settings-page.hbs b/app/components/settings-page.hbs deleted file mode 100644 index 00fdcfcaabf..00000000000 --- a/app/components/settings-page.hbs +++ /dev/null @@ -1,10 +0,0 @@ -
    - - Profile - API Tokens - - -
    - {{yield}} -
    -
    \ No newline at end of file diff --git a/app/components/settings/api-tokens.gjs b/app/components/settings/api-tokens.gjs new file mode 100644 index 00000000000..5d905f9bb18 --- /dev/null +++ b/app/components/settings/api-tokens.gjs @@ -0,0 +1,234 @@ +import { hash } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { LinkTo } from '@ember/routing'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import { task } from 'ember-concurrency'; +import perform from 'ember-concurrency/helpers/perform'; +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; +import eq from 'ember-truth-helpers/helpers/eq'; +import or from 'ember-truth-helpers/helpers/or'; + +import CopyButton from 'crates-io/components/copy-button'; +import LoadingSpinner from 'crates-io/components/loading-spinner'; +import Tooltip from 'crates-io/components/tooltip'; +import dateFormatDistanceToNow from 'crates-io/helpers/date-format-distance-to-now'; +import isClipboardSupported from 'crates-io/helpers/is-clipboard-supported'; + +import { patternDescription, scopeDescription } from '../../utils/token-scopes'; + +export default class ApiTokens extends Component { + @service store; + @service notifications; + @service router; + + scopeDescription = scopeDescription; + patternDescription = patternDescription; + + get sortedTokens() { + return this.args.tokens + .filter(t => !t.isNew) + .sort((a, b) => { + // Expired tokens are always shown after active ones. + if (a.isExpired && !b.isExpired) { + return 1; + } else if (b.isExpired && !a.isExpired) { + return -1; + } + + // Otherwise, sort normally based on creation time. + return a.created_at < b.created_at ? 1 : -1; + }); + } + + listToParts(list) { + // We hardcode `en-US` here because the rest of the interface text is also currently displayed only in English. + return new Intl.ListFormat('en-US').formatToParts(list); + } + + @action startNewToken() { + this.router.transitionTo('settings.tokens.new'); + } + + revokeTokenTask = task(async token => { + try { + await token.destroyRecord(); + + let index = this.args.tokens.indexOf(token); + if (index !== -1) { + this.args.tokens.splice(index, 1); + } + } catch (error) { + let detail = error.errors?.[0]?.detail; + + let msg = + detail && !detail.startsWith('{') + ? `An error occurred while revoking this token, ${detail}` + : 'An unknown error occurred while revoking this token'; + + this.notifications.error(msg); + } + }); + + +} diff --git a/app/components/settings/api-tokens.hbs b/app/components/settings/api-tokens.hbs deleted file mode 100644 index 00e67ea503e..00000000000 --- a/app/components/settings/api-tokens.hbs +++ /dev/null @@ -1,155 +0,0 @@ -
    -

    API Tokens

    -
    - - New Token - -
    -
    - -

    - You can use the API tokens generated on this page to run cargo - commands that need write access to crates.io. If you want to publish your own - crates then this is required. -

    - -

    - To prevent keys being silently leaked they are stored on crates.io in hashed form. This means you - can only download keys when you first create them. If you have old unused keys you can safely delete - them and create a new one. -

    - -

    - To use an API token, run cargo login - on the command line and paste the key when prompted. This will save it to a - local credentials file. - For CI systems you can use the - CARGO_REGISTRY_TOKEN - environment variable, but make sure that the token stays secret! -

    - -{{#if this.sortedTokens}} - -{{else}} -
    -
    - You have not generated any API tokens yet. -
    - - - New Token - -
    -{{/if}} \ No newline at end of file diff --git a/app/components/settings/api-tokens.js b/app/components/settings/api-tokens.js deleted file mode 100644 index dc66cdd70b9..00000000000 --- a/app/components/settings/api-tokens.js +++ /dev/null @@ -1,61 +0,0 @@ -import { action } from '@ember/object'; -import { service } from '@ember/service'; -import Component from '@glimmer/component'; - -import { task } from 'ember-concurrency'; - -import { patternDescription, scopeDescription } from '../../utils/token-scopes'; - -export default class ApiTokens extends Component { - @service store; - @service notifications; - @service router; - - scopeDescription = scopeDescription; - patternDescription = patternDescription; - - get sortedTokens() { - return this.args.tokens - .filter(t => !t.isNew) - .sort((a, b) => { - // Expired tokens are always shown after active ones. - if (a.isExpired && !b.isExpired) { - return 1; - } else if (b.isExpired && !a.isExpired) { - return -1; - } - - // Otherwise, sort normally based on creation time. - return a.created_at < b.created_at ? 1 : -1; - }); - } - - listToParts(list) { - // We hardcode `en-US` here because the rest of the interface text is also currently displayed only in English. - return new Intl.ListFormat('en-US').formatToParts(list); - } - - @action startNewToken() { - this.router.transitionTo('settings.tokens.new'); - } - - revokeTokenTask = task(async token => { - try { - await token.destroyRecord(); - - let index = this.args.tokens.indexOf(token); - if (index !== -1) { - this.args.tokens.splice(index, 1); - } - } catch (error) { - let detail = error.errors?.[0]?.detail; - - let msg = - detail && !detail.startsWith('{') - ? `An error occurred while revoking this token, ${detail}` - : 'An unknown error occurred while revoking this token'; - - this.notifications.error(msg); - } - }); -} diff --git a/app/components/side-menu.gjs b/app/components/side-menu.gjs new file mode 100644 index 00000000000..08b30d73b4c --- /dev/null +++ b/app/components/side-menu.gjs @@ -0,0 +1,9 @@ +import { hash } from '@ember/helper'; + +import SideMenuItem from 'crates-io/components/side-menu/item'; + + diff --git a/app/components/side-menu.hbs b/app/components/side-menu.hbs deleted file mode 100644 index e82c9fa141c..00000000000 --- a/app/components/side-menu.hbs +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/app/components/side-menu/item.gjs b/app/components/side-menu/item.gjs new file mode 100644 index 00000000000..f131781432f --- /dev/null +++ b/app/components/side-menu/item.gjs @@ -0,0 +1,7 @@ +import { on } from '@ember/modifier'; + + diff --git a/app/components/side-menu/item.hbs b/app/components/side-menu/item.hbs deleted file mode 100644 index ed05714cdbb..00000000000 --- a/app/components/side-menu/item.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
  • - {{yield}} -
  • \ No newline at end of file diff --git a/app/components/sort-dropdown.gjs b/app/components/sort-dropdown.gjs new file mode 100644 index 00000000000..59fbc7091a1 --- /dev/null +++ b/app/components/sort-dropdown.gjs @@ -0,0 +1,20 @@ +import { hash } from '@ember/helper'; + +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + +import Dropdown from 'crates-io/components/dropdown'; +import SortDropdownOption from 'crates-io/components/sort-dropdown/option'; + + diff --git a/app/components/sort-dropdown.hbs b/app/components/sort-dropdown.hbs deleted file mode 100644 index dd6c157233b..00000000000 --- a/app/components/sort-dropdown.hbs +++ /dev/null @@ -1,10 +0,0 @@ - - - {{svg-jar "sort" class=(scoped-class "icon")}} - {{@current}} - - - - {{yield (hash Option=(component "sort-dropdown/option" menu=menu))}} - - \ No newline at end of file diff --git a/app/components/sort-dropdown/option.gjs b/app/components/sort-dropdown/option.gjs new file mode 100644 index 00000000000..e5787485cca --- /dev/null +++ b/app/components/sort-dropdown/option.gjs @@ -0,0 +1,7 @@ +import { LinkTo } from '@ember/routing'; + + diff --git a/app/components/sort-dropdown/option.hbs b/app/components/sort-dropdown/option.hbs deleted file mode 100644 index c0322393a52..00000000000 --- a/app/components/sort-dropdown/option.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<@menu.Item ...attributes> - {{yield}} - \ No newline at end of file diff --git a/app/components/stats-value.gjs b/app/components/stats-value.gjs new file mode 100644 index 00000000000..0940c5f11b3 --- /dev/null +++ b/app/components/stats-value.gjs @@ -0,0 +1,10 @@ +import scopedClass from 'ember-scoped-css/helpers/scoped-class'; +import svgJar from 'ember-svg-jar/helpers/svg-jar'; + + diff --git a/app/components/stats-value.hbs b/app/components/stats-value.hbs deleted file mode 100644 index 7d0cc254bb6..00000000000 --- a/app/components/stats-value.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
    - {{@value}} - {{@label}} - {{svg-jar @icon role="img" aria-hidden="true" class=(scoped-class "icon")}} -
    \ No newline at end of file diff --git a/app/components/support/crate-report-form.gjs b/app/components/support/crate-report-form.gjs new file mode 100644 index 00000000000..926c972f01a --- /dev/null +++ b/app/components/support/crate-report-form.gjs @@ -0,0 +1,199 @@ +import { Input, Textarea } from '@ember/component'; +import { fn, uniqueId } from '@ember/helper'; +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import autoFocus from '@zestia/ember-auto-focus/modifiers/auto-focus'; +import preventDefault from 'ember-event-helpers/helpers/prevent-default'; +import window from 'ember-window-mock'; + +const REASONS = [ + { + reason: 'spam', + description: 'it contains spam', + }, + { + reason: 'name-squatting', + description: 'it is name-squatting (reserving a crate name without content)', + }, + { + reason: 'abuse', + description: 'it is abusive or otherwise harmful', + }, + { + reason: 'security', + description: 'it contains a vulnerability (please try to contact the crate author first)', + }, + { + reason: 'other', + description: 'it is violating the usage policy in some other way (please specify below)', + }, +]; + +export default class CrateReportForm extends Component { + @service store; + + @tracked crate = ''; + @tracked selectedReasons = []; + @tracked detail = ''; + @tracked crateInvalid = false; + @tracked reasonsInvalid = false; + @tracked detailInvalid = false; + + reasons = REASONS; + + constructor() { + super(...arguments); + this.crate = this.args.crate; + } + + validate() { + this.crateInvalid = !this.crate || !this.crate.trim(); + this.reasonsInvalid = this.selectedReasons.length === 0; + this.detailInvalid = this.selectedReasons.includes('other') && !this.detail?.trim(); + return !this.crateInvalid && !this.reasonsInvalid && !this.detailInvalid; + } + + @action resetCrateValidation() { + this.crateInvalid = false; + } + + @action resetDetailValidation() { + this.detailInvalid = false; + } + + @action isReasonSelected(reason) { + return this.selectedReasons.includes(reason); + } + + @action toggleReason(reason) { + this.selectedReasons = this.selectedReasons.includes(reason) + ? this.selectedReasons.filter(it => it !== reason) + : [...this.selectedReasons, reason]; + this.reasonsInvalid = false; + } + + @action + submit() { + if (!this.validate()) { + return; + } + + let mailto = this.composeMail(); + window.open(mailto, '_self'); + } + + composeMail() { + let crate = this.crate; + let reasons = this.reasons + .map(({ reason, description }) => { + let selected = this.isReasonSelected(reason); + return `${selected ? '- [x]' : '- [ ]'} ${description}`; + }) + .join('\n'); + let body = `I'm reporting the https://crates.io/crates/${crate} crate because: + +${reasons} + +Additional details: + +${this.detail} +`; + let subject = `The "${crate}" crate`; + let address = 'help@crates.io'; + let mailto = `mailto:${address}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + return mailto; + } + +