Skip to content

Commit 34770e8

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 34770e8

File tree

4 files changed

+136
-17
lines changed

4 files changed

+136
-17
lines changed

app/controllers/settings/tokens/new.js

Lines changed: 30 additions & 6 deletions
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

Lines changed: 50 additions & 8 deletions
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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<h2>New API Token</h2>
22

33
<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
4-
<div local-class="form-group">
4+
<div local-class="form-group" data-test-name-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,43 @@
1617
{{auto-focus}}
1718
{{on "input" this.resetNameValidation}}
1819
/>
20+
21+
{{#if this.nameInvalid}}
22+
<div local-class="form-group-error" data-test-error>
23+
Please enter a name for this token.
24+
</div>
25+
{{/if}}
1926
{{/let}}
2027
</div>
2128

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

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,14 @@ 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"]');
5758
await click('[data-test-generate]');
5859

5960
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
6061
assert.ok(Boolean(token), 'API token has been created in the backend database');
6162
assert.strictEqual(token.name, 'token-name');
6263
assert.strictEqual(token.crateScopes, null);
63-
assert.strictEqual(token.endpointScopes, null);
64+
assert.deepEqual(token.endpointScopes, ['publish-update']);
6465

6566
assert.strictEqual(currentURL(), '/settings/tokens');
6667
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
@@ -77,6 +78,7 @@ module('/settings/tokens/new', function (hooks) {
7778
assert.strictEqual(currentURL(), '/settings/tokens/new');
7879

7980
await fillIn('[data-test-name]', 'token-name');
81+
await click('[data-test-scope="publish-update"]');
8082
let clickPromise = click('[data-test-generate]');
8183
await waitFor('[data-test-generate] [data-test-spinner]');
8284
assert.dom('[data-test-name]').isDisabled();
@@ -107,8 +109,24 @@ module('/settings/tokens/new', function (hooks) {
107109
await visit('/settings/tokens/new');
108110
assert.strictEqual(currentURL(), '/settings/tokens/new');
109111

112+
await click('[data-test-scope="publish-update"]');
110113
await click('[data-test-generate]');
111114
assert.strictEqual(currentURL(), '/settings/tokens/new');
112115
assert.dom('[data-test-name]').hasAria('invalid', 'true');
116+
assert.dom('[data-test-name-group] [data-test-error]').exists();
117+
assert.dom('[data-test-scopes-group] [data-test-error]').doesNotExist();
118+
});
119+
120+
test('no scopes selected shows an error', async function (assert) {
121+
prepare(this);
122+
123+
await visit('/settings/tokens/new');
124+
assert.strictEqual(currentURL(), '/settings/tokens/new');
125+
126+
await fillIn('[data-test-name]', 'token-name');
127+
await click('[data-test-generate]');
128+
assert.strictEqual(currentURL(), '/settings/tokens/new');
129+
assert.dom('[data-test-name-group] [data-test-error]').doesNotExist();
130+
assert.dom('[data-test-scopes-group] [data-test-error]').exists();
113131
});
114132
});

0 commit comments

Comments
 (0)