DEV: Ensure `post.updateFromPost` syncs tracked properties (#29970)

This commit ensures that tracked properties added to the post model are correctly synced when using `post.updateFromPost`.

It also introduces a plugin API to allow plugins to register new tracked properties in the post model without needing to modify the class.
This commit is contained in:
Sérgio Saquetim 2024-11-28 17:19:35 -03:00 committed by GitHub
parent 607dd2cbd8
commit a710d3f377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 112 additions and 16 deletions

View File

@ -3,7 +3,7 @@
// docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version // docs/CHANGELOG-JAVASCRIPT-PLUGIN-API.md whenever you change the version
// using the format described at https://keepachangelog.com/en/1.0.0/. // using the format described at https://keepachangelog.com/en/1.0.0/.
export const PLUGIN_API_VERSION = "1.38.0"; export const PLUGIN_API_VERSION = "1.39.0";
import $ from "jquery"; import $ from "jquery";
import { h } from "virtual-dom"; import { h } from "virtual-dom";
@ -119,6 +119,7 @@ import Composer, {
registerCustomizationCallback, registerCustomizationCallback,
} from "discourse/models/composer"; } from "discourse/models/composer";
import { addNavItem } from "discourse/models/nav-item"; import { addNavItem } from "discourse/models/nav-item";
import { _addTrackedPostProperty } from "discourse/models/post";
import { registerCustomLastUnreadUrlCallback } from "discourse/models/topic"; import { registerCustomLastUnreadUrlCallback } from "discourse/models/topic";
import { import {
addSaveableUserField, addSaveableUserField,
@ -819,6 +820,17 @@ class PluginApi {
includeAttributes(...attributes); includeAttributes(...attributes);
} }
/**
* Adds a tracked property to the post model.
*
* This method is used to mark a property as tracked for post updates.
*
* @param {string} name - The name of the property to track.
*/
addTrackedPostProperty(name) {
_addTrackedPostProperty(name);
}
/** /**
* Add a new button below a post with your plugin. * Add a new button below a post with your plugin.
* *

View File

@ -10,6 +10,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { propertyEqual } from "discourse/lib/computed"; import { propertyEqual } from "discourse/lib/computed";
import { cook } from "discourse/lib/text"; import { cook } from "discourse/lib/text";
import { fancyTitle } from "discourse/lib/topic-fancy-title"; import { fancyTitle } from "discourse/lib/topic-fancy-title";
import { defineTrackedProperty } from "discourse/lib/tracked-tools";
import { userPath } from "discourse/lib/url"; import { userPath } from "discourse/lib/url";
import { postUrl } from "discourse/lib/utilities"; import { postUrl } from "discourse/lib/utilities";
import ActionSummary from "discourse/models/action-summary"; import ActionSummary from "discourse/models/action-summary";
@ -20,6 +21,46 @@ import User from "discourse/models/user";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
const pluginTrackedProperties = new Set();
const trackedPropertiesForPostUpdate = new Set();
/**
* @internal
* Adds a tracked property to the post model.
*
* Intended to be used only in the plugin API.
*
* @param {string} propertyKey - The key of the property to track.
*/
export function _addTrackedPostProperty(propertyKey) {
pluginTrackedProperties.add(propertyKey);
}
/**
* Clears all tracked properties added using the API
*
* USE ONLY FOR TESTING PURPOSES.
*/
export function clearAddedTrackedPostProperties() {
pluginTrackedProperties.clear();
}
/**
* Decorator to mark a property as post property as tracked.
*
* It extends the standard Ember @tracked behavior to also keep track of the fields
* that need to be copied when using `post.updateFromPost`.
*
* @param {Object} target - The target object.
* @param {string} propertyKey - The key of the property to track.
* @param {PropertyDescriptor} descriptor - The property descriptor.
* @returns {PropertyDescriptor} The updated property descriptor.
*/
function trackedPostProperty(target, propertyKey, descriptor) {
trackedPropertiesForPostUpdate.add(propertyKey);
return tracked(target, propertyKey, descriptor);
}
export default class Post extends RestModel { export default class Post extends RestModel {
static munge(json) { static munge(json) {
if (json.actions_summary) { if (json.actions_summary) {
@ -107,17 +148,22 @@ export default class Post extends RestModel {
@service currentUser; @service currentUser;
@service site; @service site;
@tracked bookmarked; // Use @trackedPostProperty here instead of Glimmer's @tracked because we need to know which properties are tracked
@tracked can_delete; // in order to correctly update the post in the updateFromPost method. Currently this is not possible using only
@tracked can_edit; // the standard tracked method because these properties are added to the class prototype and are not enumarated by
@tracked can_permanently_delete; // object.keys().
@tracked can_recover; // See https://github.com/emberjs/ember.js/issues/18220
@tracked deleted_at; @trackedPostProperty bookmarked;
@tracked likeAction; @trackedPostProperty can_delete;
@tracked post_type; @trackedPostProperty can_edit;
@tracked user_deleted; @trackedPostProperty can_permanently_delete;
@tracked user_id; @trackedPostProperty can_recover;
@tracked yours; @trackedPostProperty deleted_at;
@trackedPostProperty likeAction;
@trackedPostProperty post_type;
@trackedPostProperty user_deleted;
@trackedPostProperty user_id;
@trackedPostProperty yours;
customShare = null; customShare = null;
@ -132,6 +178,15 @@ export default class Post extends RestModel {
// Posts can show up as deleted if the topic is deleted // Posts can show up as deleted if the topic is deleted
@and("firstPost", "topic.deleted_at") deletedViaTopic; @and("firstPost", "topic.deleted_at") deletedViaTopic;
constructor() {
super(...arguments);
// adds tracked properties defined by plugin to the instance
pluginTrackedProperties.forEach((propertyKey) => {
defineTrackedProperty(this, propertyKey);
});
}
@discourseComputed("url", "customShare") @discourseComputed("url", "customShare")
shareUrl(url) { shareUrl(url) {
return this.customShare || resolveShareUrl(url, this.currentUser); return this.customShare || resolveShareUrl(url, this.currentUser);
@ -466,11 +521,15 @@ export default class Post extends RestModel {
} }
/** /**
Updates a post from another's attributes. This will normally happen when a post is loading but * Updates a post from another's attributes. This will normally happen when a post is loading but
is already found in an identity map. * is already found in an identity map.
**/ **/
updateFromPost(otherPost) { updateFromPost(otherPost) {
Object.keys(otherPost).forEach((key) => { [
...Object.keys(otherPost),
...trackedPropertiesForPostUpdate,
...pluginTrackedProperties,
].forEach((key) => {
let value = otherPost[key], let value = otherPost[key],
oldValue = this[key]; oldValue = this[key];

View File

@ -82,6 +82,7 @@ import { resetUserSearchCache } from "discourse/lib/user-search";
import { resetComposerCustomizations } from "discourse/models/composer"; import { resetComposerCustomizations } from "discourse/models/composer";
import { clearAuthMethods } from "discourse/models/login-method"; import { clearAuthMethods } from "discourse/models/login-method";
import { clearNavItems } from "discourse/models/nav-item"; import { clearNavItems } from "discourse/models/nav-item";
import { clearAddedTrackedPostProperties } from "discourse/models/post";
import { resetLastEditNotificationClick } from "discourse/models/post-stream"; import { resetLastEditNotificationClick } from "discourse/models/post-stream";
import Site from "discourse/models/site"; import Site from "discourse/models/site";
import User from "discourse/models/user"; import User from "discourse/models/user";
@ -257,6 +258,7 @@ export function testCleanup(container, app) {
clearPluginHeaderActionComponents(); clearPluginHeaderActionComponents();
clearRegisteredTabs(); clearRegisteredTabs();
resetNeedsHbrTopicList(); resetNeedsHbrTopicList();
clearAddedTrackedPostProperties();
} }
function cleanupCssGeneratorTags() { function cleanupCssGeneratorTags() {

View File

@ -1,6 +1,7 @@
import { getOwner } from "@ember/owner"; import { getOwner } from "@ember/owner";
import { setupTest } from "ember-qunit"; import { setupTest } from "ember-qunit";
import { module, test } from "qunit"; import { module, test } from "qunit";
import { withPluginApi } from "discourse/lib/plugin-api";
module("Unit | Model | post", function (hooks) { module("Unit | Model | post", function (hooks) {
setupTest(hooks); setupTest(hooks);
@ -32,18 +33,36 @@ module("Unit | Model | post", function (hooks) {
}); });
test("updateFromPost", function (assert) { test("updateFromPost", function (assert) {
withPluginApi("1.39.0", (api) => {
api.addTrackedPostProperty("plugin_property");
});
const post = this.store.createRecord("post", { const post = this.store.createRecord("post", {
post_number: 1, post_number: 1,
raw: "hello world", raw: "hello world",
likeAction: null, // `likeAction` is a tracked property from the model added using `@trackedPostProperty`
}); });
post.updateFromPost( post.updateFromPost(
this.store.createRecord("post", { this.store.createRecord("post", {
raw: "different raw", raw: "different raw",
yours: false,
likeAction: { count: 1 },
plugin_property: "different plugin value",
}) })
); );
assert.strictEqual(post.raw, "different raw", "raw field updated"); assert.strictEqual(post.raw, "different raw", "`raw` field was updated");
assert.deepEqual(
post.likeAction,
{ count: 1 },
"`likeAction` field was updated"
);
assert.strictEqual(
post.plugin_property,
"different plugin value",
"`plugin_property` field was updated"
);
}); });
test("destroy by staff", async function (assert) { test("destroy by staff", async function (assert) {

View File

@ -7,6 +7,10 @@ in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.39.0] - 2024-11-27
- Added `addTrackedPostProperty` which allows plugins/TCs to add a new tracked property to the post model.
## [1.38.0] - 2024-10-30 ## [1.38.0] - 2024-10-30
- Added `registerMoreTopicsTab` and "more-topics-tabs" value transformer that allows to add or remove new tabs to the "more topics" (suggested/related) area. - Added `registerMoreTopicsTab` and "more-topics-tabs" value transformer that allows to add or remove new tabs to the "more topics" (suggested/related) area.