diff --git a/app/components/version-list/row.hbs b/app/components/version-list/row.hbs
index 11ce1dcc151..97a3bc2c2bb 100644
--- a/app/components/version-list/row.hbs
+++ b/app/components/version-list/row.hbs
@@ -129,16 +129,15 @@
-
+
diff --git a/app/components/version-list/row.js b/app/components/version-list/row.js
index 15deed1253a..82ee537993c 100644
--- a/app/components/version-list/row.js
+++ b/app/components/version-list/row.js
@@ -4,12 +4,9 @@ import { htmlSafe } from '@ember/template';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
-import { keepLatestTask } from 'ember-concurrency';
-
import styles from './row.module.css';
export default class VersionRow extends Component {
- @service notifications;
@service session;
@tracked focused = false;
@@ -62,16 +59,4 @@ export default class VersionRow extends Component {
@action setFocused(value) {
this.focused = value;
}
-
- rebuildDocsTask = keepLatestTask(async () => {
- let { version } = this.args;
- try {
- await version.rebuildDocs();
- this.notifications.success('Docs rebuild task was enqueued successfully!');
- } catch (error) {
- let reason = error?.errors?.[0]?.detail ?? 'Failed to equeue docs rebuild task.';
- let msg = `Error: ${reason}`;
- this.notifications.error(msg);
- }
- });
}
diff --git a/app/controllers/crate/rebuild-docs.js b/app/controllers/crate/rebuild-docs.js
new file mode 100644
index 00000000000..accf689c248
--- /dev/null
+++ b/app/controllers/crate/rebuild-docs.js
@@ -0,0 +1,22 @@
+import Controller from '@ember/controller';
+import { service } from '@ember/service';
+
+import { keepLatestTask } from 'ember-concurrency';
+
+export default class RebuildDocsController extends Controller {
+ @service notifications;
+ @service router;
+
+ rebuildTask = keepLatestTask(async () => {
+ let { version } = this.model;
+ try {
+ await version.rebuildDocs();
+ this.notifications.success('Docs rebuild task was enqueued successfully!');
+ this.router.transitionTo('crate.versions', version.crate.name);
+ } catch (error) {
+ let reason = error?.errors?.[0]?.detail ?? 'Failed to enqueue docs rebuild task.';
+ let msg = `Error: ${reason}`;
+ this.notifications.error(msg);
+ }
+ });
+}
diff --git a/app/router.js b/app/router.js
index 4a4a34363c6..54aca0ef7ca 100644
--- a/app/router.js
+++ b/app/router.js
@@ -14,6 +14,7 @@ Router.map(function () {
this.route('dependencies');
this.route('version', { path: '/:version_num' });
this.route('version-dependencies', { path: '/:version_num/dependencies' });
+ this.route('rebuild-docs', { path: '/:version_num/rebuild-docs' });
this.route('range', { path: '/range/:range' });
this.route('reverse-dependencies', { path: 'reverse_dependencies' });
diff --git a/app/routes/crate/rebuild-docs.js b/app/routes/crate/rebuild-docs.js
new file mode 100644
index 00000000000..ed634e0690a
--- /dev/null
+++ b/app/routes/crate/rebuild-docs.js
@@ -0,0 +1,34 @@
+import { service } from '@ember/service';
+
+import AuthenticatedRoute from '../-authenticated-route';
+
+export default class RebuildDocsRoute extends AuthenticatedRoute {
+ @service router;
+ @service session;
+ @service store;
+
+ async model(params) {
+ // Get the crate from parent route
+ let crate = this.modelFor('crate');
+
+ // Load the specific version
+ let version = await this.store.queryRecord('version', {
+ name: crate.id,
+ num: params.version_num,
+ });
+
+ return { crate, version };
+ }
+
+ async afterModel(model, transition) {
+ let user = this.session.currentUser;
+ let owners = await model.crate.owner_user;
+ let isOwner = owners.some(owner => owner.id === user.id);
+ if (!isOwner) {
+ this.router.replaceWith('catch-all', {
+ transition,
+ title: 'This page is only accessible by crate owners',
+ });
+ }
+ }
+}
diff --git a/app/styles/crate/rebuild-docs.module.css b/app/styles/crate/rebuild-docs.module.css
new file mode 100644
index 00000000000..7d5369f9860
--- /dev/null
+++ b/app/styles/crate/rebuild-docs.module.css
@@ -0,0 +1,35 @@
+.content {
+ max-width: 600px;
+ margin: var(--space-xl) auto;
+
+ h1 {
+ margin-top: 0;
+ }
+}
+
+.crate-info {
+ background-color: var(--orange-50);
+ border-radius: 8px;
+ padding: var(--space-m);
+ margin: var(--space-m) 0;
+ border: 1px solid var(--orange-200);
+
+ h2 {
+ margin: 0 0 var(--space-s);
+ }
+}
+
+.info-row {
+ margin-top: var(--space-xs);
+}
+
+.description {
+ margin: var(--space-m) 0;
+}
+
+.actions {
+ margin-top: var(--space-m);
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-s);
+}
diff --git a/app/templates/crate/rebuild-docs.hbs b/app/templates/crate/rebuild-docs.hbs
new file mode 100644
index 00000000000..35639a39abc
--- /dev/null
+++ b/app/templates/crate/rebuild-docs.hbs
@@ -0,0 +1,47 @@
+{{page-title 'Rebuild Documentation'}}
+
+
+
Rebuild Documentation
+
+
+
Crate Information
+
+ Crate: {{@model.crate.name}}
+
+
+ Version: {{@model.version.num}}
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
\ No newline at end of file
diff --git a/e2e/acceptance/rebuild-docs.spec.ts b/e2e/acceptance/rebuild-docs.spec.ts
new file mode 100644
index 00000000000..f9bd14b49d8
--- /dev/null
+++ b/e2e/acceptance/rebuild-docs.spec.ts
@@ -0,0 +1,78 @@
+import { expect, test } from '@/e2e/helper';
+
+test.describe('Acceptance | rebuild docs page', { tag: '@acceptance' }, () => {
+ test('navigates to rebuild docs confirmation page', async ({ page, msw }) => {
+ let user = msw.db.user.create();
+ await msw.authenticateAs(user);
+
+ let crate = msw.db.crate.create({ name: 'nanomsg' });
+ msw.db.crateOwnership.create({ crate, user });
+
+ msw.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
+ msw.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
+ msw.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
+ msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await page.goto('/crates/nanomsg/versions');
+ await expect(page).toHaveURL('/crates/nanomsg/versions');
+
+ await expect(page.locator('[data-test-version]')).toHaveCount(4);
+ let versions = await page.locator('[data-test-version]').evaluateAll(el => el.map(it => it.dataset.testVersion));
+ expect(versions).toEqual(['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
+
+ let v021 = page.locator('[data-test-version="0.2.1"]');
+ await v021.locator('[data-test-actions-toggle]').click();
+ await v021.getByRole('link', { name: 'Rebuild Docs' }).click();
+
+ await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
+ await expect(page.locator('[data-test-title]')).toHaveText('Rebuild Documentation');
+ });
+
+ test('rebuild docs confirmation page shows crate info and allows confirmation', async ({ page, msw }) => {
+ let user = msw.db.user.create();
+ await msw.authenticateAs(user);
+
+ let crate = msw.db.crate.create({ name: 'nanomsg' });
+ msw.db.crateOwnership.create({ crate, user });
+
+ msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
+ await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
+
+ await expect(page.locator('[data-test-title]')).toHaveText('Rebuild Documentation');
+ await expect(page.locator('[data-test-crate-name]')).toHaveText('nanomsg');
+ await expect(page.locator('[data-test-version-num]')).toHaveText('0.2.1');
+
+ await page.getByRole('button', { name: 'Confirm Rebuild' }).click();
+
+ let message = 'Docs rebuild task was enqueued successfully!';
+ await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message);
+ await expect(page).toHaveURL('/crates/nanomsg/versions');
+ });
+
+ test('rebuild docs confirmation page redirects non-owners to error page', async ({ page, msw }) => {
+ let user = msw.db.user.create();
+ await msw.authenticateAs(user);
+
+ let crate = msw.db.crate.create({ name: 'nanomsg' });
+ msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
+
+ // Non-owners should be redirected to the catch-all error page
+ await expect(page.getByText('This page is only accessible by crate owners')).toBeVisible();
+ });
+
+ test('rebuild docs confirmation page shows authentication error for unauthenticated users', async ({ page, msw }) => {
+ let crate = msw.db.crate.create({ name: 'nanomsg' });
+ msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await page.goto('/crates/nanomsg/0.2.1/rebuild-docs');
+
+ // Unauthenticated users should see authentication error
+ await expect(page).toHaveURL('/crates/nanomsg/0.2.1/rebuild-docs');
+ await expect(page.locator('[data-test-title]')).toHaveText('This page requires authentication');
+ await expect(page.locator('[data-test-login]')).toBeVisible();
+ });
+});
diff --git a/e2e/acceptance/versions.spec.ts b/e2e/acceptance/versions.spec.ts
index c895a34f7b6..c61d297a6b0 100644
--- a/e2e/acceptance/versions.spec.ts
+++ b/e2e/acceptance/versions.spec.ts
@@ -68,31 +68,4 @@ test.describe('Acceptance | crate versions page', { tag: '@acceptance' }, () =>
await expect(v020).not.toHaveClass(/.*latest/);
await expect(v020).not.toHaveClass(/.yanked/);
});
-
- test('triggers a rebuild for crate documentation', async ({ page, msw }) => {
- let user = msw.db.user.create();
- await msw.authenticateAs(user);
-
- let crate = msw.db.crate.create({ name: 'nanomsg' });
- msw.db.crateOwnership.create({ crate, user });
-
- msw.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
- msw.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
- msw.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
- msw.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
-
- await page.goto('/crates/nanomsg/versions');
- await expect(page).toHaveURL('/crates/nanomsg/versions');
-
- await expect(page.locator('[data-test-version]')).toHaveCount(4);
- let versions = await page.locator('[data-test-version]').evaluateAll(el => el.map(it => it.dataset.testVersion));
- expect(versions).toEqual(['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
-
- let v021 = page.locator('[data-test-version="0.2.1"]');
- await v021.locator('[data-test-actions-toggle]').click();
- await v021.getByRole('button', { name: 'Rebuild Docs' }).click();
-
- let message = 'Docs rebuild task was enqueued successfully!';
- await expect(page.locator('[data-test-notification-message="success"]')).toHaveText(message);
- });
});
diff --git a/tests/acceptance/rebuild-docs-test.js b/tests/acceptance/rebuild-docs-test.js
new file mode 100644
index 00000000000..7162408a3b5
--- /dev/null
+++ b/tests/acceptance/rebuild-docs-test.js
@@ -0,0 +1,82 @@
+import { click, currentURL, findAll } from '@ember/test-helpers';
+import { module, test } from 'qunit';
+
+import { setupApplicationTest } from 'crates-io/tests/helpers';
+
+import { visit } from '../helpers/visit-ignoring-abort';
+
+module('Acceptance | rebuild docs page', function (hooks) {
+ setupApplicationTest(hooks);
+
+ test('navigates to rebuild docs confirmation page', async function (assert) {
+ let user = this.db.user.create();
+ this.authenticateAs(user);
+
+ let crate = this.db.crate.create({ name: 'nanomsg' });
+ this.db.crateOwnership.create({ crate, user });
+
+ this.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
+ this.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
+ this.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
+ this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await visit('/crates/nanomsg/versions');
+ assert.strictEqual(currentURL(), '/crates/nanomsg/versions');
+
+ let versions = findAll('[data-test-version]').map(it => it.dataset.testVersion);
+ assert.deepEqual(versions, ['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
+
+ await click('[data-test-version="0.2.1"] [data-test-actions-toggle]');
+ await click('[data-test-version="0.2.1"] [data-test-id="btn-rebuild-docs"]');
+
+ assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
+ assert.dom('[data-test-title]').hasText('Rebuild Documentation');
+ });
+
+ test('rebuild docs confirmation page shows crate info and allows confirmation', async function (assert) {
+ let user = this.db.user.create();
+ this.authenticateAs(user);
+
+ let crate = this.db.crate.create({ name: 'nanomsg' });
+ this.db.crateOwnership.create({ crate, user });
+
+ this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await visit('/crates/nanomsg/0.2.1/rebuild-docs');
+ assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
+
+ assert.dom('[data-test-title]').hasText('Rebuild Documentation');
+ assert.dom('[data-test-crate-name]').hasText('nanomsg');
+ assert.dom('[data-test-version-num]').hasText('0.2.1');
+
+ await click('[data-test-confirm-rebuild-button]');
+
+ let message = 'Docs rebuild task was enqueued successfully!';
+ assert.dom('[data-test-notification-message="success"]').hasText(message);
+ assert.strictEqual(currentURL(), '/crates/nanomsg/versions');
+ });
+
+ test('rebuilds docs confirmation page redirects non-owners to error page', async function (assert) {
+ let user = this.db.user.create();
+ this.authenticateAs(user);
+
+ let crate = this.db.crate.create({ name: 'nanomsg' });
+ this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await visit('/crates/nanomsg/0.2.1/rebuild-docs');
+ assert.dom('[data-test-title]').hasText('This page is only accessible by crate owners');
+ assert.dom('[data-test-go-back]').exists();
+ });
+
+ test('rebuild docs confirmation page shows authentication error for unauthenticated users', async function (assert) {
+ let crate = this.db.crate.create({ name: 'nanomsg' });
+ this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
+
+ await visit('/crates/nanomsg/0.2.1/rebuild-docs');
+
+ // Unauthenticated users should see authentication error
+ assert.strictEqual(currentURL(), '/crates/nanomsg/0.2.1/rebuild-docs');
+ assert.dom('[data-test-title]').hasText('This page requires authentication');
+ assert.dom('[data-test-login]').exists();
+ });
+});
diff --git a/tests/acceptance/versions-test.js b/tests/acceptance/versions-test.js
index 5cc09f13169..cff0df2e9b2 100644
--- a/tests/acceptance/versions-test.js
+++ b/tests/acceptance/versions-test.js
@@ -81,29 +81,4 @@ module('Acceptance | crate versions page', function (hooks) {
.hasNoClass(/.*latest/)
.hasNoClass(/.yanked/);
});
-
- test('triggers a rebuild for crate documentation', async function (assert) {
- let user = this.db.user.create();
- this.authenticateAs(user);
-
- let crate = this.db.crate.create({ name: 'nanomsg' });
- this.db.crateOwnership.create({ crate, user });
-
- this.db.version.create({ crate, num: '0.1.0', created_at: '2017-01-01' });
- this.db.version.create({ crate, num: '0.2.0', created_at: '2018-01-01' });
- this.db.version.create({ crate, num: '0.3.0', created_at: '2019-01-01', rust_version: '1.69' });
- this.db.version.create({ crate, num: '0.2.1', created_at: '2020-01-01' });
-
- await visit('/crates/nanomsg/versions');
- assert.strictEqual(currentURL(), '/crates/nanomsg/versions');
-
- let versions = findAll('[data-test-version]').map(it => it.dataset.testVersion);
- assert.deepEqual(versions, ['0.2.1', '0.3.0', '0.2.0', '0.1.0']);
-
- await click('[data-test-version="0.2.1"] [data-test-actions-toggle]');
- await click('[data-test-version="0.2.1"] [data-test-id="btn-rebuild-docs"]');
-
- let message = 'Docs rebuild task was enqueued successfully!';
- assert.dom('[data-test-notification-message="success"]').hasText(message);
- });
});