Skip to content

Commit 2411fae

Browse files
Merge #1143
1143: #1117 UI for managing crate ownership r=carols10cents Created a separate page for this rather than start tacking on a bunch of modals #1117
2 parents d767028 + 351c703 commit 2411fae

File tree

10 files changed

+309
-2
lines changed

10 files changed

+309
-2
lines changed

app/adapters/crate.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,31 @@ export default ApplicationAdapter.extend({
55
return this.ajax(this.urlForFollowAction(id), 'PUT');
66
},
77

8+
inviteOwner(id, username) {
9+
return this.ajax(this.urlForOwnerAction(id), 'PUT', {
10+
data: {
11+
owners: [username],
12+
}
13+
});
14+
},
15+
16+
removeOwner(id, username) {
17+
return this.ajax(this.urlForOwnerAction(id), 'DELETE', {
18+
data: {
19+
owners: [username],
20+
}
21+
});
22+
},
23+
824
unfollow(id) {
925
return this.ajax(this.urlForFollowAction(id), 'DELETE');
1026
},
1127

1228
urlForFollowAction(id) {
1329
return `${this.buildURL('crate', id)}/follow`;
1430
},
31+
32+
urlForOwnerAction(id) {
33+
return `${this.buildURL('crate', id)}/owners`;
34+
}
1535
});

app/controllers/crate/owners.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Controller, { inject as controller } from '@ember/controller';
2+
import { computed } from '@ember/object';
3+
4+
export default Controller.extend({
5+
crateController: controller('crate'),
6+
crate: computed.alias('crateController.model'),
7+
error: false,
8+
invited: false,
9+
removed: false,
10+
username: '',
11+
12+
actions: {
13+
addOwner() {
14+
this.set('error', false);
15+
this.set('invited', false);
16+
17+
const username = this.get('username');
18+
19+
if (!username) {
20+
this.set('error', 'Please enter a username');
21+
return false;
22+
}
23+
24+
return this.get('crate').inviteOwner(username).then(() => {
25+
this.set('invited', `An invite has been sent to ${username}`);
26+
}).catch((error) => {
27+
if (error.payload) {
28+
this.set('error',
29+
`Error sending invite: ${error.payload.errors[0].detail}`
30+
);
31+
} else {
32+
this.set('error', 'Error sending invite');
33+
}
34+
});
35+
},
36+
37+
removeOwner(user) {
38+
this.set('removed', false);
39+
40+
return this.get('crate').removeOwner(user.get('login')).then(() => {
41+
this.set('removed', `User ${user.get('login')} removed as crate owner`);
42+
43+
this.get('crate.owner_user').removeObject(user);
44+
}).catch((error) => {
45+
if (error.payload) {
46+
this.set('removed',
47+
`Error removing owner: ${error.payload.errors[0].detail}`
48+
);
49+
} else {
50+
this.set('removed', 'Error removing owner');
51+
}
52+
});
53+
}
54+
}
55+
});

