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(`<li>Some text</li>`), }); ``` 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.
This commit is contained in:
parent
6e8e4430dd
commit
4072786f5b
|
@ -20,19 +20,14 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if (or this.dialog.message this.dialog.confirmPhrase)}}
|
{{#if (or this.dialog.message this.dialog.bodyComponent)}}
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
{{#if this.dialog.message}}
|
{{#if this.dialog.bodyComponent}}
|
||||||
<p>{{this.dialog.message}}</p>
|
<this.dialog.bodyComponent
|
||||||
{{/if}}
|
@model={{this.dialog.bodyComponentModel}}
|
||||||
{{#if this.dialog.confirmPhrase}}
|
|
||||||
<TextField
|
|
||||||
@value={{this.dialog.confirmPhraseInput}}
|
|
||||||
{{on "input" this.dialog.onConfirmPhraseInput}}
|
|
||||||
@id="confirm-phrase"
|
|
||||||
@autocorrect="off"
|
|
||||||
@autocapitalize="off"
|
|
||||||
/>
|
/>
|
||||||
|
{{else if this.dialog.message}}
|
||||||
|
<p>{{this.dialog.message}}</p>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
import Service from "@ember/service";
|
import Service from "@ember/service";
|
||||||
import A11yDialog from "a11y-dialog";
|
import A11yDialog from "a11y-dialog";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { isBlank } from "@ember/utils";
|
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
message: null,
|
|
||||||
type: null,
|
|
||||||
dialogInstance: null,
|
dialogInstance: null,
|
||||||
|
message: null,
|
||||||
title: null,
|
title: null,
|
||||||
titleElementId: null,
|
titleElementId: null,
|
||||||
|
type: null,
|
||||||
|
|
||||||
|
bodyComponent: null,
|
||||||
|
bodyComponentModel: null,
|
||||||
|
|
||||||
confirmButtonIcon: null,
|
confirmButtonIcon: null,
|
||||||
confirmButtonLabel: null,
|
confirmButtonLabel: null,
|
||||||
confirmButtonClass: null,
|
confirmButtonClass: null,
|
||||||
confirmPhrase: null,
|
confirmButtonDisabled: false,
|
||||||
confirmPhraseInput: null,
|
|
||||||
cancelButtonLabel: null,
|
cancelButtonLabel: null,
|
||||||
cancelButtonClass: null,
|
cancelButtonClass: null,
|
||||||
shouldDisplayCancel: null,
|
shouldDisplayCancel: null,
|
||||||
|
@ -29,15 +29,18 @@ export default Service.extend({
|
||||||
dialog(params) {
|
dialog(params) {
|
||||||
const {
|
const {
|
||||||
message,
|
message,
|
||||||
|
bodyComponent,
|
||||||
|
bodyComponentModel,
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
|
|
||||||
|
confirmButtonClass = "btn-primary",
|
||||||
confirmButtonIcon,
|
confirmButtonIcon,
|
||||||
confirmButtonLabel = "ok_value",
|
confirmButtonLabel = "ok_value",
|
||||||
confirmButtonClass = "btn-primary",
|
confirmButtonDisabled = false,
|
||||||
cancelButtonLabel = "cancel_value",
|
|
||||||
cancelButtonClass = "btn-default",
|
cancelButtonClass = "btn-default",
|
||||||
confirmPhrase,
|
cancelButtonLabel = "cancel_value",
|
||||||
shouldDisplayCancel,
|
shouldDisplayCancel,
|
||||||
|
|
||||||
didConfirm,
|
didConfirm,
|
||||||
|
@ -45,25 +48,25 @@ export default Service.extend({
|
||||||
buttons,
|
buttons,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let confirmButtonDisabled = !isBlank(confirmPhrase);
|
|
||||||
|
|
||||||
const element = document.getElementById("dialog-holder");
|
const element = document.getElementById("dialog-holder");
|
||||||
|
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
message,
|
message,
|
||||||
|
bodyComponent,
|
||||||
|
bodyComponentModel,
|
||||||
type,
|
type,
|
||||||
dialogInstance: new A11yDialog(element),
|
dialogInstance: new A11yDialog(element),
|
||||||
|
|
||||||
title,
|
title,
|
||||||
titleElementId: title !== null ? "dialog-title" : null,
|
titleElementId: title !== null ? "dialog-title" : null,
|
||||||
|
|
||||||
confirmButtonDisabled,
|
|
||||||
confirmButtonClass,
|
confirmButtonClass,
|
||||||
confirmButtonLabel,
|
confirmButtonDisabled,
|
||||||
confirmButtonIcon,
|
confirmButtonIcon,
|
||||||
confirmPhrase,
|
confirmButtonLabel,
|
||||||
cancelButtonLabel,
|
|
||||||
cancelButtonClass,
|
cancelButtonClass,
|
||||||
|
cancelButtonLabel,
|
||||||
shouldDisplayCancel,
|
shouldDisplayCancel,
|
||||||
|
|
||||||
didConfirm,
|
didConfirm,
|
||||||
|
@ -133,19 +136,21 @@ export default Service.extend({
|
||||||
reset() {
|
reset() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
message: null,
|
message: null,
|
||||||
|
bodyComponent: null,
|
||||||
|
bodyComponentModel: null,
|
||||||
type: null,
|
type: null,
|
||||||
dialogInstance: null,
|
dialogInstance: null,
|
||||||
|
|
||||||
title: null,
|
title: null,
|
||||||
titleElementId: null,
|
titleElementId: null,
|
||||||
|
|
||||||
confirmButtonLabel: null,
|
confirmButtonDisabled: false,
|
||||||
confirmButtonIcon: null,
|
confirmButtonIcon: null,
|
||||||
cancelButtonLabel: null,
|
confirmButtonLabel: null,
|
||||||
|
|
||||||
cancelButtonClass: null,
|
cancelButtonClass: null,
|
||||||
|
cancelButtonLabel: null,
|
||||||
shouldDisplayCancel: null,
|
shouldDisplayCancel: null,
|
||||||
confirmPhrase: null,
|
|
||||||
confirmPhraseInput: null,
|
|
||||||
|
|
||||||
didConfirm: null,
|
didConfirm: null,
|
||||||
didCancel: null,
|
didCancel: null,
|
||||||
|
@ -176,10 +181,7 @@ export default Service.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onConfirmPhraseInput() {
|
enableConfirmButton() {
|
||||||
this.set(
|
this.set("confirmButtonDisabled", false);
|
||||||
"confirmButtonDisabled",
|
|
||||||
this.confirmPhrase && this.confirmPhraseInput !== this.confirmPhrase
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{i18n "admin.groups.delete_with_messages_confirm" count=@model.message_count}}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{{i18n "user.second_factor.delete_confirm_header"}}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{#each @model.totps as |totp|}}
|
||||||
|
<li>{{totp.name}}</li>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#each @model.security_keys as |sk|}}
|
||||||
|
<li>{{sk.name}}</li>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#if this.currentUser.second_factor_backup_enabled}}
|
||||||
|
<li>{{i18n "user.second_factor_backup.title"}}</li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{html-safe
|
||||||
|
(i18n
|
||||||
|
"user.second_factor.delete_confirm_instruction"
|
||||||
|
confirm=this.disabledString
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
@value={{this.confirmPhraseInput}}
|
||||||
|
{{on "input" this.onConfirmPhraseInput}}
|
||||||
|
@id="confirm-phrase"
|
||||||
|
@autocorrect="off"
|
||||||
|
@autocapitalize="off"
|
||||||
|
/>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import I18n from "I18n";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { capitalize } from "@ember/string";
|
import { capitalize } from "@ember/string";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import GroupDeleteDialog from "discourse/components/dialog-messages/group-delete";
|
||||||
|
|
||||||
const Tab = EmberObject.extend({
|
const Tab = EmberObject.extend({
|
||||||
init() {
|
init() {
|
||||||
|
@ -138,17 +139,16 @@ export default Controller.extend({
|
||||||
|
|
||||||
const model = this.model;
|
const model = this.model;
|
||||||
const title = I18n.t("admin.groups.delete_confirm");
|
const title = I18n.t("admin.groups.delete_confirm");
|
||||||
let message = null;
|
let bodyComponent = null;
|
||||||
|
|
||||||
if (model.has_messages && model.message_count > 0) {
|
if (model.has_messages && model.message_count > 0) {
|
||||||
message = I18n.t("admin.groups.delete_with_messages_confirm", {
|
bodyComponent = GroupDeleteDialog;
|
||||||
count: model.message_count,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dialog.deleteConfirm({
|
this.dialog.deleteConfirm({
|
||||||
title,
|
title,
|
||||||
message,
|
bodyComponent,
|
||||||
|
bodyComponentModel: model,
|
||||||
didConfirm: () => {
|
didConfirm: () => {
|
||||||
model
|
model
|
||||||
.destroy()
|
.destroy()
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { findAll } from "discourse/models/login-method";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import { inject as service } from "@ember/service";
|
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, {
|
export default Controller.extend(CanCheckEmails, {
|
||||||
dialog: service(),
|
dialog: service(),
|
||||||
|
@ -114,29 +114,6 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
.finally(() => this.set("resetPasswordLoading", false));
|
.finally(() => this.set("resetPasswordLoading", false));
|
||||||
},
|
},
|
||||||
|
|
||||||
disableAllMessage() {
|
|
||||||
let templateElements = [I18n.t("user.second_factor.delete_confirm_header")];
|
|
||||||
templateElements.push("<ul>");
|
|
||||||
this.totps.forEach((totp) => {
|
|
||||||
templateElements.push(`<li>${totp.name}</li>`);
|
|
||||||
});
|
|
||||||
this.security_keys.forEach((key) => {
|
|
||||||
templateElements.push(`<li>${key.name}</li>`);
|
|
||||||
});
|
|
||||||
if (this.currentUser.second_factor_backup_enabled) {
|
|
||||||
templateElements.push(
|
|
||||||
`<li>${I18n.t("user.second_factor_backup.title")}</li>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
templateElements.push("</ul>");
|
|
||||||
templateElements.push(
|
|
||||||
I18n.t("user.second_factor.delete_confirm_instruction", {
|
|
||||||
confirm: I18n.t("user.second_factor.disable"),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return htmlSafe(templateElements.join(""));
|
|
||||||
},
|
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
confirmPassword() {
|
confirmPassword() {
|
||||||
if (!this.password) {
|
if (!this.password) {
|
||||||
|
@ -154,9 +131,13 @@ export default Controller.extend(CanCheckEmails, {
|
||||||
|
|
||||||
this.dialog.deleteConfirm({
|
this.dialog.deleteConfirm({
|
||||||
title: I18n.t("user.second_factor.disable_confirm"),
|
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",
|
confirmButtonLabel: "user.second_factor.disable",
|
||||||
confirmPhrase: I18n.t("user.second_factor.disable"),
|
confirmButtonDisabled: true,
|
||||||
confirmButtonIcon: "ban",
|
confirmButtonIcon: "ban",
|
||||||
cancelButtonClass: "btn-flat",
|
cancelButtonClass: "btn-flat",
|
||||||
didConfirm: () => {
|
didConfirm: () => {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import {
|
||||||
} from "@ember/test-helpers";
|
} from "@ember/test-helpers";
|
||||||
import { hbs } from "ember-cli-htmlbars";
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
import { query } from "discourse/tests/helpers/qunit-helpers";
|
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) {
|
module("Integration | Component | dialog-holder", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
@ -394,17 +396,41 @@ module("Integration | Component | dialog-holder", function (hooks) {
|
||||||
".btn-primary element is not present in the dialog"
|
".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`<DialogHolder />`);
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
this.dialog.deleteConfirm({
|
this.dialog.deleteConfirm({
|
||||||
message: "A delete confirm message",
|
bodyComponent: SecondFactorConfirmPhrase,
|
||||||
confirmPhrase: "test",
|
confirmButtonDisabled: true,
|
||||||
});
|
});
|
||||||
await settled();
|
await settled();
|
||||||
|
|
||||||
assert.strictEqual(query(".btn-danger").disabled, true);
|
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);
|
assert.strictEqual(query(".btn-danger").disabled, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("delete confirm with a component and model", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue