FEATURE: multiple use invite links (#9813)

FEATURE: multiple use invite links (#9813)

diff --git a/app/assets/javascripts/discourse/app/components/invite-link-panel.js b/app/assets/javascripts/discourse/app/components/invite-link-panel.js
new file mode 100644
index 0000000..e2691e3
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/invite-link-panel.js
@@ -0,0 +1,98 @@
+import I18n from "I18n";
+import Component from "@ember/component";
+import Group from "discourse/models/group";
+import { readOnly } from "@ember/object/computed";
+import { action } from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
+import Invite from "discourse/models/invite";
+
+export default Component.extend({
+  inviteModel: readOnly("panel.model.inviteModel"),
+  userInvitedShow: readOnly("panel.model.userInvitedShow"),
+  isStaff: readOnly("currentUser.staff"),
+  maxRedemptionAllowed: 5,
+  inviteExpiresAt: moment()
+    .add(1, "month")
+    .format("YYYY-MM-DD"),
+
+  willDestroyElement() {
+    this._super(...arguments);
+
+    this.reset();
+  },
+
+  @discourseComputed("isStaff", "inviteModel.saving", "maxRedemptionAllowed")
+  disabled(isStaff, saving, canInviteTo, maxRedemptionAllowed) {
+    if (saving) return true;
+    if (!isStaff) return true;
+    if (maxRedemptionAllowed < 2) return true;
+
+    return false;
+  },
+
+  groupFinder(term) {
+    return Group.findAll({ term, ignore_automatic: true });
+  },
+
+  errorMessage: I18n.t("user.invited.invite_link.error"),
+
+  reset() {
+    this.set("maxRedemptionAllowed", 5);
+
+    this.inviteModel.setProperties({
+      groupNames: null,
+      error: false,
+      saving: false,
+      finished: false,
+      inviteLink: null
+    });
+  },
+
+  @action
+  generateMultipleUseInviteLink() {
+    if (this.disabled) {
+      return;
+    }
+
+    const groupNames = this.get("inviteModel.groupNames");
+    const maxRedemptionAllowed = this.maxRedemptionAllowed;
+    const inviteExpiresAt = this.inviteExpiresAt;
+    const userInvitedController = this.userInvitedShow;
+    const model = this.inviteModel;
+    model.setProperties({ saving: true, error: false });
+
+    return model
+      .generateMultipleUseInviteLink(
+        groupNames,
+        maxRedemptionAllowed,
+        inviteExpiresAt
+      )
+      .then(result => {
+        model.setProperties({
+          saving: false,
+          finished: true,
+          inviteLink: result
+        });
+
+        if (userInvitedController) {
+          Invite.findInvitedBy(
+            this.currentUser,
+            userInvitedController.filter
+          ).then(inviteModel => {
+            userInvitedController.setProperties({
+              model: inviteModel,
+              totalInvites: inviteModel.invites.length
+            });
+          });
+        }
+      })
+      .catch(e => {
+        if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
+          this.set("errorMessage", e.jqXHR.responseJSON.errors[0]);
+        } else {
+          this.set("errorMessage", I18n.t("user.invited.invite_link.error"));
+        }
+        model.setProperties({ saving: false, error: true });
+      });
+  }
+});
diff --git a/app/assets/javascripts/discourse/app/controllers/invites-show.js b/app/assets/javascripts/discourse/app/controllers/invites-show.js
index b421b77..a38ec6e 100644
--- a/app/assets/javascripts/discourse/app/controllers/invites-show.js
+++ b/app/assets/javascripts/discourse/app/controllers/invites-show.js
@@ -1,16 +1,18 @@
 import I18n from "I18n";
 import { isEmpty } from "@ember/utils";
-import { alias, notEmpty } from "@ember/object/computed";
+import { alias, notEmpty, or, readOnly } from "@ember/object/computed";
 import Controller from "@ember/controller";
 import discourseComputed from "discourse-common/utils/decorators";
 import getUrl from "discourse-common/lib/get-url";
 import DiscourseURL from "discourse/lib/url";
 import { ajax } from "discourse/lib/ajax";
+import { emailValid } from "discourse/lib/utilities";
 import PasswordValidation from "discourse/mixins/password-validation";
 import UsernameValidation from "discourse/mixins/username-validation";
 import NameValidation from "discourse/mixins/name-validation";
 import UserFieldsValidation from "discourse/mixins/user-fields-validation";
 import { findAll as findLoginMethods } from "discourse/models/login-method";
+import EmberObject from "@ember/object";
 
 export default Controller.extend(
   PasswordValidation,
@@ -18,7 +20,7 @@ export default Controller.extend(
   NameValidation,
   UserFieldsValidation,
   {
-    invitedBy: alias("model.invited_by"),
+    invitedBy: readOnly("model.invited_by"),
     email: alias("model.email"),
     accountUsername: alias("model.username"),
     passwordRequired: notEmpty("accountPassword"),
@@ -26,6 +28,21 @@ export default Controller.extend(
     errorMessage: null,
     userFields: null,
     inviteImageUrl: getUrl("/images/envelope.svg"),
+    isInviteLink: readOnly("model.is_invite_link"),
+    submitDisabled: or(
+      "emailValidation.failed",
+      "usernameValidation.failed",
+      "passwordValidation.failed",
+      "nameValidation.failed",
+      "userFieldsValidation.failed"
+    ),
+    rejectedEmails: null,
+
+    init() {
+      this._super(...arguments);
+
+      this.rejectedEmails = [];
+    },
 
     @discourseComputed
     welcomeTitle() {
@@ -44,21 +61,6 @@ export default Controller.extend(
       return findLoginMethods().length > 0;
     },
 
-    @discourseComputed(
-      "usernameValidation.failed",
-      "passwordValidation.failed",
-      "nameValidation.failed",
-      "userFieldsValidation.failed"
-    )
-    submitDisabled(
-      usernameFailed,
-      passwordFailed,
-      nameFailed,
-      userFieldsFailed
-    ) {
-      return usernameFailed || passwordFailed || nameFailed || userFieldsFailed;
-    },
-
     @discourseComputed
     fullnameRequired() {
       return (
@@ -66,6 +68,35 @@ export default Controller.extend(
       );
     },
 
+    @discourseComputed("email", "rejectedEmails.[]")
+    emailValidation(email, rejectedEmails) {
+      // If blank, fail without a reason
+      if (isEmpty(email)) {
+        return EmberObject.create({
+          failed: true
+        });
+      }
+
+      if (rejectedEmails.includes(email)) {
+        return EmberObject.create({
+          failed: true,
+          reason: I18n.t("user.email.invalid")
+        });
+      }
+
+      if (emailValid(email)) {
+        return EmberObject.create({
+          ok: true,
+          reason: I18n.t("user.email.ok")
+        });
+      }
+
+      return EmberObject.create({
+        failed: true,
+        reason: I18n.t("user.email.invalid")
+      });
+    },
+
     actions: {
       submit() {
         const userFields = this.userFields;
@@ -80,6 +111,7 @@ export default Controller.extend(
           url: `/invites/show/${this.get("model.token")}.json`,
           type: "PUT",
           data: {
+            email: this.email,
             username: this.accountUsername,
             name: this.accountName,
             password: this.accountPassword,
@@ -99,6 +131,14 @@ export default Controller.extend(
             } else {
               if (
                 result.errors &&
+                result.errors.email &&
+                result.errors.email.length > 0 &&
+                result.values
+              ) {
+                this.rejectedEmails.pushObject(result.values.email);
+              }
+              if (
+                result.errors &&
                 result.errors.password &&
                 result.errors.password.length > 0
               ) {
diff --git a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js
index eb3a42f..7e8f67e 100644
--- a/app/assets/javascripts/discourse/app/controllers/user-invited-show.js
+++ b/app/assets/javascripts/discourse/app/controllers/user-invited-show.js
@@ -1,5 +1,5 @@
 import I18n from "I18n";

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

GitHub sha: 3094459c

This commit appears in #9813 which was approved by davidtaylorhq. It was merged by techAPJ.

https://meta.discourse.org/t/multiple-use-invite-links/154325

This commit has been mentioned on Discourse Meta. There might be relevant details there:

https://meta.discourse.org/t/cosmetic-problem-with-invites-screen-in-brave/153613/5