Merge "Add getAccountEmails to user-model"
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
index e5c2bc2..2220d2f 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.ts
@@ -3,7 +3,6 @@
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import '@polymer/iron-input/iron-input';
import '../../shared/gr-button/gr-button';
import {EmailInfo} from '../../../types/common';
import {getAppContext} from '../../../services/app-context';
@@ -14,6 +13,9 @@
import {ValueChangedEvent} from '../../../types/events';
import {fire} from '../../../utils/event-util';
import {deepClone} from '../../../utils/deep-util';
+import {userModelToken} from '../../../models/user/user-model';
+import {resolve} from '../../../models/dependency';
+import {subscribe} from '../../lit/subscription-controller';
@customElement('gr-email-editor')
export class GrEmailEditor extends LitElement {
@@ -30,6 +32,21 @@
readonly restApiService = getAppContext().restApiService;
+ private readonly getUserModel = resolve(this, userModelToken);
+
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getUserModel().emails$,
+ x => {
+ if (!x) return;
+ this.originalEmails = deepClone<EmailInfo[]>(x);
+ this.emails = deepClone<EmailInfo[]>(x);
+ }
+ );
+ }
+
static override get styles() {
return [
sharedStyles,
@@ -83,24 +100,20 @@
return html`<tr>
<td class="emailColumn">${email.email}</td>
<td class="preferredControl" @click=${this.handlePreferredControlClick}>
- <iron-input
+ <!-- We have to use \`.checked\` rather then \`?checked\` as there
+ appears to be an issue when deleting, checked doesn't work correctly. -->
+ <input
class="preferredRadio"
+ type="radio"
+ name="preferred"
+ .value=${email.email}
+ .checked=${email.preferred}
@change=${this.handlePreferredChange}
- .bindValue=${email.email}
- >
- <input
- class="preferredRadio"
- type="radio"
- @change=${this.handlePreferredChange}
- name="preferred"
- ?checked=${email.preferred}
- />
- </iron-input>
+ />
</td>
<td>
<gr-button
- data-index=${index}
- @click=${this.handleDeleteButton}
+ @click=${() => this.handleDeleteButton(index)}
?disabled=${this.checkPreferred(email.preferred)}
class="remove-button"
>Delete</gr-button
@@ -109,14 +122,6 @@
</tr>`;
}
- loadData() {
- return this.restApiService.getAccountEmails().then(emails => {
- if (!emails) return;
- this.originalEmails = deepClone<EmailInfo[]>(emails);
- this.emails = emails;
- });
- }
-
save() {
const promises: Promise<unknown>[] = [];
@@ -130,26 +135,21 @@
);
}
- return Promise.all(promises).then(() => {
- this.originalEmails = this.emails;
+ return Promise.all(promises).then(async () => {
this.emailsToRemove = [];
this.newPreferred = '';
+ await this.getUserModel().loadEmails(true);
this.setHasUnsavedChanges();
});
}
- private handleDeleteButton(e: Event) {
- const target = e.target;
- if (!(target instanceof Element)) return;
- const indexStr = target.getAttribute('data-index');
- if (indexStr === null) return;
- const index = Number(indexStr);
+ private handleDeleteButton(index: number) {
const email = this.emails[index];
// Don't add project to emailsToRemove if it wasn't in
- // originalEmails.
+ // emails.
// We have to use JSON.stringify as we cloned the array
// so the reference is not the same.
- const emails = this.originalEmails.some(
+ const emails = this.emails.some(
x => JSON.stringify(email) === JSON.stringify(x)
);
if (emails) this.emailsToRemove.push(email);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index 39c3288..84ed8dc 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -11,6 +11,7 @@
suite('gr-email-editor tests', () => {
let element: GrEmailEditor;
+ let accountEmailStub: sinon.SinonStub;
setup(async () => {
const emails = [
@@ -19,13 +20,14 @@
{email: 'email@three.com'},
];
- stubRestApi('getAccountEmails').returns(Promise.resolve(emails));
+ accountEmailStub = stubRestApi('getAccountEmails').returns(
+ Promise.resolve(emails)
+ );
element = await fixture<GrEmailEditor>(
html`<gr-email-editor></gr-email-editor>`
);
- await element.loadData();
await element.updateComplete;
});
@@ -45,20 +47,17 @@
<tr>
<td class="emailColumn">email@one.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@one.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@one.com"
+ />
</td>
<td>
<gr-button
aria-disabled="false"
class="remove-button"
- data-index="0"
role="button"
tabindex="0"
>
@@ -69,21 +68,17 @@
<tr>
<td class="emailColumn">email@two.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- checked=""
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@two.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@two.com"
+ />
</td>
<td>
<gr-button
aria-disabled="true"
class="remove-button"
- data-index="1"
disabled=""
role="button"
tabindex="-1"
@@ -95,20 +90,17 @@
<tr>
<td class="emailColumn">email@three.com</td>
<td class="preferredControl">
- <iron-input class="preferredRadio">
- <input
- class="preferredRadio"
- name="preferred"
- type="radio"
- value="email@three.com"
- />
- </iron-input>
+ <input
+ class="preferredRadio"
+ name="preferred"
+ type="radio"
+ value="email@three.com"
+ />
</td>
<td>
<gr-button
aria-disabled="false"
class="remove-button"
- data-index="2"
role="button"
tabindex="0"
>
@@ -239,6 +231,12 @@
assert.equal(element.emailsToRemove[0].email, 'email@one.com');
assert.equal(element.emails.length, 2);
+ accountEmailStub.restore();
+
+ accountEmailStub = stubRestApi('getAccountEmails').returns(
+ Promise.resolve(element.emails)
+ );
+
await element.save();
assert.equal(deleteEmailSpy.callCount, 1);
assert.equal(deleteEmailSpy.getCall(0).args[0], 'email@one.com');
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
index dfa1271..7626e14 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.ts
@@ -38,7 +38,7 @@
import {GrSshEditor} from '../gr-ssh-editor/gr-ssh-editor';
import {GrGpgEditor} from '../gr-gpg-editor/gr-gpg-editor';
import {GrEmailEditor} from '../gr-email-editor/gr-email-editor';
-import {fireAlert, fireTitleChange} from '../../../utils/event-util';
+import {fire, fireAlert, fireTitleChange} from '../../../utils/event-util';
import {getAppContext} from '../../../services/app-context';
import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
import {LitElement, css, html} from 'lit';
@@ -190,7 +190,7 @@
const message = await this.restApiService.confirmEmail(this.emailToken);
if (message) fireAlert(this, message);
this.getViewModel().clearToken();
- await this.emailEditor.loadData();
+ await this.getUserModel().loadEmails(true);
}
override connectedCallback() {
@@ -230,8 +230,6 @@
})
);
- promises.push(this.emailEditor.loadData());
-
this._testOnly_loadingPromise = Promise.all(promises).then(() => {
this.loading = false;
@@ -341,6 +339,9 @@
@unsaved-changes-changed=${(e: ValueChangedEvent<boolean>) => {
this.accountInfoChanged = e.detail.value;
}}
+ @account-detail-update=${() => {
+ fire(this, 'account-detail-update', {});
+ }}
></gr-account-info>
<gr-button
@click=${() => {
@@ -470,8 +471,8 @@
}}
></gr-email-editor>
<gr-button
- @click=${() => {
- this.emailEditor.save();
+ @click=${async () => {
+ await this.emailEditor.save();
}}
?disabled=${!this.emailsChanged}
>Save changes</gr-button
@@ -604,7 +605,7 @@
};
reloadAccountDetail() {
- Promise.all([this.accountInfo.loadData(), this.emailEditor.loadData()]);
+ Promise.all([this.accountInfo.loadData()]);
}
// private but used in test
@@ -642,7 +643,7 @@
if (!this.isNewEmailValid(this.newEmail)) return;
this.addingEmail = true;
- this.restApiService.addAccountEmail(this.newEmail).then(response => {
+ this.restApiService.addAccountEmail(this.newEmail).then(async response => {
this.addingEmail = false;
// If it was unsuccessful.
@@ -652,6 +653,8 @@
this.lastSentVerificationEmail = this.newEmail;
this.newEmail = '';
+
+ await this.getUserModel().loadEmails(true);
});
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
index b6690b6..74bcca9 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.ts
@@ -331,12 +331,6 @@
assert.isNotOk(element.lastSentVerificationEmail);
});
- test('emails are loaded without emailToken', () => {
- const emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
- element.firstUpdated();
- assert.isTrue(emailEditorLoadDataStub.calledOnce);
- });
-
test('handleSaveChangeTable', () => {
let newColumns = ['Owner', 'Project', 'Branch'];
element.localChangeTableColumns = newColumns.slice(0);
@@ -387,10 +381,8 @@
value: string | PromiseLike<string | null> | null
) => void;
let confirmEmailStub: sinon.SinonStub;
- let emailEditorLoadDataStub: sinon.SinonStub;
setup(() => {
- emailEditorLoadDataStub = sinon.stub(element.emailEditor, 'loadData');
confirmEmailStub = stubRestApi('confirmEmail').returns(
new Promise(resolve => {
resolveConfirm = resolve;
@@ -406,16 +398,6 @@
assert.isTrue(confirmEmailStub.calledWith('foo'));
});
- test('emails are not loaded initially', () => {
- assert.isFalse(emailEditorLoadDataStub.called);
- });
-
- test('user emails are loaded after email confirmed', async () => {
- resolveConfirm('bar');
- await element._testOnly_loadingPromise;
- assert.isTrue(emailEditorLoadDataStub.calledOnce);
- });
-
test('show-alert is fired when email is confirmed', async () => {
const dispatchEventSpy = sinon.spy(element, 'dispatchEvent');
resolveConfirm('bar');
diff --git a/polygerrit-ui/app/models/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
index cd6a66a..4973307 100644
--- a/polygerrit-ui/app/models/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {from, of, Observable} from 'rxjs';
-import {filter, switchMap} from 'rxjs/operators';
+import {filter, switchMap, tap} from 'rxjs/operators';
import {
DiffPreferencesInfo as DiffPreferencesInfoAPI,
DiffViewMode,
@@ -13,6 +13,7 @@
AccountCapabilityInfo,
AccountDetailInfo,
EditPreferencesInfo,
+ EmailInfo,
PreferencesInfo,
TopMenuItemInfo,
} from '../../types/common';
@@ -48,6 +49,7 @@
* `account` is known, then use `accountLoaded` below.
*/
account?: AccountDetailInfo;
+ emails?: EmailInfo[];
/**
* Starts as `false` and switches to `true` after the first `getAccount` call.
* A common use case for this is to wait with loading or doing something until
@@ -82,6 +84,15 @@
userState => userState.account
);
+ readonly emails$: Observable<EmailInfo[] | undefined> = select(
+ this.state$,
+ userState => userState.emails
+ ).pipe(
+ tap(emails => {
+ if (emails === undefined) this.loadEmails();
+ })
+ );
+
/**
* Only emits once we have tried to actually load the account. Note that
* this does not initially emit a value.
@@ -148,12 +159,8 @@
super({
accountLoaded: false,
});
+ this.loadAccount();
this.subscriptions = [
- from(this.restApiService.getAccount()).subscribe(
- (account?: AccountDetailInfo) => {
- this.setAccount(account);
- }
- ),
this.loadedAccount$
.pipe(
switchMap(account => {
@@ -261,4 +268,22 @@
setAccount(account?: AccountDetailInfo) {
this.updateState({account, accountLoaded: true});
}
+
+ private setAccountEmails(emails?: EmailInfo[]) {
+ this.updateState({emails});
+ }
+
+ loadAccount(noCache?: boolean) {
+ if (noCache) this.restApiService.invalidateAccountsDetailCache();
+ return this.restApiService.getAccount().then(account => {
+ this.setAccount(account);
+ });
+ }
+
+ loadEmails(noCache?: boolean) {
+ if (noCache) this.restApiService.invalidateAccountsEmailCache();
+ return this.restApiService.getAccountEmails().then(emails => {
+ this.setAccountEmails(emails);
+ });
+ }
}
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
index 0be3ab5..7a12e7a 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api-impl.ts
@@ -1565,6 +1565,10 @@
this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/self/detail');
}
+ invalidateAccountsEmailCache() {
+ this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/self/emails');
+ }
+
getGroups(filter: string, groupsPerPage: number, offset?: number) {
const url = this._getGroupsUrl(filter, groupsPerPage, offset);
diff --git a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
index 947952c..814e97a 100644
--- a/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
+++ b/polygerrit-ui/app/services/gr-rest-api/gr-rest-api.ts
@@ -542,6 +542,7 @@
invalidateReposCache(): void;
invalidateAccountsCache(): void;
invalidateAccountsDetailCache(): void;
+ invalidateAccountsEmailCache(): void;
removeFromAttentionSet(
changeNum: NumericChangeId,
user: AccountId,
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
index 77f2498..570b50a 100644
--- a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -442,6 +442,7 @@
invalidateGroupsCache(): void {},
invalidateReposCache(): void {},
invalidateAccountsDetailCache(): void {},
+ invalidateAccountsEmailCache(): void {},
probePath(): Promise<boolean> {
return Promise.resolve(true);
},