feat: store topic title and tags in diffs (#10900)

* feat: store topic title and tags in diffs

allow restoring post diff if tags didn't change

* test: fix tests, fast computer problems
v1.18.x
Barış Soner Uşaklı 2 years ago
parent a06c05c224
commit 175d53421b

@ -158,7 +158,7 @@ define('forum/topic/events', [
hooks.fire('action:posts.edited', data); hooks.fire('action:posts.edited', data);
} }
if (data.topic.tags && tagsUpdated(data.topic.tags)) { if (data.topic.tags && data.topic.tagsupdated) {
Benchpress.render('partials/topic/tags', { tags: data.topic.tags }).then(function (html) { Benchpress.render('partials/topic/tags', { tags: data.topic.tags }).then(function (html) {
const tags = $('.tags'); const tags = $('.tags');
@ -171,19 +171,6 @@ define('forum/topic/events', [
postTools.removeMenu(components.get('post', 'pid', data.post.pid)); postTools.removeMenu(components.get('post', 'pid', data.post.pid));
} }
function tagsUpdated(tags) {
if (tags.length !== $('.tags').first().children().length) {
return true;
}
for (let i = 0; i < tags.length; i += 1) {
if (!$('.tags .tag-item[data-tag="' + tags[i].value + '"]').length) {
return true;
}
}
return false;
}
function onPostPurged(postData) { function onPostPurged(postData) {
if (!postData || parseInt(postData.tid, 10) !== parseInt(ajaxify.data.tid, 10)) { if (!postData || parseInt(postData.tid, 10) !== parseInt(ajaxify.data.tid, 10)) {
return; return;

@ -7,7 +7,7 @@ const db = require('../database');
const meta = require('../meta'); const meta = require('../meta');
const plugins = require('../plugins'); const plugins = require('../plugins');
const translator = require('../translator'); const translator = require('../translator');
const topics = require('../topics');
module.exports = function (Posts) { module.exports = function (Posts) {
const Diffs = {}; const Diffs = {};
@ -38,16 +38,24 @@ module.exports = function (Posts) {
}; };
Diffs.save = async function (data) { Diffs.save = async function (data) {
const { pid, uid, oldContent, newContent, edited } = data; const { pid, uid, oldContent, newContent, edited, topic } = data;
const editTimestamp = edited || Date.now(); const editTimestamp = edited || Date.now();
const patch = diff.createPatch('', newContent, oldContent); const diffData = {
await Promise.all([
db.listPrepend(`post:${pid}:diffs`, editTimestamp),
db.setObject(`diff:${pid}.${editTimestamp}`, {
uid: uid, uid: uid,
pid: pid, pid: pid,
patch: patch, };
}), if (oldContent !== newContent) {
diffData.patch = diff.createPatch('', newContent, oldContent);
}
if (topic.renamed) {
diffData.title = topic.oldTitle;
}
if (topic.tagsupdated && Array.isArray(topic.oldTags)) {
diffData.tags = topic.oldTags.map(tag => tag && tag.value).filter(Boolean).join(',');
}
await Promise.all([
db.listPrepend(`post:${pid}:diffs`, editTimestamp),
db.setObject(`diff:${pid}.${editTimestamp}`, diffData),
]); ]);
}; };
@ -71,6 +79,8 @@ module.exports = function (Posts) {
content: post.content, content: post.content,
req: req, req: req,
timestamp: since, timestamp: since,
title: post.topic.title,
tags: post.topic.tags.map(tag => tag.value),
}); });
}; };
@ -130,6 +140,16 @@ module.exports = function (Posts) {
// Replace content with re-constructed content from that point in time // Replace content with re-constructed content from that point in time
post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content)); post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content));
const titleDiffs = diffs.filter(d => d.hasOwnProperty('title') && d.title);
if (titleDiffs.length && post[0].topic) {
post[0].topic.title = validator.unescape(String(titleDiffs[titleDiffs.length - 1].title));
}
const tagDiffs = diffs.filter(d => d.hasOwnProperty('tags') && d.tags);
if (tagDiffs.length && post[0].topic) {
const tags = tagDiffs[tagDiffs.length - 1].tags.split(',').map(tag => ({ value: tag }));
post[0].topic.tags = await topics.getTagData(tags);
}
return post[0]; return post[0];
} }
@ -144,9 +164,12 @@ module.exports = function (Posts) {
} }
function applyPatch(content, aDiff) { function applyPatch(content, aDiff) {
if (aDiff && aDiff.patch) {
const result = diff.applyPatch(content, aDiff.patch, { const result = diff.applyPatch(content, aDiff.patch, {
fuzzFactor: 1, fuzzFactor: 1,
}); });
return typeof result === 'string' ? result : content; return typeof result === 'string' ? result : content;
} }
return content;
}
}; };

