From cc4af80c7d50945f7e57fd0b79152e28b9f252ee Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Tue, 27 Sep 2022 14:47:13 -0400 Subject: [PATCH] DEV: refactor bootbox alerts (#18292) --- .../addon/components/admin-watched-word.js | 5 +- .../admin/addon/components/permalink-form.js | 12 +++-- .../components/screened-ip-address-form.js | 25 ++++++---- .../admin/addon/components/tags-uploader.js | 10 ++-- .../addon/components/watched-word-form.js | 23 ++++++---- .../addon/components/watched-word-uploader.js | 4 +- .../addon/controllers/admin-badges/award.js | 5 +- .../admin-customize-email-style-edit.js | 6 ++- .../controllers/admin-email-preview-digest.js | 6 +-- .../modals/admin-edit-badge-groupings.js | 6 ++- .../addon/controllers/modals/admin-reseed.js | 13 +++--- .../admin/addon/routes/admin-badges/show.js | 6 ++- .../admin/addon/services/admin-tools.js | 6 ++- .../discourse/app/components/badge-title.js | 7 ++- .../discourse/app/components/bookmark.js | 6 ++- .../discourse/app/components/d-navigation.js | 5 +- .../components/group-manage-email-settings.js | 30 +++++------- .../app/components/pick-files-button.js | 5 +- .../discourse/app/components/tag-info.js | 5 +- .../discourse/app/controllers/groups-new.js | 5 +- .../discourse/app/controllers/history.js | 5 +- .../discourse/app/controllers/login.js | 5 +- .../discourse/app/controllers/tag-show.js | 4 +- .../discourse/app/controllers/tags-index.js | 6 ++- .../app/controllers/topic-bulk-actions.js | 6 ++- .../discourse/app/controllers/user-status.js | 3 +- .../discourse/app/lib/ajax-error.js | 5 +- .../discourse/app/lib/click-track.js | 5 +- .../discourse/app/lib/export-result.js | 8 ++-- .../javascripts/discourse/app/lib/uploads.js | 34 ++++++++------ .../app/mixins/composer-upload-uppy.js | 5 +- .../discourse/app/mixins/uppy-upload.js | 5 +- .../discourse/app/routes/application.js | 6 +-- .../discourse/app/routes/new-message.js | 8 ++-- .../javascripts/discourse/app/widgets/post.js | 4 +- .../tests/acceptance/category-banner-test.js | 8 ++-- .../acceptance/composer-uploads-uppy-test.js | 28 +++++------ .../group-manage-email-settings-test.js | 12 ++--- .../user-preferences-sidebar-test.js | 8 ++-- .../components/watched-word-uploader-test.js | 16 +++++-- .../discourse/tests/unit/lib/uploads-test.js | 46 +++++++++---------- .../addon/components/wizard-field-image.js | 4 +- .../stylesheets/common/base/dialog.scss | 2 +- .../common/foundation/variables.scss | 1 + .../javascripts/controllers/poll-breakdown.js | 5 +- 45 files changed, 243 insertions(+), 186 deletions(-) diff --git a/app/assets/javascripts/admin/addon/components/admin-watched-word.js b/app/assets/javascripts/admin/addon/components/admin-watched-word.js index f0523d374c6..7de7e3d8ab5 100644 --- a/app/assets/javascripts/admin/addon/components/admin-watched-word.js +++ b/app/assets/javascripts/admin/addon/components/admin-watched-word.js @@ -1,12 +1,13 @@ import Component from "@ember/component"; import { alias, equal } from "@ember/object/computed"; -import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; import { action } from "@ember/object"; import I18n from "I18n"; +import { inject as service } from "@ember/service"; export default Component.extend({ classNames: ["watched-word"], + dialog: service(), isReplace: equal("actionKey", "replace"), isTag: equal("actionKey", "tag"), @@ -26,7 +27,7 @@ export default Component.extend({ this.action(this.word); }) .catch((e) => { - bootbox.alert( + this.dialog.alert( I18n.t("generic_error_with_reason", { error: `http: ${e.status} - ${e.body}`, }) diff --git a/app/assets/javascripts/admin/addon/components/permalink-form.js b/app/assets/javascripts/admin/addon/components/permalink-form.js index 62c62bdf741..9c0c64de6b3 100644 --- a/app/assets/javascripts/admin/addon/components/permalink-form.js +++ b/app/assets/javascripts/admin/addon/components/permalink-form.js @@ -1,14 +1,15 @@ import Component from "@ember/component"; import I18n from "I18n"; import Permalink from "admin/models/permalink"; -import bootbox from "bootbox"; import discourseComputed, { bind } from "discourse-common/utils/decorators"; import { fmt } from "discourse/lib/computed"; import { schedule } from "@ember/runloop"; import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; export default Component.extend({ tagName: "", + dialog: service(), formSubmitted: false, permalinkType: "topic_id", permalinkTypePlaceholder: fmt("permalinkType", "admin.permalink.%@"), @@ -29,7 +30,7 @@ export default Component.extend({ @bind focusPermalink() { schedule("afterRender", () => - this.element.querySelector(".permalink-url")?.focus() + document.querySelector(".permalink-url")?.focus() ); }, @@ -74,7 +75,12 @@ export default Component.extend({ } else { error = I18n.t("generic_error"); } - bootbox.alert(error, this.focusPermalink); + + this.dialog.alert({ + message: error, + didConfirm: () => this.focusPermalink(), + didCancel: () => this.focusPermalink(), + }); } ); } diff --git a/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js b/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js index ae965dd26e9..ab553b6a213 100644 --- a/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js +++ b/app/assets/javascripts/admin/addon/components/screened-ip-address-form.js @@ -2,8 +2,8 @@ import discourseComputed from "discourse-common/utils/decorators"; import Component from "@ember/component"; import I18n from "I18n"; import ScreenedIpAddress from "admin/models/screened-ip-address"; -import bootbox from "bootbox"; import { schedule } from "@ember/runloop"; +import { inject as service } from "@ember/service"; /** A form to create an IP address that will be blocked or allowed. @@ -18,6 +18,7 @@ import { schedule } from "@ember/runloop"; export default Component.extend({ tagName: "form", + dialog: service(), classNames: ["screened-ip-address-form", "inline-form"], formSubmitted: false, actionName: "block", @@ -47,6 +48,12 @@ export default Component.extend({ } }, + focusInput() { + schedule("afterRender", () => { + this.element.querySelector("input").focus(); + }); + }, + actions: { submit() { if (!this.formSubmitted) { @@ -60,22 +67,20 @@ export default Component.extend({ .then((result) => { this.setProperties({ ip_address: "", formSubmitted: false }); this.action(ScreenedIpAddress.create(result.screened_ip_address)); - schedule("afterRender", () => - this.element.querySelector("input").focus() - ); + this.focusInput(); }) .catch((e) => { this.set("formSubmitted", false); - const msg = e.jqXHR.responseJSON?.errors + const message = e.jqXHR.responseJSON?.errors ? I18n.t("generic_error_with_reason", { error: e.jqXHR.responseJSON.errors.join(". "), }) : I18n.t("generic_error"); - bootbox.alert(msg, () => - schedule("afterRender", () => - this.element.querySelector("input").focus() - ) - ); + this.dialog.alert({ + message, + didConfirm: () => this.focusInput(), + didCancel: () => this.focusInput(), + }); }); } }, diff --git a/app/assets/javascripts/admin/addon/components/tags-uploader.js b/app/assets/javascripts/admin/addon/components/tags-uploader.js index dcd57a45fd8..39425b9b656 100644 --- a/app/assets/javascripts/admin/addon/components/tags-uploader.js +++ b/app/assets/javascripts/admin/addon/components/tags-uploader.js @@ -2,10 +2,11 @@ import Component from "@ember/component"; import I18n from "I18n"; import UppyUploadMixin from "discourse/mixins/uppy-upload"; import { alias } from "@ember/object/computed"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; export default Component.extend(UppyUploadMixin, { type: "csv", + dialog: service(), uploadUrl: "/tags/upload", addDisabled: alias("uploading"), elementId: "tag-uploader", @@ -16,9 +17,8 @@ export default Component.extend(UppyUploadMixin, { }, uploadDone() { - bootbox.alert(I18n.t("tagging.upload_successful"), () => { - this.refresh(); - this.closeModal(); - }); + this.closeModal(); + this.refresh(); + this.dialog.alert(I18n.t("tagging.upload_successful")); }, }); diff --git a/app/assets/javascripts/admin/addon/components/watched-word-form.js b/app/assets/javascripts/admin/addon/components/watched-word-form.js index e00032f22c1..1d6ba8ccbab 100644 --- a/app/assets/javascripts/admin/addon/components/watched-word-form.js +++ b/app/assets/javascripts/admin/addon/components/watched-word-form.js @@ -2,13 +2,14 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators"; import Component from "@ember/component"; import I18n from "I18n"; import WatchedWord from "admin/models/watched-word"; -import bootbox from "bootbox"; import { equal } from "@ember/object/computed"; import { isEmpty } from "@ember/utils"; import { schedule } from "@ember/runloop"; +import { inject as service } from "@ember/service"; export default Component.extend({ tagName: "form", + dialog: service(), classNames: ["watched-word-form"], formSubmitted: false, actionKey: null, @@ -55,6 +56,10 @@ export default Component.extend({ }); }, + focusInput() { + schedule("afterRender", () => this.element.querySelector("input").focus()); + }, + actions: { changeSelectedTags(tags) { this.setProperties({ @@ -98,22 +103,20 @@ export default Component.extend({ isCaseSensitive: false, }); this.action(WatchedWord.create(result)); - schedule("afterRender", () => - this.element.querySelector("input").focus() - ); + this.focusInput(); }) .catch((e) => { this.set("formSubmitted", false); - const msg = e.jqXHR.responseJSON?.errors + const message = e.jqXHR.responseJSON?.errors ? I18n.t("generic_error_with_reason", { error: e.jqXHR.responseJSON.errors.join(". "), }) : I18n.t("generic_error"); - bootbox.alert(msg, () => - schedule("afterRender", () => - this.element.querySelector("input").focus() - ) - ); + this.dialog.alert({ + message, + didConfirm: () => this.focusInput(), + didCancel: () => this.focusInput(), + }); }); } }, diff --git a/app/assets/javascripts/admin/addon/components/watched-word-uploader.js b/app/assets/javascripts/admin/addon/components/watched-word-uploader.js index efbacb9c0b3..27c43ad5761 100644 --- a/app/assets/javascripts/admin/addon/components/watched-word-uploader.js +++ b/app/assets/javascripts/admin/addon/components/watched-word-uploader.js @@ -2,7 +2,7 @@ import Component from "@ember/component"; import I18n from "I18n"; import UppyUploadMixin from "discourse/mixins/uppy-upload"; import { alias } from "@ember/object/computed"; -import bootbox from "bootbox"; +import { dialog } from "discourse/lib/uploads"; export default Component.extend(UppyUploadMixin, { type: "txt", @@ -21,7 +21,7 @@ export default Component.extend(UppyUploadMixin, { uploadDone() { if (this) { - bootbox.alert(I18n.t("admin.watched_words.form.upload_successful")); + dialog.alert(I18n.t("admin.watched_words.form.upload_successful")); this.done(); } }, diff --git a/app/assets/javascripts/admin/addon/controllers/admin-badges/award.js b/app/assets/javascripts/admin/addon/controllers/admin-badges/award.js index e564840115f..93745bd79ed 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-badges/award.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-badges/award.js @@ -1,12 +1,13 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { extractError } from "discourse/lib/ajax-error"; import { action } from "@ember/object"; import { tracked } from "@glimmer/tracking"; +import { inject as service } from "@ember/service"; export default class AdminBadgesAwardController extends Controller { + @service dialog; @tracked saving = false; @tracked replaceBadgeOwners = false; @tracked grantExistingHolders = false; @@ -84,7 +85,7 @@ export default class AdminBadgesAwardController extends Controller { }) .finally(() => (this.saving = false)); } else { - bootbox.alert(I18n.t("admin.badges.mass_award.aborted")); + this.dialog.alert(I18n.t("admin.badges.mass_award.aborted")); } } } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-customize-email-style-edit.js b/app/assets/javascripts/admin/addon/controllers/admin-customize-email-style-edit.js index 79813399b28..ad664f79b5f 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-customize-email-style-edit.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-customize-email-style-edit.js @@ -1,9 +1,11 @@ import Controller from "@ember/controller"; import I18n from "I18n"; -import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; +import { inject as service } from "@ember/service"; export default Controller.extend({ + dialog: service(), + @discourseComputed("model.isSaving") saveButtonText(isSaving) { return isSaving ? I18n.t("saving") : I18n.t("admin.customize.save"); @@ -27,7 +29,7 @@ export default Controller.extend({ error: e.jqXHR.responseJSON.errors.join(". "), }) : I18n.t("generic_error"); - bootbox.alert(msg); + this.dialog.alert(msg); }) .finally(() => this.set("model.changed", false)); } diff --git a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js index 1a6fbf21847..50d74d35d8e 100644 --- a/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js +++ b/app/assets/javascripts/admin/addon/controllers/admin-email-preview-digest.js @@ -1,14 +1,14 @@ import { empty, notEmpty, or } from "@ember/object/computed"; import Controller from "@ember/controller"; import EmailPreview from "admin/models/email-preview"; -import bootbox from "bootbox"; import { get } from "@ember/object"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { inject as service } from "@ember/service"; export default Controller.extend({ + dialog: service(), username: null, lastSeen: null, - emailEmpty: empty("email"), sendEmailDisabled: or("emailEmpty", "sendingEmail"), showSendEmailForm: notEmpty("model.html_content"), @@ -50,7 +50,7 @@ export default Controller.extend({ EmailPreview.sendDigest(this.username, this.lastSeen, this.email) .then((result) => { if (result.errors) { - bootbox.alert(result.errors); + this.dialog.alert(result.errors); } else { this.set("sentEmail", true); } diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-edit-badge-groupings.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-edit-badge-groupings.js index aa7d5665fa3..c453a4f8b41 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-edit-badge-groupings.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-edit-badge-groupings.js @@ -3,10 +3,12 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { observes } from "discourse-common/utils/decorators"; +import { inject as service } from "@ember/service"; export default Controller.extend(ModalFunctionality, { + dialog: service(), + @observes("model") modelChanged() { const model = this.model; @@ -78,7 +80,7 @@ export default Controller.extend(ModalFunctionality, { this.setProperties({ model: null, workingCopy: null }); this.send("closeModal"); }, - () => bootbox.alert(I18n.t("generic_error")) + () => this.dialog.alert(I18n.t("generic_error")) ); }, }, diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-reseed.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-reseed.js index 672f6d8db3f..744921b02a5 100644 --- a/app/assets/javascripts/admin/addon/controllers/modals/admin-reseed.js +++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-reseed.js @@ -2,9 +2,10 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; export default Controller.extend(ModalFunctionality, { + dialog: service(), loading: true, reseeding: false, categories: null, @@ -35,11 +36,11 @@ export default Controller.extend(ModalFunctionality, { }, type: "POST", }) - .then( - () => this.send("closeModal"), - () => bootbox.alert(I18n.t("generic_error")) - ) - .finally(() => this.set("reseeding", false)); + .catch(() => this.dialog.alert(I18n.t("generic_error"))) + .finally(() => { + this.set("reseeding", false); + this.send("closeModal"); + }); }, }, }); diff --git a/app/assets/javascripts/admin/addon/routes/admin-badges/show.js b/app/assets/javascripts/admin/addon/routes/admin-badges/show.js index 4d1eb1db762..b17d253b666 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-badges/show.js +++ b/app/assets/javascripts/admin/addon/routes/admin-badges/show.js @@ -2,11 +2,13 @@ import Badge from "discourse/models/badge"; import I18n from "I18n"; import Route from "@ember/routing/route"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { action, get } from "@ember/object"; import showModal from "discourse/lib/show-modal"; +import { inject as service } from "@ember/service"; export default class AdminBadgesShowRoute extends Route { + @service dialog; + serialize(m) { return { badge_id: get(m, "id") || "new" }; } @@ -58,7 +60,7 @@ export default class AdminBadgesShowRoute extends Route { badge.set("preview_loading", false); // eslint-disable-next-line no-console console.error(error); - bootbox.alert("Network error"); + this.dialog.alert("Network error"); }); } } diff --git a/app/assets/javascripts/admin/addon/services/admin-tools.js b/app/assets/javascripts/admin/addon/services/admin-tools.js index 17266be8229..3d06845508a 100644 --- a/app/assets/javascripts/admin/addon/services/admin-tools.js +++ b/app/assets/javascripts/admin/addon/services/admin-tools.js @@ -1,7 +1,7 @@ import AdminUser from "admin/models/admin-user"; import I18n from "I18n"; import { Promise } from "rsvp"; -import Service from "@ember/service"; +import Service, { inject as service } from "@ember/service"; import { ajax } from "discourse/lib/ajax"; import bootbox from "bootbox"; import { getOwner } from "discourse-common/lib/get-owner"; @@ -12,6 +12,8 @@ import showModal from "discourse/lib/show-modal"; // and the admin application. Use this if you need front end code to access admin // modules. Inject it optionally, and if it exists go to town! export default Service.extend({ + dialog: service(), + showActionLogs(target, filters) { const controller = getOwner(target).lookup( "controller:adminLogs.staffActionLogs" @@ -120,7 +122,7 @@ export default Service.extend({ } }) .catch(() => { - bootbox.alert(I18n.t("admin.user.delete_failed")); + this.dialog.alert(I18n.t("admin.user.delete_failed")); reject(); }); }, diff --git a/app/assets/javascripts/discourse/app/components/badge-title.js b/app/assets/javascripts/discourse/app/components/badge-title.js index cb622495217..ec8db2e8da5 100644 --- a/app/assets/javascripts/discourse/app/components/badge-title.js +++ b/app/assets/javascripts/discourse/app/components/badge-title.js @@ -2,13 +2,12 @@ import Component from "@ember/component"; import { action } from "@ember/object"; import I18n from "I18n"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; export default Component.extend({ + dialog: service(), tagName: "", - selectableUserBadges: null, - _selectedUserBadgeId: null, _isSaved: false, _isSaving: false, @@ -42,7 +41,7 @@ export default Component.extend({ this.currentUser.set("title", selectedUserBadge?.badge?.name || ""); }, () => { - bootbox.alert(I18n.t("generic_error")); + this.dialog.alert(I18n.t("generic_error")); } ) .finally(() => this.set("_isSaving", false)); diff --git a/app/assets/javascripts/discourse/app/components/bookmark.js b/app/assets/javascripts/discourse/app/components/bookmark.js index b5739fa66cf..0ede10b3c25 100644 --- a/app/assets/javascripts/discourse/app/components/bookmark.js +++ b/app/assets/javascripts/discourse/app/components/bookmark.js @@ -18,14 +18,16 @@ import { and, notEmpty } from "@ember/object/computed"; import { popupAjaxError } from "discourse/lib/ajax-error"; import discourseLater from "discourse-common/lib/later"; +import { inject as service } from "@ember/service"; + const BOOKMARK_BINDINGS = { enter: { handler: "saveAndClose" }, "d d": { handler: "delete" }, }; export default Component.extend({ + dialog: service(), tagName: "", - errorMessage: null, selectedReminderType: null, _closeWithoutSaving: null, @@ -227,7 +229,7 @@ export default Component.extend({ _handleSaveError(e) { this._savingBookmarkManually = false; if (typeof e === "string") { - bootbox.alert(e); + this.dialog.alert(e); } else { popupAjaxError(e); } diff --git a/app/assets/javascripts/discourse/app/components/d-navigation.js b/app/assets/javascripts/discourse/app/components/d-navigation.js index 32f8756b42b..2153fc5f9ce 100644 --- a/app/assets/javascripts/discourse/app/components/d-navigation.js +++ b/app/assets/javascripts/discourse/app/components/d-navigation.js @@ -1,7 +1,6 @@ import Component from "@ember/component"; import FilterModeMixin from "discourse/mixins/filter-mode"; import NavItem from "discourse/models/nav-item"; -import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; import { NotificationLevels } from "discourse/lib/notification-levels"; import { getOwner } from "discourse-common/lib/get-owner"; @@ -9,7 +8,7 @@ import { inject as service } from "@ember/service"; export default Component.extend(FilterModeMixin, { router: service(), - + dialog: service(), tagName: "", // Should be a `readOnly` instead but some themes/plugins still pass @@ -158,7 +157,7 @@ export default Component.extend(FilterModeMixin, { clickCreateTopicButton() { if (this.categoryReadOnlyBanner && !this.hasDraft) { - bootbox.alert(this.categoryReadOnlyBanner); + this.dialog.alert(this.categoryReadOnlyBanner); } else { this.createTopic(); } diff --git a/app/assets/javascripts/discourse/app/components/group-manage-email-settings.js b/app/assets/javascripts/discourse/app/components/group-manage-email-settings.js index ffa586b32a3..786f2a3a5b1 100644 --- a/app/assets/javascripts/discourse/app/components/group-manage-email-settings.js +++ b/app/assets/javascripts/discourse/app/components/group-manage-email-settings.js @@ -2,11 +2,12 @@ import Component from "@ember/component"; import { isEmpty } from "@ember/utils"; import discourseComputed, { on } from "discourse-common/utils/decorators"; import I18n from "I18n"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; import { action } from "@ember/object"; export default Component.extend({ tagName: "", + dialog: service(), imapSettingsValid: false, smtpSettingsValid: false, @@ -71,16 +72,11 @@ export default Component.extend({ this.group.smtp_enabled && this._anySmtpFieldsFilled() ) { - bootbox.confirm( - I18n.t("groups.manage.email.smtp_disable_confirm"), - (result) => { - if (!result) { - this.group.set("smtp_enabled", true); - } else { - this.group.set("imap_enabled", false); - } - } - ); + this.dialog.confirm({ + message: I18n.t("groups.manage.email.smtp_disable_confirm"), + didConfirm: () => this.group.set("smtp_enabled", true), + didCancel: () => this.group.set("imap_enabled", false), + }); } this.group.set("smtp_enabled", event.target.checked); @@ -93,14 +89,10 @@ export default Component.extend({ this.group.imap_enabled && this._anyImapFieldsFilled() ) { - bootbox.confirm( - I18n.t("groups.manage.email.imap_disable_confirm"), - (result) => { - if (!result) { - this.group.set("imap_enabled", true); - } - } - ); + this.dialog.confirm({ + message: I18n.t("groups.manage.email.imap_disable_confirm"), + didConfirm: () => this.group.set("imap_enabled", true), + }); } this.group.set("imap_enabled", event.target.checked); diff --git a/app/assets/javascripts/discourse/app/components/pick-files-button.js b/app/assets/javascripts/discourse/app/components/pick-files-button.js index 3f2482ad50f..991ffe1592f 100644 --- a/app/assets/javascripts/discourse/app/components/pick-files-button.js +++ b/app/assets/javascripts/discourse/app/components/pick-files-button.js @@ -1,5 +1,4 @@ import Component from "@ember/component"; -import bootbox from "bootbox"; import { isBlank } from "@ember/utils"; import { authorizedExtensions, @@ -8,6 +7,7 @@ import { import { action } from "@ember/object"; import discourseComputed, { bind } from "discourse-common/utils/decorators"; import I18n from "I18n"; +import { inject as service } from "@ember/service"; // This picker is intended to be used with UppyUploadMixin or with // ComposerUploadUppy, which is why there are no change events registered @@ -18,6 +18,7 @@ import I18n from "I18n"; // is sometimes useful if you need to do something outside the uppy upload with // the file, such as directly using JSON or CSV data from a file in JS. export default Component.extend({ + dialog: service(), fileInputId: null, fileInputClass: null, fileInputDisabled: false, @@ -87,7 +88,7 @@ export default Component.extend({ const message = I18n.t("pick_files_button.unsupported_file_picked", { types: this.acceptedFileTypesString, }); - bootbox.alert(message); + this.dialog.alert(message); return; } this.onFilesPicked(files); diff --git a/app/assets/javascripts/discourse/app/components/tag-info.js b/app/assets/javascripts/discourse/app/components/tag-info.js index e1402a23279..6cb2a35dba8 100644 --- a/app/assets/javascripts/discourse/app/components/tag-info.js +++ b/app/assets/javascripts/discourse/app/components/tag-info.js @@ -9,6 +9,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; import { inject as service } from "@ember/service"; export default Component.extend({ + dialog: service(), tagName: "", loading: false, tagInfo: null, @@ -159,13 +160,13 @@ export default Component.extend({ this.set("newSynonyms", null); this.loadTagInfo(); } else if (response.failed_tags) { - bootbox.alert( + this.dialog.alert( I18n.t("tagging.add_synonyms_failed", { tag_names: Object.keys(response.failed_tags).join(", "), }) ); } else { - bootbox.alert(I18n.t("generic_error")); + this.dialog.alert(I18n.t("generic_error")); } }) .catch(popupAjaxError); diff --git a/app/assets/javascripts/discourse/app/controllers/groups-new.js b/app/assets/javascripts/discourse/app/controllers/groups-new.js index ebb683bdc4d..faddce7b720 100644 --- a/app/assets/javascripts/discourse/app/controllers/groups-new.js +++ b/app/assets/javascripts/discourse/app/controllers/groups-new.js @@ -2,9 +2,9 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import { action } from "@ember/object"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { popupAjaxError } from "discourse/lib/ajax-error"; import discourseComputed from "discourse-common/utils/decorators"; +import { inject as service } from "@ember/service"; export function popupAutomaticMembershipAlert(group_id, email_domains) { if (!email_domains) { @@ -25,7 +25,7 @@ export function popupAutomaticMembershipAlert(group_id, email_domains) { const count = result.user_count; if (count > 0) { - bootbox.alert( + this.dialog.alert( I18n.t( "admin.groups.manage.membership.automatic_membership_user_count", { count } @@ -36,6 +36,7 @@ export function popupAutomaticMembershipAlert(group_id, email_domains) { } export default Controller.extend({ + dialog: service(), saving: null, @discourseComputed("model.ownerUsernames") diff --git a/app/assets/javascripts/discourse/app/controllers/history.js b/app/assets/javascripts/discourse/app/controllers/history.js index 4888ce0f303..2540c78b54b 100644 --- a/app/assets/javascripts/discourse/app/controllers/history.js +++ b/app/assets/javascripts/discourse/app/controllers/history.js @@ -9,10 +9,10 @@ import Controller from "@ember/controller"; import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import Post from "discourse/models/post"; -import bootbox from "bootbox"; import { categoryBadgeHTML } from "discourse/helpers/category-link"; import { iconHTML } from "discourse-common/lib/icon-library"; import { sanitizeAsync } from "discourse/lib/text"; +import { inject as service } from "@ember/service"; function customTagArray(val) { if (!val) { @@ -26,6 +26,7 @@ function customTagArray(val) { // This controller handles displaying of history export default Controller.extend(ModalFunctionality, { + dialog: service(), loading: true, viewMode: "side_by_side", @@ -139,7 +140,7 @@ export default Controller.extend(ModalFunctionality, { e.jqXHR.responseJSON.errors && e.jqXHR.responseJSON.errors[0] ) { - bootbox.alert(e.jqXHR.responseJSON.errors[0]); + this.dialog.alert(e.jqXHR.responseJSON.errors[0]); } }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/login.js b/app/assets/javascripts/discourse/app/controllers/login.js index b5d1dcef7cc..1a5a4ecc7f8 100644 --- a/app/assets/javascripts/discourse/app/controllers/login.js +++ b/app/assets/javascripts/discourse/app/controllers/login.js @@ -8,7 +8,6 @@ import I18n from "I18n"; import ModalFunctionality from "discourse/mixins/modal-functionality"; import { SECOND_FACTOR_METHODS } from "discourse/models/user"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; import { escape } from "pretty-text/sanitizer"; import { extractError } from "discourse/lib/ajax-error"; @@ -19,6 +18,7 @@ import { isEmpty } from "@ember/utils"; import { setting } from "discourse/lib/computed"; import showModal from "discourse/lib/show-modal"; import { wavingHandURL } from "discourse/lib/waving-hand-url"; +import { inject as service } from "@ember/service"; // This is happening outside of the app via popup const AuthErrors = [ @@ -33,6 +33,7 @@ export default Controller.extend(ModalFunctionality, { createAccount: controller(), forgotPassword: controller(), application: controller(), + dialog: service(), loggingIn: false, loggedIn: false, @@ -199,7 +200,7 @@ export default Controller.extend(ModalFunctionality, { }); } else if (result.reason === "suspended") { this.send("closeModal"); - bootbox.alert(result.error); + this.dialog.alert(result.error); } else { this.flash(result.error, "error"); } diff --git a/app/assets/javascripts/discourse/app/controllers/tag-show.js b/app/assets/javascripts/discourse/app/controllers/tag-show.js index e0faae1e25f..619c5e860df 100644 --- a/app/assets/javascripts/discourse/app/controllers/tag-show.js +++ b/app/assets/javascripts/discourse/app/controllers/tag-show.js @@ -10,12 +10,14 @@ import { readOnly } from "@ember/object/computed"; import bootbox from "bootbox"; import { endWith } from "discourse/lib/computed"; import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; export default DiscoverySortableController.extend( BulkTopicSelection, FilterModeMixin, { application: controller(), + dialog: service(), tag: null, additionalTags: null, @@ -172,7 +174,7 @@ export default DiscoverySortableController.extend( this.tag .destroyRecord() .then(() => this.transitionToRoute("tags.index")) - .catch(() => bootbox.alert(I18n.t("generic_error"))); + .catch(() => this.dialog.alert(I18n.t("generic_error"))); }); }, diff --git a/app/assets/javascripts/discourse/app/controllers/tags-index.js b/app/assets/javascripts/discourse/app/controllers/tags-index.js index 00553d4e51c..ec56d24003a 100644 --- a/app/assets/javascripts/discourse/app/controllers/tags-index.js +++ b/app/assets/javascripts/discourse/app/controllers/tags-index.js @@ -7,10 +7,12 @@ import discourseComputed from "discourse-common/utils/decorators"; import { popupAjaxError } from "discourse/lib/ajax-error"; import showModal from "discourse/lib/show-modal"; +import { inject as service } from "@ember/service"; + export default Controller.extend({ + dialog: service(), sortedByCount: true, sortedByName: false, - canAdminTags: alias("currentUser.staff"), groupedByCategory: notEmpty("model.extras.categories"), groupedByTagGroup: notEmpty("model.extras.tag_groups"), @@ -67,7 +69,7 @@ export default Controller.extend({ const tags = result["tags"]; if (tags.length === 0) { - bootbox.alert(I18n.t("tagging.delete_no_unused_tags")); + this.dialog.alert(I18n.t("tagging.delete_no_unused_tags")); return; } diff --git a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js index dc4283c407d..7efe21e1376 100644 --- a/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js +++ b/app/assets/javascripts/discourse/app/controllers/topic-bulk-actions.js @@ -6,6 +6,8 @@ import { Promise } from "rsvp"; import Topic from "discourse/models/topic"; import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; + const _buttons = []; const alwaysTrue = () => true; @@ -118,7 +120,7 @@ addBulkButton("deleteTopics", "delete", { // Modal for performing bulk actions on topics export default Controller.extend(ModalFunctionality, { userPrivateMessages: controller("user-private-messages"), - + dialog: service(), tags: null, emptyTags: empty("tags"), categoryId: alias("model.category.id"), @@ -151,7 +153,7 @@ export default Controller.extend(ModalFunctionality, { return this._processChunks(operation) .catch(() => { - bootbox.alert(I18n.t("generic_error")); + this.dialog.alert(I18n.t("generic_error")); }) .finally(() => { this.set("loading", false); diff --git a/app/assets/javascripts/discourse/app/controllers/user-status.js b/app/assets/javascripts/discourse/app/controllers/user-status.js index 4040f6f9183..c438ac9c0d2 100644 --- a/app/assets/javascripts/discourse/app/controllers/user-status.js +++ b/app/assets/javascripts/discourse/app/controllers/user-status.js @@ -3,7 +3,6 @@ import ModalFunctionality from "discourse/mixins/modal-functionality"; import { action } from "@ember/object"; import { inject as service } from "@ember/service"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import bootbox from "bootbox"; import discourseComputed from "discourse-common/utils/decorators"; import ItsATrap from "@discourse/itsatrap"; import { @@ -90,7 +89,7 @@ export default Controller.extend(ModalFunctionality, { _handleError(e) { if (typeof e === "string") { - bootbox.alert(e); + this.dialog.alert(e); } else { popupAjaxError(e); } diff --git a/app/assets/javascripts/discourse/app/lib/ajax-error.js b/app/assets/javascripts/discourse/app/lib/ajax-error.js index 4796e627d71..55a9a808335 100644 --- a/app/assets/javascripts/discourse/app/lib/ajax-error.js +++ b/app/assets/javascripts/discourse/app/lib/ajax-error.js @@ -1,5 +1,5 @@ import I18n from "I18n"; -import bootbox from "bootbox"; +import { getOwner } from "discourse-common/lib/get-owner"; export function extractError(error, defaultMessage) { if (error instanceof Error) { @@ -64,5 +64,6 @@ export function throwAjaxError(undoCallback) { } export function popupAjaxError(error) { - bootbox.alert(extractError(error)); + const dialog = getOwner(this).lookup("service:dialog"); + dialog.alert(extractError(error)); } diff --git a/app/assets/javascripts/discourse/app/lib/click-track.js b/app/assets/javascripts/discourse/app/lib/click-track.js index 85754e3ef1a..bbce8cdcc60 100644 --- a/app/assets/javascripts/discourse/app/lib/click-track.js +++ b/app/assets/javascripts/discourse/app/lib/click-track.js @@ -3,13 +3,13 @@ import I18n from "I18n"; import { Promise } from "rsvp"; import User from "discourse/models/user"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import getURL, { samePrefix } from "discourse-common/lib/get-url"; import { isTesting } from "discourse-common/config/environment"; import discourseLater from "discourse-common/lib/later"; import { selectedText } from "discourse/lib/utilities"; import { wantsNewWindow } from "discourse/lib/intercept-click"; import deprecated from "discourse-common/lib/deprecated"; +import { getOwner } from "discourse-common/lib/get-owner"; export function isValidLink(link) { // eslint-disable-next-line no-undef @@ -121,7 +121,8 @@ export default { siteSettings?.prevent_anons_from_downloading_files && !User.current() ) { - bootbox.alert(I18n.t("post.errors.attachment_download_requires_login")); + const dialog = getOwner(this).lookup("service:dialog"); + dialog.alert(I18n.t("post.errors.attachment_download_requires_login")); } else if (wantsNewWindow(e)) { const newWindow = window.open(href, "_blank"); newWindow.opener = null; diff --git a/app/assets/javascripts/discourse/app/lib/export-result.js b/app/assets/javascripts/discourse/app/lib/export-result.js index 303708cd603..9492726b5a8 100644 --- a/app/assets/javascripts/discourse/app/lib/export-result.js +++ b/app/assets/javascripts/discourse/app/lib/export-result.js @@ -1,10 +1,12 @@ import I18n from "I18n"; -import bootbox from "bootbox"; +import { getOwner } from "discourse-common/lib/get-owner"; export function outputExportResult(result) { + const dialog = getOwner(this).lookup("service:dialog"); + if (result.success) { - bootbox.alert(I18n.t("admin.export_csv.success")); + dialog.alert(I18n.t("admin.export_csv.success")); } else { - bootbox.alert(I18n.t("admin.export_csv.failed")); + dialog.alert(I18n.t("admin.export_csv.failed")); } } diff --git a/app/assets/javascripts/discourse/app/lib/uploads.js b/app/assets/javascripts/discourse/app/lib/uploads.js index d019cfd84ef..366e842c79e 100644 --- a/app/assets/javascripts/discourse/app/lib/uploads.js +++ b/app/assets/javascripts/discourse/app/lib/uploads.js @@ -1,7 +1,7 @@ import I18n from "I18n"; import deprecated from "discourse-common/lib/deprecated"; -import bootbox from "bootbox"; import { isAppleDevice } from "discourse/lib/utilities"; +import { getOwner } from "discourse-common/lib/get-owner"; function isGUID(value) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test( @@ -9,6 +9,14 @@ function isGUID(value) { ); } +// This wrapper simplifies unit testing the dialog service +export const dialog = { + alert(msg) { + const dg = getOwner(this).lookup("service:dialog"); + dg.alert(msg); + }, +}; + export function markdownNameFromFileName(fileName) { let name = fileName.slice(0, fileName.lastIndexOf(".")); @@ -25,7 +33,7 @@ export function validateUploadedFiles(files, opts) { } if (files.length > 1) { - bootbox.alert(I18n.t("post.errors.too_many_uploads")); + dialog.alert(I18n.t("post.errors.too_many_uploads")); return false; } @@ -74,7 +82,7 @@ export function validateUploadedFile(file, opts) { if (opts.imagesOnly) { if (!isImage(name) && !isAuthorizedImage(name, staff, opts.siteSettings)) { - bootbox.alert( + dialog.alert( I18n.t("post.errors.upload_not_authorized", { authorized_extensions: authorizedImagesExtensions( staff, @@ -86,7 +94,7 @@ export function validateUploadedFile(file, opts) { } } else if (opts.csvOnly) { if (!/\.csv$/i.test(name)) { - bootbox.alert(I18n.t("user.invited.bulk_invite.error")); + dialog.alert(I18n.t("user.invited.bulk_invite.error")); return false; } } else { @@ -94,7 +102,7 @@ export function validateUploadedFile(file, opts) { !authorizesAllExtensions(staff, opts.siteSettings) && !isAuthorizedFile(name, staff, opts.siteSettings) ) { - bootbox.alert( + dialog.alert( I18n.t("post.errors.upload_not_authorized", { authorized_extensions: authorizedExtensions( staff, @@ -109,7 +117,7 @@ export function validateUploadedFile(file, opts) { if (!opts.bypassNewUserRestriction) { // ensures that new users can upload a file if (user && !user.isAllowedToUploadAFile(opts.type)) { - bootbox.alert( + dialog.alert( I18n.t(`post.errors.${opts.type}_upload_not_allowed_for_new_user`) ); return false; @@ -119,7 +127,7 @@ export function validateUploadedFile(file, opts) { if (file.size === 0) { /* eslint-disable no-console */ console.warn("File with a 0 byte size detected, cancelling upload.", file); - bootbox.alert(I18n.t("post.errors.file_size_zero")); + dialog.alert(I18n.t("post.errors.file_size_zero")); return false; } @@ -321,26 +329,26 @@ export function displayErrorForUpload(data, siteSettings, fileName) { return; } } else if (data.errors && data.errors.length > 0) { - bootbox.alert(data.errors.join("\n")); + dialog.alert(data.errors.join("\n")); return; } // otherwise, display a generic error message - bootbox.alert(I18n.t("post.errors.upload")); + dialog.alert(I18n.t("post.errors.upload")); } function displayErrorByResponseStatus(status, body, fileName, siteSettings) { switch (status) { // didn't get headers from server, or browser refuses to tell us case 0: - bootbox.alert(I18n.t("post.errors.upload")); + dialog.alert(I18n.t("post.errors.upload")); return true; // entity too large, usually returned from the web server case 413: const type = uploadTypeFromFileName(fileName); const max_size_kb = siteSettings[`max_${type}_size_kb`]; - bootbox.alert( + dialog.alert( I18n.t("post.errors.file_too_large_humanized", { max_size: I18n.toHumanSize(max_size_kb * 1024), }) @@ -350,9 +358,9 @@ function displayErrorByResponseStatus(status, body, fileName, siteSettings) { // the error message is provided by the server case 422: if (body.message) { - bootbox.alert(body.message); + dialog.alert(body.message); } else { - bootbox.alert(body.errors.join("\n")); + dialog.alert(body.errors.join("\n")); } return true; } diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index fd921f07268..8d8f7c9678c 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -19,7 +19,7 @@ import { validateUploadedFile, } from "discourse/lib/uploads"; import { cacheShortUploadUrl } from "pretty-text/upload-short-url"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; import { run } from "@ember/runloop"; import escapeRegExp from "discourse-common/utils/escape-regexp"; @@ -36,6 +36,7 @@ import escapeRegExp from "discourse-common/utils/escape-regexp"; // functionality and event binding. // export default Mixin.create(ExtendableUploader, UppyS3Multipart, { + dialog: service(), uploadRootPath: "/uploads", uploadTargetBound: false, useUploadPlaceholders: true, @@ -186,7 +187,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, { // _not_ been handled by an upload handler. const fileCount = Object.keys(unhandledFiles).length; if (maxFiles > 0 && fileCount > maxFiles) { - bootbox.alert( + this.dialog.alert( I18n.t("post.errors.too_many_dragged_and_dropped_files", { count: maxFiles, }) diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index 95991d5b2f1..9428e4aea89 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -21,11 +21,12 @@ import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart"; import UppyChunkedUploader from "discourse/lib/uppy-chunked-uploader-plugin"; import { bind, on } from "discourse-common/utils/decorators"; import { warn } from "@ember/debug"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB export default Mixin.create(UppyS3Multipart, ExtendableUploader, { + dialog: service(), uploading: false, uploadProgress: 0, _uppyInstance: null, @@ -130,7 +131,7 @@ export default Mixin.create(UppyS3Multipart, ExtendableUploader, { } if (tooMany) { - bootbox.alert( + this.dialog.alert( I18n.t("post.errors.too_many_dragged_and_dropped_files", { count: this.allowMultipleFiles ? maxFiles : 1, }) diff --git a/app/assets/javascripts/discourse/app/routes/application.js b/app/assets/javascripts/discourse/app/routes/application.js index 558ab27bc11..a0962b77732 100644 --- a/app/assets/javascripts/discourse/app/routes/application.js +++ b/app/assets/javascripts/discourse/app/routes/application.js @@ -5,7 +5,6 @@ import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; import OpenComposer from "discourse/mixins/open-composer"; import { ajax } from "discourse/lib/ajax"; -import bootbox from "bootbox"; import { findAll } from "discourse/models/login-method"; import { getOwner } from "discourse-common/lib/get-owner"; import getURL from "discourse-common/lib/get-url"; @@ -18,7 +17,7 @@ import showModal from "discourse/lib/show-modal"; function unlessReadOnly(method, message) { return function () { if (this.site.isReadOnly) { - bootbox.alert(message); + this.dialog.alert(message); } else { this[method](); } @@ -28,7 +27,7 @@ function unlessReadOnly(method, message) { function unlessStrictlyReadOnly(method, message) { return function () { if (this.site.isReadOnly && !this.site.isStaffWritesOnly) { - bootbox.alert(message); + this.dialog.alert(message); } else { this[method](); } @@ -39,6 +38,7 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, { siteTitle: setting("title"), shortSiteDescription: setting("short_site_description"), documentTitle: service(), + dialog: service(), actions: { toggleAnonymous() { diff --git a/app/assets/javascripts/discourse/app/routes/new-message.js b/app/assets/javascripts/discourse/app/routes/new-message.js index f7e32b53417..c9794ce116f 100644 --- a/app/assets/javascripts/discourse/app/routes/new-message.js +++ b/app/assets/javascripts/discourse/app/routes/new-message.js @@ -1,11 +1,13 @@ import DiscourseRoute from "discourse/routes/discourse"; import Group from "discourse/models/group"; import I18n from "I18n"; -import bootbox from "bootbox"; import cookie from "discourse/lib/cookie"; import { next } from "@ember/runloop"; +import { inject as service } from "@ember/service"; export default DiscourseRoute.extend({ + dialog: service(), + beforeModel(transition) { const params = transition.to.queryParams; @@ -32,12 +34,12 @@ export default DiscourseRoute.extend({ }) ); } else { - bootbox.alert( + this.dialog.alert( I18n.t("composer.cant_send_pm", { username: groupName }) ); } }) - .catch(() => bootbox.alert(I18n.t("generic_error"))); + .catch(() => this.dialog.alert(I18n.t("generic_error"))); } else { e.send("createNewMessageViaParams", { topicTitle: params.title, diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js index e4354c844a7..e2fa3ab3c0a 100644 --- a/app/assets/javascripts/discourse/app/widgets/post.js +++ b/app/assets/javascripts/discourse/app/widgets/post.js @@ -11,7 +11,6 @@ import I18n from "I18n"; import PostCooked from "discourse/widgets/post-cooked"; import { Promise } from "rsvp"; import RawHtml from "discourse/widgets/raw-html"; -import bootbox from "bootbox"; import { dateNode } from "discourse/helpers/node"; import { h } from "virtual-dom"; import hbs from "discourse/widgets/hbs-compiler"; @@ -814,6 +813,7 @@ export function addPostClassesCallback(callback) { export default createWidget("post", { buildKey: (attrs) => `post-${attrs.id}`, + services: ["dialog"], shadowTree: true, buildAttributes(attrs) { @@ -918,7 +918,7 @@ export default createWidget("post", { const { remaining, max } = result; const threshold = Math.ceil(max * 0.1); if (remaining === threshold) { - bootbox.alert(I18n.t("post.few_likes_left")); + this.dialog.alert(I18n.t("post.few_likes_left")); kvs.set({ key: "lastWarnedLikes", value: Date.now() }); } }, diff --git a/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js b/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js index 8e9f67798d7..d6874ab1c2f 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/category-banner-test.js @@ -44,7 +44,7 @@ acceptance("Category Banners", function (needs) { await visit("/c/test-read-only-without-banner"); await click("#create-topic"); - assert.ok(!visible(".bootbox.modal"), "it does not pop up a modal"); + assert.ok(!visible(".dialog-body"), "it does not pop up a modal"); assert.ok( !visible(".category-read-only-banner"), "it does not show a banner" @@ -55,10 +55,10 @@ acceptance("Category Banners", function (needs) { await visit("/c/test-read-only-with-banner"); await click("#create-topic"); - assert.ok(visible(".bootbox.modal"), "it pops up a modal"); + assert.ok(visible(".dialog-body"), "it pops up a modal"); - await click(".modal-footer>.btn-primary"); - assert.ok(!visible(".bootbox.modal"), "it closes the modal"); + await click(".dialog-footer .btn-primary"); + assert.ok(!visible(".dialog-body"), "it closes the modal"); assert.ok(visible(".category-read-only-banner"), "it shows a banner"); assert.strictEqual( count(".category-read-only-banner .inner"), diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js index c966f796d4a..0ff88838705 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-uploads-uppy-test.js @@ -6,8 +6,7 @@ import { query, } from "discourse/tests/helpers/qunit-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; -import bootbox from "bootbox"; -import { authorizedExtensions } from "discourse/lib/uploads"; +import { authorizedExtensions, dialog } from "discourse/lib/uploads"; import { click, fillIn, settled, visit } from "@ember/test-helpers"; import I18n from "I18n"; import { skip, test } from "qunit"; @@ -125,15 +124,16 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const image2 = createFile("avatar2.png"); const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { + await settled(); assert.strictEqual( - query(".bootbox .modal-body").innerHTML, + query(".dialog-body").textContent.trim(), I18n.t("post.errors.too_many_dragged_and_dropped_files", { count: 2, }), "it should warn about too many files added" ); - await click(".modal-footer .btn-primary"); + await click(".dialog-footer .btn-primary"); done(); }); @@ -149,8 +149,9 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { + await settled(); assert.strictEqual( - query(".bootbox .modal-body").innerHTML, + query(".dialog-body").textContent.trim(), I18n.t("post.errors.upload_not_authorized", { authorized_extensions: authorizedExtensions( false, @@ -160,7 +161,7 @@ acceptance("Uppy Composer Attachment - Upload Placeholder", function (needs) { "it should warn about unauthorized extensions" ); - await click(".modal-footer .btn-primary"); + await click(".dialog-footer .btn-primary"); done(); }); @@ -438,14 +439,14 @@ acceptance("Uppy Composer Attachment - Upload Error", function (needs) { appEvents.on("composer:upload-error", async () => { sinon.assert.calledOnce(stub); + await settled(); assert.strictEqual( - query(".bootbox .modal-body").innerHTML, + query(".dialog-body").textContent.trim(), "There was an error uploading the file, the gif was way too cool.", "it should show the error message from the server" ); - await click(".modal-footer .btn-primary"); - + await click(".dialog-footer .btn-primary"); done(); }); @@ -465,7 +466,7 @@ acceptance("Uppy Composer Attachment - Upload Handler", function (needs) { api.addComposerUploadHandler(["png"], (files) => { const file = files[0]; const isNativeFile = file instanceof File ? "WAS" : "WAS NOT"; - bootbox.alert( + dialog.alert( `This is an upload handler test for ${file.name}. The file ${isNativeFile} a native file object.` ); }); @@ -480,12 +481,13 @@ acceptance("Uppy Composer Attachment - Upload Handler", function (needs) { const done = assert.async(); appEvents.on("composer:uploads-aborted", async () => { + await settled(); assert.strictEqual( - query(".bootbox .modal-body").innerHTML, + query(".dialog-body").textContent.trim(), "This is an upload handler test for handler-test.png. The file WAS a native file object.", - "it should show the bootbox triggered by the upload handler" + "it should show the dialog triggered by the upload handler" ); - await click(".modal-footer .btn"); + await click(".dialog-footer .btn-primary"); done(); }); diff --git a/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js b/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js index cff0acf17b2..9fb65779816 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/group-manage-email-settings-test.js @@ -126,12 +126,12 @@ acceptance( await click("#enable_smtp"); assert.strictEqual( - query(".modal-body").innerText, + query(".dialog-body").innerText.trim(), I18n.t("groups.manage.email.smtp_disable_confirm"), "shows a confirm dialogue warning SMTP settings will be wiped" ); - await click(".modal-footer .btn.btn-primary"); + await click(".dialog-footer .btn-primary"); }); test("enabling IMAP, testing, and saving", async function (assert) { @@ -202,11 +202,11 @@ acceptance( await click("#enable_imap"); assert.strictEqual( - query(".modal-body").innerText, + query(".dialog-body").innerText.trim(), I18n.t("groups.manage.email.imap_disable_confirm"), "shows a confirm dialogue warning IMAP settings will be wiped" ); - await click(".modal-footer .btn.btn-primary"); + await click(".dialog-footer .btn-primary"); }); } ); @@ -362,11 +362,11 @@ acceptance( await click(".test-smtp-settings"); assert.strictEqual( - query(".modal-body").innerText, + query(".dialog-body").innerText.trim(), "There was an issue with the SMTP credentials provided, check the username and password and try again.", "shows a dialogue with the error message from the server" ); - await click(".modal-footer .btn.btn-primary"); + await click(".dialog-footer .btn-primary"); }); } ); diff --git a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js index c5c2b3aeb4c..5e48f669bdc 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/user-preferences-sidebar-test.js @@ -84,9 +84,9 @@ acceptance("User Preferences - Sidebar", function (needs) { "contains the right request body to update user's sidebar category links" ); - assert.ok(exists(".modal-body"), "error message is displayed"); + assert.ok(exists(".dialog-body"), "error message is displayed"); - await click(".modal .d-button-label"); + await click(".dialog-footer .btn-primary"); assert.ok( !exists(".sidebar-section-categories .sidebar-section-link-howto"), @@ -152,9 +152,9 @@ acceptance("User Preferences - Sidebar", function (needs) { "contains the right request body to update user's sidebar tag links" ); - assert.ok(exists(".modal-body"), "error message is displayed"); + assert.ok(exists(".dialog-body"), "error message is displayed"); - await click(".modal .d-button-label"); + await click(".dialog-footer .btn-primary"); assert.ok( !exists(".sidebar-section-tags .sidebar-section-link-gazelle"), diff --git a/app/assets/javascripts/discourse/tests/integration/components/watched-word-uploader-test.js b/app/assets/javascripts/discourse/tests/integration/components/watched-word-uploader-test.js index 4899bd9d455..8276b1514a8 100644 --- a/app/assets/javascripts/discourse/tests/integration/components/watched-word-uploader-test.js +++ b/app/assets/javascripts/discourse/tests/integration/components/watched-word-uploader-test.js @@ -1,9 +1,12 @@ import { module, test } from "qunit"; import { setupRenderingTest } from "discourse/tests/helpers/component-test"; -import { click, render, waitFor } from "@ember/test-helpers"; +import { render } from "@ember/test-helpers"; import { createFile } from "discourse/tests/helpers/qunit-helpers"; import { hbs } from "ember-cli-htmlbars"; import pretender, { response } from "discourse/tests/helpers/create-pretender"; +import sinon from "sinon"; +import I18n from "I18n"; +import { dialog } from "discourse/lib/uploads"; module("Integration | Component | watched-word-uploader", function (hooks) { setupRenderingTest(hooks); @@ -15,6 +18,8 @@ module("Integration | Component | watched-word-uploader", function (hooks) { }); test("sets the proper action key on uploads", async function (assert) { + sinon.stub(dialog, "alert"); + const done = assert.async(); this.set("actionNameKey", "flag"); this.set("doneUpload", function () { @@ -23,6 +28,12 @@ module("Integration | Component | watched-word-uploader", function (hooks) { .action_key, "flag" ); + assert.ok( + dialog.alert.calledWith( + I18n.t("admin.watched_words.form.upload_successful") + ), + "alert shown" + ); done(); }); @@ -38,8 +49,5 @@ module("Integration | Component | watched-word-uploader", function (hooks) { await this.container .lookup("service:app-events") .trigger("upload-mixin:watched-word-uploader:add-files", words); - await waitFor(".bootbox span.d-button-label"); - - await click(".bootbox span.d-button-label"); }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js index e2ca35c97a0..dcadb831f1f 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/uploads-test.js @@ -3,6 +3,7 @@ import { allowsAttachments, allowsImages, authorizedExtensions, + dialog, displayErrorForUpload, getUploadMarkdown, isImage, @@ -10,7 +11,6 @@ import { } from "discourse/lib/uploads"; import I18n from "I18n"; import User from "discourse/models/user"; -import bootbox from "bootbox"; import { discourseModule } from "discourse/tests/helpers/qunit-helpers"; import sinon from "sinon"; import { test } from "qunit"; @@ -32,17 +32,17 @@ discourseModule("Unit | Utility | uploads", function () { }); test("uploading one file", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.notOk( validateUploadedFiles([1, 2], { siteSettings: this.siteSettings }) ); - assert.ok(bootbox.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); + assert.ok(dialog.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); }); test("new user cannot upload images", function (assert) { this.siteSettings.newuser_max_embedded_media = 0; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.notOk( validateUploadedFiles([{ name: "image.png" }], { @@ -52,7 +52,7 @@ discourseModule("Unit | Utility | uploads", function () { "the upload is not valid" ); assert.ok( - bootbox.alert.calledWith( + dialog.alert.calledWith( I18n.t("post.errors.image_upload_not_allowed_for_new_user") ), "the alert is called" @@ -62,7 +62,7 @@ discourseModule("Unit | Utility | uploads", function () { test("new user can upload images if allowed", function (assert) { this.siteSettings.newuser_max_embedded_media = 1; this.siteSettings.default_trust_level = 0; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.ok( validateUploadedFiles([{ name: "image.png" }], { @@ -74,7 +74,7 @@ discourseModule("Unit | Utility | uploads", function () { test("TL1 can upload images", function (assert) { this.siteSettings.newuser_max_embedded_media = 0; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.ok( validateUploadedFiles([{ name: "image.png" }], { @@ -86,7 +86,7 @@ discourseModule("Unit | Utility | uploads", function () { test("new user cannot upload attachments", function (assert) { this.siteSettings.newuser_max_attachments = 0; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.notOk( validateUploadedFiles([{ name: "roman.txt" }], { @@ -95,21 +95,21 @@ discourseModule("Unit | Utility | uploads", function () { }) ); assert.ok( - bootbox.alert.calledWith( + dialog.alert.calledWith( I18n.t("post.errors.attachment_upload_not_allowed_for_new_user") ) ); }); test("ensures an authorized upload", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.notOk( validateUploadedFiles([{ name: "unauthorized.html" }], { siteSettings: this.siteSettings, }) ); assert.ok( - bootbox.alert.calledWith( + dialog.alert.calledWith( I18n.t("post.errors.upload_not_authorized", { authorized_extensions: authorizedExtensions( false, @@ -122,7 +122,7 @@ discourseModule("Unit | Utility | uploads", function () { test("skipping validation works", function (assert) { const files = [{ name: "backup.tar.gz" }]; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); assert.notOk( validateUploadedFiles(files, { @@ -141,7 +141,7 @@ discourseModule("Unit | Utility | uploads", function () { test("staff can upload anything in PM", function (assert) { const files = [{ name: "some.docx" }]; this.siteSettings.authorized_extensions = "jpeg"; - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); let user = User.create({ moderator: true }); assert.notOk( @@ -175,7 +175,7 @@ discourseModule("Unit | Utility | uploads", function () { }; test("allows valid uploads to go through", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); let user = User.create({ trust_level: 1 }); @@ -193,7 +193,7 @@ discourseModule("Unit | Utility | uploads", function () { }) ); - assert.notOk(bootbox.alert.calledOnce); + assert.notOk(dialog.alert.calledOnce); }); test("isImage", function (assert) { @@ -315,7 +315,7 @@ discourseModule("Unit | Utility | uploads", function () { }); test("displayErrorForUpload - jquery file upload - jqXHR present", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); displayErrorForUpload( { jqXHR: { status: 422, responseJSON: { message: "upload failed" } }, @@ -323,11 +323,11 @@ discourseModule("Unit | Utility | uploads", function () { { max_attachment_size_kb: 1024, max_image_size_kb: 1024 }, "test.png" ); - assert.ok(bootbox.alert.calledWith("upload failed"), "the alert is called"); + assert.ok(dialog.alert.calledWith("upload failed"), "the alert is called"); }); test("displayErrorForUpload - jquery file upload - jqXHR missing, errors present", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); displayErrorForUpload( { errors: ["upload failed"], @@ -335,11 +335,11 @@ discourseModule("Unit | Utility | uploads", function () { { max_attachment_size_kb: 1024, max_image_size_kb: 1024 }, "test.png" ); - assert.ok(bootbox.alert.calledWith("upload failed"), "the alert is called"); + assert.ok(dialog.alert.calledWith("upload failed"), "the alert is called"); }); test("displayErrorForUpload - jquery file upload - no errors", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); displayErrorForUpload( {}, { @@ -349,13 +349,13 @@ discourseModule("Unit | Utility | uploads", function () { "test.png" ); assert.ok( - bootbox.alert.calledWith(I18n.t("post.errors.upload")), + dialog.alert.calledWith(I18n.t("post.errors.upload")), "the alert is called" ); }); test("displayErrorForUpload - uppy - with response status and body", function (assert) { - sinon.stub(bootbox, "alert"); + sinon.stub(dialog, "alert"); displayErrorForUpload( { status: 422, @@ -364,6 +364,6 @@ discourseModule("Unit | Utility | uploads", function () { "test.png", { max_attachment_size_kb: 1024, max_image_size_kb: 1024 } ); - assert.ok(bootbox.alert.calledWith("upload failed"), "the alert is called"); + assert.ok(dialog.alert.calledWith("upload failed"), "the alert is called"); }); }); diff --git a/app/assets/javascripts/wizard/addon/components/wizard-field-image.js b/app/assets/javascripts/wizard/addon/components/wizard-field-image.js index 283c8695c4f..0b9140d5dd3 100644 --- a/app/assets/javascripts/wizard/addon/components/wizard-field-image.js +++ b/app/assets/javascripts/wizard/addon/components/wizard-field-image.js @@ -8,9 +8,11 @@ import getUrl from "discourse-common/lib/get-url"; import Uppy from "@uppy/core"; import DropTarget from "@uppy/drop-target"; import XHRUpload from "@uppy/xhr-upload"; +import { inject as service } from "@ember/service"; export default Component.extend({ classNames: ["wizard-container__image-upload"], + dialog: service(), uploading: false, @discourseComputed("field.id") @@ -57,7 +59,7 @@ export default Component.extend({ message = response.body.errors.join("\n"); } - window.bootbox.alert(message); + this.dialog.alert(message); this.set("uploading", false); }); diff --git a/app/assets/stylesheets/common/base/dialog.scss b/app/assets/stylesheets/common/base/dialog.scss index 64067180b89..5f74a25695a 100644 --- a/app/assets/stylesheets/common/base/dialog.scss +++ b/app/assets/stylesheets/common/base/dialog.scss @@ -8,7 +8,7 @@ } .dialog-container { - z-index: z("modal", "overlay"); + z-index: z("modal", "dialog"); display: flex; } diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 1600c031c1b..56627397a7c 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -79,6 +79,7 @@ $line-height-large: var(--line-height-large) !default; $z-layers: ( "max": 9999, "modal": ( + "dialog": 1700, "tooltip": 1600, "popover": 1500, "dropdown": 1400, diff --git a/plugins/poll/assets/javascripts/controllers/poll-breakdown.js b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js index 77d1c1e7a62..1361dbd5673 100644 --- a/plugins/poll/assets/javascripts/controllers/poll-breakdown.js +++ b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js @@ -8,9 +8,10 @@ import discourseComputed from "discourse-common/utils/decorators"; import { htmlSafe } from "@ember/template"; import loadScript from "discourse/lib/load-script"; import { popupAjaxError } from "discourse/lib/ajax-error"; -import bootbox from "bootbox"; +import { inject as service } from "@ember/service"; export default Controller.extend(ModalFunctionality, { + dialog: service(), model: null, charts: null, groupedBy: null, @@ -64,7 +65,7 @@ export default Controller.extend(ModalFunctionality, { if (error) { popupAjaxError(error); } else { - bootbox.alert(I18n.t("poll.error_while_fetching_voters")); + this.dialog.alert(I18n.t("poll.error_while_fetching_voters")); } }) .then((result) => {