mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: category filter on post queue (#8710)
* feat: category filter on post queue category filter module * feat: add spec
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							f14b49457c
						
					
				
				
					commit
					5d9a868142
				
			| @@ -42,6 +42,49 @@ get: | |||||||
|                           type: number |                           type: number | ||||||
|                         slug: |                         slug: | ||||||
|                           type: string |                           type: string | ||||||
|  |                   categories: | ||||||
|  |                     type: array | ||||||
|  |                     items: | ||||||
|  |                       type: object | ||||||
|  |                       properties: | ||||||
|  |                         bgColor: | ||||||
|  |                           type: string | ||||||
|  |                         cid: | ||||||
|  |                           type: number | ||||||
|  |                         color: | ||||||
|  |                           type: string | ||||||
|  |                         disabledClass: | ||||||
|  |                           nullable: true | ||||||
|  |                         icon: | ||||||
|  |                           type: string | ||||||
|  |                         imageClass: | ||||||
|  |                           type: string | ||||||
|  |                         level: | ||||||
|  |                           type: string | ||||||
|  |                         link: | ||||||
|  |                           type: string | ||||||
|  |                         name: | ||||||
|  |                           type: string | ||||||
|  |                         parentCid: | ||||||
|  |                           type: number | ||||||
|  |                         slug: | ||||||
|  |                           type: string | ||||||
|  |                   allCategoriesUrl: | ||||||
|  |                     type: string | ||||||
|  |                   selectedCategory: | ||||||
|  |                     type: object | ||||||
|  |                     properties: | ||||||
|  |                       icon: | ||||||
|  |                         type: string | ||||||
|  |                       name: | ||||||
|  |                         type: string | ||||||
|  |                       bgColor: | ||||||
|  |                         type: string | ||||||
|  |                     nullable: true | ||||||
|  |                   selectedCids: | ||||||
|  |                     type: array | ||||||
|  |                     items: | ||||||
|  |                       type: number | ||||||
|                   posts: |                   posts: | ||||||
|                     type: array |                     type: array | ||||||
|                     items: |                     items: | ||||||
|   | |||||||
| @@ -1,12 +1,16 @@ | |||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
|  |  | ||||||
| define('forum/post-queue', ['categorySelector'], function (categorySelector) { | define('forum/post-queue', [ | ||||||
|  | 	'categoryFilter', 'categorySelector', | ||||||
|  | ], function (categoryFilter, categorySelector) { | ||||||
| 	var PostQueue = {}; | 	var PostQueue = {}; | ||||||
|  |  | ||||||
| 	PostQueue.init = function () { | 	PostQueue.init = function () { | ||||||
| 		$('[data-toggle="tooltip"]').tooltip(); | 		$('[data-toggle="tooltip"]').tooltip(); | ||||||
|  |  | ||||||
|  | 		categoryFilter.init($('[component="category/dropdown"]')); | ||||||
|  |  | ||||||
| 		$('.posts-list').on('click', '[data-action]', function () { | 		$('.posts-list').on('click', '[data-action]', function () { | ||||||
| 			var parent = $(this).parents('[data-id]'); | 			var parent = $(this).parents('[data-id]'); | ||||||
| 			var action = $(this).attr('data-action'); | 			var action = $(this).attr('data-action'); | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								public/src/modules/categoryFilter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								public/src/modules/categoryFilter.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | 'use strict'; | ||||||
|  |  | ||||||
|  | define('categoryFilter', ['categorySearch'], function (categorySearch) { | ||||||
|  | 	var categoryFilter = {}; | ||||||
|  |  | ||||||
|  | 	categoryFilter.init = function (el) { | ||||||
|  | 		categorySearch.init(el); | ||||||
|  | 		var listEl = el.find('[component="category/list"]'); | ||||||
|  |  | ||||||
|  | 		el.on('hidden.bs.dropdown', function () { | ||||||
|  | 			var cids = getSelectedCids(el); | ||||||
|  | 			var changed = ajaxify.data.selectedCids.length !== cids.length; | ||||||
|  | 			ajaxify.data.selectedCids.forEach(function (cid, index) { | ||||||
|  | 				if (cid !== cids[index]) { | ||||||
|  | 					changed = true; | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  |  | ||||||
|  | 			if (changed) { | ||||||
|  | 				var url = window.location.pathname; | ||||||
|  | 				var currentParams = utils.params(); | ||||||
|  | 				if (cids.length) { | ||||||
|  | 					currentParams.cid = cids; | ||||||
|  | 					url += '?' + decodeURIComponent($.param(currentParams)); | ||||||
|  | 				} | ||||||
|  | 				ajaxify.go(url); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  |  | ||||||
|  | 		listEl.on('click', '[data-cid]', function (ev) { | ||||||
|  | 			function selectChildren(parentCid, flag) { | ||||||
|  | 				listEl.find('[data-parent-cid="' + parentCid + '"] [component="category/select/icon"]').toggleClass('invisible', flag); | ||||||
|  | 				listEl.find('[data-parent-cid="' + parentCid + '"]').each(function (index, el) { | ||||||
|  | 					selectChildren($(el).attr('data-cid'), flag); | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 			var categoryEl = $(this); | ||||||
|  | 			var link = categoryEl.find('a').attr('href'); | ||||||
|  | 			if (link && link !== '#' && link.length) { | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			var cid = categoryEl.attr('data-cid'); | ||||||
|  | 			if (ev.ctrlKey) { | ||||||
|  | 				selectChildren(cid, !categoryEl.find('[component="category/select/icon"]').hasClass('invisible')); | ||||||
|  | 			} | ||||||
|  | 			categoryEl.find('[component="category/select/icon"]').toggleClass('invisible'); | ||||||
|  | 			listEl.find('li').first().find('i').toggleClass('invisible', !!getSelectedCids(el).length); | ||||||
|  | 			return false; | ||||||
|  | 		}); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	function getSelectedCids(el) { | ||||||
|  | 		var cids = []; | ||||||
|  | 		el.find('[component="category/list"] [data-cid]').each(function (index, el) { | ||||||
|  | 			if (!$(el).find('[component="category/select/icon"]').hasClass('invisible')) { | ||||||
|  | 				cids.push(parseInt($(el).attr('data-cid'), 10)); | ||||||
|  | 			} | ||||||
|  | 		}); | ||||||
|  | 		cids.sort(function (a, b) { | ||||||
|  | 			return a - b; | ||||||
|  | 		}); | ||||||
|  | 		return cids; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return categoryFilter; | ||||||
|  | }); | ||||||
| @@ -4,9 +4,9 @@ define('topicList', [ | |||||||
| 	'forum/infinitescroll', | 	'forum/infinitescroll', | ||||||
| 	'handleBack', | 	'handleBack', | ||||||
| 	'topicSelect', | 	'topicSelect', | ||||||
| 	'categorySearch', | 	'categoryFilter', | ||||||
| 	'forum/category/tools', | 	'forum/category/tools', | ||||||
| ], function (infinitescroll, handleBack, topicSelect, categorySearch, categoryTools) { | ], function (infinitescroll, handleBack, topicSelect, categoryFilter, categoryTools) { | ||||||
| 	var TopicList = {}; | 	var TopicList = {}; | ||||||
| 	var templateName = ''; | 	var templateName = ''; | ||||||
|  |  | ||||||
| @@ -38,7 +38,7 @@ define('topicList', [ | |||||||
|  |  | ||||||
| 		TopicList.watchForNewPosts(); | 		TopicList.watchForNewPosts(); | ||||||
|  |  | ||||||
| 		TopicList.handleCategorySelection(); | 		categoryFilter.init($('[component="category/dropdown"]')); | ||||||
|  |  | ||||||
| 		if (!config.usePagination) { | 		if (!config.usePagination) { | ||||||
| 			infinitescroll.init(TopicList.loadMoreTopics); | 			infinitescroll.init(TopicList.loadMoreTopics); | ||||||
| @@ -159,64 +159,6 @@ define('topicList', [ | |||||||
| 		$('#category-no-topics').addClass('hide'); | 		$('#category-no-topics').addClass('hide'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	TopicList.handleCategorySelection = function () { |  | ||||||
| 		function getSelectedCids() { |  | ||||||
| 			var cids = []; |  | ||||||
| 			$('[component="category/list"] [data-cid]').each(function (index, el) { |  | ||||||
| 				if ($(el).find('i.fa-check').length) { |  | ||||||
| 					cids.push(parseInt($(el).attr('data-cid'), 10)); |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
| 			cids.sort(function (a, b) { |  | ||||||
| 				return a - b; |  | ||||||
| 			}); |  | ||||||
| 			return cids; |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		categorySearch.init($('[component="category/dropdown"]')); |  | ||||||
|  |  | ||||||
| 		$('[component="category/dropdown"]').on('hidden.bs.dropdown', function () { |  | ||||||
| 			var cids = getSelectedCids(); |  | ||||||
| 			var changed = ajaxify.data.selectedCids.length !== cids.length; |  | ||||||
| 			ajaxify.data.selectedCids.forEach(function (cid, index) { |  | ||||||
| 				if (cid !== cids[index]) { |  | ||||||
| 					changed = true; |  | ||||||
| 				} |  | ||||||
| 			}); |  | ||||||
|  |  | ||||||
| 			if (changed) { |  | ||||||
| 				var url = window.location.pathname; |  | ||||||
| 				var currentParams = utils.params(); |  | ||||||
| 				if (cids.length) { |  | ||||||
| 					currentParams.cid = cids; |  | ||||||
| 					url += '?' + decodeURIComponent($.param(currentParams)); |  | ||||||
| 				} |  | ||||||
| 				ajaxify.go(url); |  | ||||||
| 			} |  | ||||||
| 		}); |  | ||||||
|  |  | ||||||
| 		$('[component="category/list"]').on('click', '[data-cid]', function (ev) { |  | ||||||
| 			function selectChildren(parentCid, flag) { |  | ||||||
| 				$('[component="category/list"] [data-parent-cid="' + parentCid + '"] [component="category/select/icon"]').toggleClass('fa-check', flag); |  | ||||||
| 				$('[component="category/list"] [data-parent-cid="' + parentCid + '"]').each(function (index, el) { |  | ||||||
| 					selectChildren($(el).attr('data-cid'), flag); |  | ||||||
| 				}); |  | ||||||
| 			} |  | ||||||
| 			var categoryEl = $(this); |  | ||||||
| 			var link = categoryEl.find('a').attr('href'); |  | ||||||
| 			if (link && link !== '#' && link.length) { |  | ||||||
| 				return; |  | ||||||
| 			} |  | ||||||
| 			var cid = categoryEl.attr('data-cid'); |  | ||||||
| 			if (ev.ctrlKey) { |  | ||||||
| 				selectChildren(cid, !categoryEl.find('[component="category/select/icon"]').hasClass('fa-check')); |  | ||||||
| 			} |  | ||||||
| 			categoryEl.find('[component="category/select/icon"]').toggleClass('fa-check'); |  | ||||||
| 			$('[component="category/list"] li').first().find('i').toggleClass('fa-check', !getSelectedCids().length); |  | ||||||
| 			return false; |  | ||||||
| 		}); |  | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	TopicList.loadMoreTopics = function (direction) { | 	TopicList.loadMoreTopics = function (direction) { | ||||||
| 		if (!topicListEl.length || !topicListEl.children().length) { | 		if (!topicListEl.length || !topicListEl.children().length) { | ||||||
| 			return; | 			return; | ||||||
|   | |||||||
| @@ -214,15 +214,15 @@ helpers.buildTitle = function (pageTitle) { | |||||||
|  |  | ||||||
| helpers.getCategories = async function (set, uid, privilege, selectedCid) { | helpers.getCategories = async function (set, uid, privilege, selectedCid) { | ||||||
| 	const cids = await categories.getCidsByPrivilege(set, uid, privilege); | 	const cids = await categories.getCidsByPrivilege(set, uid, privilege); | ||||||
| 	return await getCategoryData(cids, uid, selectedCid); | 	return await getCategoryData(cids, uid, selectedCid, privilege); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| helpers.getCategoriesByStates = async function (uid, selectedCid, states) { | helpers.getCategoriesByStates = async function (uid, selectedCid, states, privilege = 'topics:read') { | ||||||
| 	const cids = await categories.getAllCidsFromSet('categories:cid'); | 	const cids = await categories.getAllCidsFromSet('categories:cid'); | ||||||
| 	return await getCategoryData(cids, uid, selectedCid, states); | 	return await getCategoryData(cids, uid, selectedCid, states, privilege); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| async function getCategoryData(cids, uid, selectedCid, states) { | async function getCategoryData(cids, uid, selectedCid, states, privilege) { | ||||||
| 	if (selectedCid && !Array.isArray(selectedCid)) { | 	if (selectedCid && !Array.isArray(selectedCid)) { | ||||||
| 		selectedCid = [selectedCid]; | 		selectedCid = [selectedCid]; | ||||||
| 	} | 	} | ||||||
| @@ -230,7 +230,7 @@ async function getCategoryData(cids, uid, selectedCid, states) { | |||||||
| 	states = states || [categories.watchStates.watching, categories.watchStates.notwatching]; | 	states = states || [categories.watchStates.watching, categories.watchStates.notwatching]; | ||||||
|  |  | ||||||
| 	const [allowed, watchState, categoryData, isAdmin] = await Promise.all([ | 	const [allowed, watchState, categoryData, isAdmin] = await Promise.all([ | ||||||
| 		privileges.categories.isUserAllowedTo('topics:read', cids, uid), | 		privileges.categories.isUserAllowedTo(privilege, cids, uid), | ||||||
| 		categories.getWatchState(cids, uid), | 		categories.getWatchState(cids, uid), | ||||||
| 		categories.getCategoriesData(cids), | 		categories.getCategoriesData(cids), | ||||||
| 		user.isAdministrator(uid), | 		user.isAdministrator(uid), | ||||||
| @@ -246,6 +246,11 @@ async function getCategoryData(cids, uid, selectedCid, states) { | |||||||
| 		const hasVisibleChildren = checkVisibleChildren(c, cidToAllowed, cidToWatchState, states); | 		const hasVisibleChildren = checkVisibleChildren(c, cidToAllowed, cidToWatchState, states); | ||||||
| 		const isCategoryVisible = c && cidToAllowed[c.cid] && !c.link && !c.disabled && states.includes(cidToWatchState[c.cid]); | 		const isCategoryVisible = c && cidToAllowed[c.cid] && !c.link && !c.disabled && states.includes(cidToWatchState[c.cid]); | ||||||
| 		const shouldBeRemoved = !hasVisibleChildren && !isCategoryVisible; | 		const shouldBeRemoved = !hasVisibleChildren && !isCategoryVisible; | ||||||
|  | 		const shouldBeDisaplayedAsDisabled = hasVisibleChildren && !isCategoryVisible; | ||||||
|  |  | ||||||
|  | 		if (shouldBeDisaplayedAsDisabled) { | ||||||
|  | 			c.disabledClass = true; | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if (shouldBeRemoved && c && c.parent && c.parent.cid && cidToCategory[c.parent.cid]) { | 		if (shouldBeRemoved && c && c.parent && c.parent.cid && cidToCategory[c.parent.cid]) { | ||||||
| 			cidToCategory[c.parent.cid].children = cidToCategory[c.parent.cid].children.filter(child => child.cid !== c.cid); | 			cidToCategory[c.parent.cid].children = cidToCategory[c.parent.cid].children.filter(child => child.cid !== c.cid); | ||||||
| @@ -254,7 +259,7 @@ async function getCategoryData(cids, uid, selectedCid, states) { | |||||||
| 		return c && !shouldBeRemoved; | 		return c && !shouldBeRemoved; | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	const categoriesData = categories.buildForSelectCategories(visibleCategories); | 	const categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass']); | ||||||
|  |  | ||||||
| 	let selectedCategory = []; | 	let selectedCategory = []; | ||||||
| 	const selectedCids = []; | 	const selectedCids = []; | ||||||
|   | |||||||
| @@ -202,15 +202,16 @@ modsController.postQueue = async function (req, res, next) { | |||||||
| 	if (!isPrivileged) { | 	if (!isPrivileged) { | ||||||
| 		return next(); | 		return next(); | ||||||
| 	} | 	} | ||||||
|  | 	const cid = req.query.cid; | ||||||
| 	const page = parseInt(req.query.page, 10) || 1; | 	const page = parseInt(req.query.page, 10) || 1; | ||||||
| 	const postsPerPage = 20; | 	const postsPerPage = 20; | ||||||
|  |  | ||||||
| 	const [ids, isAdminOrGlobalMod, moderatedCids, allCategories] = await Promise.all([ | 	const [ids, isAdminOrGlobalMod, moderatedCids, allCategories, categoriesData] = await Promise.all([ | ||||||
| 		db.getSortedSetRange('post:queue', 0, -1), | 		db.getSortedSetRange('post:queue', 0, -1), | ||||||
| 		user.isAdminOrGlobalMod(req.uid), | 		user.isAdminOrGlobalMod(req.uid), | ||||||
| 		user.getModeratedCids(req.uid), | 		user.getModeratedCids(req.uid), | ||||||
| 		categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']), | 		categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']), | ||||||
|  | 		helpers.getCategoriesByStates(req.uid, cid, null, 'moderate'), | ||||||
| 	]); | 	]); | ||||||
|  |  | ||||||
| 	allCategories.forEach((c) => { | 	allCategories.forEach((c) => { | ||||||
| @@ -218,7 +219,9 @@ modsController.postQueue = async function (req, res, next) { | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	let postData = await getQueuedPosts(ids); | 	let postData = await getQueuedPosts(ids); | ||||||
| 	postData = postData.filter(p => p && (isAdminOrGlobalMod || moderatedCids.includes(String(p.category.cid)))); | 	postData = postData.filter(p => p && | ||||||
|  | 		(!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) && | ||||||
|  | 		(isAdminOrGlobalMod || moderatedCids.includes(String(p.category.cid)))); | ||||||
|  |  | ||||||
| 	({ posts: postData } = await plugins.fireHook('filter:post-queue.get', { | 	({ posts: postData } = await plugins.fireHook('filter:post-queue.get', { | ||||||
| 		posts: postData, | 		posts: postData, | ||||||
| @@ -234,6 +237,8 @@ modsController.postQueue = async function (req, res, next) { | |||||||
| 		title: '[[pages:post-queue]]', | 		title: '[[pages:post-queue]]', | ||||||
| 		posts: postData, | 		posts: postData, | ||||||
| 		allCategories: allCategories, | 		allCategories: allCategories, | ||||||
|  | 		...categoriesData, | ||||||
|  | 		allCategoriesUrl: 'post-queue' + helpers.buildQueryString(req.query, 'cid', ''), | ||||||
| 		pagination: pagination.create(page, pageCount), | 		pagination: pagination.create(page, pageCount), | ||||||
| 		breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]), | 		breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:post-queue]]' }]), | ||||||
| 	}); | 	}); | ||||||
| @@ -268,7 +273,7 @@ async function addMetaData(postData) { | |||||||
| 	} | 	} | ||||||
| 	postData.topic = { cid: 0 }; | 	postData.topic = { cid: 0 }; | ||||||
| 	if (postData.data.cid) { | 	if (postData.data.cid) { | ||||||
| 		postData.topic = { cid: postData.data.cid }; | 		postData.topic = { cid: parseInt(postData.data.cid, 10) }; | ||||||
| 	} else if (postData.data.tid) { | 	} else if (postData.data.tid) { | ||||||
| 		postData.topic = await topics.getTopicFields(postData.data.tid, ['title', 'cid']); | 		postData.topic = await topics.getTopicFields(postData.data.tid, ['title', 'cid']); | ||||||
| 	} | 	} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user