DEV: Update create-invite modal to component-based API (#23797)

This commit is contained in:
David Taylor 2023-10-05 11:25:17 +01:00 committed by GitHub
parent 07c93918ec
commit 8a7b5b00ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 415 additions and 429 deletions

View File

@ -0,0 +1,204 @@
<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={{action (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={{action (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>

View File

@ -0,0 +1,205 @@
import Component from "@ember/component";
import { action } from "@ember/object";
import { not, readOnly } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { getNativeContact } from "discourse/lib/pwa-utils";
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 I18n from "I18n";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import { sanitize } from "discourse/lib/text";
import { timeShortcuts } from "discourse/lib/time-shortcut";
export default Component.extend(bufferedProperty("invite"), {
allGroups: null,
topics: null,
flashText: null,
flashClass: null,
flashLink: false,
editing: readOnly("model.editing"),
inviteToTopic: false,
limitToEmail: false,
@discourseComputed("buffered.emailOrDomain")
isEmail(emailOrDomain) {
return emailValid(emailOrDomain?.trim());
},
@discourseComputed("buffered.emailOrDomain")
isDomain(emailOrDomain) {
return hostnameValid(emailOrDomain?.trim());
},
isLink: not("isEmail"),
init() {
this._super();
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.buffered.setProperties({
max_redemptions_allowed: 1,
expires_at: moment()
.add(this.siteSettings.invite_expiry_days, "days")
.format(FORMAT),
groupIds: this.model?.groupIds,
});
},
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);
},
});

View File

@ -1,232 +0,0 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { not } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractError } from "discourse/lib/ajax-error";
import { getNativeContact } from "discourse/lib/pwa-utils";
import { emailValid, hostnameValid } from "discourse/lib/utilities";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import Group from "discourse/models/group";
import Invite from "discourse/models/invite";
import I18n from "I18n";
import { FORMAT } from "select-kit/components/future-date-input-selector";
import { sanitize } from "discourse/lib/text";
import { timeShortcuts } from "discourse/lib/time-shortcut";
export default Controller.extend(
ModalFunctionality,
bufferedProperty("invite"),
{
allGroups: null,
topics: null,
flashText: null,
flashClass: null,
flashLink: false,
invite: null,
invites: null,
editing: false,
inviteToTopic: false,
limitToEmail: false,
@discourseComputed("buffered.emailOrDomain")
isEmail(emailOrDomain) {
return emailValid(emailOrDomain?.trim());
},
@discourseComputed("buffered.emailOrDomain")
isDomain(emailOrDomain) {
return hostnameValid(emailOrDomain?.trim());
},
isLink: not("isEmail"),
onShow() {
Group.findAll().then((groups) => {
this.set("allGroups", groups.filterBy("automatic", false));
});
this.setProperties({
topics: [],
flashText: null,
flashClass: null,
flashLink: false,
invite: null,
invites: null,
editing: false,
inviteToTopic: false,
limitToEmail: false,
});
this.setInvite(Invite.create());
this.buffered.setProperties({
max_redemptions_allowed: 1,
expires_at: moment()
.add(this.siteSettings.invite_expiry_days, "days")
.format(FORMAT),
});
},
onClose() {
this.appEvents.trigger("modal-body:clearFlash");
},
setInvite(invite) {
this.setProperties({ invite, topics: invite.topics });
},
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();
if (
this.invites &&
!this.invites.any((i) => i.id === this.invite.id)
) {
this.invites.unshiftObject(this.invite);
}
if (this.isEmail && opts.sendEmail) {
this.send("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.appEvents.trigger("modal-body:clearFlash");
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);
},
}
);

View File

@ -7,11 +7,11 @@ import discourseComputed, {
observes, observes,
} from "discourse-common/utils/decorators"; } from "discourse-common/utils/decorators";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import Invite from "discourse/models/invite"; import Invite from "discourse/models/invite";
import I18n from "I18n"; import I18n from "I18n";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import CreateInviteBulk from "discourse/components/modal/create-invite-bulk"; import CreateInviteBulk from "discourse/components/modal/create-invite-bulk";
import CreateInvite from "discourse/components/modal/create-invite";
export default Controller.extend({ export default Controller.extend({
dialog: service(), dialog: service(),
@ -73,8 +73,7 @@ export default Controller.extend({
@action @action
createInvite() { createInvite() {
const controller = showModal("create-invite"); this.modal.show(CreateInvite, { model: { invites: this.model.invites } });
controller.set("invites", this.model.invites);
}, },
@action @action
@ -84,9 +83,7 @@ export default Controller.extend({
@action @action
editInvite(invite) { editInvite(invite) {
const controller = showModal("create-invite"); this.modal.show(CreateInvite, { model: { editing: true, invite } });
controller.set("editing", true);
controller.setInvite(invite);
}, },
@action @action

View File

@ -1,9 +1,9 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import I18n from "I18n"; import I18n from "I18n";
import { action } from "@ember/object"; import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import GroupAddMembersModal from "discourse/components/modal/group-add-members"; import GroupAddMembersModal from "discourse/components/modal/group-add-members";
import CreateInvite from "discourse/components/modal/create-invite";
export default DiscourseRoute.extend({ export default DiscourseRoute.extend({
modal: service(), modal: service(),
@ -34,9 +34,8 @@ export default DiscourseRoute.extend({
@action @action
showInviteModal() { showInviteModal() {
const model = this.modelFor("group"); const group = this.modelFor("group");
const controller = showModal("create-invite"); this.modal.show(CreateInvite, { model: { groupIds: [group.id] } });
controller.buffered.set("groupIds", [model.id]);
}, },
@action @action

View File

@ -1,187 +0,0 @@
{{#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}}
<DModalBody
@title={{if
this.editing
"user.invited.invite.edit_title"
"user.invited.invite.new_title"
}}
>
<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={{action (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={{action (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>
</DModalBody>
<div class="modal-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"
/>
</div>