diff --git a/app/components/trustpub-only-checkbox.css b/app/components/trustpub-only-checkbox.css new file mode 100644 index 00000000000..f6fc65159c8 --- /dev/null +++ b/app/components/trustpub-only-checkbox.css @@ -0,0 +1,25 @@ +.trustpub-only-checkbox { + display: grid; + grid-template: + 'checkbox label' auto + 'checkbox note' auto / 16px 1fr; + row-gap: var(--space-3xs); + column-gap: var(--space-xs); + padding: var(--space-s) var(--space-m); + cursor: pointer; +} + +.checkbox { + grid-area: checkbox; +} + +.label { + grid-area: label; + font-weight: bold; +} + +.note { + grid-area: note; + font-size: 85%; + color: var(--main-color-light); +} diff --git a/app/components/trustpub-only-checkbox.gjs b/app/components/trustpub-only-checkbox.gjs new file mode 100644 index 00000000000..57f1288b6bb --- /dev/null +++ b/app/components/trustpub-only-checkbox.gjs @@ -0,0 +1,41 @@ +import { on } from '@ember/modifier'; +import { action } from '@ember/object'; +import { service } from '@ember/service'; +import Component from '@glimmer/component'; + +import LoadingSpinner from 'crates-io/components/loading-spinner'; + +export default class TrustpubOnlyCheckbox extends Component { + @service notifications; + + @action async toggle(event) { + let { checked } = event.target; + try { + await this.args.crate.setTrustpubOnlyTask.perform(checked); + } catch (error) { + let detail = error.errors?.[0]?.detail; + if (detail && !detail.startsWith('{')) { + this.notifications.error(detail); + } else { + this.notifications.error('Failed to update trusted publishing setting'); + } + } + } + + +} diff --git a/app/controllers/crate/settings/index.js b/app/controllers/crate/settings/index.js index 6d30528bdd4..5c4d10e17f9 100644 --- a/app/controllers/crate/settings/index.js +++ b/app/controllers/crate/settings/index.js @@ -13,6 +13,25 @@ export default class CrateSettingsController extends Controller { username = ''; @tracked addOwnerVisible = false; + /** + * Tracks whether the trustpub_only checkbox was visible when the page loaded. + * This prevents the checkbox from disappearing immediately when unchecked + * if there are no configs - it will only disappear on the next page visit. + */ + trustpubOnlyCheckboxWasVisible = false; + + get #hasConfigs() { + return this.githubConfigs?.length > 0 || this.gitlabConfigs?.length > 0; + } + + get showTrustpubOnlyCheckbox() { + return this.#hasConfigs || this.crate?.trustpub_only || this.trustpubOnlyCheckboxWasVisible; + } + + get showTrustpubOnlyWarning() { + return this.crate?.trustpub_only && !this.#hasConfigs; + } + @action showAddOwnerForm() { this.addOwnerVisible = true; this.username = ''; diff --git a/app/models/crate.js b/app/models/crate.js index f7112d80ea5..e764999d15b 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -4,7 +4,7 @@ import { waitForPromise } from '@ember/test-waiters'; import { cached } from '@glimmer/tracking'; import { apiAction } from '@mainmatter/ember-api-actions'; -import { task } from 'ember-concurrency'; +import { keepLatestTask, task } from 'ember-concurrency'; export default class Crate extends Model { @attr name; @@ -29,6 +29,12 @@ export default class Crate extends Model { @attr documentation; @attr repository; + /** + * Whether this crate can only be published via Trusted Publishing. + * @type {boolean} + */ + @attr trustpub_only; + /** * This isn't an attribute in the crate response. * It's actually the `meta` attribute that belongs to `versions` @@ -123,4 +129,10 @@ export default class Crate extends Model { let fut = reload === true ? versionsRef.reload() : versionsRef.load(); return (await fut) ?? []; }); + + setTrustpubOnlyTask = keepLatestTask(async trustpubOnly => { + let data = { crate: { trustpub_only: trustpubOnly } }; + let payload = await waitForPromise(apiAction(this, { method: 'PATCH', data })); + this.store.pushPayload(payload); + }); } diff --git a/app/routes/crate/settings/index.js b/app/routes/crate/settings/index.js index 5e2ca1bc792..8e26adc8a40 100644 --- a/app/routes/crate/settings/index.js +++ b/app/routes/crate/settings/index.js @@ -19,5 +19,9 @@ export default class SettingsIndexRoute extends Route { controller.set('crate', crate); controller.set('githubConfigs', githubConfigs); controller.set('gitlabConfigs', gitlabConfigs); + + // Capture whether the trustpub_only checkbox should be visible on initial load + let hasConfigs = githubConfigs?.length > 0 || gitlabConfigs?.length > 0; + controller.set('trustpubOnlyCheckboxWasVisible', hasConfigs || crate.trustpub_only); } } diff --git a/app/templates/crate/settings/index.css b/app/templates/crate/settings/index.css index 817498307b0..779b83a9b37 100644 --- a/app/templates/crate/settings/index.css +++ b/app/templates/crate/settings/index.css @@ -54,6 +54,11 @@ background-color: light-dark(white, #141413); border-radius: var(--space-3xs); box-shadow: 0 1px 3px light-dark(hsla(51, 90%, 42%, .35), #232321); +} + +.trustpub table { + width: 100%; + border-spacing: 0; :global(tbody) > :global(tr) > :global(td) { border-top: 1px solid light-dark(hsla(51, 90%, 42%, .25), #232321); @@ -82,7 +87,7 @@ display: none; } - tbody > tr > td:first-child { + tbody > tr:not(.no-trustpub-config) > td:first-child { padding-bottom: 0; } @@ -109,6 +114,14 @@ } } +.trustpub-only-warning { + margin-bottom: var(--space-s); +} + +.trustpub-only-checkbox { + border-top: 1px solid light-dark(hsla(51, 90%, 42%, 0.25), #232321); +} + .email-column { width: 25%; color: var(--main-color-light); diff --git a/app/templates/crate/settings/index.gjs b/app/templates/crate/settings/index.gjs index 0095058b929..ef8c2e8166c 100644 --- a/app/templates/crate/settings/index.gjs +++ b/app/templates/crate/settings/index.gjs @@ -8,8 +8,10 @@ import pageTitle from 'ember-page-title/helpers/page-title'; import not from 'ember-truth-helpers/helpers/not'; import or from 'ember-truth-helpers/helpers/or'; +import Alert from 'crates-io/components/alert'; import CrateHeader from 'crates-io/components/crate-header'; import Tooltip from 'crates-io/components/tooltip'; +import TrustpubOnlyCheckbox from 'crates-io/components/trustpub-only-checkbox'; import UserAvatar from 'crates-io/components/user-avatar';