diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index f736000fb9..bb7f44d88b 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -36,6 +36,8 @@ "share": "Share", "tools": "Tools", "locked": "Locked", + "resolved": "Resolved", + "unresolved": "Unresolved", "pinned": "Pinned", "pinned-with-expiry": "Pinned until %1", "scheduled": "Scheduled", @@ -115,6 +117,8 @@ "thread-tools.unpin": "Unpin Topic", "thread-tools.lock": "Lock Topic", "thread-tools.unlock": "Unlock Topic", + "thread-tools.resolve": "Mark Resolved", + "thread-tools.unresolve": "Mark Unresolved", "thread-tools.move": "Move Topic", "thread-tools.crosspost": "Crosspost Topic", "thread-tools.move-posts": "Move Posts", diff --git a/public/language/en-GB/unread.json b/public/language/en-GB/unread.json index 6e8d2ccf95..a3027a48c6 100644 --- a/public/language/en-GB/unread.json +++ b/public/language/en-GB/unread.json @@ -12,5 +12,7 @@ "new-topics": "New Topics", "watched-topics": "Watched Topics", "unreplied-topics": "Unreplied Topics", + "resolved-topics": "Resolved Topics", + "unresolved-topics": "Unresolved Topics", "multiple-categories-selected": "Multiple Selected" -} \ No newline at end of file +} diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json index 42c3c17e66..dfb816f828 100644 --- a/public/language/en-US/topic.json +++ b/public/language/en-US/topic.json @@ -33,6 +33,8 @@ "share": "Share", "tools": "Tools", "locked": "Locked", + "resolved": "Resolved", + "unresolved": "Unresolved", "pinned": "Pinned", "pinned-with-expiry": "Pinned until %1", "scheduled": "Scheduled", @@ -102,6 +104,8 @@ "thread-tools.unpin": "Unpin Topic", "thread-tools.lock": "Lock Topic", "thread-tools.unlock": "Unlock Topic", + "thread-tools.resolve": "Mark Resolved", + "thread-tools.unresolve": "Mark Unresolved", "thread-tools.move": "Move Topic", "thread-tools.crosspost": "Crosspost Topic", "thread-tools.move-posts": "Move Posts", @@ -231,4 +235,4 @@ "thumb-image": "Topic thumbnail image", "announcers": "Shares", "announcers-x": "Shares (%1)" -} \ No newline at end of file +} diff --git a/public/language/en-US/unread.json b/public/language/en-US/unread.json index 4f7dbdc653..fbeea08cee 100644 --- a/public/language/en-US/unread.json +++ b/public/language/en-US/unread.json @@ -12,5 +12,7 @@ "new-topics": "New Topics", "watched-topics": "Watched Topics", "unreplied-topics": "Unreplied Topics", + "resolved-topics": "Resolved Topics", + "unresolved-topics": "Unresolved Topics", "multiple-categories-selected": "Multiple Selected" -} \ No newline at end of file +} diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index dfef0aa0cf..b7928e67ff 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -146,6 +146,8 @@ paths: $ref: 'write/topics/tid/state.yaml' /topics/{tid}/lock: $ref: 'write/topics/tid/lock.yaml' + /topics/{tid}/resolve: + $ref: 'write/topics/tid/resolve.yaml' /topics/{tid}/pin: $ref: 'write/topics/tid/pin.yaml' /topics/{tid}/follow: @@ -287,4 +289,4 @@ paths: /files/: $ref: 'write/files.yaml' /files/folder: - $ref: 'write/files/folder.yaml' \ No newline at end of file + $ref: 'write/files/folder.yaml' diff --git a/public/openapi/write/topics/tid/resolve.yaml b/public/openapi/write/topics/tid/resolve.yaml new file mode 100644 index 0000000000..b5b280e6d2 --- /dev/null +++ b/public/openapi/write/topics/tid/resolve.yaml @@ -0,0 +1,52 @@ +put: + tags: + - topics + summary: mark a topic as resolved + description: This operation marks an existing Q&A topic as resolved. + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + responses: + '200': + description: Topic successfully marked as resolved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} +delete: + tags: + - topics + summary: mark a topic as unresolved + description: This operation marks an existing Q&A topic as unresolved. + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + responses: + '200': + description: Topic successfully marked as unresolved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js index f298cbe85d..a746695eee 100644 --- a/public/src/client/category/tools.js +++ b/public/src/client/category/tools.js @@ -136,6 +136,8 @@ define('forum/category/tools', [ socket.on('event:topic_unlocked', setLockedState); socket.on('event:topic_pinned', setPinnedState); socket.on('event:topic_unpinned', setPinnedState); + socket.on('event:topic_resolved', setResolvedState); + socket.on('event:topic_unresolved', setResolvedState); socket.on('event:topic_moved', onTopicMoved); }; @@ -182,6 +184,8 @@ define('forum/category/tools', [ socket.removeListener('event:topic_unlocked', setLockedState); socket.removeListener('event:topic_pinned', setPinnedState); socket.removeListener('event:topic_unpinned', setPinnedState); + socket.removeListener('event:topic_resolved', setResolvedState); + socket.removeListener('event:topic_unresolved', setResolvedState); socket.removeListener('event:topic_moved', onTopicMoved); }; @@ -284,6 +288,14 @@ define('forum/category/tools', [ topic.find('[component="topic/locked"]').toggleClass('hidden', !data.isLocked); } + function setResolvedState(data) { + const topic = getTopicEl(data.tid); + const isResolved = !!(data.isResolved || parseInt(data.resolved, 10) === 1); + topic.toggleClass('resolved', isResolved); + topic.find('[component="topic/resolved"]').toggleClass('hidden', !isResolved); + topic.find('[component="topic/unresolved"]').toggleClass('hidden', isResolved); + } + async function onTopicMoved(data) { if (ajaxify.data.template.category || String(data.toCid) === '-1') { getTopicEl(data.tid).remove(); diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 10c5d54cf2..f4595255b2 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -29,6 +29,9 @@ define('forum/topic/events', [ 'event:topic_pinned': threadTools.setPinnedState, 'event:topic_unpinned': threadTools.setPinnedState, + 'event:topic_resolved': threadTools.setResolvedState, + 'event:topic_unresolved': threadTools.setResolvedState, + 'event:topic_moved': onTopicMoved, 'event:post_edited': onPostEdited, diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 7e1a001ada..4758f9070c 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -51,6 +51,16 @@ define('forum/topic/threadTools', [ return false; }); + topicContainer.on('click', '[component="topic/resolve"]', function () { + topicCommand('put', '/resolve', 'resolve'); + return false; + }); + + topicContainer.on('click', '[component="topic/unresolve"]', function () { + topicCommand('del', '/resolve', 'unresolve'); + return false; + }); + topicContainer.on('click', '[component="topic/pin"]', function () { topicCommand('put', '/pin', 'pin'); return false; @@ -394,6 +404,21 @@ define('forum/topic/threadTools', [ posts.addTopicEvents(data.events); }; + ThreadTools.setResolvedState = function (data) { + const threadEl = components.get('topic'); + if (String(data.tid) !== threadEl.attr('data-tid')) { + return; + } + + const isResolved = !!(data.isResolved || parseInt(data.resolved, 10) === 1); + components.get('topic/resolve').toggleClass('hidden', isResolved).parent().attr('hidden', isResolved ? '' : null); + components.get('topic/unresolve').toggleClass('hidden', !isResolved).parent().attr('hidden', !isResolved ? '' : null); + + $('[component="topic/labels"] [component="topic/resolved"]').toggleClass('hidden', !isResolved); + $('[component="topic/labels"] [component="topic/unresolved"]').toggleClass('hidden', isResolved); + ajaxify.data.resolved = isResolved ? 1 : 0; + }; + function setFollowState(state) { const titles = { follow: '[[topic:watching]]', diff --git a/src/api/topics.js b/src/api/topics.js index 054602e7a2..ef466eee76 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -165,6 +165,18 @@ topicsAPI.unlock = async function (caller, data) { }); }; +topicsAPI.resolve = async function (caller, data) { + await doTopicAction('resolve', 'event:topic_resolved', caller, { + tids: data.tids, + }); +}; + +topicsAPI.unresolve = async function (caller, data) { + await doTopicAction('unresolve', 'event:topic_unresolved', caller, { + tids: data.tids, + }); +}; + topicsAPI.follow = async function (caller, data) { await topics.follow(data.tid, caller.uid); }; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index a6ade8c73b..0c253f47a2 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -70,8 +70,8 @@ helpers.addLinkTags = function (params) { }); }; -helpers.buildFilters = function (url, filter, query) { - return [{ +helpers.buildFilters = function (url, filter, query, options = {}) { + const filters = [{ name: '[[unread:all-topics]]', url: url + helpers.buildQueryString(query, 'filter', ''), selected: filter === '', @@ -96,6 +96,24 @@ helpers.buildFilters = function (url, filter, query) { filter: 'unreplied', icon: 'fa-reply', }]; + + if (options.includeQandAFilters) { + filters.push({ + name: '[[unread:resolved-topics]]', + url: url + helpers.buildQueryString(query, 'filter', 'resolved'), + selected: filter === 'resolved', + filter: 'resolved', + icon: 'fa-check-circle', + }, { + name: '[[unread:unresolved-topics]]', + url: url + helpers.buildQueryString(query, 'filter', 'unresolved'), + selected: filter === 'unresolved', + filter: 'unresolved', + icon: 'fa-question-circle', + }); + } + + return filters; }; helpers.buildTerms = function (url, term, query) { diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 73d5348c0d..59081a26fb 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -87,7 +87,9 @@ recentController.getData = async function (req, url, sort, selectedTerm = 'allti } } - data.filters = helpers.buildFilters(baseUrl, filter, query); + data.filters = helpers.buildFilters(baseUrl, filter, query, { + includeQandAFilters: true, + }); data.selectedFilter = data.filters.find(filter => filter && filter.selected); data.terms = helpers.buildTerms(baseUrl, term, query); data.selectedTerm = data.terms.find(term => term && term.selected); diff --git a/src/controllers/unread.js b/src/controllers/unread.js index 871f3252c0..f69a5f4c90 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -73,7 +73,9 @@ unreadController.get = async function (req, res) { data.showCategorySelectLabel = true; data.selectedTag = tagData.selectedTag; data.selectedTags = tagData.selectedTags; - data.filters = helpers.buildFilters(baseUrl, filter, req.query); + data.filters = helpers.buildFilters(baseUrl, filter, req.query, { + includeQandAFilters: true, + }); data.selectedFilter = data.filters.find(filter => filter && filter.selected); data['reputation:disabled'] = meta.config['reputation:disabled']; diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index 868fc08e8e..d44b1523b8 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -90,6 +90,16 @@ Topics.unlock = async (req, res) => { helpers.formatApiResponse(200, res); }; +Topics.resolve = async (req, res) => { + await api.topics.resolve(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.unresolve = async (req, res) => { + await api.topics.unresolve(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + Topics.follow = async (req, res) => { await api.topics.follow(req, req.params); helpers.formatApiResponse(200, res); diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index a1c7810558..bf97f1e4bd 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -23,6 +23,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:tid/lock', [...middlewares], controllers.write.topics.lock); setupApiRoute(router, 'delete', '/:tid/lock', [...middlewares], controllers.write.topics.unlock); + setupApiRoute(router, 'put', '/:tid/resolve', [...middlewares], controllers.write.topics.resolve); + setupApiRoute(router, 'delete', '/:tid/resolve', [...middlewares], controllers.write.topics.unresolve); setupApiRoute(router, 'put', '/:tid/follow', [...middlewares, middleware.assert.topic], controllers.write.topics.follow); setupApiRoute(router, 'delete', '/:tid/follow', [...middlewares, middleware.assert.topic], controllers.write.topics.unfollow); diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js index e4044a5272..802ca48800 100644 --- a/src/socket.io/topics/tools.js +++ b/src/socket.io/topics/tools.js @@ -1,6 +1,7 @@ 'use strict'; const topics = require('../../topics'); +const categories = require('../../categories'); const privileges = require('../../privileges'); const plugins = require('../../plugins'); @@ -21,6 +22,7 @@ module.exports = function (SocketTopics) { if (!userPrivileges['topics:read'] || !userPrivileges.view_thread_tools) { throw new Error('[[error:no-privileges]]'); } + topicData.isQandA = await categories.isQandACategory(topicData.cid); topicData.privileges = userPrivileges; const result = await plugins.hooks.fire('filter:topic.thread_tools', { topic: topicData, diff --git a/src/topics/sorted.js b/src/topics/sorted.js index 2112fe3ad1..735790c78a 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -254,7 +254,7 @@ module.exports = function (Topics) { } tids = await privileges.topics.filterTids('topics:read', tids, uid); - let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid', 'tags']); + let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid', 'tags', 'resolved']); const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); async function getIgnoredCids() { @@ -263,9 +263,17 @@ module.exports = function (Topics) { } return await categories.isIgnored(topicCids, uid); } - const [ignoredCids, filtered] = await Promise.all([ + async function getQandACidsMap() { + const categoryData = await categories.getCategoriesFields(topicCids, ['cid', 'isQandA']); + return _.zipObject( + topicCids, + categoryData.map(category => category && parseInt(category.isQandA, 10) === 1) + ); + } + const [ignoredCids, filtered, qandaCidsMap] = await Promise.all([ getIgnoredCids(), user.blocks.filter(uid, topicData), + getQandACidsMap(), ]); const isCidIgnored = _.zipObject(topicCids, ignoredCids); @@ -273,13 +281,22 @@ module.exports = function (Topics) { const cids = params.cids && params.cids.map(String); const { tags } = params; + const filterResolved = filter === 'resolved'; + const filterUnresolved = filter === 'unresolved'; tids = topicData.filter(t => ( t && t.cid && !isCidIgnored[t.cid] && (cids || parseInt(t.cid, 10) !== -1) && (!cids || cids.includes(String(t.cid))) && - (!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag))) + (!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag))) && + ( + (!filterResolved && !filterUnresolved) || + (qandaCidsMap[t.cid] && ( + (filterResolved && parseInt(t.resolved, 10) === 1) || + (filterUnresolved && parseInt(t.resolved, 10) !== 1) + )) + ) )).map(t => t.tid); const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { diff --git a/src/topics/tools.js b/src/topics/tools.js index f5e9b13dc7..6538866355 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -92,6 +92,14 @@ module.exports = function (Topics) { return await toggleLock(tid, uid, false); }; + topicTools.resolve = async function (tid, uid) { + return await toggleResolve(tid, uid, true); + }; + + topicTools.unresolve = async function (tid, uid) { + return await toggleResolve(tid, uid, false); + }; + async function toggleLock(tid, uid, lock) { const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid']); if (!topicData || !topicData.cid) { @@ -110,6 +118,31 @@ module.exports = function (Topics) { return topicData; } + async function toggleResolve(tid, uid, resolve) { + const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid', 'resolved']); + if (!topicData || !topicData.cid) { + throw new Error('[[error:no-topic]]'); + } + + const [isQandA, canResolve] = await Promise.all([ + categories.isQandACategory(topicData.cid), + privileges.topics.isAdminOrMod(tid, uid), + ]); + if (!isQandA) { + throw new Error('[[error:invalid-data]]'); + } + if (!canResolve) { + throw new Error('[[error:no-privileges]]'); + } + + await Topics.setTopicField(tid, 'resolved', resolve ? 1 : 0); + topicData.isResolved = resolve; // deprecate in v2.0 + topicData.resolved = resolve ? 1 : 0; + + plugins.hooks.fire('action:topic.resolve', { topic: _.clone(topicData), uid: uid }); + return topicData; + } + topicTools.pin = async function (tid, uid) { return await togglePin(tid, uid, true); }; diff --git a/src/topics/unread.js b/src/topics/unread.js index ed93f19abf..99e911e8d9 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -90,8 +90,8 @@ module.exports = function (Topics) { }; async function getTids(params) { - const counts = { '': 0, new: 0, watched: 0, unreplied: 0 }; - const tidsByFilter = { '': [], new: [], watched: [], unreplied: [] }; + const counts = { '': 0, new: 0, watched: 0, unreplied: 0, resolved: 0, unresolved: 0 }; + const tidsByFilter = { '': [], new: [], watched: [], unreplied: [], resolved: [], unresolved: [] }; const unreadCids = []; if (params.uid <= 0) { return { counts, tids: [], tidsByFilter, unreadCids }; @@ -141,12 +141,19 @@ module.exports = function (Topics) { }); tids = await privileges.topics.filterTids('topics:read', tids, params.uid); - const topicData = (await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount', 'deleted', 'scheduled', 'tags'])) + const topicData = (await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount', 'deleted', 'scheduled', 'tags', 'resolved'])) .filter(t => t.scheduled || !t.deleted); const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); - const categoryWatchState = await categories.getWatchState(topicCids, params.uid); + const [categoryWatchState, categoryData] = await Promise.all([ + categories.getWatchState(topicCids, params.uid), + categories.getCategoriesFields(topicCids, ['cid', 'isQandA']), + ]); const userCidState = _.zipObject(topicCids, categoryWatchState); + const cidIsQandA = _.zipObject( + topicCids, + categoryData.map(category => category && parseInt(category.isQandA, 10) === 1) + ); const filterCids = params.cid && params.cid.map(cid => utils.isNumber(cid) ? parseInt(cid, 10) : cid); const filterTags = params.tag && params.tag.map(tag => String(tag)); @@ -173,6 +180,14 @@ module.exports = function (Topics) { if (!userReadTimes[topic.tid]) { tidsByFilter.new.push(topic.tid); } + + if (cidIsQandA[topic.cid]) { + if (parseInt(topic.resolved, 10) === 1) { + tidsByFilter.resolved.push(topic.tid); + } else { + tidsByFilter.unresolved.push(topic.tid); + } + } } }); @@ -180,6 +195,8 @@ module.exports = function (Topics) { counts.watched = tidsByFilter.watched.length; counts.unreplied = tidsByFilter.unreplied.length; counts.new = tidsByFilter.new.length; + counts.resolved = tidsByFilter.resolved.length; + counts.unresolved = tidsByFilter.unresolved.length; return { counts: counts, diff --git a/src/views/partials/topic/topic-menu-list.tpl b/src/views/partials/topic/topic-menu-list.tpl index f32ea80ec5..626d49fd7e 100644 --- a/src/views/partials/topic/topic-menu-list.tpl +++ b/src/views/partials/topic/topic-menu-list.tpl @@ -7,6 +7,16 @@ [[topic:thread-tools.unlock]] +{{{ if isQandA }}} +
  • + [[topic:thread-tools.resolve]] +
  • + +
  • + [[topic:thread-tools.unresolve]] +
  • +{{{ end }}} +
  • [[topic:thread-tools.pin]]
  • diff --git a/test/qanda.js b/test/qanda.js index 10635f465d..77ac05b270 100644 --- a/test/qanda.js +++ b/test/qanda.js @@ -13,6 +13,7 @@ describe('Q&A Features (Categories + Data Model)', () => { let qandaCid; let nonQandaCid; let topicData; + let nonQandaTopicData; before(async () => { adminUid = await User.create({ username: 'qanda_admin' }); @@ -88,6 +89,42 @@ describe('Q&A Features (Categories + Data Model)', () => { assert.strictEqual(data.resolved, 0); assert.strictEqual(data.acceptedPid, 0); }); + + it('should create a topic in a non-Q&A category for guard checks', async () => { + const result = await Topics.post({ + uid: adminUid, + cid: nonQandaCid, + title: 'Test non-Q&A topic', + content: 'This is a normal topic', + }); + nonQandaTopicData = result.topicData; + assert.strictEqual(nonQandaTopicData.resolved, 0); + }); + }); + + describe('Resolve/unresolve topic tools', () => { + it('should resolve a topic in a Q&A category', async () => { + const resolvedData = await Topics.tools.resolve(topicData.tid, adminUid); + assert.strictEqual(resolvedData.resolved, 1); + + const persisted = await Topics.getTopicField(topicData.tid, 'resolved'); + assert.strictEqual(persisted, 1); + }); + + it('should unresolve a topic in a Q&A category', async () => { + const unresolvedData = await Topics.tools.unresolve(topicData.tid, adminUid); + assert.strictEqual(unresolvedData.resolved, 0); + + const persisted = await Topics.getTopicField(topicData.tid, 'resolved'); + assert.strictEqual(persisted, 0); + }); + + it('should reject resolving a topic in a non-Q&A category', async () => { + await assert.rejects( + Topics.tools.resolve(nonQandaTopicData.tid, adminUid), + /\[\[error:invalid-data\]\]/ + ); + }); }); describe('Questions categories upgrade script', () => { @@ -107,4 +144,34 @@ describe('Q&A Features (Categories + Data Model)', () => { assert.strictEqual(topics[0].isQandA, true); }); }); + + describe('resolved/unresolved filters', () => { + it('should only include Q&A resolved topics in resolved filter', async () => { + await Topics.tools.resolve(topicData.tid, adminUid); + const data = await Topics.getSortedTopics({ + uid: adminUid, + start: 0, + stop: 20, + filter: 'resolved', + sort: 'recent', + term: 'alltime', + }); + assert.strictEqual(data.topics.some(t => t.tid === topicData.tid), true); + assert.strictEqual(data.topics.every(t => t.isQandA && t.resolved === 1), true); + }); + + it('should only include Q&A unresolved topics in unresolved filter', async () => { + await Topics.tools.unresolve(topicData.tid, adminUid); + const data = await Topics.getSortedTopics({ + uid: adminUid, + start: 0, + stop: 20, + filter: 'unresolved', + sort: 'recent', + term: 'alltime', + }); + assert.strictEqual(data.topics.some(t => t.tid === topicData.tid), true); + assert.strictEqual(data.topics.every(t => t.isQandA && t.resolved === 0), true); + }); + }); }); diff --git a/vendor/nodebb-theme-harmony-2.1.35/scss/common.scss b/vendor/nodebb-theme-harmony-2.1.35/scss/common.scss index 2ba4e047a2..2a877aae43 100644 --- a/vendor/nodebb-theme-harmony-2.1.35/scss/common.scss +++ b/vendor/nodebb-theme-harmony-2.1.35/scss/common.scss @@ -94,6 +94,17 @@ body:not(.page-user) { top: 0; padding: 0.25rem 0; } +.qa-status-resolved { + color: var(--bs-success-text-emphasis); + background-color: var(--bs-success-bg-subtle); + border-color: var(--bs-success-border-subtle) !important; +} + +.qa-status-unresolved { + color: var(--bs-warning-text-emphasis); + background-color: var(--bs-warning-bg-subtle); + border-color: var(--bs-warning-border-subtle) !important; +} // quartz doesn't need body-bg for tool background .skin-quartz .sticky-tools { background-color: initial; @@ -129,4 +140,4 @@ body:not(.page-user) { [component="category/posts"] .post-content, .post-queue.posts-list .post-content { a { text-decoration: underline;} -} \ No newline at end of file +} diff --git a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topics_list.tpl b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topics_list.tpl index 0e247cbb7d..148b33e3d3 100644 --- a/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topics_list.tpl +++ b/vendor/nodebb-theme-harmony-2.1.35/templates/partials/topics_list.tpl @@ -44,6 +44,14 @@ [[topic:locked]] + + + [[topic:resolved]] + + + + [[topic:unresolved]] + [[topic:moved]] diff --git a/vendor/nodebb-theme-harmony-2.1.35/templates/topic.tpl b/vendor/nodebb-theme-harmony-2.1.35/templates/topic.tpl index 8a298b1f10..88d0df8ff1 100644 --- a/vendor/nodebb-theme-harmony-2.1.35/templates/topic.tpl +++ b/vendor/nodebb-theme-harmony-2.1.35/templates/topic.tpl @@ -37,7 +37,7 @@
    - + [[topic:scheduled]] @@ -47,6 +47,12 @@ [[topic:locked]] + + [[topic:resolved]] + + + [[topic:unresolved]] + {{{ if privileges.isAdminOrMod }}}[[topic:moved-from, {oldCategory.name}]]{{{ else }}}[[topic:moved]]{{{ end }}} diff --git a/vendor/nodebb-theme-harmony-main/scss/common.scss b/vendor/nodebb-theme-harmony-main/scss/common.scss index 2ba4e047a2..2a877aae43 100644 --- a/vendor/nodebb-theme-harmony-main/scss/common.scss +++ b/vendor/nodebb-theme-harmony-main/scss/common.scss @@ -94,6 +94,17 @@ body:not(.page-user) { top: 0; padding: 0.25rem 0; } +.qa-status-resolved { + color: var(--bs-success-text-emphasis); + background-color: var(--bs-success-bg-subtle); + border-color: var(--bs-success-border-subtle) !important; +} + +.qa-status-unresolved { + color: var(--bs-warning-text-emphasis); + background-color: var(--bs-warning-bg-subtle); + border-color: var(--bs-warning-border-subtle) !important; +} // quartz doesn't need body-bg for tool background .skin-quartz .sticky-tools { background-color: initial; @@ -129,4 +140,4 @@ body:not(.page-user) { [component="category/posts"] .post-content, .post-queue.posts-list .post-content { a { text-decoration: underline;} -} \ No newline at end of file +} diff --git a/vendor/nodebb-theme-harmony-main/templates/partials/topics_list.tpl b/vendor/nodebb-theme-harmony-main/templates/partials/topics_list.tpl index 0e247cbb7d..148b33e3d3 100644 --- a/vendor/nodebb-theme-harmony-main/templates/partials/topics_list.tpl +++ b/vendor/nodebb-theme-harmony-main/templates/partials/topics_list.tpl @@ -44,6 +44,14 @@ [[topic:locked]] + + + [[topic:resolved]] + + + + [[topic:unresolved]] + [[topic:moved]] diff --git a/vendor/nodebb-theme-harmony-main/templates/topic.tpl b/vendor/nodebb-theme-harmony-main/templates/topic.tpl index 8a298b1f10..88d0df8ff1 100644 --- a/vendor/nodebb-theme-harmony-main/templates/topic.tpl +++ b/vendor/nodebb-theme-harmony-main/templates/topic.tpl @@ -37,7 +37,7 @@
    - + [[topic:scheduled]] @@ -47,6 +47,12 @@ [[topic:locked]] + + [[topic:resolved]] + + + [[topic:unresolved]] + {{{ if privileges.isAdminOrMod }}}[[topic:moved-from, {oldCategory.name}]]{{{ else }}}[[topic:moved]]{{{ end }}}