diff --git a/public/src/modules/topicThumbs.js b/public/src/modules/topicThumbs.js index b406d35e82..e08345ab3e 100644 --- a/public/src/modules/topicThumbs.js +++ b/public/src/modules/topicThumbs.js @@ -1,6 +1,6 @@ 'use strict'; -define('topicThumbs', ['api', 'bootbox', 'uploader', 'benchpress', 'translator'], function (api, bootbox, uploader, Benchpress, translator) { +define('topicThumbs', ['api', 'bootbox', 'uploader', 'benchpress', 'translator', 'jquery-ui/widgets/sortable'], function (api, bootbox, uploader, Benchpress, translator) { const Thumbs = {}; Thumbs.get = id => api.get(`/topics/${id}/thumbs`, {}); @@ -32,30 +32,31 @@ define('topicThumbs', ['api', 'bootbox', 'uploader', 'benchpress', 'translator'] Thumbs.modal.open = function (payload) { const { id, pid } = payload; let { modal } = payload; + let numThumbs; return new Promise((resolve) => { Promise.all([ Thumbs.get(id), pid ? Thumbs.getByPid(pid) : [], ]).then(results => new Promise((resolve) => { - resolve(results.reduce((memo, cur) => memo.concat(cur))); + const thumbs = results.reduce((memo, cur) => memo.concat(cur)); + numThumbs = thumbs.length; + + resolve(thumbs); })).then(thumbs => Benchpress.render('modals/topic-thumbs', { thumbs })).then((html) => { if (modal) { translator.translate(html, function (translated) { modal.find('.bootbox-body').html(translated); + Thumbs.modal.handleSort({ modal, numThumbs }); }); } else { modal = bootbox.dialog({ title: '[[modules:thumbs.modal.title]]', message: html, buttons: { - close: { - label: '[[global:close]]', - className: 'btn-default', - }, add: { label: ' [[modules:thumbs.modal.add]]', - className: 'btn-primary', + className: 'btn-success', callback: () => { Thumbs.upload(id).then(() => { Thumbs.modal.open({ ...payload, modal }); @@ -64,9 +65,14 @@ define('topicThumbs', ['api', 'bootbox', 'uploader', 'benchpress', 'translator'] return false; }, }, + close: { + label: '[[global:close]]', + className: 'btn-primary', + }, }, }); Thumbs.modal.handleDelete({ ...payload, modal }); + Thumbs.modal.handleSort({ modal, numThumbs }); } }); }); @@ -94,5 +100,26 @@ define('topicThumbs', ['api', 'bootbox', 'uploader', 'benchpress', 'translator'] }); }; + Thumbs.modal.handleSort = ({ modal, numThumbs }) => { + if (numThumbs > 1) { + const selectorEl = modal.find('.topic-thumbs-modal'); + selectorEl.sortable({ + items: '[data-id]', + }); + selectorEl.on('sortupdate', Thumbs.modal.handleSortChange); + } + }; + + Thumbs.modal.handleSortChange = (ev, ui) => { + const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]'); + Array.from(items).forEach((el, order) => { + const id = el.getAttribute('data-id'); + let path = el.getAttribute('data-path'); + path = path.replace(new RegExp(`^${config.upload_url}`), ''); + + api.put(`/topics/${id}/thumbs/order`, { path, order }).catch(app.alertError); + }); + }; + return Thumbs; }); diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index dc9b62fc93..4132b79b9e 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -166,6 +166,25 @@ Topics.deleteThumb = async (req, res) => { helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); }; +Topics.reorderThumbs = async (req, res) => { + await checkThumbPrivileges({ tid: req.params.tid, uid: req.user.uid, res }); + if (res.headersSent) { + return; + } + + const exists = await topics.thumbs.exists(req.params.tid, req.body.path); + if (!exists) { + return helpers.formatApiResponse(404, res); + } + + await topics.thumbs.associate({ + id: req.params.tid, + path: req.body.path, + score: req.body.order, + }); + helpers.formatApiResponse(200, res); +}; + async function checkThumbPrivileges({ tid, uid, res }) { // req.params.tid could be either a tid (pushing a new thumb to an existing topic) // or a post UUID (a new topic being composed) diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index 8877b2a535..41851e6eb0 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -37,8 +37,9 @@ module.exports = function () { setupApiRoute(router, 'get', '/:tid/thumbs', middleware.authenticateOrGuest, controllers.write.topics.getThumbs); setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, ...middlewares], controllers.write.topics.addThumb); - setupApiRoute(router, 'put', '/:tid/thumbs', [], controllers.write.topics.migrateThumbs); + setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs); setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); + setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs); setupApiRoute(router, 'get', '/:tid/events', [middleware.authenticateOrGuest, middleware.assert.topic], controllers.write.topics.getEvents); diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index ffbbec47bd..8bc6789ba0 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -15,9 +15,11 @@ const cache = require('../cache'); const Thumbs = module.exports; -Thumbs.exists = async function (tid, path) { - // TODO: tests - return db.isSortedSetMember(`topic:${tid}:thumbs`, path); +Thumbs.exists = async function (id, path) { + const isDraft = validator.isUUID(String(id)); + const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; + + return db.isSortedSetMember(set, path); }; Thumbs.load = async function (topicData) { diff --git a/src/views/modals/topic-thumbs.tpl b/src/views/modals/topic-thumbs.tpl index 7ee9773836..7c87b3e439 100644 --- a/src/views/modals/topic-thumbs.tpl +++ b/src/views/modals/topic-thumbs.tpl @@ -14,7 +14,7 @@ -
[[modules:thumbs.modal.resize-note, {config.thumbs.size}]]
\ No newline at end of file diff --git a/test/topicThumbs.js b/test/topicThumbs.js index c2c56c4a2f..ab2de29bf7 100644 --- a/test/topicThumbs.js +++ b/test/topicThumbs.js @@ -80,11 +80,11 @@ describe('Topic thumbs', () => { // Touch a couple files and associate it to a topic createFiles(); - await db.sortedSetAdd(`topic:${topicObj.topicData.tid}:thumbs`, 0, `/${relativeThumbPaths[0]}`); + await db.sortedSetAdd(`topic:${topicObj.topicData.tid}:thumbs`, 0, `${relativeThumbPaths[0]}`); }); it('should return bool for whether a thumb exists', async () => { - const exists = await topics.thumbs.exists(topicObj.topicData.tid, `/${relativeThumbPaths[0]}`); + const exists = await topics.thumbs.exists(topicObj.topicData.tid, `${relativeThumbPaths[0]}`); assert.strictEqual(exists, true); });