FEATURE: admin UI to merge two users. (#9509)

FEATURE: admin UI to merge two users. (#9509)

diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js b/app/assets/javascripts/admin/controllers/admin-user-index.js
index 7bd7abe..f69993c 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-index.js
+++ b/app/assets/javascripts/admin/controllers/admin-user-index.js
@@ -9,6 +9,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
 import discourseComputed from "discourse-common/utils/decorators";
 import { fmt } from "discourse/lib/computed";
 import { htmlSafe } from "@ember/template";
+import showModal from "discourse/lib/show-modal";
 
 export default Controller.extend(CanCheckEmails, {
   adminTools: service(),
@@ -207,6 +208,27 @@ export default Controller.extend(CanCheckEmails, {
       }
     },
 
+    promptTargetUser() {
+      showModal("admin-merge-users-prompt", {
+        admin: true,
+        model: this.model
+      });
+    },
+
+    showMergeConfirmation(targetUsername) {
+      showModal("admin-merge-users-confirmation", {
+        admin: true,
+        model: {
+          username: this.model.username,
+          targetUsername: targetUsername
+        }
+      });
+    },
+
+    merge(targetUsername) {
+      return this.model.merge({ targetUsername: targetUsername });
+    },
+
     viewActionLogs() {
       this.adminTools.showActionLogs(this, {
         target_user: this.get("model.username")
diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js
new file mode 100644
index 0000000..2195c29
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-confirmation.js
@@ -0,0 +1,35 @@
+import Controller, { inject as controller } from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+
+export default Controller.extend(ModalFunctionality, {
+  adminUserIndex: controller(),
+  username: alias("model.username"),
+  targetUsername: alias("model.targetUsername"),
+
+  onShow() {
+    this.set("value", null);
+  },
+
+  @discourseComputed("username", "targetUsername")
+  text(username, targetUsername) {
+    return `transfer @${username} to @${targetUsername}`;
+  },
+
+  @discourseComputed("value", "text")
+  mergeDisabled(value, text) {
+    return !value || text !== value;
+  },
+
+  actions: {
+    merge() {
+      this.adminUserIndex.send("merge", this.targetUsername);
+      this.send("closeModal");
+    },
+
+    cancel() {
+      this.send("closeModal");
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js
new file mode 100644
index 0000000..535870c
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/modals/admin-merge-users-prompt.js
@@ -0,0 +1,29 @@
+import Controller, { inject as controller } from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import discourseComputed from "discourse-common/utils/decorators";
+import { alias } from "@ember/object/computed";
+
+export default Controller.extend(ModalFunctionality, {
+  adminUserIndex: controller(),
+  username: alias("model.username"),
+
+  onShow() {
+    this.set("targetUsername", null);
+  },
+
+  @discourseComputed("username", "targetUsername")
+  mergeDisabled(username, targetUsername) {
+    return !targetUsername || username === targetUsername;
+  },
+
+  actions: {
+    merge() {
+      this.send("closeModal");
+      this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
+    },
+
+    cancel() {
+      this.send("closeModal");
+    }
+  }
+});
diff --git a/app/assets/javascripts/admin/models/admin-user.js b/app/assets/javascripts/admin/models/admin-user.js
index 25a822f..454daf4 100644
--- a/app/assets/javascripts/admin/models/admin-user.js
+++ b/app/assets/javascripts/admin/models/admin-user.js
@@ -503,6 +503,43 @@ const AdminUser = User.extend({
     bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
   },
 
+  merge(opts) {
+    const user = this;
+    const location = document.location.pathname;
+
+    bootbox.dialog(I18n.t("admin.user.merging_user"));
+    let formData = { context: location };
+
+    if (opts && opts.targetUsername) {
+      formData["target_username"] = opts.targetUsername;
+    }
+
+    return ajax(`/admin/users/${user.get("id")}/merge.json`, {
+      type: "POST",
+      data: formData
+    })
+      .then(function(data) {
+        if (data.merged) {
+          if (/^\/admin\/users\/list\//.test(location)) {
+            document.location = location;
+          } else {
+            document.location = Discourse.getURL(
+              `/admin/users/${data.user.id}/${data.user.username}`
+            );
+          }
+        } else {
+          bootbox.alert(I18n.t("admin.user.merge_failed"));
+          if (data.user) {
+            user.setProperties(data.user);
+          }
+        }
+      })
+      .catch(function() {
+        AdminUser.find(user.get("id")).then(u => user.setProperties(u));
+        bootbox.alert(I18n.t("admin.user.merge_failed"));
+      });
+  },
+
   loadDetails() {
     if (this.loadedDetails) {
       return Promise.resolve(this);
diff --git a/app/assets/javascripts/admin/templates/modal/admin-merge-users-confirmation.hbs b/app/assets/javascripts/admin/templates/modal/admin-merge-users-confirmation.hbs
new file mode 100644
index 0000000..119cd3a
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/admin-merge-users-confirmation.hbs
@@ -0,0 +1,21 @@
+<div>
+  {{#d-modal-body rawTitle=(i18n "admin.user.merge.confirmation.title" username=username)}}
+    <p>{{html-safe (i18n "admin.user.merge.confirmation.description" username=username targetUsername=targetUsername text=text)}}</p>
+    {{input type="text" value=value}}
+  {{/d-modal-body}}
+
+  <div class="modal-footer">
+    {{#d-button
+      class="btn-danger"
+      action=(action "merge")
+      icon="trash-alt"
+      disabled=mergeDisabled
+    }}
+      {{i18n "admin.user.merge.confirmation.transfer_and_delete" username=username}}
+    {{/d-button}}
+    {{d-button
+      action=(action "cancel")
+      label="admin.user.merge.confirmation.cancel"
+    }}
+  </div>
+</div>
diff --git a/app/assets/javascripts/admin/templates/modal/admin-merge-users-prompt.hbs b/app/assets/javascripts/admin/templates/modal/admin-merge-users-prompt.hbs
new file mode 100644
index 0000000..69bdd69
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/admin-merge-users-prompt.hbs
@@ -0,0 +1,24 @@
+<div>
+  {{#d-modal-body rawTitle=(i18n "admin.user.merge.prompt.title" username=username)}}
+    <p>{{html-safe (i18n "admin.user.merge.prompt.description" username=username)}}</p>
+    {{user-selector single=true
+                    placeholderKey="admin.user.merge.prompt.target_username_placeholder"
+                    usernames=targetUsername
+                    autocomplete="discourse"}}
+  {{/d-modal-body}}
+
+  <div class="modal-footer">
+    {{#d-button
+      class="btn-primary"
+      action=(action "merge")
+      icon="trash-alt"
+      disabled=mergeDisabled
+    }}
+      {{i18n "admin.user.merge.prompt.transfer_and_delete" username=username}}
+    {{/d-button}}
+    {{d-button
+      action=(action "cancel")
+      label="admin.user.merge.prompt.cancel"
+    }}
+  </div>
+</div>
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs
index c23c88a..76aee47 100644
--- a/app/assets/javascripts/admin/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/templates/user-index.hbs
@@ -680,9 +680,13 @@
 
     {{#if model.canBeDeleted}}
       {{d-button label="admin.user.delete"

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

GitHub sha: a511bea4

This commit appears in #9509 which was merged by vinothkannans.

please use @action

can you try to have more meaningful names for merge and cancel please ?

this should be localized no ?

use @action please

we have DiscourseURL.redirectTo for this

I dont think you need function here

I don’t think you need function here

do we need get here ?

do we need get here ?

return this.model.merge({ targetUsername });

maybe we should have a test to ensure we are not merging into staff ?

It’s already done in “minor code improvement · discourse/discourse@ceee855 · GitHub”.

I thought we shouldn’t localize since these are the words to type 🤦. Yes, we should.

I think merging into staff is not an issue. Why a staff shouldn’t be “target_user”?

We should raise a 404 if target_username is invalid

Can the target_user be a staff?

Also this can be simplified into

is_admin? && !source_user&.staff? && !target_user&.staff?

I would probably test for the guardian as well since the guard lies in the controller action.

I’m curious why we use AdminDetailedUserSerializer here but not for the success_json above?