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 UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
|
||||
import UserMenuMessageItem from "discourse/lib/user-menu/message-item";
|
||||
import Topic from "discourse/models/topic";
|
||||
|
||||
export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
||||
get dismissTypes() {
|
||||
|
@ -47,7 +48,7 @@ export default class UserMenuMessagesList extends UserMenuNotificationsList {
|
|||
fetchItems() {
|
||||
return ajax(
|
||||
`/u/${this.currentUser.username}/user-menu-private-messages`
|
||||
).then((data) => {
|
||||
).then(async (data) => {
|
||||
const content = [];
|
||||
data.notifications.forEach((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(
|
||||
...data.topics.map((topic) => {
|
||||
...topics.map((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 { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager";
|
||||
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
|
||||
// based on Semantic Versioning 2.0.0. Please update the changelog at
|
||||
|
@ -1926,6 +1927,37 @@ class PluginApi {
|
|||
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
|
||||
|
|
|
@ -22,6 +22,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
|||
import { resolveShareUrl } from "discourse/helpers/share-url";
|
||||
import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import { applyModelTransformations } from "discourse/lib/model-transformers";
|
||||
|
||||
export function loadTopicView(topic, args) {
|
||||
const data = deepMerge({}, args);
|
||||
|
@ -866,6 +867,10 @@ Topic.reopenClass({
|
|||
|
||||
return ajax(`/t/${topicId}/slow_mode`, { type: "PUT", data });
|
||||
},
|
||||
|
||||
async applyTransformations(topics) {
|
||||
await applyModelTransformations("topic", topics);
|
||||
},
|
||||
});
|
||||
|
||||
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 UserMenuFixtures from "discourse/tests/fixtures/user-menu";
|
||||
import TopicFixtures from "discourse/tests/fixtures/topic";
|
||||
import { Promise } from "rsvp";
|
||||
import { later } from "@ember/runloop";
|
||||
import I18n from "I18n";
|
||||
|
||||
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) {
|
||||
updateCurrentUser({ draft_count: 13 });
|
||||
await visit("/");
|
||||
|
|
|
@ -136,52 +136,6 @@ export default {
|
|||
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 { resetUserMenuTabs } from "discourse/lib/user-menu/tab";
|
||||
import { reset as resetLinkLookup } from "discourse/lib/link-lookup";
|
||||
import { resetModelTransformers } from "discourse/lib/model-transformers";
|
||||
|
||||
export function currentUser() {
|
||||
return User.create(sessionFixtures["/session/current.json"].current_user);
|
||||
|
@ -206,6 +207,7 @@ export function testCleanup(container, app) {
|
|||
clearExtraHeaderIcons();
|
||||
resetUserMenuTabs();
|
||||
resetLinkLookup();
|
||||
resetModelTransformers();
|
||||
}
|
||||
|
||||
export function discourseModule(name, options) {
|
||||
|
|
Loading…
Reference in New Issue