FEATURE: Various improvements to invite system (#12314)

* FEATURE: Do not delete invite if link was copied

* FIX: Show error to user if invite redeeming fails

The error was only displayed to console.

* UX: Better placement of bulk buttons

Destroy all expired invites should be on the expired tab, not pending.

* FIX: Ensure invited_groups is unique per invite and group

* FIX: Do not refresh topic list if title unchanged

* FIX: Do not close modal on enter

This intereferes with the group and topic chooser.
Wrapping everything in a form disables this behavior.

* FIX: Move link and email options outside advanced section

* FIX: Do not close modal if saving a link invite

User may still want to copy the link.
This commit is contained in:
Dan Ungureanu 2021-03-09 00:15:14 +02:00 committed by GitHub
parent d1de245e5d
commit fecf3e20d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 118 additions and 71 deletions

View File

@ -57,10 +57,15 @@ export default Component.extend({
@observes("topicTitle")
topicTitleChanged() {
if (this.oldTopicTitle === this.topicTitle) {
return;
}
this.setProperties({
loading: true,
noResults: true,
selectedTopicId: null,
oldTopicTitle: this.topicTitle,
});
this.search(this.topicTitle);

View File

@ -11,6 +11,9 @@ export default Component.extend({
target.setSelectionRange(0, target.value.length);
try {
document.execCommand("copy");
if (this.copied) {
this.copied();
}
} catch (err) {}
},
});

View File

@ -7,6 +7,7 @@ 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,
@ -50,12 +51,15 @@ export default Controller.extend(
});
},
setAutogenerated(value) {
if ((this.autogenerated || !this.invite.id) && !value) {
this.invites.unshiftObject(this.invite);
}
this.set("autogenerated", value);
},
save(opts) {
const newRecord =
(this.autogenerated || !this.invite.id) && !opts.autogenerated;
this.set("autogenerated", opts.autogenerated);
const data = { ...this.buffered.buffer };
if (data.groupIds !== undefined) {
@ -90,13 +94,16 @@ export default Controller.extend(
.save(data)
.then(() => {
this.rollbackBuffer();
if (newRecord) {
this.invites.unshiftObject(this.invite);
}
this.setAutogenerated(opts.autogenerated);
if (!this.autogenerated) {
this.send("closeModal");
if (this.type === "email" && opts.sendEmail) {
this.send("closeModal");
} else {
this.appEvents.trigger("modal-body:flash", {
text: I18n.t("user.invited.invite.invite_saved"),
messageClass: "success",
});
}
}
})
.catch((e) =>
@ -138,6 +145,11 @@ export default Controller.extend(
return hasBufferedChanges || (inviteEmail ? "email" : "link") !== type;
},
@action
mutAutogenerated(value) {
this.setAutogenerated(value);
},
@action
saveInvite(sendEmail) {
this.appEvents.trigger("modal-body:clearFlash");

View File

@ -8,6 +8,7 @@ import PasswordValidation from "discourse/mixins/password-validation";
import UserFieldsValidation from "discourse/mixins/user-fields-validation";
import UsernameValidation from "discourse/mixins/username-validation";
import { ajax } from "discourse/lib/ajax";
import { extractError } from "discourse/lib/ajax-error";
import discourseComputed from "discourse-common/utils/decorators";
import { emailValid } from "discourse/lib/utilities";
import { findAll as findLoginMethods } from "discourse/models/login-method";
@ -154,7 +155,7 @@ export default Controller.extend(
}
})
.catch((error) => {
throw new Error(error);
this.set("errorMessage", extractError(error));
});
},
},

View File

@ -49,7 +49,7 @@ export default Controller.extend({
showBulkActionButtons(filter) {
return (
filter === "pending" &&
this.model.invites.length > 1 &&
this.model.invites.length > 0 &&
this.currentUser.staff
);
},

View File

@ -1,24 +1,16 @@
{{#d-modal-body title=(if invite.id "user.invited.invite.edit_title" "user.invited.invite.new_title")}}
<div class="input-group">
<label for="invite_link">{{i18n "user.invited.invite.instructions"}}</label>
{{input name="invite_link"
class="invite-link"
value=invite.link
readonly=true}}
{{copy-button selector="input.invite-link"}}
</div>
<form>
<div class="input-group">
<label for="invite_link">{{i18n "user.invited.invite.instructions"}}</label>
{{input name="invite_link"
class="invite-link"
value=invite.link
readonly=true}}
{{copy-button selector="input.invite-link" copied=(action "mutAutogenerated" false)}}
</div>
<p>{{i18n "user.invited.invite.expires_at_time" time=expiresAtRelative}}</p>
<p>{{i18n "user.invited.invite.expires_at_time" time=expiresAtRelative}}</p>
<p>
{{#if showAdvanced}}
<a href {{action (mut showAdvanced) false}}>{{d-icon "caret-down"}} {{i18n "user.invited.invite.hide_advanced"}}</a>
{{else}}
<a href {{action (mut showAdvanced) true}}>{{d-icon "caret-right"}} {{i18n "user.invited.invite.show_advanced"}}</a>
{{/if}}
</p>
{{#if showAdvanced}}
<div class="input-group">
<div class="radio-group">
{{radio-button id="invite-type-link" name="invite-type" value="link" selection=type}}
@ -56,43 +48,53 @@
{{/if}}
{{#if currentUser.staff}}
<div class="input-group">
<label>{{i18n "user.invited.invite.add_to_groups"}}</label>
{{group-chooser
content=allGroups
value=buffered.groupIds
labelProperty="name"
onChange=(action (mut buffered.groupIds))
}}
</div>
<p>
{{#if showAdvanced}}
<a href {{action (mut showAdvanced) false}}>{{d-icon "caret-down"}} {{i18n "user.invited.invite.hide_advanced"}}</a>
{{else}}
<a href {{action (mut showAdvanced) true}}>{{d-icon "caret-right"}} {{i18n "user.invited.invite.show_advanced"}}</a>
{{/if}}
</p>
<div class="input-group">
{{choose-topic
selectedTopicId=buffered.topicId
topicTitle=buffered.topicTitle
additionalFilters="status:public"
label="user.invited.invite.invite_to_topic"
}}
</div>
<div class="input-group">
{{future-date-input
displayLabel=(i18n "user.invited.invite.expires_at")
includeDateTime=true
includeMidFuture=true
clearable=true
onChangeInput=(action (mut buffered.expires_at))
}}
</div>
{{#if isEmail}}
{{#if showAdvanced}}
<div class="input-group">
<label for="invite-message">{{i18n "user.invited.invite.custom_message"}}</label>
{{textarea id="invite-message" value=buffered.custom_message}}
<label>{{i18n "user.invited.invite.add_to_groups"}}</label>
{{group-chooser
content=allGroups
value=buffered.groupIds
labelProperty="name"
onChange=(action (mut buffered.groupIds))
}}
</div>
<div class="input-group">
{{choose-topic
selectedTopicId=buffered.topicId
topicTitle=buffered.topicTitle
additionalFilters="status:public"
label="user.invited.invite.invite_to_topic"
}}
</div>
<div class="input-group">
{{future-date-input
displayLabel=(i18n "user.invited.invite.expires_at")
includeDateTime=true
includeMidFuture=true
clearable=true
onChangeInput=(action (mut buffered.expires_at))
}}
</div>
{{#if isEmail}}
<div class="input-group">
<label for="invite-message">{{i18n "user.invited.invite.custom_message"}}</label>
{{textarea id="invite-message" value=buffered.custom_message}}
</div>
{{/if}}
{{/if}}
{{/if}}
{{/if}}
</form>
{{/d-modal-body}}
<div class="modal-footer">

View File

@ -18,15 +18,20 @@
{{d-button icon="upload" action=(action "createInviteCsv") label="user.invited.bulk_invite.text"}}
{{/if}}
{{#if showBulkActionButtons}}
{{#if removedAll}}
{{i18n "user.invited.removed_all"}}
{{else}}
{{d-button icon="times" action=(action "destroyAllExpired") label="user.invited.remove_all"}}
{{#if inviteExpired}}
{{#if removedAll}}
{{i18n "user.invited.removed_all"}}
{{else}}
{{d-button icon="times" action=(action "destroyAllExpired") label="user.invited.remove_all"}}
{{/if}}
{{/if}}
{{#if reinvitedAll}}
{{i18n "user.invited.reinvited_all"}}
{{else}}
{{d-button icon="sync" action=(action "reinviteAll") label="user.invited.reinvite_all"}}
{{#if invitePending}}
{{#if reinvitedAll}}
{{i18n "user.invited.reinvited_all"}}
{{else}}
{{d-button icon="sync" action=(action "reinviteAll") label="user.invited.reinvite_all"}}
{{/if}}
{{/if}}
{{/if}}
</div>

View File

@ -15,3 +15,7 @@ end
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_invited_groups_on_group_id_and_invite_id (group_id,invite_id) UNIQUE
#

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddUniqueIndexToInvitedGroups < ActiveRecord::Migration[6.0]
def change
execute <<~SQL
DELETE FROM invited_groups a
USING invited_groups b
WHERE a.id < b.id
AND a.invite_id = b.invite_id
AND a.group_id = b.group_id
SQL
add_index :invited_groups, [:group_id, :invite_id], unique: true
end
end