UX: Simplify invite modal (#28974)
This commit simplifies the initial state of the invite modal when it's opened to make it one click away from creating an invite link. The existing options/fields within the invite modal are still available, but are now hidden behind an advanced mode which can be enabled. On the technical front, this PR also switches the invite modal to use our FormKit library. Internal topic: t/134023.
This commit is contained in:
parent
b1321b985a
commit
a5497b74be
|
@ -1,6 +1,7 @@
|
|||
<DButton
|
||||
@icon={{this.copyIcon}}
|
||||
@action={{this.copy}}
|
||||
class={{this.copyClass}}
|
||||
class="copy-button {{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) {}
|
||||
|
|
|
@ -371,7 +371,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>
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
<div class="future-date-input">
|
||||
<div class="control-group">
|
||||
<label class={{this.labelClasses}}>
|
||||
{{#if this.displayLabelIcon}}{{d-icon
|
||||
this.displayLabelIcon
|
||||
}}{{/if}}{{this.displayLabel}}
|
||||
</label>
|
||||
<FutureDateInputSelector
|
||||
@value={{readonly this.selection}}
|
||||
@content={{this.shortcuts}}
|
||||
@clearable={{this.clearable}}
|
||||
@onChangeInput={{this.onChangeInput}}
|
||||
@onChange={{fn (mut this.selection)}}
|
||||
@options={{hash none="time_shortcut.select_timeframe"}}
|
||||
/>
|
||||
</div>
|
||||
{{#unless this.noRelativeOptions}}
|
||||
<div class="control-group">
|
||||
<label class={{this.labelClasses}}>
|
||||
{{#if this.displayLabelIcon}}{{d-icon
|
||||
this.displayLabelIcon
|
||||
}}{{/if}}{{this.displayLabel}}
|
||||
</label>
|
||||
<FutureDateInputSelector
|
||||
@value={{readonly this.selection}}
|
||||
@content={{this.shortcuts}}
|
||||
@clearable={{this.clearable}}
|
||||
@onChangeInput={{this.onChangeInput}}
|
||||
@onChange={{fn (mut this.selection)}}
|
||||
@options={{hash none="time_shortcut.select_timeframe"}}
|
||||
/>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#if this.displayDateAndTimePicker}}
|
||||
<div class="control-group future-date-input-date-picker">
|
||||
|
|
|
@ -42,7 +42,7 @@ export default class FutureDateInput extends Component {
|
|||
if (this.input) {
|
||||
const dateTime = moment(this.input);
|
||||
const closestShortcut = this._findClosestShortcut(dateTime);
|
||||
if (closestShortcut) {
|
||||
if (!this.noRelativeOptions && closestShortcut) {
|
||||
this.set("selection", closestShortcut.id);
|
||||
} else {
|
||||
this.setProperties({
|
||||
|
|
|
@ -0,0 +1,503 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { fn, hash } from "@ember/helper";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { and, notEq, or } 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 FutureDateInput from "discourse/components/future-date-input";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { canNativeShare, nativeShare } 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 I18n from "discourse-i18n";
|
||||
import { FORMAT as DATE_INPUT_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 capabilities;
|
||||
@service currentUser;
|
||||
@service siteSettings;
|
||||
@service site;
|
||||
|
||||
@tracked saving = false;
|
||||
@tracked displayAdvancedOptions = false;
|
||||
|
||||
@tracked flashText;
|
||||
@tracked flashClass = "info";
|
||||
|
||||
@tracked topics = this.invite.topics ?? this.model.topics ?? [];
|
||||
@tracked allGroups;
|
||||
|
||||
model = this.args.model;
|
||||
invite = this.model.invite ?? Invite.create();
|
||||
formApi;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
Group.findAll().then((groups) => {
|
||||
this.allGroups = groups.filter((group) => !group.automatic);
|
||||
});
|
||||
}
|
||||
|
||||
get linkValidityMessageFormat() {
|
||||
return I18n.messageFormat("user.invited.invite.link_validity_MF", {
|
||||
user_count: this.defaultRedemptionsAllowed,
|
||||
duration_days: this.siteSettings.invite_expiry_days,
|
||||
});
|
||||
}
|
||||
|
||||
get expireAfterOptions() {
|
||||
let list = [1, 7, 30, 90];
|
||||
|
||||
if (!list.includes(this.siteSettings.invite_expiry_days)) {
|
||||
list.push(this.siteSettings.invite_expiry_days);
|
||||
}
|
||||
|
||||
list = list
|
||||
.sort((a, b) => a - b)
|
||||
.map((days) => {
|
||||
return {
|
||||
value: days,
|
||||
text: I18n.t("dates.medium.x_days", { count: days }),
|
||||
};
|
||||
});
|
||||
|
||||
list.push({
|
||||
value: 999999,
|
||||
text: I18n.t("time_shortcut.never"),
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@cached
|
||||
get data() {
|
||||
const data = {
|
||||
restrictTo: this.invite.emailOrDomain ?? "",
|
||||
maxRedemptions:
|
||||
this.invite.max_redemptions_allowed ?? this.defaultRedemptionsAllowed,
|
||||
inviteToTopic: this.invite.topicId,
|
||||
inviteToGroups: this.model.groupIds ?? this.invite.groupIds ?? [],
|
||||
customMessage: this.invite.custom_message ?? "",
|
||||
};
|
||||
|
||||
if (this.inviteCreated) {
|
||||
data.expiresAt = this.invite.expires_at;
|
||||
} else {
|
||||
data.expiresAfterDays = this.siteSettings.invite_expiry_days;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async save(data) {
|
||||
let isLink = true;
|
||||
let isEmail = false;
|
||||
|
||||
if (data.emailOrDomain) {
|
||||
if (emailValid(data.emailOrDomain)) {
|
||||
isEmail = true;
|
||||
isLink = false;
|
||||
data.email = data.emailOrDomain;
|
||||
} else if (hostnameValid(data.emailOrDomain)) {
|
||||
data.domain = data.emailOrDomain;
|
||||
}
|
||||
delete data.emailOrDomain;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
data.send_email = true;
|
||||
if (data.topic_id) {
|
||||
data.invite_to_topic = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.saving = true;
|
||||
try {
|
||||
await this.invite.save(data);
|
||||
const invites = this.model?.invites;
|
||||
if (invites && !invites.some((i) => i.id === this.invite.id)) {
|
||||
invites.unshiftObject(this.invite);
|
||||
}
|
||||
|
||||
if (!this.simpleMode) {
|
||||
this.flashText = sanitize(I18n.t("user.invited.invite.invite_saved"));
|
||||
this.flashClass = "success";
|
||||
}
|
||||
} catch (error) {
|
||||
this.flashText = sanitize(extractError(error));
|
||||
this.flashClass = "error";
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
get maxRedemptionsAllowedLimit() {
|
||||
if (this.currentUser.staff) {
|
||||
return this.siteSettings.invite_link_max_redemptions_limit;
|
||||
}
|
||||
|
||||
return this.siteSettings.invite_link_max_redemptions_limit_users;
|
||||
}
|
||||
|
||||
get defaultRedemptionsAllowed() {
|
||||
const max = this.maxRedemptionsAllowedLimit;
|
||||
const val = this.currentUser.staff ? 100 : 10;
|
||||
return Math.min(max, val);
|
||||
}
|
||||
|
||||
get canInviteToGroup() {
|
||||
return (
|
||||
this.currentUser.staff ||
|
||||
this.currentUser.groups.some((g) => g.group_user?.owner)
|
||||
);
|
||||
}
|
||||
|
||||
get canArriveAtTopic() {
|
||||
return this.currentUser.staff && !this.siteSettings.must_approve_users;
|
||||
}
|
||||
|
||||
@action
|
||||
async onFormSubmit(data) {
|
||||
const submitData = {
|
||||
emailOrDomain: data.restrictTo?.trim(),
|
||||
group_ids: data.inviteToGroups,
|
||||
topic_id: data.inviteToTopic,
|
||||
max_redemptions_allowed: data.maxRedemptions,
|
||||
custom_message: data.customMessage,
|
||||
};
|
||||
|
||||
if (data.expiresAt) {
|
||||
submitData.expires_at = data.expiresAt;
|
||||
} else if (data.expiresAfterDays) {
|
||||
submitData.expires_at = moment()
|
||||
.add(data.expiresAfterDays, "days")
|
||||
.format(DATE_INPUT_FORMAT);
|
||||
}
|
||||
|
||||
await this.save(submitData);
|
||||
}
|
||||
|
||||
@action
|
||||
saveInvite() {
|
||||
this.formApi.submit();
|
||||
}
|
||||
|
||||
@action
|
||||
onChangeTopic(fieldSet, topicId, topic) {
|
||||
this.topics = [topic];
|
||||
fieldSet(topicId);
|
||||
}
|
||||
|
||||
@action
|
||||
showAdvancedMode() {
|
||||
this.displayAdvancedOptions = true;
|
||||
}
|
||||
|
||||
get simpleMode() {
|
||||
return !this.args.model.editing && !this.displayAdvancedOptions;
|
||||
}
|
||||
|
||||
get inviteCreated() {
|
||||
// use .get to track the id
|
||||
return !!this.invite.get("id");
|
||||
}
|
||||
|
||||
@action
|
||||
async createLink() {
|
||||
await this.save({
|
||||
max_redemptions_allowed: this.defaultRedemptionsAllowed,
|
||||
expires_at: moment()
|
||||
.add(this.siteSettings.invite_expiry_days, "days")
|
||||
.format(DATE_INPUT_FORMAT),
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
this.args.closeModal();
|
||||
}
|
||||
|
||||
@action
|
||||
registerApi(api) {
|
||||
this.formApi = api;
|
||||
}
|
||||
|
||||
<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.inviteCreated}}
|
||||
>
|
||||
<:belowHeader>
|
||||
{{#if (or this.flashText @model.editing)}}
|
||||
<InviteModalAlert
|
||||
@invite={{this.invite}}
|
||||
@alertClass={{this.flashClass}}
|
||||
@showInviteLink={{and
|
||||
this.inviteCreated
|
||||
(notEq this.flashClass "error")
|
||||
}}
|
||||
>
|
||||
{{#if this.flashText}}
|
||||
{{htmlSafe this.flashText}}
|
||||
{{/if}}
|
||||
{{#if (and this.inviteCreated (notEq this.flashClass "error"))}}
|
||||
{{#if @model.editing}}
|
||||
{{i18n "user.invited.invite.copy_link_and_share_it"}}
|
||||
{{else}}
|
||||
{{i18n "user.invited.invite.instructions"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</InviteModalAlert>
|
||||
{{/if}}
|
||||
</:belowHeader>
|
||||
<:body>
|
||||
{{#if this.simpleMode}}
|
||||
{{#if this.inviteCreated}}
|
||||
{{#unless this.site.mobileView}}
|
||||
<p>
|
||||
{{i18n "user.invited.invite.copy_link_and_share_it"}}
|
||||
</p>
|
||||
{{/unless}}
|
||||
<div class="link-share-container">
|
||||
<ShareOrCopyInviteLink @invite={{this.invite}} />
|
||||
</div>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "user.invited.invite.create_link_to_invite"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p class="link-limits-info">
|
||||
{{this.linkValidityMessageFormat}}
|
||||
<a
|
||||
class="edit-link-options"
|
||||
role="button"
|
||||
{{on "click" this.showAdvancedMode}}
|
||||
>{{i18n "user.invited.invite.edit_link_options"}}</a>
|
||||
</p>
|
||||
{{else}}
|
||||
<Form
|
||||
@data={{this.data}}
|
||||
@onSubmit={{this.onFormSubmit}}
|
||||
@onRegisterApi={{this.registerApi}}
|
||||
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}}
|
||||
|
||||
{{#if this.inviteCreated}}
|
||||
<form.Field
|
||||
@name="expiresAt"
|
||||
@title={{i18n "user.invited.invite.expires_at"}}
|
||||
@format="large"
|
||||
@validation="required"
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<FutureDateInput
|
||||
@clearable={{true}}
|
||||
@input={{field.value}}
|
||||
@noRelativeOptions={{true}}
|
||||
@onChangeInput={{field.set}}
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
{{else}}
|
||||
<form.Field
|
||||
@name="expiresAfterDays"
|
||||
@title={{i18n "user.invited.invite.expires_after"}}
|
||||
@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}}
|
||||
|
||||
{{#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>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.canInviteToGroup}}
|
||||
<form.Field
|
||||
@name="inviteToGroups"
|
||||
@title={{i18n "user.invited.invite.add_to_groups"}}
|
||||
@format="large"
|
||||
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="full"
|
||||
as |field|
|
||||
>
|
||||
<field.Textarea
|
||||
height={{100}}
|
||||
placeholder={{i18n
|
||||
"user.invited.invite.custom_message_placeholder"
|
||||
}}
|
||||
/>
|
||||
</form.Field>
|
||||
{{/if}}
|
||||
</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 cancel-button"
|
||||
/>
|
||||
</:footer>
|
||||
</DModal>
|
||||
</template>
|
||||
}
|
||||
|
||||
const InviteModalAlert = <template>
|
||||
<div id="modal-alert" role="alert" class="alert alert-{{@alertClass}}">
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">
|
||||
{{yield}}
|
||||
</label>
|
||||
{{#if @showInviteLink}}
|
||||
<div class="link-share-container">
|
||||
<ShareOrCopyInviteLink @invite={{@invite}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>;
|
||||
|
||||
class ShareOrCopyInviteLink extends Component {
|
||||
@service capabilities;
|
||||
|
||||
@action
|
||||
async nativeShare() {
|
||||
await nativeShare(this.capabilities, { url: this.args.invite.link });
|
||||
}
|
||||
|
||||
<template>
|
||||
<input
|
||||
name="invite-link"
|
||||
type="text"
|
||||
class="invite-link"
|
||||
value={{@invite.link}}
|
||||
readonly={{true}}
|
||||
/>
|
||||
{{#if (canNativeShare this.capabilities)}}
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@icon="share"
|
||||
@translatedLabel={{i18n "user.invited.invite.share_link"}}
|
||||
@action={{this.nativeShare}}
|
||||
/>
|
||||
{{else}}
|
||||
<CopyButton
|
||||
@selector="input.invite-link"
|
||||
@translatedLabel={{i18n "user.invited.invite.copy_link"}}
|
||||
@translatedLabelAfterCopy={{i18n "user.invited.invite.link_copied"}}
|
||||
/>
|
||||
{{/if}}
|
||||
</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);
|
||||
}
|
||||
}
|
|
@ -1,28 +1,29 @@
|
|||
import { Promise } from "rsvp";
|
||||
|
||||
export function canNativeShare(caps) {
|
||||
return (
|
||||
(caps.isIOS || caps.isAndroid || caps.isWinphone) &&
|
||||
window.location.protocol === "https:" &&
|
||||
typeof window.navigator.share !== "undefined"
|
||||
);
|
||||
}
|
||||
|
||||
export function nativeShare(caps, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!(caps.isIOS || caps.isAndroid || caps.isWinphone)) {
|
||||
if (!canNativeShare(caps)) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
window.location.protocol === "https:" &&
|
||||
typeof window.navigator.share !== "undefined"
|
||||
) {
|
||||
window.navigator
|
||||
.share(data)
|
||||
.then(resolve)
|
||||
.catch((e) => {
|
||||
if (e.name === "AbortError") {
|
||||
// closing share panel do nothing
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
window.navigator
|
||||
.share(data)
|
||||
.then(resolve)
|
||||
.catch((e) => {
|
||||
if (e.name === "AbortError") {
|
||||
// closing share panel do nothing
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
@icon="plus"
|
||||
@action={{this.createInvite}}
|
||||
@label="user.invited.create"
|
||||
class="btn-default"
|
||||
class="btn-default invite-button"
|
||||
/>
|
||||
{{#if this.canBulkInvite}}
|
||||
{{#if this.siteSettings.allow_bulk_invite}}
|
||||
|
|
|
@ -1,290 +0,0 @@
|
|||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
fakeTime,
|
||||
loggedInUser,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
acceptance("Invites - Create & Edit Invite Modal", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
const inviteData = {
|
||||
id: 1,
|
||||
invite_key: "52641ae8878790bc7b79916247cfe6ba",
|
||||
link: "http://example.com/invites/52641ae8878790bc7b79916247cfe6ba",
|
||||
max_redemptions_allowed: 1,
|
||||
redemption_count: 0,
|
||||
created_at: "2021-01-26T12:00:00.000Z",
|
||||
updated_at: "2021-01-26T12:00:00.000Z",
|
||||
expires_at: "2121-01-26T12:00:00.000Z",
|
||||
expired: false,
|
||||
topics: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
server.post("/invites", () => helper.response(inviteData));
|
||||
server.put("/invites/1", (request) => {
|
||||
const data = helper.parsePostData(request.requestBody);
|
||||
if (data.email === "error") {
|
||||
return helper.response(422, {
|
||||
errors: ["error isn't a valid email address."],
|
||||
});
|
||||
} else {
|
||||
return helper.response(inviteData);
|
||||
}
|
||||
});
|
||||
|
||||
server.delete("/invites", () => {
|
||||
return helper.response({});
|
||||
});
|
||||
});
|
||||
|
||||
test("basic functionality", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
|
||||
await assert.dom(".invite-to-groups").exists();
|
||||
await assert.dom(".invite-to-topic").exists();
|
||||
await assert.dom(".invite-expires-at").exists();
|
||||
});
|
||||
|
||||
test("saving", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
|
||||
assert
|
||||
.dom("table.user-invite-list tbody tr")
|
||||
.exists({ count: 3 }, "is seeded with three rows");
|
||||
|
||||
await click(".btn-primary");
|
||||
|
||||
assert
|
||||
.dom("table.user-invite-list tbody tr")
|
||||
.exists({ count: 4 }, "gets added to the list");
|
||||
});
|
||||
|
||||
test("copying saves invite", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
|
||||
await click(".save-invite");
|
||||
assert.dom(".invite-link .btn").exists();
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Invites - Link Invites", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
const inviteData = {
|
||||
id: 1,
|
||||
invite_key: "52641ae8878790bc7b79916247cfe6ba",
|
||||
link: "http://example.com/invites/52641ae8878790bc7b79916247cfe6ba",
|
||||
max_redemptions_allowed: 1,
|
||||
redemption_count: 0,
|
||||
created_at: "2021-01-26T12:00:00.000Z",
|
||||
updated_at: "2021-01-26T12:00:00.000Z",
|
||||
expires_at: "2121-01-26T12:00:00.000Z",
|
||||
expired: false,
|
||||
topics: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
server.post("/invites", () => helper.response(inviteData));
|
||||
server.put("/invites/1", () => helper.response(inviteData));
|
||||
server.delete("/invites", () => helper.response({}));
|
||||
});
|
||||
|
||||
test("invite links", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
|
||||
assert.ok(exists("#invite-max-redemptions"), "shows max redemptions field");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Invites - Email Invites", function (needs) {
|
||||
let lastRequest;
|
||||
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
const inviteData = {
|
||||
id: 1,
|
||||
invite_key: "52641ae8878790bc7b79916247cfe6ba",
|
||||
link: "http://example.com/invites/52641ae8878790bc7b79916247cfe6ba",
|
||||
email: "test@example.com",
|
||||
emailed: false,
|
||||
custom_message: null,
|
||||
created_at: "2021-01-26T12:00:00.000Z",
|
||||
updated_at: "2021-01-26T12:00:00.000Z",
|
||||
expires_at: "2121-01-26T12:00:00.000Z",
|
||||
expired: false,
|
||||
topics: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
server.post("/invites", (request) => {
|
||||
lastRequest = request;
|
||||
return helper.response(inviteData);
|
||||
});
|
||||
|
||||
server.put("/invites/1", (request) => {
|
||||
lastRequest = request;
|
||||
return helper.response(inviteData);
|
||||
});
|
||||
});
|
||||
needs.hooks.beforeEach(() => {
|
||||
lastRequest = null;
|
||||
});
|
||||
|
||||
test("invite email", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
|
||||
assert.ok(exists("#invite-email"), "shows email field");
|
||||
await fillIn("#invite-email", "test@example.com");
|
||||
|
||||
assert.ok(exists(".save-invite"), "shows save without email button");
|
||||
await click(".save-invite");
|
||||
assert.ok(
|
||||
lastRequest.requestBody.includes("skip_email=true"),
|
||||
"sends skip_email to server"
|
||||
);
|
||||
|
||||
await fillIn("#invite-email", "test2@example.com ");
|
||||
assert.ok(exists(".send-invite"), "shows save and send email button");
|
||||
await click(".send-invite");
|
||||
assert.ok(
|
||||
lastRequest.requestBody.includes("send_email=true"),
|
||||
"sends send_email to server"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance(
|
||||
"Invites - Create & Edit Invite Modal - timeframe choosing",
|
||||
function (needs) {
|
||||
let clock = null;
|
||||
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
const inviteData = {
|
||||
id: 1,
|
||||
invite_key: "52641ae8878790bc7b79916247cfe6ba",
|
||||
link: "http://example.com/invites/52641ae8878790bc7b79916247cfe6ba",
|
||||
max_redemptions_allowed: 1,
|
||||
redemption_count: 0,
|
||||
created_at: "2021-01-26T12:00:00.000Z",
|
||||
updated_at: "2021-01-26T12:00:00.000Z",
|
||||
expires_at: "2121-01-26T12:00:00.000Z",
|
||||
expired: false,
|
||||
topics: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
server.post("/invites", () => helper.response(inviteData));
|
||||
server.put("/invites/1", () => helper.response(inviteData));
|
||||
});
|
||||
|
||||
needs.hooks.beforeEach(() => {
|
||||
const timezone = loggedInUser().user_option.timezone;
|
||||
clock = fakeTime("2100-05-03T08:00:00", timezone, true); // Monday morning
|
||||
});
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
test("shows correct timeframe options", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
await click(".future-date-input-selector-header");
|
||||
|
||||
const options = Array.from(
|
||||
queryAll(`ul.select-kit-collection li span.name`).map((_, x) =>
|
||||
x.innerText.trim()
|
||||
)
|
||||
);
|
||||
|
||||
const expected = [
|
||||
I18n.t("time_shortcut.later_today"),
|
||||
I18n.t("time_shortcut.tomorrow"),
|
||||
I18n.t("time_shortcut.later_this_week"),
|
||||
I18n.t("time_shortcut.start_of_next_business_week_alt"),
|
||||
I18n.t("time_shortcut.two_weeks"),
|
||||
I18n.t("time_shortcut.next_month"),
|
||||
I18n.t("time_shortcut.two_months"),
|
||||
I18n.t("time_shortcut.three_months"),
|
||||
I18n.t("time_shortcut.four_months"),
|
||||
I18n.t("time_shortcut.six_months"),
|
||||
I18n.t("time_shortcut.custom"),
|
||||
];
|
||||
|
||||
assert.deepEqual(options, expected, "options are correct");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
acceptance(
|
||||
"Invites - Create Invite on Site with must_approve_users Setting",
|
||||
function (needs) {
|
||||
needs.user();
|
||||
needs.settings({ must_approve_users: true });
|
||||
|
||||
test("hides `Arrive at Topic` field on sites with `must_approve_users`", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(".user-invite-buttons .btn:first-child");
|
||||
assert.dom(".invite-to-topic").doesNotExist();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
acceptance(
|
||||
"Invites - Populates Edit Invite Form with saved invite data",
|
||||
function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/groups/search.json", () => {
|
||||
return helper.response([
|
||||
{
|
||||
id: 41,
|
||||
automatic: false,
|
||||
name: "Macdonald",
|
||||
},
|
||||
{
|
||||
id: 47, // must match group-fixtures.js because lookup is by ID
|
||||
automatic: false,
|
||||
name: "Discourse",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
server.post("/invites", () => helper.response({}));
|
||||
});
|
||||
|
||||
test("shows correct saved data in form", async function (assert) {
|
||||
await visit("/u/eviltrout/invited/pending");
|
||||
await click(
|
||||
".user-invite-list tbody tr:nth-child(3) .invite-actions .btn:first-child"
|
||||
); // third invite edit button
|
||||
assert.dom("#invite-max-redemptions").hasValue("10");
|
||||
assert
|
||||
.dom(".invite-to-topic .name")
|
||||
.hasText("Welcome to Discourse! :wave:");
|
||||
assert.dom(".invite-to-groups .formatted-selection").hasText("Macdonald");
|
||||
assert.dom("#invite-email").hasValue("cat.com");
|
||||
});
|
||||
|
||||
test("shows correct saved data in group invite form", async function (assert) {
|
||||
await visit("/g/discourse");
|
||||
await click(".group-members-invite");
|
||||
assert.dom(".invite-to-groups .formatted-selection").hasText("Discourse");
|
||||
|
||||
await click(".save-invite");
|
||||
});
|
||||
}
|
||||
);
|
|
@ -127,6 +127,10 @@
|
|||
animation-duration: 0s;
|
||||
}
|
||||
}
|
||||
|
||||
#modal-alert {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
//legacy
|
||||
|
|
|
@ -13,6 +13,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.link-share-container {
|
||||
.invite-link {
|
||||
flex: 1 0;
|
||||
}
|
||||
|
||||
.mobile-view & {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.link-share-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -42,100 +52,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,6 @@ html:not(.keyboard-visible.mobile-view) {
|
|||
max-height: calc(var(--composer-vh, var(--1dvh)) * 85);
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
|
|
@ -1964,14 +1964,26 @@ en:
|
|||
error: "There was an error generating Invite link"
|
||||
|
||||
invite:
|
||||
new_title: "Create Invite"
|
||||
edit_title: "Edit 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!"
|
||||
share_link: "Share link"
|
||||
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"
|
||||
|
||||
|
@ -1984,12 +1996,16 @@ en:
|
|||
|
||||
add_to_groups: "Add to groups"
|
||||
invite_to_topic: "Arrive at topic"
|
||||
expires_at: "Expire after"
|
||||
custom_message: "Optional personal message"
|
||||
expires_at: "Expire at"
|
||||
expires_after: "Expire after"
|
||||
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."
|
||||
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
describe "Creating Invites", type: :system do
|
||||
fab!(:group)
|
||||
fab!(:user) { Fabricate(:user, groups: [group]) }
|
||||
fab!(:topic) { Fabricate(:post).topic }
|
||||
let(:user_invited_pending_page) { PageObjects::Pages::UserInvitedPending.new }
|
||||
let(:create_invite_modal) { PageObjects::Modals::CreateInvite.new }
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
|
||||
def open_invite_modal
|
||||
find(".user-invite-buttons .btn", match: :first).click
|
||||
end
|
||||
|
||||
def display_advanced_options
|
||||
create_invite_modal.edit_options_link.click
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.invite_allowed_groups = "#{group.id}"
|
||||
SiteSetting.invite_link_max_redemptions_limit_users = 7
|
||||
SiteSetting.invite_link_max_redemptions_limit = 63
|
||||
SiteSetting.invite_expiry_days = 3
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
before do
|
||||
user_invited_pending_page.visit(user)
|
||||
open_invite_modal
|
||||
end
|
||||
|
||||
it "is possible to create an invite link without toggling the advanced options" do
|
||||
cdp.allow_clipboard
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
create_invite_modal.copy_button.click
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
cdp.clipboard_has_text?(invite_link)
|
||||
|
||||
expect(create_invite_modal.link_limits_info_paragraph).to have_text(
|
||||
"Link is valid for up to 7 users and expires in 3 days.",
|
||||
)
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_link_type(
|
||||
key: invite_key,
|
||||
redemption_count: 0,
|
||||
max_redemption_count: 7,
|
||||
)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 3.days,
|
||||
)
|
||||
end
|
||||
|
||||
it "has the correct modal title when creating a new invite" do
|
||||
expect(create_invite_modal.header).to have_text(I18n.t("js.user.invited.invite.new_title"))
|
||||
end
|
||||
|
||||
it "hides the modal footer after creating an invite via simple mode" do
|
||||
expect(create_invite_modal).to have_footer
|
||||
create_invite_modal.save_button.click
|
||||
expect(create_invite_modal).to have_no_footer
|
||||
end
|
||||
|
||||
context "when editing an invite" do
|
||||
before do
|
||||
create_invite_modal.save_button.click
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
user_invited_pending_page.latest_invite.edit_button.click
|
||||
end
|
||||
|
||||
it "has the correct modal title" do
|
||||
expect(create_invite_modal.header).to have_text(I18n.t("js.user.invited.invite.edit_title"))
|
||||
end
|
||||
|
||||
it "displays the invite link and a copy button" do
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
expect(create_invite_modal).to have_invite_link_input
|
||||
end
|
||||
end
|
||||
|
||||
context "with the advanced options" do
|
||||
before { display_advanced_options }
|
||||
|
||||
it "is possible to populate all the fields" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("discourse.org")
|
||||
create_invite_modal.form.field("maxRedemptions").fill_in("53")
|
||||
create_invite_modal.form.field("expiresAfterDays").select(90)
|
||||
|
||||
create_invite_modal.choose_topic(topic)
|
||||
create_invite_modal.choose_groups([group])
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_link_type(
|
||||
key: invite_key,
|
||||
redemption_count: 0,
|
||||
max_redemption_count: 53,
|
||||
)
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_topic(topic)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 90.days,
|
||||
)
|
||||
end
|
||||
|
||||
it "is possible to create an email invite" do
|
||||
another_group = Fabricate(:group)
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("someone@discourse.org")
|
||||
create_invite_modal.form.field("expiresAfterDays").select(1)
|
||||
|
||||
create_invite_modal.choose_topic(topic)
|
||||
create_invite_modal.choose_groups([group, another_group])
|
||||
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("customMessage")
|
||||
.fill_in("Hello someone, this is a test invite")
|
||||
|
||||
create_invite_modal.save_button.click
|
||||
|
||||
expect(create_invite_modal).to have_copy_button
|
||||
|
||||
invite_link = create_invite_modal.invite_link_input.value
|
||||
invite_key = invite_link.split("/").last
|
||||
|
||||
create_invite_modal.close
|
||||
|
||||
expect(user_invited_pending_page.invites_list.size).to eq(1)
|
||||
|
||||
expect(user_invited_pending_page.latest_invite).to be_email_type("someone@discourse.org")
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_group(another_group)
|
||||
expect(user_invited_pending_page.latest_invite).to have_topic(topic)
|
||||
expect(user_invited_pending_page.latest_invite.expiry_date).to be_within(2.minutes).of(
|
||||
Time.zone.now + 1.day,
|
||||
)
|
||||
end
|
||||
|
||||
it "adds the invite_expiry_days site setting to the list of options for the expiresAfterDays field" do
|
||||
options =
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("expiresAfterDays")
|
||||
.component
|
||||
.all(".form-kit__control-option")
|
||||
.map(&:text)
|
||||
expect(options).to eq(["1 day", "3 days", "7 days", "30 days", "90 days", "Never"])
|
||||
|
||||
SiteSetting.invite_expiry_days = 90
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
options =
|
||||
create_invite_modal
|
||||
.form
|
||||
.field("expiresAfterDays")
|
||||
.component
|
||||
.all(".form-kit__control-option")
|
||||
.map(&:text)
|
||||
expect(options).to eq(["1 day", "7 days", "30 days", "90 days", "Never"])
|
||||
end
|
||||
|
||||
it "uses the invite_link_max_redemptions_limit_users setting as the default value for the maxRedemptions field if the setting is lower than 10" do
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("7")
|
||||
|
||||
SiteSetting.invite_link_max_redemptions_limit_users = 11
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("10")
|
||||
end
|
||||
|
||||
it "uses the invite_link_max_redemptions_limit setting as the default value for the maxRedemptions field for staff users if the setting is lower than 100" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("63")
|
||||
|
||||
SiteSetting.invite_link_max_redemptions_limit = 108
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form.field("maxRedemptions").value).to eq("100")
|
||||
end
|
||||
|
||||
it "shows the inviteToGroups field for a normal user if they're owner on at least 1 group" do
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToGroups")
|
||||
|
||||
group.add_owner(user)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToGroups")
|
||||
end
|
||||
|
||||
it "shows the inviteToGroups field for admins" do
|
||||
user.update!(admin: true)
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToGroups")
|
||||
end
|
||||
|
||||
it "doesn't show the inviteToTopic field to normal users" do
|
||||
SiteSetting.must_approve_users = false
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "shows the inviteToTopic field to admins if the must_approve_users setting is false" do
|
||||
user.update!(admin: true)
|
||||
SiteSetting.must_approve_users = false
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "doesn't show the inviteToTopic field to admins if the must_approve_users setting is true" do
|
||||
user.update!(admin: true)
|
||||
SiteSetting.must_approve_users = true
|
||||
page.refresh
|
||||
open_invite_modal
|
||||
display_advanced_options
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("inviteToTopic")
|
||||
end
|
||||
|
||||
it "replaces the expiresAfterDays field with expiresAt with date and time controls after creating the invite" do
|
||||
create_invite_modal.form.field("expiresAfterDays").select(1)
|
||||
create_invite_modal.save_button.click
|
||||
now = Time.zone.now
|
||||
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("expiresAfterDays")
|
||||
expect(create_invite_modal.form).to have_field_with_name("expiresAt")
|
||||
|
||||
expires_at_field = create_invite_modal.form.field("expiresAt").component
|
||||
date = expires_at_field.find(".date-picker").value
|
||||
time = expires_at_field.find(".time-input").value
|
||||
|
||||
expire_date = Time.parse("#{date} #{time}:#{now.strftime("%S")}").utc
|
||||
expect(expire_date).to be_within_one_minute_of(now + 1.day)
|
||||
end
|
||||
|
||||
context "when an email is given to the restrictTo field" do
|
||||
it "shows the customMessage field and hides the maxRedemptions field" do
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("customMessage")
|
||||
expect(create_invite_modal.form).to have_field_with_name("maxRedemptions")
|
||||
|
||||
create_invite_modal.form.field("restrictTo").fill_in("discourse@cdck.org")
|
||||
|
||||
expect(create_invite_modal.form).to have_field_with_name("customMessage")
|
||||
expect(create_invite_modal.form).to have_no_field_with_name("maxRedemptions")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -114,7 +114,12 @@ module PageObjects
|
|||
picker.search(value)
|
||||
picker.select_row_by_value(value)
|
||||
when "select"
|
||||
component.find(".form-kit__control-option[value='#{value}']").click
|
||||
selector = component.find(".form-kit__control-select")
|
||||
selector.find(".form-kit__control-option[value='#{value}']").select_option
|
||||
selector.execute_script(<<~JS, selector)
|
||||
var selector = arguments[0];
|
||||
selector.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
|
||||
JS
|
||||
when "menu"
|
||||
trigger = component.find(".fk-d-menu__trigger.form-kit__control-menu")
|
||||
trigger.click
|
||||
|
@ -193,6 +198,14 @@ module PageObjects
|
|||
end
|
||||
end
|
||||
|
||||
def has_field_with_name?(name)
|
||||
has_css?(".form-kit__field[data-name='#{name}']")
|
||||
end
|
||||
|
||||
def has_no_field_with_name?(name)
|
||||
has_no_css?(".form-kit__field[data-name='#{name}']")
|
||||
end
|
||||
|
||||
def container(name)
|
||||
within component do
|
||||
FormKitContainer.new(find(".form-kit__container[data-name='#{name}']"))
|
||||
|
|
|
@ -8,6 +8,10 @@ module PageObjects
|
|||
|
||||
BODY_SELECTOR = ""
|
||||
|
||||
def header
|
||||
find(".d-modal__header")
|
||||
end
|
||||
|
||||
def body
|
||||
find(".d-modal__body#{BODY_SELECTOR}")
|
||||
end
|
||||
|
@ -16,6 +20,14 @@ module PageObjects
|
|||
find(".d-modal__footer")
|
||||
end
|
||||
|
||||
def has_footer?
|
||||
has_css?(".d-modal__footer")
|
||||
end
|
||||
|
||||
def has_no_footer?
|
||||
has_no_css?(".d-modal__footer")
|
||||
end
|
||||
|
||||
def close
|
||||
find(".modal-close").click
|
||||
end
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Modals
|
||||
class CreateInvite < PageObjects::Modals::Base
|
||||
def modal
|
||||
find(".create-invite-modal")
|
||||
end
|
||||
|
||||
def edit_options_link
|
||||
within(modal) { find(".edit-link-options") }
|
||||
end
|
||||
|
||||
def save_button
|
||||
within(modal) { find(".save-invite") }
|
||||
end
|
||||
|
||||
def copy_button
|
||||
within(modal) { find(".copy-button") }
|
||||
end
|
||||
|
||||
def has_copy_button?
|
||||
within(modal) { has_css?(".copy-button") }
|
||||
end
|
||||
|
||||
def has_invite_link_input?
|
||||
within(modal) { has_css?("input.invite-link") }
|
||||
end
|
||||
|
||||
def invite_link_input
|
||||
within(modal) { find("input.invite-link") }
|
||||
end
|
||||
|
||||
def link_limits_info_paragraph
|
||||
within(modal) { find("p.link-limits-info") }
|
||||
end
|
||||
|
||||
def form
|
||||
PageObjects::Components::FormKit.new(".create-invite-modal .form-kit")
|
||||
end
|
||||
|
||||
def choose_topic(topic)
|
||||
topic_picker = PageObjects::Components::SelectKit.new(".topic-chooser")
|
||||
topic_picker.expand
|
||||
topic_picker.search(topic.id)
|
||||
topic_picker.select_row_by_index(0)
|
||||
end
|
||||
|
||||
def choose_groups(groups)
|
||||
group_picker = PageObjects::Components::SelectKit.new(".group-chooser")
|
||||
group_picker.expand
|
||||
groups.each { |group| group_picker.select_row_by_value(group.id) }
|
||||
group_picker.collapse
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class UserInvitedPending < PageObjects::Pages::Base
|
||||
class Invite
|
||||
attr_reader :tr_element
|
||||
|
||||
def initialize(tr_element)
|
||||
@tr_element = tr_element
|
||||
end
|
||||
|
||||
def link_type?(key: nil, redemption_count: nil, max_redemption_count: nil)
|
||||
if key && redemption_count && max_redemption_count
|
||||
invite_type_col.has_text?(
|
||||
I18n.t(
|
||||
"js.user.invited.invited_via_link",
|
||||
key: "#{key[0...4]}...",
|
||||
count: redemption_count,
|
||||
max: max_redemption_count,
|
||||
),
|
||||
)
|
||||
else
|
||||
invite_type_col.has_css?(".d-icon-link")
|
||||
end
|
||||
end
|
||||
|
||||
def email_type?(email)
|
||||
invite_type_col.has_text?(email) && invite_type_col.has_css?(".d-icon-envelope")
|
||||
end
|
||||
|
||||
def has_group?(group)
|
||||
invite_type_col.has_css?(".invite-extra", text: group.name)
|
||||
end
|
||||
|
||||
def has_topic?(topic)
|
||||
invite_type_col.has_css?(".invite-extra", text: topic.title)
|
||||
end
|
||||
|
||||
def edit_button
|
||||
tr_element.find(".invite-actions .btn-default")
|
||||
end
|
||||
|
||||
def expiry_date
|
||||
Time.parse(tr_element.find(".invite-expires-at").text).utc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invite_type_col
|
||||
tr_element.find(".invite-type")
|
||||
end
|
||||
end
|
||||
|
||||
def visit(user)
|
||||
url = "/u/#{user.username_lower}/invited/pending"
|
||||
page.visit(url)
|
||||
end
|
||||
|
||||
def invite_button
|
||||
find("#user-content .invite-button")
|
||||
end
|
||||
|
||||
def invites_list
|
||||
all("#user-content .user-invite-list tbody tr").map { |row| Invite.new(row) }
|
||||
end
|
||||
|
||||
def latest_invite
|
||||
Invite.new(find("#user-content .user-invite-list tbody tr:first-of-type"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue