feat: closes #13961, rename ban-reasons to custom reasons

use them for ban, mute and post queue depending on the type selected
if type is set to all, the reason is displayed in ban/mute and post queue
move reason label + dropdown + textarea  to a partial
This commit is contained in:
Barış Soner Uşaklı
2026-02-07 21:41:11 -05:00
parent 1d17352f67
commit 0eaf2beeb2
23 changed files with 266 additions and 194 deletions

View File

@@ -1266,60 +1266,60 @@ trans.vi = public/language/vi/admin/manage/uploads.json
trans.zh_CN = public/language/zh-CN/admin/manage/uploads.json trans.zh_CN = public/language/zh-CN/admin/manage/uploads.json
trans.zh_TW = public/language/zh-TW/admin/manage/uploads.json trans.zh_TW = public/language/zh-TW/admin/manage/uploads.json
[o:nodebb:p:nodebb:r:admin-manage-user-ban-reasons] [o:nodebb:p:nodebb:r:admin-manage-user-custom-reasons]
file_filter = public/language/<lang>/admin/manage/ban-reasons.json file_filter = public/language/<lang>/admin/manage/custom-reasons.json
source_file = public/language/en-GB/admin/manage/ban-reasons.json source_file = public/language/en-GB/admin/manage/custom-reasons.json
source_lang = en_GB source_lang = en_GB
type = KEYVALUEJSON type = KEYVALUEJSON
trans.ar = public/language/ar/admin/manage/ban-reasons.json trans.ar = public/language/ar/admin/manage/custom-reasons.json
trans.az = public/language/az/admin/manage/ban-reasons.json trans.az = public/language/az/admin/manage/custom-reasons.json
trans.bg = public/language/bg/admin/manage/ban-reasons.json trans.bg = public/language/bg/admin/manage/custom-reasons.json
trans.bn = public/language/bn/admin/manage/ban-reasons.json trans.bn = public/language/bn/admin/manage/custom-reasons.json
trans.cs = public/language/cs/admin/manage/ban-reasons.json trans.cs = public/language/cs/admin/manage/custom-reasons.json
trans.da = public/language/da/admin/manage/ban-reasons.json trans.da = public/language/da/admin/manage/custom-reasons.json
trans.de = public/language/de/admin/manage/ban-reasons.json trans.de = public/language/de/admin/manage/custom-reasons.json
trans.el = public/language/el/admin/manage/ban-reasons.json trans.el = public/language/el/admin/manage/custom-reasons.json
trans.en_US = public/language/en-US/admin/manage/ban-reasons.json trans.en_US = public/language/en-US/admin/manage/custom-reasons.json
trans.en@pirate = public/language/en-x-pirate/admin/manage/ban-reasons.json trans.en@pirate = public/language/en-x-pirate/admin/manage/custom-reasons.json
trans.es = public/language/es/admin/manage/ban-reasons.json trans.es = public/language/es/admin/manage/custom-reasons.json
trans.et = public/language/et/admin/manage/ban-reasons.json trans.et = public/language/et/admin/manage/custom-reasons.json
trans.fa_IR = public/language/fa-IR/admin/manage/ban-reasons.json trans.fa_IR = public/language/fa-IR/admin/manage/custom-reasons.json
trans.fi = public/language/fi/admin/manage/ban-reasons.json trans.fi = public/language/fi/admin/manage/custom-reasons.json
trans.fr = public/language/fr/admin/manage/ban-reasons.json trans.fr = public/language/fr/admin/manage/custom-reasons.json
trans.gl = public/language/gl/admin/manage/ban-reasons.json trans.gl = public/language/gl/admin/manage/custom-reasons.json
trans.he = public/language/he/admin/manage/ban-reasons.json trans.he = public/language/he/admin/manage/custom-reasons.json
trans.hr = public/language/hr/admin/manage/ban-reasons.json trans.hr = public/language/hr/admin/manage/custom-reasons.json
trans.hu = public/language/hu/admin/manage/ban-reasons.json trans.hu = public/language/hu/admin/manage/custom-reasons.json
trans.hy = public/language/hy/admin/manage/ban-reasons.json trans.hy = public/language/hy/admin/manage/custom-reasons.json
trans.id = public/language/id/admin/manage/ban-reasons.json trans.id = public/language/id/admin/manage/custom-reasons.json
trans.it = public/language/it/admin/manage/ban-reasons.json trans.it = public/language/it/admin/manage/custom-reasons.json
trans.ja = public/language/ja/admin/manage/ban-reasons.json trans.ja = public/language/ja/admin/manage/custom-reasons.json
trans.ko = public/language/ko/admin/manage/ban-reasons.json trans.ko = public/language/ko/admin/manage/custom-reasons.json
trans.lt = public/language/lt/admin/manage/ban-reasons.json trans.lt = public/language/lt/admin/manage/custom-reasons.json
trans.lv = public/language/lv/admin/manage/ban-reasons.json trans.lv = public/language/lv/admin/manage/custom-reasons.json
trans.ms = public/language/ms/admin/manage/ban-reasons.json trans.ms = public/language/ms/admin/manage/custom-reasons.json
trans.nb = public/language/nb/admin/manage/ban-reasons.json trans.nb = public/language/nb/admin/manage/custom-reasons.json
trans.nl = public/language/nl/admin/manage/ban-reasons.json trans.nl = public/language/nl/admin/manage/custom-reasons.json
trans.nn_NO = public/language/nn-NO/admin/manage/ban-reasons.json trans.nn_NO = public/language/nn-NO/admin/manage/custom-reasons.json
trans.pl = public/language/pl/admin/manage/ban-reasons.json trans.pl = public/language/pl/admin/manage/custom-reasons.json
trans.pt_BR = public/language/pt-BR/admin/manage/ban-reasons.json trans.pt_BR = public/language/pt-BR/admin/manage/custom-reasons.json
trans.pt_PT = public/language/pt-PT/admin/manage/ban-reasons.json trans.pt_PT = public/language/pt-PT/admin/manage/custom-reasons.json
trans.ro = public/language/ro/admin/manage/ban-reasons.json trans.ro = public/language/ro/admin/manage/custom-reasons.json
trans.ru = public/language/ru/admin/manage/ban-reasons.json trans.ru = public/language/ru/admin/manage/custom-reasons.json
trans.rw = public/language/rw/admin/manage/ban-reasons.json trans.rw = public/language/rw/admin/manage/custom-reasons.json
trans.sc = public/language/sc/admin/manage/ban-reasons.json trans.sc = public/language/sc/admin/manage/custom-reasons.json
trans.sk = public/language/sk/admin/manage/ban-reasons.json trans.sk = public/language/sk/admin/manage/custom-reasons.json
trans.sl = public/language/sl/admin/manage/ban-reasons.json trans.sl = public/language/sl/admin/manage/custom-reasons.json
trans.sq_AL = public/language/sq-AL/admin/manage/ban-reasons.json trans.sq_AL = public/language/sq-AL/admin/manage/custom-reasons.json
trans.sr = public/language/sr/admin/manage/ban-reasons.json trans.sr = public/language/sr/admin/manage/custom-reasons.json
trans.sv = public/language/sv/admin/manage/ban-reasons.json trans.sv = public/language/sv/admin/manage/custom-reasons.json
trans.th = public/language/th/admin/manage/ban-reasons.json trans.th = public/language/th/admin/manage/custom-reasons.json
trans.tr = public/language/tr/admin/manage/ban-reasons.json trans.tr = public/language/tr/admin/manage/custom-reasons.json
trans.uk = public/language/uk/admin/manage/ban-reasons.json trans.uk = public/language/uk/admin/manage/custom-reasons.json
trans.ur = public/language/ur/admin/manage/ban-reasons.json trans.ur = public/language/ur/admin/manage/custom-reasons.json
trans.vi = public/language/vi/admin/manage/ban-reasons.json trans.vi = public/language/vi/admin/manage/custom-reasons.json
trans.zh_CN = public/language/zh-CN/admin/manage/ban-reasons.json trans.zh_CN = public/language/zh-CN/admin/manage/custom-reasons.json
trans.zh_TW = public/language/zh-TW/admin/manage/ban-reasons.json trans.zh_TW = public/language/zh-TW/admin/manage/custom-reasons.json
[o:nodebb:p:nodebb:r:admin-manage-user-custom-fields] [o:nodebb:p:nodebb:r:admin-manage-user-custom-fields]
file_filter = public/language/<lang>/admin/manage/user-custom-fields.json file_filter = public/language/<lang>/admin/manage/user-custom-fields.json

