DEV: Support theme/plugin overrides of colocated component templates (#19237)
To theme/plugin developers, the process is the same as for overriding non-colocated component templates. Once merged, this should allow us to seamlessly convert all of core's component templates to be colocated.
This commit is contained in:
parent
105f500693
commit
9f022112e3
|
@ -0,0 +1,58 @@
|
|||
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
|
||||
import * as GlimmerManager from "@glimmer/manager";
|
||||
|
||||
const COLOCATED_TEMPLATE_OVERRIDES = new Map();
|
||||
|
||||
// This patch is not ideal, but Ember does not allow us to change a component template after initial association
|
||||
// https://github.com/glimmerjs/glimmer-vm/blob/03a4b55c03/packages/%40glimmer/manager/lib/public/template.ts#L14-L20
|
||||
const originalGetTemplate = GlimmerManager.getComponentTemplate;
|
||||
GlimmerManager.getComponentTemplate = (component) => {
|
||||
return (
|
||||
COLOCATED_TEMPLATE_OVERRIDES.get(component) ??
|
||||
originalGetTemplate(component)
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "colocated-template-overrides",
|
||||
after: "populate-template-map",
|
||||
|
||||
initialize(container) {
|
||||
this.eachThemePluginTemplate((templateKey, moduleNames) => {
|
||||
if (!templateKey.startsWith("components/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DiscourseTemplateMap.coreTemplates.has(templateKey)) {
|
||||
// It's a non-colocated core component. Template will be overridden at runtime.
|
||||
return;
|
||||
}
|
||||
|
||||
const componentName = templateKey.slice("components/".length);
|
||||
const component = container.owner.resolveRegistration(
|
||||
`component:${componentName}`
|
||||
);
|
||||
|
||||
if (component && originalGetTemplate(component)) {
|
||||
const finalOverrideModuleName = moduleNames[moduleNames.length - 1];
|
||||
const overrideTemplate = require(finalOverrideModuleName).default;
|
||||
|
||||
COLOCATED_TEMPLATE_OVERRIDES.set(component, overrideTemplate);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
eachThemePluginTemplate(cb) {
|
||||
for (const [key, value] of DiscourseTemplateMap.pluginTemplates) {
|
||||
cb(key, value);
|
||||
}
|
||||
|
||||
for (const [key, value] of DiscourseTemplateMap.themeTemplates) {
|
||||
cb(key, value);
|
||||
}
|
||||
},
|
||||
|
||||
teardown() {
|
||||
COLOCATED_TEMPLATE_OVERRIDES.clear();
|
||||
},
|
||||
};
|
|
@ -2,11 +2,11 @@ import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
|||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
|
||||
|
||||
acceptance("CustomHTML template", function (needs) {
|
||||
needs.hooks.beforeEach(() => {
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
"discourse/templates/top",
|
||||
hbs`<span class='top-span'>TOP</span>`
|
||||
);
|
||||
|
|
|
@ -10,7 +10,7 @@ import { test } from "qunit";
|
|||
import I18n from "I18n";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { registerTemplateModule } from "../helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
|
||||
acceptance("Modal", function (needs) {
|
||||
let _translations;
|
||||
|
@ -54,7 +54,7 @@ acceptance("Modal", function (needs) {
|
|||
await triggerKeyEvent("#main-outlet", "keydown", "Escape");
|
||||
assert.ok(!exists(".d-modal:visible"), "ESC should close the modal");
|
||||
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
"discourse/templates/modal/not-dismissable",
|
||||
hbs`{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}`
|
||||
);
|
||||
|
@ -79,7 +79,7 @@ acceptance("Modal", function (needs) {
|
|||
});
|
||||
|
||||
test("rawTitle in modal panels", async function (assert) {
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
"discourse/templates/modal/test-raw-title-panels",
|
||||
hbs``
|
||||
);
|
||||
|
@ -100,8 +100,8 @@ acceptance("Modal", function (needs) {
|
|||
});
|
||||
|
||||
test("modal title", async function (assert) {
|
||||
registerTemplateModule("discourse/templates/modal/test-title", hbs``);
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule("discourse/templates/modal/test-title", hbs``);
|
||||
registerTemporaryModule(
|
||||
"discourse/templates/modal/test-title-with-body",
|
||||
hbs`{{#d-modal-body}}test{{/d-modal-body}}`
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { action } from "@ember/object";
|
|||
import { extraConnectorClass } from "discourse/lib/plugin-connectors";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { test } from "qunit";
|
||||
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
|
||||
|
||||
const PREFIX = "discourse/plugins/some-plugin/templates/connectors";
|
||||
|
||||
|
@ -49,19 +49,19 @@ acceptance("Plugin Outlet - Connector Class", function (needs) {
|
|||
},
|
||||
});
|
||||
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
`${PREFIX}/user-profile-primary/hello`,
|
||||
hbs`<span class='hello-username'>{{model.username}}</span>
|
||||
<button class='say-hello' {{on "click" (action "sayHello")}}></button>
|
||||
<button class='say-hello-using-this' {{on "click" this.sayHello}}></button>
|
||||
<span class='hello-result'>{{hello}}</span>`
|
||||
);
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
`${PREFIX}/user-profile-primary/hi`,
|
||||
hbs`<button class='say-hi' {{on "click" (action "sayHi")}}></button>
|
||||
<span class='hi-result'>{{hi}}</span>`
|
||||
);
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
`${PREFIX}/user-profile-primary/dont-render`,
|
||||
hbs`I'm not rendered!`
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@ import { hbs } from "ember-cli-htmlbars";
|
|||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { registerTemplateModule } from "../helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
|
||||
const PREFIX = "discourse/plugins/some-plugin/templates/connectors";
|
||||
|
||||
|
@ -15,11 +15,11 @@ acceptance("Plugin Outlet - Decorator", function (needs) {
|
|||
needs.user();
|
||||
|
||||
needs.hooks.beforeEach(() => {
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
`${PREFIX}/discovery-list-container-top/foo`,
|
||||
hbs`FOO`
|
||||
);
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
`${PREFIX}/discovery-list-container-top/bar`,
|
||||
hbs`BAR`
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { registerTemplateModule } from "../helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
|
||||
const HELLO =
|
||||
"discourse/plugins/my-plugin/templates/connectors/user-profile-primary/hello";
|
||||
|
@ -15,8 +15,11 @@ const GOODBYE =
|
|||
|
||||
acceptance("Plugin Outlet - Multi Template", function (needs) {
|
||||
needs.hooks.beforeEach(() => {
|
||||
registerTemplateModule(HELLO, hbs`<span class='hello-span'>Hello</span>`);
|
||||
registerTemplateModule(GOODBYE, hbs`<span class='bye-span'>Goodbye</span>`);
|
||||
registerTemporaryModule(HELLO, hbs`<span class='hello-span'>Hello</span>`);
|
||||
registerTemporaryModule(
|
||||
GOODBYE,
|
||||
hbs`<span class='bye-span'>Goodbye</span>`
|
||||
);
|
||||
});
|
||||
|
||||
test("Renders a template into the outlet", async function (assert) {
|
||||
|
|
|
@ -6,14 +6,14 @@ import {
|
|||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { test } from "qunit";
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { registerTemplateModule } from "../helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
|
||||
const CONNECTOR_MODULE =
|
||||
"discourse/theme-12/templates/connectors/user-profile-primary/hello";
|
||||
|
||||
acceptance("Plugin Outlet - Single Template", function (needs) {
|
||||
needs.hooks.beforeEach(() => {
|
||||
registerTemplateModule(
|
||||
registerTemporaryModule(
|
||||
CONNECTOR_MODULE,
|
||||
hbs`<span class='hello-username'>{{model.username}}</span>`
|
||||
);
|
||||
|
|
|
@ -76,7 +76,7 @@ import { resetNotificationTypeRenderers } from "discourse/lib/notification-types
|
|||
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
||||
import { resetModelTransformers } from "discourse/lib/model-transformers";
|
||||
import { cleanupTemporaryTemplateRegistrations } from "./template-module-helper";
|
||||
import { cleanupTemporaryModuleRegistrations } from "./temporary-module-helper";
|
||||
|
||||
export function currentUser() {
|
||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||
|
@ -208,7 +208,7 @@ export function testCleanup(container, app) {
|
|||
resetUserMenuTabs();
|
||||
resetLinkLookup();
|
||||
resetModelTransformers();
|
||||
cleanupTemporaryTemplateRegistrations();
|
||||
cleanupTemporaryModuleRegistrations();
|
||||
}
|
||||
|
||||
export function discourseModule(name, options) {
|
||||
|
|
|
@ -3,28 +3,28 @@ import { expireConnectorCache } from "discourse/lib/plugin-connectors";
|
|||
|
||||
const modifications = [];
|
||||
|
||||
function generateTemplateModule(template) {
|
||||
function generateTemporaryModule(defaultExport) {
|
||||
return function (_exports) {
|
||||
Object.defineProperty(_exports, "__esModule", {
|
||||
value: true,
|
||||
});
|
||||
_exports.default = template;
|
||||
_exports.default = defaultExport;
|
||||
};
|
||||
}
|
||||
|
||||
export function registerTemplateModule(moduleName, template) {
|
||||
export function registerTemporaryModule(moduleName, defaultExport) {
|
||||
const modificationData = {
|
||||
moduleName,
|
||||
existingModule: requirejs.entries[moduleName],
|
||||
};
|
||||
delete requirejs.entries[moduleName];
|
||||
define(moduleName, ["exports"], generateTemplateModule(template));
|
||||
define(moduleName, ["exports"], generateTemporaryModule(defaultExport));
|
||||
modifications.push(modificationData);
|
||||
expireConnectorCache();
|
||||
DiscourseTemplateMap.setModuleNames(Object.keys(requirejs.entries));
|
||||
}
|
||||
|
||||
export function cleanupTemporaryTemplateRegistrations() {
|
||||
export function cleanupTemporaryModuleRegistrations() {
|
||||
for (const modificationData of modifications.reverse()) {
|
||||
const { moduleName, existingModule } = modificationData;
|
||||
delete requirejs.entries[moduleName];
|
|
@ -0,0 +1,172 @@
|
|||
import { assert, module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { render } from "@ember/test-helpers";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
import { registerTemporaryModule } from "../helpers/temporary-module-helper";
|
||||
import { setComponentTemplate } from "@glimmer/manager";
|
||||
import Component from "@glimmer/component";
|
||||
|
||||
class MockColocatedComponent extends Component {}
|
||||
setComponentTemplate(hbs`Colocated Original`, MockColocatedComponent);
|
||||
|
||||
class MockResolvedComponent extends Component {}
|
||||
const MockResolvedComponentTemplate = hbs`Resolved Original`;
|
||||
|
||||
const TestTemplate = hbs`
|
||||
<div id='mock-colocated'><MockColocated /></div>
|
||||
<div id='mock-resolved'><MockResolved /></div>
|
||||
`;
|
||||
|
||||
function registerBaseComponents(namespace = "discourse") {
|
||||
registerTemporaryModule(
|
||||
`${namespace}/components/mock-colocated`,
|
||||
MockColocatedComponent
|
||||
);
|
||||
registerTemporaryModule(
|
||||
`${namespace}/components/mock-resolved`,
|
||||
MockResolvedComponent
|
||||
);
|
||||
registerTemporaryModule(
|
||||
`${namespace}/templates/components/mock-resolved`,
|
||||
MockResolvedComponentTemplate
|
||||
);
|
||||
}
|
||||
|
||||
function registerThemeOverrides() {
|
||||
registerTemporaryModule(
|
||||
"discourse/theme-12/discourse/templates/components/mock-colocated",
|
||||
hbs`Colocated Theme Override`
|
||||
);
|
||||
registerTemporaryModule(
|
||||
"discourse/theme-12/discourse/templates/components/mock-resolved",
|
||||
hbs`Resolved Theme Override`
|
||||
);
|
||||
}
|
||||
|
||||
function registerPluginOverrides() {
|
||||
registerTemporaryModule(
|
||||
`discourse/plugins/some-plugin-name/discourse/templates/components/mock-colocated`,
|
||||
hbs`Colocated Plugin Override`
|
||||
);
|
||||
registerTemporaryModule(
|
||||
`discourse/plugins/some-plugin-name/discourse/templates/components/mock-resolved`,
|
||||
hbs`Resolved Plugin Override`
|
||||
);
|
||||
}
|
||||
|
||||
function registerOtherPluginOverrides() {
|
||||
registerTemporaryModule(
|
||||
`discourse/plugins/other-plugin-name/discourse/templates/components/mock-colocated`,
|
||||
hbs`Colocated Other Plugin Override`
|
||||
);
|
||||
registerTemporaryModule(
|
||||
`discourse/plugins/other-plugin-name/discourse/templates/components/mock-resolved`,
|
||||
hbs`Resolved Other Plugin Override`
|
||||
);
|
||||
}
|
||||
|
||||
module("Integration | Initializers | template-overrides", function () {
|
||||
module("with no overrides", function (hooks) {
|
||||
hooks.beforeEach(() => registerBaseComponents());
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("renders core templates when there are no overrides", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText("Colocated Original", "colocated component correct");
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText("Resolved Original", "resolved component correct");
|
||||
});
|
||||
});
|
||||
|
||||
module("with theme overrides", function (hooks) {
|
||||
hooks.beforeEach(() => registerBaseComponents());
|
||||
hooks.beforeEach(registerThemeOverrides);
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("theme overrides are used", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText("Colocated Theme Override", "colocated component correct");
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText("Resolved Theme Override", "resolved component correct");
|
||||
});
|
||||
});
|
||||
|
||||
module("with plugin overrides", function (hooks) {
|
||||
hooks.beforeEach(() => registerBaseComponents());
|
||||
hooks.beforeEach(registerPluginOverrides);
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("plugin overrides are used", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText("Colocated Plugin Override", "colocated component correct");
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText("Resolved Plugin Override", "resolved component correct");
|
||||
});
|
||||
});
|
||||
|
||||
module("with theme and plugin overrides", function (hooks) {
|
||||
hooks.beforeEach(registerPluginOverrides);
|
||||
hooks.beforeEach(registerThemeOverrides);
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("plugin overrides are used", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText("Colocated Theme Override", "colocated component correct");
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText("Resolved Theme Override", "resolved component correct");
|
||||
});
|
||||
});
|
||||
|
||||
module("with multiple plugin overrides", function (hooks) {
|
||||
hooks.beforeEach(() => registerBaseComponents());
|
||||
hooks.beforeEach(registerPluginOverrides);
|
||||
hooks.beforeEach(registerOtherPluginOverrides);
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("last-defined plugin overrides are used", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText(
|
||||
"Colocated Other Plugin Override",
|
||||
"colocated component correct"
|
||||
);
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText(
|
||||
"Resolved Other Plugin Override",
|
||||
"resolved component correct"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
module("theme overriding plugin", function (hooks) {
|
||||
hooks.beforeEach(() =>
|
||||
registerBaseComponents("discourse/plugins/base-plugin/discourse")
|
||||
);
|
||||
hooks.beforeEach(registerThemeOverrides);
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("theme overrides plugin component", async function () {
|
||||
await render(TestTemplate);
|
||||
assert
|
||||
.dom("#mock-colocated")
|
||||
.hasText("Colocated Theme Override", "colocated component correct");
|
||||
assert
|
||||
.dom("#mock-resolved")
|
||||
.hasText("Resolved Theme Override", "resolved component correct");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import { buildResolver, setResolverOption } from "discourse-common/resolver";
|
||||
import { module, test } from "qunit";
|
||||
import { registerTemplateModule } from "discourse/tests/helpers/template-module-helper";
|
||||
import { registerTemporaryModule } from "discourse/tests/helpers/temporary-module-helper";
|
||||
import DiscourseTemplateMap from "discourse-common/lib/discourse-template-map";
|
||||
|
||||
let resolver;
|
||||
|
@ -13,7 +13,7 @@ function lookupTemplate(assert, name, expectedTemplate, message) {
|
|||
|
||||
function setTemplates(templateModuleNames) {
|
||||
for (const name of templateModuleNames) {
|
||||
registerTemplateModule(name, name);
|
||||
registerTemporaryModule(name, name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue