From 4072786f5b40713525a414b84a10f43ea80f0fb3 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 13 Feb 2023 13:03:31 -0500 Subject: [PATCH] DEV: Support using Ember components in dialog service (#20230) We often have the need to use rich HTML in dialog messages (to show lists, icons, etc.). Previously, our only option was to wrap the message in an `htmlSafe()` call. This PR adds the ability to pass a component name and model to the dialog, which means that we can write the HTML in regular Ember components. Example, whereas previously we would do this: ``` this.dialog.deleteConfirm({ message: htmlSafe(`
  • Some text
  • `), }); ``` instead we can now do this: ```javascript import SecondFactorConfirmPhrase from "discourse/components/dialog-messages/second-factor-confirm-phrase"; ... this.dialog.deleteConfirm({ title: I18n.t("user.second_factor.disable_confirm"), bodyComponent: SecondFactorConfirmPhrase, bodyComponentModel: model, }) ``` The model passed to the component is optional and will be available as `@model` in the Handlebars template. --- .../addon/components/dialog-holder.hbs | 17 +++---- .../dialog-holder/addon/services/dialog.js | 50 ++++++++++--------- .../dialog-messages/group-delete.hbs | 1 + .../second-factor-confirm-phrase.hbs | 32 ++++++++++++ .../second-factor-confirm-phrase.js | 22 ++++++++ .../discourse/app/controllers/group.js | 10 ++-- .../controllers/preferences/second-factor.js | 33 +++--------- .../components/dialog-holder-test.js | 34 +++++++++++-- 8 files changed, 129 insertions(+), 70 deletions(-) create mode 100644 app/assets/javascripts/discourse/app/components/dialog-messages/group-delete.hbs create mode 100644 app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.hbs create mode 100644 app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.js diff --git a/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs index 7a1faaa0db9..52e9808bcd7 100644 --- a/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs +++ b/app/assets/javascripts/dialog-holder/addon/components/dialog-holder.hbs @@ -20,19 +20,14 @@ {{/if}} - {{#if (or this.dialog.message this.dialog.confirmPhrase)}} + {{#if (or this.dialog.message this.dialog.bodyComponent)}}
    - {{#if this.dialog.message}} -

    {{this.dialog.message}}

    - {{/if}} - {{#if this.dialog.confirmPhrase}} - + {{else if this.dialog.message}} +

    {{this.dialog.message}}

    {{/if}}
    {{/if}} diff --git a/app/assets/javascripts/dialog-holder/addon/services/dialog.js b/app/assets/javascripts/dialog-holder/addon/services/dialog.js index 3fed4080757..0724bbe81ca 100644 --- a/app/assets/javascripts/dialog-holder/addon/services/dialog.js +++ b/app/assets/javascripts/dialog-holder/addon/services/dialog.js @@ -1,21 +1,21 @@ import Service from "@ember/service"; import A11yDialog from "a11y-dialog"; import { bind } from "discourse-common/utils/decorators"; -import { isBlank } from "@ember/utils"; export default Service.extend({ - message: null, - type: null, dialogInstance: null, - + message: null, title: null, titleElementId: null, + type: null, + + bodyComponent: null, + bodyComponentModel: null, confirmButtonIcon: null, confirmButtonLabel: null, confirmButtonClass: null, - confirmPhrase: null, - confirmPhraseInput: null, + confirmButtonDisabled: false, cancelButtonLabel: null, cancelButtonClass: null, shouldDisplayCancel: null, @@ -29,15 +29,18 @@ export default Service.extend({ dialog(params) { const { message, + bodyComponent, + bodyComponentModel, type, title, + confirmButtonClass = "btn-primary", confirmButtonIcon, confirmButtonLabel = "ok_value", - confirmButtonClass = "btn-primary", - cancelButtonLabel = "cancel_value", + confirmButtonDisabled = false, + cancelButtonClass = "btn-default", - confirmPhrase, + cancelButtonLabel = "cancel_value", shouldDisplayCancel, didConfirm, @@ -45,25 +48,25 @@ export default Service.extend({ buttons, } = params; - let confirmButtonDisabled = !isBlank(confirmPhrase); - const element = document.getElementById("dialog-holder"); this.setProperties({ message, + bodyComponent, + bodyComponentModel, type, dialogInstance: new A11yDialog(element), title, titleElementId: title !== null ? "dialog-title" : null, - confirmButtonDisabled, confirmButtonClass, - confirmButtonLabel, + confirmButtonDisabled, confirmButtonIcon, - confirmPhrase, - cancelButtonLabel, + confirmButtonLabel, + cancelButtonClass, + cancelButtonLabel, shouldDisplayCancel, didConfirm, @@ -133,19 +136,21 @@ export default Service.extend({ reset() { this.setProperties({ message: null, + bodyComponent: null, + bodyComponentModel: null, type: null, dialogInstance: null, title: null, titleElementId: null, - confirmButtonLabel: null, + confirmButtonDisabled: false, confirmButtonIcon: null, - cancelButtonLabel: null, + confirmButtonLabel: null, + cancelButtonClass: null, + cancelButtonLabel: null, shouldDisplayCancel: null, - confirmPhrase: null, - confirmPhraseInput: null, didConfirm: null, didCancel: null, @@ -176,10 +181,7 @@ export default Service.extend({ }, @bind - onConfirmPhraseInput() { - this.set( - "confirmButtonDisabled", - this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase - ); + enableConfirmButton() { + this.set("confirmButtonDisabled", false); }, }); diff --git a/app/assets/javascripts/discourse/app/components/dialog-messages/group-delete.hbs b/app/assets/javascripts/discourse/app/components/dialog-messages/group-delete.hbs new file mode 100644 index 00000000000..40e56d0fb87 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/dialog-messages/group-delete.hbs @@ -0,0 +1 @@ +{{i18n "admin.groups.delete_with_messages_confirm" count=@model.message_count}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.hbs b/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.hbs new file mode 100644 index 00000000000..47faec691fc --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.hbs @@ -0,0 +1,32 @@ +{{i18n "user.second_factor.delete_confirm_header"}} + + + +

    + {{html-safe + (i18n + "user.second_factor.delete_confirm_instruction" + confirm=this.disabledString + ) + }} +

    + + \ No newline at end of file diff --git a/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.js b/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.js new file mode 100644 index 00000000000..267352f6890 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/dialog-messages/second-factor-confirm-phrase.js @@ -0,0 +1,22 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; + +export default class SecondFactorConfirmPhrase extends Component { + @service dialog; + @service currentUser; + + @tracked confirmPhraseInput = ""; + disabledString = I18n.t("user.second_factor.disable"); + + @action + onConfirmPhraseInput() { + if (this.confirmPhraseInput === this.disabledString) { + this.dialog.set("confirmButtonDisabled", false); + } else { + this.dialog.set("confirmButtonDisabled", true); + } + } +} diff --git a/app/assets/javascripts/discourse/app/controllers/group.js b/app/assets/javascripts/discourse/app/controllers/group.js index 6ee7e58e1d3..f49b1bcb902 100644 --- a/app/assets/javascripts/discourse/app/controllers/group.js +++ b/app/assets/javascripts/discourse/app/controllers/group.js @@ -4,6 +4,7 @@ import I18n from "I18n"; import discourseComputed from "discourse-common/utils/decorators"; import { capitalize } from "@ember/string"; import { inject as service } from "@ember/service"; +import GroupDeleteDialog from "discourse/components/dialog-messages/group-delete"; const Tab = EmberObject.extend({ init() { @@ -138,17 +139,16 @@ export default Controller.extend({ const model = this.model; const title = I18n.t("admin.groups.delete_confirm"); - let message = null; + let bodyComponent = null; if (model.has_messages && model.message_count > 0) { - message = I18n.t("admin.groups.delete_with_messages_confirm", { - count: model.message_count, - }); + bodyComponent = GroupDeleteDialog; } this.dialog.deleteConfirm({ title, - message, + bodyComponent, + bodyComponentModel: model, didConfirm: () => { model .destroy() diff --git a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js index 257c99e1c99..5025c862afd 100644 --- a/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js +++ b/app/assets/javascripts/discourse/app/controllers/preferences/second-factor.js @@ -10,7 +10,7 @@ import { findAll } from "discourse/models/login-method"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; import { inject as service } from "@ember/service"; -import { htmlSafe } from "@ember/template"; +import SecondFactorConfirmPhrase from "discourse/components/dialog-messages/second-factor-confirm-phrase"; export default Controller.extend(CanCheckEmails, { dialog: service(), @@ -114,29 +114,6 @@ export default Controller.extend(CanCheckEmails, { .finally(() => this.set("resetPasswordLoading", false)); }, - disableAllMessage() { - let templateElements = [I18n.t("user.second_factor.delete_confirm_header")]; - templateElements.push(""); - templateElements.push( - I18n.t("user.second_factor.delete_confirm_instruction", { - confirm: I18n.t("user.second_factor.disable"), - }) - ); - return htmlSafe(templateElements.join("")); - }, - actions: { confirmPassword() { if (!this.password) { @@ -154,9 +131,13 @@ export default Controller.extend(CanCheckEmails, { this.dialog.deleteConfirm({ title: I18n.t("user.second_factor.disable_confirm"), - message: this.disableAllMessage(), + bodyComponent: SecondFactorConfirmPhrase, + bodyComponentModel: { + totps: this.totps, + security_keys: this.security_keys, + }, confirmButtonLabel: "user.second_factor.disable", - confirmPhrase: I18n.t("user.second_factor.disable"), + confirmButtonDisabled: true, confirmButtonIcon: "ban", cancelButtonClass: "btn-flat", didConfirm: () => { diff --git a/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js b/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js index a1ed2606e0c..ba63247dcee 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/dialog-holder-test.js @@ -10,6 +10,8 @@ import { } from "@ember/test-helpers"; import { hbs } from "ember-cli-htmlbars"; import { query } from "discourse/tests/helpers/qunit-helpers"; +import GroupDeleteDialogMessage from "discourse/components/dialog-messages/group-delete"; +import SecondFactorConfirmPhrase from "discourse/components/dialog-messages/second-factor-confirm-phrase"; module("Integration | Component | dialog-holder", function (hooks) { setupRenderingTest(hooks); @@ -394,17 +396,41 @@ module("Integration | Component | dialog-holder", function (hooks) { ".btn-primary element is not present in the dialog" ); }); - test("delete confirm with confirmation phase", async function (assert) { + + test("delete confirm with confirmation phrase component", async function (assert) { await render(hbs``); this.dialog.deleteConfirm({ - message: "A delete confirm message", - confirmPhrase: "test", + bodyComponent: SecondFactorConfirmPhrase, + confirmButtonDisabled: true, }); await settled(); assert.strictEqual(query(".btn-danger").disabled, true); - await fillIn("#confirm-phrase", "test"); + await fillIn("#confirm-phrase", "Disa"); + assert.strictEqual(query(".btn-danger").disabled, true); + await fillIn("#confirm-phrase", "Disable"); assert.strictEqual(query(".btn-danger").disabled, false); }); + + test("delete confirm with a component and model", async function (assert) { + await render(hbs``); + const message_count = 5; + + this.dialog.deleteConfirm({ + bodyComponent: GroupDeleteDialogMessage, + bodyComponentModel: { + message_count, + }, + }); + await settled(); + + assert.strictEqual( + query(".dialog-body").innerText.trim(), + I18n.t("admin.groups.delete_with_messages_confirm", { + count: message_count, + }), + "correct message is shown in dialog" + ); + }); });