FEATURE: Improve UX support for multiple email addresses (#9691)

FEATURE: Improve UX support for multiple email addresses (#9691)

diff --git a/app/assets/javascripts/discourse/app/components/email-dropdown.js b/app/assets/javascripts/discourse/app/components/email-dropdown.js
new file mode 100644
index 0000000..1a4046b
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/email-dropdown.js
@@ -0,0 +1,63 @@
+import { action, computed } from "@ember/object";
+import { inject as service } from "@ember/service";
+import I18n from "I18n";
+import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
+
+export default DropdownSelectBoxComponent.extend({
+  router: service(),
+
+  classNames: ["email-dropdown"],
+
+  selectKitOptions: {
+    icon: "wrench",
+    showFullTitle: false
+  },
+
+  content: computed("email", function() {
+    const content = [];
+
+    if (this.email.primary) {
+      content.push({
+        id: "updateEmail",
+        icon: "pencil-alt",
+        name: I18n.t("user.email.update_email"),
+        description: ""
+      });
+    }
+
+    if (!this.email.primary && this.email.confirmed) {
+      content.push({
+        id: "setPrimaryEmail",
+        icon: "star",
+        name: I18n.t("user.email.set_primary"),
+        description: ""
+      });
+    }
+
+    if (!this.email.primary) {
+      content.push({
+        id: "destroyEmail",
+        icon: "times",
+        name: I18n.t("user.email.destroy"),
+        description: ""
+      });
+    }
+
+    return content;
+  }),
+
+  @action
+  onChange(id) {
+    switch (id) {
+      case "updateEmail":
+        this.router.transitionTo("preferences.email");
+        break;
+      case "setPrimaryEmail":
+        this.setPrimaryEmail(this.email.email);
+        break;
+      case "destroyEmail":
+        this.destroyEmail(this.email.email);
+        break;
+    }
+  }
+});
diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/account.js b/app/assets/javascripts/discourse/app/controllers/preferences/account.js
index 0a6f54c..6eecf3e 100644
--- a/app/assets/javascripts/discourse/app/controllers/preferences/account.js
+++ b/app/assets/javascripts/discourse/app/controllers/preferences/account.js
@@ -12,6 +12,7 @@ import { findAll } from "discourse/models/login-method";
 import { ajax } from "discourse/lib/ajax";
 import { userPath } from "discourse/lib/url";
 import logout from "discourse/lib/logout";
+import EmberObject from "@ember/object";
 
 // Number of tokens shown by default.
 const DEFAULT_AUTH_TOKENS_COUNT = 2;
@@ -96,6 +97,39 @@ export default Controller.extend(CanCheckEmails, {
   disableConnectButtons: propertyNotEqual("model.id", "currentUser.id"),
 
   @discourseComputed(
+    "model.email",
+    "model.secondary_emails.[]",
+    "model.unconfirmed_emails.[]"
+  )
+  emails(primaryEmail, secondaryEmails, unconfirmedEmails) {
+    const emails = [];
+
+    if (primaryEmail) {
+      emails.push(
+        EmberObject.create({
+          email: primaryEmail,
+          primary: true,
+          confirmed: true
+        })
+      );
+    }
+
+    if (secondaryEmails) {
+      secondaryEmails.forEach(email => {
+        emails.push(EmberObject.create({ email, confirmed: true }));
+      });
+    }
+
+    if (unconfirmedEmails) {
+      unconfirmedEmails.forEach(email => {
+        emails.push(EmberObject.create({ email }));
+      });
+    }
+
+    return emails.sort((a, b) => a.email.localeCompare(b.email));
+  },
+
+  @discourseComputed(
     "model.second_factor_enabled",
     "canCheckEmails",
     "model.is_anonymous"
@@ -149,6 +183,26 @@ export default Controller.extend(CanCheckEmails, {
         .catch(popupAjaxError);
     },
 
+    setPrimaryEmail(email) {
+      this.model.setPrimaryEmail(email).catch(popupAjaxError);
+    },
+
+    destroyEmail(email) {
+      this.model.destroyEmail(email);
+    },
+
+    resendConfirmationEmail(email) {
+      email.set("resending", true);
+      this.model
+        .addEmail(email.email)
+        .then(() => {
+          email.set("resent", true);
+        })
+        .finally(() => {
+          email.set("resending", false);
+        });
+    },
+
     changePassword() {
       if (!this.passwordProgress) {
         this.set(
diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/email.js b/app/assets/javascripts/discourse/app/controllers/preferences/email.js
index f79e4d1..3365faf 100644
--- a/app/assets/javascripts/discourse/app/controllers/preferences/email.js
+++ b/app/assets/javascripts/discourse/app/controllers/preferences/email.js
@@ -7,10 +7,13 @@ import EmberObject from "@ember/object";
 import { emailValid } from "discourse/lib/utilities";
 
 export default Controller.extend({
+  queryParams: ["new"],
+
   taken: false,
   saving: false,
   error: false,
   success: false,
+  oldEmail: null,
   newEmail: null,
 
   newEmailEmpty: empty("newEmail"),
@@ -23,16 +26,17 @@ export default Controller.extend({
     "invalidEmail"
   ),
 
-  unchanged: propertyEqual("newEmailLower", "currentUser.email"),
+  unchanged: propertyEqual("newEmailLower", "oldEmail"),
 
   @discourseComputed("newEmail")
   newEmailLower(newEmail) {
     return newEmail.toLowerCase().trim();
   },
 
-  @discourseComputed("saving")
-  saveButtonText(saving) {
+  @discourseComputed("saving", "new")
+  saveButtonText(saving, isNew) {
     if (saving) return I18n.t("saving");
+    if (isNew) return I18n.t("user.add_email.add");
     return I18n.t("user.change");
   },
 
@@ -41,9 +45,9 @@ export default Controller.extend({
     return !emailValid(newEmail);
   },
 
-  @discourseComputed("invalidEmail")
-  emailValidation(invalidEmail) {
-    if (invalidEmail) {
+  @discourseComputed("invalidEmail", "oldEmail", "newEmail")
+  emailValidation(invalidEmail, oldEmail, newEmail) {
+    if (invalidEmail && (oldEmail || newEmail)) {
       return EmberObject.create({
         failed: true,
         reason: I18n.t("user.email.invalid")
@@ -62,10 +66,13 @@ export default Controller.extend({
   },
 
   actions: {
-    changeEmail() {
+    saveEmail() {
       this.set("saving", true);
 
-      return this.model.changeEmail(this.newEmail).then(
+      return (this.new
+        ? this.model.addEmail(this.newEmail)
+        : this.model.changeEmail(this.newEmail)
+      ).then(
         () => this.set("success", true),
         e => {
           this.setProperties({ error: true, saving: false });
diff --git a/app/assets/javascripts/discourse/app/models/user.js b/app/assets/javascripts/discourse/app/models/user.js
index 113b592..0f67d61 100644
--- a/app/assets/javascripts/discourse/app/models/user.js
+++ b/app/assets/javascripts/discourse/app/models/user.js
@@ -246,6 +246,13 @@ const User = RestModel.extend({
     });
   },
 
+  addEmail(email) {
+    return ajax(userPath(`${this.username_lower}/preferences/email`), {
+      type: "POST",
+      data: { email }
+    });
+  },
+
   changeEmail(email) {
     return ajax(userPath(`${this.username_lower}/preferences/email`), {
       type: "PUT",
@@ -375,6 +382,27 @@ const User = RestModel.extend({
       });
   },
 
+  setPrimaryEmail(email) {
+    return ajax(userPath(`${this.username}/preferences/primary-email.json`), {
+      type: "PUT",
+      data: { email }
+    }).then(() => {
+      this.secondary_emails.removeObject(email);
+      this.secondary_emails.pushObject(this.email);
+      this.set("email", email);
+    });
+  },
+
+  destroyEmail(email) {
+    return ajax(userPath(`${this.username}/preferences/email.json`), {
+      type: "DELETE",
+      data: { email }
+    }).then(() => {
+      this.secondary_emails.removeObject(email);
+      this.unconfirmed_emails.removeObject(email);
+    });
+  },
+
   changePassword() {
     return ajax("/session/forgot_password", {
       dataType: "json",
diff --git a/app/assets/javascripts/discourse/app/routes/preferences-email.js b/app/assets/javascripts/discourse/app/routes/preferences-email.js
index c3fb268..f2d070a 100644
--- a/app/assets/javascripts/discourse/app/routes/preferences-email.js

[... diff too long, it was truncated ...]

GitHub sha: 5bfe1ee4

This commit appears in #9691 which was approved by ZogStriP and ZogStriP. It was merged by udan11.