Skip to content

Commit 9897c80

Browse files
committed
Use a shared member emails render
1 parent 41d0ffe commit 9897c80

File tree

4 files changed

+124
-111
lines changed

4 files changed

+124
-111
lines changed

src/queries/me/render.ts

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {getGravatarThumbnail} from '../../templates/avatar';
2-
import {html, sanitizeOption, sanitizeString} from '../../types/html';
2+
import {html, sanitizeOption} from '../../types/html';
33
import {ViewModel} from './view-model';
44
import {renderMemberNumber} from '../../templates/member-number';
55
import {renderOwnerAgreementStatus} from '../shared-render/owner-agreement';
@@ -10,10 +10,6 @@ import {
1010
import {howToGetTrained} from '../shared-render/training-status';
1111
import {ownerResources} from './owner-resources';
1212
import { renderTrainingMatrix } from '../training-matrix/render';
13-
import * as O from 'fp-ts/Option';
14-
import {MemberEmail} from '../../read-models/shared-state/return-types';
15-
import {EmailAddress} from '../../types';
16-
import {SEND_EMAIL_VERIFICATION_COOLDOWN_MS} from '../../commands/members/email-state';
1713
import {renderMemberEmails} from '../shared-render/member-emails';
1814

1915
const editFormOfAddress = (viewModel: ViewModel) => html`
@@ -24,52 +20,15 @@ const editFormOfAddress = (viewModel: ViewModel) => html`
2420
</a>
2521
`;
2622

27-
const sendVerifyEmail = (memberNumber: number, email: MemberEmail) => {
28-
if (
29-
O.isSome(email.verificationLastSent) && (
30-
(Date.now() - email.verificationLastSent.value.getTime()) < SEND_EMAIL_VERIFICATION_COOLDOWN_MS
31-
)
32-
) {
33-
return html`Verification Email Sent At ${sanitizeString(email.verificationLastSent.value.toLocaleTimeString())}!`
34-
}
35-
return html`
36-
<a
37-
href="/members/send-email-verification?email=${sanitizeString(email.emailAddress)}&member=${memberNumber}"
38-
>
39-
Send Verification Email
40-
</a>
41-
`;
42-
}
43-
44-
const setPrimaryEmail = (email: EmailAddress, memberNumber: number) => html`
45-
<a
46-
href="/members/change-primary-email?email=${sanitizeString(email)}&member=${memberNumber}"
47-
>
48-
Make Primary Email
49-
</a>
50-
`;
51-
52-
const addEmail = (memberNumber: number) => html`
53-
<a
54-
href="/members/add-email?member=${memberNumber}"
55-
>
56-
Add New Email
57-
</a>
58-
`;
59-
6023
const editAvatar = () =>
6124
html`<a href="https://gravatar.com/profile">Edit via Gravatar</a>`;
6225

6326
const renderEmailAddresses = (viewModel: ViewModel) =>
64-
renderMemberEmails({
65-
primaryEmailAddress: viewModel.member.primaryEmailAddress,
66-
emails: viewModel.member.emails,
67-
renderAction: email =>
68-
O.isSome(email.verifiedAt)
69-
? setPrimaryEmail(email.emailAddress, viewModel.member.memberNumber)
70-
: sendVerifyEmail(viewModel.member.memberNumber, email),
71-
addEmailAction: O.some(addEmail(viewModel.member.memberNumber)),
72-
});
27+
renderMemberEmails(
28+
viewModel.member.memberNumber,
29+
viewModel.member.emails,
30+
viewModel.member.primaryEmailAddress,
31+
);
7332

7433
const renderMemberDetails = (viewModel: ViewModel) => html`
7534
<table>

src/queries/member/render.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as O from 'fp-ts/Option';
21
import {getGravatarProfile, getGravatarThumbnail} from '../../templates/avatar';
32
import {Html, html, sanitizeOption} from '../../types/html';
43
import {ViewModel} from './view-model';
@@ -29,21 +28,12 @@ const editFormOfAddress = (viewModel: ViewModel) =>
2928
const editAvatar = () =>
3029
html`<a href="https://gravatar.com/profile">Edit via Gravatar</a>`;
3130

32-
const addEmail = (memberNumber: number) => html`
33-
<a href="/members/add-email?member=${memberNumber}">
34-
Add New Email
35-
</a>
36-
`;
37-
3831
const renderEmails = (viewModel: ViewModel) =>
39-
renderMemberEmails({
40-
primaryEmailAddress: viewModel.member.primaryEmailAddress,
41-
emails: viewModel.member.emails,
42-
renderAction: () => html``,
43-
addEmailAction: viewModel.isSuperUser
44-
? O.some(addEmail(viewModel.member.memberNumber))
45-
: O.none,
46-
});
32+
renderMemberEmails(
33+
viewModel.member.memberNumber,
34+
viewModel.member.emails,
35+
viewModel.member.primaryEmailAddress,
36+
);
4737

4838
const ifSelf = (viewModel: ViewModel, fragment: Html) =>
4939
viewModel.isSelf ? fragment : '';
@@ -69,10 +59,14 @@ export const render = (viewModel: ViewModel) => html`
6959
<th scope="row">Other Member Numbers ${otherMemberNumbersTooltip}</th>
7060
<td>${renderMemberNumbers(viewModel.member.pastMemberNumbers)}</td>
7161
</tr>
72-
<tr>
73-
<th scope="row">Email addresses</th>
74-
<td>${renderEmails(viewModel)}</td>
75-
</tr>
62+
${
63+
viewModel.isSelf || viewModel.isSuperUser ? html`
64+
<tr>
65+
<th scope="row">Email addresses</th>
66+
<td>${renderEmails(viewModel)}</td>
67+
</tr>
68+
` : html``
69+
}
7670
<tr>
7771
<th scope="row">
7872
<p>Name</p>

src/queries/shared-render/member-emails.ts

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as O from 'fp-ts/Option';
22
import {MemberEmail} from '../../read-models/shared-state/return-types';
33
import {Html, html, joinHtml, sanitizeString} from '../../types/html';
4+
import { EmailAddress } from '../../types/email-address';
5+
import { SEND_EMAIL_VERIFICATION_COOLDOWN_MS } from '../../commands/members/email-state';
46

57
const sortMemberEmailByVerifiedThenAddedDate = (
68
a: MemberEmail,
@@ -34,19 +36,44 @@ const sortMemberEmailByVerifiedThenAddedDate = (
3436
return -1;
3537
};
3638

37-
type RenderMemberEmailsArgs = {
38-
addEmailAction: O.Option<Html>;
39-
emails: ReadonlyArray<MemberEmail>;
40-
primaryEmailAddress: string;
41-
renderAction: (email: MemberEmail) => Html;
42-
};
39+
const sendVerifyEmail = (memberNumber: number, email: MemberEmail) => {
40+
if (
41+
O.isSome(email.verificationLastSent) && (
42+
(Date.now() - email.verificationLastSent.value.getTime()) < SEND_EMAIL_VERIFICATION_COOLDOWN_MS
43+
)
44+
) {
45+
return html`Verification Email Sent At ${sanitizeString(email.verificationLastSent.value.toLocaleTimeString())}!`
46+
}
47+
return html`
48+
<a
49+
href="/members/send-email-verification?email=${sanitizeString(email.emailAddress)}&member=${memberNumber}"
50+
>
51+
Send Verification Email
52+
</a>
53+
`;
54+
}
4355

44-
export const renderMemberEmails = ({
45-
addEmailAction,
46-
emails,
47-
primaryEmailAddress,
48-
renderAction,
49-
}: RenderMemberEmailsArgs): Html => {
56+
const setPrimaryEmail = (email: EmailAddress, memberNumber: number) => html`
57+
<a
58+
href="/members/change-primary-email?email=${sanitizeString(email)}&member=${memberNumber}"
59+
>
60+
Make Primary Email
61+
</a>
62+
`;
63+
64+
const addEmail = (memberNumber: number) => html`
65+
<a
66+
href="/members/add-email?member=${memberNumber}"
67+
>
68+
Add New Email
69+
</a>
70+
`;
71+
72+
export const renderMemberEmails = (
73+
memberNumber: number,
74+
emails: ReadonlyArray<MemberEmail>,
75+
primaryEmailAddress: EmailAddress
76+
): Html => {
5077
const secondaryEmails = emails
5178
.filter(email => email.emailAddress !== primaryEmailAddress)
5279
.toSorted(sortMemberEmailByVerifiedThenAddedDate);
@@ -58,7 +85,11 @@ export const renderMemberEmails = ({
5885
${sanitizeString(email.emailAddress)}
5986
${O.isSome(email.verifiedAt) ? html`✅` : html``}
6087
</td>
61-
<td>${renderAction(email)}</td>
88+
<td>${
89+
O.isSome(email.verifiedAt)
90+
? setPrimaryEmail(email.emailAddress, memberNumber)
91+
: sendVerifyEmail(memberNumber, email)
92+
}</td>
6293
</tr>
6394
`;
6495

@@ -70,11 +101,9 @@ export const renderMemberEmails = ({
70101
<td></td>
71102
</tr>
72103
${joinHtml(secondaryEmails.map(renderEmailTableRow))}
73-
${O.isSome(addEmailAction)
74-
? html`<tr>
75-
<td colspan="3">${addEmailAction.value}</td>
76-
</tr>`
77-
: html``}
104+
<tr>
105+
<td colspan="3">${addEmail(memberNumber)}</td>
106+
</tr>
78107
</table>
79108
`;
80109
};

tests/queries/member/render.test.ts

Lines changed: 58 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const primaryEmail = 'primary@example.com' as EmailAddress;
1111
const unverifiedEmail = 'extra@example.com' as EmailAddress;
1212
const verifiedSecondaryEmail = 'verified@example.com' as EmailAddress;
1313

14-
const buildViewModel = (isSuperUser: boolean): ViewModel => ({
14+
const buildViewModel = (isSuperUser: boolean, isSelf: boolean): ViewModel => ({
1515
member: {
1616
memberNumber: 123,
1717
pastMemberNumbers: [122],
@@ -48,50 +48,81 @@ const buildViewModel = (isSuperUser: boolean): ViewModel => ({
4848
trainerFor: [],
4949
ownerOf: [],
5050
},
51-
user: {
51+
user: isSelf ? {
52+
memberNumber: 123,
53+
emailAddress: primaryEmail,
54+
} : {
5255
memberNumber: 999,
5356
emailAddress: 'viewer@example.com' as EmailAddress,
5457
},
55-
isSelf: false,
58+
isSelf,
5659
isSuperUser,
5760
trainingMatrix: [],
5861
});
5962

60-
const renderPage = (viewModel: ViewModel) => {
61-
const rendered = render(viewModel);
63+
const renderPage = (viewModel: ViewModel): HTMLBodyElement => {
6264
const body = document.createElement('body');
63-
body.innerHTML = rendered;
65+
body.innerHTML = render(viewModel);
6466
return body;
6567
};
6668

6769
describe('member render', () => {
68-
it('shows the same email table shape used on /me', () => {
69-
const page = renderPage(buildViewModel(true));
70+
describe('as super user', () => {
71+
let viewModel: ViewModel;
72+
let page: HTMLBodyElement;
73+
beforeEach(() => {
74+
viewModel = buildViewModel(true, false);
75+
page = renderPage(viewModel);
76+
});
77+
78+
it('shows the add email action', () => {
79+
expect(
80+
page.querySelector<HTMLAnchorElement>(
81+
'a[href="/members/add-email?member=123"]'
82+
)!.textContent
83+
).toContain('Add New Email');
84+
});
7085

71-
expect(page.textContent).toContain('Email addresses');
72-
expect(page.textContent).toContain(primaryEmail);
73-
expect(page.textContent).toContain(verifiedSecondaryEmail);
74-
expect(page.textContent).toContain(unverifiedEmail);
75-
expect(page.textContent).toContain('Primary');
76-
expect(page.textContent).not.toContain('Send Verification Email');
77-
expect(page.textContent).not.toContain('Make Primary Email');
86+
it('shows the same email table shape used on /me', () => {
87+
expect(page.textContent).toContain('Email addresses');
88+
expect(page.textContent).toContain(primaryEmail);
89+
expect(page.textContent).toContain(verifiedSecondaryEmail);
90+
expect(page.textContent).toContain(unverifiedEmail);
91+
expect(page.textContent).toContain('Primary');
92+
expect(page.textContent).toContain('Send Verification Email');
93+
expect(page.textContent).toContain('Make Primary Email');
94+
});
7895
});
7996

80-
it('shows the add email action to super users', () => {
81-
const page = renderPage(buildViewModel(true));
82-
const addEmailLink = page.querySelector<HTMLAnchorElement>(
83-
'a[href="/members/add-email?member=123"]'
84-
);
97+
describe('as self', () => {
98+
let viewModel: ViewModel;
99+
let page: HTMLBodyElement;
100+
beforeEach(() => {
101+
viewModel = buildViewModel(false, true);
102+
page = renderPage(viewModel);
103+
});
85104

86-
expect(addEmailLink?.textContent).toContain('Add New Email');
105+
it('shows the add email action', () => {
106+
expect(
107+
page.querySelector<HTMLAnchorElement>(
108+
'a[href="/members/add-email?member=123"]'
109+
)!.textContent
110+
).toContain('Add New Email');
111+
});
87112
});
88113

89-
it('does not show the add email action to non-super users', () => {
90-
const page = renderPage(buildViewModel(false));
91-
const addEmailLink = page.querySelector<HTMLAnchorElement>(
92-
'a[href="/members/add-email?member=123"]'
93-
);
114+
describe('non-superuser non-self', () => {
115+
let viewModel: ViewModel;
116+
let page: HTMLBodyElement;
117+
beforeEach(() => {
118+
viewModel = buildViewModel(false, false);
119+
page = renderPage(viewModel);
120+
});
94121

95-
expect(addEmailLink).toBeNull();
122+
it('does not show the add email action', () => {
123+
expect(page.querySelector<HTMLAnchorElement>(
124+
'a[href="/members/add-email?member=123"]'
125+
)).toBeNull();
126+
});
96127
});
97128
});

0 commit comments

Comments
 (0)