From d1364c313021e48a879a818b24947e1457c062f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 1 Feb 2021 15:19:32 -0500 Subject: [PATCH] Categories refactor (#9233) * feat: wip categories pagination * feat: add subCategoriesPerPage setting * feat: add load more sub categories button to category page * fix: openapi spec * feat: show sub categories left on category page hide button when no more categories left * breaking: rename categories to allCategories on /search categories contains the search results * fix: spec * refactor: remove cidsPerPage * fix: tests * feat: use component for subcategories * fix: prevent negative subCategoriesLeft --- install/data/defaults.json | 1 + .../en-GB/admin/manage/categories.json | 1 + .../en-GB/admin/settings/pagination.json | 4 +- public/language/en-GB/category.json | 3 +- .../components/schemas/CategoryObject.yaml | 3 ++ public/openapi/read/categories.yaml | 1 + public/openapi/read/index.yaml | 1 + public/openapi/read/search.yaml | 8 ++-- public/src/client/category.js | 30 ++++++++++++ src/categories/create.js | 1 + src/categories/data.js | 2 +- src/controllers/categories.js | 46 ++++++++++++++----- src/controllers/category.js | 7 +++ src/controllers/search.js | 6 +-- src/socket.io/categories.js | 17 +++++++ src/upgrades/1.17.0/subcategories_per_page.js | 23 ++++++++++ src/views/admin/manage/category.tpl | 12 ++++- src/views/admin/settings/pagination.tpl | 13 +++++- 18 files changed, 152 insertions(+), 27 deletions(-) create mode 100644 src/upgrades/1.17.0/subcategories_per_page.js diff --git a/install/data/defaults.json b/install/data/defaults.json index 556cf055c7..639c5c40a5 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -101,6 +101,7 @@ "maxPostsPerPage": 20, "topicsPerPage": 20, "postsPerPage": 20, + "categoriesPerPage": 50, "userSearchResultsPerPage": 50, "maximumGroupNameLength": 255, "maximumGroupTitleLength": 40, diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json index 1144e16433..eef2e00529 100644 --- a/public/language/en-GB/admin/manage/categories.json +++ b/public/language/en-GB/admin/manage/categories.json @@ -10,6 +10,7 @@ "custom-class": "Custom Class", "num-recent-replies": "# of Recent Replies", "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", "is-section": "Treat this category as a section", "post-queue": "Post queue", "tag-whitelist": "Tag Whitelist", diff --git a/public/language/en-GB/admin/settings/pagination.json b/public/language/en-GB/admin/settings/pagination.json index 3eb210243e..3bf306b2f9 100644 --- a/public/language/en-GB/admin/settings/pagination.json +++ b/public/language/en-GB/admin/settings/pagination.json @@ -1,10 +1,12 @@ { "pagination": "Pagination Settings", "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", "topics": "Topic Pagination", "posts-per-page": "Posts per Page", "max-posts-per-page": "Maximum posts per page", "categories": "Category Pagination", "topics-per-page": "Topics per Page", - "max-topics-per-page": "Maximum topics per page" + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" } \ No newline at end of file diff --git a/public/language/en-GB/category.json b/public/language/en-GB/category.json index 96122292f0..ef2c4beec5 100644 --- a/public/language/en-GB/category.json +++ b/public/language/en-GB/category.json @@ -23,5 +23,6 @@ "notwatching.message": "You are not watching updates from this category and all subcategories", "ignoring.message": "You are now ignoring updates from this category and all subcategories", - "watched-categories": "Watched categories" + "watched-categories": "Watched categories", + "x-more-categories": "%1 more categories" } diff --git a/public/openapi/components/schemas/CategoryObject.yaml b/public/openapi/components/schemas/CategoryObject.yaml index 384349bc9e..4d6cb0ca4e 100644 --- a/public/openapi/components/schemas/CategoryObject.yaml +++ b/public/openapi/components/schemas/CategoryObject.yaml @@ -72,6 +72,9 @@ CategoryObject: totalTopicCount: type: number description: The number of topics in the category + subCategoriesPerPage: + type: number + description: The number of subcategories to display on the categories and category page - type: object description: Optional properties that may or may not be present (except for `cid`, which is always present, and is only here as a hack to pass validation) properties: diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml index 36f7b3b797..7ec6fe5d81 100644 --- a/public/openapi/read/categories.yaml +++ b/public/openapi/read/categories.yaml @@ -207,5 +207,6 @@ get: type: string imageClass: type: string + - $ref: ../components/schemas/Pagination.yaml#/Pagination - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/index.yaml b/public/openapi/read/index.yaml index 33a1daadd9..7750cb54e1 100644 --- a/public/openapi/read/index.yaml +++ b/public/openapi/read/index.yaml @@ -205,4 +205,5 @@ get: type: string imageClass: type: string + - $ref: ../components/schemas/Pagination.yaml#/Pagination - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/search.yaml b/public/openapi/read/search.yaml index b1c7984c68..9ada567f59 100644 --- a/public/openapi/read/search.yaml +++ b/public/openapi/read/search.yaml @@ -25,7 +25,7 @@ get: type: string term: type: string - categories: + allCategories: type: array items: type: object @@ -36,7 +36,7 @@ get: - type: number text: type: string - categoriesCount: + allCategoriesCount: type: number expandSearch: type: boolean @@ -64,8 +64,8 @@ get: - time - multiplePages - search_query - - categories - - categoriesCount + - allCategories + - allCategoriesCount - expandSearch - showAsPosts - showAsTopics diff --git a/public/src/client/category.js b/public/src/client/category.js index bf6b5da4f7..ce5484bb29 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -36,6 +36,8 @@ define('forum/category', [ handleIgnoreWatch(cid); + handleLoadMoreSubcategories(); + $(window).trigger('action:topics.loaded', { topics: ajaxify.data.topics }); $(window).trigger('action:category.loaded', { cid: ajaxify.data.cid }); }; @@ -74,6 +76,34 @@ define('forum/category', [ }); } + function handleLoadMoreSubcategories() { + $('[component="category/load-more-subcategories"]').on('click', function () { + var btn = $(this); + socket.emit('categories.loadMoreSubCategories', { + cid: ajaxify.data.cid, + start: ajaxify.data.nextSubCategoryStart, + }, function (err, data) { + if (err) { + return app.alertError(err); + } + btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage); + if (!data.length) { + return; + } + app.parseAndTranslate('category', 'children', { children: data }, function (html) { + html.find('.timeago').timeago(); + $('[component="category/subcategory/container"]').append(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + app.createUserTooltips(html); + ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage; + ajaxify.data.subCategoriesLeft -= data.length; + btn.translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]'); + }); + }); + return false; + }); + } + Category.toTop = function () { navigator.scrollTop(0); }; diff --git a/src/categories/create.js b/src/categories/create.js index 81097650e1..fa6bbf3a9a 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -39,6 +39,7 @@ module.exports = function (Categories) { class: (data.class ? data.class : 'col-md-3 col-xs-6'), imageClass: 'cover', isSection: 0, + subCategoriesPerPage: 10, }; if (data.backgroundImage) { diff --git a/src/categories/data.js b/src/categories/data.js index e75b825cb9..d7fe32f766 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -10,7 +10,7 @@ const utils = require('../utils'); const intFields = [ 'cid', 'parentCid', 'disabled', 'isSection', 'order', 'topic_count', 'post_count', 'numRecentReplies', - 'minTags', 'maxTags', 'postQueue', + 'minTags', 'maxTags', 'postQueue', 'subCategoriesPerPage', ]; module.exports = function (Categories) { diff --git a/src/controllers/categories.js b/src/controllers/categories.js index 98dae31fa5..65d3fcb4f5 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -1,10 +1,13 @@ 'use strict'; const nconf = require('nconf'); +const _ = require('lodash'); const categories = require('../categories'); const meta = require('../meta'); +const pagination = require('../pagination'); const helpers = require('./helpers'); +const privileges = require('../privileges'); const categoriesController = module.exports; @@ -17,15 +20,45 @@ categoriesController.list = async function (req, res) { content: 'website', }]; - const categoryData = await categories.getCategoriesByPrivilege('categories:cid', req.uid, 'find'); + const allRootCids = await categories.getAllCidsFromSet('cid:0:children'); + const rootCids = await privileges.categories.filterCids('find', allRootCids, req.uid); + const pageCount = Math.max(1, Math.ceil(rootCids.length / meta.config.categoriesPerPage)); + const page = Math.min(parseInt(req.query.page, 10) || 1, pageCount); + const start = Math.max(0, (page - 1) * meta.config.categoriesPerPage); + const stop = start + meta.config.categoriesPerPage - 1; + const pageCids = rootCids.slice(start, stop + 1); + + const allChildCids = _.flatten(await Promise.all(pageCids.map(cid => categories.getChildrenCids(cid)))); + const childCids = await privileges.categories.filterCids('find', allChildCids, req.uid); + const categoryData = await categories.getCategories(pageCids.concat(childCids), req.uid); const tree = categories.getTree(categoryData, 0); await categories.getRecentTopicReplies(categoryData, req.uid, req.query); const data = { title: meta.config.homePageTitle || '[[pages:home]]', categories: tree, + pagination: pagination.create(page, pageCount, req.query), }; + data.categories.forEach(function (category) { + if (category) { + if (Array.isArray(category.children)) { + category.children = category.children.slice(0, category.subCategoriesPerPage); + category.children.forEach(function (child) { + child.children = undefined; + }); + } + if (Array.isArray(category.posts) && category.posts.length && category.posts[0]) { + category.teaser = { + url: nconf.get('relative_path') + '/post/' + category.posts[0].pid, + timestampISO: category.posts[0].timestampISO, + pid: category.posts[0].pid, + topic: category.posts[0].topic, + }; + } + } + }); + if (req.originalUrl.startsWith(nconf.get('relative_path') + '/api/categories') || req.originalUrl.startsWith(nconf.get('relative_path') + '/categories')) { data.title = '[[pages:categories]]'; data.breadcrumbs = helpers.buildBreadcrumbs([{ text: data.title }]); @@ -35,16 +68,5 @@ categoriesController.list = async function (req, res) { }); } - data.categories.forEach(function (category) { - if (category && Array.isArray(category.posts) && category.posts.length && category.posts[0]) { - category.teaser = { - url: nconf.get('relative_path') + '/post/' + category.posts[0].pid, - timestampISO: category.posts[0].timestampISO, - pid: category.posts[0].pid, - topic: category.posts[0].topic, - }; - } - }); - res.render('categories', data); }; diff --git a/src/controllers/category.js b/src/controllers/category.js index 53984d04a6..09e9ce800b 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -100,6 +100,13 @@ categoryController.get = async function (req, res, next) { const allCategories = []; categories.flattenCategories(allCategories, categoryData.children); await categories.getRecentTopicReplies(allCategories, req.uid, req.query); + categoryData.subCategoriesLeft = Math.max(0, categoryData.children.length - categoryData.subCategoriesPerPage); + categoryData.hasMoreSubCategories = categoryData.children.length > categoryData.subCategoriesPerPage; + categoryData.nextSubCategoryStart = categoryData.subCategoriesPerPage; + categoryData.children = categoryData.children.slice(0, categoryData.subCategoriesPerPage); + categoryData.children.forEach(function (child) { + child.children = undefined; + }); } categoryData.title = translator.escape(categoryData.name); diff --git a/src/controllers/search.js b/src/controllers/search.js index afc4ff34ab..9386f3eeff 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -78,10 +78,8 @@ searchController.search = async function (req, res, next) { return res.json(searchData); } - if (['titles', 'titlesposts', 'posts'].includes(req.query.in)) { - searchData.categories = categoriesData; - searchData.categoriesCount = Math.max(10, Math.min(20, categoriesData.length)); - } + searchData.allCategories = categoriesData; + searchData.allCategoriesCount = Math.max(10, Math.min(20, categoriesData.length)); searchData.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[global:search]]' }]); searchData.expandSearch = !req.query.term; diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index b7aabb8174..d9fdb25502 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -153,4 +153,21 @@ SocketCategories.getCategory = async function (socket, cid) { // return await apiController.getCategoryData(cid, socket.uid); }; +SocketCategories.loadMoreSubCategories = async function (socket, data) { + if (!data || !data.cid || !(parseInt(data.start, 10) > 0)) { + throw new Error('[[error:invalid-data]]'); + } + const allowed = await privileges.categories.can('read', data.cid, socket.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + const category = await categories.getCategoryData(data.cid); + await categories.getChildrenTree(category, socket.uid); + const allCategories = []; + categories.flattenCategories(allCategories, category.children); + await categories.getRecentTopicReplies(allCategories, socket.uid); + const start = parseInt(data.start, 10); + return category.children.slice(start, start + category.subCategoriesPerPage); +}; + require('../promisify')(SocketCategories); diff --git a/src/upgrades/1.17.0/subcategories_per_page.js b/src/upgrades/1.17.0/subcategories_per_page.js new file mode 100644 index 0000000000..344df5d699 --- /dev/null +++ b/src/upgrades/1.17.0/subcategories_per_page.js @@ -0,0 +1,23 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Create subCategoriesPerPage property for categories', + timestamp: Date.UTC(2021, 0, 31), + method: async function () { + const progress = this.progress; + + await batch.processSortedSet('categories:cid', async function (cids) { + const keys = cids.map(cid => 'category:' + cid); + await db.setObject(keys, { + subCategoriesPerPage: 10, + }); + progress.incr(cids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 332b82adf9..869e23d42e 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -82,7 +82,15 @@
-
+
+
+ + +
+
+
-
+
-
[[admin/settings/pagination:topics]]
+
[[admin/settings/pagination:posts]]
[[admin/settings/pagination:posts-per-page]]

@@ -25,7 +25,7 @@
-
[[admin/settings/pagination:categories]]
+
[[admin/settings/pagination:topics]]
[[admin/settings/pagination:topics-per-page]]

@@ -34,4 +34,13 @@
+
+
[[admin/settings/pagination:categories]]
+
+ + [[admin/settings/pagination:categories-per-page]]

+ +
+
+ \ No newline at end of file