diff --git a/public/src/client/topic/merge.js b/public/src/client/topic/merge.js index 3955f51648..228d318f1b 100644 --- a/public/src/client/topic/merge.js +++ b/public/src/client/topic/merge.js @@ -77,12 +77,19 @@ define('forum/topic/merge', function () { function mergeTopics(btn) { btn.attr('disabled', true); var tids = Object.keys(selectedTids); - socket.emit('topics.merge', tids, function (err) { + var options = {}; + if (modal.find('.merge-main-topic-radio').is(':checked')) { + options.mainTid = modal.find('.merge-main-topic-select').val(); + } else if (modal.find('.merge-new-title-radio').is(':checked')) { + options.newTopicTitle = modal.find('.merge-new-title-input').val(); + } + + socket.emit('topics.merge', { tids: tids, options: options }, function (err, tid) { btn.removeAttr('disabled'); if (err) { return app.alertError(err.message); } - ajaxify.go('/topic/' + tids[0]); + ajaxify.go('/topic/' + tid); closeModal(); }); } @@ -103,7 +110,7 @@ define('forum/topic/merge', function () { topics: topics, }, function (html) { modal.find('.topics-section').html(html.find('.topics-section').html()); - modal.find('.main-topic-select').html(html.find('.main-topic-select').html()); + modal.find('.merge-main-topic-select').html(html.find('.merge-main-topic-select').html()); }); } else { modal.find('.topics-section').translateHtml('[[error:no-topics-selected]]'); diff --git a/src/socket.io/topics/merge.js b/src/socket.io/topics/merge.js index 124ede9b32..8fa275042a 100644 --- a/src/socket.io/topics/merge.js +++ b/src/socket.io/topics/merge.js @@ -4,14 +4,18 @@ const topics = require('../../topics'); const privileges = require('../../privileges'); module.exports = function (SocketTopics) { - SocketTopics.merge = async function (socket, tids) { - if (!Array.isArray(tids)) { + SocketTopics.merge = async function (socket, data) { + if (!data || !Array.isArray(data.tids)) { throw new Error('[[error:invalid-data]]'); } - const allowed = await Promise.all(tids.map(tid => privileges.topics.isAdminOrMod(tid, socket.uid))); + const allowed = await Promise.all(data.tids.map(tid => privileges.topics.isAdminOrMod(tid, socket.uid))); if (allowed.includes(false)) { throw new Error('[[error:no-privileges]]'); } - await topics.merge(tids, socket.uid); + if (data.options && data.options.mainTid && !data.tids.includes(data.options.mainTid)) { + throw new Error('[[error:invalid-data]]'); + } + const mergeIntoTid = await topics.merge(data.tids, socket.uid, data.options); + return mergeIntoTid; }; }; diff --git a/src/topics/merge.js b/src/topics/merge.js index fb6a6e59d4..dbcfebc5c2 100644 --- a/src/topics/merge.js +++ b/src/topics/merge.js @@ -4,10 +4,18 @@ const async = require('async'); const plugins = require('../plugins'); module.exports = function (Topics) { - Topics.merge = async function (tids, uid) { - const mergeIntoTid = findOldestTopic(tids); + Topics.merge = async function (tids, uid, options) { + options = options || {}; + const oldestTid = findOldestTopic(tids); + let mergeIntoTid = oldestTid; + if (options.mainTid) { + mergeIntoTid = options.mainTid; + } else if (options.newTopicTitle) { + mergeIntoTid = await createNewTopic(options.newTopicTitle, oldestTid); + } - const otherTids = tids.filter(tid => tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10)); + const otherTids = tids.sort((a, b) => a - b) + .filter(tid => tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10)); await async.eachSeries(otherTids, async function (tid) { const pids = await Topics.getPids(tid); @@ -25,8 +33,19 @@ module.exports = function (Topics) { }); plugins.fireHook('action:topic.merge', { uid: uid, tids: tids, mergeIntoTid: mergeIntoTid, otherTids: otherTids }); + return mergeIntoTid; }; + async function createNewTopic(title, oldestTid) { + const topicData = await Topics.getTopicFields(oldestTid, ['uid', 'cid']); + const tid = await Topics.create({ + uid: topicData.uid, + cid: topicData.cid, + title: title, + }); + return tid; + } + function findOldestTopic(tids) { return Math.min.apply(null, tids); } diff --git a/test/topics.js b/test/topics.js index 13e43efdd2..06c25afa1b 100644 --- a/test/topics.js +++ b/test/topics.js @@ -2080,6 +2080,11 @@ describe('Topic\'s', function () { var topic1Data; var topic2Data; + async function getTopic(tid) { + const topicData = await topics.getTopicData(tid); + return await topics.getTopicWithPosts(topicData, 'tid:' + topicData.tid + ':posts', adminUid, 0, 19, false); + } + before(function (done) { async.waterfall([ function (next) { @@ -2111,18 +2116,17 @@ describe('Topic\'s', function () { }); it('should error if user does not have privileges', function (done) { - socketTopics.merge({ uid: 0 }, [topic2Data.tid, topic1Data.tid], function (err) { + socketTopics.merge({ uid: 0 }, { tids: [topic2Data.tid, topic1Data.tid] }, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); }); }); it('should merge 2 topics', async function () { - await socketTopics.merge({ uid: adminUid }, [topic2Data.tid, topic1Data.tid]); - async function getTopic(tid) { - const topicData = await topics.getTopicData(tid); - return await topics.getTopicWithPosts(topicData, 'tid:' + topicData.tid + ':posts', adminUid, 0, 19, false); - } + await socketTopics.merge({ uid: adminUid }, { + tids: [topic2Data.tid, topic1Data.tid], + }); + const [topic1, topic2] = await Promise.all([ getTopic(topic1Data.tid), getTopic(topic2Data.tid), @@ -2136,6 +2140,7 @@ describe('Topic\'s', function () { assert.equal(topic1.posts[1].content, 'topic 2 OP'); assert.equal(topic1.posts[2].content, 'topic 1 reply'); assert.equal(topic1.posts[3].content, 'topic 2 reply'); + assert.equal(topic1.title, 'topic 1'); }); it('should return properly for merged topic', function (done) { @@ -2147,6 +2152,65 @@ describe('Topic\'s', function () { done(); }); }); + + it('should merge 2 topics with options mainTid', async function () { + const topic1Result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 1', content: 'topic 1 OP' }); + const topic2Result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 2', content: 'topic 2 OP' }); + await topics.reply({ uid: uid, content: 'topic 1 reply', tid: topic1Result.topicData.tid }); + await topics.reply({ uid: uid, content: 'topic 2 reply', tid: topic2Result.topicData.tid }); + await socketTopics.merge({ uid: adminUid }, { + tids: [topic2Result.topicData.tid, topic1Result.topicData.tid], + options: { + mainTid: topic2Result.topicData.tid, + }, + }); + + const [topic1, topic2] = await Promise.all([ + getTopic(topic1Result.topicData.tid), + getTopic(topic2Result.topicData.tid), + ]); + + assert.equal(topic1.posts.length, 0); + assert.equal(topic2.posts.length, 4); + assert.equal(topic1.deleted, true); + + assert.equal(topic2.posts[0].content, 'topic 2 OP'); + assert.equal(topic2.posts[1].content, 'topic 1 OP'); + assert.equal(topic2.posts[2].content, 'topic 1 reply'); + assert.equal(topic2.posts[3].content, 'topic 2 reply'); + assert.equal(topic2.title, 'topic 2'); + }); + + it('should merge 2 topics with options newTopicTitle', async function () { + const topic1Result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 1', content: 'topic 1 OP' }); + const topic2Result = await topics.post({ uid: uid, cid: categoryObj.cid, title: 'topic 2', content: 'topic 2 OP' }); + await topics.reply({ uid: uid, content: 'topic 1 reply', tid: topic1Result.topicData.tid }); + await topics.reply({ uid: uid, content: 'topic 2 reply', tid: topic2Result.topicData.tid }); + const mergeTid = await socketTopics.merge({ uid: adminUid }, { + tids: [topic2Result.topicData.tid, topic1Result.topicData.tid], + options: { + newTopicTitle: 'new merge topic', + }, + }); + + const [topic1, topic2, topic3] = await Promise.all([ + getTopic(topic1Result.topicData.tid), + getTopic(topic2Result.topicData.tid), + getTopic(mergeTid), + ]); + + assert.equal(topic1.posts.length, 0); + assert.equal(topic2.posts.length, 0); + assert.equal(topic3.posts.length, 4); + assert.equal(topic1.deleted, true); + assert.equal(topic2.deleted, true); + + assert.equal(topic3.posts[0].content, 'topic 1 OP'); + assert.equal(topic3.posts[1].content, 'topic 2 OP'); + assert.equal(topic3.posts[2].content, 'topic 1 reply'); + assert.equal(topic3.posts[3].content, 'topic 2 reply'); + assert.equal(topic3.title, 'new merge topic'); + }); }); describe('sorted topics', function () {