mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-30 02:25:55 +01:00
Categories refactor (#9257)
* 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 * feat: new category filter/search WIP * feat: remove categories from /tag * fix: dont load all categories when showing move modal * feat: allow adding custom categories to list * breaking: dont load entire category tree on post queue removed unused code add hooks to filter/selector add options to filter/selector * feat: make selector modal work again * feat: replace old search module * fix: topic move selector * feat: dont load all categories on create category modal * fix: fix more categorySelectors * feat: dont load entire category tree on group details page * feat: dont load all categories on home page and user settings page * feat: add pagination to /user/:userslug/categories * fix: update schemas * fix: more tests * fix: test * feat: flags page, dont return entire category tree * fix: flag test * feat: categories manage page dont load all categories allow changing root category clear caches properly * fix: spec * feat: admins&mods page dont load all categories * fix: spec * fix: dont load all children when opening dropdown * fix: on search results dont return all children * refactor: pass all options, rename options.cids to options.selectedCids * fix: #9266 * fix: index 0 * fix: spec * feat: #9265, add setObjectBulk * refactor: shoter updateOrder * feat: selectors on categories/category * fix: tests and search filter * fix: category update test * feat: pagination on acp categories page show order in set order modal * fix: allow drag&drop on pages > 1 in /admin/manage/categories * fix: teasers for deep nested categories fix sub category display on /category page * fix: spec * refactor: use eslint-disable-next-line * refactor: shorter
This commit is contained in:
committed by
GitHub
parent
2cfab3678e
commit
47299ea587
@@ -19,6 +19,7 @@
|
||||
"category-image": "Category Image",
|
||||
"parent-category": "Parent Category",
|
||||
"optional-parent-category": "(Optional) Parent Category",
|
||||
"top-level": "Top Level",
|
||||
"parent-category-none": "(None)",
|
||||
"copy-parent": "Copy Parent",
|
||||
"copy-settings": "Copy Settings From",
|
||||
@@ -31,6 +32,7 @@
|
||||
"edit": "Edit",
|
||||
"analytics": "Analytics",
|
||||
"view-category": "View category",
|
||||
"set-order": "Set order",
|
||||
|
||||
"select-category": "Select Category",
|
||||
"set-parent-category": "Set Parent Category",
|
||||
|
||||
@@ -213,5 +213,8 @@
|
||||
|
||||
"plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP",
|
||||
|
||||
"topic-event-unrecognized": "Topic event '%1' unrecognized"
|
||||
"topic-event-unrecognized": "Topic event '%1' unrecognized",
|
||||
|
||||
"cant-set-child-as-parent": "Can't set child as parent category",
|
||||
"cant-set-self-as-parent": "Can't set self as parent category"
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@
|
||||
"details.member_count": "Member Count",
|
||||
"details.creation_date": "Creation Date",
|
||||
"details.description": "Description",
|
||||
"details.member-post-cids": "Categories to display posts from",
|
||||
"details.member-post-cids-help": "<strong>Note</strong>: Selecting no categories will assume all categories are included. Use <code>ctrl</code> and <code>shift</code> to select multiple options.",
|
||||
"details.member-post-cids": "Category IDs to display posts from",
|
||||
"details.badge_preview": "Badge Preview",
|
||||
"details.change_icon": "Change Icon",
|
||||
"details.change_label_colour": "Change Label Colour",
|
||||
|
||||
@@ -149,7 +149,7 @@
|
||||
"homepage": "Homepage",
|
||||
"homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.",
|
||||
"custom_route": "Custom Homepage Route",
|
||||
"custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\", or \"popular\")",
|
||||
"custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")",
|
||||
|
||||
"sso.title": "Single Sign-on Services",
|
||||
"sso.associated": "Associated with",
|
||||
|
||||
@@ -46,6 +46,8 @@ GroupFullObject:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
memberPostCids:
|
||||
type: string
|
||||
memberPostCidsArray:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
@@ -64,32 +66,6 @@ GroupFullObject:
|
||||
type: string
|
||||
descriptionParsed:
|
||||
type: string
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
members:
|
||||
type: array
|
||||
items:
|
||||
@@ -169,6 +145,8 @@ GroupDataObject:
|
||||
cover:position:
|
||||
type: string
|
||||
memberPostCids:
|
||||
type: string
|
||||
memberPostCidsArray:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
|
||||
@@ -15,41 +15,90 @@ get:
|
||||
$ref: ../../../components/schemas/GroupObject.yaml#/GroupFullObject
|
||||
globalMods:
|
||||
$ref: ../../../components/schemas/GroupObject.yaml#/GroupFullObject
|
||||
categories:
|
||||
categoryMods:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier assigned upon category creation (this value cannot be changed)
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: number
|
||||
example: 0
|
||||
description: The category's name/title
|
||||
description:
|
||||
type: string
|
||||
description: A variable-length description of the category (usually displayed underneath the category name)
|
||||
descriptionParsed:
|
||||
type: string
|
||||
description: A variable-length description of the category (usually displayed underneath the category name). Unlike `description`, this value here will have been run through any parsers installed on the forum (e.g. Markdown)
|
||||
icon:
|
||||
type: string
|
||||
description: A FontAwesome icon string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The parent category's identifier
|
||||
color:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
example: fa-comments-o
|
||||
bgColor:
|
||||
type: string
|
||||
description: A six-character hexadecimal colour code
|
||||
imageClass:
|
||||
description: Theme-related, a six-character hexadecimal string representing the background colour of the category
|
||||
color:
|
||||
type: string
|
||||
depth:
|
||||
description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category
|
||||
slug:
|
||||
type: string
|
||||
description: An URL-safe variant of the category title. This value is automatically generated.
|
||||
readOnly: true
|
||||
parentCid:
|
||||
type: number
|
||||
description: The depth of the category relative to the forum root (`0` is root level)
|
||||
description: The category identifier for the category that is the immediate ancestor of the current category
|
||||
topic_count:
|
||||
type: number
|
||||
description: The number of topics in the category
|
||||
post_count:
|
||||
type: number
|
||||
description: The number of posts in the category
|
||||
disabled:
|
||||
type: number
|
||||
description: Whether or not this category is disabled.
|
||||
order:
|
||||
type: number
|
||||
description: A number representing the category's place in the hierarchy
|
||||
link:
|
||||
type: string
|
||||
description: If set, attempting to access the forum will go to this external link instead (theme-specific)
|
||||
numRecentReplies:
|
||||
type: number
|
||||
description: The number of posts to render in the API response (this is mostly used at the theme level)
|
||||
class:
|
||||
type: string
|
||||
description: Values that are appended to the `class` attribute of the category's parent/root element
|
||||
imageClass:
|
||||
type: string
|
||||
enum: [auto, cover, contain]
|
||||
description: The `background-position` of the category background image, if one is set
|
||||
isSection:
|
||||
type: number
|
||||
minTags:
|
||||
type: number
|
||||
description: Minimum tags per topic in this category
|
||||
maxTags:
|
||||
type: number
|
||||
description: Maximum tags per topic in this category
|
||||
postQueue:
|
||||
type: number
|
||||
totalPostCount:
|
||||
type: number
|
||||
description: The number of posts in the category
|
||||
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
|
||||
moderators:
|
||||
type: array
|
||||
items:
|
||||
$ref: ../../../components/schemas/UserObject.yaml#/UserObjectSlim
|
||||
selectedCategory:
|
||||
$ref: ../../../components/schemas/CategoryObject.yaml#/CategoryObject
|
||||
allPrivileges:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -11,7 +11,9 @@ get:
|
||||
allOf:
|
||||
- type : object
|
||||
properties:
|
||||
categories:
|
||||
categoriesPerPage:
|
||||
type: number
|
||||
categoriesTree:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
@@ -40,7 +42,12 @@ get:
|
||||
nullable: true
|
||||
imageClass:
|
||||
type: string
|
||||
order:
|
||||
type: number
|
||||
subCategoriesPerPage:
|
||||
type: number
|
||||
children:
|
||||
type: array
|
||||
description: Array of children categories
|
||||
- $ref: ../../../components/schemas/Pagination.yaml#/Pagination
|
||||
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -31,60 +31,8 @@ get:
|
||||
type: string
|
||||
parent:
|
||||
$ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
level:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
required:
|
||||
- cid
|
||||
- name
|
||||
- icon
|
||||
selectedCategory:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
$ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
|
||||
customClasses:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -67,6 +67,8 @@ get:
|
||||
ownerUid:
|
||||
type: number
|
||||
memberPostCids:
|
||||
type: string
|
||||
memberPostCidsArray:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
@@ -93,36 +95,6 @@ get:
|
||||
- textColor
|
||||
- createtimeISO
|
||||
- cover:thumb:url
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
level:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
required:
|
||||
- cid
|
||||
- name
|
||||
- icon
|
||||
yourid:
|
||||
type: number
|
||||
- $ref: ../../../components/schemas/Pagination.yaml#/Pagination
|
||||
|
||||
@@ -31,36 +31,6 @@ get:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
level:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
required:
|
||||
- cid
|
||||
- name
|
||||
- icon
|
||||
allowPrivateGroups:
|
||||
type: number
|
||||
maximumGroupNameLength:
|
||||
|
||||
@@ -121,29 +121,7 @@ get:
|
||||
- icon
|
||||
- selected
|
||||
selectedCategory:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
$ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
|
||||
@@ -23,6 +23,9 @@ get:
|
||||
title:
|
||||
description: The page title
|
||||
type: string
|
||||
selectCategoryLabel:
|
||||
type: string
|
||||
description: Label to use for the category selector
|
||||
categories:
|
||||
description: A collection of category data objects
|
||||
type: array
|
||||
@@ -112,10 +115,6 @@ get:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
topic:
|
||||
type: object
|
||||
properties:
|
||||
@@ -177,10 +176,6 @@ get:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
topic:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -58,6 +58,9 @@ get:
|
||||
type: boolean
|
||||
title:
|
||||
type: string
|
||||
selectCategoryLabel:
|
||||
type: string
|
||||
description: Label to use for the category selector
|
||||
privileges:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -46,12 +46,6 @@ get:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
categories:
|
||||
type: object
|
||||
properties: {}
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: All categories will be listed here, with the `cid` as the key, and the category name as the value
|
||||
hasFilter:
|
||||
type: boolean
|
||||
filters:
|
||||
@@ -65,6 +59,16 @@ get:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
selectedCategory:
|
||||
type: object
|
||||
properties:
|
||||
icon:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
nullable: true
|
||||
- $ref: ../components/schemas/Pagination.yaml#/Pagination
|
||||
- $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
|
||||
- $ref: ../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -61,6 +61,8 @@ get:
|
||||
cover:position:
|
||||
type: string
|
||||
memberPostCids:
|
||||
type: string
|
||||
memberPostCidsArray:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
|
||||
@@ -18,6 +18,9 @@ get:
|
||||
title:
|
||||
type: string
|
||||
description: The page title
|
||||
selectCategoryLabel:
|
||||
type: string
|
||||
description: Label to use for the category selector
|
||||
categories:
|
||||
description: A collection of category data objects
|
||||
type: array
|
||||
@@ -175,10 +178,6 @@ get:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
topic:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -32,32 +32,6 @@ get:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
allCategoriesUrl:
|
||||
type: string
|
||||
selectedCategory:
|
||||
|
||||
@@ -13,70 +13,6 @@ get:
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
allCategories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
bgColor:
|
||||
type: string
|
||||
cid:
|
||||
type: number
|
||||
color:
|
||||
type: string
|
||||
disabled:
|
||||
type: number
|
||||
disabledClass:
|
||||
type: boolean
|
||||
icon:
|
||||
type: string
|
||||
imageClass:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
link:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
slug:
|
||||
type: string
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
required:
|
||||
- bgColor
|
||||
- cid
|
||||
- color
|
||||
- icon
|
||||
- imageClass
|
||||
- level
|
||||
- name
|
||||
- parentCid
|
||||
allCategoriesUrl:
|
||||
type: string
|
||||
selectedCategory:
|
||||
|
||||
@@ -30,32 +30,6 @@ get:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
allCategoriesUrl:
|
||||
type: string
|
||||
selectedCategory:
|
||||
|
||||
@@ -227,32 +227,6 @@ get:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
rssFeedUrl:
|
||||
type: string
|
||||
feeds:disableRSS:
|
||||
|
||||
@@ -30,32 +30,6 @@ get:
|
||||
type: boolean
|
||||
showTopicTools:
|
||||
type: boolean
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
allCategoriesUrl:
|
||||
type: string
|
||||
selectedCategory:
|
||||
|
||||
@@ -200,32 +200,6 @@ get:
|
||||
type: string
|
||||
pageCount:
|
||||
type: number
|
||||
categories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
cid:
|
||||
type: number
|
||||
description: A category identifier
|
||||
name:
|
||||
type: string
|
||||
level:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
parentCid:
|
||||
type: number
|
||||
description: The category identifier for the category that is the immediate
|
||||
ancestor of the current category
|
||||
color:
|
||||
type: string
|
||||
bgColor:
|
||||
type: string
|
||||
selected:
|
||||
type: boolean
|
||||
imageClass:
|
||||
type: string
|
||||
allCategoriesUrl:
|
||||
type: string
|
||||
selectedCategory:
|
||||
|
||||
@@ -58,5 +58,6 @@ get:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
- $ref: ../../../components/schemas/Pagination.yaml#/Pagination
|
||||
- $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
|
||||
- $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
define('admin/manage/admins-mods', [
|
||||
'translator', 'benchpress', 'autocomplete', 'api', 'bootbox',
|
||||
], function (translator, Benchpress, autocomplete, api, bootbox) {
|
||||
'autocomplete', 'api', 'bootbox', 'categorySelector',
|
||||
], function (autocomplete, api, bootbox, categorySelector) {
|
||||
var AdminsMods = {};
|
||||
|
||||
AdminsMods.init = function () {
|
||||
@@ -77,6 +77,13 @@ define('admin/manage/admins-mods', [
|
||||
});
|
||||
|
||||
|
||||
categorySelector.init($('[component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
ajaxify.go('admin/manage/admins-mods' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : ''));
|
||||
},
|
||||
localCategories: [],
|
||||
});
|
||||
|
||||
autocomplete.user($('.moderator-search'), function (ev, ui) {
|
||||
var input = $(ev.target);
|
||||
var cid = $(ev.target).attr('data-cid');
|
||||
|
||||
@@ -6,13 +6,21 @@ define('admin/manage/categories', [
|
||||
'categorySelector',
|
||||
'api',
|
||||
'Sortable',
|
||||
], function (translator, Benchpress, categorySelector, api, Sortable) {
|
||||
'bootbox',
|
||||
], function (translator, Benchpress, categorySelector, api, Sortable, bootbox) {
|
||||
var Categories = {};
|
||||
var newCategoryId = -1;
|
||||
var sortables;
|
||||
|
||||
Categories.init = function () {
|
||||
Categories.render(ajaxify.data.categories);
|
||||
categorySelector.init($('.category [component="category-selector"]'), {
|
||||
parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0,
|
||||
onSelect: function (selectedCategory) {
|
||||
ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : ''));
|
||||
},
|
||||
localCategories: [],
|
||||
});
|
||||
Categories.render(ajaxify.data.categoriesTree);
|
||||
|
||||
$('button[data-action="create"]').on('click', Categories.throwCreateModal);
|
||||
|
||||
@@ -36,6 +44,34 @@ define('admin/manage/categories', [
|
||||
el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden');
|
||||
});
|
||||
|
||||
$('.categories').on('click', '.set-order', function () {
|
||||
var cid = $(this).attr('data-cid');
|
||||
var order = $(this).attr('data-order');
|
||||
var modal = bootbox.dialog({
|
||||
title: '[[admin/manage/categories:set-order]]',
|
||||
message: '<input class="form-control input-lg" value=' + order + ' />',
|
||||
show: true,
|
||||
buttons: {
|
||||
save: {
|
||||
label: '[[modules:bootbox.confirm]]',
|
||||
className: 'btn-primary',
|
||||
callback: function () {
|
||||
var val = modal.find('input').val();
|
||||
if (val && cid) {
|
||||
var modified = {};
|
||||
modified[cid] = { order: Math.max(1, parseInt(val, 10)) };
|
||||
api.put('/categories/' + cid, modified[cid]).then(function () {
|
||||
ajaxify.refresh();
|
||||
}).catch(err => app.alertError(err));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
$('#collapse-all').on('click', function () {
|
||||
toggleAll(false);
|
||||
});
|
||||
@@ -49,69 +85,10 @@ define('admin/manage/categories', [
|
||||
el.find('i').toggleClass('fa-minus', expand).toggleClass('fa-plus', !expand);
|
||||
el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand);
|
||||
}
|
||||
|
||||
$('#category-search').on('keyup', function () {
|
||||
searchCategory();
|
||||
});
|
||||
};
|
||||
|
||||
function searchCategory() {
|
||||
var container = $('#content .categories');
|
||||
function revealParents(cid) {
|
||||
var parentCid = container.find('li[data-cid="' + cid + '"]').attr('data-parent-cid');
|
||||
if (parentCid) {
|
||||
container.find('li[data-cid="' + parentCid + '"]').removeClass('hidden');
|
||||
revealParents(parentCid);
|
||||
}
|
||||
}
|
||||
|
||||
function revealChildren(cid) {
|
||||
var els = container.find('li[data-parent-cid="' + cid + '"]');
|
||||
els.each(function (index, el) {
|
||||
var $el = $(el);
|
||||
$el.removeClass('hidden');
|
||||
revealChildren($el.attr('data-cid'));
|
||||
});
|
||||
}
|
||||
|
||||
var categoryEls = container.find('li[data-cid]');
|
||||
var val = $('#category-search').val().toLowerCase();
|
||||
var noMatch = true;
|
||||
var cids = [];
|
||||
categoryEls.each(function () {
|
||||
var liEl = $(this);
|
||||
var isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1;
|
||||
if (noMatch && isMatch) {
|
||||
noMatch = false;
|
||||
}
|
||||
if (isMatch && val) {
|
||||
cids.push(liEl.attr('data-cid'));
|
||||
}
|
||||
liEl.toggleClass('hidden', !isMatch);
|
||||
});
|
||||
|
||||
cids.forEach(function (cid) {
|
||||
revealParents(cid);
|
||||
revealChildren(cid);
|
||||
});
|
||||
|
||||
$('[component="category/no-matches"]').toggleClass('hidden', !noMatch);
|
||||
}
|
||||
|
||||
Categories.throwCreateModal = function () {
|
||||
socket.emit('categories.getSelectCategories', {}, function (err, categories) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
categories.unshift({
|
||||
cid: 0,
|
||||
name: '[[admin/manage/categories:parent-category-none]]',
|
||||
icon: 'fa-none',
|
||||
});
|
||||
Benchpress.render('admin/partials/categories/create', {
|
||||
categories: categories,
|
||||
}).then(function (html) {
|
||||
Benchpress.render('admin/partials/categories/create', {}).then(function (html) {
|
||||
var modal = bootbox.dialog({
|
||||
title: '[[admin/manage/categories:alert.create]]',
|
||||
message: html,
|
||||
@@ -123,9 +100,17 @@ define('admin/manage/categories', [
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'));
|
||||
var cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'));
|
||||
var options = {
|
||||
localCategories: [
|
||||
{
|
||||
cid: 0,
|
||||
name: '[[admin/manage/categories:parent-category-none]]',
|
||||
icon: 'fa-none',
|
||||
},
|
||||
],
|
||||
};
|
||||
var parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options);
|
||||
var cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options);
|
||||
function submit() {
|
||||
var formData = modal.find('form').serializeObject();
|
||||
formData.description = '';
|
||||
@@ -153,7 +138,6 @@ define('admin/manage/categories', [
|
||||
|
||||
modal.find('form').on('submit', submit);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Categories.create = function (payload) {
|
||||
@@ -210,25 +194,21 @@ define('admin/manage/categories', [
|
||||
|
||||
// Update needed?
|
||||
if ((e.newIndex != null && parseInt(e.oldIndex, 10) !== parseInt(e.newIndex, 10)) || isCategoryUpdate) {
|
||||
var parentCategory = isCategoryUpdate ? sortables[newCategoryId] : sortables[e.from.dataset.cid];
|
||||
var cid = e.item.dataset.cid;
|
||||
var modified = {};
|
||||
var i = 0;
|
||||
var list = parentCategory.toArray();
|
||||
var len = list.length;
|
||||
|
||||
for (i; i < len; i += 1) {
|
||||
modified[list[i]] = {
|
||||
order: (i + 1),
|
||||
// on page 1 baseIndex is 0, on page n baseIndex is (n - 1) * ajaxify.data.categoriesPerPage
|
||||
// this makes sure order is correct when drag & drop is used on pages > 1
|
||||
var baseIndex = (ajaxify.data.pagination.currentPage - 1) * ajaxify.data.categoriesPerPage;
|
||||
modified[cid] = {
|
||||
order: baseIndex + e.newIndex + 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (isCategoryUpdate) {
|
||||
modified[e.item.dataset.cid].parentCid = newCategoryId;
|
||||
modified[cid].parentCid = newCategoryId;
|
||||
}
|
||||
|
||||
newCategoryId = -1;
|
||||
|
||||
Object.keys(modified).map(cid => api.put('/categories/' + cid, modified[cid]));
|
||||
api.put('/categories/' + cid, modified[cid]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ define('admin/manage/category', [
|
||||
$this.val($this.attr('data-value'));
|
||||
});
|
||||
|
||||
categorySelector.init($('[component="category-selector"]'), function (selectedCategory) {
|
||||
categorySelector.init($('[component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
ajaxify.go('admin/manage/categories/' + selectedCategory.cid);
|
||||
},
|
||||
showLinks: true,
|
||||
});
|
||||
|
||||
handleTags();
|
||||
@@ -114,14 +117,7 @@ define('admin/manage/category', [
|
||||
});
|
||||
|
||||
$('.copy-settings').on('click', function () {
|
||||
socket.emit('categories.getSelectCategories', {}, function (err, allCategories) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
Benchpress.render('admin/partials/categories/copy-settings', {
|
||||
categories: allCategories,
|
||||
}).then(function (html) {
|
||||
Benchpress.render('admin/partials/categories/copy-settings', {}).then(function (html) {
|
||||
var selectedCid;
|
||||
var modal = bootbox.dialog({
|
||||
title: '[[modules:composer.select_category]]',
|
||||
@@ -154,16 +150,18 @@ define('admin/manage/category', [
|
||||
},
|
||||
});
|
||||
modal.find('.modal-footer button').prop('disabled', true);
|
||||
categorySelector.init(modal.find('[component="category-selector"]'), function (selectedCategory) {
|
||||
categorySelector.init(modal.find('[component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
selectedCid = selectedCategory && selectedCategory.cid;
|
||||
if (selectedCid) {
|
||||
modal.find('.modal-footer button').prop('disabled', false);
|
||||
}
|
||||
},
|
||||
showLinks: true,
|
||||
});
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
$('.upload-button').on('click', function () {
|
||||
var inputEl = $(this);
|
||||
@@ -261,34 +259,27 @@ define('admin/manage/category', [
|
||||
}
|
||||
|
||||
Category.launchParentSelector = function () {
|
||||
socket.emit('categories.getSelectCategories', {}, function (err, allCategories) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
categorySelector.modal({
|
||||
onSubmit: function (selectedCategory) {
|
||||
var parentCid = selectedCategory.cid;
|
||||
if (!parentCid) {
|
||||
return;
|
||||
}
|
||||
var parents = [parseInt(ajaxify.data.category.cid, 10)];
|
||||
var categories = allCategories.filter(function (category) {
|
||||
var isChild = parents.includes(parseInt(category.parentCid, 10));
|
||||
if (isChild) {
|
||||
parents.push(parseInt(category.cid, 10));
|
||||
}
|
||||
return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10) && !isChild;
|
||||
});
|
||||
|
||||
categorySelector.modal(categories, function (parentCid) {
|
||||
api.put('/categories/' + ajaxify.data.category.cid, {
|
||||
parentCid: parentCid,
|
||||
}).then(() => {
|
||||
var parent = allCategories.filter(function (category) {
|
||||
return category && parseInt(category.cid, 10) === parseInt(parentCid, 10);
|
||||
api.get(`/category/${parentCid}`).then(function (parent) {
|
||||
if (parent && parent.icon && parent.name) {
|
||||
var buttonHtml = '<i class="fa ' + parent.icon + '"></i> ' + parent.name;
|
||||
$('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide');
|
||||
}
|
||||
});
|
||||
parent = parent[0];
|
||||
|
||||
$('button[data-action="removeParent"]').parent().removeClass('hide');
|
||||
$('button[data-action="setParent"]').addClass('hide');
|
||||
var buttonHtml = '<i class="fa ' + parent.icon + '"></i> ' + parent.name;
|
||||
$('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide');
|
||||
}).catch(app.alertError);
|
||||
});
|
||||
},
|
||||
showLinks: true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -57,8 +57,21 @@ define('admin/manage/group', [
|
||||
});
|
||||
});
|
||||
|
||||
categorySelector.init($('[component="category-selector"]'), function (selectedCategory) {
|
||||
categorySelector.init($('.edit-privileges-selector [component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
navigateToCategory(selectedCategory.cid);
|
||||
},
|
||||
showLinks: true,
|
||||
});
|
||||
|
||||
var cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
var cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10));
|
||||
cids.push(selectedCategory.cid);
|
||||
cids = cids.filter((cid, index, array) => array.indexOf(cid) === index);
|
||||
$('#memberPostCids').val(cids.join(','));
|
||||
cidSelector.selectCategory(0);
|
||||
},
|
||||
});
|
||||
|
||||
groupSearch.init($('[component="group-selector"]'));
|
||||
|
||||
@@ -75,8 +75,11 @@ define('admin/manage/groups', [
|
||||
function enableCategorySelectors() {
|
||||
$('.groups-list [component="category-selector"]').each(function () {
|
||||
var nameEncoded = $(this).parents('[data-name-encoded]').attr('data-name-encoded');
|
||||
categorySelector.init($(this), function (selectedCategory) {
|
||||
categorySelector.init($(this), {
|
||||
onSelect: function (selectedCategory) {
|
||||
ajaxify.go('admin/manage/privileges/' + selectedCategory.cid + '?group=' + nameEncoded);
|
||||
},
|
||||
showLinks: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,11 +17,16 @@ define('admin/manage/privileges', [
|
||||
|
||||
checkboxRowSelector.init('.privilege-table-container');
|
||||
|
||||
categorySelector.init($('[component="category-selector"]'), function (category) {
|
||||
categorySelector.init($('[component="category-selector"]'), {
|
||||
onSelect: function (category) {
|
||||
cid = parseInt(category.cid, 10);
|
||||
cid = isNaN(cid) ? 'admin' : cid;
|
||||
Privileges.refreshPrivilegeTable();
|
||||
ajaxify.updateHistory('admin/manage/privileges/' + (cid || ''));
|
||||
},
|
||||
localCategories: ajaxify.data.categories,
|
||||
privilege: 'find',
|
||||
showLinks: true,
|
||||
});
|
||||
|
||||
Privileges.setupPrivilegeTable();
|
||||
@@ -262,13 +267,21 @@ define('admin/manage/privileges', [
|
||||
};
|
||||
|
||||
Privileges.copyPrivilegesFromCategory = function (cid, group) {
|
||||
categorySelector.modal(ajaxify.data.categories.slice(1), function (fromCid) {
|
||||
socket.emit('admin.categories.copyPrivilegesFrom', { toCid: cid, fromCid: fromCid, group: group }, function (err) {
|
||||
categorySelector.modal({
|
||||
localCategories: [],
|
||||
showLinks: true,
|
||||
onSubmit: function (selectedCategory) {
|
||||
socket.emit('admin.categories.copyPrivilegesFrom', {
|
||||
toCid: cid,
|
||||
fromCid: selectedCategory.cid,
|
||||
group: group,
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
ajaxify.refresh();
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('forum/categories', ['components'], function (components) {
|
||||
define('forum/categories', ['components', 'categorySelector'], function (components, categorySelector) {
|
||||
var categories = {};
|
||||
|
||||
$(window).on('action:ajaxify.start', function (ev, data) {
|
||||
@@ -15,6 +15,12 @@ define('forum/categories', ['components'], function (components) {
|
||||
|
||||
socket.removeListener('event:new_post', categories.onNewPost);
|
||||
socket.on('event:new_post', categories.onNewPost);
|
||||
categorySelector.init($('[component="category-selector"]'), {
|
||||
privilege: 'find',
|
||||
onSelect: function (category) {
|
||||
ajaxify.go('/category/' + category.cid);
|
||||
},
|
||||
});
|
||||
|
||||
$('.category-header').tooltip({
|
||||
placement: 'bottom',
|
||||
|
||||
@@ -6,7 +6,8 @@ define('forum/category', [
|
||||
'navigator',
|
||||
'topicList',
|
||||
'sort',
|
||||
], function (infinitescroll, share, navigator, topicList, sort) {
|
||||
'categorySelector',
|
||||
], function (infinitescroll, share, navigator, topicList, sort, categorySelector) {
|
||||
var Category = {};
|
||||
|
||||
$(window).on('action:ajaxify.start', function (ev, data) {
|
||||
@@ -38,6 +39,14 @@ define('forum/category', [
|
||||
|
||||
handleLoadMoreSubcategories();
|
||||
|
||||
categorySelector.init($('[component="category-selector"]'), {
|
||||
privilege: 'find',
|
||||
parentCid: ajaxify.data.cid,
|
||||
onSelect: function (category) {
|
||||
ajaxify.go('/category/' + category.cid);
|
||||
},
|
||||
});
|
||||
|
||||
$(window).trigger('action:topics.loaded', { topics: ajaxify.data.topics });
|
||||
$(window).trigger('action:category.loaded', { cid: ajaxify.data.cid });
|
||||
};
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
define('forum/flags/list', ['components', 'Chart'], function (components, Chart) {
|
||||
define('forum/flags/list', ['components', 'Chart', 'categoryFilter'], function (components, Chart, categoryFilter) {
|
||||
var Flags = {};
|
||||
|
||||
var selectedCids;
|
||||
|
||||
Flags.init = function () {
|
||||
Flags.enableFilterForm();
|
||||
Flags.enableCheckboxes();
|
||||
Flags.handleBulkActions();
|
||||
|
||||
selectedCids = [];
|
||||
if (ajaxify.data.filters.hasOwnProperty('cid')) {
|
||||
selectedCids = Array.isArray(ajaxify.data.filters.cid) ?
|
||||
ajaxify.data.filters.cid : [ajaxify.data.filters.cid];
|
||||
}
|
||||
|
||||
categoryFilter.init($('[component="category/dropdown"]'), {
|
||||
privilege: 'moderate',
|
||||
selectedCids: selectedCids,
|
||||
onHidden: function (data) {
|
||||
selectedCids = data.selectedCids;
|
||||
},
|
||||
});
|
||||
|
||||
components.get('flags/list')
|
||||
.on('click', '[data-flag-id]', function (e) {
|
||||
if (['BUTTON', 'A'].includes(e.target.nodeName)) {
|
||||
@@ -39,6 +55,11 @@ define('forum/flags/list', ['components', 'Chart'], function (components, Chart)
|
||||
|
||||
document.getElementById('apply-filters').addEventListener('click', function () {
|
||||
var payload = filtersEl.serializeArray();
|
||||
// cid is special comes from categoryFilter module
|
||||
selectedCids.forEach(function (cid) {
|
||||
payload.push({ name: 'cid', value: cid });
|
||||
});
|
||||
|
||||
ajaxify.go('flags?' + (payload.length ? $.param(payload) : 'reset=1'));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -9,7 +9,8 @@ define('forum/groups/details', [
|
||||
'translator',
|
||||
'api',
|
||||
'slugify',
|
||||
], function (memberList, iconSelect, components, coverPhoto, pictureCropper, translator, api, slugify) {
|
||||
'categorySelector',
|
||||
], function (memberList, iconSelect, components, coverPhoto, pictureCropper, translator, api, slugify, categorySelector) {
|
||||
var Details = {};
|
||||
var groupName;
|
||||
|
||||
@@ -165,6 +166,16 @@ define('forum/groups/details', [
|
||||
previewEl.addClass('hide');
|
||||
}
|
||||
});
|
||||
|
||||
var cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), {
|
||||
onSelect: function (selectedCategory) {
|
||||
var cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10));
|
||||
cids.push(selectedCategory.cid);
|
||||
cids = cids.filter((cid, index, array) => array.indexOf(cid) === index);
|
||||
$('#memberPostCids').val(cids.join(','));
|
||||
cidSelector.selectCategory(0);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Details.update = function () {
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
|
||||
define('forum/post-queue', [
|
||||
'categoryFilter', 'categorySelector',
|
||||
], function (categoryFilter, categorySelector) {
|
||||
'categoryFilter', 'categorySelector', 'api',
|
||||
], function (categoryFilter, categorySelector, api) {
|
||||
var PostQueue = {};
|
||||
|
||||
PostQueue.init = function () {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
|
||||
categoryFilter.init($('[component="category/dropdown"]'));
|
||||
categoryFilter.init($('[component="category/dropdown"]'), {
|
||||
privilege: 'moderate',
|
||||
});
|
||||
|
||||
$('.posts-list').on('click', '[data-action]', function () {
|
||||
var parent = $(this).parents('[data-id]');
|
||||
@@ -42,17 +44,16 @@ define('forum/post-queue', [
|
||||
$('.posts-list').on('click', '.topic-category[data-editable]', function () {
|
||||
var $this = $(this);
|
||||
var id = $this.parents('[data-id]').attr('data-id');
|
||||
categorySelector.modal(ajaxify.data.allCategories, function (cid) {
|
||||
var category = ajaxify.data.allCategories.find(function (c) {
|
||||
return parseInt(c.cid, 10) === parseInt(cid, 10);
|
||||
});
|
||||
categorySelector.modal({
|
||||
onSubmit: function (selectedCategory) {
|
||||
Promise.all([
|
||||
api.get(`/categories/${selectedCategory.cid}`, {}),
|
||||
socket.emit('posts.editQueuedContent', {
|
||||
id: id,
|
||||
cid: cid,
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
cid: selectedCategory.cid,
|
||||
}),
|
||||
]).then(function (result) {
|
||||
var category = result[0];
|
||||
app.parseAndTranslate('post-queue', 'posts', {
|
||||
posts: [{
|
||||
category: category,
|
||||
@@ -65,7 +66,10 @@ define('forum/post-queue', [
|
||||
$this.replaceWith(html.find('.topic-category'));
|
||||
}
|
||||
});
|
||||
}).catch(function (err) {
|
||||
app.alertError(err);
|
||||
});
|
||||
},
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -12,18 +12,12 @@ define('forum/topic/move', ['categorySelector', 'alerts'], function (categorySel
|
||||
Move.onComplete = onComplete;
|
||||
Move.moveAll = !tids;
|
||||
|
||||
socket.emit('categories.getMoveCategories', onCategoriesLoaded);
|
||||
showModal();
|
||||
};
|
||||
|
||||
function onCategoriesLoaded(err, categories) {
|
||||
if (err) {
|
||||
return app.alertError(err.message);
|
||||
}
|
||||
|
||||
app.parseAndTranslate('partials/move_thread_modal', {
|
||||
categories: categories,
|
||||
}, function (html) {
|
||||
modal = $(html);
|
||||
function showModal() {
|
||||
app.parseAndTranslate('partials/move_thread_modal', {}, function (html) {
|
||||
modal = html;
|
||||
modal.on('hidden.bs.modal', function () {
|
||||
modal.remove();
|
||||
});
|
||||
@@ -34,7 +28,10 @@ define('forum/topic/move', ['categorySelector', 'alerts'], function (categorySel
|
||||
modal.find('.modal-header h3').translateText('[[topic:move_topics]]');
|
||||
}
|
||||
|
||||
categorySelector.init(modal.find('[component="category-selector"]'), onCategorySelected);
|
||||
categorySelector.init(modal.find('[component="category-selector"]'), {
|
||||
onSelect: onCategorySelected,
|
||||
privilege: 'moderate',
|
||||
});
|
||||
|
||||
modal.find('#move_thread_commit').on('click', onCommitClicked);
|
||||
|
||||
|
||||
@@ -3,64 +3,75 @@
|
||||
define('categoryFilter', ['categorySearch'], function (categorySearch) {
|
||||
var categoryFilter = {};
|
||||
|
||||
categoryFilter.init = function (el) {
|
||||
categorySearch.init(el);
|
||||
var listEl = el.find('[component="category/list"]');
|
||||
categoryFilter.init = function (el, options) {
|
||||
if (!el || !el.length) {
|
||||
return;
|
||||
}
|
||||
options = options || {};
|
||||
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
|
||||
options.template = 'partials/category-filter';
|
||||
$(window).trigger('action:category.filter.options', { el: el, options: options });
|
||||
|
||||
categorySearch.init(el, options);
|
||||
|
||||
var selectedCids = [];
|
||||
var initialCids = [];
|
||||
if (Array.isArray(options.selectedCids)) {
|
||||
selectedCids = options.selectedCids.map(cid => parseInt(cid, 10));
|
||||
} else if (Array.isArray(ajaxify.data.selectedCids)) {
|
||||
selectedCids = ajaxify.data.selectedCids.map(cid => parseInt(cid, 10));
|
||||
}
|
||||
initialCids = selectedCids.slice();
|
||||
|
||||
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]) {
|
||||
var changed = initialCids.length !== selectedCids.length;
|
||||
initialCids.forEach(function (cid, index) {
|
||||
if (cid !== selectedCids[index]) {
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (options.onHidden) {
|
||||
options.onHidden({ changed: changed, selectedCids: selectedCids.slice() });
|
||||
return;
|
||||
}
|
||||
if (changed) {
|
||||
var url = window.location.pathname;
|
||||
var currentParams = utils.params();
|
||||
if (cids.length) {
|
||||
currentParams.cid = cids;
|
||||
if (selectedCids.length) {
|
||||
currentParams.cid = selectedCids;
|
||||
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);
|
||||
});
|
||||
}
|
||||
el.on('click', '[component="category/list"] [data-cid]', function () {
|
||||
var listEl = el.find('[component="category/list"]');
|
||||
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'));
|
||||
var cid = parseInt(categoryEl.attr('data-cid'), 10);
|
||||
var icon = categoryEl.find('[component="category/select/icon"]');
|
||||
|
||||
if (selectedCids.includes(cid)) {
|
||||
selectedCids.splice(selectedCids.indexOf(cid), 1);
|
||||
} else {
|
||||
selectedCids.push(cid);
|
||||
}
|
||||
selectedCids.sort(function (a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
icon.toggleClass('invisible');
|
||||
listEl.find('li[data-all="all"] i').toggleClass('invisible', !!selectedCids.length);
|
||||
if (options.onSelect) {
|
||||
options.onSelect({ cid: cid, selectedCids: selectedCids.slice() });
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -3,79 +3,52 @@
|
||||
define('categorySearch', function () {
|
||||
var categorySearch = {};
|
||||
|
||||
categorySearch.init = function (el) {
|
||||
if (utils.isTouchDevice()) {
|
||||
return;
|
||||
categorySearch.init = function (el, options) {
|
||||
var categoriesList = null;
|
||||
options = options || {};
|
||||
options.privilege = options.privilege || 'topics:read';
|
||||
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
|
||||
|
||||
var localCategories = [];
|
||||
if (Array.isArray(options.localCategories)) {
|
||||
localCategories = options.localCategories.map(c => ({ ...c }));
|
||||
}
|
||||
options.selectedCids = options.selectedCids || ajaxify.data.selectedCids || [];
|
||||
|
||||
var searchEl = el.find('[component="category-selector-search"]');
|
||||
if (!searchEl.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var toggleVisibility = searchEl.parent('[component="category/dropdown"]').length > 0 ||
|
||||
searchEl.parent('[component="category-selector"]').length > 0;
|
||||
|
||||
var listEl = el.find('[component="category/list"]');
|
||||
var clonedList = listEl.clone();
|
||||
var categoryEls = clonedList.find('[data-cid]');
|
||||
|
||||
el.on('show.bs.dropdown', function () {
|
||||
var cidToParentCid = {};
|
||||
|
||||
function revealParents(cid) {
|
||||
var parentCid = cidToParentCid[cid];
|
||||
if (parentCid) {
|
||||
clonedList.find('[data-cid="' + parentCid + '"]').removeClass('hidden');
|
||||
revealParents(parentCid);
|
||||
}
|
||||
}
|
||||
|
||||
function revealChildren(cid) {
|
||||
var els = clonedList.find('[data-parent-cid="' + cid + '"]');
|
||||
els.each(function (index, el) {
|
||||
var $el = $(el);
|
||||
$el.removeClass('hidden');
|
||||
revealChildren($el.attr('data-cid'));
|
||||
});
|
||||
}
|
||||
|
||||
function updateList() {
|
||||
var val = searchEl.find('input').val().toLowerCase();
|
||||
var noMatch = true;
|
||||
var cids = [];
|
||||
categoryEls.each(function () {
|
||||
var liEl = $(this);
|
||||
var isMatch = cids.length < 100 && (!val || (val.length > 1 && liEl.attr('data-name').toLowerCase().indexOf(val) !== -1));
|
||||
if (noMatch && isMatch) {
|
||||
noMatch = false;
|
||||
}
|
||||
if (isMatch && val) {
|
||||
var cid = liEl.attr('data-cid');
|
||||
cids.push(cid);
|
||||
cidToParentCid[cid] = parseInt(liEl.attr('data-parent-cid'), 10);
|
||||
}
|
||||
liEl.toggleClass('hidden', !isMatch).find('[component="category-markup"]').css({ 'font-weight': val && isMatch ? 'bold' : 'normal' });
|
||||
});
|
||||
|
||||
cids.forEach(function (cid) {
|
||||
revealParents(cid);
|
||||
revealChildren(cid);
|
||||
});
|
||||
|
||||
listEl.html(clonedList.html());
|
||||
el.find('[component="category/list"] [component="category/no-matches"]').toggleClass('hidden', !noMatch);
|
||||
}
|
||||
if (toggleVisibility) {
|
||||
el.find('.dropdown-toggle').addClass('hidden');
|
||||
searchEl.removeClass('hidden');
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
var val = searchEl.find('input').val();
|
||||
if (val.length > 1 || (!val && !categoriesList)) {
|
||||
loadList(val, function (categories) {
|
||||
categoriesList = categoriesList || categories;
|
||||
renderList(categories);
|
||||
});
|
||||
} else if (!val && categoriesList) {
|
||||
renderList(categoriesList);
|
||||
}
|
||||
}
|
||||
|
||||
searchEl.on('click', function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
searchEl.find('input').val('').on('keyup', utils.debounce(updateList, 200));
|
||||
updateList();
|
||||
searchEl.find('input').val('').on('keyup', utils.debounce(doSearch, 300));
|
||||
doSearch();
|
||||
});
|
||||
|
||||
el.on('shown.bs.dropdown', function () {
|
||||
searchEl.find('input').focus();
|
||||
});
|
||||
@@ -89,6 +62,35 @@ define('categorySearch', function () {
|
||||
searchEl.off('click');
|
||||
searchEl.find('input').off('keyup');
|
||||
});
|
||||
|
||||
function loadList(query, callback) {
|
||||
socket.emit('categories.categorySearch', {
|
||||
query: query,
|
||||
parentCid: options.parentCid || 0,
|
||||
selectedCids: options.selectedCids,
|
||||
privilege: options.privilege,
|
||||
states: options.states,
|
||||
showLinks: options.showLinks,
|
||||
}, function (err, categories) {
|
||||
if (err) {
|
||||
return app.alertError(err);
|
||||
}
|
||||
callback(localCategories.concat(categories));
|
||||
});
|
||||
}
|
||||
|
||||
function renderList(categories) {
|
||||
app.parseAndTranslate(options.template, {
|
||||
categoryItems: categories.slice(0, 200),
|
||||
selectedCategory: ajaxify.data.selectedCategory,
|
||||
allCategoriesUrl: ajaxify.data.allCategoriesUrl,
|
||||
}, function (html) {
|
||||
el.find('[component="category/list"]')
|
||||
.replaceWith(html.find('[component="category/list"]'));
|
||||
el.find('[component="category/list"] [component="category/no-matches"]')
|
||||
.toggleClass('hidden', !!categories.length);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return categorySearch;
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
define('categorySelector', ['benchpress', 'translator', 'categorySearch'], function (Benchpress, translator, categorySearch) {
|
||||
define('categorySelector', ['categorySearch'], function (categorySearch) {
|
||||
var categorySelector = {};
|
||||
|
||||
categorySelector.init = function (el, callback) {
|
||||
callback = callback || function () {};
|
||||
categorySelector.init = function (el, options) {
|
||||
if (!el || !el.length) {
|
||||
return;
|
||||
}
|
||||
options = options || {};
|
||||
var onSelect = options.onSelect || function () {};
|
||||
|
||||
options.states = options.states || ['watching', 'notwatching', 'ignoring'];
|
||||
options.template = 'partials/category-selector';
|
||||
$(window).trigger('action:category.selector.options', { el: el, options: options });
|
||||
|
||||
categorySearch.init(el, options);
|
||||
|
||||
var selector = {
|
||||
el: el,
|
||||
selectedCategory: null,
|
||||
};
|
||||
|
||||
el.on('click', '[data-cid]', function () {
|
||||
var categoryEl = $(this);
|
||||
if (categoryEl.hasClass('disabled')) {
|
||||
return false;
|
||||
}
|
||||
selector.selectCategory(categoryEl.attr('data-cid'));
|
||||
callback(selector.selectedCategory);
|
||||
onSelect(selector.selectedCategory);
|
||||
});
|
||||
|
||||
categorySearch.init(el);
|
||||
|
||||
selector.selectCategory = function (cid) {
|
||||
var categoryEl = selector.el.find('[data-cid="' + cid + '"]');
|
||||
selector.selectedCategory = {
|
||||
@@ -43,14 +51,11 @@ define('categorySelector', ['benchpress', 'translator', 'categorySearch'], funct
|
||||
return selector;
|
||||
};
|
||||
|
||||
categorySelector.modal = function (categories, callback) {
|
||||
if (typeof categories === 'function') {
|
||||
callback = categories;
|
||||
categories = ajaxify.data.allCategories;
|
||||
}
|
||||
app.parseAndTranslate('admin/partials/categories/select-category', {
|
||||
categories: categories,
|
||||
}, function (html) {
|
||||
categorySelector.modal = function (options) {
|
||||
options = options || {};
|
||||
options.onSelect = options.onSelect || function () {};
|
||||
options.onSubmit = options.onSubmit || function () {};
|
||||
app.parseAndTranslate('admin/partials/categories/select-category', {}, function (html) {
|
||||
var modal = bootbox.dialog({
|
||||
title: '[[modules:composer.select_category]]',
|
||||
message: html,
|
||||
@@ -62,16 +67,21 @@ define('categorySelector', ['benchpress', 'translator', 'categorySearch'], funct
|
||||
},
|
||||
},
|
||||
});
|
||||
var selector = categorySelector.init(modal.find('[component="category-selector"]'));
|
||||
|
||||
var selector = categorySelector.init(modal.find('[component="category-selector"]'), options);
|
||||
function submit(ev) {
|
||||
ev.preventDefault();
|
||||
if (selector.selectedCategory) {
|
||||
callback(selector.selectedCategory.cid);
|
||||
options.onSubmit(selector.selectedCategory);
|
||||
modal.modal('hide');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.openOnLoad) {
|
||||
modal.on('shown.bs.modal', function () {
|
||||
modal.find('.dropdown-toggle').dropdown('toggle');
|
||||
});
|
||||
}
|
||||
modal.find('form').on('submit', submit);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -37,8 +37,16 @@ define('topicList', [
|
||||
categoryTools.init();
|
||||
|
||||
TopicList.watchForNewPosts();
|
||||
var states = ['watching'];
|
||||
if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') {
|
||||
states.push('notwatching', 'ignoring');
|
||||
} else if (template !== 'unread') {
|
||||
states.push('notwatching');
|
||||
}
|
||||
|
||||
categoryFilter.init($('[component="category/dropdown"]'));
|
||||
categoryFilter.init($('[component="category/dropdown"]'), {
|
||||
states: states,
|
||||
});
|
||||
|
||||
if (!config.usePagination) {
|
||||
infinitescroll.init(TopicList.loadMoreTopics);
|
||||
@@ -86,18 +94,11 @@ define('topicList', [
|
||||
socket.removeListener('event:new_post', onNewPost);
|
||||
};
|
||||
|
||||
function isCategoryVisible(cid) {
|
||||
return ajaxify.data.categories && ajaxify.data.categories.length && ajaxify.data.categories.some(function (c) {
|
||||
return parseInt(c.cid, 10) === parseInt(cid, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function onNewTopic(data) {
|
||||
if (
|
||||
(ajaxify.data.selectedCids && ajaxify.data.selectedCids.length && ajaxify.data.selectedCids.indexOf(parseInt(data.cid, 10)) === -1) ||
|
||||
(ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') ||
|
||||
(ajaxify.data.template.category && parseInt(ajaxify.data.cid, 10) !== parseInt(data.cid, 10)) ||
|
||||
(!isCategoryVisible(data.cid))
|
||||
(ajaxify.data.template.category && parseInt(ajaxify.data.cid, 10) !== parseInt(data.cid, 10))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -116,8 +117,7 @@ define('topicList', [
|
||||
(ajaxify.data.selectedCids && ajaxify.data.selectedCids.length && ajaxify.data.selectedCids.indexOf(parseInt(post.topic.cid, 10)) === -1) ||
|
||||
(ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'new') ||
|
||||
(ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched' && !post.topic.isFollowing) ||
|
||||
(ajaxify.data.template.category && parseInt(ajaxify.data.cid, 10) !== parseInt(post.topic.cid, 10)) ||
|
||||
(!isCategoryVisible(post.topic.cid))
|
||||
(ajaxify.data.template.category && parseInt(ajaxify.data.cid, 10) !== parseInt(post.topic.cid, 10))
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,6 @@ const cacheCreate = require('./cacheCreate');
|
||||
|
||||
module.exports = cacheCreate({
|
||||
name: 'local',
|
||||
max: 4000,
|
||||
max: 40000,
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
@@ -73,7 +73,9 @@ module.exports = function (Categories) {
|
||||
'categories:cid',
|
||||
'cid:0:children',
|
||||
'cid:' + parentCid + ':children',
|
||||
'cid:' + parentCid + ':children:all',
|
||||
'cid:' + cid + ':children',
|
||||
'cid:' + cid + ':children:all',
|
||||
'cid:' + cid + ':tag:whitelist',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ Categories.getAllCidsFromSet = async function (key) {
|
||||
}
|
||||
|
||||
cids = await db.getSortedSetRange(key, 0, -1);
|
||||
cids = cids.map(cid => parseInt(cid, 10));
|
||||
cache.set(key, cids);
|
||||
return cids.slice();
|
||||
};
|
||||
@@ -229,6 +230,19 @@ async function getChildrenTree(category, uid) {
|
||||
|
||||
Categories.getChildrenTree = getChildrenTree;
|
||||
|
||||
Categories.getParentCids = async function (currentCid) {
|
||||
let cid = currentCid;
|
||||
const parents = [];
|
||||
while (parseInt(cid, 10)) {
|
||||
// eslint-disable-next-line
|
||||
cid = await Categories.getCategoryField(cid, 'parentCid');
|
||||
if (cid) {
|
||||
parents.unshift(cid);
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
Categories.getChildrenCids = async function (rootCid) {
|
||||
let allCids = [];
|
||||
async function recursive(keys) {
|
||||
@@ -243,7 +257,7 @@ Categories.getChildrenCids = async function (rootCid) {
|
||||
await recursive(keys);
|
||||
}
|
||||
const key = 'cid:' + rootCid + ':children';
|
||||
const cacheKey = 'cache:' + key;
|
||||
const cacheKey = key + ':all';
|
||||
const childrenCids = cache.get(cacheKey);
|
||||
if (childrenCids) {
|
||||
return childrenCids.slice();
|
||||
@@ -311,10 +325,17 @@ Categories.getTree = function (categories, parentCid) {
|
||||
}
|
||||
});
|
||||
function sortTree(tree) {
|
||||
tree.sort((a, b) => a.order - b.order);
|
||||
if (tree.children) {
|
||||
sortTree(tree.children);
|
||||
tree.sort((a, b) => {
|
||||
if (a.order !== b.order) {
|
||||
return a.order - b.order;
|
||||
}
|
||||
return a.cid - b.cid;
|
||||
});
|
||||
tree.forEach((category) => {
|
||||
if (category && Array.isArray(category.children)) {
|
||||
sortTree(category.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
sortTree(tree);
|
||||
|
||||
@@ -338,7 +359,7 @@ async function getSelectData(cids, fields) {
|
||||
return Categories.buildForSelectCategories(tree, fields);
|
||||
}
|
||||
|
||||
Categories.buildForSelectCategories = function (categories, fields) {
|
||||
Categories.buildForSelectCategories = function (categories, fields, parentCid) {
|
||||
function recursive(category, categoriesData, level, depth) {
|
||||
const bullet = level ? '• ' : '';
|
||||
category.value = category.cid;
|
||||
@@ -350,10 +371,10 @@ Categories.buildForSelectCategories = function (categories, fields) {
|
||||
category.children.forEach(child => recursive(child, categoriesData, ' ' + level, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
parentCid = parentCid || 0;
|
||||
const categoriesData = [];
|
||||
|
||||
const rootCategories = categories.filter(category => category && !category.parentCid);
|
||||
const rootCategories = categories.filter(category => category && category.parentCid === parentCid);
|
||||
|
||||
rootCategories.forEach(category => recursive(category, categoriesData, '', 0));
|
||||
|
||||
|
||||
@@ -91,25 +91,27 @@ module.exports = function (Categories) {
|
||||
};
|
||||
|
||||
async function getTopics(tids, uid) {
|
||||
const topicData = await topics.getTopicsFields(tids, ['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount']);
|
||||
const topicData = await topics.getTopicsFields(
|
||||
tids,
|
||||
['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount']
|
||||
);
|
||||
topicData.forEach(function (topic) {
|
||||
if (topic) {
|
||||
topic.teaserPid = topic.teaserPid || topic.mainPid;
|
||||
}
|
||||
});
|
||||
var cids = _.uniq(topicData.map(topic => topic && topic.cid).filter(cid => parseInt(cid, 10)));
|
||||
const [categoryData, teasers] = await Promise.all([
|
||||
Categories.getCategoriesFields(cids, ['cid', 'parentCid']),
|
||||
const cids = _.uniq(topicData.map(t => t && t.cid).filter(cid => parseInt(cid, 10)));
|
||||
const getToRoot = async () => await Promise.all(cids.map(Categories.getParentCids));
|
||||
const [toRoot, teasers] = await Promise.all([
|
||||
getToRoot(),
|
||||
topics.getTeasers(topicData, uid),
|
||||
]);
|
||||
var parentCids = {};
|
||||
categoryData.forEach(function (category) {
|
||||
parentCids[category.cid] = category.parentCid;
|
||||
});
|
||||
const cidToRoot = _.zipObject(cids, toRoot);
|
||||
|
||||
teasers.forEach(function (teaser, index) {
|
||||
if (teaser) {
|
||||
teaser.cid = topicData[index].cid;
|
||||
teaser.parentCid = parseInt(parentCids[teaser.cid], 10) || 0;
|
||||
teaser.parentCids = cidToRoot[teaser.cid];
|
||||
teaser.tid = undefined;
|
||||
teaser.uid = undefined;
|
||||
teaser.topic = {
|
||||
@@ -124,11 +126,12 @@ module.exports = function (Categories) {
|
||||
function assignTopicsToCategories(categories, topics) {
|
||||
categories.forEach(function (category) {
|
||||
if (category) {
|
||||
category.posts = topics.filter(topic => topic.cid && (topic.cid === category.cid || topic.parentCid === category.cid))
|
||||
category.posts = topics.filter(t => t.cid && (t.cid === category.cid || t.parentCids.includes(category.cid)))
|
||||
.sort((a, b) => b.pid - a.pid)
|
||||
.slice(0, parseInt(category.numRecentReplies, 10));
|
||||
}
|
||||
});
|
||||
topics.forEach((t) => { t.parentCids = undefined; });
|
||||
}
|
||||
|
||||
function bubbleUpChildrenPosts(categoryData) {
|
||||
@@ -137,7 +140,8 @@ module.exports = function (Categories) {
|
||||
if (category.posts.length) {
|
||||
return;
|
||||
}
|
||||
var posts = [];
|
||||
|
||||
const posts = [];
|
||||
getPostsRecursive(category, posts);
|
||||
|
||||
posts.sort((a, b) => b.pid - a.pid);
|
||||
@@ -150,15 +154,12 @@ module.exports = function (Categories) {
|
||||
|
||||
function getPostsRecursive(category, posts) {
|
||||
if (Array.isArray(category.posts)) {
|
||||
category.posts.forEach(function (p) {
|
||||
posts.push(p);
|
||||
});
|
||||
category.posts.forEach(p => posts.push(p));
|
||||
}
|
||||
|
||||
category.children.forEach(function (child) {
|
||||
getPostsRecursive(child, posts);
|
||||
});
|
||||
category.children.forEach(child => getPostsRecursive(child, posts));
|
||||
}
|
||||
|
||||
// terrible name, should be topics.moveTopicPosts
|
||||
Categories.moveRecentReplies = async function (tid, oldCid, cid) {
|
||||
await updatePostCount(tid, oldCid, cid);
|
||||
|
||||
@@ -41,6 +41,15 @@ module.exports = function (Categories) {
|
||||
|
||||
Categories.getTree(categoryData, 0);
|
||||
await Categories.getRecentTopicReplies(categoryData, uid, data.qs);
|
||||
categoryData.forEach(function (category) {
|
||||
if (category && Array.isArray(category.children)) {
|
||||
category.children = category.children.slice(0, category.subCategoriesPerPage);
|
||||
category.children.forEach(function (child) {
|
||||
child.children = undefined;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
categoryData.sort(function (c1, c2) {
|
||||
if (c1.parentCid !== c2.parentCid) {
|
||||
return c1.parentCid - c2.parentCid;
|
||||
|
||||
@@ -50,12 +50,12 @@ module.exports = function (Categories) {
|
||||
return await updateTagWhitelist(cid, value);
|
||||
} else if (key === 'name') {
|
||||
return await updateName(cid, value);
|
||||
} else if (key === 'order') {
|
||||
return await updateOrder(cid, value);
|
||||
}
|
||||
|
||||
await db.setObjectField('category:' + cid, key, value);
|
||||
if (key === 'order') {
|
||||
await updateOrder(cid, value);
|
||||
} else if (key === 'description') {
|
||||
if (key === 'description') {
|
||||
await Categories.parseDescription(cid, value);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,12 @@ module.exports = function (Categories) {
|
||||
db.setObjectField('category:' + cid, 'parentCid', newParent),
|
||||
]);
|
||||
|
||||
cache.del(['cid:' + oldParent + ':children', 'cid:' + newParent + ':children']);
|
||||
cache.del([
|
||||
'cid:' + oldParent + ':children',
|
||||
'cid:' + newParent + ':children',
|
||||
'cid:' + oldParent + ':children:all',
|
||||
'cid:' + newParent + ':children:all',
|
||||
]);
|
||||
}
|
||||
|
||||
async function updateTagWhitelist(cid, tags) {
|
||||
@@ -90,8 +95,38 @@ module.exports = function (Categories) {
|
||||
|
||||
async function updateOrder(cid, order) {
|
||||
const parentCid = await Categories.getCategoryField(cid, 'parentCid');
|
||||
await db.sortedSetsAdd(['categories:cid', 'cid:' + parentCid + ':children'], order, cid);
|
||||
cache.del(['categories:cid', 'cid:' + parentCid + ':children']);
|
||||
await db.sortedSetsAdd('categories:cid', order, cid);
|
||||
|
||||
const childrenCids = await db.getSortedSetRange(
|
||||
'cid:' + parentCid + ':children', 0, -1
|
||||
);
|
||||
|
||||
const currentIndex = childrenCids.indexOf(String(cid));
|
||||
if (currentIndex === -1) {
|
||||
throw new Error('[[error:no-category]]');
|
||||
}
|
||||
// moves cid to index order-1 in the array
|
||||
if (childrenCids.length > 1) {
|
||||
childrenCids.splice(Math.max(0, order - 1), 0, childrenCids.splice(currentIndex, 1)[0]);
|
||||
}
|
||||
|
||||
// recalculate orders from array indices
|
||||
await db.sortedSetAdd(
|
||||
'cid:' + parentCid + ':children',
|
||||
childrenCids.map((cid, index) => index + 1),
|
||||
childrenCids
|
||||
);
|
||||
|
||||
await db.setObjectBulk(
|
||||
childrenCids.map(cid => 'category:' + cid),
|
||||
childrenCids.map((cid, index) => ({ order: index + 1 }))
|
||||
);
|
||||
|
||||
cache.del([
|
||||
'categories:cid',
|
||||
'cid:' + parentCid + ':children',
|
||||
'cid:' + parentCid + ':children:all',
|
||||
]);
|
||||
}
|
||||
|
||||
Categories.parseDescription = async function (cid, description) {
|
||||
|
||||
@@ -4,6 +4,8 @@ const user = require('../../user');
|
||||
const categories = require('../../categories');
|
||||
const accountHelpers = require('./helpers');
|
||||
const helpers = require('../helpers');
|
||||
const pagination = require('../../pagination');
|
||||
const meta = require('../../meta');
|
||||
|
||||
const categoriesController = module.exports;
|
||||
|
||||
@@ -12,11 +14,18 @@ categoriesController.get = async function (req, res, next) {
|
||||
if (!userData) {
|
||||
return next();
|
||||
}
|
||||
const [states, categoriesData] = await Promise.all([
|
||||
const [states, allCategoriesData] = await Promise.all([
|
||||
user.getCategoryWatchState(userData.uid),
|
||||
categories.buildForSelect(userData.uid, 'find', ['descriptionParsed', 'depth', 'slug']),
|
||||
]);
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(allCategoriesData.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 categoriesData = allCategoriesData.slice(start, stop + 1);
|
||||
|
||||
|
||||
categoriesData.forEach(function (category) {
|
||||
if (category) {
|
||||
category.isIgnored = states[category.cid] === categories.watchStates.ignoring;
|
||||
@@ -30,5 +39,6 @@ categoriesController.get = async function (req, res, next) {
|
||||
{ text: userData.username, url: '/user/' + userData.userslug },
|
||||
{ text: '[[pages:categories]]' },
|
||||
]);
|
||||
userData.pagination = pagination.create(page, pageCount, req.query);
|
||||
res.render('account/categories', userData);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const db = require('../../database');
|
||||
const groups = require('../../groups');
|
||||
const categories = require('../../categories');
|
||||
const privileges = require('../../privileges');
|
||||
@@ -9,29 +10,34 @@ const user = require('../../user');
|
||||
|
||||
const AdminsMods = module.exports;
|
||||
|
||||
AdminsMods.get = async function (req, res) {
|
||||
const [admins, globalMods, categories] = await Promise.all([
|
||||
AdminsMods.get = async function (req, res, next) {
|
||||
let cid = parseInt(req.query.cid, 10) || 0;
|
||||
if (!cid) {
|
||||
cid = (await db.getSortedSetRange('cid:0:children', 0, 0))[0];
|
||||
}
|
||||
const selectedCategory = await categories.getCategoryData(cid);
|
||||
if (!selectedCategory) {
|
||||
return next();
|
||||
}
|
||||
const [admins, globalMods, moderators] = await Promise.all([
|
||||
groups.get('administrators', { uid: req.uid }),
|
||||
groups.get('Global Moderators', { uid: req.uid }),
|
||||
getModeratorsOfCategories(),
|
||||
getModeratorsOfCategories(selectedCategory),
|
||||
]);
|
||||
|
||||
res.render('admin/manage/admins-mods', {
|
||||
admins: admins,
|
||||
globalMods: globalMods,
|
||||
categories: categories,
|
||||
categoryMods: [moderators],
|
||||
selectedCategory: selectedCategory,
|
||||
allPrivileges: privileges.userPrivilegeList,
|
||||
});
|
||||
};
|
||||
|
||||
async function getModeratorsOfCategories() {
|
||||
const categoryData = await categories.buildForSelectAll(['depth', 'disabled']);
|
||||
const moderatorUids = await categories.getModeratorUids(categoryData.map(c => c.cid));
|
||||
async function getModeratorsOfCategories(categoryData) {
|
||||
const moderatorUids = await categories.getModeratorUids([categoryData.cid]);
|
||||
const uids = _.uniq(_.flatten(moderatorUids));
|
||||
const moderatorData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']);
|
||||
const moderatorMap = _.zipObject(uids, moderatorData);
|
||||
categoryData.forEach((c, index) => {
|
||||
c.moderators = moderatorUids[index].map(uid => moderatorMap[uid]);
|
||||
});
|
||||
categoryData.moderators = moderatorData;
|
||||
return categoryData;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const nconf = require('nconf');
|
||||
const categories = require('../../categories');
|
||||
const analytics = require('../../analytics');
|
||||
const plugins = require('../../plugins');
|
||||
const translator = require('../../translator');
|
||||
const meta = require('../../meta');
|
||||
const helpers = require('../helpers');
|
||||
const pagination = require('../../pagination');
|
||||
|
||||
const categoriesController = module.exports;
|
||||
|
||||
categoriesController.get = async function (req, res, next) {
|
||||
const [categoryData, parent, allCategories] = await Promise.all([
|
||||
const [categoryData, parent, selectedData] = await Promise.all([
|
||||
categories.getCategories([req.params.category_id], req.uid),
|
||||
categories.getParents([req.params.category_id]),
|
||||
categories.buildForSelectAll(),
|
||||
helpers.getSelectedCategory(req.params.category_id),
|
||||
]);
|
||||
|
||||
const category = categoryData[0];
|
||||
@@ -21,47 +25,108 @@ categoriesController.get = async function (req, res, next) {
|
||||
}
|
||||
|
||||
category.parent = parent[0];
|
||||
allCategories.forEach(function (category) {
|
||||
if (category) {
|
||||
category.selected = parseInt(category.cid, 10) === parseInt(req.params.category_id, 10);
|
||||
}
|
||||
});
|
||||
const selectedCategory = allCategories.find(c => c.selected);
|
||||
|
||||
const data = await plugins.hooks.fire('filter:admin.category.get', {
|
||||
req: req,
|
||||
res: res,
|
||||
category: category,
|
||||
customClasses: [],
|
||||
allCategories: allCategories,
|
||||
});
|
||||
data.category.name = translator.escape(String(data.category.name));
|
||||
data.category.description = translator.escape(String(data.category.description));
|
||||
|
||||
res.render('admin/manage/category', {
|
||||
category: data.category,
|
||||
categories: data.allCategories,
|
||||
selectedCategory: selectedCategory,
|
||||
selectedCategory: selectedData.selectedCategory,
|
||||
customClasses: data.customClasses,
|
||||
postQueueEnabled: !!meta.config.postQueue,
|
||||
});
|
||||
};
|
||||
|
||||
categoriesController.getAll = async function (req, res) {
|
||||
const rootCid = parseInt(req.query.cid, 10) || 0;
|
||||
async function getRootAndChildren() {
|
||||
const rootChildren = await categories.getAllCidsFromSet(`cid:${rootCid}:children`);
|
||||
const childCids = _.flatten(await Promise.all(rootChildren.map(cid => categories.getChildrenCids(cid))));
|
||||
return [rootCid].concat(rootChildren.concat(childCids));
|
||||
}
|
||||
|
||||
// Categories list will be rendered on client side with recursion, etc.
|
||||
const cids = await categories.getAllCidsFromSet('categories:cid');
|
||||
const cids = await (rootCid ? getRootAndChildren() : categories.getAllCidsFromSet('categories:cid'));
|
||||
|
||||
let rootParent = 0;
|
||||
if (rootCid) {
|
||||
rootParent = await categories.getCategoryField(rootCid, 'parentCid') || 0;
|
||||
}
|
||||
|
||||
const fields = [
|
||||
'cid', 'name', 'icon', 'parentCid', 'disabled', 'link',
|
||||
'color', 'bgColor', 'backgroundImage', 'imageClass',
|
||||
'cid', 'name', 'icon', 'parentCid', 'disabled', 'link', 'order',
|
||||
'color', 'bgColor', 'backgroundImage', 'imageClass', 'subCategoriesPerPage',
|
||||
];
|
||||
const categoriesData = await categories.getCategoriesFields(cids, fields);
|
||||
const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields });
|
||||
const tree = categories.getTree(result.categories, 0);
|
||||
let tree = categories.getTree(result.categories, rootParent);
|
||||
|
||||
const cidsCount = rootCid ? cids.length - 1 : tree.length;
|
||||
|
||||
const pageCount = Math.max(1, Math.ceil(cidsCount / 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;
|
||||
|
||||
function trim(c) {
|
||||
if (c.children) {
|
||||
c.children = c.children.slice(0, c.subCategoriesPerPage);
|
||||
c.children.forEach(c => trim(c));
|
||||
}
|
||||
}
|
||||
if (rootCid && tree[0] && Array.isArray(tree[0].children)) {
|
||||
tree[0].children = tree[0].children.slice(start, stop);
|
||||
tree[0].children.forEach(trim);
|
||||
} else {
|
||||
tree = tree.slice(start, stop);
|
||||
tree.forEach(trim);
|
||||
}
|
||||
|
||||
let selectedCategory;
|
||||
if (rootCid) {
|
||||
selectedCategory = await categories.getCategoryData(rootCid);
|
||||
}
|
||||
const crumbs = await buildBreadcrumbs(req, selectedCategory);
|
||||
res.render('admin/manage/categories', {
|
||||
categories: tree,
|
||||
categoriesTree: tree,
|
||||
selectedCategory: selectedCategory,
|
||||
breadcrumbs: crumbs,
|
||||
pagination: pagination.create(page, pageCount, req.query),
|
||||
categoriesPerPage: meta.config.categoriesPerPage,
|
||||
});
|
||||
};
|
||||
|
||||
async function buildBreadcrumbs(req, categoryData) {
|
||||
if (!categoryData) {
|
||||
return;
|
||||
}
|
||||
const breadcrumbs = [
|
||||
{
|
||||
text: categoryData.name,
|
||||
url: nconf.get('relative_path') + '/admin/manage/categories?cid=' + categoryData.cid,
|
||||
cid: categoryData.cid,
|
||||
},
|
||||
];
|
||||
const allCrumbs = await helpers.buildCategoryBreadcrumbs(categoryData.parentCid);
|
||||
const crumbs = allCrumbs.filter(c => c.cid);
|
||||
|
||||
crumbs.forEach(function (c) {
|
||||
c.url = '/admin/manage/categories?cid=' + c.cid;
|
||||
});
|
||||
crumbs.unshift({
|
||||
text: '[[admin/manage/categories:top-level]]',
|
||||
url: '/admin/manage/categories',
|
||||
});
|
||||
|
||||
return crumbs.concat(breadcrumbs);
|
||||
}
|
||||
|
||||
categoriesController.getAnalytics = async function (req, res) {
|
||||
const [name, analyticsData] = await Promise.all([
|
||||
categories.getCategoryField(req.params.category_id, 'name'),
|
||||
|
||||
@@ -5,7 +5,6 @@ const validator = require('validator');
|
||||
|
||||
const db = require('../../database');
|
||||
const user = require('../../user');
|
||||
const categories = require('../../categories');
|
||||
const groups = require('../../groups');
|
||||
const meta = require('../../meta');
|
||||
const pagination = require('../../pagination');
|
||||
@@ -23,22 +22,19 @@ groupsController.list = async function (req, res) {
|
||||
const stop = start + groupsPerPage - 1;
|
||||
groupNames = groupNames.slice(start, stop + 1);
|
||||
|
||||
const allCategories = await categories.buildForSelectAll();
|
||||
const groupData = await groups.getGroupsData(groupNames);
|
||||
res.render('admin/manage/groups', {
|
||||
groups: groupData,
|
||||
pagination: pagination.create(page, pageCount),
|
||||
yourid: req.uid,
|
||||
categories: allCategories,
|
||||
});
|
||||
};
|
||||
|
||||
groupsController.get = async function (req, res, next) {
|
||||
const groupName = req.params.name;
|
||||
const [groupNames, group, allCategories] = await Promise.all([
|
||||
const [groupNames, group] = await Promise.all([
|
||||
getGroupNames(),
|
||||
groups.get(groupName, { uid: req.uid, truncateUserList: true, userListCount: 20 }),
|
||||
categories.buildForSelectAll(),
|
||||
]);
|
||||
|
||||
if (!group || groupName === groups.BANNED_USERS) {
|
||||
@@ -60,7 +56,6 @@ groupsController.get = async function (req, res, next) {
|
||||
allowPrivateGroups: meta.config.allowPrivateGroups,
|
||||
maximumGroupNameLength: meta.config.maximumGroupNameLength,
|
||||
maximumGroupTitleLength: meta.config.maximumGroupTitleLength,
|
||||
categories: allCategories,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -9,19 +9,14 @@ privilegesController.get = async function (req, res) {
|
||||
const cid = req.params.cid ? parseInt(req.params.cid, 10) || 0 : 0;
|
||||
const isAdminPriv = req.params.cid === 'admin';
|
||||
|
||||
let method;
|
||||
let privilegesData;
|
||||
if (cid > 0) {
|
||||
method = privileges.categories.list.bind(null, cid);
|
||||
privilegesData = await privileges.categories.list(cid);
|
||||
} else if (cid === 0) {
|
||||
method = isAdminPriv ? privileges.admin.list : privileges.global.list;
|
||||
privilegesData = await (isAdminPriv ? privileges.admin.list(req.uid) : privileges.global.list());
|
||||
}
|
||||
|
||||
const [privilegesData, categoriesData] = await Promise.all([
|
||||
method(isAdminPriv ? req.uid : undefined),
|
||||
categories.buildForSelectAll(),
|
||||
]);
|
||||
|
||||
categoriesData.unshift({
|
||||
const categoriesData = [{
|
||||
cid: 0,
|
||||
name: '[[admin/manage/privileges:global]]',
|
||||
icon: 'fa-list',
|
||||
@@ -29,7 +24,7 @@ privilegesController.get = async function (req, res) {
|
||||
cid: 'admin', // what do?
|
||||
name: '[[admin/manage/privileges:admin]]',
|
||||
icon: 'fa-lock',
|
||||
});
|
||||
}];
|
||||
|
||||
let selectedCategory;
|
||||
categoriesData.forEach(function (category) {
|
||||
@@ -41,6 +36,10 @@ privilegesController.get = async function (req, res) {
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!selectedCategory) {
|
||||
selectedCategory = await categories.getCategoryFields(cid, ['cid', 'name', 'icon', 'bgColor', 'color']);
|
||||
}
|
||||
|
||||
const group = req.query.group ? req.query.group : '';
|
||||
res.render('admin/manage/privileges', {
|
||||
privileges: privilegesData,
|
||||
|
||||
@@ -28,7 +28,7 @@ categoriesController.list = async function (req, res) {
|
||||
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 allChildCids = _.flatten(await Promise.all(pageCids.map(categories.getChildrenCids)));
|
||||
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);
|
||||
@@ -36,26 +36,15 @@ categoriesController.list = async function (req, res) {
|
||||
|
||||
const data = {
|
||||
title: meta.config.homePageTitle || '[[pages:home]]',
|
||||
selectCategoryLabel: '[[pages:categories]]',
|
||||
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,
|
||||
};
|
||||
}
|
||||
helpers.trimChildren(category);
|
||||
helpers.setCategoryTeaser(category);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -105,11 +105,15 @@ categoryController.get = async function (req, res, next) {
|
||||
categoryData.nextSubCategoryStart = categoryData.subCategoriesPerPage;
|
||||
categoryData.children = categoryData.children.slice(0, categoryData.subCategoriesPerPage);
|
||||
categoryData.children.forEach(function (child) {
|
||||
child.children = undefined;
|
||||
if (child) {
|
||||
helpers.trimChildren(child);
|
||||
helpers.setCategoryTeaser(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
categoryData.title = translator.escape(categoryData.name);
|
||||
categoryData.selectCategoryLabel = '[[category:subcategories]]';
|
||||
categoryData.description = translator.escape(categoryData.description);
|
||||
categoryData.privileges = userPrivileges;
|
||||
categoryData.showSelect = userPrivileges.editable;
|
||||
|
||||
@@ -244,36 +244,9 @@ async function getCategoryData(cids, uid, selectedCid, states, privilege) {
|
||||
selectedCid = [selectedCid];
|
||||
}
|
||||
selectedCid = selectedCid && selectedCid.map(String);
|
||||
states = states || [categories.watchStates.watching, categories.watchStates.notwatching];
|
||||
|
||||
const [allowed, watchState, categoryData, isAdmin] = await Promise.all([
|
||||
privileges.categories.isUserAllowedTo(privilege, cids, uid),
|
||||
categories.getWatchState(cids, uid),
|
||||
categories.getCategoriesData(cids),
|
||||
user.isAdministrator(uid),
|
||||
]);
|
||||
|
||||
categories.getTree(categoryData);
|
||||
|
||||
const cidToAllowed = _.zipObject(cids, allowed.map(allowed => isAdmin || allowed));
|
||||
const cidToCategory = _.zipObject(cids, categoryData);
|
||||
const cidToWatchState = _.zipObject(cids, watchState);
|
||||
|
||||
const visibleCategories = categoryData.filter(function (c) {
|
||||
const hasVisibleChildren = checkVisibleChildren(c, cidToAllowed, cidToWatchState, states);
|
||||
const isCategoryVisible = c && cidToAllowed[c.cid] && !c.link && !c.disabled && states.includes(cidToWatchState[c.cid]);
|
||||
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]) {
|
||||
cidToCategory[c.parent.cid].children = cidToCategory[c.parent.cid].children.filter(child => child.cid !== c.cid);
|
||||
}
|
||||
|
||||
return c && !shouldBeRemoved;
|
||||
const visibleCategories = await helpers.getVisibleCategories({
|
||||
cids, uid, states, privilege, showLinks: false,
|
||||
});
|
||||
|
||||
const categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass']);
|
||||
@@ -308,26 +281,112 @@ async function getCategoryData(cids, uid, selectedCid, states, privilege) {
|
||||
};
|
||||
}
|
||||
|
||||
helpers.getVisibleCategories = async function (params) {
|
||||
const cids = params.cids;
|
||||
const uid = params.uid;
|
||||
const states = params.states || [categories.watchStates.watching, categories.watchStates.notwatching];
|
||||
const privilege = params.privilege;
|
||||
const showLinks = !!params.showLinks;
|
||||
|
||||
let [allowed, watchState, categoriesData, isAdmin, isModerator] = await Promise.all([
|
||||
privileges.categories.isUserAllowedTo(privilege, cids, uid),
|
||||
categories.getWatchState(cids, uid),
|
||||
categories.getCategoriesData(cids),
|
||||
user.isAdministrator(uid),
|
||||
user.isModerator(uid, cids),
|
||||
]);
|
||||
|
||||
const filtered = await plugins.hooks.fire('filter:helpers.getVisibleCategories', {
|
||||
uid: uid,
|
||||
allowed: allowed,
|
||||
watchState: watchState,
|
||||
categoriesData: categoriesData,
|
||||
isModerator: isModerator,
|
||||
isAdmin: isAdmin,
|
||||
});
|
||||
({ allowed, watchState, categoriesData, isModerator, isAdmin } = filtered);
|
||||
|
||||
categories.getTree(categoriesData, params.parentCid);
|
||||
|
||||
const cidToAllowed = _.zipObject(cids, allowed.map((allowed, i) => isAdmin || isModerator[i] || allowed));
|
||||
const cidToCategory = _.zipObject(cids, categoriesData);
|
||||
const cidToWatchState = _.zipObject(cids, watchState);
|
||||
|
||||
return categoriesData.filter(function (c) {
|
||||
if (!c) {
|
||||
return false;
|
||||
}
|
||||
const hasVisibleChildren = checkVisibleChildren(c, cidToAllowed, cidToWatchState, states);
|
||||
const isCategoryVisible = cidToAllowed[c.cid] && (showLinks || !c.link) && !c.disabled && states.includes(cidToWatchState[c.cid]);
|
||||
const shouldBeRemoved = !hasVisibleChildren && !isCategoryVisible;
|
||||
const shouldBeDisaplayedAsDisabled = hasVisibleChildren && !isCategoryVisible;
|
||||
|
||||
if (shouldBeDisaplayedAsDisabled) {
|
||||
c.disabledClass = true;
|
||||
}
|
||||
|
||||
if (shouldBeRemoved && 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);
|
||||
}
|
||||
|
||||
return !shouldBeRemoved;
|
||||
});
|
||||
};
|
||||
|
||||
helpers.getSelectedCategory = async function (cid) {
|
||||
if (cid && !Array.isArray(cid)) {
|
||||
cid = [cid];
|
||||
}
|
||||
cid = cid && cid.map(cid => parseInt(cid, 10));
|
||||
let selectedCategories = await categories.getCategoriesData(cid);
|
||||
|
||||
if (selectedCategories.length > 1) {
|
||||
selectedCategories = {
|
||||
icon: 'fa-plus',
|
||||
name: '[[unread:multiple-categories-selected]]',
|
||||
bgColor: '#ddd',
|
||||
};
|
||||
} else if (selectedCategories.length === 1) {
|
||||
selectedCategories = selectedCategories[0];
|
||||
} else {
|
||||
selectedCategories = null;
|
||||
}
|
||||
return {
|
||||
selectedCids: cid || [],
|
||||
selectedCategory: selectedCategories,
|
||||
};
|
||||
};
|
||||
|
||||
helpers.trimChildren = function (category) {
|
||||
if (Array.isArray(category.children)) {
|
||||
category.children = category.children.slice(0, category.subCategoriesPerPage);
|
||||
category.children.forEach(function (child) {
|
||||
child.children = undefined;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
helpers.setCategoryTeaser = function (category) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function checkVisibleChildren(c, cidToAllowed, cidToWatchState, states) {
|
||||
if (!c || !Array.isArray(c.children)) {
|
||||
return false;
|
||||
}
|
||||
return c.children.some(c => c && !c.disabled && (
|
||||
return c.children.some(c => !c.disabled && (
|
||||
(cidToAllowed[c.cid] && states.includes(cidToWatchState[c.cid])) || checkVisibleChildren(c, cidToAllowed, cidToWatchState, states)
|
||||
));
|
||||
}
|
||||
|
||||
helpers.getHomePageRoutes = async function (uid) {
|
||||
let cids = await categories.getAllCidsFromSet('categories:cid');
|
||||
cids = await privileges.categories.filterCids('find', cids, uid);
|
||||
const categoryData = await categories.getCategoriesFields(cids, ['name', 'slug']);
|
||||
|
||||
const categoryRoutes = categoryData.map(function (category) {
|
||||
return {
|
||||
route: 'category/' + category.slug,
|
||||
name: 'Category: ' + category.name,
|
||||
};
|
||||
});
|
||||
const routes = [
|
||||
{
|
||||
route: 'categories',
|
||||
@@ -349,13 +408,15 @@ helpers.getHomePageRoutes = async function (uid) {
|
||||
route: 'popular',
|
||||
name: 'Popular',
|
||||
},
|
||||
].concat(categoryRoutes, [
|
||||
{
|
||||
route: 'custom',
|
||||
name: 'Custom',
|
||||
},
|
||||
]);
|
||||
const data = await plugins.hooks.fire('filter:homepage.get', { routes: routes });
|
||||
];
|
||||
const data = await plugins.hooks.fire('filter:homepage.get', {
|
||||
uid: uid,
|
||||
routes: routes,
|
||||
});
|
||||
return data.routes;
|
||||
};
|
||||
|
||||
|
||||
@@ -21,12 +21,6 @@ modsController.flags.list = async function (req, res, next) {
|
||||
const validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick', 'page', 'perPage'];
|
||||
const validSorts = ['newest', 'oldest', 'reports', 'upvotes', 'downvotes', 'replies'];
|
||||
|
||||
// Reset filters if explicitly requested
|
||||
if (parseInt(req.query.reset, 10) === 1) {
|
||||
delete req.session.flags_filters;
|
||||
delete req.session.flags_sort;
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
user.isAdminOrGlobalMod(req.uid),
|
||||
user.getModeratedCids(req.uid),
|
||||
@@ -41,30 +35,21 @@ modsController.flags.list = async function (req, res, next) {
|
||||
}
|
||||
|
||||
if (!isAdminOrGlobalMod && moderatedCids.length) {
|
||||
res.locals.cids = moderatedCids;
|
||||
res.locals.cids = moderatedCids.map(cid => String(cid));
|
||||
}
|
||||
|
||||
// Parse query string params for filters, eliminate non-valid filters
|
||||
filters = filters.reduce(function (memo, cur) {
|
||||
if (req.query.hasOwnProperty(cur)) {
|
||||
if (req.query[cur] === '') {
|
||||
if (req.session.hasOwnProperty('flags_filters')) {
|
||||
delete req.session.flags_filters[cur];
|
||||
}
|
||||
} else {
|
||||
if (req.query[cur] !== '') {
|
||||
memo[cur] = req.query[cur];
|
||||
}
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
let hasFilter = !!Object.keys(filters).length;
|
||||
|
||||
if (!hasFilter && req.session.hasOwnProperty('flags_filters')) {
|
||||
// Load filters from session object
|
||||
filters = req.session.flags_filters;
|
||||
hasFilter = true;
|
||||
}
|
||||
let hasFilter = !!Object.keys(filters).length;
|
||||
|
||||
if (res.locals.cids) {
|
||||
if (!filters.cid) {
|
||||
@@ -89,9 +74,7 @@ modsController.flags.list = async function (req, res, next) {
|
||||
|
||||
// Parse sort from query string
|
||||
let sort;
|
||||
if (!req.query.sort && req.session.hasOwnProperty('flags_sort')) {
|
||||
sort = req.session.flags_sort;
|
||||
} else {
|
||||
if (req.query.sort) {
|
||||
sort = sorts.includes(req.query.sort) ? req.query.sort : null;
|
||||
}
|
||||
if (sort === 'newest') {
|
||||
@@ -99,26 +82,20 @@ modsController.flags.list = async function (req, res, next) {
|
||||
}
|
||||
hasFilter = hasFilter || !!sort;
|
||||
|
||||
// Save filters and sorting into session unless removed
|
||||
if (hasFilter) {
|
||||
req.session.flags_filters = filters;
|
||||
}
|
||||
req.session.flags_sort = sort;
|
||||
|
||||
const [flagsData, analyticsData, categoriesData] = await Promise.all([
|
||||
const [flagsData, analyticsData, selectData] = await Promise.all([
|
||||
flags.list({
|
||||
filters: filters,
|
||||
sort: sort,
|
||||
uid: req.uid,
|
||||
}),
|
||||
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30),
|
||||
categories.buildForSelect(req.uid, 'read'),
|
||||
helpers.getSelectedCategory(filters.cid),
|
||||
]);
|
||||
|
||||
res.render('flags/list', {
|
||||
flags: flagsData.flags,
|
||||
analytics: analyticsData,
|
||||
categories: filterCategories(res.locals.cids, categoriesData),
|
||||
selectedCategory: selectData.selectedCategory,
|
||||
hasFilter: hasFilter,
|
||||
filters: filters,
|
||||
sort: sort || 'newest',
|
||||
@@ -170,27 +147,6 @@ modsController.flags.detail = async function (req, res, next) {
|
||||
}));
|
||||
};
|
||||
|
||||
function filterCategories(moderatedCids, categories) {
|
||||
// If cids is populated, then slim down the categories list
|
||||
if (moderatedCids) {
|
||||
categories = categories.filter(category => moderatedCids.includes(String(category.cid)));
|
||||
}
|
||||
|
||||
return categories.reduce(function (memo, cur) {
|
||||
if (!moderatedCids) {
|
||||
memo[cur.cid] = cur.name;
|
||||
return memo;
|
||||
}
|
||||
|
||||
// If mod, remove categories they can't moderate
|
||||
if (moderatedCids.includes(String(cur.cid))) {
|
||||
memo[cur.cid] = cur.name;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
}
|
||||
|
||||
modsController.postQueue = async function (req, res, next) {
|
||||
// Admins, global mods, and individual mods only
|
||||
const isPrivileged = await user.isPrivileged(req.uid);
|
||||
@@ -201,20 +157,16 @@ modsController.postQueue = async function (req, res, next) {
|
||||
const page = parseInt(req.query.page, 10) || 1;
|
||||
const postsPerPage = 20;
|
||||
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids, allCategories, categoriesData] = await Promise.all([
|
||||
const [ids, isAdminOrGlobalMod, moderatedCids, categoriesData] = await Promise.all([
|
||||
db.getSortedSetRange('post:queue', 0, -1),
|
||||
user.isAdminOrGlobalMod(req.uid),
|
||||
user.getModeratedCids(req.uid),
|
||||
categories.buildForSelect(req.uid, 'find', ['disabled', 'link', 'slug']),
|
||||
helpers.getCategoriesByStates(req.uid, cid, null, 'moderate'),
|
||||
helpers.getSelectedCategory(cid),
|
||||
]);
|
||||
|
||||
if (cid && !moderatedCids.includes(String(cid)) && !isAdminOrGlobalMod) {
|
||||
return next();
|
||||
}
|
||||
allCategories.forEach((c) => {
|
||||
c.disabledClass = !isAdminOrGlobalMod && !moderatedCids.includes(String(c.cid));
|
||||
});
|
||||
|
||||
let postData = await getQueuedPosts(ids);
|
||||
postData = postData.filter(p => p &&
|
||||
@@ -234,7 +186,6 @@ modsController.postQueue = async function (req, res, next) {
|
||||
res.render('post-queue', {
|
||||
title: '[[pages:post-queue]]',
|
||||
posts: postData,
|
||||
allCategories: allCategories,
|
||||
...categoriesData,
|
||||
allCategoriesUrl: 'post-queue' + helpers.buildQueryString(req.query, 'cid', ''),
|
||||
pagination: pagination.create(page, pageCount),
|
||||
|
||||
@@ -32,14 +32,9 @@ recentController.getData = async function (req, url, sort) {
|
||||
}
|
||||
term = term || 'alltime';
|
||||
|
||||
const states = [categories.watchStates.watching, categories.watchStates.notwatching];
|
||||
if (filter === 'watched') {
|
||||
states.push(categories.watchStates.ignoring);
|
||||
}
|
||||
|
||||
const [settings, categoryData, rssToken, canPost, isPrivileged] = await Promise.all([
|
||||
user.getSettings(req.uid),
|
||||
helpers.getCategoriesByStates(req.uid, cid, states),
|
||||
helpers.getSelectedCategory(cid),
|
||||
user.auth.getFeedToken(req.uid),
|
||||
canPostTopic(req.uid),
|
||||
user.isPrivileged(req.uid),
|
||||
@@ -49,7 +44,7 @@ recentController.getData = async function (req, url, sort) {
|
||||
const stop = start + settings.topicsPerPage - 1;
|
||||
|
||||
const data = await topics.getSortedTopics({
|
||||
cids: cid || categoryData.categories.map(c => c.cid),
|
||||
cids: cid,
|
||||
uid: req.uid,
|
||||
start: start,
|
||||
stop: stop,
|
||||
@@ -63,7 +58,6 @@ recentController.getData = async function (req, url, sort) {
|
||||
data.canPost = canPost;
|
||||
data.showSelect = isPrivileged;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = categoryData.categories;
|
||||
data.allCategoriesUrl = url + helpers.buildQueryString(req.query, 'cid', '');
|
||||
data.selectedCategory = categoryData.selectedCategory;
|
||||
data.selectedCids = categoryData.selectedCids;
|
||||
|
||||
@@ -30,16 +30,12 @@ tagsController.getTag = async function (req, res) {
|
||||
]);
|
||||
const start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||
const stop = start + settings.topicsPerPage - 1;
|
||||
const states = [categories.watchStates.watching, categories.watchStates.notwatching, categories.watchStates.ignoring];
|
||||
|
||||
const [topicCount, tids, categoriesData] = await Promise.all([
|
||||
const [topicCount, tids] = await Promise.all([
|
||||
topics.getTagTopicCount(tag, cids),
|
||||
topics.getTagTidsByCids(tag, cids, start, stop),
|
||||
helpers.getCategoriesByStates(req.uid, '', states),
|
||||
]);
|
||||
|
||||
templateData.categories = categoriesData.categories;
|
||||
|
||||
templateData.topics = await topics.getTopics(tids, req.uid);
|
||||
topics.calculateTopicIndices(templateData.topics, start);
|
||||
res.locals.metaTags = [
|
||||
|
||||
@@ -7,9 +7,7 @@ const querystring = require('querystring');
|
||||
const meta = require('../meta');
|
||||
const pagination = require('../pagination');
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const topics = require('../topics');
|
||||
const plugins = require('../plugins');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
const unreadController = module.exports;
|
||||
@@ -18,8 +16,8 @@ unreadController.get = async function (req, res) {
|
||||
const cid = req.query.cid;
|
||||
const filter = req.query.filter || '';
|
||||
|
||||
const [watchedCategories, userSettings, isPrivileged] = await Promise.all([
|
||||
getWatchedCategories(req.uid, cid, filter),
|
||||
const [categoryData, userSettings, isPrivileged] = await Promise.all([
|
||||
helpers.getSelectedCategory(cid),
|
||||
user.getSettings(req.uid),
|
||||
user.isPrivileged(req.uid),
|
||||
]);
|
||||
@@ -47,10 +45,9 @@ unreadController.get = async function (req, res) {
|
||||
}
|
||||
data.showSelect = true;
|
||||
data.showTopicTools = isPrivileged;
|
||||
data.categories = watchedCategories.categories;
|
||||
data.allCategoriesUrl = 'unread' + helpers.buildQueryString(req.query, 'cid', '');
|
||||
data.selectedCategory = watchedCategories.selectedCategory;
|
||||
data.selectedCids = watchedCategories.selectedCids;
|
||||
data.selectedCategory = categoryData.selectedCategory;
|
||||
data.selectedCids = categoryData.selectedCids;
|
||||
if (req.originalUrl.startsWith(nconf.get('relative_path') + '/api/unread') || req.originalUrl.startsWith(nconf.get('relative_path') + '/unread')) {
|
||||
data.title = '[[pages:unread]]';
|
||||
data.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[unread:title]]' }]);
|
||||
@@ -63,17 +60,6 @@ unreadController.get = async function (req, res) {
|
||||
res.render('unread', data);
|
||||
};
|
||||
|
||||
async function getWatchedCategories(uid, cid, filter) {
|
||||
if (plugins.hooks.hasListeners('filter:unread.categories')) {
|
||||
return await plugins.hooks.fire('filter:unread.categories', { uid: uid, cid: cid });
|
||||
}
|
||||
const states = [categories.watchStates.watching];
|
||||
if (filter === 'watched') {
|
||||
states.push(categories.watchStates.notwatching, categories.watchStates.ignoring);
|
||||
}
|
||||
return await helpers.getCategoriesByStates(uid, cid, states);
|
||||
}
|
||||
|
||||
unreadController.unreadTotal = async function (req, res, next) {
|
||||
const filter = req.query.filter || '';
|
||||
try {
|
||||
|
||||
@@ -32,6 +32,26 @@ module.exports = function (module) {
|
||||
cache.del(key);
|
||||
};
|
||||
|
||||
module.setObjectBulk = async function (keys, data) {
|
||||
if (!keys.length || !data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const writeData = data.map(helpers.serializeData);
|
||||
try {
|
||||
const bulk = module.client.collection('objects').initializeUnorderedBulkOp();
|
||||
keys.forEach((key, i) => bulk.find({ _key: key }).upsert().updateOne({ $set: writeData[i] }));
|
||||
await bulk.execute();
|
||||
} catch (err) {
|
||||
if (err && err.message.startsWith('E11000 duplicate key error')) {
|
||||
return await module.setObjectBulk(keys, data);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
cache.del(keys);
|
||||
};
|
||||
|
||||
module.setObjectField = async function (key, field, value) {
|
||||
if (!field) {
|
||||
return;
|
||||
|
||||
@@ -34,6 +34,14 @@ module.exports = function (module) {
|
||||
});
|
||||
};
|
||||
|
||||
module.setObjectBulk = async function (keys, data) {
|
||||
if (!keys.length || !data.length) {
|
||||
return;
|
||||
}
|
||||
// TODO: single query?
|
||||
await Promise.all(keys.map((k, i) => module.setObject(k, data[i])));
|
||||
};
|
||||
|
||||
module.setObjectField = async function (key, field, value) {
|
||||
if (!field) {
|
||||
return;
|
||||
|
||||
@@ -36,6 +36,16 @@ module.exports = function (module) {
|
||||
cache.del(key);
|
||||
};
|
||||
|
||||
module.setObjectBulk = async function (keys, data) {
|
||||
if (!keys.length || !data.length) {
|
||||
return;
|
||||
}
|
||||
const batch = module.client.batch();
|
||||
keys.forEach((k, i) => batch.hmset(k, data[i]));
|
||||
await helpers.execBatch(batch);
|
||||
cache.del(keys);
|
||||
};
|
||||
|
||||
module.setObjectField = async function (key, field, value) {
|
||||
if (!field) {
|
||||
return;
|
||||
|
||||
@@ -76,7 +76,8 @@ function modifyGroup(group, fields) {
|
||||
group.icon = validator.escape(String(group.icon || ''));
|
||||
group.createtimeISO = utils.toISOString(group.createtime);
|
||||
group.private = ([null, undefined].includes(group.private)) ? 1 : group.private;
|
||||
group.memberPostCids = (group.memberPostCids || '').split(',').map(cid => parseInt(cid, 10)).filter(Boolean);
|
||||
group.memberPostCids = group.memberPostCids || '';
|
||||
group.memberPostCidsArray = group.memberPostCids.split(',').map(cid => parseInt(cid, 10)).filter(Boolean);
|
||||
|
||||
group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url'];
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const db = require('../database');
|
||||
const plugins = require('../plugins');
|
||||
const slugify = require('../slugify');
|
||||
@@ -121,10 +120,9 @@ Groups.get = async function (groupName, options) {
|
||||
stop = (parseInt(options.userListCount, 10) || 4) - 1;
|
||||
}
|
||||
|
||||
const [groupData, members, selectCategories, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([
|
||||
const [groupData, members, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([
|
||||
Groups.getGroupData(groupName),
|
||||
Groups.getOwnersAndMembers(groupName, options.uid, 0, stop),
|
||||
categories.buildForSelect(groupName, 'topics:read', []),
|
||||
Groups.getUsersFromSet('group:' + groupName + ':pending', ['username', 'userslug', 'picture']),
|
||||
Groups.getUsersFromSet('group:' + groupName + ':invited', ['username', 'userslug', 'picture']),
|
||||
Groups.isMember(options.uid, groupName),
|
||||
@@ -138,10 +136,6 @@ Groups.get = async function (groupName, options) {
|
||||
}
|
||||
const descriptionParsed = await plugins.hooks.fire('filter:parse.raw', groupData.description);
|
||||
groupData.descriptionParsed = descriptionParsed;
|
||||
groupData.categories = selectCategories.map((category) => {
|
||||
category.selected = groupData.memberPostCids.includes(category.cid);
|
||||
return category;
|
||||
});
|
||||
groupData.members = members;
|
||||
groupData.membersNextStart = stop + 1;
|
||||
groupData.pending = pending.filter(Boolean);
|
||||
|
||||
@@ -15,8 +15,10 @@ module.exports = function (Groups) {
|
||||
groupNames = groupNames[0];
|
||||
|
||||
// Only process those groups that have the cid in its memberPostCids setting (or no setting at all)
|
||||
const groupsCids = await groups.getGroupsFields(groupNames, ['memberPostCids']);
|
||||
groupNames = groupNames.filter((groupName, idx) => !groupsCids[idx].memberPostCids.length || groupsCids[idx].memberPostCids.includes(postData.cid));
|
||||
const groupData = await groups.getGroupsFields(groupNames, ['memberPostCids']);
|
||||
groupNames = groupNames.filter(
|
||||
(groupName, idx) => !groupData[idx].memberPostCidsArray.length || groupData[idx].memberPostCidsArray.includes(postData.cid)
|
||||
);
|
||||
|
||||
const keys = groupNames.map(groupName => 'group:' + groupName + ':member:pids');
|
||||
await db.sortedSetsAdd(keys, postData.timestamp, postData.pid);
|
||||
|
||||
@@ -77,7 +77,8 @@ module.exports = function (Groups) {
|
||||
|
||||
if (values.hasOwnProperty('memberPostCids')) {
|
||||
const validCids = await categories.getCidsByPrivilege('categories:cid', groupName, 'topics:read');
|
||||
payload.memberPostCids = values.memberPostCids.filter(cid => validCids.includes(cid)).join(',') || '';
|
||||
const cidsArray = values.memberPostCids.split(',').map(cid => parseInt(cid.trim(), 10)).filter(Boolean);
|
||||
payload.memberPostCids = cidsArray.filter(cid => validCids.includes(cid)).join(',') || '';
|
||||
}
|
||||
|
||||
await db.setObject('group:' + groupName, payload);
|
||||
|
||||
@@ -77,6 +77,9 @@ module.exports = function (privileges) {
|
||||
};
|
||||
|
||||
privileges.categories.isUserAllowedTo = async function (privilege, cid, uid) {
|
||||
if ((Array.isArray(privilege) && !privilege.length) || (Array.isArray(cid) && !cid.length)) {
|
||||
return [];
|
||||
}
|
||||
if (!cid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const sockets = require('.');
|
||||
|
||||
const SocketCategories = module.exports;
|
||||
|
||||
require('./categories/search')(SocketCategories);
|
||||
|
||||
SocketCategories.getRecentReplies = async function (socket, cid) {
|
||||
return await categories.getRecentReplies(cid, socket.uid, 4);
|
||||
};
|
||||
@@ -148,7 +150,7 @@ SocketCategories.isModerator = async function (socket, cid) {
|
||||
};
|
||||
|
||||
SocketCategories.getCategory = async function (socket, cid) {
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/categories/:tid');
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid');
|
||||
return await api.categories.get(socket, { cid });
|
||||
// return await apiController.getCategoryData(cid, socket.uid);
|
||||
};
|
||||
|
||||
94
src/socket.io/categories/search.js
Normal file
94
src/socket.io/categories/search.js
Normal file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const meta = require('../../meta');
|
||||
const categories = require('../../categories');
|
||||
const privileges = require('../../privileges');
|
||||
const controllersHelpers = require('../../controllers/helpers');
|
||||
|
||||
module.exports = function (SocketCategories) {
|
||||
// used by categorySeach module
|
||||
SocketCategories.categorySearch = async function (socket, data) {
|
||||
let cids = [];
|
||||
let matchedCids = [];
|
||||
const privilege = data.privilege || 'topics:read';
|
||||
data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map(
|
||||
state => categories.watchStates[state]
|
||||
);
|
||||
|
||||
if (data.query) {
|
||||
({ cids, matchedCids } = await findMatchedCids(socket.uid, data));
|
||||
} else {
|
||||
cids = await loadCids(socket.uid, data.parentCid);
|
||||
}
|
||||
|
||||
const visibleCategories = await controllersHelpers.getVisibleCategories({
|
||||
cids, uid: socket.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
|
||||
});
|
||||
|
||||
if (Array.isArray(data.selectedCids)) {
|
||||
data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
|
||||
}
|
||||
|
||||
let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
|
||||
categoriesData = categoriesData.slice(0, 200);
|
||||
|
||||
categoriesData.forEach(function (category) {
|
||||
category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
|
||||
if (matchedCids.includes(category.cid)) {
|
||||
category.match = true;
|
||||
}
|
||||
});
|
||||
return categoriesData;
|
||||
};
|
||||
|
||||
async function findMatchedCids(uid, data) {
|
||||
const result = await categories.search({
|
||||
query: data.query,
|
||||
paginate: false,
|
||||
});
|
||||
|
||||
|
||||
let matchedCids = result.categories.map(c => c.cid);
|
||||
// no need to filter if all 3 states are used
|
||||
const filterByWatchState = !Object.values(categories.watchStates)
|
||||
.every(state => data.states.includes(state));
|
||||
|
||||
if (filterByWatchState) {
|
||||
const states = await categories.getWatchState(matchedCids, uid);
|
||||
matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
|
||||
}
|
||||
|
||||
const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
|
||||
const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));
|
||||
|
||||
return {
|
||||
cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
|
||||
matchedCids: matchedCids,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCids(uid, parentCid) {
|
||||
let resultCids = [];
|
||||
async function getCidsRecursive(cids) {
|
||||
const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
|
||||
const cidToData = _.zipObject(cids, categoryData);
|
||||
await Promise.all(cids.map(async (cid) => {
|
||||
const allChildCids = await categories.getAllCidsFromSet('cid:' + cid + ':children');
|
||||
if (allChildCids.length) {
|
||||
const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
|
||||
resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
|
||||
await getCidsRecursive(childCids);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const allRootCids = await categories.getAllCidsFromSet('cid:' + parentCid + ':children');
|
||||
const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
|
||||
const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
|
||||
resultCids = pageCids;
|
||||
await getCidsRecursive(pageCids);
|
||||
return resultCids;
|
||||
}
|
||||
};
|
||||
@@ -11,8 +11,6 @@ const privileges = require('../privileges');
|
||||
module.exports = function (User) {
|
||||
User.bans = {};
|
||||
|
||||
const systemGroups = groups.systemGroups.filter(group => group !== groups.BANNED_USERS);
|
||||
|
||||
User.bans.ban = async function (uid, until, reason) {
|
||||
// "until" (optional) is unix timestamp in milliseconds
|
||||
// "reason" (optional) is a string
|
||||
@@ -37,6 +35,7 @@ module.exports = function (User) {
|
||||
}
|
||||
|
||||
// Leaving all other system groups to have privileges constrained to the "banned-users" group
|
||||
const systemGroups = groups.systemGroups.filter(group => group !== groups.BANNED_USERS);
|
||||
await groups.leave(systemGroups, uid);
|
||||
await groups.join(groups.BANNED_USERS, uid);
|
||||
await db.sortedSetAdd('users:banned', now, uid);
|
||||
|
||||
@@ -38,26 +38,28 @@
|
||||
|
||||
<br/>
|
||||
|
||||
{{{ each categories }}}
|
||||
<div class="categories category-wrapper category-depth-{categories.depth}">
|
||||
<h4><!-- IF categories.icon --><i class="fa {categories.icon}"></i> <!-- ENDIF categories.icon -->[[admin/manage/admins-mods:moderators-of-category, {categories.name}]]{{{if categories.disabled}}}<span class="badge badge-primary">[[admin/manage/admins-mods:disabled]]</span>{{{end}}}</h4>
|
||||
<div class="moderator-area" data-cid="{categories.cid}">
|
||||
{{{ each categories.moderators }}}
|
||||
<div class="user-card pull-left" data-uid="{categories.moderators.uid}">
|
||||
<!-- IF categories.moderators.picture -->
|
||||
<img class="avatar avatar-sm" src="{categories.moderators.picture}" />
|
||||
<!-- ELSE -->
|
||||
<div class="avatar avatar-sm" style="background-color: {categories.moderators.icon:bgColor};">{categories.moderators.icon:text}</div>
|
||||
<!-- ENDIF categories.moderators.picture -->
|
||||
<a href="{config.relative_path}/user/{categories.moderators.userslug}">{categories.moderators.username}</a>
|
||||
<!-- IMPORT partials/category-selector.tpl -->
|
||||
|
||||
{{{ each categoryMods }}}
|
||||
<div class="categories category-wrapper category-depth-{categoryMods.depth}">
|
||||
<h4>{{{ if categoryMods.icon }}}<i class="fa {categoryMods.icon}"></i> {{{ end }}}[[admin/manage/admins-mods:moderators-of-category, {categoryMods.name}]]{{{if categoryMods.disabled}}}<span class="badge badge-primary">[[admin/manage/admins-mods:disabled]]</span>{{{end}}}</h4>
|
||||
<div class="moderator-area" data-cid="{categoryMods.cid}">
|
||||
{{{ each categoryMods.moderators }}}
|
||||
<div class="user-card pull-left" data-uid="{categoryMods.moderators.uid}">
|
||||
{{{ if categoryMods.moderators.picture }}}
|
||||
<img class="avatar avatar-sm" src="{categoryMods.moderators.picture}" />
|
||||
{{{ else }}}
|
||||
<div class="avatar avatar-sm" style="background-color: {categoryMods.moderators.icon:bgColor};">{categoryMods.moderators.icon:text}</div>
|
||||
{{{ end }}}
|
||||
<a href="{config.relative_path}/user/{categoryMods.moderators.userslug}">{categoryMods.moderators.username}</a>
|
||||
<i class="remove-user-icon fa fa-times" role="button"></i>
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</div>
|
||||
|
||||
<div data-cid="{categories.cid}" class="no-moderator-warning <!-- IF categories.moderators.length -->hidden<!-- ENDIF categories.moderators.length -->">[[admin/manage/admins-mods:no-moderators]]</div>
|
||||
<div data-cid="{categoryMods.cid}" class="no-moderator-warning {{{ if categoryMods.moderators.length }}}hidden{{{ end }}}">[[admin/manage/admins-mods:no-moderators]]</div>
|
||||
|
||||
<input data-cid="{categories.cid}" class="form-control moderator-search" placeholder="[[admin/manage/admins-mods:add-moderator]]" />
|
||||
<input data-cid="{categoryMods.cid}" class="form-control moderator-search" placeholder="[[admin/manage/admins-mods:add-moderator]]" />
|
||||
</div>
|
||||
<br/>
|
||||
{{{ end }}}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<!-- IMPORT partials/breadcrumbs.tpl -->
|
||||
<div class="row">
|
||||
<div class="col-lg-9">
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button> <button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
<div class="col-lg-12">
|
||||
<div class="category btn-group">
|
||||
<!-- IMPORT partials/category-selector.tpl -->
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="[[global:search]]" id="category-search">
|
||||
<span class="input-group-addon search-button"><i class="fa fa-search"></i></span>
|
||||
<div class="btn-group">
|
||||
<button id="collapse-all" class="btn btn-default">[[admin/manage/categories:collapse-all]]</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button id="expand-all" class="btn btn-default">[[admin/manage/categories:expand-all]]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,7 +17,9 @@
|
||||
<hr/>
|
||||
<div component="category/no-matches" class="hidden">[[admin/manage/categories:no-matches]]</div>
|
||||
<div class="categories"></div>
|
||||
|
||||
<div>
|
||||
<!-- IMPORT partials/paginator.tpl -->
|
||||
</div>
|
||||
<button data-action="create" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
<i class="material-icons">add</i>
|
||||
</button>
|
||||
@@ -99,14 +99,14 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="memberPostCids">[[groups:details.member-post-cids]]</label>
|
||||
<select multiple="true" name="memberPostCids" id="memberPostCids" class="form-control" size="15">
|
||||
{{{each group.categories}}}
|
||||
<option value="{categories.cid}"{{{ if ../selected }}} selected{{{ end }}}>
|
||||
{../level}{../name}
|
||||
</option>
|
||||
{{{end}}}
|
||||
</select>
|
||||
<p class="help-block">[[groups:details.member-post-cids-help]]</p>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<input id="memberPostCids" type="text" class="form-control" value="{group.memberPostCids}">
|
||||
</div>
|
||||
<div class="col-md-3 member-post-cids-selector">
|
||||
<!-- IMPORT partials/category-selector.tpl -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
@@ -143,7 +143,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="well">
|
||||
<div class="well edit-privileges-selector">
|
||||
<strong class="pull-left">[[admin/manage/privileges:edit-privileges]]</strong><br />
|
||||
<!-- IMPORT partials/category-selector.tpl -->
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
{{{end}}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="set-order" data-cid="{categories.cid}" data-order="{categories.order}">[[admin/manage/categories:set-order]]</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[[admin/settings/homepage:description]]
|
||||
</p>
|
||||
<form class="row">
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-12">
|
||||
<label>[[admin/settings/homepage:home-page-route]]</label>
|
||||
<select class="form-control" data-field="homePageRoute">
|
||||
<!-- BEGIN routes -->
|
||||
@@ -16,6 +16,7 @@
|
||||
<br>
|
||||
<label>[[admin/settings/homepage:custom-route]]</label>
|
||||
<input type="text" class="form-control" data-field="homePageCustom"/>
|
||||
<p class="help-block">[[user:custom_route_help]]</p>
|
||||
</div>
|
||||
<br>
|
||||
<div class="checkbox">
|
||||
|
||||
@@ -585,9 +585,7 @@ describe('Admin Controllers', function () {
|
||||
assert.ifError(err);
|
||||
assert(body);
|
||||
assert(body.flags);
|
||||
assert(body.categories);
|
||||
assert(body.filters);
|
||||
assert.equal(body.categories[cid], 'Test Category');
|
||||
assert.equal(body.filters.cid.indexOf(cid), -1);
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -71,6 +71,15 @@ describe('Hash methods', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set multiple keys to different okjects', async function () {
|
||||
const keys = ['bulkKey1', 'bulkKey2'];
|
||||
const data = [{ foo: '1' }, { baz: 'baz' }];
|
||||
|
||||
await db.setObjectBulk(keys, data);
|
||||
const result = await db.getObjects(keys);
|
||||
assert.deepStrictEqual(result, data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setObjectField()', function () {
|
||||
|
||||
@@ -40,6 +40,11 @@ const relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
|
||||
nconf.set('relative_path', relativePath);
|
||||
nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
|
||||
nconf.set('upload_url', '/assets/uploads');
|
||||
nconf.set('url_parsed', urlObject);
|
||||
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
|
||||
nconf.set('secure', urlObject.protocol === 'https:');
|
||||
nconf.set('use_port', !!urlObject.port);
|
||||
nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
|
||||
|
||||
// cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353
|
||||
const domain = nconf.get('cookieDomain') || urlObject.hostname;
|
||||
@@ -118,11 +123,7 @@ before(async function () {
|
||||
|
||||
// Parse out the relative_url and other goodies from the configured URL
|
||||
const urlObject = url.parse(nconf.get('url'));
|
||||
nconf.set('url_parsed', urlObject);
|
||||
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
|
||||
nconf.set('secure', urlObject.protocol === 'https:');
|
||||
nconf.set('use_port', !!urlObject.port);
|
||||
nconf.set('port', urlObject.port || nconf.get('port') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
|
||||
|
||||
|
||||
nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
|
||||
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
|
||||
|
||||
Reference in New Issue
Block a user