Skip to content

Commit 1fb1163

Browse files
committed
frontend/settings: Add "Publish Notifications" checkbox
1 parent d471fba commit 1fb1163

File tree

7 files changed

+247
-1
lines changed

7 files changed

+247
-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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { expect, test } from '@/e2e/helper';
2+
3+
test.describe('Acceptance | publish notifications', { tag: '@acceptance' }, () => {
4+
test('unsubscribe and resubscribe', async ({ page, mirage }) => {
5+
await mirage.addHook(server => {
6+
let user = server.create('user');
7+
globalThis.user = user;
8+
authenticateAs(user);
9+
});
10+
11+
await page.goto('/settings/profile');
12+
await expect(page).toHaveURL('/settings/profile');
13+
await expect(page.locator('[data-test-notifications] input[type=checkbox]')).toBeChecked();
14+
15+
await page.click('[data-test-notifications] input[type=checkbox]');
16+
await expect(page.locator('[data-test-notifications] input[type=checkbox]')).not.toBeChecked();
17+
18+
await page.click('[data-test-notifications] button');
19+
await page.waitForFunction(() => globalThis.user.reload().publishNotifications === false);
20+
21+
await page.click('[data-test-notifications] input[type=checkbox]');
22+
await expect(page.locator('[data-test-notifications] input[type=checkbox]')).toBeChecked();
23+
24+
await page.click('[data-test-notifications] button');
25+
await page.waitForFunction(() => globalThis.user.reload().publishNotifications === true);
26+
});
27+
28+
test('loading state', async ({ page, mirage }) => {
29+
await mirage.addHook(server => {
30+
let user = server.create('user');
31+
authenticateAs(user);
32+
globalThis.user = user;
33+
34+
globalThis.deferred = require('rsvp').defer();
35+
server.put('/api/v1/users/:user_id', globalThis.deferred.promise);
36+
});
37+
38+
await page.goto('/settings/profile');
39+
await expect(page).toHaveURL('/settings/profile');
40+
41+
await page.click('[data-test-notifications] input[type=checkbox]');
42+
await page.click('[data-test-notifications] button');
43+
await expect(page.locator('[data-test-notifications] [data-test-spinner]')).toBeVisible();
44+
await expect(page.locator('[data-test-notifications] input[type=checkbox]')).toBeDisabled();
45+
await expect(page.locator('[data-test-notifications] button')).toBeDisabled();
46+
47+
await page.evaluate(async () => globalThis.deferred.resolve());
48+
await expect(page.locator('[data-test-notifications] [data-test-spinner]')).not.toBeVisible();
49+
await expect(page.locator('[data-test-notification-message="error"]')).not.toBeVisible();
50+
});
51+
52+
test('error state', async ({ page, mirage }) => {
53+
await mirage.addHook(server => {
54+
server.logging = true;
55+
let user = server.create('user');
56+
authenticateAs(user);
57+
globalThis.user = user;
58+
59+
server.put('/api/v1/users/:user_id', '', 500);
60+
});
61+
62+
await page.goto('/settings/profile');
63+
await expect(page).toHaveURL('/settings/profile');
64+
65+
await page.click('[data-test-notifications] input[type=checkbox]');
66+
await page.click('[data-test-notifications] button');
67+
await expect(page.locator('[data-test-notifications] [data-test-spinner]')).not.toBeVisible();
68+
await expect(page.locator('[data-test-notification-message="error"]')).toHaveText(
69+
'Something went wrong while updating your notification settings. Please try again later!',
70+
);
71+
});
72+
});
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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+
assert.false(user.reload().publishNotifications);
28+
29+
await click('[data-test-notifications] input[type=checkbox]');
30+
assert.dom('[data-test-notifications] input[type=checkbox]').isChecked();
31+
32+
await click('[data-test-notifications] button');
33+
assert.true(user.reload().publishNotifications);
34+
});
35+
36+
test('loading and error state', async function (assert) {
37+
let user = this.server.create('user');
38+
39+
let deferred = defer();
40+
this.server.put('/api/v1/users/:user_id', deferred.promise);
41+
42+
this.authenticateAs(user);
43+
assert.true(user.publishNotifications);
44+
45+
await visit('/settings/profile');
46+
assert.strictEqual(currentURL(), '/settings/profile');
47+
48+
await click('[data-test-notifications] input[type=checkbox]');
49+
50+
let clickPromise = click('[data-test-notifications] button');
51+
await waitFor('[data-test-notifications] [data-test-spinner]');
52+
assert.dom('[data-test-notifications] input[type=checkbox]').isDisabled();
53+
assert.dom('[data-test-notifications] button').isDisabled();
54+
55+
deferred.resolve(new Response(500));
56+
await clickPromise;
57+
58+
assert
59+
.dom('[data-test-notification-message="error"]')
60+
.hasText('Something went wrong while updating your notification settings. Please try again later!');
61+
});
62+
});

0 commit comments

Comments
 (0)