View File

@@ -1,9 +0,0 @@
{
"title": "Manage Ban Reasons",
"create-reason": "Create Reason",
"edit-reason": "Edit Reason",
"reason-title": "Title",
"reason-body": "Body",
"ban-reasons-saved": "Ban reasons saved successfully",
"delete-reason-confirm-x": "Are you sure you want to delete the ban reason with the title <strong>%1</strong>?"
}

View File

@@ -0,0 +1,16 @@
{
"title": "Manage Custom Reasons",
"create-reason": "Create Reason",
"edit-reason": "Edit Reason",
"reasons-help": "Reasons are predefined explanations used when banning or muting users, or when rejecting posts in the post queue.",
"reason-title": "Title",
"reason-type": "Type",
"reason-body": "Body",
"reason-all": "All",
"reason-ban": "Ban",
"reason-mute": "Mute",
"reason-post-queue": "Post Queue",
"reason-type-help": "The type of action this reason applies to. If 'All' is selected, this reason will be available for all action types.",
"custom-reasons-saved": "Custom reasons saved successfully",
"delete-reason-confirm-x": "Are you sure you want to delete the custom reason with the title <strong>%1</strong>?"
}

View File

@@ -23,7 +23,7 @@
"purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>", "purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>",
"download-csv": "Download CSV", "download-csv": "Download CSV",
"custom-user-fields": "Custom User Fields", "custom-user-fields": "Custom User Fields",
"ban-reasons": "Ban Reasons", "custom-reasons": "Custom Reasons",
"manage-groups": "Manage Groups", "manage-groups": "Manage Groups",
"set-reputation": "Set Reputation", "set-reputation": "Set Reputation",
"add-group": "Add Group", "add-group": "Add Group",

