From 66fe7e62e754c6c1cd91d65aa0696d3f593b953f Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Thu, 19 Sep 2024 16:04:45 +0300 Subject: [PATCH] UX: Simplify invite modal --- .../discourse/app/components/copy-button.hbs | 1 + .../discourse/app/components/copy-button.js | 8 + .../discourse/app/components/d-modal.gjs | 2 +- .../app/components/modal/create-invite.gjs | 505 ++++++++++++++++++ .../app/components/modal/create-invite.hbs | 204 ------- .../app/components/modal/create-invite.js | 209 -------- .../stylesheets/common/base/share_link.scss | 103 +--- config/locales/client.en.yml | 26 +- 8 files changed, 542 insertions(+), 516 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/modal/create-invite.gjs delete mode 100644 app/assets/javascripts/discourse/app/components/modal/create-invite.hbs delete mode 100644 app/assets/javascripts/discourse/app/components/modal/create-invite.js diff --git a/app/assets/javascripts/discourse/app/components/copy-button.hbs b/app/assets/javascripts/discourse/app/components/copy-button.hbs index 620a20a017b..0aa581ace1e 100644 --- a/app/assets/javascripts/discourse/app/components/copy-button.hbs +++ b/app/assets/javascripts/discourse/app/components/copy-button.hbs @@ -3,4 +3,5 @@ @action={{this.copy}} class={{this.copyClass}} @ariaLabel={{this.ariaLabel}} + @translatedLabel={{this.copyTranslatedLabel}} /> \ 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 db7796f0531..b52b0eea540 100644 --- a/app/assets/javascripts/discourse/app/components/d-modal.gjs +++ b/app/assets/javascripts/discourse/app/components/d-modal.gjs @@ -387,7 +387,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/modal/create-invite.gjs b/app/assets/javascripts/discourse/app/components/modal/create-invite.gjs new file mode 100644 index 00000000000..7032e5d5b22 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/modal/create-invite.gjs @@ -0,0 +1,505 @@ +import Component from "@glimmer/component"; +import { tracked } from "@glimmer/tracking"; +import { fn, hash } from "@ember/helper"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; +import didInsert from "@ember/render-modifiers/modifiers/did-insert"; +import { service } from "@ember/service"; +import { htmlSafe } from "@ember/template"; +import { and, not } from "truth-helpers"; +import CopyButton from "discourse/components/copy-button"; +import DButton from "discourse/components/d-button"; +import DModal from "discourse/components/d-modal"; +import Form from "discourse/components/form"; +import { extractError } from "discourse/lib/ajax-error"; +import { getNativeContact } from "discourse/lib/pwa-utils"; +import { sanitize } from "discourse/lib/text"; +import { emailValid, hostnameValid } from "discourse/lib/utilities"; +import Group from "discourse/models/group"; +import Invite from "discourse/models/invite"; +import i18n from "discourse-common/helpers/i18n"; +import getURL from "discourse-common/lib/get-url"; +import I18n from "discourse-i18n"; +import { FORMAT } from "select-kit/components/future-date-input-selector"; +import GroupChooser from "select-kit/components/group-chooser"; +import TopicChooser from "select-kit/components/topic-chooser"; + +export default class CreateInvite extends Component { + @service currentUser; + @service siteSettings; + + @tracked saving = false; + @tracked displayAdvancedOptions = false; + @tracked submitButton; + + @tracked flashText = null; + @tracked flashClass = null; + @tracked flashLink = false; + + @tracked topics = this.invite.topics ?? this.model.topics ?? []; + @tracked allGroups = null; + + model = this.args.model; + invite = this.model.invite ?? Invite.create(); + + constructor() { + super(...arguments); + + Group.findAll().then((groups) => { + this.allGroups = groups.filterBy("automatic", false); + }); + } + + get linkValidityMessageFormat() { + return I18n.messageFormat("user.invited.invite.link_validity_MF", { + user_count: this.maxRedemptionsAllowedLimit, + duration_days: this.siteSettings.invite_expiry_days, + }); + } + + get expireAfterOptions() { + return [ + { + value: 1, + text: I18n.t("dates.medium.x_days", { count: 1 }), + }, + { + value: 7, + text: I18n.t("dates.medium.x_days", { count: 7 }), + }, + { + value: 30, + text: I18n.t("dates.medium.x_days", { count: 30 }), + }, + { + value: 90, + text: I18n.t("dates.medium.x_days", { count: 90 }), + }, + { + value: 99999, + text: I18n.t("time_shortcut.never"), + }, + ]; + } + + get data() { + return { + restrictTo: this.invite.emailOrDomain ?? "", + maxRedemptions: + this.invite.max_redemptions_allowed ?? this.maxRedemptionsAllowedLimit, + expireAfterDays: + this.invite.expires_at ?? this.siteSettings.invite_expiry_days, + inviteToTopic: this.invite.topicId, + inviteToGroups: this.model.groupIds ?? this.invite.groupIds ?? [], + customMessage: this.invite.custom_message ?? "", + }; + } + + async save(data, opts) { + let isLink = true; + let isEmail = false; + + if (data.emailOrDomain) { + if (emailValid(data.emailOrDomain)) { + isEmail = true; + isLink = false; + data.email = data.emailOrDomain?.trim(); + } else if (hostnameValid(data.emailOrDomain)) { + data.domain = data.emailOrDomain?.trim(); + } + delete data.emailOrDomain; + } + + if (data.groupIds !== undefined) { + data.group_ids = data.groupIds.length > 0 ? data.groupIds : ""; + delete data.groupIds; + } + + if (data.topicId !== undefined) { + data.topic_id = data.topicId; + delete data.topicId; + delete data.topicTitle; + } + + if (isLink) { + if (this.invite.email) { + data.email = data.custom_message = ""; + } + } else if (isEmail) { + if (data.max_redemptions_allowed > 1) { + data.max_redemptions_allowed = 1; + } + + if (opts.sendEmail) { + data.send_email = true; + + // TODO: check what's up with this. nothing updates this property + if (this.inviteToTopic) { + data.invite_to_topic = true; + } + } else { + data.skip_email = true; + } + } + + this.saving = true; + try { + await this.invite.save(data); + const invites = this.model?.invites; + if (invites && !invites.any((i) => i.id === this.invite.id)) { + invites.unshiftObject(this.invite); + } + this.flashText = sanitize(I18n.t("user.invited.invite.invite_saved")); + this.flashClass = "success"; + this.flashLink = !this.args.model.editing; + } catch (error) { + this.flashText = sanitize(extractError(error)); + this.flashClass = "error"; + this.flashLink = false; + } finally { + this.saving = false; + } + } + + get maxRedemptionsAllowedLimit() { + if (this.currentUser.staff) { + return this.siteSettings.invite_link_max_redemptions_limit; + } else { + return this.siteSettings.invite_link_max_redemptions_limit_users; + } + } + + get canInviteToGroup() { + return ( + this.currentUser.staff || this.currentUser.groups.any((g) => g.owner) + ); + } + + get canArriveAtTopic() { + return this.currentUser.staff && !this.siteSettings.must_approve_users; + } + + @action + copied() { + this.save({ sendEmail: false, copy: true }); + } + + @action + async onFormSubmit(data) { + await this.save( + { + emailOrDomain: data.restrictTo, + groupIds: data.inviteToGroups, + topicId: data.inviteToTopic, + max_redemptions_allowed: data.maxRedemptions, + expires_at: moment().add(data.expireAfterDays, "days").format(FORMAT), + custom_message: data.customMessage, + }, + {} + ); + } + + @action + registerSubmitButton(submitButton) { + this.submitButton = submitButton; + } + + @action + saveInvite() { + this.submitButton.click(); + } + + @action + searchContact() { + getNativeContact(this.capabilities, ["email"], false).then((result) => { + this.set("buffered.email", result[0].email[0]); + }); + } + + @action + onChangeTopic(fieldSet, topicId, topic) { + this.topics = [topic]; + fieldSet(topicId); + } + + @action + showAdvancedMode() { + this.displayAdvancedOptions = true; + } + + get simpleMode() { + return !this.invite?.id && !this.displayAdvancedOptions; + } + + get isNewInvite() { + // use .get to track the id + return !this.invite.get("id"); + } + + get isExistingInvite() { + return !this.isNewInvite; + } + + @action + async createLink() { + // TODO: do we need topicId here when the modal is opended via share topic? + await this.save( + { + max_redemptions_allowed: this.maxRedemptionsAllowedLimit, + expires_at: moment() + .add(this.siteSettings.invite_expiry_days, "days") + .format(FORMAT), + }, + {} + ); + } + + @action + cancel() { + this.args.closeModal(); + } + + +} 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}} -
- -