From 346db0d84dbfdd9109cd30188c94a956f6745ca9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 18 Aug 2020 21:03:59 -0400 Subject: [PATCH] feat: flags list sorting, closes #8569 --- public/language/en-GB/flags.json | 5 ++++ public/src/client/flags/list.js | 1 + src/controllers/mods.js | 39 ++++++++++++++++++++++++-------- src/flags.js | 39 ++++++++++++++++++++++++++------ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json index 1259cf8cbb..958ff73852 100644 --- a/public/language/en-GB/flags.json +++ b/public/language/en-GB/flags.json @@ -57,6 +57,11 @@ "state-rejected": "Rejected", "no-assignee": "Not Assigned", + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "modal-title": "Report Inappropriate Content", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-reason-spam": "Spam", diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js index 3dcd99b389..e9e44f2a74 100644 --- a/public/src/client/flags/list.js +++ b/public/src/client/flags/list.js @@ -28,6 +28,7 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart) filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]); } } + filtersEl.find('[name="sort"]').val(ajaxify.data.sort); document.getElementById('apply-filters').addEventListener('click', function () { var payload = filtersEl.serializeArray().filter(function (item) { diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 3c0a423a18..2484361ea4 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -18,18 +18,23 @@ const modsController = module.exports; modsController.flags = {}; modsController.flags.list = async function (req, res, next) { - let validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick', 'page', 'perPage']; + const validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick', 'page', 'perPage']; + const validSorts = ['newest', 'oldest', 'reports']; // Reset filters if explicitly requested if (parseInt(req.query.reset, 10) === 1) { delete req.session.flags_filters; + delete req.session.flags_sort; } - const [isAdminOrGlobalMod, moderatedCids, data] = await Promise.all([ + const results = await Promise.all([ user.isAdminOrGlobalMod(req.uid), user.getModeratedCids(req.uid), plugins.fireHook('filter:flags.validateFilters', { filters: validFilters }), + plugins.fireHook('filter:flags.validateSort', { sorts: validSorts }), ]); + const [isAdminOrGlobalMod, moderatedCids,, { sorts }] = results; + let [,, { filters }] = results; if (!(isAdminOrGlobalMod || !!moderatedCids.length)) { return next(new Error('[[error:no-privileges]]')); @@ -39,10 +44,8 @@ modsController.flags.list = async function (req, res, next) { res.locals.cids = moderatedCids; } - validFilters = data.filters; - - // Parse query string params for filters - let filters = validFilters.reduce(function (memo, cur) { + // Parse query string params for filters, eliminate non-valid filters + filters = filters.reduce(function (memo, cur) { if (req.query.hasOwnProperty(cur)) { memo[cur] = req.query[cur]; } @@ -71,15 +74,32 @@ modsController.flags.list = async function (req, res, next) { } // Pagination doesn't count as a filter - if (Object.keys(filters).length === 1 && filters.hasOwnProperty('page')) { + if ( + (Object.keys(filters).length === 1 && filters.hasOwnProperty('page')) || + (Object.keys(filters).length === 2 && filters.hasOwnProperty('page') && filters.hasOwnProperty('perPage')) + ) { hasFilter = false; } - // Save filters into session unless removed + // Parse sort from query string + let sort; + if (!req.query.sort && req.session.hasOwnProperty('flags_sort')) { + sort = req.session.flags_sort; + } else { + sort = sorts.includes(req.query.sort) ? req.query.sort : null; + } + hasFilter = hasFilter || !!sort; + + // Save filters and sorting into session unless removed req.session.flags_filters = filters; + req.session.flags_sort = sort; const [flagsData, analyticsData, categoriesData] = await Promise.all([ - flags.list(filters, req.uid), + flags.list({ + filters: filters, + sort: sort, + uid: req.uid, + }), analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30), categories.buildForSelect(req.uid, 'read'), ]); @@ -90,6 +110,7 @@ modsController.flags.list = async function (req, res, next) { categories: filterCategories(res.locals.cids, categoriesData), hasFilter: hasFilter, filters: filters, + sort: sort || 'newest', title: '[[pages:flags]]', pagination: pagination.create(flagsData.page, flagsData.pageCount, req.query), breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:flags]]' }]), diff --git a/src/flags.js b/src/flags.js index d2a3c3273e..c445d4de73 100644 --- a/src/flags.js +++ b/src/flags.js @@ -112,8 +112,8 @@ Flags.get = async function (flagId) { return data.flag; }; -Flags.list = async function (filters, uid) { - filters = filters || {}; +Flags.list = async function (data) { + const filters = data.filters || {}; let sets = []; const orSets = []; @@ -125,7 +125,7 @@ Flags.list = async function (filters, uid) { for (var type in filters) { if (filters.hasOwnProperty(type)) { if (Flags._filters.hasOwnProperty(type)) { - Flags._filters[type](sets, orSets, filters[type], uid); + Flags._filters[type](sets, orSets, filters[type], data.uid); } else { winston.warn('[flags/list] No flag filter type found: ' + type); } @@ -151,6 +151,8 @@ Flags.list = async function (filters, uid) { } } + flagIds = await Flags.sort(flagIds, data.sort); + // Create subset for parsing based on page number (n=20) const flagsPerPage = Math.abs(parseInt(filters.perPage, 10) || 1); const pageCount = Math.ceil(flagIds.length / flagsPerPage); @@ -174,19 +176,42 @@ Flags.list = async function (filters, uid) { }); })); - const data = await plugins.fireHook('filter:flags.list', { + const payload = await plugins.fireHook('filter:flags.list', { flags: flags, page: filters.page, - uid: uid, + uid: data.uid, }); return { - flags: data.flags, - page: data.page, + flags: payload.flags, + page: payload.page, pageCount: pageCount, }; }; +Flags.sort = async function (flagIds, sort) { + switch (sort) { + // 'newest' is not handled because that is default + case 'oldest': + flagIds = flagIds.reverse(); + break; + + case 'reports': { + const keys = flagIds.map(id => `flag:${id}:reports`); + const heat = await db.sortedSetsCard(keys); + const mapped = heat.map((el, i) => ({ + index: i, heat: el, + })); + mapped.sort(function (a, b) { + return b.heat - a.heat; + }); + flagIds = mapped.map(obj => flagIds[obj.index]); + break; + } + } + return flagIds; +}; + Flags.validate = async function (payload) { const [target, reporter] = await Promise.all([ Flags.getTarget(payload.type, payload.id, payload.uid),