@ -29,7 +29,9 @@ module.exports = function (Posts) {
throw new Error('[[error:no-post]]'); throw new Error('[[error:no-post]]');
} }
const topicData = await topics.getTopicFields(postData.tid, ['cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug']); const topicData = await topics.getTopicFields(postData.tid, [
'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags',
]);
await scheduledTopicCheck(data, topicData); await scheduledTopicCheck(data, topicData);
@ -53,7 +55,10 @@ module.exports = function (Posts) {
]); ]);
await Posts.setPostFields(data.pid, result.post); await Posts.setPostFields(data.pid, result.post);
const contentChanged = data.content !== oldContent; const contentChanged = data.content !== oldContent ||
topic.renamed ||
topic.tagsupdated;
if (meta.config.enablePostHistory === 1 && contentChanged) { if (meta.config.enablePostHistory === 1 && contentChanged) {
await Posts.diffs.save({ await Posts.diffs.save({
pid: data.pid, pid: data.pid,
@ -61,6 +66,7 @@ module.exports = function (Posts) {
oldContent: oldContent, oldContent: oldContent,
newContent: data.content, newContent: data.content,
edited: editPostData.edited, edited: editPostData.edited,
topic,
}); });
} }
await Posts.uploads.sync(data.pid); await Posts.uploads.sync(data.pid);
@ -109,6 +115,7 @@ module.exports = function (Posts) {
title: validator.escape(String(topicData.title)), title: validator.escape(String(topicData.title)),
isMainPost: false, isMainPost: false,
renamed: false, renamed: false,
tagsupdated: false,
}; };
} }
@ -124,15 +131,16 @@ module.exports = function (Posts) {
newTopicData.slug = `${tid}/${slugify(title) || 'topic'}`; newTopicData.slug = `${tid}/${slugify(title) || 'topic'}`;
} }
data.tags = data.tags || []; const tagsupdated = Array.isArray(data.tags) &&
!_.isEqual(data.tags, topicData.tags.map(tag => tag.value));
if (data.tags.length) { if (tagsupdated) {
const canTag = await privileges.categories.can('topics:tag', topicData.cid, data.uid); const canTag = await privileges.categories.can('topics:tag', topicData.cid, data.uid);
if (!canTag) { if (!canTag) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
}
await topics.validateTags(data.tags, topicData.cid, data.uid, tid); await topics.validateTags(data.tags, topicData.cid, data.uid, tid);
}
const results = await plugins.hooks.fire('filter:topic.edit', { const results = await plugins.hooks.fire('filter:topic.edit', {
req: data.req, req: data.req,
@ -140,7 +148,9 @@ module.exports = function (Posts) {
data: data, data: data,
}); });
await db.setObject(`topic:${tid}`, results.topic); await db.setObject(`topic:${tid}`, results.topic);
if (tagsupdated) {
await topics.updateTopicTags(tid, data.tags); await topics.updateTopicTags(tid, data.tags);
}
const tags = await topics.getTopicTagsObjects(tid); const tags = await topics.getTopicTagsObjects(tid);
if (rescheduling(data, topicData)) { if (rescheduling(data, topicData)) {
@ -149,7 +159,7 @@ module.exports = function (Posts) {
newTopicData.tags = data.tags; newTopicData.tags = data.tags;
newTopicData.oldTitle = topicData.title; newTopicData.oldTitle = topicData.title;
const renamed = translator.escape(validator.escape(String(title))) !== topicData.title; const renamed = title && translator.escape(validator.escape(String(title))) !== topicData.title;
plugins.hooks.fire('action:topic.edit', { topic: newTopicData, uid: data.uid }); plugins.hooks.fire('action:topic.edit', { topic: newTopicData, uid: data.uid });
return { return {
tid: tid, tid: tid,
@ -160,8 +170,10 @@ module.exports = function (Posts) {
slug: newTopicData.slug || topicData.slug, slug: newTopicData.slug || topicData.slug,
isMainPost: true, isMainPost: true,
renamed: renamed, renamed: renamed,
rescheduled: rescheduling(data, topicData), tagsupdated: tagsupdated,
tags: tags, tags: tags,
oldTags: topicData.tags,
rescheduled: rescheduling(data, topicData),
}; };
} }

@ -76,9 +76,15 @@ module.exports = function (Posts) {
} }
async function getTopicAndCategories(tids) { async function getTopicAndCategories(tids) {
const topicsData = await topics.getTopicsFields(tids, ['uid', 'tid', 'title', 'cid', 'slug', 'deleted', 'scheduled', 'postcount', 'mainPid', 'teaserPid']); const topicsData = await topics.getTopicsFields(tids, [
'uid', 'tid', 'title', 'cid', 'tags', 'slug',
'deleted', 'scheduled', 'postcount', 'mainPid', 'teaserPid',
]);
const cids = _.uniq(topicsData.map(topic => topic && topic.cid)); const cids = _.uniq(topicsData.map(topic => topic && topic.cid));
const categoriesData = await categories.getCategoriesFields(cids, ['cid', 'name', 'icon', 'slug', 'parentCid', 'bgColor', 'color', 'backgroundImage', 'imageClass']); const categoriesData = await categories.getCategoriesFields(cids, [
'cid', 'name', 'icon', 'slug', 'parentCid',
'bgColor', 'color', 'backgroundImage', 'imageClass',
]);
return { topics: topicsData, categories: categoriesData }; return { topics: topicsData, categories: categoriesData };
} }

@ -191,8 +191,11 @@ describe('API', async () => {
const socketAdmin = require('../src/socket.io/admin'); const socketAdmin = require('../src/socket.io/admin');
// export data for admin user // export data for admin user
await socketUser.exportProfile({ uid: adminUid }, { uid: adminUid }); await socketUser.exportProfile({ uid: adminUid }, { uid: adminUid });
await wait(2000);
await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid }); await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid });
await wait(2000);
await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid }); await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid });
await wait(2000);
await socketAdmin.user.exportUsersCSV({ uid: adminUid }, {}); await socketAdmin.user.exportUsersCSV({ uid: adminUid }, {});
// wait for export child process to complete // wait for export child process to complete
await wait(5000); await wait(5000);

@ -425,6 +425,7 @@ describe('Post\'s', () => {
cid: cid, cid: cid,
title: 'topic to edit', title: 'topic to edit',
content: 'A post to edit', content: 'A post to edit',
tags: ['nodebb'],
}, (err, data) => { }, (err, data) => {
assert.ifError(err); assert.ifError(err);
pid = data.postData.pid; pid = data.postData.pid;

Loading…
Cancel
Save