View File

@@ -78,6 +78,7 @@
"users-csv-exported": "Users csv exported, click to download", "users-csv-exported": "Users csv exported, click to download",
"post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.",
"post-queue-rejected": "Your queued post has been rejected.", "post-queue-rejected": "Your queued post has been rejected.",
"post-queue-rejected-for-reason": "Your queued post has been rejected for the following reason: \"%1\"",
"post-queue-notify": "Queued post received a notification: \"%1\"", "post-queue-notify": "Queued post received a notification: \"%1\"",
"email-confirmed": "Email Confirmed", "email-confirmed": "Email Confirmed",

View File

@@ -260,7 +260,7 @@ define('admin/manage/users', [
alerts.error('[[error:no-users-selected]]'); alerts.error('[[error:no-users-selected]]');
return false; // specifically to keep the menu open return false; // specifically to keep the menu open
} }
const reasons = await socket.emit('user.getBanReasons'); const reasons = await socket.emit('user.getCustomReasons', { type: 'ban' });
const html = await app.parseAndTranslate('modals/temporary-ban', { reasons }); const html = await app.parseAndTranslate('modals/temporary-ban', { reasons });
const modal = bootbox.dialog({ const modal = bootbox.dialog({
title: '[[user:ban-account]]', title: '[[user:ban-account]]',

View File

@@ -1,9 +1,9 @@
define('admin/manage/user/ban-reasons', [ define('admin/manage/user/custom-reasons', [
'benchpress', 'bootbox', 'alerts', 'translator', 'jquery-ui/widgets/sortable', 'benchpress', 'bootbox', 'alerts', 'translator', 'jquery-ui/widgets/sortable',
], function (benchpress, bootbox, alerts, translator) { ], function (benchpress, bootbox, alerts, translator) {
const manageBanReasons = {}; const manageCustomReasons = {};
manageBanReasons.init = function () { manageCustomReasons.init = function () {
const table = $('table'); const table = $('table');
$('#new').on('click', () => showModal()); $('#new').on('click', () => showModal());
@@ -16,7 +16,7 @@ define('admin/manage/user/ban-reasons', [
table.on('click', '[data-action="delete"]', function () { table.on('click', '[data-action="delete"]', function () {
const row = $(this).parents('[data-key]'); const row = $(this).parents('[data-key]');
const title = row.attr('data-title'); const title = row.attr('data-title');
bootbox.confirm(`[[admin/manage/ban-reasons:delete-reason-confirm-x, "${title}"]]`, function (ok) { bootbox.confirm(`[[admin/manage/custom-reasons:delete-reason-confirm-x, "${title}"]]`, function (ok) {
if (!ok) { if (!ok) {
return; return;
} }
@@ -35,11 +35,11 @@ define('admin/manage/user/ban-reasons', [
$('tbody tr[data-key]').each((index, el) => { $('tbody tr[data-key]').each((index, el) => {
reasons.push(getDataFromEl($(el))); reasons.push(getDataFromEl($(el)));
}); });
socket.emit('admin.user.saveBanReasons', reasons, function (err) { socket.emit('admin.user.saveCustomReasons', reasons, function (err) {
if (err) { if (err) {
return alerts.error(err); return alerts.error(err);
} }
alerts.success('[[admin/manage/ban-reasons:ban-reasons-saved]]'); alerts.success('[[admin/manage/custom-reasons:custom-reasons-saved]]');
}); });
}); });
}; };
@@ -48,18 +48,19 @@ define('admin/manage/user/ban-reasons', [
return { return {
key: el.attr('data-key'), key: el.attr('data-key'),
title: el.attr('data-title'), title: el.attr('data-title'),
type: el.attr('data-type'),
body: el.attr('data-body'), body: el.attr('data-body'),
}; };
} }
async function showModal(reason = null) { async function showModal(reason = null) {
const html = await benchpress.render('admin/partials/manage-ban-reasons-modal', reason); const html = await benchpress.render('admin/partials/manage-custom-reasons-modal', reason);
const modal = bootbox.dialog({ const modal = bootbox.dialog({
message: html, message: html,
onEscape: true, onEscape: true,
title: reason ? title: reason ?
'[[admin/manage/ban-reasons:edit-reason]]' : '[[admin/manage/custom-reasons:edit-reason]]' :
'[[admin/manage/ban-reasons:create-reason]]', '[[admin/manage/custom-reasons:create-reason]]',
buttons: { buttons: {
submit: { submit: {
label: '[[global:save]]', label: '[[global:save]]',
@@ -69,7 +70,7 @@ define('admin/manage/user/ban-reasons', [
formData.body = translator.escape(formData.body); formData.body = translator.escape(formData.body);
formData.parsedBody = translator.escape(await socket.emit('admin.parseRaw', formData.body)); formData.parsedBody = translator.escape(await socket.emit('admin.parseRaw', formData.body));
app.parseAndTranslate('admin/manage/users/ban-reasons', 'reasons', { app.parseAndTranslate('admin/manage/users/custom-reasons', 'reasons', {
reasons: [formData], reasons: [formData],
}, (html) => { }, (html) => {
if (reason) { if (reason) {
@@ -90,7 +91,7 @@ define('admin/manage/user/ban-reasons', [
} }
return manageBanReasons; return manageCustomReasons;
}); });

View File

@@ -2,11 +2,11 @@
define('forum/post-queue', [ define('forum/post-queue', [
'categoryFilter', 'categorySelector', 'api', 'alerts', 'bootbox', 'categoryFilter', 'categorySelector', 'api', 'alerts', 'translator',
'accounts/moderate', 'accounts/delete', 'bootbox', 'accounts/moderate', 'accounts/delete',
], function ( ], function (
categoryFilter, categorySelector, api, alerts, bootbox, categoryFilter, categorySelector, api, alerts, translator,
AccountModerate, AccountsDelete bootbox, AccountModerate, AccountsDelete
) { ) {
const PostQueue = {}; const PostQueue = {};
@@ -158,70 +158,122 @@ define('forum/post-queue', [
AccountsDelete.purge(uid, ajaxify.go.bind(null, 'post-queue')); AccountsDelete.purge(uid, ajaxify.go.bind(null, 'post-queue'));
break; break;
default: case 'accept':
handleQueueActions.call(e.target); handleAccept(subselector);
break; break;
case 'reject':
handleReject(subselector);
break;
case 'notify':
handleNotify(subselector);
break;
default:
throw new Error(`Unknown action: ${action}`);
} }
} }
}); });
} }
} }
async function handleQueueActions() { function handleAccept(btn) {
// accept, reject, notify const parent = $(btn).parents('[data-id]');
const parent = $(this).parents('[data-id]');
const action = $(this).attr('data-action');
const id = parent.attr('data-id'); const id = parent.attr('data-id');
const listContainer = parent.get(0).parentNode; doAction('accept', id).then(() => removePostQueueElement(parent)).catch(alerts.error);
}
if ((!['accept', 'reject', 'notify'].includes(action)) || async function handleReject(btn) {
(action === 'reject' && !await confirmReject(ajaxify.data.canAccept ? '[[post-queue:confirm-reject]]' : '[[post-queue:confirm-remove]]'))) { const parent = $(btn).parents('[data-id]');
const id = parent.attr('data-id');
const translationString = ajaxify.data.canAccept ?
'[[post-queue:confirm-reject]]' :
'[[post-queue:confirm-remove]]';
const message = await getMessage(translationString);
if (message === false) {
return;
}
doAction('reject', id, message).then(() => removePostQueueElement(parent)).catch(alerts.error);
}
function removePostQueueElement(parent) {
const listContainer = parent.get(0).parentNode;
parent.remove();
if (listContainer.childElementCount === 0) {
if (ajaxify.data.singlePost) {
ajaxify.go('/post-queue' + window.location.search);
} else {
ajaxify.refresh();
}
}
}
async function handleNotify(btn) {
const parent = $(btn).parents('[data-id]');
const id = parent.attr('data-id');
const message = await getMessage('[[post-queue:notify-user]]');
if (message === false) {
return; return;
} }
doAction(action, id).then(function () { doAction('notify', id, message).catch(alerts.error);
if (action === 'accept' || action === 'reject') {
parent.remove();
}
if (listContainer.childElementCount === 0) {
if (ajaxify.data.singlePost) {
ajaxify.go('/post-queue' + window.location.search);
} else {
ajaxify.refresh();
}
}
}).catch(alerts.error);
return false;
} }
async function doAction(action, id) { async function getMessage(title) {
function getMessage() { const reasons = await socket.emit('user.getCustomReasons', { type: 'post-queue' });
return new Promise((resolve) => { const html = await app.parseAndTranslate('partials/custom-reason', { reasons });
const modal = bootbox.dialog({
title: '[[post-queue:notify-user]]', return new Promise((resolve) => {
message: '<textarea class="form-control"></textarea>', let resolved = false;
buttons: { const done = (value) => {
OK: { if (resolved) {
label: '[[modules:bootbox.send]]', return;
callback: function () { }
const val = modal.find('textarea').val(); resolved = true;
if (val) { resolve(value);
resolve(val); };
}
}, const modal = bootbox.dialog({
title: title,
message: `<form class="form">${html.html()}</form>`,
show: true,
onEscape: true,
buttons: {
close: {
label: '[[global:close]]',
className: 'btn-link',
callback: function () {
done(false);
}, },
}, },
}); submit: {
label: '[[modules:bootbox.confirm]]',
callback: function () {
done(modal.find('[name="reason"]').val());
},
},
},
}); });
}
modal.on('hidden.bs.modal', () => {
done(false);
});
modal.find('[data-key]').on('click', function () {
const reason = reasons.find(r => String(r.key) === $(this).attr('data-key'));
if (reason && reason.body) {
modal.find('[name="reason"]').val(translator.unescape(reason.body));
}
});
});
}
async function doAction(action, id, message = '') {
const actionsMap = { const actionsMap = {
accept: () => api.post(`/posts/queue/${id}`, {}), accept: () => api.post(`/posts/queue/${id}`, {}),
reject: () => api.del(`/posts/queue/${id}`, {}), reject: () => api.del(`/posts/queue/${id}`, { message }),
notify: async () => api.post(`/posts/queue/${id}/notify`, { message: await getMessage() }), notify: () => api.post(`/posts/queue/${id}/notify`, { message }),
}; };
if (actionsMap[action]) { if (actionsMap[action]) {
const result = actionsMap[action](); const result = actionsMap[action]();

View File

@@ -14,6 +14,7 @@ define('forum/account/moderate', [
throwModal({ throwModal({
tpl: 'modals/temporary-ban', tpl: 'modals/temporary-ban',
title: '[[user:ban-account]]', title: '[[user:ban-account]]',
type: 'ban',
onSubmit: function (formData) { onSubmit: function (formData) {
const until = formData.length > 0 ? ( const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
@@ -36,6 +37,7 @@ define('forum/account/moderate', [
throwModal({ throwModal({
tpl: 'modals/unban', tpl: 'modals/unban',
title: '[[user:unban-account]]', title: '[[user:unban-account]]',
type: 'ban',
onSubmit: function (formData) { onSubmit: function (formData) {
api.del('/users/' + encodeURIComponent(theirid) + '/ban', { api.del('/users/' + encodeURIComponent(theirid) + '/ban', {
reason: formData.reason || '', reason: formData.reason || '',
@@ -51,6 +53,7 @@ define('forum/account/moderate', [
throwModal({ throwModal({
tpl: 'modals/temporary-mute', tpl: 'modals/temporary-mute',
title: '[[user:mute-account]]', title: '[[user:mute-account]]',
type: 'mute',
onSubmit: function (formData) { onSubmit: function (formData) {
const until = formData.length > 0 ? ( const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
@@ -84,7 +87,7 @@ define('forum/account/moderate', [
}; };
async function throwModal(options) { async function throwModal(options) {
const reasons = await socket.emit('user.getBanReasons'); const reasons = await socket.emit('user.getCustomReasons', { type: options.type || '' });
const html = await app.parseAndTranslate(options.tpl, { reasons }); const html = await app.parseAndTranslate(options.tpl, { reasons });
const modal = bootbox.dialog({ const modal = bootbox.dialog({
title: options.title, title: options.title,

View File

@@ -592,7 +592,10 @@ postsAPI.removeQueuedPost = async (caller, data) => {
await canEditQueue(caller.uid, data, 'reject'); await canEditQueue(caller.uid, data, 'reject');
const result = await posts.removeFromQueue(data.id); const result = await posts.removeFromQueue(data.id);
if (result && caller.uid !== parseInt(result.uid, 10)) { if (result && caller.uid !== parseInt(result.uid, 10)) {
await sendQueueNotification('post-queue-rejected', result.uid, '/'); const msg = validator.escape(String(data.message ? data.message : ''));
await sendQueueNotification(
msg ? 'post-queue-rejected-for-reason' : 'post-queue-rejected', result.uid, '/', msg
);
} }
await logQueueEvent(caller, result, 'reject'); await logQueueEvent(caller, result, 'reject');
}; };
@@ -612,7 +615,7 @@ postsAPI.notifyQueuedPostOwner = async (caller, data) => {
await canEditQueue(caller.uid, data, 'notify'); await canEditQueue(caller.uid, data, 'notify');
const result = await posts.getFromQueue(data.id); const result = await posts.getFromQueue(data.id);
if (result) { if (result) {
await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message))); await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message || '')));
} }
}; };

View File

@@ -316,6 +316,6 @@ usersController.customFields = async function (req, res) {
}; };
usersController.banReasons = async function (req, res) { usersController.banReasons = async function (req, res) {
const reasons = await user.bans.getBanReasons(); const reasons = await user.bans.getCustomReasons();
res.render('admin/manage/users/ban-reasons', { reasons }); res.render('admin/manage/users/custom-reasons', { reasons });
}; };

View File

@@ -196,7 +196,7 @@ Posts.acceptQueuedPost = async (req, res) => {
}; };
Posts.removeQueuedPost = async (req, res) => { Posts.removeQueuedPost = async (req, res) => {
await api.posts.removeQueuedPost(req, { id: req.params.id }); await api.posts.removeQueuedPost(req, { id: req.params.id, message: req.body.message });
helpers.formatApiResponse(200, res); helpers.formatApiResponse(200, res);
}; };

View File

@@ -23,7 +23,7 @@ module.exports = function (app, name, middleware, controllers) {
helpers.setupAdminPageRoute(app, `/${name}/manage/users`, middlewares, controllers.admin.users.index); helpers.setupAdminPageRoute(app, `/${name}/manage/users`, middlewares, controllers.admin.users.index);
helpers.setupAdminPageRoute(app, `/${name}/manage/users/custom-fields`, middlewares, controllers.admin.users.customFields); helpers.setupAdminPageRoute(app, `/${name}/manage/users/custom-fields`, middlewares, controllers.admin.users.customFields);
helpers.setupAdminPageRoute(app, `/${name}/manage/users/ban-reasons`, middlewares, controllers.admin.users.banReasons); helpers.setupAdminPageRoute(app, `/${name}/manage/users/custom-reasons`, middlewares, controllers.admin.users.banReasons);
helpers.setupAdminPageRoute(app, `/${name}/manage/registration`, middlewares, controllers.admin.users.registrationQueue); helpers.setupAdminPageRoute(app, `/${name}/manage/registration`, middlewares, controllers.admin.users.registrationQueue);
helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get); helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get);

View File

@@ -219,11 +219,11 @@ User.saveCustomFields = async function (socket, fields) {
await user.reloadCustomFieldWhitelist(); await user.reloadCustomFieldWhitelist();
}; };
User.saveBanReasons = async function (socket, reasons) { User.saveCustomReasons = async function (socket, reasons) {
const keys = await db.getSortedSetRange('ban-reasons', 0, -1); const keys = await db.getSortedSetRange('custom-reasons', 0, -1);
await db.delete('ban-reasons'); await db.delete('custom-reasons');
await db.deleteAll(keys.map(k => `ban-reason:${k}`)); await db.deleteAll(keys.map(k => `custom-reason:${k}`));
const ids = reasons.map((f, i) => i); const ids = reasons.map((f, i) => i);
await db.sortedSetAdd(`ban-reasons`, ids, ids); await db.sortedSetAdd(`custom-reasons`, ids, ids);
await db.setObjectBulk(reasons.map((reason, i) => [`ban-reason:${i}`, reason])); await db.setObjectBulk(reasons.map((reason, i) => [`custom-reason:${i}`, reason]));
}; };

View File

@@ -174,12 +174,12 @@ SocketUser.editModerationNote = async function (socket, data) {
return await user.getModerationNotesByIds(data.uid, [data.id]); return await user.getModerationNotesByIds(data.uid, [data.id]);
}; };
SocketUser.getBanReasons = async function (socket) { SocketUser.getCustomReasons = async function (socket, { type }) {
const canBan = await privileges.users.hasBanPrivilege(socket.uid); const canBan = await privileges.users.hasBanPrivilege(socket.uid);
if (!canBan) { if (!canBan) {
throw new Error('[[error:no-privileges]]'); throw new Error('[[error:no-privileges]]');
} }
return await user.bans.getBanReasons(); return await user.bans.getCustomReasons({ type });
}; };
SocketUser.deleteUpload = async function (socket, data) { SocketUser.deleteUpload = async function (socket, data) {

View File

@@ -165,14 +165,18 @@ module.exports = function (User) {
return banObj && banObj.reason ? banObj.reason : ''; return banObj && banObj.reason ? banObj.reason : '';
}; };
User.bans.getBanReasons = async function () { User.bans.getCustomReasons = async function ({ type = '' } = {}) {
const keys = await db.getSortedSetRange('ban-reasons', 0, -1); const keys = await db.getSortedSetRange('custom-reasons', 0, -1);
const reasons = (await db.getObjects(keys.map(k => `ban-reason:${k}`))).filter(Boolean); type = type || '';
const reasons = (await db.getObjects(keys.map(k => `custom-reason:${k}`))).filter(Boolean);
await Promise.all(reasons.map(async (reason, i) => { await Promise.all(reasons.map(async (reason, i) => {
reason.key = i; reason.key = i;
reason.parsedBody = translator.escape(await plugins.hooks.fire('filter:parse.raw', reason.body || '')); reason.parsedBody = translator.escape(await plugins.hooks.fire('filter:parse.raw', reason.body || ''));
reason.body = translator.escape(reason.body); reason.body = translator.escape(reason.body);
})); }));
if (type !== '') {
return reasons.filter(reason => reason.type === type || reason.type === '');
}
return reasons; return reasons;
}; };
}; };

View File

@@ -83,7 +83,7 @@
<li><a target="_blank" href="#" class="dropdown-item rounded-1 export-csv" role="menuitem">[[admin/manage/users:download-csv]]</a></li> <li><a target="_blank" href="#" class="dropdown-item rounded-1 export-csv" role="menuitem">[[admin/manage/users:download-csv]]</a></li>
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/custom-fields">[[admin/manage/users:custom-user-fields]]</a> <li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/custom-fields">[[admin/manage/users:custom-user-fields]]</a>
</li> </li>
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/ban-reasons">[[admin/manage/users:ban-reasons]]</a> <li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/custom-reasons">[[admin/manage/users:custom-reasons]]</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@@ -1,16 +1,18 @@
<div class="manage-users d-flex flex-column gap-2 px-lg-4 h-100"> <div class="manage-users d-flex flex-column gap-2 px-lg-4 h-100">
<div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2"> <div class="d-flex border-bottom py-2 m-0 sticky-top acp-page-main-header align-items-center justify-content-between flex-wrap gap-2">
<div class=""> <div class="">
<h4 class="fw-bold tracking-tight mb-0">[[admin/manage/ban-reasons:title]]</h4> <h4 class="fw-bold tracking-tight mb-0">[[admin/manage/custom-reasons:title]]</h4>
</div> </div>
<div class="d-flex align-items-center gap-1"> <div class="d-flex align-items-center gap-1">
<button id="new" class="btn btn-light btn-sm text-nowrap" type="button"> <button id="new" class="btn btn-light btn-sm text-nowrap" type="button">
<i class="fa fa-fw fa-plus"></i> [[admin/manage/ban-reasons:create-reason]] <i class="fa fa-fw fa-plus"></i> [[admin/manage/custom-reasons:create-reason]]
</button> </button>
<button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary w-100 text-center text-nowrap">[[admin/admin:save-changes]]</button> <button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary w-100 text-center text-nowrap">[[admin/admin:save-changes]]</button>
</div> </div>
</div> </div>
<p class="text-secondary">[[admin/manage/custom-reasons:reasons-help]]</p>
<div class="row flex-grow-1"> <div class="row flex-grow-1">
<div class="col-lg-12 d-flex flex-column gap-2"> <div class="col-lg-12 d-flex flex-column gap-2">
<div class="table-responsive flex-grow-1"> <div class="table-responsive flex-grow-1">
@@ -18,18 +20,20 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th class="text-muted">[[admin/manage/ban-reasons:reason-title]]</th> <th class="text-muted">[[admin/manage/custom-reasons:reason-title]]</th>
<th class="text-muted">[[admin/manage/ban-reasons:reason-body]]</th> <th class="text-muted">[[admin/manage/custom-reasons:reason-type]]</th>
<th class="text-muted">[[admin/manage/custom-reasons:reason-body]]</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{{ each reasons }}} {{{ each reasons }}}
<tr data-key="{./key}" data-title="{./title}" data-body="{./body}"> <tr data-key="{./key}" data-title="{./title}" data-type="{./type}" data-body="{./body}">
<td class="" style="width: 32px;"> <td class="" style="width: 32px;">
<a href="#" component="sort/handle" class="btn btn-light btn-sm d-none d-md-block ui-sortable-handle" style="cursor:grab;"><i class="fa fa-arrows-up-down text-muted"></i></a> <a href="#" component="sort/handle" class="btn btn-light btn-sm d-none d-md-block ui-sortable-handle" style="cursor:grab;"><i class="fa fa-arrows-up-down text-muted"></i></a>
</td> </td>
<td class="text-nowrap ">{./title}</td> <td class="text-nowrap">{./title}</td>
<td class="text-nowrap">{{{ if ./type }}}[[admin/manage/custom-reasons:reason-{{./type}}]]{{{ else }}}[[admin/manage/custom-reasons:reason-all]]{{{ end }}}</td>
<td class="">{./parsedBody}</td> <td class="">{./parsedBody}</td>
<td class=""> <td class="">
<div class="d-flex justify-content-end gap-1"> <div class="d-flex justify-content-end gap-1">

View File

@@ -1,11 +0,0 @@
<form>
<div class="mb-3">
<label class="form-label">[[admin/manage/ban-reasons:reason-title]]</label>
<input class="form-control" type="text" name="title" value="{./title}">
</div>
<div class="mb-3">
<label class="form-label">[[admin/manage/ban-reasons:reason-body]]</label>
<textarea rows="8" class="form-control" type="text" name="body">{./body}</textarea>
</div>
</form>

View File

@@ -0,0 +1,22 @@
<form>
<div class="mb-3">
<label class="form-label">[[admin/manage/custom-reasons:reason-title]]</label>
<input class="form-control" type="text" name="title" value="{./title}">
</div>
<div class="mb-3">
<label class="form-label">[[admin/manage/custom-reasons:reason-type]]</label>
<select class="form-select" name="type">
<option value="" {{{ if (type == "") }}}selected{{{ end }}} >[[admin/manage/custom-reasons:reason-all]]</option>
<option value="ban" {{{ if (type == "ban") }}}selected{{{ end }}}>[[admin/manage/custom-reasons:reason-ban]]</option>
<option value="mute" {{{ if (type == "mute") }}}selected{{{ end }}}>[[admin/manage/custom-reasons:reason-mute]]</option>
<option value="post-queue" {{{ if (type == "post-queue") }}}selected{{{ end }}}>[[admin/manage/custom-reasons:reason-post-queue]]</option>
</select>
<p class="form-text">[[admin/manage/custom-reasons:reason-type-help]]</p>
</div>
<div class="mb-3">
<label class="form-label">[[admin/manage/custom-reasons:reason-body]]</label>
<textarea rows="8" class="form-control" type="text" name="body">{./body}</textarea>
</div>
</form>

View File

@@ -23,24 +23,7 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="mb-3"> <div class="mb-3">
<div class="d-flex align-items-center justify-content-between mb-2"> <!-- IMPORT partials/custom-reason.tpl -->
<label class="form-label mb-0" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
{{{ if reasons.length }}}
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" id="reasonDropdown" data-bs-toggle="dropdown" aria-expanded="false">
[[admin/manage/users:temp-ban.select-reason]]
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="reasonDropdown">
{{{ each reasons }}}
<li><a class="dropdown-item rounded-1" href="#" data-key="{./key}">{./title}</a></li>
{{{ end }}}
</ul>
</div>
{{{ end }}}
</div>
<textarea rows="8" type="text" class="form-control" id="reason" name="reason"></textarea>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -24,24 +24,7 @@
<div class="col-12"> <div class="col-12">
<div class="mb-3"> <div class="mb-3">
<div class="d-flex align-items-center justify-content-between mb-2"> <!-- IMPORT partials/custom-reason.tpl -->
<label class="form-label mb-0" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
{{{ if reasons.length }}}
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" id="reasonDropdown" data-bs-toggle="dropdown" aria-expanded="false">
[[admin/manage/users:temp-ban.select-reason]]
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="reasonDropdown">
{{{ each reasons }}}
<li><a class="dropdown-item rounded-1" href="#" data-key="{./key}">{./title}</a></li>
{{{ end }}}
</ul>
</div>
{{{ end }}}
</div>
<textarea rows="8" type="text" class="form-control" id="reason" name="reason"></textarea>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,20 @@
<div>
<div class="d-flex align-items-center justify-content-between mb-2">
<label class="form-label mb-0" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
{{{ if reasons.length }}}
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" id="reasonDropdown" data-bs-toggle="dropdown" aria-expanded="false">
[[admin/manage/users:temp-ban.select-reason]]
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-end p-1" aria-labelledby="reasonDropdown">
{{{ each reasons }}}
<li><a class="dropdown-item rounded-1" href="#" data-key="{./key}">{./title}</a></li>
{{{ end }}}
</ul>
</div>
{{{ end }}}
</div>
<textarea rows="8" type="text" class="form-control" id="reason" name="reason"></textarea>
</div>