gr-email-editor: improve unsaved changes detection

Release-Notes: gr-email-editor: improve unsaved changes detection
Change-Id: I87b35b5ca7d4e50037ff9e2d6005db7a9ee18b88
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 9c99ae0..634b17a 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
@@ -8,15 +8,16 @@
 import {EmailInfo} from '../../../types/common';
 import {getAppContext} from '../../../services/app-context';
 import {LitElement, css, html} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
+import {customElement, state} from 'lit/decorators.js';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {grFormStyles} from '../../../styles/gr-form-styles';
 import {ValueChangedEvent} from '../../../types/events';
 import {fire} from '../../../utils/event-util';
+import {notDeepEqual} from '../../../utils/deep-util';
 
 @customElement('gr-email-editor')
 export class GrEmailEditor extends LitElement {
-  @property({type: Boolean}) hasUnsavedChanges = false;
+  @state() private originalEmails: EmailInfo[] = [];
 
   /* private but used in test */
   @state() emails: EmailInfo[] = [];
@@ -110,7 +111,8 @@
 
   loadData() {
     return this.restApiService.getAccountEmails().then(emails => {
-      this.emails = emails ?? [];
+      this.originalEmails = emails ?? [];
+      this.emails = emails ? [...emails] : [];
     });
   }
 
@@ -128,9 +130,10 @@
     }
 
     return Promise.all(promises).then(() => {
+      this.originalEmails = this.emails;
       this.emailsToRemove = [];
       this.newPreferred = '';
-      this.setHasUnsavedChanges(false);
+      this.setHasUnsavedChanges();
     });
   }
 
@@ -141,10 +144,12 @@
     if (indexStr === null) return;
     const index = Number(indexStr);
     const email = this.emails[index];
-    this.emailsToRemove = [...this.emailsToRemove, email];
+    // Don't add project to emailsToRemove if it wasn't in
+    // originalEmails.
+    if (this.originalEmails.includes(email)) this.emailsToRemove.push(email);
     this.emails.splice(index, 1);
     this.requestUpdate();
-    this.setHasUnsavedChanges(true);
+    this.setHasUnsavedChanges();
   }
 
   private handlePreferredControlClick(e: Event) {
@@ -165,7 +170,7 @@
         this.emails[i].preferred = true;
         this.requestUpdate();
         this.newPreferred = preferred;
-        this.setHasUnsavedChanges(true);
+        this.setHasUnsavedChanges();
       } else if (this.emails[i].preferred) {
         this.emails[i].preferred = false;
         this.requestUpdate();
@@ -177,9 +182,11 @@
     return preferred ?? false;
   }
 
-  private setHasUnsavedChanges(value: boolean) {
-    this.hasUnsavedChanges = value;
-    fire(this, 'has-unsaved-changes-changed', {value});
+  private setHasUnsavedChanges() {
+    const hasUnsavedChanges =
+      notDeepEqual(this.originalEmails, this.emails) ||
+      this.emailsToRemove.length > 0;
+    fire(this, 'has-unsaved-changes-changed', {value: hasUnsavedChanges});
   }
 }
 
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 25c9b97..39c3288 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
@@ -123,6 +123,12 @@
   });
 
   test('renders', () => {
+    const hasUnsavedChangesSpy = sinon.spy();
+    element.addEventListener(
+      'has-unsaved-changes-changed',
+      hasUnsavedChangesSpy
+    );
+
     const rows = element
       .shadowRoot!.querySelector('table')!
       .querySelectorAll('tbody tr');
@@ -144,15 +150,21 @@
     );
     assert.isNotOk(rows[2].querySelector('gr-button')!.disabled);
 
-    assert.isFalse(element.hasUnsavedChanges);
+    assert.isFalse(hasUnsavedChangesSpy.called);
   });
 
   test('edit preferred', () => {
+    const hasUnsavedChangesSpy = sinon.spy();
+    element.addEventListener(
+      'has-unsaved-changes-changed',
+      hasUnsavedChangesSpy
+    );
+
     const radios = element
       .shadowRoot!.querySelector('table')!
       .querySelectorAll<HTMLInputElement>('input[type=radio]');
 
-    assert.isFalse(element.hasUnsavedChanges);
+    assert.isFalse(hasUnsavedChangesSpy.called);
     assert.isNotOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 0);
     assert.equal(element.emails.length, 3);
@@ -162,7 +174,7 @@
 
     radios[0].click();
 
-    assert.isTrue(element.hasUnsavedChanges);
+    assert.isTrue(hasUnsavedChangesSpy.called);
     assert.isOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 0);
     assert.equal(element.emails.length, 3);
@@ -172,18 +184,24 @@
   });
 
   test('delete email', () => {
+    const hasUnsavedChangesSpy = sinon.spy();
+    element.addEventListener(
+      'has-unsaved-changes-changed',
+      hasUnsavedChangesSpy
+    );
+
     const buttons = element
       .shadowRoot!.querySelector('table')!
       .querySelectorAll('gr-button');
 
-    assert.isFalse(element.hasUnsavedChanges);
+    assert.isFalse(hasUnsavedChangesSpy.called);
     assert.isNotOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 0);
     assert.equal(element.emails.length, 3);
 
     buttons[2].click();
 
-    assert.isTrue(element.hasUnsavedChanges);
+    assert.isTrue(hasUnsavedChangesSpy.called);
     assert.isNotOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 1);
     assert.equal(element.emails.length, 2);
@@ -192,6 +210,12 @@
   });
 
   test('save changes', async () => {
+    const hasUnsavedChangesSpy = sinon.spy();
+    element.addEventListener(
+      'has-unsaved-changes-changed',
+      hasUnsavedChangesSpy
+    );
+
     const deleteEmailSpy = spyRestApi('deleteAccountEmail');
     const setPreferredSpy = spyRestApi('setPreferredAccountEmail');
 
@@ -199,7 +223,7 @@
       .shadowRoot!.querySelector('table')!
       .querySelectorAll('tbody tr');
 
-    assert.isFalse(element.hasUnsavedChanges);
+    assert.isFalse(hasUnsavedChangesSpy.called);
     assert.isNotOk(element.newPreferred);
     assert.equal(element.emailsToRemove.length, 0);
     assert.equal(element.emails.length, 3);
@@ -208,7 +232,8 @@
     rows[0].querySelector('gr-button')!.click();
     rows[2].querySelector<HTMLInputElement>('input[type=radio]')!.click();
 
-    assert.isTrue(element.hasUnsavedChanges);
+    assert.isTrue(hasUnsavedChangesSpy.called);
+    assert.isTrue(hasUnsavedChangesSpy.lastCall.args[0].detail.value);
     assert.equal(element.newPreferred, 'email@three.com');
     assert.equal(element.emailsToRemove.length, 1);
     assert.equal(element.emailsToRemove[0].email, 'email@one.com');
@@ -219,5 +244,6 @@
     assert.equal(deleteEmailSpy.getCall(0).args[0], 'email@one.com');
     assert.isTrue(setPreferredSpy.called);
     assert.equal(setPreferredSpy.getCall(0).args[0], 'email@three.com');
+    assert.isFalse(hasUnsavedChangesSpy.lastCall.args[0].detail.value);
   });
 });
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 7ad0bef..e1ce5cc 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
@@ -462,7 +462,6 @@
           <fieldset id="email">
             <gr-email-editor
               id="emailEditor"
-              ?hasUnsavedChanges=${this.emailsChanged}
               @has-unsaved-changes-changed=${(
                 e: ValueChangedEvent<boolean>
               ) => {