Skip to content

Commit 6894987

Browse files
committed
Auto merge of #2623 - thiagoarrais:yank, r=locks
Add yank button to the web UI Trying to fix #1659 This adds a yank/unyank button to the crate and versions pages. By the way, I'm learning Ember and the crates.io codebase for this one. Please review accordingly. I'm assuming that there is a lot of things out of place. Just let me know where everything should go. **Some questions:** 1. I've based this button on the edit email button from the account page, but had to make it smaller because of lack of screen real estate. Is that ok? If so, should this live in the component's `yank-button.module.css` or should I create a `.smaller` entry in `shared/buttons.module.css`? 2. Added `yank-button.js` just to specify an empty tag name that in turn makes Ember avoid generating an enclosing `div` that breaks styling. Is there a cleaner way to do that?
2 parents 3358454 + fca52d6 commit 6894987

File tree

10 files changed

+179
-5
lines changed

10 files changed

+179
-5
lines changed

app/components/yank-button.hbs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{{#if @version.yanked}}
2+
<button
3+
type="button"
4+
local-class="{{this.localClass}}"
5+
...attributes
6+
data-test-version-unyank-button={{@version.num}}
7+
disabled={{@version.unyankTask.isRunning}}
8+
{{on "click" (perform @version.unyankTask)}}
9+
>
10+
{{#if @version.unyankTask.isRunning }}
11+
Unyanking...
12+
{{else}}
13+
Unyank
14+
{{/if}}
15+
</button>
16+
{{else}}
17+
<button
18+
type="button"
19+
local-class="{{this.localClass}}"
20+
...attributes
21+
data-test-version-yank-button={{@version.num}}
22+
disabled={{@version.yankTask.isRunning}}
23+
{{on "click" (perform @version.yankTask)}}
24+
>
25+
{{#if @version.yankTask.isRunning }}
26+
Yanking...
27+
{{else}}
28+
Yank
29+
{{/if}}
30+
</button>
31+
{{/if}}

app/components/yank-button.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Component from '@glimmer/component';
2+
3+
export default class YankButton extends Component {
4+
get tagName() {
5+
return '';
6+
}
7+
8+
get localClass() {
9+
if (this.args.tan) {
10+
return 'tan-button';
11+
}
12+
13+
return 'yellow-button';
14+
}
15+
}

app/components/yank-button.module.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.yellow-button {
2+
composes: yellow-button small from '../styles/shared/buttons.module.css';
3+
}
4+
5+
.tan-button {
6+
composes: tan-button small from '../styles/shared/buttons.module.css';
7+
}

app/controllers/crate/versions.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Controller from '@ember/controller';
2+
import { computed } from '@ember/object';
3+
import { inject as service } from '@ember/service';
4+
5+
export default Controller.extend({
6+
session: service(),
7+
8+
isOwner: computed('model.owner_user', 'session.currentUser.id', function () {
9+
return this.get('model.owner_user').findBy('id', this.get('session.currentUser.id'));
10+
}),
11+
});

app/models/version.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,26 @@ export default class Version extends Model {
6060
}
6161
}).keepLatest())
6262
loadReadmeTask;
63+
64+
@(task(function* () {
65+
let response = yield fetch(`/api/v1/crates/${this.crate.id}/${this.num}/yank`, { method: 'DELETE' });
66+
if (!response.ok) {
67+
throw new Error(`Yank request for ${this.crateName} v${this.num} failed`);
68+
}
69+
this.set('yanked', true);
70+
71+
return yield response.text();
72+
}).keepLatest())
73+
yankTask;
74+
75+
@(task(function* () {
76+
let response = yield fetch(`/api/v1/crates/${this.crate.id}/${this.num}/unyank`, { method: 'PUT' });
77+
if (!response.ok) {
78+
throw new Error(`Unyank request for ${this.crateName} v${this.num} failed`);
79+
}
80+
this.set('yanked', false);
81+
82+
return yield response.text();
83+
}).keepLatest())
84+
unyankTask;
6385
}

app/templates/crate/version.hbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
{{svg-jar "crate" local-class='crate-icon'}}
77
<h1 data-test-crate-name>{{ this.crate.name }}</h1>
88
<h2 data-test-crate-version>{{ this.currentVersion.num }}</h2>
9+
{{#if this.isOwner }}
10+
<YankButton @version={{this.currentVersion}} @tan={{true}} />
11+
{{/if}}
912
</div>
1013

1114
{{#if this.session.currentUser}}

app/templates/crate/versions.hbs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
<span local-class='yanked'>yanked</span>
1818
{{/if}}
1919
</div>
20-
<LinkTo @route="crate.version" @model={{version.num}} local-class="arrow">
21-
{{svg-jar "right-arrow"}}
22-
</LinkTo>
20+
{{#if this.isOwner}}
21+
<YankButton @version={{version}} />
22+
{{else}}
23+
<LinkTo @route="crate.version" @model={{version.num}} local-class="arrow">
24+
{{svg-jar "right-arrow"}}
25+
</LinkTo>
26+
{{/if}}
2327
</div>
2428
{{/each}}
2529
</div>

mirage/route-handlers/crates.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,4 +254,28 @@ export function register(server) {
254254

255255
return {};
256256
});
257+
258+
server.delete('/api/v1/crates/:crate_id/:version/yank', (schema, request) => {
259+
const crateId = request.params.crate_id;
260+
const versionNum = request.params.version;
261+
262+
const version = schema.versions.findBy({ crateId, num: versionNum });
263+
if (!version) {
264+
return notFound();
265+
}
266+
267+
return {};
268+
});
269+
270+
server.put('/api/v1/crates/:crate_id/:version/unyank', (schema, request) => {
271+
const crateId = request.params.crate_id;
272+
const versionNum = request.params.version;
273+
274+
const version = schema.versions.findBy({ crateId, num: versionNum });
275+
if (!version) {
276+
return notFound();
277+
}
278+
279+
return {};
280+
});
257281
}

src/tests/krate.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,24 @@ impl crate::util::MockTokenUser {
7979
}
8080
}
8181

82+
impl crate::util::MockCookieUser {
83+
/// Yank the specified version of the specified crate and run all pending background jobs
84+
fn yank(&self, krate_name: &str, version: &str) -> crate::util::Response<OkBool> {
85+
let url = format!("/api/v1/crates/{}/{}/yank", krate_name, version);
86+
let response = self.delete(&url);
87+
self.app().run_pending_background_jobs();
88+
response
89+
}
90+
91+
/// Unyank the specified version of the specified crate and run all pending background jobs
92+
fn unyank(&self, krate_name: &str, version: &str) -> crate::util::Response<OkBool> {
93+
let url = format!("/api/v1/crates/{}/{}/unyank", krate_name, version);
94+
let response = self.put(&url, &[]);
95+
self.app().run_pending_background_jobs();
96+
response
97+
}
98+
}
99+
82100
#[test]
83101
fn index() {
84102
let (app, anon) = TestApp::init().empty();
@@ -1574,7 +1592,7 @@ fn following() {
15741592

15751593
#[test]
15761594
fn yank_works_as_intended() {
1577-
let (app, anon, _, token) = TestApp::full().with_token();
1595+
let (app, anon, cookie, token) = TestApp::full().with_token();
15781596

15791597
// Upload a new crate, putting it in the git index
15801598
let crate_to_publish = PublishBuilder::new("fyk");
@@ -1608,6 +1626,26 @@ fn yank_works_as_intended() {
16081626

16091627
let json = anon.show_version("fyk", "1.0.0");
16101628
assert!(!json.version.yanked);
1629+
1630+
// yank it
1631+
cookie.yank("fyk", "1.0.0").good();
1632+
1633+
let crates = app.crates_from_index_head("3/f/fyk");
1634+
assert!(crates.len() == 1);
1635+
assert!(crates[0].yanked.unwrap());
1636+
1637+
let json = anon.show_version("fyk", "1.0.0");
1638+
assert!(json.version.yanked);
1639+
1640+
// un-yank it
1641+
cookie.unyank("fyk", "1.0.0").good();
1642+
1643+
let crates = app.crates_from_index_head("3/f/fyk");
1644+
assert!(crates.len() == 1);
1645+
assert!(!crates[0].yanked.unwrap());
1646+
1647+
let json = anon.show_version("fyk", "1.0.0");
1648+
assert!(!json.version.yanked);
16111649
}
16121650

16131651
#[test]

tests/acceptance/crate-test.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { click, fillIn, currentURL, currentRouteName, visit } from '@ember/test-helpers';
1+
import { click, fillIn, currentURL, currentRouteName, visit, waitFor } from '@ember/test-helpers';
22
import { setupApplicationTest } from 'ember-qunit';
33
import { module, test } from 'qunit';
44

@@ -219,6 +219,25 @@ module('Acceptance | crate page', function (hooks) {
219219
assert.dom('[data-test-license]').hasText('MIT/Apache-2.0');
220220
});
221221

222+
test('crates can be yanked by owner', async function (assert) {
223+
this.server.loadFixtures();
224+
225+
let user = this.server.schema.users.findBy({ login: 'thehydroimpulse' });
226+
this.authenticateAs(user);
227+
228+
await visit('/crates/nanomsg/0.5.0');
229+
await click('[data-test-version-yank-button="0.5.0"]');
230+
assert.dom('[data-test-version-yank-button="0.5.0"]').hasText('Yanking...');
231+
assert.dom('[data-test-version-yank-button="0.5.0"]').isDisabled();
232+
233+
await waitFor('[data-test-version-unyank-button="0.5.0"]');
234+
await click('[data-test-version-unyank-button="0.5.0"]');
235+
assert.dom('[data-test-version-unyank-button="0.5.0"]').hasText('Unyanking...');
236+
assert.dom('[data-test-version-unyank-button="0.5.0"]').isDisabled();
237+
238+
await waitFor('[data-test-version-yank-button="0.5.0"]');
239+
});
240+
222241
test('navigating to the owners page when not logged in', async function (assert) {
223242
this.server.loadFixtures();
224243

0 commit comments

Comments
 (0)