Skip to content

Commit 4a86805

Browse files
committed
frontend/settings: Add "Publish Notifications" checkbox
1 parent 7389aaf commit 4a86805

File tree

6 files changed

+177
-1
lines changed

6 files changed

+177
-1
lines changed

app/controllers/settings/profile.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Controller from '@ember/controller';
2+
import { action } from '@ember/object';
3+
import { service } from '@ember/service';
4+
import { tracked } from '@glimmer/tracking';
5+
6+
import { task } from 'ember-concurrency';
7+
8+
export default class extends Controller {
9+
@service notifications;
10+
11+
@tracked publishNotifications;
12+
13+
@action handleNotificationsChange(event) {
14+
this.publishNotifications = event.target.checked;
15+
}
16+
17+
updateNotificationSettings = task(async () => {
18+
try {
19+
await this.model.user.updatePublishNotifications(this.publishNotifications);
20+
} catch {
21+
this.notifications.error(
22+
'Something went wrong while updating your notification settings. Please try again later!',
23+
);
24+
}
25+
});
26+
}

app/models/user.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export default class User extends Model {
1616
@attr avatar;
1717
@attr url;
1818
@attr kind;
19+
@attr publish_notifications;
1920

2021
async stats() {
2122
return await waitForPromise(apiAction(this, { method: 'GET', path: 'stats' }));
@@ -34,6 +35,17 @@ export default class User extends Model {
3435
});
3536
}
3637

38+
async updatePublishNotifications(enabled) {
39+
await waitForPromise(apiAction(this, { method: 'PUT', data: { user: { publish_notifications: enabled } } }));
40+
41+
this.store.pushPayload({
42+
user: {
43+
id: this.id,
44+
publish_notifications: enabled,
45+
},
46+
});
47+
}
48+
3749
async resendVerificationEmail() {
3850
return await waitForPromise(apiAction(this, { method: 'PUT', path: 'resend' }));
3951
}

app/routes/settings/profile.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,9 @@ export default class ProfileSettingsRoute extends AuthenticatedRoute {
88
async model() {
99
return { user: this.session.currentUser };
1010
}
11+
12+
setupController(controller, model) {
13+
super.setupController(...arguments);
14+
controller.publishNotifications = model.user.publish_notifications;
15+
}
1116
}

app/styles/settings/profile.module.css

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,43 @@
3434
}
3535

3636
.me-email {
37-
margin-bottom: var(--space-s);
37+
margin-bottom: var(--space-m);
3838
display: flex;
3939
flex-direction: column;
4040
}
41+
42+
.notifications {
43+
margin-bottom: var(--space-s);
44+
}
45+
46+
.checkbox-input {
47+
display: grid;
48+
grid-template:
49+
"checkbox label" auto
50+
"- note" auto /
51+
auto 1fr;
52+
row-gap: var(--space-3xs);
53+
column-gap: var(--space-xs);
54+
}
55+
56+
.label {
57+
grid-area: label;
58+
font-weight: bold;
59+
}
60+
61+
.note {
62+
grid-area: note;
63+
display: block;
64+
font-size: 85%;
65+
}
66+
67+
.buttons {
68+
display: flex;
69+
align-items: center;
70+
gap: var(--space-2xs);
71+
margin-top: var(--space-s);
72+
}
73+
74+
.update-prefs-button {
75+
composes: yellow-button small from '../shared/buttons.module.css';
76+
}

app/templates/settings/profile.hbs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,37 @@
3131
data-test-email-input
3232
/>
3333
</div>
34+
35+
<div local-class="notifications" data-test-notifications>
36+
<h2>Notification Settings</h2>
37+
38+
<label local-class="checkbox-input">
39+
<Input
40+
@type="checkbox"
41+
@checked={{this.publishNotifications}}
42+
disabled={{this.updateNotificationSettings.isRunning}}
43+
{{on "change" this.handleNotificationsChange}}
44+
/>
45+
<span local-class="label">Publish Notifications</span>
46+
<span local-class="note">
47+
Publish notifications are sent to your email address whenever new
48+
versions of a crate that you own are published. These can be useful to
49+
quickly detect compromised accounts or API tokens.
50+
</span>
51+
</label>
52+
53+
<div local-class="buttons">
54+
<button
55+
type="button"
56+
local-class="update-prefs-button"
57+
disabled={{this.updateNotificationSettings.isRunning}}
58+
{{on "click" (perform this.updateNotificationSettings)}}
59+
>
60+
Update preferences
61+
</button>
62+
{{#if this.updateNotificationSettings.isRunning}}
63+
<LoadingSpinner local-class="spinner" data-test-spinner />
64+
{{/if}}
65+
</div>
66+
</div>
3467
</SettingsPage>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { click, currentURL, visit, waitFor } from '@ember/test-helpers';
2+
import { module, test } from 'qunit';
3+
4+
import { defer } from 'rsvp';
5+
6+
import { Response } from 'miragejs';
7+
8+
import { setupApplicationTest } from 'crates-io/tests/helpers';
9+
10+
module('Acceptance | publish notifications', function (hooks) {
11+
setupApplicationTest(hooks);
12+
13+
test('unsubscribe and resubscribe', async function (assert) {
14+
let user = this.server.create('user');
15+
16+
this.authenticateAs(user);
17+
assert.true(user.publishNotifications);
18+
19+
await visit('/settings/profile');
20+
assert.strictEqual(currentURL(), '/settings/profile');
21+
assert.dom('[data-test-notifications] input[type=checkbox]').isChecked();
22+
23+
await click('[data-test-notifications] input[type=checkbox]');
24+
assert.dom('[data-test-notifications] input[type=checkbox]').isNotChecked();
25+
26+
await click('[data-test-notifications] button');
27+
user.reload();
28+
assert.false(user.publishNotifications);
29+
30+
await click('[data-test-notifications] input[type=checkbox]');
31+
assert.dom('[data-test-notifications] input[type=checkbox]').isChecked();
32+
33+
await click('[data-test-notifications] button');
34+
user.reload();
35+
assert.true(user.publishNotifications);
36+
});
37+
38+
test('loading and error state', async function (assert) {
39+
let user = this.server.create('user');
40+
41+
let deferred = defer();
42+
this.server.put('/api/v1/users/:user_id', deferred.promise);
43+
44+
this.authenticateAs(user);
45+
assert.true(user.publishNotifications);
46+
47+
await visit('/settings/profile');
48+
assert.strictEqual(currentURL(), '/settings/profile');
49+
50+
await click('[data-test-notifications] input[type=checkbox]');
51+
52+
let clickPromise = click('[data-test-notifications] button');
53+
await waitFor('[data-test-notifications] [data-test-spinner]');
54+
assert.dom('[data-test-notifications] input[type=checkbox]').isDisabled();
55+
assert.dom('[data-test-notifications] button').isDisabled();
56+
57+
deferred.resolve(new Response(500));
58+
await clickPromise;
59+
60+
assert
61+
.dom('[data-test-notification-message="error"]')
62+
.hasText('Something went wrong while updating your notification settings. Please try again later!');
63+
});
64+
});

0 commit comments

Comments
 (0)