Skip to content

Commit

Permalink
FEATURE: Various improvements to invite system (#12023)
Browse files Browse the repository at this point in the history
The user interface has been reorganized to show email and link invites
in the same screen. Staff has more control over creating and updating
invites. Bulk invite has also been improved with better explanations.

On the server side, many code paths for email and link invites have
been merged to avoid duplicated logic. The API returns better responses
with more appropriate HTTP status codes.
  • Loading branch information
udan11 committed Mar 3, 2021
1 parent 039d0d3 commit c047640
Show file tree
Hide file tree
Showing 37 changed files with 1,265 additions and 1,061 deletions.
16 changes: 16 additions & 0 deletions app/assets/javascripts/discourse/app/components/copy-button.js
@@ -0,0 +1,16 @@
import Component from "@ember/component";
import { action } from "@ember/object";

export default Component.extend({
tagName: "",

@action
copy() {
const target = document.querySelector(this.selector);
target.select();
target.setSelectionRange(0, target.value.length);
try {
document.execCommand("copy");
} catch (err) {}
},
});
@@ -0,0 +1,111 @@
import Component from "@ember/component";
import getUrl from "discourse-common/lib/get-url";
import discourseComputed from "discourse-common/utils/decorators";
import {
displayErrorForUpload,
validateUploadedFiles,
} from "discourse/lib/uploads";

export default Component.extend({
tagName: "",

data: null,
uploading: false,
progress: 0,
uploaded: null,

@discourseComputed("messageBus.clientId")
clientId() {
return this.messageBus && this.messageBus.clientId;
},

@discourseComputed("data", "uploading")
submitDisabled(data, uploading) {
return !data || uploading;
},

didInsertElement() {
this._super(...arguments);

this.setProperties({
data: null,
uploading: false,
progress: 0,
uploaded: null,
});

const $upload = $("#csv-file");

$upload.fileupload({
url: getUrl("/invites/upload_csv.json") + "?client_id=" + this.clientId,
dataType: "json",
dropZone: null,
replaceFileInput: false,
autoUpload: false,
});

$upload.on("fileuploadadd", (e, data) => {
this.set("data", data);
});

$upload.on("fileuploadsubmit", (e, data) => {
const isValid = validateUploadedFiles(data.files, {
user: this.currentUser,
siteSettings: this.siteSettings,
bypassNewUserRestriction: true,
csvOnly: true,
});

data.formData = { type: "csv" };
this.setProperties({ progress: 0, uploading: isValid });

return isValid;
});

$upload.on("fileuploadprogress", (e, data) => {
const progress = parseInt((data.loaded / data.total) * 100, 10);
this.set("progress", progress);
});

$upload.on("fileuploaddone", (e, data) => {
const upload = data.result;
this.set("uploaded", upload);
this.reset();
});

$upload.on("fileuploadfail", (e, data) => {
if (data.errorThrown !== "abort") {
displayErrorForUpload(data, this.siteSettings);
}
this.reset();
});
},

willDestroyElement() {
this._super(...arguments);

if (this.messageBus) {
this.messageBus.unsubscribe("/uploads/csv");
}

const $upload = $(this.element);

try {
$upload.fileupload("destroy");
} catch (e) {
/* wasn't initialized yet */
} finally {
$upload.off();
}
},

reset() {
this.setProperties({
data: null,
uploading: false,
progress: 0,
});

document.getElementById("csv-file").value = "";
},
});
49 changes: 0 additions & 49 deletions app/assets/javascripts/discourse/app/components/csv-uploader.js

This file was deleted.

@@ -0,0 +1,24 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";

export default Controller.extend(ModalFunctionality, {
data: null,

onShow() {
this.set("data", null);
},

onClose() {
if (this.data) {
this.data.abort();
this.set("data", null);
}
},

@action
submit(data) {
this.set("data", data);
data.submit();
},
});
144 changes: 144 additions & 0 deletions app/assets/javascripts/discourse/app/controllers/create-invite.js
@@ -0,0 +1,144 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import Group from "discourse/models/group";
import Invite from "discourse/models/invite";
import I18n from "I18n";

export default Controller.extend(
ModalFunctionality,
bufferedProperty("invite"),
{
allGroups: null,

invite: null,
invites: null,

autogenerated: false,
showAdvanced: false,
showOnly: false,

type: "link",

topicId: null,
topicTitle: null,
groupIds: null,

onShow() {
Group.findAll().then((groups) => {
this.set("allGroups", groups.filterBy("automatic", false));
});

this.setProperties({
autogenerated: false,
showAdvanced: false,
showOnly: false,
});

this.setInvite(Invite.create());
},

onClose() {
if (this.autogenerated) {
this.invite
.destroy()
.then(() => this.invites.removeObject(this.invite));
}
},

setInvite(invite) {
this.setProperties({
invite,
type: invite.email ? "email" : "link",
groupIds: invite.groups ? invite.groups.map((g) => g.id) : null,
});

if (invite.topics && invite.topics.length > 0) {
this.setProperties({
topicId: invite.topics[0].id,
topicTitle: invite.topics[0].title,
});
} else {
this.setProperties({ topicId: null, topicTitle: null });
}
},

save(autogenerated) {
this.set("autogenerated", autogenerated);

const data = {
group_ids: this.groupIds,
topic_id: this.topicId,
expires_at: this.buffered.get("expires_at"),
};

if (this.type === "link") {
data.max_redemptions_allowed = this.buffered.get(
"max_redemptions_allowed"
);
} else if (this.type === "email") {
data.email = this.buffered.get("email");
data.custom_message = this.buffered.get("custom_message");
}

const newRecord = !this.invite.id;
return this.invite
.save(data)
.then(() => {
this.rollbackBuffer();

if (newRecord) {
this.invites.unshiftObject(this.invite);
}

if (!this.autogenerated) {
this.appEvents.trigger("modal-body:flash", {
text: I18n.t("user.invited.invite.invite_saved"),
messageClass: "success",
});
}
})
.catch((e) =>
this.appEvents.trigger("modal-body:flash", {
text: extractError(e),
messageClass: "error",
})
);
},

isLink: equal("type", "link"),
isEmail: equal("type", "email"),

@discourseComputed("buffered.expires_at")
expiresAtRelative(expires_at) {
return moment.duration(moment(expires_at) - moment()).humanize();
},

@discourseComputed("type", "buffered.email")
disabled(type, email) {
if (type === "email") {
return !email;
}

return false;
},

@discourseComputed("type", "invite.email", "buffered.email")
saveLabel(type, email, bufferedEmail) {
return type === "email" && email !== bufferedEmail
? "user.invited.invite.send_invite_email"
: "user.invited.invite.save_invite";
},

@action
saveInvite() {
this.appEvents.trigger("modal-body:clearFlash");

this.save();
},
}
);

1 comment on commit c047640

@raphael-attie
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new invite system may have an unexpected behavior that created a security issue with invite links sent since we updated. I have reported it here: https://meta.discourse.org/t/confused-by-save-invite-button-in-2-7-0-beta4/181958/3
It would be great if someone could check this out.

Please sign in to comment.