mirror of
https://github.com/discourse/discourse.git
synced 2025-03-29 02:55:44 +08:00
DEV: Convert modal wrapper from named outlet to component (#21970)
This removes the modal container named-outlet/controller/template and replaces it with a component. Named outlets will be removed in Ember 4.x, so this change is part of that upgrade project. Smaller changes include: - update some of the computed values to be getters rather than calculated during `show()`. - update tests which were previously depending on the modal class persisting after the modal was closed Much of the logic in the service will be deprecated once we introduce component-based modals. This work is split out from https://github.com/discourse/discourse/pull/21304 Previously merged in 80b77b2e and then reverted due to issues with the PM invite modal. This PR fixes the issue, and introduces a test which would have caught the issue.
This commit is contained in:
parent
330137e7e4
commit
fab506149a
@ -3,7 +3,6 @@ import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
|||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { getOwner } from "@ember/application";
|
|
||||||
|
|
||||||
function pick(object, keys) {
|
function pick(object, keys) {
|
||||||
const result = {};
|
const result = {};
|
||||||
@ -18,6 +17,7 @@ function pick(object, keys) {
|
|||||||
@disableImplicitInjections
|
@disableImplicitInjections
|
||||||
export default class DModalBody extends Component {
|
export default class DModalBody extends Component {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
@tracked fixed = false;
|
@tracked fixed = false;
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ export default class DModalBody extends Component {
|
|||||||
if (fixedParent) {
|
if (fixedParent) {
|
||||||
this.fixed = true;
|
this.fixed = true;
|
||||||
$(fixedParent).modal("show");
|
$(fixedParent).modal("show");
|
||||||
getOwner(this).lookup("controller:modal").hidden = false;
|
this.modal.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appEvents.trigger(
|
this.appEvents.trigger(
|
||||||
|
@ -6,11 +6,11 @@ import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { getOwner } from "@ember/application";
|
|
||||||
|
|
||||||
@disableImplicitInjections
|
@disableImplicitInjections
|
||||||
export default class DModal extends Component {
|
export default class DModal extends Component {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
@service modal;
|
||||||
|
|
||||||
@tracked wrapperElement;
|
@tracked wrapperElement;
|
||||||
@tracked modalBodyData = {};
|
@tracked modalBodyData = {};
|
||||||
@ -147,7 +147,7 @@ export default class DModal extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.fixed) {
|
if (data.fixed) {
|
||||||
getOwner(this).lookup("controller:modal").hidden = false;
|
this.modal.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.modalBodyData = data;
|
this.modalBodyData = data;
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<DModal
|
||||||
|
@modalClass={{concat-class
|
||||||
|
this.modal.modalClass
|
||||||
|
(if this.modal.opts.panels "has-tabs")
|
||||||
|
}}
|
||||||
|
@title={{this.modal.title}}
|
||||||
|
@titleAriaElementId={{this.modal.opts.titleAriaElementId}}
|
||||||
|
@panels={{this.modal.opts.panels}}
|
||||||
|
@selectedPanel={{this.modal.selectedPanel}}
|
||||||
|
@onSelectPanel={{this.modal.onSelectPanel}}
|
||||||
|
@hidden={{this.modal.hidden}}
|
||||||
|
@class="hidden"
|
||||||
|
@errors={{this.modal.errors}}
|
||||||
|
@closeModal={{this.closeModal}}
|
||||||
|
>
|
||||||
|
{{outlet "modalBody"}}
|
||||||
|
</DModal>
|
@ -0,0 +1,12 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
|
export default class ModalContainer extends Component {
|
||||||
|
@service modal;
|
||||||
|
|
||||||
|
@action
|
||||||
|
closeModal(initiatedBy) {
|
||||||
|
this.modal.close(initiatedBy);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +0,0 @@
|
|||||||
import Controller from "@ember/controller";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
|
||||||
|
|
||||||
export default class ModalController extends Controller {
|
|
||||||
@tracked hidden = true;
|
|
||||||
}
|
|
@ -16,8 +16,7 @@ export default Mixin.create({
|
|||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
closeModal() {
|
closeModal() {
|
||||||
this.modal.send("closeModal");
|
this.modal.close();
|
||||||
this.set("panels", []);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -212,11 +212,6 @@ const ApplicationRoute = DiscourseRoute.extend(OpenComposer, {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
renderTemplate() {
|
|
||||||
this.render("application");
|
|
||||||
this.render("modal", { into: "application", outlet: "modal" });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleShowLogin() {
|
handleShowLogin() {
|
||||||
if (this.siteSettings.enable_discourse_connect) {
|
if (this.siteSettings.enable_discourse_connect) {
|
||||||
const returnPath = encodeURIComponent(window.location.pathname);
|
const returnPath = encodeURIComponent(window.location.pathname);
|
||||||
|
@ -3,28 +3,66 @@ import { getOwner } from "@ember/application";
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import { dasherize } from "@ember/string";
|
import { dasherize } from "@ember/string";
|
||||||
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
import { disableImplicitInjections } from "discourse/lib/implicit-injections";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
@disableImplicitInjections
|
@disableImplicitInjections
|
||||||
export default class ModalService extends Service {
|
export default class ModalService extends Service {
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
|
||||||
|
@tracked name;
|
||||||
|
@tracked opts = {};
|
||||||
|
@tracked selectedPanel;
|
||||||
|
@tracked hidden = true;
|
||||||
|
|
||||||
|
@tracked titleOverride;
|
||||||
|
@tracked modalClassOverride;
|
||||||
|
@tracked onSelectPanel;
|
||||||
|
|
||||||
|
get title() {
|
||||||
|
if (this.titleOverride) {
|
||||||
|
return this.titleOverride;
|
||||||
|
} else if (this.opts.titleTranslated) {
|
||||||
|
return this.opts.titleTranslated;
|
||||||
|
} else if (this.opts.title) {
|
||||||
|
return I18n.t(this.opts.title);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set title(value) {
|
||||||
|
this.titleOverride = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get modalClass() {
|
||||||
|
if (!this.#isRendered) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.modalClassOverride ||
|
||||||
|
this.opts.modalClass ||
|
||||||
|
`${dasherize(this.name.replace(/^modals\//, "")).toLowerCase()}-modal`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
set modalClass(value) {
|
||||||
|
this.modalClassOverride = value;
|
||||||
|
}
|
||||||
|
|
||||||
show(name, opts = {}) {
|
show(name, opts = {}) {
|
||||||
const container = getOwner(this);
|
const container = getOwner(this);
|
||||||
const route = container.lookup("route:application");
|
const route = container.lookup("route:application");
|
||||||
const modalController = route.controllerFor("modal");
|
|
||||||
|
|
||||||
modalController.set(
|
this.opts = opts;
|
||||||
"modalClass",
|
|
||||||
opts.modalClass || `${dasherize(name).toLowerCase()}-modal`
|
|
||||||
);
|
|
||||||
|
|
||||||
const controllerName = opts.admin ? `modals/${name}` : name;
|
const controllerName = opts.admin ? `modals/${name}` : name;
|
||||||
modalController.set("name", controllerName);
|
this.name = controllerName;
|
||||||
|
|
||||||
let controller = container.lookup("controller:" + controllerName);
|
let controller = container.lookup("controller:" + controllerName);
|
||||||
const templateName = opts.templateName || dasherize(name);
|
const templateName = opts.templateName || dasherize(name);
|
||||||
|
|
||||||
const renderArgs = { into: "modal", outlet: "modalBody" };
|
const renderArgs = { into: "application", outlet: "modalBody" };
|
||||||
if (controller) {
|
if (controller) {
|
||||||
renderArgs.controller = controllerName;
|
renderArgs.controller = controllerName;
|
||||||
} else {
|
} else {
|
||||||
@ -40,40 +78,15 @@ export default class ModalService extends Service {
|
|||||||
const modalName = `modal/${templateName}`;
|
const modalName = `modal/${templateName}`;
|
||||||
const fullName = opts.admin ? `admin/templates/${modalName}` : modalName;
|
const fullName = opts.admin ? `admin/templates/${modalName}` : modalName;
|
||||||
route.render(fullName, renderArgs);
|
route.render(fullName, renderArgs);
|
||||||
if (opts.title) {
|
|
||||||
modalController.set("title", I18n.t(opts.title));
|
|
||||||
} else if (opts.titleTranslated) {
|
|
||||||
modalController.set("title", opts.titleTranslated);
|
|
||||||
} else {
|
|
||||||
modalController.set("title", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.titleAriaElementId) {
|
|
||||||
modalController.set("titleAriaElementId", opts.titleAriaElementId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.panels) {
|
if (opts.panels) {
|
||||||
modalController.setProperties({
|
|
||||||
panels: opts.panels,
|
|
||||||
selectedPanel: opts.panels[0],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (controller.actions.onSelectPanel) {
|
if (controller.actions.onSelectPanel) {
|
||||||
modalController.set(
|
this.onSelectPanel = controller.actions.onSelectPanel.bind(controller);
|
||||||
"onSelectPanel",
|
|
||||||
controller.actions.onSelectPanel.bind(controller)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
this.selectedPanel = opts.panels[0];
|
||||||
modalController.set(
|
|
||||||
"modalClass",
|
|
||||||
`${modalController.get("modalClass")} has-tabs`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
modalController.setProperties({ panels: [], selectedPanel: null });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
controller.set("modal", modalController);
|
controller.set("modal", this);
|
||||||
const model = opts.model;
|
const model = opts.model;
|
||||||
if (model) {
|
if (model) {
|
||||||
controller.set("model", model);
|
controller.set("model", model);
|
||||||
@ -87,44 +100,44 @@ export default class ModalService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close(initiatedBy) {
|
close(initiatedBy) {
|
||||||
const route = getOwner(this).lookup("route:application");
|
const controllerName = this.name;
|
||||||
let modalController = route.controllerFor("modal");
|
const controller = controllerName
|
||||||
const controllerName = modalController.get("name");
|
? getOwner(this).lookup(`controller:${controllerName}`)
|
||||||
|
: null;
|
||||||
|
|
||||||
if (controllerName) {
|
if (controller?.beforeClose?.() === false) {
|
||||||
const controller = getOwner(this).lookup(`controller:${controllerName}`);
|
return;
|
||||||
if (controller && controller.beforeClose) {
|
|
||||||
if (false === controller.beforeClose()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOwner(this)
|
getOwner(this)
|
||||||
.lookup("route:application")
|
.lookup("route:application")
|
||||||
.render("hide-modal", { into: "modal", outlet: "modalBody" });
|
.render("hide-modal", { into: "application", outlet: "modalBody" });
|
||||||
$(".d-modal.fixed-modal").modal("hide");
|
$(".d-modal.fixed-modal").modal("hide");
|
||||||
|
|
||||||
if (controllerName) {
|
if (controller) {
|
||||||
const controller = getOwner(this).lookup(`controller:${controllerName}`);
|
this.appEvents.trigger("modal:closed", {
|
||||||
|
name: controllerName,
|
||||||
|
controller,
|
||||||
|
});
|
||||||
|
|
||||||
if (controller) {
|
if (controller.onClose) {
|
||||||
this.appEvents.trigger("modal:closed", {
|
controller.onClose({
|
||||||
name: controllerName,
|
initiatedByCloseButton: initiatedBy === "initiatedByCloseButton",
|
||||||
controller,
|
initiatedByClickOut: initiatedBy === "initiatedByClickOut",
|
||||||
|
initiatedByESC: initiatedBy === "initiatedByESC",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (controller.onClose) {
|
|
||||||
controller.onClose({
|
|
||||||
initiatedByCloseButton: initiatedBy === "initiatedByCloseButton",
|
|
||||||
initiatedByClickOut: initiatedBy === "initiatedByClickOut",
|
|
||||||
initiatedByESC: initiatedBy === "initiatedByESC",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
modalController.set("name", null);
|
|
||||||
}
|
}
|
||||||
modalController.hidden = true;
|
this.hidden = true;
|
||||||
|
|
||||||
|
this.name =
|
||||||
|
this.selectedPanel =
|
||||||
|
this.modalClassOverride =
|
||||||
|
this.titleOverride =
|
||||||
|
this.onSelectPanel =
|
||||||
|
null;
|
||||||
|
|
||||||
|
this.opts = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
@ -134,4 +147,8 @@ export default class ModalService extends Service {
|
|||||||
reopen() {
|
reopen() {
|
||||||
$(".d-modal.fixed-modal").modal("show");
|
$(".d-modal.fixed-modal").modal("show");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get #isRendered() {
|
||||||
|
return !!this.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@
|
|||||||
@outletArgs={{hash showFooter=this.showFooter}}
|
@outletArgs={{hash showFooter=this.showFooter}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{outlet "modal"}}
|
<ModalContainer />
|
||||||
<DialogHolder />
|
<DialogHolder />
|
||||||
<TopicEntrance />
|
<TopicEntrance />
|
||||||
<ComposerContainer />
|
<ComposerContainer />
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
<DModal
|
|
||||||
@modalClass={{this.modalClass}}
|
|
||||||
@title={{this.title}}
|
|
||||||
@titleAriaElementId={{this.titleAriaElementId}}
|
|
||||||
@subtitle={{this.subtitle}}
|
|
||||||
@panels={{this.panels}}
|
|
||||||
@selectedPanel={{this.selectedPanel}}
|
|
||||||
@onSelectPanel={{this.onSelectPanel}}
|
|
||||||
@hidden={{this.hidden}}
|
|
||||||
@errors={{this.errors}}
|
|
||||||
@closeModal={{route-action "closeModal"}}
|
|
||||||
>
|
|
||||||
{{outlet "modalBody"}}
|
|
||||||
</DModal>
|
|
@ -185,7 +185,7 @@ export default createWidget("private-message-map", {
|
|||||||
this.attach("button", {
|
this.attach("button", {
|
||||||
action: "showInvite",
|
action: "showInvite",
|
||||||
icon: "plus",
|
icon: "plus",
|
||||||
className: "btn btn-default no-text btn-icon",
|
className: "btn btn-default no-text btn-icon add-participant-btn",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ acceptance("Do not disturb", function (needs) {
|
|||||||
|
|
||||||
await click(tiles[0]);
|
await click(tiles[0]);
|
||||||
|
|
||||||
assert.ok(query(".do-not-disturb-modal.hidden"), "modal is hidden");
|
assert.ok(query(".d-modal.hidden"), "modal is hidden");
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
exists(".header-dropdown-toggle .do-not-disturb-background .d-icon-moon"),
|
exists(".header-dropdown-toggle .do-not-disturb-background .d-icon-moon"),
|
||||||
@ -69,7 +69,7 @@ acceptance("Do not disturb", function (needs) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
query(".do-not-disturb-modal.hidden"),
|
query(".d-modal.hidden"),
|
||||||
"DND modal is hidden after making a choice"
|
"DND modal is hidden after making a choice"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -38,15 +38,15 @@ acceptance("Modal", function (needs) {
|
|||||||
await click(".login-button");
|
await click(".login-button");
|
||||||
assert.strictEqual(count(".d-modal:visible"), 1, "modal should appear");
|
assert.strictEqual(count(".d-modal:visible"), 1, "modal should appear");
|
||||||
|
|
||||||
const controller = getOwner(this).lookup("controller:modal");
|
const service = getOwner(this).lookup("service:modal");
|
||||||
assert.strictEqual(controller.name, "login");
|
assert.strictEqual(service.name, "login");
|
||||||
|
|
||||||
await click(".modal-outer-container");
|
await click(".modal-outer-container");
|
||||||
assert.ok(
|
assert.ok(
|
||||||
!exists(".d-modal:visible"),
|
!exists(".d-modal:visible"),
|
||||||
"modal should disappear when you click outside"
|
"modal should disappear when you click outside"
|
||||||
);
|
);
|
||||||
assert.strictEqual(controller.name, null);
|
assert.strictEqual(service.name, null);
|
||||||
|
|
||||||
await click(".login-button");
|
await click(".login-button");
|
||||||
assert.strictEqual(count(".d-modal:visible"), 1, "modal should reappear");
|
assert.strictEqual(count(".d-modal:visible"), 1, "modal should reappear");
|
||||||
|
@ -72,3 +72,17 @@ acceptance("Personal Message (regular user)", function (needs) {
|
|||||||
assert.true(DiscourseURL.redirectTo.calledWith("/"));
|
assert.true(DiscourseURL.redirectTo.calledWith("/"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
acceptance("Personal Message - invite", function (needs) {
|
||||||
|
needs.user();
|
||||||
|
|
||||||
|
test("suggested messages", async function (assert) {
|
||||||
|
await visit("/t/pm-for-testing/12");
|
||||||
|
await click(".add-remove-participant-btn");
|
||||||
|
await click(".private-message-map .controls .add-participant-btn");
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom(".d-modal.share-and-invite .invite-user-control")
|
||||||
|
.exists("invite modal is displayed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user