Skip to content

Commit ae3b553

Browse files
committed
settings/tokens/new: Add "Scopes" section
This only includes the endpoint scopes for now. The crate scopes will be implemented in a dedicated pull request.
1 parent f9d9468 commit ae3b553

File tree

4 files changed

+128
-16
lines changed

4 files changed

+128
-16
lines changed

app/controllers/settings/tokens/new.js

+30-6
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,30 @@ export default class NewTokenController extends Controller {
1313

1414
@tracked name;
1515
@tracked nameInvalid;
16+
@tracked scopes;
17+
@tracked scopesInvalid;
18+
19+
ENDPOINT_SCOPES = [
20+
{ id: 'change-owners', description: 'Invite new crate owners or remove existing ones' },
21+
{ id: 'publish-new', description: 'Publish new crates' },
22+
{ id: 'publish-update', description: 'Publish new versions of existing crates' },
23+
{ id: 'yank', description: 'Yank and unyank crate versions' },
24+
];
1625

1726
constructor() {
1827
super(...arguments);
1928
this.reset();
2029
}
2130

31+
@action isScopeSelected(id) {
32+
return this.scopes.includes(id);
33+
}
34+
2235
saveTokenTask = task(async () => {
23-
let { name } = this;
24-
if (!name) {
25-
this.nameInvalid = true;
26-
return;
27-
}
36+
if (!this.validate()) return;
37+
let { name, scopes } = this;
2838

29-
let token = this.store.createRecord('api-token', { name });
39+
let token = this.store.createRecord('api-token', { name, endpoint_scopes: scopes });
3040

3141
try {
3242
// Save the new API token on the backend
@@ -48,9 +58,23 @@ export default class NewTokenController extends Controller {
4858
reset() {
4959
this.name = '';
5060
this.nameInvalid = false;
61+
this.scopes = [];
62+
this.scopesInvalid = false;
63+
}
64+
65+
validate() {
66+
this.nameInvalid = !this.name;
67+
this.scopesInvalid = this.scopes.length === 0;
68+
69+
return !this.nameInvalid && !this.scopesInvalid;
5170
}
5271

5372
@action resetNameValidation() {
5473
this.nameInvalid = false;
5574
}
75+
76+
@action toggleScope(id) {
77+
this.scopes = this.scopes.includes(id) ? this.scopes.filter(it => it !== id) : [...this.scopes, id];
78+
this.scopesInvalid = false;
79+
}
5680
}

app/styles/settings/tokens/new.module.css

+50-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
.form-group, .buttons {
22
position: relative;
3-
margin: var(--space-s) 0;
3+
margin: var(--space-m) 0;
44
}
55

6-
.form-group {
7-
label {
8-
display: block;
9-
margin-bottom: var(--space-3xs);
10-
font-weight: 600;
11-
}
6+
.form-group-name {
7+
display: block;
8+
margin-bottom: var(--space-2xs);
9+
font-weight: 600;
10+
}
11+
12+
.form-group-error {
13+
display: block;
14+
color: red;
15+
font-size: 0.9em;
16+
margin-top: var(--space-2xs);
1217
}
1318

1419
.buttons {
@@ -21,7 +26,7 @@
2126
max-width: 440px;
2227
width: 100%;
2328
padding: var(--space-2xs);
24-
border: 1px solid #ada796;
29+
border: 1px solid var(--gray-border);
2530
border-radius: var(--space-3xs);
2631

2732
&[aria-invalid="true"] {
@@ -30,6 +35,43 @@
3035
}
3136
}
3237

38+
.scopes-list {
39+
list-style: none;
40+
padding: 0;
41+
margin: 0;
42+
background-color: white;
43+
border: 1px solid var(--gray-border);
44+
border-radius: var(--space-3xs);
45+
46+
&.invalid {
47+
background: #fff2f2;
48+
border-color: red;
49+
}
50+
51+
> * + * {
52+
border-top: inherit;
53+
}
54+
55+
label {
56+
padding: var(--space-xs) var(--space-s);
57+
display: flex;
58+
flex-wrap: wrap;
59+
gap: var(--space-xs);
60+
font-size: 0.9em;
61+
}
62+
}
63+
64+
.scope-id {
65+
display: inline-block;
66+
max-width: 170px;
67+
flex-grow: 1;
68+
font-weight: bold;
69+
}
70+
71+
.scope-description {
72+
display: inline-block;
73+
}
74+
3375
.generate-button {
3476
composes: yellow-button small from '../../../styles/shared/buttons.module.css';
3577
border-radius: 4px;

app/templates/settings/tokens/new.hbs

+32-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
44
<div local-class="form-group">
55
{{#let (unique-id) as |id|}}
6-
<label for={{id}}>Name</label>
6+
<label for={{id}} local-class="form-group-name">Name</label>
7+
78
<Input
89
id={{id}}
910
@type="text"
@@ -16,9 +17,39 @@
1617
{{auto-focus}}
1718
{{on "input" this.resetNameValidation}}
1819
/>
20+
21+
{{#if this.nameInvalid}}
22+
<div local-class="form-group-error">Please enter a name for this token.</div>
23+
{{/if}}
1924
{{/let}}
2025
</div>
2126

27+
<div local-class="form-group">
28+
<div local-class="form-group-name">Scopes</div>
29+
30+
<ul role="list" local-class="scopes-list {{if this.scopesInvalid "invalid"}}">
31+
{{#each this.ENDPOINT_SCOPES as |scope|}}
32+
<li>
33+
<label data-test-scope={{scope.id}}>
34+
<Input
35+
@type="checkbox"
36+
@checked={{this.isScopeSelected scope.id}}
37+
disabled={{this.saveTokenTask.isRunning}}
38+
{{on "change" (fn this.toggleScope scope.id)}}
39+
/>
40+
41+
<span local-class="scope-id">{{scope.id}}</span>
42+
<span local-class="scope-description">{{scope.description}}</span>
43+
</label>
44+
</li>
45+
{{/each}}
46+
</ul>
47+
48+
{{#if this.scopesInvalid}}
49+
<div local-class="form-group-error">Please select at least one token scope.</div>
50+
{{/if}}
51+
</div>
52+
2253
<div local-class="buttons">
2354
<button
2455
type="submit"

tests/routes/settings/tokens/new-test.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ module('/settings/tokens/new', function (hooks) {
5454
assert.strictEqual(currentURL(), '/settings/tokens/new');
5555

5656
await fillIn('[data-test-name]', 'token-name');
57+
await click('[data-test-scope="publish-update"]');
58+
await this.pauseTest();
5759
await click('[data-test-generate]');
5860

5961
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
6062
assert.ok(Boolean(token), 'API token has been created in the backend database');
6163
assert.strictEqual(token.name, 'token-name');
6264
assert.strictEqual(token.crateScopes, null);
63-
assert.strictEqual(token.endpointScopes, null);
65+
assert.deepEqual(token.endpointScopes, ['publish-update']);
6466

6567
assert.strictEqual(currentURL(), '/settings/tokens');
6668
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
@@ -111,4 +113,17 @@ module('/settings/tokens/new', function (hooks) {
111113
assert.strictEqual(currentURL(), '/settings/tokens/new');
112114
assert.dom('[data-test-name]').hasAria('invalid', 'true');
113115
});
116+
117+
test('no scopes selected shows an error', async function (assert) {
118+
prepare(this);
119+
120+
await visit('/settings/tokens/new');
121+
assert.strictEqual(currentURL(), '/settings/tokens/new');
122+
123+
await fillIn('[data-test-name]', 'token-name');
124+
await click('[data-test-generate]');
125+
assert.strictEqual(currentURL(), '/settings/tokens/new');
126+
assert.dom('[data-test-name]').hasAria('invalid', 'true');
127+
await this.pauseTest();
128+
});
114129
});

0 commit comments

Comments
 (0)