app/controllers/crate/version.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export default Controller.extend({
2626
keywords: computed.alias('crate.keywords'),
2727
categories: computed.alias('crate.categories'),
2828
badges: computed.alias('crate.badges'),
29+
isOwner: computed('crate.owner_user', function() {
30+
return this.get('crate.owner_user').findBy('login', this.session.get('currentUser.login'));
31+
}),
2932

3033
sortedVersions: computed.readOnly('crate.versions'),
3134

app/models/crate.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ export default DS.Model.extend({
3838
return this.store.adapterFor('crate').follow(this.get('id'));
3939
},
4040

41+
inviteOwner(username) {
42+
return this.store.adapterFor('crate').inviteOwner(this.get('id'), username);
43+
},
44+
45+
removeOwner(username) {
46+
return this.store.adapterFor('crate').removeOwner(this.get('id'), username);
47+
},
48+
4149
unfollow() {
4250
return this.store.adapterFor('crate').unfollow(this.get('id'));
4351
},

app/router.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Router.map(function() {
2121

2222
this.route('reverse_dependencies');
2323

24+
this.route('owners');
25+
2426
// Well-known routes
2527
this.route('docs');
2628
this.route('repo');

app/templates/crate/owners.hbs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
{{ title 'Manage Crate Owners' }}
2+
3+
<div id='crates-heading'>
4+
{{svg-jar "gear"}}
5+
<h1>Manage Crate Owners</h1>
6+
<h2>{{ crate.name }}</h2>
7+
</div>
8+
9+
<div id="me-email">
10+
<h2>Add Owner</h2>
11+
12+
<div class="row">
13+
<div class="label">
14+
<dt>Username</dt>
15+
</div>
16+
17+
<form class="email-form" {{action 'addOwner' on='submit'}}>
18+
{{input type='text' value=username placeholder='Username' class='form-control space-right' name='username'}}
19+
20+
{{#if error}}
21+
<div class='error'>
22+
<p class='small-text error'>{{error}}</p>
23+
</div>
24+
{{/if}}
25+
26+
{{#if invited}}
27+
<div class='invited'>
28+
<p>{{invited}}</p>
29+
</div>
30+
{{/if}}
31+
32+
<div class="actions">
33+
<button id="add-owner" type="submit" class="small yellow-button space-right">Save</button>
34+
</div>
35+
</form>
36+
</div>
37+
</div>
38+
39+
<h2>Owners</h2>
40+
41+
{{#if removed}}
42+
<div class='removed'>
43+
<p>{{removed}}</p>
44+
</div>
45+
{{/if}}
46+
47+
<div class='owners white-rows'>
48+
{{#each crate.owner_user as |user|}}
49+
<div class='crate row'>
50+
<div>
51+
{{#link-to user.kind user.login}}
52+
{{user-avatar user=user size='medium-small'}}
53+
{{/link-to}}
54+
</div>
55+
<div>
56+
{{#link-to user.kind user.login}}
57+
{{ user.name }}
58+
{{/link-to}}
59+
</div>
60+
<div class='stats'>
61+
{{{ if user.email user.email "&nbsp;" }}}
62+
</div>
63+
<div class='stats downloads'>
64+
<button class='remove-owner small yellow-button' {{action 'removeOwner' user}}>Remove</button>
65+
</div>
66+
</div>
67+
{{/each}}
68+
</div>

app/templates/crate/version.hbs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,17 @@
167167
{{/if}}
168168
{{/unless}}
169169

170-
<div>
170+
<div id='crate-owners'>
171171
<h3>Owners</h3>
172+
173+
{{#if isOwner}}
174+
<p>
175+
{{#link-to 'crate.owners' crate}}
176+
Manage owners
177+
{{/link-to}}
178+
</p>
179+
{{/if}}
180+
172181
<ul class='owners' data-test-owners>
173182
{{#each crate.owner_team as |team|}}
174183
<li>

mirage/config.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,44 @@ export default function() {
206206
let user = schema.users.findBy({ login });
207207
return user ? user : notFound();
208208
});
209+
210+
this.put('/crates/:crate_id/owners', (schema, request) => {
211+
const crateId = request.params.crate_id;
212+
const crate = schema.crates.find(crateId);
213+
214+
if (!crate) {
215+
return notFound();
216+
}
217+
218+
const body = JSON.parse(request.requestBody);
219+
const [ownerId] = body.owners;
220+
const user = schema.users.findBy({ login: ownerId });
221+
222+
if (!user) {
223+
return notFound();
224+
}
225+
226+
return { ok: true };
227+
});
228+
229+
this.delete('/crates/:crate_id/owners', (schema, request) => {
230+
const crateId = request.params.crate_id;
231+
const crate = schema.crates.find(crateId);
232+
233+
if (!crate) {
234+
return notFound();
235+
}
236+
237+
const body = JSON.parse(request.requestBody);
238+
const [ownerId] = body.owners;
239+
const user = schema.users.findBy({ login: ownerId });
240+
241+
if (!user) {
242+
return notFound();
243+
}
244+
245+
return {};
246+
});
209247
}
210248

211249
function notFound() {

mirage/fixtures/users.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,11 @@ export default [{
1313
"login": "thehydroimpulse",
1414
"name": "Daniel Fagnan",
1515
"url": "https://github.com/thehydroimpulse"
16+
}, {
17+
"avatar": "https://avatars3.githubusercontent.com/u/1179195?v=3",
18+
"email": "[email protected]",
19+
"id": 10982,
20+
"login": "iain8",
21+
"name": "Iain Buchanan",
22+
"url": "https://github.com/iain8"
1623
}];

tests/acceptance/crate-test.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { test } from 'qunit';
2-
import { click, visit, currentURL, currentRouteName } from 'ember-native-dom-helpers';
2+
import { click, visit, currentURL, currentRouteName, fillIn } from 'ember-native-dom-helpers';
33
import moduleForAcceptance from 'cargo/tests/helpers/module-for-acceptance';
4+
import Ember from 'ember';
45

56
moduleForAcceptance('Acceptance | crate page');
67

@@ -127,3 +128,99 @@ test('crates license is supplied by version', async function(assert) {
127128
await click('[data-test-version-link="0.5.0"]');
128129
assert.dom('[data-test-license]').hasText('MIT/Apache-2.0');
129130
});
131+
132+
test('navigating to the owners page when not logged in', async function(assert) {
133+
server.loadFixtures();
134+
135+
await visit('/crates/nanomsg');
136+
137+
assert.dom('#crate-owners p a').doesNotExist();
138+
});
139+
140+
test('navigating to the owners page when not an owner', async function(assert) {
141+
server.loadFixtures();
142+
143+
this.application.register('service:session-b', Ember.Service.extend({
144+
currentUser: {
145+
login: 'iain8'
146+
}
147+
}));
148+
149+
this.application.inject('controller', 'session', 'service:session-b');
150+
151+
await visit('/crates/nanomsg');
152+
153+
assert.dom('#crate-owners p a').doesNotExist();
154+
});
155+
156+
test('navigating to the owners page', async function(assert) {
157+
server.loadFixtures();
158+
159+
this.application.register('service:session-b', Ember.Service.extend({
160+
currentUser: {
161+
login: 'thehydroimpulse'
162+
}
163+
}));
164+
165+
this.application.inject('controller', 'session', 'service:session-b');
166+
167+
await visit('/crates/nanomsg');
168+
await click('#crate-owners p a');
169+
170+
assert.dom('#crates-heading h1').hasText('Manage Crate Owners');
171+
});
172+
173+
test('listing crate owners', async function(assert) {
174+
server.loadFixtures();
175+
176+
await visit('/crates/nanomsg/owners');
177+
178+
assert.dom('.owners .row').exists({ count: 2 });
179+
assert.dom('a[href="/users/thehydroimpulse"]').exists();
180+
assert.dom('a[href="/users/blabaere"]').exists();
181+
});
182+
183+
test('attempting to add owner without username', async function(assert) {
184+
server.loadFixtures();
185+
186+
await visit('/crates/nanomsg/owners');
187+
await click('#add-owner');
188+
189+
assert.dom('.error').exists();
190+
assert.dom('.error').hasText('Please enter a username');
191+
assert.dom('.owners .row').exists({ count: 2 });
192+
});
193+
194+
test('attempting to add non-existent owner', async function(assert) {
195+
server.loadFixtures();
196+
197+
await visit('/crates/nanomsg/owners');
198+
await fillIn('input[name="username"]', 'spookyghostboo');
199+
await click('#add-owner');
200+
201+
assert.dom('.error').exists();
202+
assert.dom('.error').hasText('Error sending invite');
203+
assert.dom('.owners .row').exists({ count: 2 });
204+
});
205+
206+
test('add a new owner', async function(assert) {
207+
server.loadFixtures();
208+
209+
await visit('/crates/nanomsg/owners');
210+
await fillIn('input[name="username"]', 'iain8');
211+
await click('#add-owner');
212+
213+
assert.dom('.invited').exists();
214+
assert.dom('.invited').hasText('An invite has been sent to iain8');
215+
assert.dom('.owners .row').exists({ count: 2 });
216+
});
217+
218+
test('remove a crate owner', async function(assert) {
219+
server.loadFixtures();
220+
221+
await visit('/crates/nanomsg/owners');
222+
await click('.owners .row:first-child .remove-owner');
223+
224+
assert.dom('.removed').exists();
225+
assert.dom('.owners .row').exists({ count: 1 });
226+
});

0 commit comments

Comments
 (0)