diff --git a/app/assets/javascripts/discourse/app/components/copy-button.hbs b/app/assets/javascripts/discourse/app/components/copy-button.hbs index 620a20a017b..3eccc6b803f 100644 --- a/app/assets/javascripts/discourse/app/components/copy-button.hbs +++ b/app/assets/javascripts/discourse/app/components/copy-button.hbs @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/copy-button.js b/app/assets/javascripts/discourse/app/components/copy-button.js index 10f6bdc08df..83d2864d500 100644 --- a/app/assets/javascripts/discourse/app/components/copy-button.js +++ b/app/assets/javascripts/discourse/app/components/copy-button.js @@ -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) {} diff --git a/app/assets/javascripts/discourse/app/components/d-modal.gjs b/app/assets/javascripts/discourse/app/components/d-modal.gjs index b2a80750df6..3956a7991a7 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal.gjs +++ b/app/assets/javascripts/discourse/app/components/d-modal.gjs @@ -371,7 +371,7 @@ export default class DModal extends Component { {{/if}} - {{#if (has-block "footer")}} + {{#if (and (has-block "footer") (not @hideFooter))}} diff --git a/app/assets/javascripts/discourse/app/components/future-date-input.hbs b/app/assets/javascripts/discourse/app/components/future-date-input.hbs index 9732ee59dfc..c151ce42169 100644 --- a/app/assets/javascripts/discourse/app/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/app/components/future-date-input.hbs @@ -1,19 +1,21 @@
-
- - -
+ {{#unless this.noRelativeOptions}} +
+ + +
+ {{/unless}} {{#if this.displayDateAndTimePicker}}
diff --git a/app/assets/javascripts/discourse/app/components/future-date-input.js b/app/assets/javascripts/discourse/app/components/future-date-input.js index 47bb9a2fcd0..a89db34ee38 100644 --- a/app/assets/javascripts/discourse/app/components/future-date-input.js +++ b/app/assets/javascripts/discourse/app/components/future-date-input.js @@ -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({ diff --git a/app/assets/javascripts/discourse/app/components/modal/create-invite.gjs b/app/assets/javascripts/discourse/app/components/modal/create-invite.gjs new file mode 100644 index 00000000000..62baabbfbaa --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/modal/create-invite.gjs @@ -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; + } + + +} + +const InviteModalAlert = ; + +class ShareOrCopyInviteLink extends Component { + @service capabilities; + + @action + async nativeShare() { + await nativeShare(this.capabilities, { url: this.args.invite.link }); + } + + +} diff --git a/app/assets/javascripts/discourse/app/components/modal/create-invite.hbs b/app/assets/javascripts/discourse/app/components/modal/create-invite.hbs deleted file mode 100644 index 3bed9058866..00000000000 --- a/app/assets/javascripts/discourse/app/components/modal/create-invite.hbs +++ /dev/null @@ -1,204 +0,0 @@ - - <:belowHeader> - {{#if this.flashText}} - - {{/if}} - - <:body> -
- {{#if this.editing}} - - {{/if}} - -
- -
- - {{#if this.capabilities.hasContactPicker}} - - {{/if}} -
-
- - {{#if this.isLink}} -
- - -
- {{/if}} - - {{#if this.isEmail}} -
- -