Skip to content

Commit dc4b98c

Browse files
committed
Add new settings/tokens/new page
1 parent c85c801 commit dc4b98c

File tree

7 files changed

+209
-4
lines changed

7 files changed

+209
-4
lines changed

app/components/header.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
<dd.Menu local-class="current-user-links" as |menu|>
2929
<menu.Item><LinkTo @route="dashboard">Dashboard</LinkTo></menu.Item>
30-
<menu.Item><LinkTo @route="settings">Account Settings</LinkTo></menu.Item>
30+
<menu.Item><LinkTo @route="settings" data-test-settings>Account Settings</LinkTo></menu.Item>
3131
<menu.Item><LinkTo @route="me.pending-invites">Owner Invites</LinkTo></menu.Item>
3232
<menu.Item local-class="menu-item-with-separator">
3333
<button

app/components/settings-page.hbs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<div local-class="page" ...attributes>
2-
<SideMenu as |menu|>
2+
<SideMenu data-test-settings-menu as |menu|>
33
<menu.Item @link={{link "settings.profile"}}>Profile</menu.Item>
44
{{#if this.design.showToggleButton}}
55
<menu.Item @link={{link "settings.appearance"}}>Appearance</menu.Item>
66
{{/if}}
77
<menu.Item @link={{link "settings.email-notifications"}}>Email Notifications</menu.Item>
8-
<menu.Item @link={{link "settings.tokens"}}>API Tokens</menu.Item>
8+
<menu.Item @link={{link "settings.tokens"}} data-test-tokens>API Tokens</menu.Item>
99
</SideMenu>
1010

1111
<div local-class="content">
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
3+
import { inject as service } from '@ember/service';
4+
import { tracked } from '@glimmer/tracking';
5+
6+
import { task } from 'ember-concurrency';
7+
8+
export default class NewTokenController extends Controller {
9+
@service notifications;
10+
@service sentry;
11+
@service store;
12+
@service router;
13+
14+
@tracked name;
15+
@tracked nameInvalid;
16+
17+
constructor() {
18+
super(...arguments);
19+
this.reset();
20+
}
21+
22+
saveTokenTask = task(async () => {
23+
let { name } = this;
24+
if (!name) {
25+
this.nameInvalid = true;
26+
return;
27+
}
28+
29+
let token = this.store.createRecord('api-token', { name });
30+
31+
try {
32+
// Save the new API token on the backend
33+
await token.save();
34+
// Reset the form
35+
this.reset();
36+
// Navigate to the API token list
37+
this.router.transitionTo('settings.tokens.index');
38+
} catch (error) {
39+
// Notify the user
40+
this.notifications.error('An error has occurred while generating your API token. Please try again later.');
41+
// Notify the crates.io team
42+
this.sentry.captureException(error);
43+
// Notify the developer
44+
console.error(error);
45+
}
46+
});
47+
48+
reset() {
49+
this.name = '';
50+
this.nameInvalid = false;
51+
}
52+
53+
@action resetNameValidation() {
54+
this.nameInvalid = false;
55+
}
56+
}

app/router.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ Router.map(function () {
3434
this.route('appearance');
3535
this.route('email-notifications');
3636
this.route('profile');
37-
this.route('tokens');
37+
this.route('tokens', function () {
38+
this.route('new');
39+
});
3840
});
3941
this.route('user', { path: '/users/:user_id' });
4042
this.route('install');
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.form-group, .buttons {
2+
position: relative;
3+
margin: var(--space-s) 0;
4+
}
5+
6+
.form-group {
7+
label {
8+
display: block;
9+
margin-bottom: var(--space-3xs);
10+
font-weight: 600;
11+
}
12+
}
13+
14+
.buttons {
15+
display: flex;
16+
gap: var(--space-2xs);
17+
flex-wrap: wrap;
18+
}
19+
20+
.name-input {
21+
max-width: 440px;
22+
width: 100%;
23+
padding: var(--space-2xs);
24+
border: 1px solid #ada796;
25+
border-radius: var(--space-3xs);
26+
27+
&[aria-invalid="true"] {
28+
background: #fff2f2;
29+
border-color: red;
30+
}
31+
}
32+
33+
.generate-button {
34+
composes: yellow-button small from '../../../styles/shared/buttons.module.css';
35+
border-radius: 4px;
36+
}
37+
38+
.cancel-button {
39+
composes: tan-button small from '../../../styles/shared/buttons.module.css';
40+
border-radius: 4px;
41+
}

app/templates/settings/tokens/new.hbs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<h2>New API Token</h2>
2+
3+
<form local-class="form" {{on "submit" (prevent-default (perform this.saveTokenTask))}}>
4+
<div local-class="form-group">
5+
<label for={{this.id}}>Name</label>
6+
<Input
7+
id={{this.id}}
8+
@type="text"
9+
@value={{this.name}}
10+
aria-required="true"
11+
aria-invalid={{if this.nameInvalid "true" "false"}}
12+
local-class="name-input"
13+
data-test-name
14+
{{auto-focus}}
15+
{{on "input" this.resetNameValidation}}
16+
/>
17+
</div>
18+
19+
<div local-class="buttons">
20+
<button
21+
type="submit"
22+
local-class="generate-button"
23+
data-test-generate
24+
>
25+
Generate Token
26+
</button>
27+
28+
<LinkTo
29+
@route="settings.tokens.index"
30+
local-class="cancel-button"
31+
data-test-cancel
32+
>
33+
Cancel
34+
</LinkTo>
35+
</div>
36+
37+
{{#if this.newToken.isSaving}}
38+
<LoadingSpinner local-class="spinner" data-test-saving-spinner />
39+
{{/if}}
40+
</form>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { click, currentURL, fillIn, findAll } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import { setupApplicationTest } from 'cargo/tests/helpers';
5+
6+
import { visit } from '../../../helpers/visit-ignoring-abort';
7+
8+
module('/settings/tokens/new', function (hooks) {
9+
setupApplicationTest(hooks);
10+
11+
function prepare(context) {
12+
let user = context.server.create('user', {
13+
login: 'johnnydee',
14+
name: 'John Doe',
15+
email: 'john@doe.com',
16+
avatar: 'https://avatars2.githubusercontent.com/u/1234567?v=4',
17+
});
18+
19+
context.authenticateAs(user);
20+
}
21+
22+
test('access is blocked if unauthenticated', async function (assert) {
23+
await visit('/settings/tokens/new');
24+
assert.strictEqual(currentURL(), '/settings/tokens/new');
25+
assert.dom('[data-test-title]').hasText('This page requires authentication');
26+
assert.dom('[data-test-login]').exists();
27+
});
28+
29+
test('happy path', async function (assert) {
30+
prepare(this);
31+
32+
await visit('/settings/tokens/new');
33+
assert.strictEqual(currentURL(), '/settings/tokens/new');
34+
35+
await fillIn('[data-test-name]', 'token-name');
36+
await click('[data-test-generate]');
37+
38+
let token = this.server.schema.apiTokens.findBy({ name: 'token-name' });
39+
assert.ok(Boolean(token), 'API token has been created in the backend database');
40+
41+
assert.strictEqual(currentURL(), '/settings/tokens');
42+
assert.dom('[data-test-api-token="1"] [data-test-name]').hasText('token-name');
43+
assert.dom('[data-test-api-token="1"] [data-test-token]').hasText(token.token);
44+
});
45+
46+
test('cancel button navigates back to the token list', async function (assert) {
47+
prepare(this);
48+
49+
await visit('/settings/tokens/new');
50+
assert.strictEqual(currentURL(), '/settings/tokens/new');
51+
52+
await click('[data-test-cancel]');
53+
assert.strictEqual(currentURL(), '/settings/tokens');
54+
});
55+
56+
test('empty name shows an error', async function (assert) {
57+
prepare(this);
58+
59+
await visit('/settings/tokens/new');
60+
assert.strictEqual(currentURL(), '/settings/tokens/new');
61+
62+
await click('[data-test-generate]');
63+
assert.strictEqual(currentURL(), '/settings/tokens/new');
64+
assert.dom('[data-test-name]').hasAria('invalid', 'true');
65+
});
66+
});

0 commit comments

Comments
 (0)