DEV: Add model transformer plugin API (#18081)
This API allows plugins to transform a list of model objects before they're rendered in the UI. At the moment, this API is limited to items/lists of the experimental user menu, but it may be extended in the future to other parts of the app. Additional context can be found in https://github.com/discourse/discourse/pull/18046.
This commit is contained in:
parent
9ebebfb4cc
commit
40fd82e2d1
|
@ -5,6 +5,7 @@ import showModal from "discourse/lib/show-modal";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
|
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
|
||||||
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
|
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
|
||||||
|
import Topic from "discourse/models/topic";
|
||||||
|
|
||||||
export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||||
get dismissTypes() {
|
get dismissTypes() {
|
||||||
|
@ -47,7 +48,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||||
fetchItems() {
|
fetchItems() {
|
||||||
return ajax(
|
return ajax(
|
||||||
`/u/${this.currentUser.username}/user-menu-private-messages`
|
`/u/${this.currentUser.username}/user-menu-private-messages`
|
||||||
).then((data) => {
|
).then(async (data) => {
|
||||||
const content = [];
|
const content = [];
|
||||||
data.notifications.forEach((rawNotification) => {
|
data.notifications.forEach((rawNotification) => {
|
||||||
const notification = Notification.create(rawNotification);
|
const notification = Notification.create(rawNotification);
|
||||||
|
@ -60,8 +61,10 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
const topics = data.topics.map((t) => Topic.create(t));
|
||||||
|
await Topic.applyTransformations(topics);
|
||||||
content.push(
|
content.push(
|
||||||
...data.topics.map((topic) => {
|
...topics.map((topic) => {
|
||||||
return new UserMenuMessageItem({ message: topic });
|
return new UserMenuMessageItem({ message: topic });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { consolePrefix } from "discourse/lib/source-identifier";
|
||||||
|
|
||||||
|
let modelTransformersMap = {};
|
||||||
|
|
||||||
|
export function registerModelTransformer(modelName, func) {
|
||||||
|
if (!modelTransformersMap[modelName]) {
|
||||||
|
modelTransformersMap[modelName] = [];
|
||||||
|
}
|
||||||
|
const transformer = {
|
||||||
|
prefix: consolePrefix(),
|
||||||
|
execute: func,
|
||||||
|
};
|
||||||
|
modelTransformersMap[modelName].push(transformer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyModelTransformations(modelName, models) {
|
||||||
|
for (const transformer of modelTransformersMap[modelName] || []) {
|
||||||
|
try {
|
||||||
|
await transformer.execute(models);
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
transformer.prefix,
|
||||||
|
`transformer for the \`${modelName}\` model failed with:`,
|
||||||
|
err,
|
||||||
|
err.stack
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetModelTransformers() {
|
||||||
|
modelTransformersMap = {};
|
||||||
|
}
|
|
@ -100,6 +100,7 @@ import { addSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||||
import DiscourseURL from "discourse/lib/url";
|
import DiscourseURL from "discourse/lib/url";
|
||||||
import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager";
|
import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager";
|
||||||
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
import { registerUserMenuTab } from "discourse/lib/user-menu/tab";
|
||||||
|
import { registerModelTransformer } from "discourse/lib/model-transformers";
|
||||||
|
|
||||||
// If you add any methods to the API ensure you bump up the version number
|
// If you add any methods to the API ensure you bump up the version number
|
||||||
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||||
|
@ -1926,6 +1927,37 @@ class PluginApi {
|
||||||
registerUserMenuTab(func) {
|
registerUserMenuTab(func) {
|
||||||
registerUserMenuTab(func);
|
registerUserMenuTab(func);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EXPERIMENTAL. Do not use.
|
||||||
|
* Apply transformation using a callback on a list of model instances of a
|
||||||
|
* specific type. Currently, this API only works on lists rendered in the
|
||||||
|
* user menu such as notifications, bookmarks and topics (i.e. messages), but
|
||||||
|
* it may be extended to other lists in other parts of the app.
|
||||||
|
*
|
||||||
|
* You can pass an `async` callback to this API and it'll be `await`ed and
|
||||||
|
* block rendering until the callback finishes executing.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* api.registerModelTransformer("topic", async (topics) => {
|
||||||
|
* for (const topic of topics) {
|
||||||
|
* const decryptedTitle = await decryptTitle(topic.encrypted_title);
|
||||||
|
* if (decryptedTitle) {
|
||||||
|
* topic.fancy_title = decryptedTitle;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @callback registerModelTransformerCallback
|
||||||
|
* @param {Object[]} A list of model instances
|
||||||
|
*
|
||||||
|
* @param {string} modelName - Model type on which transformation should be applied. Currently the only valid type is "topic".
|
||||||
|
* @param {registerModelTransformerCallback} transformer - Callback function that receives a list of model objects of the specified type and applies transformation on them.
|
||||||
|
*/
|
||||||
|
registerModelTransformer(modelName, transformer) {
|
||||||
|
registerModelTransformer(modelName, transformer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
|
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { resolveShareUrl } from "discourse/helpers/share-url";
|
import { resolveShareUrl } from "discourse/helpers/share-url";
|
||||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||||
import deprecated from "discourse-common/lib/deprecated";
|
import deprecated from "discourse-common/lib/deprecated";
|
||||||
|
import { applyModelTransformations } from "discourse/lib/model-transformers";
|
||||||
|
|
||||||
export function loadTopicView(topic, args) {
|
export function loadTopicView(topic, args) {
|
||||||
const data = deepMerge({}, args);
|
const data = deepMerge({}, args);
|
||||||
|
@ -866,6 +867,10 @@ Topic.reopenClass({
|
||||||
|
|
||||||
return ajax(`/t/${topicId}/slow_mode`, { type: "PUT", data });
|
return ajax(`/t/${topicId}/slow_mode`, { type: "PUT", data });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async applyTransformations(topics) {
|
||||||
|
await applyModelTransformations("topic", topics);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function moveResult(result) {
|
function moveResult(result) {
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { withPluginApi } from "discourse/lib/plugin-api";
|
||||||
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
|
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
|
||||||
import UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
import UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
||||||
import TopicFixtures from "discourse/tests/fixtures/topic";
|
import TopicFixtures from "discourse/tests/fixtures/topic";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
import { later } from "@ember/runloop";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
|
|
||||||
acceptance("User menu", function (needs) {
|
acceptance("User menu", function (needs) {
|
||||||
|
@ -187,6 +189,33 @@ acceptance("User menu", function (needs) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("messages tab applies model transformations registered by plugins", async function (assert) {
|
||||||
|
withPluginApi("0.1", (api) => {
|
||||||
|
api.registerModelTransformer("topic", (topics) => {
|
||||||
|
topics.forEach((topic) => {
|
||||||
|
topic.fancy_title = `pluginTransformer#1 ${topic.fancy_title}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
api.registerModelTransformer("topic", async (topics) => {
|
||||||
|
// sleep 1 ms
|
||||||
|
await new Promise((resolve) => later(resolve, 1));
|
||||||
|
topics.forEach((topic) => {
|
||||||
|
topic.fancy_title = `pluginTransformer#2 ${topic.fancy_title}`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await visit("/");
|
||||||
|
await click(".d-header-icons .current-user");
|
||||||
|
await click("#user-menu-button-messages");
|
||||||
|
|
||||||
|
const messages = queryAll("#quick-access-messages ul li.message");
|
||||||
|
assert.strictEqual(
|
||||||
|
messages[0].textContent.replace(/\s+/g, " ").trim(),
|
||||||
|
"mixtape pluginTransformer#2 pluginTransformer#1 BUG: Can not render emoji properly"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("the profile tab", async function (assert) {
|
test("the profile tab", async function (assert) {
|
||||||
updateCurrentUser({ draft_count: 13 });
|
updateCurrentUser({ draft_count: 13 });
|
||||||
await visit("/");
|
await visit("/");
|
||||||
|
|
|
@ -136,52 +136,6 @@ export default {
|
||||||
primary_group_id: null,
|
primary_group_id: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fancy_title: "BUG: Can not render emoji properly :confused:",
|
|
||||||
slug: "bug-can-not-render-emoji-properly",
|
|
||||||
posts_count: 1,
|
|
||||||
reply_count: 0,
|
|
||||||
highest_post_number: 2,
|
|
||||||
image_url: null,
|
|
||||||
created_at: "2019-07-26T01:29:24.008Z",
|
|
||||||
last_posted_at: "2019-07-26T01:29:24.177Z",
|
|
||||||
bumped: true,
|
|
||||||
bumped_at: "2019-07-26T01:29:24.177Z",
|
|
||||||
unseen: false,
|
|
||||||
last_read_post_number: 2,
|
|
||||||
unread_posts: 0,
|
|
||||||
pinned: false,
|
|
||||||
unpinned: null,
|
|
||||||
visible: true,
|
|
||||||
closed: false,
|
|
||||||
archived: false,
|
|
||||||
notification_level: 3,
|
|
||||||
bookmarked: false,
|
|
||||||
bookmarks: [],
|
|
||||||
liked: false,
|
|
||||||
views: 5,
|
|
||||||
like_count: 0,
|
|
||||||
has_summary: false,
|
|
||||||
archetype: "private_message",
|
|
||||||
last_poster_username: "mixtape",
|
|
||||||
category_id: null,
|
|
||||||
pinned_globally: false,
|
|
||||||
featured_link: null,
|
|
||||||
posters: [
|
|
||||||
{
|
|
||||||
extras: "latest single",
|
|
||||||
description: "Original Poster, Most Recent Poster",
|
|
||||||
user_id: 13,
|
|
||||||
primary_group_id: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
participants: [
|
|
||||||
{
|
|
||||||
extras: "latest",
|
|
||||||
description: null,
|
|
||||||
user_id: 13,
|
|
||||||
primary_group_id: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ import { resetSidebarSection } from "discourse/lib/sidebar/custom-sections";
|
||||||
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
|
import { resetNotificationTypeRenderers } from "discourse/lib/notification-types-manager";
|
||||||
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
import { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||||
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
||||||
|
import { resetModelTransformers } from "discourse/lib/model-transformers";
|
||||||
|
|
||||||
export function currentUser() {
|
export function currentUser() {
|
||||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||||
|
@ -206,6 +207,7 @@ export function testCleanup(container, app) {
|
||||||
clearExtraHeaderIcons();
|
clearExtraHeaderIcons();
|
||||||
resetUserMenuTabs();
|
resetUserMenuTabs();
|
||||||
resetLinkLookup();
|
resetLinkLookup();
|
||||||
|
resetModelTransformers();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function discourseModule(name, options) {
|
export function discourseModule(name, options) {
|
||||||
|
|
Loading…
Reference in New Issue