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:
parent
607dd2cbd8
commit
a710d3f377
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue