mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 23:31:18 +08:00
DEV: Add A11Y-friendly dialog addon (#18028)
This adds a new framework for accessible dialogs that will eventually replace bootbox. Under the hood, it uses the a11y-dialog package and an in-repo Ember addon. See PR for usage details.
This commit is contained in:
@ -1,11 +1,15 @@
|
|||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import { empty } from "@ember/object/computed";
|
import { empty } from "@ember/object/computed";
|
||||||
import { observes } from "discourse-common/utils/decorators";
|
import { observes } from "discourse-common/utils/decorators";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { htmlSafe } from "@ember/template";
|
||||||
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
|
dialog: service(),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Is the "send test email" button disabled?
|
Is the "send test email" button disabled?
|
||||||
|
|
||||||
@ -44,13 +48,17 @@ export default Controller.extend({
|
|||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e.jqXHR.responseJSON?.errors) {
|
if (e.jqXHR.responseJSON?.errors) {
|
||||||
bootbox.alert(
|
this.dialog.alert({
|
||||||
I18n.t("admin.email.error", {
|
message: htmlSafe(
|
||||||
server_error: e.jqXHR.responseJSON.errors[0],
|
I18n.t("admin.email.error", {
|
||||||
})
|
server_error: escapeExpression(
|
||||||
);
|
e.jqXHR.responseJSON.errors[0]
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
bootbox.alert(I18n.t("admin.email.test_error"));
|
this.dialog.alert({ message: I18n.t("admin.email.test_error") });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => this.set("sendingEmail", false));
|
.finally(() => this.set("sendingEmail", false));
|
||||||
|
@ -2,12 +2,13 @@ import EmberObject, { action, computed } from "@ember/object";
|
|||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import { sort } from "@ember/object/computed";
|
import { sort } from "@ember/object/computed";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
const ALL_FILTER = "all";
|
const ALL_FILTER = "all";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
|
dialog: service(),
|
||||||
filter: null,
|
filter: null,
|
||||||
sorting: null,
|
sorting: null,
|
||||||
|
|
||||||
@ -72,19 +73,17 @@ export default Controller.extend({
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
destroyEmoji(emoji) {
|
destroyEmoji(emoji) {
|
||||||
return bootbox.confirm(
|
this.dialog.yesNoConfirm({
|
||||||
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
|
message: I18n.t("admin.emoji.delete_confirm", {
|
||||||
I18n.t("no_value"),
|
name: emoji.get("name"),
|
||||||
I18n.t("yes_value"),
|
}),
|
||||||
(destroy) => {
|
didConfirm: () => {
|
||||||
if (destroy) {
|
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
|
||||||
return ajax("/admin/customize/emojis/" + emoji.get("name"), {
|
type: "DELETE",
|
||||||
type: "DELETE",
|
}).then(() => {
|
||||||
}).then(() => {
|
this.model.removeObject(emoji);
|
||||||
this.model.removeObject(emoji);
|
});
|
||||||
});
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default Controller.extend(bufferedProperty("siteText"), {
|
export default Controller.extend(bufferedProperty("siteText"), {
|
||||||
|
dialog: service(),
|
||||||
saved: false,
|
saved: false,
|
||||||
queryParams: ["locale"],
|
queryParams: ["locale"],
|
||||||
|
|
||||||
@ -14,35 +16,36 @@ export default Controller.extend(bufferedProperty("siteText"), {
|
|||||||
return this.siteText.value === value;
|
return this.siteText.value === value;
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
@action
|
||||||
saveChanges() {
|
saveChanges() {
|
||||||
const attrs = this.buffered.getProperties("value");
|
const attrs = this.buffered.getProperties("value");
|
||||||
attrs.locale = this.locale;
|
attrs.locale = this.locale;
|
||||||
|
|
||||||
this.siteText
|
this.siteText
|
||||||
.save(attrs)
|
.save(attrs)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.commitBuffer();
|
this.commitBuffer();
|
||||||
this.set("saved", true);
|
this.set("saved", true);
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
|
|
||||||
revertChanges() {
|
@action
|
||||||
this.set("saved", false);
|
revertChanges() {
|
||||||
|
this.set("saved", false);
|
||||||
|
|
||||||
bootbox.confirm(I18n.t("admin.site_text.revert_confirm"), (result) => {
|
this.dialog.yesNoConfirm({
|
||||||
if (result) {
|
message: I18n.t("admin.site_text.revert_confirm"),
|
||||||
this.siteText
|
didConfirm: () => {
|
||||||
.revert(this.locale)
|
this.siteText
|
||||||
.then((props) => {
|
.revert(this.locale)
|
||||||
const buffered = this.buffered;
|
.then((props) => {
|
||||||
buffered.setProperties(props);
|
const buffered = this.buffered;
|
||||||
this.commitBuffer();
|
buffered.setProperties(props);
|
||||||
})
|
this.commitBuffer();
|
||||||
.catch(popupAjaxError);
|
})
|
||||||
}
|
.catch(popupAjaxError);
|
||||||
});
|
},
|
||||||
},
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -6,17 +6,16 @@ import CanCheckEmails from "discourse/mixins/can-check-emails";
|
|||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import discourseComputed from "discourse-common/utils/decorators";
|
import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
|
||||||
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
|
import { extractError, popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
|
||||||
export default Controller.extend(CanCheckEmails, {
|
export default Controller.extend(CanCheckEmails, {
|
||||||
router: service(),
|
router: service(),
|
||||||
|
dialog: service(),
|
||||||
adminTools: service(),
|
adminTools: service(),
|
||||||
originalPrimaryGroupId: null,
|
originalPrimaryGroupId: null,
|
||||||
customGroupIdsBuffer: null,
|
customGroupIdsBuffer: null,
|
||||||
@ -130,7 +129,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
groupAdded(added) {
|
groupAdded(added) {
|
||||||
this.model
|
this.model
|
||||||
.groupAdded(added)
|
.groupAdded(added)
|
||||||
.catch(() => bootbox.alert(I18n.t("generic_error")));
|
.catch(() => this.dialog.alert(I18n.t("generic_error")));
|
||||||
},
|
},
|
||||||
|
|
||||||
groupRemoved(groupId) {
|
groupRemoved(groupId) {
|
||||||
@ -141,7 +140,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
this.set("originalPrimaryGroupId", null);
|
this.set("originalPrimaryGroupId", null);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => bootbox.alert(I18n.t("generic_error")));
|
.catch(() => this.dialog.alert(I18n.t("generic_error")));
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("ssoLastPayload")
|
@discourseComputed("ssoLastPayload")
|
||||||
@ -156,16 +155,16 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
.then(() => DiscourseURL.redirectTo("/"))
|
.then(() => DiscourseURL.redirectTo("/"))
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e.status === 404) {
|
if (e.status === 404) {
|
||||||
bootbox.alert(I18n.t("admin.impersonate.not_found"));
|
this.dialog.alert(I18n.t("admin.impersonate.not_found"));
|
||||||
} else {
|
} else {
|
||||||
bootbox.alert(I18n.t("admin.impersonate.invalid"));
|
this.dialog.alert(I18n.t("admin.impersonate.invalid"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logOut() {
|
logOut() {
|
||||||
return this.model
|
return this.model
|
||||||
.logOut()
|
.logOut()
|
||||||
.then(() => bootbox.alert(I18n.t("admin.user.logged_out")));
|
.then(() => this.dialog.alert(I18n.t("admin.user.logged_out")));
|
||||||
},
|
},
|
||||||
resetBounceScore() {
|
resetBounceScore() {
|
||||||
return this.model.resetBounceScore();
|
return this.model.resetBounceScore();
|
||||||
@ -188,13 +187,15 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
const error = I18n.t("admin.user.deactivate_failed", {
|
const error = I18n.t("admin.user.deactivate_failed", {
|
||||||
error: this._formatError(e),
|
error: this._formatError(e),
|
||||||
});
|
});
|
||||||
bootbox.alert(error);
|
this.dialog.alert(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendActivationEmail() {
|
sendActivationEmail() {
|
||||||
return this.model
|
return this.model
|
||||||
.sendActivationEmail()
|
.sendActivationEmail()
|
||||||
.then(() => bootbox.alert(I18n.t("admin.user.activation_email_sent")))
|
.then(() =>
|
||||||
|
this.dialog.alert(I18n.t("admin.user.activation_email_sent"))
|
||||||
|
)
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
},
|
},
|
||||||
activate() {
|
activate() {
|
||||||
@ -210,7 +211,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
const error = I18n.t("admin.user.activate_failed", {
|
const error = I18n.t("admin.user.activate_failed", {
|
||||||
error: this._formatError(e),
|
error: this._formatError(e),
|
||||||
});
|
});
|
||||||
bootbox.alert(error);
|
this.dialog.alert(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
revokeAdmin() {
|
revokeAdmin() {
|
||||||
@ -221,7 +222,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
.grantAdmin()
|
.grantAdmin()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (result.email_confirmation_required) {
|
if (result.email_confirmation_required) {
|
||||||
bootbox.alert(I18n.t("admin.user.grant_admin_confirm"));
|
this.dialog.alert(I18n.t("admin.user.grant_admin_confirm"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -255,7 +256,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
I18n.t("admin.user.trust_level_change_failed", {
|
I18n.t("admin.user.trust_level_change_failed", {
|
||||||
error: this._formatError(e),
|
error: this._formatError(e),
|
||||||
});
|
});
|
||||||
bootbox.alert(error);
|
this.dialog.alert(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
restoreTrustLevel() {
|
restoreTrustLevel() {
|
||||||
@ -275,7 +276,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
I18n.t("admin.user.trust_level_change_failed", {
|
I18n.t("admin.user.trust_level_change_failed", {
|
||||||
error: this._formatError(e),
|
error: this._formatError(e),
|
||||||
});
|
});
|
||||||
bootbox.alert(error);
|
this.dialog.alert(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
unsilence() {
|
unsilence() {
|
||||||
@ -287,7 +288,6 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
|
|
||||||
anonymize() {
|
anonymize() {
|
||||||
const user = this.model;
|
const user = this.model;
|
||||||
const message = I18n.t("admin.user.anonymize_confirm");
|
|
||||||
|
|
||||||
const performAnonymize = () => {
|
const performAnonymize = () => {
|
||||||
this.model
|
this.model
|
||||||
@ -302,31 +302,32 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
document.location = getURL("/admin/users/list/active");
|
document.location = getURL("/admin/users/list/active");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bootbox.alert(I18n.t("admin.user.anonymize_failed"));
|
this.dialog.alert(I18n.t("admin.user.anonymize_failed"));
|
||||||
if (data.user) {
|
if (data.user) {
|
||||||
user.setProperties(data.user);
|
user.setProperties(data.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => bootbox.alert(I18n.t("admin.user.anonymize_failed")));
|
.catch(() =>
|
||||||
|
this.dialog.alert(I18n.t("admin.user.anonymize_failed"))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const buttons = [
|
|
||||||
{
|
|
||||||
label: I18n.t("composer.cancel"),
|
|
||||||
class: "cancel",
|
|
||||||
link: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: I18n.t("admin.user.anonymize_yes"),
|
|
||||||
class: "btn btn-danger",
|
|
||||||
icon: iconHTML("exclamation-triangle"),
|
|
||||||
callback: () => {
|
|
||||||
performAnonymize();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
|
this.dialog.alert({
|
||||||
|
message: I18n.t("admin.user.anonymize_confirm"),
|
||||||
|
class: "delete-user-modal",
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
icon: "exclamation-triangle",
|
||||||
|
label: I18n.t("admin.user.anonymize_yes"),
|
||||||
|
class: "btn-danger",
|
||||||
|
action: () => performAnonymize(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: I18n.t("composer.cancel"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
disableSecondFactor() {
|
disableSecondFactor() {
|
||||||
@ -345,11 +346,10 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
destroy() {
|
destroy() {
|
||||||
const postCount = this.get("model.post_count");
|
const postCount = this.get("model.post_count");
|
||||||
const maxPostCount = this.siteSettings.delete_all_posts_max;
|
const maxPostCount = this.siteSettings.delete_all_posts_max;
|
||||||
const message = I18n.t("admin.user.delete_confirm");
|
|
||||||
const location = document.location.pathname;
|
const location = document.location.pathname;
|
||||||
|
|
||||||
const performDestroy = (block) => {
|
const performDestroy = (block) => {
|
||||||
bootbox.dialog(I18n.t("admin.user.deleting_user"));
|
this.dialog.notice(I18n.t("admin.user.deleting_user"));
|
||||||
let formData = { context: location };
|
let formData = { context: location };
|
||||||
if (block) {
|
if (block) {
|
||||||
formData["block_email"] = true;
|
formData["block_email"] = true;
|
||||||
@ -369,38 +369,38 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
document.location = getURL("/admin/users/list/active");
|
document.location = getURL("/admin/users/list/active");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bootbox.alert(I18n.t("admin.user.delete_failed"));
|
this.dialog.alert(I18n.t("admin.user.delete_failed"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
bootbox.alert(I18n.t("admin.user.delete_failed"));
|
this.dialog.alert(I18n.t("admin.user.delete_failed"));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttons = [
|
this.dialog.alert({
|
||||||
{
|
message: I18n.t("admin.user.delete_confirm"),
|
||||||
label: I18n.t("composer.cancel"),
|
class: "delete-user-modal",
|
||||||
class: "btn",
|
buttons: [
|
||||||
link: true,
|
{
|
||||||
},
|
label: I18n.t("admin.user.delete_dont_block"),
|
||||||
{
|
class: "btn-primary",
|
||||||
icon: iconHTML("exclamation-triangle"),
|
action: () => {
|
||||||
label: I18n.t("admin.user.delete_and_block"),
|
return performDestroy(true);
|
||||||
class: "btn btn-danger",
|
},
|
||||||
callback: () => {
|
|
||||||
performDestroy(true);
|
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
icon: "exclamation-triangle",
|
||||||
label: I18n.t("admin.user.delete_dont_block"),
|
label: I18n.t("admin.user.delete_and_block"),
|
||||||
class: "btn btn-primary",
|
class: "btn-danger",
|
||||||
callback: () => {
|
action: () => {
|
||||||
performDestroy(false);
|
return performDestroy(false);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
];
|
label: I18n.t("composer.cancel"),
|
||||||
|
},
|
||||||
bootbox.dialog(message, buttons, { classes: "delete-user-modal" });
|
],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
promptTargetUser() {
|
promptTargetUser() {
|
||||||
@ -439,12 +439,12 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
model: this.model,
|
model: this.model,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
this.dialog.alert(I18n.t("admin.user.merge_failed"));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
AdminUser.find(user.id).then((u) => user.setProperties(u));
|
AdminUser.find(user.id).then((u) => user.setProperties(u));
|
||||||
bootbox.alert(I18n.t("admin.user.merge_failed"));
|
this.dialog.alert(I18n.t("admin.user.merge_failed"));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -532,7 +532,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
data: { primary_group_id: primaryGroupId },
|
data: { primary_group_id: primaryGroupId },
|
||||||
})
|
})
|
||||||
.then(() => this.set("originalPrimaryGroupId", primaryGroupId))
|
.then(() => this.set("originalPrimaryGroupId", primaryGroupId))
|
||||||
.catch(() => bootbox.alert(I18n.t("generic_error")));
|
.catch(() => this.dialog.alert(I18n.t("generic_error")));
|
||||||
},
|
},
|
||||||
|
|
||||||
resetPrimaryGroup() {
|
resetPrimaryGroup() {
|
||||||
@ -540,16 +540,10 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteSSORecord() {
|
deleteSSORecord() {
|
||||||
return bootbox.confirm(
|
return this.dialog.yesNoConfirm({
|
||||||
I18n.t("admin.user.discourse_connect.confirm_delete"),
|
message: I18n.t("admin.user.discourse_connect.confirm_delete"),
|
||||||
I18n.t("no_value"),
|
didConfirm: () => this.model.deleteSSORecord(),
|
||||||
I18n.t("yes_value"),
|
});
|
||||||
(confirmed) => {
|
|
||||||
if (confirmed) {
|
|
||||||
return this.model.deleteSSORecord();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
checkSsoEmail() {
|
checkSsoEmail() {
|
||||||
@ -607,7 +601,7 @@ export default Controller.extend(CanCheckEmails, {
|
|||||||
let error;
|
let error;
|
||||||
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
AdminUser.find(user.get("id")).then((u) => user.setProperties(u));
|
||||||
error = extractError(e) || I18n.t("admin.user.delete_posts_failed");
|
error = extractError(e) || I18n.t("admin.user.delete_posts_failed");
|
||||||
bootbox.alert(error);
|
this.dialog.alert(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import logout from "discourse/lib/logout";
|
import logout from "discourse/lib/logout";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { setLogoffCallback } from "discourse/lib/ajax";
|
import { setLogoffCallback } from "discourse/lib/ajax";
|
||||||
@ -14,6 +13,7 @@ export function addPluginDocumentTitleCounter(counterFunction) {
|
|||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
tagName: "",
|
tagName: "",
|
||||||
documentTitle: service(),
|
documentTitle: service(),
|
||||||
|
dialog: service(),
|
||||||
_showingLogout: false,
|
_showingLogout: false,
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
@ -74,13 +74,12 @@ export default Component.extend({
|
|||||||
|
|
||||||
this._showingLogout = true;
|
this._showingLogout = true;
|
||||||
this.messageBus.stop();
|
this.messageBus.stop();
|
||||||
bootbox.dialog(
|
|
||||||
I18n.t("logout"),
|
this.dialog.alert({
|
||||||
{ label: I18n.t("refresh"), callback: logout },
|
message: I18n.t("logout"),
|
||||||
{
|
confirmButtonLabel: "refresh",
|
||||||
onEscape: () => logout(),
|
didConfirm: () => logout(),
|
||||||
backdrop: "static",
|
didCancel: () => logout(),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import Controller, { inject as controller } from "@ember/controller";
|
import Controller, { inject as controller } from "@ember/controller";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { alias } from "@ember/object/computed";
|
import { alias } from "@ember/object/computed";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import { exportUserArchive } from "discourse/lib/export-csv";
|
import { exportUserArchive } from "discourse/lib/export-csv";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
|
dialog: service(),
|
||||||
application: controller(),
|
application: controller(),
|
||||||
user: controller(),
|
user: controller(),
|
||||||
userActionType: null,
|
userActionType: null,
|
||||||
@ -44,12 +45,10 @@ export default Controller.extend({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
exportUserArchive() {
|
exportUserArchive() {
|
||||||
bootbox.confirm(
|
this.dialog.yesNoConfirm({
|
||||||
I18n.t("user.download_archive.confirm"),
|
message: I18n.t("user.download_archive.confirm"),
|
||||||
I18n.t("no_value"),
|
didConfirm: () => exportUserArchive(),
|
||||||
I18n.t("yes_value"),
|
});
|
||||||
(confirmed) => (confirmed ? exportUserArchive() : null)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import bootbox from "bootbox";
|
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
|
|
||||||
function exportEntityByType(type, entity, args) {
|
function exportEntityByType(type, entity, args) {
|
||||||
return ajax("/export_csv/export_entity.json", {
|
return ajax("/export_csv/export_entity.json", {
|
||||||
@ -11,9 +11,10 @@ function exportEntityByType(type, entity, args) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function exportUserArchive() {
|
export function exportUserArchive() {
|
||||||
|
const dialog = getOwner(this).lookup("service:dialog");
|
||||||
return exportEntityByType("user", "user_archive")
|
return exportEntityByType("user", "user_archive")
|
||||||
.then(function () {
|
.then(function () {
|
||||||
bootbox.alert(I18n.t("user.download_archive.success"));
|
dialog.alert(I18n.t("user.download_archive.success"));
|
||||||
})
|
})
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
<PluginOutlet @name="below-footer" @connectorTagName="div" @args={{hash showFooter=this.showFooter}} />
|
<PluginOutlet @name="below-footer" @connectorTagName="div" @args={{hash showFooter=this.showFooter}} />
|
||||||
|
|
||||||
{{outlet "modal"}}
|
{{outlet "modal"}}
|
||||||
|
<DialogHolder />
|
||||||
<TopicEntrance />
|
<TopicEntrance />
|
||||||
{{outlet "composer"}}
|
{{outlet "composer"}}
|
||||||
|
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
<div id="dialog-holder" class="dialog-container {{this.dialog.class}}" aria-labelledby={{this.dialog.titleElementId}} aria-hidden="true">
|
||||||
|
<div class="dialog-overlay" data-a11y-dialog-hide></div>
|
||||||
|
|
||||||
|
{{#if this.dialog.type}}
|
||||||
|
<div class="dialog-content" role="document">
|
||||||
|
{{#if this.dialog.title}}
|
||||||
|
<div class="dialog-header">
|
||||||
|
<h3 id={{this.dialog.titleElementId}}>{{this.dialog.title}}</h3>
|
||||||
|
<DButton @icon="times" @action={{action this.dialog.cancel}} @class="btn-flat dialog-close close" @title="modal.close" />
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.dialog.message}}
|
||||||
|
<div class="dialog-body">
|
||||||
|
{{this.dialog.message}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if (notEq this.dialog.type "notice")}}
|
||||||
|
<div class="dialog-footer">
|
||||||
|
{{#each this.dialog.buttons as |button|}}
|
||||||
|
<DButton @icon={{button.icon}} @class={{button.class}} @action={{action "handleButtonAction" button}} @translatedLabel={{button.label}} />
|
||||||
|
{{else}}
|
||||||
|
<DButton @class="btn-primary" @action={{this.dialog.didConfirmWrapped}} @icon={{this.dialog.confirmButtonIcon}} @label={{this.dialog.confirmButtonLabel}} />
|
||||||
|
{{#if this.dialog.shouldDisplayCancel}}
|
||||||
|
<DButton @class="btn-default" @action={{this.dialog.cancel}} @label={{this.dialog.cancelButtonLabel}} />
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
@ -0,0 +1,16 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
|
export default class DialogHolder extends Component {
|
||||||
|
@service dialog;
|
||||||
|
|
||||||
|
@action
|
||||||
|
async handleButtonAction(btn) {
|
||||||
|
if (btn.action && typeof btn.action === "function") {
|
||||||
|
await btn.action();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialog.cancel();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
import Service from "@ember/service";
|
||||||
|
import A11yDialog from "a11y-dialog";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
export default Service.extend({
|
||||||
|
message: null,
|
||||||
|
type: null,
|
||||||
|
dialogInstance: null,
|
||||||
|
|
||||||
|
title: null,
|
||||||
|
titleElementId: null,
|
||||||
|
|
||||||
|
confirmButtonIcon: null,
|
||||||
|
confirmButtonLabel: null,
|
||||||
|
cancelButtonLabel: null,
|
||||||
|
shouldDisplayCancel: null,
|
||||||
|
|
||||||
|
didConfirm: null,
|
||||||
|
didCancel: null,
|
||||||
|
buttons: null,
|
||||||
|
class: null,
|
||||||
|
_confirming: false,
|
||||||
|
|
||||||
|
dialog(params) {
|
||||||
|
const {
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
|
||||||
|
confirmButtonIcon,
|
||||||
|
confirmButtonLabel = "ok_value",
|
||||||
|
cancelButtonLabel = "cancel_value",
|
||||||
|
shouldDisplayCancel,
|
||||||
|
|
||||||
|
didConfirm,
|
||||||
|
didCancel,
|
||||||
|
buttons,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const element = document.getElementById("dialog-holder");
|
||||||
|
|
||||||
|
this.setProperties({
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
dialogInstance: new A11yDialog(element),
|
||||||
|
|
||||||
|
title,
|
||||||
|
titleElementId: title !== null ? "dialog-title" : null,
|
||||||
|
|
||||||
|
confirmButtonLabel,
|
||||||
|
confirmButtonIcon,
|
||||||
|
cancelButtonLabel,
|
||||||
|
shouldDisplayCancel,
|
||||||
|
|
||||||
|
didConfirm,
|
||||||
|
didCancel,
|
||||||
|
buttons,
|
||||||
|
class: params.class,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dialogInstance.show();
|
||||||
|
|
||||||
|
this.dialogInstance.on("hide", () => {
|
||||||
|
if (!this._confirming && this.didCancel) {
|
||||||
|
this.didCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
alert(params) {
|
||||||
|
// support string param for easier porting of bootbox.alert
|
||||||
|
if (typeof params === "string") {
|
||||||
|
return this.dialog({
|
||||||
|
message: params,
|
||||||
|
type: "alert",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.dialog({
|
||||||
|
...params,
|
||||||
|
type: "alert",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm(params) {
|
||||||
|
return this.dialog({
|
||||||
|
...params,
|
||||||
|
shouldDisplayCancel: true,
|
||||||
|
buttons: null,
|
||||||
|
type: "confirm",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
notice(message) {
|
||||||
|
return this.dialog({
|
||||||
|
message,
|
||||||
|
type: "notice",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
yesNoConfirm(params) {
|
||||||
|
return this.confirm({
|
||||||
|
...params,
|
||||||
|
confirmButtonLabel: "yes_value",
|
||||||
|
cancelButtonLabel: "no_value",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.setProperties({
|
||||||
|
message: null,
|
||||||
|
type: null,
|
||||||
|
dialogInstance: null,
|
||||||
|
|
||||||
|
title: null,
|
||||||
|
titleElementId: null,
|
||||||
|
|
||||||
|
confirmButtonLabel: null,
|
||||||
|
confirmButtonIcon: null,
|
||||||
|
cancelButtonLabel: null,
|
||||||
|
shouldDisplayCancel: null,
|
||||||
|
|
||||||
|
didConfirm: null,
|
||||||
|
didCancel: null,
|
||||||
|
buttons: null,
|
||||||
|
class: null,
|
||||||
|
|
||||||
|
_confirming: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this.dialogInstance?.destroy();
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
@bind
|
||||||
|
didConfirmWrapped() {
|
||||||
|
if (this.didConfirm) {
|
||||||
|
this.didConfirm();
|
||||||
|
}
|
||||||
|
this._confirming = true;
|
||||||
|
this.dialogInstance.hide();
|
||||||
|
},
|
||||||
|
|
||||||
|
@bind
|
||||||
|
cancel() {
|
||||||
|
this.dialogInstance.hide();
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1 @@
|
|||||||
|
export { default } from "dialog-holder/components/dialog-holder";
|
@ -0,0 +1,9 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: require("./package").name,
|
||||||
|
|
||||||
|
isDevelopingAddon() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "dialog-holder",
|
||||||
|
"keywords": [
|
||||||
|
"ember-addon"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"a11y-dialog": "7.5.0",
|
||||||
|
"ember-auto-import": "^2.4.2",
|
||||||
|
"ember-cli-babel": "^7.26.10",
|
||||||
|
"ember-cli-htmlbars": "^6.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.73.0"
|
||||||
|
}
|
||||||
|
}
|
3659
app/assets/javascripts/discourse/lib/dialog-holder/yarn.lock
Normal file
3659
app/assets/javascripts/discourse/lib/dialog-holder/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,7 @@
|
|||||||
"@discourse/itsatrap": "^2.0.10",
|
"@discourse/itsatrap": "^2.0.10",
|
||||||
"@ember/jquery": "^2.0.0",
|
"@ember/jquery": "^2.0.0",
|
||||||
"@ember/optional-features": "^2.0.0",
|
"@ember/optional-features": "^2.0.0",
|
||||||
|
"@ember/render-modifiers": "^2.0.4",
|
||||||
"@ember/test-helpers": "^2.8.1",
|
"@ember/test-helpers": "^2.8.1",
|
||||||
"@glimmer/component": "^1.1.2",
|
"@glimmer/component": "^1.1.2",
|
||||||
"@glimmer/syntax": "^0.84.2",
|
"@glimmer/syntax": "^0.84.2",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"@uppy/drop-target": "^1.1.3",
|
"@uppy/drop-target": "^1.1.3",
|
||||||
"@uppy/utils": "^4.1.0",
|
"@uppy/utils": "^4.1.0",
|
||||||
"@uppy/xhr-upload": "^2.1.2",
|
"@uppy/xhr-upload": "^2.1.2",
|
||||||
|
"a11y-dialog": "7.5.0",
|
||||||
"admin": "^1.0.0",
|
"admin": "^1.0.0",
|
||||||
"discourse-plugins": "^1.0.0",
|
"discourse-plugins": "^1.0.0",
|
||||||
"bootstrap": "3.4.1",
|
"bootstrap": "3.4.1",
|
||||||
@ -58,13 +60,12 @@
|
|||||||
"ember-exam": "^7.0.1",
|
"ember-exam": "^7.0.1",
|
||||||
"ember-export-application-global": "^2.0.1",
|
"ember-export-application-global": "^2.0.1",
|
||||||
"ember-load-initializers": "^2.1.1",
|
"ember-load-initializers": "^2.1.1",
|
||||||
|
"ember-modifier": "^3.2.7",
|
||||||
|
"ember-on-resize-modifier": "^1.1.0",
|
||||||
"ember-qunit": "^5.1.5",
|
"ember-qunit": "^5.1.5",
|
||||||
"ember-rfc176-data": "^0.3.17",
|
"ember-rfc176-data": "^0.3.17",
|
||||||
"ember-source": "~3.28.8",
|
"ember-source": "~3.28.8",
|
||||||
"ember-test-selectors": "^6.0.0",
|
"ember-test-selectors": "^6.0.0",
|
||||||
"ember-modifier": "^3.2.7",
|
|
||||||
"ember-on-resize-modifier": "^1.1.0",
|
|
||||||
"@ember/render-modifiers": "^2.0.4",
|
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-qunit": "^6.2.0",
|
"eslint-plugin-qunit": "^6.2.0",
|
||||||
"html-entities": "^2.3.3",
|
"html-entities": "^2.3.3",
|
||||||
@ -96,6 +97,7 @@
|
|||||||
},
|
},
|
||||||
"ember-addon": {
|
"ember-addon": {
|
||||||
"paths": [
|
"paths": [
|
||||||
|
"lib/dialog-holder",
|
||||||
"lib/bootstrap-json"
|
"lib/bootstrap-json"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -51,7 +51,11 @@ acceptance("Admin - Emails", function (needs) {
|
|||||||
await fillIn(".admin-controls input", "test@example.com");
|
await fillIn(".admin-controls input", "test@example.com");
|
||||||
await click(".btn-primary");
|
await click(".btn-primary");
|
||||||
|
|
||||||
assert.ok(query(".bootbox.modal").innerText.includes("some error"));
|
assert.ok(query("#dialog-holder").innerText.includes("some error"));
|
||||||
await click(".bootbox .btn-primary");
|
assert.ok(
|
||||||
|
query("#dialog-holder .dialog-body b"),
|
||||||
|
"Error message can contain html"
|
||||||
|
);
|
||||||
|
await click(".dialog-overlay");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -51,9 +51,9 @@ acceptance("Admin - Site Texts", function (needs) {
|
|||||||
// Revert the changes
|
// Revert the changes
|
||||||
await click(".revert-site-text");
|
await click(".revert-site-text");
|
||||||
|
|
||||||
assert.ok(exists(".bootbox.modal"));
|
assert.ok(exists("#dialog-holder .dialog-content"));
|
||||||
|
|
||||||
await click(".bootbox.modal .btn-primary");
|
await click("#dialog-holder .btn-primary");
|
||||||
|
|
||||||
assert.ok(!exists(".saved"));
|
assert.ok(!exists(".saved"));
|
||||||
assert.ok(!exists(".revert-site-text"));
|
assert.ok(!exists(".revert-site-text"));
|
||||||
|
@ -195,12 +195,13 @@ acceptance("Admin - User Index", function (needs) {
|
|||||||
test("grant admin - shows the confirmation bootbox", async function (assert) {
|
test("grant admin - shows the confirmation bootbox", async function (assert) {
|
||||||
await visit("/admin/users/3/user1");
|
await visit("/admin/users/3/user1");
|
||||||
await click(".grant-admin");
|
await click(".grant-admin");
|
||||||
assert.ok(exists(".bootbox"));
|
assert.ok(exists(".dialog-content"));
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
I18n.t("admin.user.grant_admin_confirm"),
|
I18n.t("admin.user.grant_admin_confirm"),
|
||||||
query(".modal-body").textContent.trim()
|
query(".dialog-body").textContent.trim()
|
||||||
);
|
);
|
||||||
await click(".bootbox .btn-primary");
|
|
||||||
|
await click(".dialog-footer .btn-primary");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("grant admin - redirects to the 2fa page", async function (assert) {
|
test("grant admin - redirects to the 2fa page", async function (assert) {
|
||||||
|
@ -317,7 +317,10 @@ acceptance("Composer", function (needs) {
|
|||||||
await fillIn(".d-editor-input", "this is the content of the first reply");
|
await fillIn(".d-editor-input", "this is the content of the first reply");
|
||||||
|
|
||||||
await visit("/t/this-is-a-test-topic/9");
|
await visit("/t/this-is-a-test-topic/9");
|
||||||
assert.strictEqual(currentURL(), "/t/this-is-a-test-topic/9");
|
assert.ok(
|
||||||
|
currentURL().startsWith("/t/this-is-a-test-topic/9"),
|
||||||
|
"moves to second topic"
|
||||||
|
);
|
||||||
await click("#topic-footer-buttons .btn.create");
|
await click("#topic-footer-buttons .btn.create");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
exists(".discard-draft-modal.modal"),
|
exists(".discard-draft-modal.modal"),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { acceptance, query } from "../helpers/qunit-helpers";
|
import { acceptance, query } from "../helpers/qunit-helpers";
|
||||||
import { test } from "qunit";
|
import { test } from "qunit";
|
||||||
import { visit } from "@ember/test-helpers";
|
import { click, visit } from "@ember/test-helpers";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
acceptance("User Activity / Replies - empty state", function (needs) {
|
acceptance("User Activity / Replies - empty state", function (needs) {
|
||||||
@ -33,3 +33,39 @@ acceptance("User Activity / Replies - empty state", function (needs) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("User Activity / Replies - Download All", function (needs) {
|
||||||
|
const currentUser = "eviltrout";
|
||||||
|
const anotherUser = "charlie";
|
||||||
|
needs.user();
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
server.post("/export_csv/export_entity.json", () => {
|
||||||
|
return helper.response({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Can see and trigger download for own data replies", async function (assert) {
|
||||||
|
await visit(`/u/${currentUser}/activity`);
|
||||||
|
|
||||||
|
assert.ok(query(".user-additional-controls .btn"), "button exists");
|
||||||
|
|
||||||
|
await click(".user-additional-controls .btn");
|
||||||
|
await click("#dialog-holder .btn-primary");
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
I18n.t("user.download_archive.success")
|
||||||
|
);
|
||||||
|
|
||||||
|
await click("#dialog-holder .btn-primary");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Cannot see 'Download All' button for another user", async function (assert) {
|
||||||
|
await visit(`/u/${anotherUser}/activity`);
|
||||||
|
|
||||||
|
assert.notOk(
|
||||||
|
query(".user-additional-controls .btn"),
|
||||||
|
"download button is not present"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -0,0 +1,367 @@
|
|||||||
|
import I18n from "I18n";
|
||||||
|
import { module, test } from "qunit";
|
||||||
|
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||||
|
import { click, render, settled, triggerKeyEvent } from "@ember/test-helpers";
|
||||||
|
import { hbs } from "ember-cli-htmlbars";
|
||||||
|
import { query } from "discourse/tests/helpers/qunit-helpers";
|
||||||
|
|
||||||
|
module("Integration | Component | dialog-holder", function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
hooks.beforeEach(function () {
|
||||||
|
this.dialog = this.container.lookup("service:dialog");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("basics", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
assert.ok(query("#dialog-holder"), "element is in DOM");
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty by default"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dialog.alert({
|
||||||
|
message: "This is an error",
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query(".dialog-overlay").offsetWidth > 0,
|
||||||
|
true,
|
||||||
|
"overlay is visible"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"This is an error",
|
||||||
|
"dialog has error message"
|
||||||
|
);
|
||||||
|
|
||||||
|
// dismiss by clicking on overlay
|
||||||
|
await click(".dialog-overlay");
|
||||||
|
|
||||||
|
assert.ok(query("#dialog-holder"), "element is still in DOM");
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-overlay").offsetWidth,
|
||||||
|
0,
|
||||||
|
"overlay is not visible"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("basics - dismiss using Esc", async function (assert) {
|
||||||
|
let cancelCallbackCalled = false;
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
assert.ok(query("#dialog-holder"), "element is in DOM");
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty by default"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dialog.alert({
|
||||||
|
message: "This is an error",
|
||||||
|
didCancel: () => {
|
||||||
|
cancelCallbackCalled = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query(".dialog-overlay").offsetWidth > 0,
|
||||||
|
true,
|
||||||
|
"overlay is visible"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"This is an error",
|
||||||
|
"dialog has error message"
|
||||||
|
);
|
||||||
|
|
||||||
|
// dismiss by pressing Esc
|
||||||
|
await triggerKeyEvent(document, "keydown", "Escape");
|
||||||
|
|
||||||
|
assert.ok(cancelCallbackCalled, "cancel callback called");
|
||||||
|
assert.ok(query("#dialog-holder"), "element is still in DOM");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-overlay").offsetWidth,
|
||||||
|
0,
|
||||||
|
"overlay is not visible"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("alert with title", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.alert({
|
||||||
|
message: "This is a note.",
|
||||||
|
title: "And this is a title",
|
||||||
|
});
|
||||||
|
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-title").innerText.trim(),
|
||||||
|
"And this is a title",
|
||||||
|
"dialog has title"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("#dialog-holder[aria-labelledby='dialog-title']"),
|
||||||
|
"aria-labelledby is correctly set"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(query(".dialog-close"), "close button present");
|
||||||
|
assert.ok(query("#dialog-holder"), "element is still in DOM");
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"This is a note.",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".dialog-close");
|
||||||
|
|
||||||
|
assert.ok(query("#dialog-holder"), "element is still in DOM");
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-overlay").offsetWidth,
|
||||||
|
0,
|
||||||
|
"overlay is not visible"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("alert with a string parameter", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.alert("An alert message");
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"An alert message",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("confirm", async function (assert) {
|
||||||
|
let confirmCallbackCalled = false;
|
||||||
|
let cancelCallbackCalled = false;
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.confirm({
|
||||||
|
message: "A confirm message",
|
||||||
|
didConfirm: () => {
|
||||||
|
confirmCallbackCalled = true;
|
||||||
|
},
|
||||||
|
didCancel: () => {
|
||||||
|
cancelCallbackCalled = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"A confirm message",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-footer .btn-primary").innerText.trim(),
|
||||||
|
I18n.t("ok_value"),
|
||||||
|
"dialog primary button says Ok"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-footer .btn-default").innerText.trim(),
|
||||||
|
I18n.t("cancel_value"),
|
||||||
|
"dialog second button is present and says No"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".dialog-footer .btn-primary");
|
||||||
|
|
||||||
|
assert.ok(confirmCallbackCalled, "confirm callback called");
|
||||||
|
assert.notOk(cancelCallbackCalled, "cancel callback NOT called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog is empty"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancel callback", async function (assert) {
|
||||||
|
let confirmCallbackCalled = false;
|
||||||
|
let cancelCallbackCalled = false;
|
||||||
|
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.confirm({
|
||||||
|
message: "A confirm message",
|
||||||
|
didConfirm: () => {
|
||||||
|
confirmCallbackCalled = true;
|
||||||
|
},
|
||||||
|
didCancel: () => {
|
||||||
|
cancelCallbackCalled = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"A confirm message",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".dialog-footer .btn-default");
|
||||||
|
assert.notOk(confirmCallbackCalled, "confirm callback NOT called");
|
||||||
|
assert.ok(cancelCallbackCalled, "cancel callback called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog has been dismissed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("yes/no confirm", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.yesNoConfirm({ message: "A yes/no confirm message" });
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"A yes/no confirm message",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-footer .btn-primary").innerText.trim(),
|
||||||
|
I18n.t("yes_value"),
|
||||||
|
"dialog primary button says Yes"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-footer .btn-default").innerText.trim(),
|
||||||
|
I18n.t("no_value"),
|
||||||
|
"dialog second button is present and says No"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("alert with custom buttons", async function (assert) {
|
||||||
|
let customCallbackTriggered = false;
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.alert({
|
||||||
|
message: "An alert with custom buttons",
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
icon: "cog",
|
||||||
|
label: "Danger ahead",
|
||||||
|
class: "btn-danger",
|
||||||
|
action: () => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
customCallbackTriggered = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"An alert with custom buttons",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-footer .btn-danger").innerText.trim(),
|
||||||
|
"Danger ahead",
|
||||||
|
"dialog custom button is present"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.notOk(
|
||||||
|
query(".dialog-footer .btn-primary"),
|
||||||
|
"default confirm button is not present"
|
||||||
|
);
|
||||||
|
assert.notOk(
|
||||||
|
query(".dialog-footer .btn-default"),
|
||||||
|
"default cancel button is not present"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".dialog-footer .btn-danger");
|
||||||
|
assert.ok(customCallbackTriggered, "custom action was triggered");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query("#dialog-holder").innerText.trim(),
|
||||||
|
"",
|
||||||
|
"dialog has been dismissed"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("alert with custom classes", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.alert({
|
||||||
|
message: "An alert with custom classes",
|
||||||
|
class: "dialog-special dialog-super",
|
||||||
|
});
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"An alert with custom classes",
|
||||||
|
"dialog message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
query("#dialog-holder.dialog-special.dialog-super"),
|
||||||
|
"additional classes are present"
|
||||||
|
);
|
||||||
|
|
||||||
|
await click(".dialog-footer .btn-primary");
|
||||||
|
|
||||||
|
assert.notOk(
|
||||||
|
query("#dialog-holder.dialog-special"),
|
||||||
|
"additional class removed on dismissal"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.notOk(
|
||||||
|
query("#dialog-holder.dialog-super"),
|
||||||
|
"additional class removed on dismissal"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("notice", async function (assert) {
|
||||||
|
await render(hbs`<DialogHolder />`);
|
||||||
|
|
||||||
|
this.dialog.notice("Noted!");
|
||||||
|
await settled();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
query(".dialog-body").innerText.trim(),
|
||||||
|
"Noted!",
|
||||||
|
"message is shown"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.notOk(query(".dialog-footer"), "no footer");
|
||||||
|
assert.notOk(query(".dialog-header"), "no header");
|
||||||
|
});
|
||||||
|
});
|
@ -1853,6 +1853,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||||
|
|
||||||
|
a11y-dialog@7.5.0:
|
||||||
|
version "7.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/a11y-dialog/-/a11y-dialog-7.5.0.tgz#1540627b18e3b1e266e0dcbdb5d1e7ac52079fe1"
|
||||||
|
integrity sha512-UF7cy4lfZQtvjRV5N4xdWFba+Pb1qW6FPp0p58dLjMTJ4PwIGGekTbmqUt3etBBRo9HbTqhlNsXQhzIuXeJpng==
|
||||||
|
dependencies:
|
||||||
|
focusable-selectors "^0.3.1"
|
||||||
|
|
||||||
abab@^2.0.6:
|
abab@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||||
@ -5783,6 +5790,11 @@ flush-write-stream@^1.0.0:
|
|||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
readable-stream "^2.3.6"
|
readable-stream "^2.3.6"
|
||||||
|
|
||||||
|
focusable-selectors@^0.3.1:
|
||||||
|
version "0.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/focusable-selectors/-/focusable-selectors-0.3.1.tgz#7eacbca8dc6cc8d7f7563e5f5cc3699b91e20aaa"
|
||||||
|
integrity sha512-5JLtr0e1YJIfmnVlpLiG+av07dd0Xkf/KfswsXcei5KmLfdwOysTQsjF058ynXniujb1fvev7nql1x+CkC5ikw==
|
||||||
|
|
||||||
follow-redirects@^1.0.0:
|
follow-redirects@^1.0.0:
|
||||||
version "1.13.3"
|
version "1.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
@import "crawler_layout";
|
@import "crawler_layout";
|
||||||
@import "d-icon";
|
@import "d-icon";
|
||||||
@import "d-popover";
|
@import "d-popover";
|
||||||
|
@import "dialog";
|
||||||
@import "directory";
|
@import "directory";
|
||||||
@import "discourse";
|
@import "discourse";
|
||||||
@import "edit-category";
|
@import "edit-category";
|
||||||
|
78
app/assets/stylesheets/common/base/dialog.scss
Normal file
78
app/assets/stylesheets/common/base/dialog.scss
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
.dialog-container,
|
||||||
|
.dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-container {
|
||||||
|
z-index: z("modal", "overlay");
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the dialog container and all its descendants are not
|
||||||
|
* visible and not focusable when it is hidden.
|
||||||
|
*/
|
||||||
|
.dialog-container[aria-hidden="true"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.dialog-overlay {
|
||||||
|
background: rgba(var(--always-black-rgb), 0.65);
|
||||||
|
animation: fade-in 250ms both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content {
|
||||||
|
margin: auto;
|
||||||
|
z-index: z("modal", "content");
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--secondary);
|
||||||
|
animation: fade-in 250ms both;
|
||||||
|
box-shadow: shadow("card");
|
||||||
|
min-width: 40vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 400px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid var(--primary-low);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: var(--font-up-3);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-close {
|
||||||
|
margin-left: auto;
|
||||||
|
.d-icon {
|
||||||
|
color: var(--primary-high);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px 15px 10px;
|
||||||
|
border-top: 1px solid var(--primary-low);
|
||||||
|
--btn-bottom-margin: 0.3em;
|
||||||
|
.btn {
|
||||||
|
margin: 0 0.75em var(--btn-bottom-margin) 0;
|
||||||
|
}
|
||||||
|
}
|
@ -251,6 +251,8 @@ en:
|
|||||||
not_implemented: "That feature hasn't been implemented yet, sorry!"
|
not_implemented: "That feature hasn't been implemented yet, sorry!"
|
||||||
no_value: "No"
|
no_value: "No"
|
||||||
yes_value: "Yes"
|
yes_value: "Yes"
|
||||||
|
ok_value: "OK"
|
||||||
|
cancel_value: "Cancel"
|
||||||
submit: "Submit"
|
submit: "Submit"
|
||||||
generic_error: "Sorry, an error has occurred."
|
generic_error: "Sorry, an error has occurred."
|
||||||
generic_error_with_reason: "An error occurred: %{error}"
|
generic_error_with_reason: "An error occurred: %{error}"
|
||||||
|
Reference in New Issue
Block a user