UX: Simplify invite modal
This commit is contained in:
parent
4406bbb020
commit
66fe7e62e7
|
@ -3,4 +3,5 @@
|
|||
@action={{this.copy}}
|
||||
class={{this.copyClass}}
|
||||
@ariaLabel={{this.ariaLabel}}
|
||||
@translatedLabel={{this.copyTranslatedLabel}}
|
||||
/>
|
|
@ -9,6 +9,12 @@ export default class CopyButton extends Component {
|
|||
copyIcon = "copy";
|
||||
copyClass = "btn-primary";
|
||||
|
||||
init() {
|
||||
super.init(...arguments);
|
||||
|
||||
this.copyTranslatedLabel = this.translatedLabel;
|
||||
}
|
||||
|
||||
@bind
|
||||
_restoreButton() {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
|
@ -17,6 +23,7 @@ export default class CopyButton extends Component {
|
|||
|
||||
this.set("copyIcon", "copy");
|
||||
this.set("copyClass", "btn-primary");
|
||||
this.set("copyTranslatedLabel", this.translatedLabel);
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -34,6 +41,7 @@ export default class CopyButton extends Component {
|
|||
|
||||
this.set("copyIcon", "check");
|
||||
this.set("copyClass", "btn-primary ok");
|
||||
this.set("copyTranslatedLabel", this.translatedLabelAfterCopy);
|
||||
|
||||
discourseDebounce(this._restoreButton, 3000);
|
||||
} catch (err) {}
|
||||
|
|
|
@ -387,7 +387,7 @@ export default class DModal extends Component {
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if (has-block "footer")}}
|
||||
{{#if (and (has-block "footer") (not @hideFooter))}}
|
||||
<div class="d-modal__footer">
|
||||
{{yield to="footer"}}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,505 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { and, not } from "truth-helpers";
|
||||
import CopyButton from "discourse/components/copy-button";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DModal from "discourse/components/d-modal";
|
||||
import Form from "discourse/components/form";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { getNativeContact } from "discourse/lib/pwa-utils";
|
||||
import { sanitize } from "discourse/lib/text";
|
||||
import { emailValid, hostnameValid } from "discourse/lib/utilities";
|
||||
import Group from "discourse/models/group";
|
||||
import Invite from "discourse/models/invite";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import I18n from "discourse-i18n";
|
||||
import { FORMAT } from "select-kit/components/future-date-input-selector";
|
||||
import GroupChooser from "select-kit/components/group-chooser";
|
||||
import TopicChooser from "select-kit/components/topic-chooser";
|
||||
|
||||
export default class CreateInvite extends Component {
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
|
||||
@tracked saving = false;
|
||||
@tracked displayAdvancedOptions = false;
|
||||
@tracked submitButton;
|
||||
|
||||
@tracked flashText = null;
|
||||
@tracked flashClass = null;
|
||||
@tracked flashLink = false;
|
||||
|
||||
@tracked topics = this.invite.topics ?? this.model.topics ?? [];
|
||||
@tracked allGroups = null;
|
||||
|
||||
model = this.args.model;
|
||||
invite = this.model.invite ?? Invite.create();
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
Group.findAll().then((groups) => {
|
||||
this.allGroups = groups.filterBy("automatic", false);
|
||||
});
|
||||
}
|
||||
|
||||
get linkValidityMessageFormat() {
|
||||
return I18n.messageFormat("user.invited.invite.link_validity_MF", {
|
||||
user_count: this.maxRedemptionsAllowedLimit,
|
||||
duration_days: this.siteSettings.invite_expiry_days,
|
||||
});
|
||||
}
|
||||
|
||||
get expireAfterOptions() {
|
||||
return [
|
||||
{
|
||||
value: 1,
|
||||
text: I18n.t("dates.medium.x_days", { count: 1 }),
|
||||
},
|
||||
{
|
||||
value: 7,
|
||||
text: I18n.t("dates.medium.x_days", { count: 7 }),
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
text: I18n.t("dates.medium.x_days", { count: 30 }),
|
||||
},
|
||||
{
|
||||
value: 90,
|
||||
text: I18n.t("dates.medium.x_days", { count: 90 }),
|
||||
},
|
||||
{
|
||||
value: 99999,
|
||||
text: I18n.t("time_shortcut.never"),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get data() {
|
||||
return {
|
||||
restrictTo: this.invite.emailOrDomain ?? "",
|
||||
maxRedemptions:
|
||||
this.invite.max_redemptions_allowed ?? this.maxRedemptionsAllowedLimit,
|
||||
expireAfterDays:
|
||||
this.invite.expires_at ?? this.siteSettings.invite_expiry_days,
|
||||
inviteToTopic: this.invite.topicId,
|
||||
inviteToGroups: this.model.groupIds ?? this.invite.groupIds ?? [],
|
||||
customMessage: this.invite.custom_message ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
async save(data, opts) {
|
||||
let isLink = true;
|
||||
let isEmail = false;
|
||||
|
||||
if (data.emailOrDomain) {
|
||||
if (emailValid(data.emailOrDomain)) {
|
||||
isEmail = true;
|
||||
isLink = false;
|
||||
data.email = data.emailOrDomain?.trim();
|
||||
} else if (hostnameValid(data.emailOrDomain)) {
|
||||
data.domain = data.emailOrDomain?.trim();
|
||||
}
|
||||
delete data.emailOrDomain;
|
||||
}
|
||||
|
||||
if (data.groupIds !== undefined) {
|
||||
data.group_ids = data.groupIds.length > 0 ? data.groupIds : "";
|
||||
delete data.groupIds;
|
||||
}
|
||||
|
||||
if (data.topicId !== undefined) {
|
||||
data.topic_id = data.topicId;
|
||||
delete data.topicId;
|
||||
delete data.topicTitle;
|
||||
}
|
||||
|
||||
if (isLink) {
|
||||
if (this.invite.email) {
|
||||
data.email = data.custom_message = "";
|
||||
}
|
||||
} else if (isEmail) {
|
||||
if (data.max_redemptions_allowed > 1) {
|
||||
data.max_redemptions_allowed = 1;
|
||||
}
|
||||
|
||||
if (opts.sendEmail) {
|
||||
data.send_email = true;
|
||||
|
||||
// TODO: check what's up with this. nothing updates this property
|
||||
if (this.inviteToTopic) {
|
||||
data.invite_to_topic = true;
|
||||
}
|
||||
} else {
|
||||
data.skip_email = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.invite.save(data);
|
||||
const invites = this.model?.invites;
|
||||
if (invites && !invites.any((i) => i.id === this.invite.id)) {
|
||||
invites.unshiftObject(this.invite);
|
||||
}
|
||||
this.flashText = sanitize(I18n.t("user.invited.invite.invite_saved"));
|
||||
this.flashClass = "success";
|
||||
this.flashLink = !this.args.model.editing;
|
||||
} catch (error) {
|
||||
this.flashText = sanitize(extractError(error));
|
||||
this.flashClass = "error";
|
||||
this.flashLink = false;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
get maxRedemptionsAllowedLimit() {
|
||||
if (this.currentUser.staff) {
|
||||
return this.siteSettings.invite_link_max_redemptions_limit;
|
||||
} else {
|
||||
return this.siteSettings.invite_link_max_redemptions_limit_users;
|
||||
}
|
||||
}
|
||||
|
||||
get canInviteToGroup() {
|
||||
return (
|
||||
this.currentUser.staff || this.currentUser.groups.any((g) => g.owner)
|
||||
);
|
||||
}
|
||||
|
||||
get canArriveAtTopic() {
|
||||
return this.currentUser.staff && !this.siteSettings.must_approve_users;
|
||||
}
|
||||
|
||||
@action
|
||||
copied() {
|
||||
this.save({ sendEmail: false, copy: true });
|
||||
}
|
||||
|
||||
@action
|
||||
async onFormSubmit(data) {
|
||||
await this.save(
|
||||
{
|
||||
emailOrDomain: data.restrictTo,
|
||||
groupIds: data.inviteToGroups,
|
||||
topicId: data.inviteToTopic,
|
||||
max_redemptions_allowed: data.maxRedemptions,
|
||||
expires_at: moment().add(data.expireAfterDays, "days").format(FORMAT),
|
||||
custom_message: data.customMessage,
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
registerSubmitButton(submitButton) {
|
||||
this.submitButton = submitButton;
|
||||
}
|
||||
|
||||
@action
|
||||
saveInvite() {
|
||||
this.submitButton.click();
|
||||
}
|
||||
|
||||
@action
|
||||
searchContact() {
|
||||
getNativeContact(this.capabilities, ["email"], false).then((result) => {
|
||||
this.set("buffered.email", result[0].email[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeTopic(fieldSet, topicId, topic) {
|
||||
this.topics = [topic];
|
||||
fieldSet(topicId);
|
||||
}
|
||||
|
||||
@action
|
||||
showAdvancedMode() {
|
||||
this.displayAdvancedOptions = true;
|
||||
}
|
||||
|
||||
get simpleMode() {
|
||||
return !this.invite?.id && !this.displayAdvancedOptions;
|
||||
}
|
||||
|
||||
get isNewInvite() {
|
||||
// use .get to track the id
|
||||
return !this.invite.get("id");
|
||||
}
|
||||
|
||||
get isExistingInvite() {
|
||||
return !this.isNewInvite;
|
||||
}
|
||||
|
||||
@action
|
||||
async createLink() {
|
||||
// TODO: do we need topicId here when the modal is opended via share topic?
|
||||
await this.save(
|
||||
{
|
||||
max_redemptions_allowed: this.maxRedemptionsAllowedLimit,
|
||||
expires_at: moment()
|
||||
.add(this.siteSettings.invite_expiry_days, "days")
|
||||
.format(FORMAT),
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.args.closeModal();
|
||||
}
|
||||
|
||||
<template>
|
||||
<DModal
|
||||
class="create-invite-modal"
|
||||
@title={{i18n
|
||||
(if
|
||||
@model.editing
|
||||
"user.invited.invite.edit_title"
|
||||
"user.invited.invite.new_title"
|
||||
)
|
||||
}}
|
||||
@closeModal={{@closeModal}}
|
||||
@hideFooter={{and this.simpleMode this.isExistingInvite}}
|
||||
>
|
||||
<:belowHeader>
|
||||
{{#if (and this.flashText (not this.simpleMode))}}
|
||||
<div
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
class="alert alert-{{this.flashClass}}"
|
||||
>
|
||||
{{#if this.flashLink}}
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{htmlSafe this.flashText}}
|
||||
{{i18n "user.invited.invite.instructions"}}</label>
|
||||
<div class="link-share-container">
|
||||
<input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
value={{this.invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
<CopyButton @selector="input.invite-link" />
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{htmlSafe this.flashText}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @model.editing}}
|
||||
<div id="modal-alert" role="alert" class="alert alert-info">
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{htmlSafe this.flashText}}
|
||||
{{i18n "user.invited.invite.copy_link_and_share_it"}}</label>
|
||||
<div class="link-share-container">
|
||||
<input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
value={{this.invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
<CopyButton
|
||||
@selector="input.invite-link"
|
||||
@translatedLabel={{i18n "user.invited.invite.copy_link"}}
|
||||
@translatedLabelAfterCopy={{i18n
|
||||
"user.invited.invite.link_copied"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</:belowHeader>
|
||||
<:body>
|
||||
{{#if this.simpleMode}}
|
||||
{{#if this.isExistingInvite}}
|
||||
<p>
|
||||
{{i18n "user.invited.invite.copy_link_and_share_it"}}
|
||||
</p>
|
||||
<div class="link-share-container">
|
||||
<input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
value={{this.invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
<CopyButton
|
||||
@selector="input.invite-link"
|
||||
@translatedLabel={{i18n "user.invited.invite.copy_link"}}
|
||||
@translatedLabelAfterCopy={{i18n
|
||||
"user.invited.invite.link_copied"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "user.invited.invite.create_link_to_invite"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{this.linkValidityMessageFormat}}
|
||||
<a {{on "click" this.showAdvancedMode}}>{{i18n
|
||||
"user.invited.invite.edit_link_options"
|
||||
}}</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<Form
|
||||
@data={{this.data}}
|
||||
@onSubmit={{this.onFormSubmit}}
|
||||
as |form transientData|
|
||||
>
|
||||
<form.Field
|
||||
@name="restrictTo"
|
||||
@title={{i18n "user.invited.invite.restrict"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
placeholder={{i18n
|
||||
"user.invited.invite.email_or_domain_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
|
||||
{{#unless (emailValid transientData.restrictTo)}}
|
||||
<form.Field
|
||||
@name="maxRedemptions"
|
||||
@title={{i18n "user.invited.invite.max_redemptions_allowed"}}
|
||||
@type="number"
|
||||
@format="small"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Input
|
||||
type="number"
|
||||
min="1"
|
||||
max={{this.maxRedemptionsAllowedLimit}}
|
||||
/>
|
||||
</form.Field>
|
||||
{{/unless}}
|
||||
|
||||
<form.Field
|
||||
@name="expireAfterDays"
|
||||
@title={{i18n "user.invited.invite.expires_at"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Select as |select|>
|
||||
{{#each this.expireAfterOptions as |option|}}
|
||||
<select.Option
|
||||
@value={{option.value}}
|
||||
>{{option.text}}</select.Option>
|
||||
{{/each}}
|
||||
</field.Select>
|
||||
</form.Field>
|
||||
|
||||
{{#if this.canArriveAtTopic}}
|
||||
<form.Field
|
||||
@name="inviteToTopic"
|
||||
@title={{i18n "user.invited.invite.invite_to_topic"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<TopicChooser
|
||||
@value={{field.value}}
|
||||
@content={{this.topics}}
|
||||
@onChange={{fn this.onChangeTopic field.set}}
|
||||
@options={{hash additionalFilters="status:public"}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{else if this.topicTitle}}
|
||||
<form.Field
|
||||
@name="inviteToTopicTitle"
|
||||
@title={{i18n "user.invited.invite.invite_to_topic"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input disabled={{true}} />
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canInviteToGroup}}
|
||||
<form.Field
|
||||
@name="inviteToGroups"
|
||||
@title={{i18n "user.invited.invite.add_to_groups"}}
|
||||
@format="large"
|
||||
@description={{i18n
|
||||
"user.invited.invite.cannot_invite_predefined_groups"
|
||||
(hash path=(getURL "/g"))
|
||||
}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<GroupChooser
|
||||
@content={{this.allGroups}}
|
||||
@value={{field.value}}
|
||||
@labelProperty="name"
|
||||
@onChange={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
{{#if (emailValid transientData.restrictTo)}}
|
||||
<form.Field
|
||||
@name="customMessage"
|
||||
@title={{i18n "user.invited.invite.custom_message"}}
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Textarea
|
||||
height={{100}}
|
||||
placeholder={{i18n
|
||||
"user.invited.invite.custom_message_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
|
||||
<form.Submit
|
||||
{{didInsert this.registerSubmitButton}}
|
||||
@label="save"
|
||||
class="hidden"
|
||||
/>
|
||||
</Form>
|
||||
{{/if}}
|
||||
</:body>
|
||||
<:footer>
|
||||
{{#if this.simpleMode}}
|
||||
<DButton
|
||||
@label="user.invited.invite.create_link"
|
||||
@action={{this.createLink}}
|
||||
@disabled={{this.saving}}
|
||||
class="btn-primary save-invite"
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
@label="user.invited.invite.save_invite"
|
||||
@action={{this.saveInvite}}
|
||||
@disabled={{this.saving}}
|
||||
class="btn-primary save-invite"
|
||||
/>
|
||||
{{/if}}
|
||||
<DButton
|
||||
@label="user.invited.invite.cancel"
|
||||
@action={{this.cancel}}
|
||||
class="btn-transparent"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
||||
</template>
|
||||
}
|
|
@ -1,204 +0,0 @@
|
|||
<DModal
|
||||
class="create-invite-modal"
|
||||
@title={{i18n
|
||||
(if
|
||||
this.editing
|
||||
"user.invited.invite.edit_title"
|
||||
"user.invited.invite.new_title"
|
||||
)
|
||||
}}
|
||||
@closeModal={{@closeModal}}
|
||||
>
|
||||
<:belowHeader>
|
||||
{{#if this.flashText}}
|
||||
<div
|
||||
id="modal-alert"
|
||||
role="alert"
|
||||
class="alert alert-{{this.flashClass}}"
|
||||
>
|
||||
{{#if this.flashLink}}
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{html-safe this.flashText}}
|
||||
{{i18n "user.invited.invite.instructions"}}</label>
|
||||
<div class="link-share-container">
|
||||
<Input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
@value={{this.invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
<CopyButton
|
||||
@selector="input.invite-link"
|
||||
@copied={{action "copied"}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{html-safe this.flashText}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</:belowHeader>
|
||||
<:body>
|
||||
<form>
|
||||
{{#if this.editing}}
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{i18n
|
||||
"user.invited.invite.instructions"
|
||||
}}</label>
|
||||
<div class="link-share-container">
|
||||
<Input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
@value={{this.invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
<CopyButton
|
||||
@selector="input.invite-link"
|
||||
@copied={{action "copied"}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="input-group input-email">
|
||||
<label for="invite-email">
|
||||
{{d-icon "envelope"}}
|
||||
{{#if this.isEmail}}
|
||||
{{i18n "user.invited.invite.restrict_email"}}
|
||||
{{else if this.isDomain}}
|
||||
{{i18n "user.invited.invite.restrict_domain"}}
|
||||
{{else}}
|
||||
{{i18n "user.invited.invite.restrict"}}
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="invite-email-container">
|
||||
<Input
|
||||
id="invite-email"
|
||||
@value={{this.buffered.emailOrDomain}}
|
||||
placeholder={{i18n
|
||||
"user.invited.invite.email_or_domain_placeholder"
|
||||
}}
|
||||
/>
|
||||
{{#if this.capabilities.hasContactPicker}}
|
||||
<DButton
|
||||
@icon="address-book"
|
||||
@action={{this.searchContact}}
|
||||
class="btn-primary open-contact-picker"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if this.isLink}}
|
||||
<div class="input-group invite-max-redemptions">
|
||||
<label for="invite-max-redemptions">{{d-icon "users"}}{{i18n
|
||||
"user.invited.invite.max_redemptions_allowed"
|
||||
}}</label>
|
||||
<Input
|
||||
id="invite-max-redemptions"
|
||||
@type="number"
|
||||
@value={{this.buffered.max_redemptions_allowed}}
|
||||
min="1"
|
||||
max={{this.maxRedemptionsAllowedLimit}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isEmail}}
|
||||
<div class="input-group invite-custom-message">
|
||||
<label for="invite-message">{{i18n
|
||||
"user.invited.invite.custom_message"
|
||||
}}</label>
|
||||
<Textarea
|
||||
id="invite-message"
|
||||
@value={{this.buffered.custom_message}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canArriveAtTopic}}
|
||||
<div class="input-group invite-to-topic">
|
||||
<label for="invite-topic">{{d-icon "hand-point-right"}}{{i18n
|
||||
"user.invited.invite.invite_to_topic"
|
||||
}}</label>
|
||||
<TopicChooser
|
||||
@value={{this.buffered.topicId}}
|
||||
@content={{this.topics}}
|
||||
@onChange={{action "onChangeTopic"}}
|
||||
@options={{hash additionalFilters="status:public"}}
|
||||
/>
|
||||
</div>
|
||||
{{else if this.buffered.topicTitle}}
|
||||
<div class="input-group invite-to-topic">
|
||||
<label for="invite-topic">{{d-icon "hand-point-right"}}{{i18n
|
||||
"user.invited.invite.invite_to_topic"
|
||||
}}</label>
|
||||
<Input
|
||||
name="invite-topic"
|
||||
class="invite-topic"
|
||||
@value={{this.buffered.topicTitle}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canInviteToGroup}}
|
||||
<div class="input-group invite-to-groups">
|
||||
<label>{{d-icon "users"}}{{i18n
|
||||
"user.invited.invite.add_to_groups"
|
||||
}}</label>
|
||||
<GroupChooser
|
||||
@content={{this.allGroups}}
|
||||
@value={{this.buffered.groupIds}}
|
||||
@labelProperty="name"
|
||||
@onChange={{fn (mut this.buffered.groupIds)}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.currentUser.staff}}
|
||||
<div class="input-group invite-expires-at">
|
||||
<FutureDateInput
|
||||
@displayLabelIcon="far-clock"
|
||||
@displayLabel={{i18n "user.invited.invite.expires_at"}}
|
||||
@customShortcuts={{this.timeShortcuts}}
|
||||
@clearable={{true}}
|
||||
@input={{this.buffered.expires_at}}
|
||||
@onChangeInput={{fn (mut this.buffered.expires_at)}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="input-group input-expires-at">
|
||||
<label>{{d-icon "far-clock"}}{{this.expiresAtLabel}}</label>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</:body>
|
||||
<:footer>
|
||||
|
||||
<DButton
|
||||
@icon="link"
|
||||
@label="user.invited.invite.save_invite"
|
||||
@action={{this.saveInvite}}
|
||||
class="btn-primary save-invite"
|
||||
/>
|
||||
|
||||
<DButton
|
||||
@icon="envelope"
|
||||
@label={{if
|
||||
this.invite.emailed
|
||||
"user.invited.reinvite"
|
||||
"user.invited.invite.send_invite_email"
|
||||
}}
|
||||
@action={{fn this.saveInvite true}}
|
||||
@title={{unless
|
||||
this.isEmail
|
||||
"user.invited.invite.send_invite_email_instructions"
|
||||
}}
|
||||
@disabled={{not this.isEmail}}
|
||||
class="btn-primary send-invite"
|
||||
/>
|
||||
|
||||
</:footer>
|
||||
</DModal>
|
|
@ -1,209 +0,0 @@
|
|||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { not, readOnly } from "@ember/object/computed";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { getNativeContact } from "discourse/lib/pwa-utils";
|
||||
import { sanitize } from "discourse/lib/text";
|
||||
import { timeShortcuts } from "discourse/lib/time-shortcut";
|
||||
import { emailValid, hostnameValid } from "discourse/lib/utilities";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
import Group from "discourse/models/group";
|
||||
import Invite from "discourse/models/invite";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import { FORMAT } from "select-kit/components/future-date-input-selector";
|
||||
|
||||
export default class CreateInvite extends Component.extend(
|
||||
bufferedProperty("invite")
|
||||
) {
|
||||
allGroups = null;
|
||||
topics = null;
|
||||
flashText = null;
|
||||
flashClass = null;
|
||||
flashLink = false;
|
||||
inviteToTopic = false;
|
||||
limitToEmail = false;
|
||||
|
||||
@readOnly("model.editing") editing;
|
||||
@not("isEmail") isLink;
|
||||
|
||||
@discourseComputed("buffered.emailOrDomain")
|
||||
isEmail(emailOrDomain) {
|
||||
return emailValid(emailOrDomain?.trim());
|
||||
}
|
||||
|
||||
@discourseComputed("buffered.emailOrDomain")
|
||||
isDomain(emailOrDomain) {
|
||||
return hostnameValid(emailOrDomain?.trim());
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
Group.findAll().then((groups) => {
|
||||
this.set("allGroups", groups.filterBy("automatic", false));
|
||||
});
|
||||
|
||||
this.set("invite", this.model.invite || Invite.create());
|
||||
this.set("topics", this.invite?.topics || this.model.topics || []);
|
||||
|
||||
this.buffered.setProperties({
|
||||
max_redemptions_allowed: this.model.invite?.max_redemptions_allowed ?? 1,
|
||||
expires_at:
|
||||
this.model.invite?.expires_at ??
|
||||
moment()
|
||||
.add(this.siteSettings.invite_expiry_days, "days")
|
||||
.format(FORMAT),
|
||||
groupIds: this.model.groupIds ?? this.model.invite?.groupIds,
|
||||
topicId: this.model.invite?.topicId,
|
||||
topicTitle: this.model.invite?.topicTitle,
|
||||
});
|
||||
}
|
||||
|
||||
save(opts) {
|
||||
const data = { ...this.buffered.buffer };
|
||||
|
||||
if (data.emailOrDomain) {
|
||||
if (emailValid(data.emailOrDomain)) {
|
||||
data.email = data.emailOrDomain?.trim();
|
||||
} else if (hostnameValid(data.emailOrDomain)) {
|
||||
data.domain = data.emailOrDomain?.trim();
|
||||
}
|
||||
delete data.emailOrDomain;
|
||||
}
|
||||
|
||||
if (data.groupIds !== undefined) {
|
||||
data.group_ids = data.groupIds.length > 0 ? data.groupIds : "";
|
||||
delete data.groupIds;
|
||||
}
|
||||
|
||||
if (data.topicId !== undefined) {
|
||||
data.topic_id = data.topicId;
|
||||
delete data.topicId;
|
||||
delete data.topicTitle;
|
||||
}
|
||||
|
||||
if (this.isLink) {
|
||||
if (this.invite.email) {
|
||||
data.email = data.custom_message = "";
|
||||
}
|
||||
} else if (this.isEmail) {
|
||||
if (this.invite.max_redemptions_allowed > 1) {
|
||||
data.max_redemptions_allowed = 1;
|
||||
}
|
||||
|
||||
if (opts.sendEmail) {
|
||||
data.send_email = true;
|
||||
if (this.inviteToTopic) {
|
||||
data.invite_to_topic = true;
|
||||
}
|
||||
} else {
|
||||
data.skip_email = true;
|
||||
}
|
||||
}
|
||||
|
||||
return this.invite
|
||||
.save(data)
|
||||
.then(() => {
|
||||
this.rollbackBuffer();
|
||||
|
||||
const invites = this.model?.invites;
|
||||
if (invites && !invites.any((i) => i.id === this.invite.id)) {
|
||||
invites.unshiftObject(this.invite);
|
||||
}
|
||||
|
||||
if (this.isEmail && opts.sendEmail) {
|
||||
this.closeModal();
|
||||
} else {
|
||||
this.setProperties({
|
||||
flashText: sanitize(I18n.t("user.invited.invite.invite_saved")),
|
||||
flashClass: "success",
|
||||
flashLink: !this.editing,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) =>
|
||||
this.setProperties({
|
||||
flashText: sanitize(extractError(e)),
|
||||
flashClass: "error",
|
||||
flashLink: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"currentUser.staff",
|
||||
"siteSettings.invite_link_max_redemptions_limit",
|
||||
"siteSettings.invite_link_max_redemptions_limit_users"
|
||||
)
|
||||
maxRedemptionsAllowedLimit(staff, staffLimit, usersLimit) {
|
||||
return staff ? staffLimit : usersLimit;
|
||||
}
|
||||
|
||||
@discourseComputed("buffered.expires_at")
|
||||
expiresAtLabel(expires_at) {
|
||||
const expiresAt = moment(expires_at);
|
||||
|
||||
return expiresAt.isBefore()
|
||||
? I18n.t("user.invited.invite.expired_at_time", {
|
||||
time: expiresAt.format("LLL"),
|
||||
})
|
||||
: I18n.t("user.invited.invite.expires_in_time", {
|
||||
time: moment.duration(expiresAt - moment()).humanize(),
|
||||
});
|
||||
}
|
||||
|
||||
@discourseComputed("currentUser.staff", "currentUser.groups")
|
||||
canInviteToGroup(staff, groups) {
|
||||
return staff || groups.any((g) => g.owner);
|
||||
}
|
||||
|
||||
@discourseComputed("currentUser.staff")
|
||||
canArriveAtTopic(staff) {
|
||||
if (staff && !this.siteSettings.must_approve_users) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@discourseComputed
|
||||
timeShortcuts() {
|
||||
const timezone = this.currentUser.user_option.timezone;
|
||||
const shortcuts = timeShortcuts(timezone);
|
||||
return [
|
||||
shortcuts.laterToday(),
|
||||
shortcuts.tomorrow(),
|
||||
shortcuts.laterThisWeek(),
|
||||
shortcuts.monday(),
|
||||
shortcuts.twoWeeks(),
|
||||
shortcuts.nextMonth(),
|
||||
shortcuts.twoMonths(),
|
||||
shortcuts.threeMonths(),
|
||||
shortcuts.fourMonths(),
|
||||
shortcuts.sixMonths(),
|
||||
];
|
||||
}
|
||||
|
||||
@action
|
||||
copied() {
|
||||
this.save({ sendEmail: false, copy: true });
|
||||
}
|
||||
|
||||
@action
|
||||
saveInvite(sendEmail) {
|
||||
this.save({ sendEmail });
|
||||
}
|
||||
|
||||
@action
|
||||
searchContact() {
|
||||
getNativeContact(this.capabilities, ["email"], false).then((result) => {
|
||||
this.set("buffered.email", result[0].email[0]);
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeTopic(topicId, topic) {
|
||||
this.set("topics", [topic]);
|
||||
this.set("buffered.topicId", topicId);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.link-share-container {
|
||||
.invite-link {
|
||||
flex: 1 0 content;
|
||||
}
|
||||
}
|
||||
|
||||
.link-share-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -42,100 +48,3 @@
|
|||
color: var(--facebook);
|
||||
}
|
||||
}
|
||||
|
||||
// topic invite modal
|
||||
|
||||
.create-invite-modal {
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 0.5em;
|
||||
.d-icon {
|
||||
color: var(--primary-medium);
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group:not(:last-of-type) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.input-group.input-expires-at,
|
||||
.input-group.input-email,
|
||||
.input-group.invite-max-redemptions {
|
||||
input[type="text"] {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.existing-topic,
|
||||
p {
|
||||
// p is for "no topics found"
|
||||
margin-left: 1.75em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.input-group.input-email {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-email-container {
|
||||
flex: 1 1 auto;
|
||||
#invite-email {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-max-redemptions {
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
input {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-custom-message {
|
||||
label {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
textarea#invite-message,
|
||||
&.invite-to-topic input[type="text"],
|
||||
.group-chooser,
|
||||
.topic-chooser,
|
||||
.user-chooser,
|
||||
.future-date-input-selector,
|
||||
.future-date-input-date-picker,
|
||||
.future-date-input-time-picker {
|
||||
margin-left: 1.75em;
|
||||
width: calc(100% - 1.75em);
|
||||
}
|
||||
|
||||
.future-date-input-date-picker,
|
||||
.future-date-input-time-picker {
|
||||
display: inline-block;
|
||||
margin: 0em 0em 0em 1.75em;
|
||||
width: calc(50% - 2em);
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1962,14 +1962,25 @@ en:
|
|||
error: "There was an error generating Invite link"
|
||||
|
||||
invite:
|
||||
new_title: "Create Invite"
|
||||
new_title: "Invite members"
|
||||
edit_title: "Edit Invite"
|
||||
|
||||
instructions: "Share this link to instantly grant access to this site:"
|
||||
copy_link: "copy link"
|
||||
expires_in_time: "Expires in %{time}"
|
||||
expired_at_time: "Expired at %{time}"
|
||||
|
||||
create_link_to_invite: "Create a link that can be shared to instantly grant access to this site."
|
||||
copy_link_and_share_it: "Copy the link below and share it to instantly grant access to this site."
|
||||
copy_link: "Copy Link"
|
||||
link_copied: "Link Copied!"
|
||||
link_validity_MF: |
|
||||
Link is valid for up to { user_count, plural,
|
||||
one {# user}
|
||||
other {# users}
|
||||
} and expires in { duration_days, plural,
|
||||
one {# day}
|
||||
other {# days}
|
||||
}.
|
||||
edit_link_options: "Edit link options."
|
||||
show_advanced: "Show Advanced Options"
|
||||
hide_advanced: "Hide Advanced Options"
|
||||
|
||||
|
@ -1981,13 +1992,18 @@ en:
|
|||
max_redemptions_allowed: "Max uses"
|
||||
|
||||
add_to_groups: "Add to groups"
|
||||
cannot_invite_predefined_groups: |
|
||||
Note: You can't invite users directly to predefined groups (e.g. trust level, admins, moderators, staff). Configure a <a href="%{path}">custom group</a> and set the desired trust level in the group's Effect section.
|
||||
invite_to_topic: "Arrive at topic"
|
||||
expires_at: "Expire after"
|
||||
custom_message: "Optional personal message"
|
||||
custom_message: "Custom message"
|
||||
custom_message_placeholder: "Add a personal note to your invitation"
|
||||
|
||||
send_invite_email: "Save and Send Email"
|
||||
send_invite_email_instructions: "Restrict invite to email to send an invite email"
|
||||
save_invite: "Save Invite"
|
||||
save_invite: "Save"
|
||||
cancel: "Cancel"
|
||||
create_link: "Create Link"
|
||||
|
||||
invite_saved: "Invite saved."
|
||||
|
||||
|
|
Loading…
Reference in New Issue