mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
refactor: category listing logic to allow remote categories to be added, disabled, and re-arranged in main forum index
This commit is contained in:
@@ -3,11 +3,13 @@
|
|||||||
"add-category": "Add category",
|
"add-category": "Add category",
|
||||||
"add-local-category": "Add Local category",
|
"add-local-category": "Add Local category",
|
||||||
"add-remote-category": "Add Remote category",
|
"add-remote-category": "Add Remote category",
|
||||||
|
"remove": "Remove",
|
||||||
"jump-to": "Jump to...",
|
"jump-to": "Jump to...",
|
||||||
"settings": "Category Settings",
|
"settings": "Category Settings",
|
||||||
"edit-category": "Edit Category",
|
"edit-category": "Edit Category",
|
||||||
"privileges": "Privileges",
|
"privileges": "Privileges",
|
||||||
"back-to-categories": "Back to categories",
|
"back-to-categories": "Back to categories",
|
||||||
|
"id": "Category ID",
|
||||||
"name": "Category Name",
|
"name": "Category Name",
|
||||||
"handle": "Category Handle",
|
"handle": "Category Handle",
|
||||||
"handle.help": "Your category handle is used as a representation of this category across other networks, similar to a username. A category handle must not match an existing username or user group.",
|
"handle.help": "Your category handle is used as a representation of this category across other networks, similar to a username. A category handle must not match an existing username or user group.",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ define('admin/manage/categories', [
|
|||||||
if (val && cid) {
|
if (val && cid) {
|
||||||
const modified = {};
|
const modified = {};
|
||||||
modified[cid] = { order: Math.max(1, parseInt(val, 10)) };
|
modified[cid] = { order: Math.max(1, parseInt(val, 10)) };
|
||||||
api.put('/categories/' + cid, modified[cid]).then(function () {
|
api.put('/categories/' + encodeURIComponent(cid), modified[cid]).then(function () {
|
||||||
ajaxify.refresh();
|
ajaxify.refresh();
|
||||||
}).catch(alerts.error);
|
}).catch(alerts.error);
|
||||||
} else {
|
} else {
|
||||||
@@ -81,6 +81,8 @@ define('admin/manage/categories', [
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('.categories').on('click', 'a[data-action="remove"]', Categories.removeCategory);
|
||||||
|
|
||||||
$('#toggle-collapse-all').on('click', function () {
|
$('#toggle-collapse-all').on('click', function () {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
const isCollapsed = parseInt($this.attr('data-collapsed'), 10) === 1;
|
const isCollapsed = parseInt($this.attr('data-collapsed'), 10) === 1;
|
||||||
@@ -167,8 +169,11 @@ define('admin/manage/categories', [
|
|||||||
});
|
});
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
// const formData = modal.find('form').serializeObject();
|
const formData = modal.find('form').serializeObject();
|
||||||
|
api.post('/api/admin/manage/categories', formData).then(() => {
|
||||||
|
ajaxify.refresh();
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
|
}).catch(alerts.error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +181,11 @@ define('admin/manage/categories', [
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Categories.removeCategory = function () {
|
||||||
|
const cid = this.getAttribute('data-cid');
|
||||||
|
api.del(`/api/admin/manage/categories/${encodeURIComponent(cid)}`).then(ajaxify.refresh);
|
||||||
|
};
|
||||||
|
|
||||||
Categories.create = function (payload) {
|
Categories.create = function (payload) {
|
||||||
api.post('/categories', payload, function (err, data) {
|
api.post('/categories', payload, function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
@@ -212,7 +222,7 @@ define('admin/manage/categories', [
|
|||||||
|
|
||||||
Categories.toggle = function (cids, disabled) {
|
Categories.toggle = function (cids, disabled) {
|
||||||
const listEl = document.querySelector('.categories [data-cid="0"]');
|
const listEl = document.querySelector('.categories [data-cid="0"]');
|
||||||
Promise.all(cids.map(cid => api.put('/categories/' + cid, {
|
Promise.all(cids.map(cid => api.put('/categories/' + encodeURIComponent(cid), {
|
||||||
disabled: disabled ? 1 : 0,
|
disabled: disabled ? 1 : 0,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const categoryEl = listEl.querySelector(`li[data-cid="${cid}"]`);
|
const categoryEl = listEl.querySelector(`li[data-cid="${cid}"]`);
|
||||||
@@ -264,7 +274,7 @@ define('admin/manage/categories', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
newCategoryId = -1;
|
newCategoryId = -1;
|
||||||
api.put('/categories/' + cid, modified[cid]).catch(alerts.error);
|
api.put('/categories/' + encodeURIComponent(cid), modified[cid]).catch(alerts.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,11 @@ activitypubApi.create.privateNote = enabledCheck(async (caller, { messageObj })
|
|||||||
activitypubApi.update = {};
|
activitypubApi.update = {};
|
||||||
|
|
||||||
activitypubApi.update.profile = enabledCheck(async (caller, { uid }) => {
|
activitypubApi.update.profile = enabledCheck(async (caller, { uid }) => {
|
||||||
|
// Local users only
|
||||||
|
if (!utils.isNumber(uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [object, targets] = await Promise.all([
|
const [object, targets] = await Promise.all([
|
||||||
activitypub.mocks.actors.user(uid),
|
activitypub.mocks.actors.user(uid),
|
||||||
db.getSortedSetMembers(`followersRemote:${caller.uid}`),
|
db.getSortedSetMembers(`followersRemote:${caller.uid}`),
|
||||||
@@ -203,6 +208,11 @@ activitypubApi.update.profile = enabledCheck(async (caller, { uid }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
activitypubApi.update.category = enabledCheck(async (caller, { cid }) => {
|
activitypubApi.update.category = enabledCheck(async (caller, { cid }) => {
|
||||||
|
// Local categories only
|
||||||
|
if (!utils.isNumber(cid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [object, targets] = await Promise.all([
|
const [object, targets] = await Promise.all([
|
||||||
activitypub.mocks.actors.category(cid),
|
activitypub.mocks.actors.category(cid),
|
||||||
activitypub.notes.getCategoryFollowers(cid),
|
activitypub.notes.getCategoryFollowers(cid),
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Categories.getAllCidsFromSet = async function (key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cids = await db.getSortedSetRange(key, 0, -1);
|
cids = await db.getSortedSetRange(key, 0, -1);
|
||||||
cids = cids.map(cid => parseInt(cid, 10));
|
cids = cids.map(cid => utils.isNumber(cid) ? parseInt(cid, 10) : cid);
|
||||||
cache.set(key, cids);
|
cache.set(key, cids);
|
||||||
return cids.slice();
|
return cids.slice();
|
||||||
};
|
};
|
||||||
@@ -274,7 +274,7 @@ Categories.getChildrenTree = getChildrenTree;
|
|||||||
Categories.getParentCids = async function (currentCid) {
|
Categories.getParentCids = async function (currentCid) {
|
||||||
let cid = currentCid;
|
let cid = currentCid;
|
||||||
const parents = [];
|
const parents = [];
|
||||||
while (parseInt(cid, 10)) {
|
while (utils.isNumber(cid) ? parseInt(cid, 10) : cid) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
cid = await Categories.getCategoryField(cid, 'parentCid');
|
cid = await Categories.getCategoryField(cid, 'parentCid');
|
||||||
if (cid) {
|
if (cid) {
|
||||||
@@ -289,12 +289,12 @@ Categories.getChildrenCids = async function (rootCid) {
|
|||||||
async function recursive(keys) {
|
async function recursive(keys) {
|
||||||
let childrenCids = await db.getSortedSetRange(keys, 0, -1);
|
let childrenCids = await db.getSortedSetRange(keys, 0, -1);
|
||||||
|
|
||||||
childrenCids = childrenCids.filter(cid => !allCids.includes(parseInt(cid, 10)));
|
childrenCids = childrenCids.filter(cid => !allCids.includes(utils.isNumber(cid) ? parseInt(cid, 10) : cid));
|
||||||
if (!childrenCids.length) {
|
if (!childrenCids.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
keys = childrenCids.map(cid => `cid:${cid}:children`);
|
keys = childrenCids.map(cid => `cid:${cid}:children`);
|
||||||
childrenCids.forEach(cid => allCids.push(parseInt(cid, 10)));
|
childrenCids.forEach(cid => allCids.push(utils.isNumber(cid) ? parseInt(cid, 10) : cid));
|
||||||
await recursive(keys);
|
await recursive(keys);
|
||||||
}
|
}
|
||||||
const key = `cid:${rootCid}:children`;
|
const key = `cid:${rootCid}:children`;
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ module.exports = function (Categories) {
|
|||||||
return await updateOrder(cid, value);
|
return await updateOrder(cid, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.setObjectField(`category:${cid}`, key, value);
|
await db.setObjectField(`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, key, value);
|
||||||
if (key === 'description') {
|
if (key === 'description') {
|
||||||
await Categories.parseDescription(cid, value);
|
await Categories.parseDescription(cid, value);
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ module.exports = function (Categories) {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
db.sortedSetRemove(`cid:${oldParent}:children`, cid),
|
db.sortedSetRemove(`cid:${oldParent}:children`, cid),
|
||||||
db.sortedSetAdd(`cid:${newParent}:children`, categoryData.order, cid),
|
db.sortedSetAdd(`cid:${newParent}:children`, categoryData.order, cid),
|
||||||
db.setObjectField(`category:${cid}`, 'parentCid', newParent),
|
db.setObjectField(`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, 'parentCid', newParent),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
cache.del([
|
cache.del([
|
||||||
@@ -104,8 +104,12 @@ module.exports = function (Categories) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateOrder(cid, order) {
|
async function updateOrder(cid, order) {
|
||||||
const parentCid = await Categories.getCategoryField(cid, 'parentCid');
|
const parentCid = (await Categories.getCategoryField(cid, 'parentCid')) || 0;
|
||||||
|
const isLocal = utils.isNumber(cid);
|
||||||
|
|
||||||
|
if (isLocal) {
|
||||||
await db.sortedSetsAdd('categories:cid', order, cid);
|
await db.sortedSetsAdd('categories:cid', order, cid);
|
||||||
|
}
|
||||||
|
|
||||||
const childrenCids = await db.getSortedSetRange(
|
const childrenCids = await db.getSortedSetRange(
|
||||||
`cid:${parentCid}:children`, 0, -1
|
`cid:${parentCid}:children`, 0, -1
|
||||||
@@ -128,7 +132,7 @@ module.exports = function (Categories) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await db.setObjectBulk(
|
await db.setObjectBulk(
|
||||||
childrenCids.map((cid, index) => [`category:${cid}`, { order: index + 1 }])
|
childrenCids.map((cid, index) => [`${utils.isNumber(cid) ? 'category' : 'categoryRemote'}:${cid}`, { order: index + 1 }])
|
||||||
);
|
);
|
||||||
|
|
||||||
cache.del([
|
cache.del([
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const meta = require('../../meta');
|
|||||||
const activitypub = require('../../activitypub');
|
const activitypub = require('../../activitypub');
|
||||||
const helpers = require('../helpers');
|
const helpers = require('../helpers');
|
||||||
const pagination = require('../../pagination');
|
const pagination = require('../../pagination');
|
||||||
|
const utils = require('../../utils');
|
||||||
|
const cache = require('../../cache');
|
||||||
|
|
||||||
const categoriesController = module.exports;
|
const categoriesController = module.exports;
|
||||||
|
|
||||||
@@ -48,14 +50,14 @@ categoriesController.get = async function (req, res, next) {
|
|||||||
|
|
||||||
categoriesController.getAll = async function (req, res) {
|
categoriesController.getAll = async function (req, res) {
|
||||||
const rootCid = parseInt(req.query.cid, 10) || 0;
|
const rootCid = parseInt(req.query.cid, 10) || 0;
|
||||||
async function getRootAndChildren() {
|
|
||||||
const rootChildren = await categories.getAllCidsFromSet(`cid:${rootCid}:children`);
|
const rootChildren = await categories.getAllCidsFromSet(`cid:${rootCid}:children`);
|
||||||
|
async function getRootAndChildren() {
|
||||||
const childCids = _.flatten(await Promise.all(rootChildren.map(cid => categories.getChildrenCids(cid))));
|
const childCids = _.flatten(await Promise.all(rootChildren.map(cid => categories.getChildrenCids(cid))));
|
||||||
return [rootCid].concat(rootChildren.concat(childCids));
|
return [rootCid].concat(rootChildren.concat(childCids));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Categories list will be rendered on client side with recursion, etc.
|
// Categories list will be rendered on client side with recursion, etc.
|
||||||
const cids = await (rootCid ? getRootAndChildren() : categories.getAllCidsFromSet('categories:cid'));
|
const cids = await getRootAndChildren();
|
||||||
|
|
||||||
let rootParent = 0;
|
let rootParent = 0;
|
||||||
if (rootCid) {
|
if (rootCid) {
|
||||||
@@ -67,9 +69,16 @@ categoriesController.getAll = async function (req, res) {
|
|||||||
'order', 'color', 'bgColor', 'backgroundImage', 'imageClass',
|
'order', 'color', 'bgColor', 'backgroundImage', 'imageClass',
|
||||||
'subCategoriesPerPage', 'description',
|
'subCategoriesPerPage', 'description',
|
||||||
];
|
];
|
||||||
const categoriesData = await categories.getCategoriesFields(cids, fields);
|
let categoriesData = await categories.getCategoriesFields(cids, fields);
|
||||||
const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields });
|
({ categories: categoriesData } = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields }));
|
||||||
let tree = categories.getTree(result.categories, rootParent);
|
|
||||||
|
// Append remote categories
|
||||||
|
categoriesData = categoriesData.map((category) => {
|
||||||
|
category.isLocal = utils.isNumber(category.cid);
|
||||||
|
return category;
|
||||||
|
});
|
||||||
|
|
||||||
|
let tree = categories.getTree(categoriesData, rootParent);
|
||||||
const cidsCount = rootCid && tree[0] ? tree[0].children.length : tree.length;
|
const cidsCount = rootCid && tree[0] ? tree[0].children.length : tree.length;
|
||||||
|
|
||||||
const pageCount = Math.max(1, Math.ceil(cidsCount / meta.config.categoriesPerPage));
|
const pageCount = Math.max(1, Math.ceil(cidsCount / meta.config.categoriesPerPage));
|
||||||
@@ -176,3 +185,41 @@ categoriesController.getFederation = async function (req, res) {
|
|||||||
followers,
|
followers,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
categoriesController.addRemote = async function (req, res) {
|
||||||
|
let { handle, id } = req.body;
|
||||||
|
if (handle && !id) {
|
||||||
|
({ actorUri: id } = await activitypub.helpers.query(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
await activitypub.actors.assertGroup(id);
|
||||||
|
const exists = await categories.exists(id);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const score = await db.sortedSetCard('cid:0:children');
|
||||||
|
const order = score + 1; // order is 1-based lol
|
||||||
|
await Promise.all([
|
||||||
|
db.sortedSetAdd('cid:0:children', order, id),
|
||||||
|
categories.setCategoryField(id, 'order', order),
|
||||||
|
]);
|
||||||
|
cache.del('cid:0:children');
|
||||||
|
|
||||||
|
res.sendStatus(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
categoriesController.removeRemote = async function (req, res) {
|
||||||
|
if (utils.isNumber(req.params.cid)) {
|
||||||
|
return helpers.formatApiResponse(400, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.sortedSetRemove('cid:0:children', req.params.cid);
|
||||||
|
cache.del('cid:0:children');
|
||||||
|
res.sendStatus(200);
|
||||||
|
};
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ function apiRoutes(router, name, middleware, controllers) {
|
|||||||
router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV));
|
router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV));
|
||||||
router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics));
|
router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics));
|
||||||
router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump));
|
router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump));
|
||||||
|
router.post(`/api/${name}/manage/categories`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.categories.addRemote));
|
||||||
|
router.delete(`/api/${name}/manage/categories/:cid`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.categories.removeRemote));
|
||||||
|
|
||||||
const multer = require('multer');
|
const multer = require('multer');
|
||||||
const storage = multer.diskStorage({});
|
const storage = multer.diskStorage({});
|
||||||
|
|||||||
12
src/views/admin/partials/categories/add.tpl
Normal file
12
src/views/admin/partials/categories/add.tpl
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<form type="form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="handle">[[admin/manage/categories:handle]]</label>
|
||||||
|
<input type="text" class="form-control" name="handle" id="handle" placeholder="handle@example.org" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label" for="id">[[admin/manage/categories:id]]</label>
|
||||||
|
<input type="text" class="form-control" name="id" id="id" placeholder="https://example.org/category/1" />
|
||||||
|
<p class="form-text">[[admin/manage/categories:alert.add-help]]</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -24,9 +24,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-shrink-0 d-flex gap-1 align-items-start">
|
<div class="flex-shrink-0 d-flex gap-1 align-items-start">
|
||||||
<a href="{{{if ./link}}}{./link}{{{else}}}{config.relative_path}/category/{./cid}{{{end}}}" class="btn btn-light btn-sm d-none d-sm-block" target="_blank">[[admin/admin:view]]</a>
|
<a href="{{{if ./link}}}{./link}{{{else}}}{config.relative_path}/category/{encodeURIComponent(./cid)}{{{end}}}" class="btn btn-light btn-sm d-none d-sm-block" target="_blank">[[admin/admin:view]]</a>
|
||||||
|
|
||||||
|
{{{ if ./isLocal }}}
|
||||||
<a href="./categories/{./cid}" class="btn btn-light btn-sm d-none d-sm-block">[[admin/manage/categories:edit]]</a>
|
<a href="./categories/{./cid}" class="btn btn-light btn-sm d-none d-sm-block">[[admin/manage/categories:edit]]</a>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
<div class="category-tools">
|
<div class="category-tools">
|
||||||
<button class="btn btn-light btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" type="button"><i class="fa fa-fw fa-gear text-primary"></i></button>
|
<button class="btn btn-light btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" type="button"><i class="fa fa-fw fa-gear text-primary"></i></button>
|
||||||
@@ -35,11 +37,13 @@
|
|||||||
<a href="{{{if ./link}}}{./link}{{{else}}}{config.relative_path}/category/{./cid}{{{end}}}" class="dropdown-item rounded-1 d-block d-sm-none" target="_blank" role="menuitem">[[admin/admin:view]]</a>
|
<a href="{{{if ./link}}}{./link}{{{else}}}{config.relative_path}/category/{./cid}{{{end}}}" class="dropdown-item rounded-1 d-block d-sm-none" target="_blank" role="menuitem">[[admin/admin:view]]</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{{{ if ./isLocal }}}
|
||||||
<li><a href="./categories/{./cid}" class="dropdown-item rounded-1 d-block d-sm-none" role="menuitem">[[admin/manage/categories:edit]]</a></li>
|
<li><a href="./categories/{./cid}" class="dropdown-item rounded-1 d-block d-sm-none" role="menuitem">[[admin/manage/categories:edit]]</a></li>
|
||||||
|
|
||||||
<li><a class="dropdown-item rounded-1" href="./categories/{categories.cid}/analytics" role="menuitem">[[admin/manage/categories:analytics]]</a></li>
|
<li><a class="dropdown-item rounded-1" href="./categories/{categories.cid}/analytics" role="menuitem">[[admin/manage/categories:analytics]]</a></li>
|
||||||
<li><a class="dropdown-item rounded-1" href="{config.relative_path}/admin/manage/privileges/{categories.cid}" role="menuitem">[[admin/manage/categories:privileges]]</a></li>
|
<li><a class="dropdown-item rounded-1" href="{config.relative_path}/admin/manage/privileges/{categories.cid}" role="menuitem">[[admin/manage/categories:privileges]]</a></li>
|
||||||
<li><a class="dropdown-item rounded-1" href="./categories/{categories.cid}/federation" role="menuitem">[[admin/manage/categories:federation]]</a></li>
|
<li><a class="dropdown-item rounded-1" href="./categories/{categories.cid}/federation" role="menuitem">[[admin/manage/categories:federation]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
<li><a href="#" class="set-order dropdown-item rounded-1" data-cid="{categories.cid}" data-order="{categories.order}" role="menuitem">[[admin/manage/categories:set-order]]</a></li>
|
<li><a href="#" class="set-order dropdown-item rounded-1" data-cid="{categories.cid}" data-order="{categories.order}" role="menuitem">[[admin/manage/categories:set-order]]</a></li>
|
||||||
<li class="dropdown-divider"></li>
|
<li class="dropdown-divider"></li>
|
||||||
<li>
|
<li>
|
||||||
@@ -51,6 +55,9 @@
|
|||||||
{{{end}}}
|
{{{end}}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{{ if !./isLocal }}}
|
||||||
|
<li><a class="dropdown-item rounded-1 text-danger" href="#" data-cid="{./cid}" data-action="remove" role="menuitem">[[admin/manage/categories:remove]]</a></li>
|
||||||
|
{{{ end }}}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user