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:
parent
d1de245e5d
commit
fecf3e20d9
|
@ -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);
|
||||
|
|
|
@ -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) {}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue