mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-03-03 11:01:20 +01:00
feat: ban/mute reasons (#13960)
add acp page to create reasons add dropdown to insert them into reason change reason field into textarea translate and parse reason before sending ban email
This commit is contained in:
9
public/language/en-GB/admin/manage/ban-reasons.json
Normal file
9
public/language/en-GB/admin/manage/ban-reasons.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"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>?"
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
"purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>",
|
||||
"download-csv": "Download CSV",
|
||||
"custom-user-fields": "Custom User Fields",
|
||||
"ban-reasons": "Ban Reasons",
|
||||
"manage-groups": "Manage Groups",
|
||||
"set-reputation": "Set Reputation",
|
||||
"add-group": "Add Group",
|
||||
@@ -77,9 +78,11 @@
|
||||
|
||||
"temp-ban.length": "Length",
|
||||
"temp-ban.reason": "Reason <span class=\"text-muted\">(Optional)</span>",
|
||||
"temp-ban.select-reason": "Select a reason",
|
||||
"temp-ban.hours": "Hours",
|
||||
"temp-ban.days": "Days",
|
||||
"temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.",
|
||||
"temp-mute.explanation": "Enter the length of time for the mute. Note that a time of 0 will be a considered a permanent mute.",
|
||||
|
||||
"alerts.confirm-ban": "Do you really want to ban this user <strong>permanently</strong>?",
|
||||
"alerts.confirm-ban-multi": "Do you really want to ban these users <strong>permanently</strong>?",
|
||||
|
||||
@@ -134,6 +134,8 @@ paths:
|
||||
$ref: 'read/admin/manage/users.yaml'
|
||||
/api/admin/manage/users/custom-fields:
|
||||
$ref: 'read/admin/manage/users/custom-fields.yaml'
|
||||
/api/admin/manage/users/ban-reasons:
|
||||
$ref: 'read/admin/manage/users/ban-reasons.yaml'
|
||||
/api/admin/manage/registration:
|
||||
$ref: 'read/admin/manage/registration.yaml'
|
||||
/api/admin/manage/admins-mods:
|
||||
|
||||
28
public/openapi/read/admin/manage/users/ban-reasons.yaml
Normal file
28
public/openapi/read/admin/manage/users/ban-reasons.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
summary: Manage ban reasons for users
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
reasons:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
parsedBody:
|
||||
type: string
|
||||
description: "body parsed with filter:parse.raw hook"
|
||||
- $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -254,47 +254,52 @@ define('admin/manage/users', [
|
||||
});
|
||||
});
|
||||
|
||||
$('.ban-user-temporary').on('click', function () {
|
||||
$('.ban-user-temporary').on('click', async function () {
|
||||
const uids = getSelectedUids();
|
||||
if (!uids.length) {
|
||||
alerts.error('[[error:no-users-selected]]');
|
||||
return false; // specifically to keep the menu open
|
||||
}
|
||||
const reasons = await socket.emit('user.getBanReasons');
|
||||
const html = await app.parseAndTranslate('modals/temporary-ban', { reasons });
|
||||
const modal = bootbox.dialog({
|
||||
title: '[[user:ban-account]]',
|
||||
message: html,
|
||||
show: true,
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
close: {
|
||||
label: '[[global:close]]',
|
||||
className: 'btn-link',
|
||||
},
|
||||
submit: {
|
||||
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
||||
callback: function () {
|
||||
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
||||
data[cur.name] = cur.value;
|
||||
return data;
|
||||
}, {});
|
||||
const until = formData.length > 0 ? (
|
||||
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
|
||||
) : 0;
|
||||
|
||||
Benchpress.render('modals/temporary-ban', {}).then(function (html) {
|
||||
const modal = bootbox.dialog({
|
||||
title: '[[user:ban-account]]',
|
||||
message: html,
|
||||
show: true,
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
close: {
|
||||
label: '[[global:close]]',
|
||||
className: 'btn-link',
|
||||
},
|
||||
submit: {
|
||||
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
|
||||
callback: function () {
|
||||
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
||||
data[cur.name] = cur.value;
|
||||
return data;
|
||||
}, {});
|
||||
const until = formData.length > 0 ? (
|
||||
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
|
||||
) : 0;
|
||||
|
||||
Promise.all(uids.map(function (uid) {
|
||||
return api.put('/users/' + encodeURIComponent(uid) + '/ban', {
|
||||
until: until,
|
||||
reason: formData.reason,
|
||||
});
|
||||
})).then(() => {
|
||||
onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
|
||||
}).catch(alerts.error);
|
||||
},
|
||||
Promise.all(uids.map(function (uid) {
|
||||
return api.put('/users/' + encodeURIComponent(uid) + '/ban', {
|
||||
until: until,
|
||||
reason: formData.reason,
|
||||
});
|
||||
})).then(() => {
|
||||
onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true);
|
||||
}).catch(alerts.error);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
96
public/src/admin/manage/users/ban-reasons.js
Normal file
96
public/src/admin/manage/users/ban-reasons.js
Normal file
@@ -0,0 +1,96 @@
|
||||
define('admin/manage/user/ban-reasons', [
|
||||
'benchpress', 'bootbox', 'alerts', 'translator', 'jquery-ui/widgets/sortable',
|
||||
], function (benchpress, bootbox, alerts, translator) {
|
||||
const manageBanReasons = {};
|
||||
|
||||
manageBanReasons.init = function () {
|
||||
const table = $('table');
|
||||
|
||||
$('#new').on('click', () => showModal());
|
||||
|
||||
table.on('click', '[data-action="edit"]', function () {
|
||||
const row = $(this).parents('[data-key]');
|
||||
showModal(getDataFromEl(row));
|
||||
});
|
||||
|
||||
table.on('click', '[data-action="delete"]', function () {
|
||||
const row = $(this).parents('[data-key]');
|
||||
const title = row.attr('data-title');
|
||||
bootbox.confirm(`[[admin/manage/ban-reasons:delete-reason-confirm-x, "${title}"]]`, function (ok) {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
|
||||
$('tbody').sortable({
|
||||
handle: '[component="sort/handle"]',
|
||||
axis: 'y',
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
$('#save').on('click', () => {
|
||||
const reasons = [];
|
||||
$('tbody tr[data-key]').each((index, el) => {
|
||||
reasons.push(getDataFromEl($(el)));
|
||||
});
|
||||
socket.emit('admin.user.saveBanReasons', reasons, function (err) {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
alerts.success('[[admin/manage/ban-reasons:ban-reasons-saved]]');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getDataFromEl(el) {
|
||||
return {
|
||||
key: el.attr('data-key'),
|
||||
title: el.attr('data-title'),
|
||||
body: el.attr('data-body'),
|
||||
};
|
||||
}
|
||||
|
||||
async function showModal(reason = null) {
|
||||
const html = await benchpress.render('admin/partials/manage-ban-reasons-modal', reason);
|
||||
const modal = bootbox.dialog({
|
||||
message: html,
|
||||
onEscape: true,
|
||||
title: reason ?
|
||||
'[[admin/manage/ban-reasons:edit-reason]]' :
|
||||
'[[admin/manage/ban-reasons:create-reason]]',
|
||||
buttons: {
|
||||
submit: {
|
||||
label: '[[global:save]]',
|
||||
callback: async function () {
|
||||
const formData = modal.find('form').serializeObject();
|
||||
formData.key = reason ? reason.key : Date.now();
|
||||
formData.body = translator.escape(formData.body);
|
||||
formData.parsedBody = translator.escape(await socket.emit('admin.parseRaw', formData.body));
|
||||
|
||||
app.parseAndTranslate('admin/manage/users/ban-reasons', 'reasons', {
|
||||
reasons: [formData],
|
||||
}, (html) => {
|
||||
if (reason) {
|
||||
const oldKey = reason.key;
|
||||
$(`tbody [data-key="${oldKey}"]`).replaceWith(html);
|
||||
} else {
|
||||
$('tbody').append(html);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// bootbox translates message we want the translation keys to be preseved.
|
||||
if (reason && reason.body) {
|
||||
modal.find('[name="body"]').val(reason.body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return manageBanReasons;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
define('forum/account/moderate', [
|
||||
'benchpress',
|
||||
'api',
|
||||
'bootbox',
|
||||
'alerts',
|
||||
], function (Benchpress, api, bootbox, alerts) {
|
||||
'translator',
|
||||
], function (api, bootbox, alerts, translator) {
|
||||
const AccountModerate = {};
|
||||
|
||||
AccountModerate.banAccount = function (theirid, onSuccess) {
|
||||
@@ -83,31 +83,37 @@ define('forum/account/moderate', [
|
||||
});
|
||||
};
|
||||
|
||||
function throwModal(options) {
|
||||
Benchpress.render(options.tpl, {}).then(function (html) {
|
||||
const modal = bootbox.dialog({
|
||||
title: options.title,
|
||||
message: html,
|
||||
show: true,
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
close: {
|
||||
label: '[[global:close]]',
|
||||
className: 'btn-link',
|
||||
},
|
||||
submit: {
|
||||
label: options.title,
|
||||
callback: function () {
|
||||
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
||||
data[cur.name] = cur.value;
|
||||
return data;
|
||||
}, {});
|
||||
async function throwModal(options) {
|
||||
const reasons = await socket.emit('user.getBanReasons');
|
||||
const html = await app.parseAndTranslate(options.tpl, { reasons });
|
||||
const modal = bootbox.dialog({
|
||||
title: options.title,
|
||||
message: html,
|
||||
show: true,
|
||||
onEscape: true,
|
||||
buttons: {
|
||||
close: {
|
||||
label: '[[global:close]]',
|
||||
className: 'btn-link',
|
||||
},
|
||||
submit: {
|
||||
label: options.title,
|
||||
callback: function () {
|
||||
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
|
||||
data[cur.name] = cur.value;
|
||||
return data;
|
||||
}, {});
|
||||
|
||||
options.onSubmit(formData);
|
||||
},
|
||||
options.onSubmit(formData);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -314,3 +314,8 @@ usersController.customFields = async function (req, res) {
|
||||
});
|
||||
res.render('admin/manage/users/custom-fields', { fields: fields });
|
||||
};
|
||||
|
||||
usersController.banReasons = async function (req, res) {
|
||||
const reasons = await user.bans.getBanReasons();
|
||||
res.render('admin/manage/users/ban-reasons', { reasons });
|
||||
};
|
||||
@@ -23,6 +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/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/registration`, middlewares, controllers.admin.users.registrationQueue);
|
||||
|
||||
helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get);
|
||||
|
||||
@@ -9,6 +9,7 @@ const db = require('../database');
|
||||
const privileges = require('../privileges');
|
||||
const websockets = require('./index');
|
||||
const batch = require('../batch');
|
||||
const plugins = require('../plugins');
|
||||
const index = require('./index');
|
||||
const getAdminSearchDict = require('../admin/search').getDictionary;
|
||||
|
||||
@@ -126,4 +127,8 @@ SocketAdmin.clearSearchHistory = async function () {
|
||||
});
|
||||
};
|
||||
|
||||
SocketAdmin.parseRaw = async function (socket, text) {
|
||||
return await plugins.hooks.fire('filter:parse.raw', text);
|
||||
};
|
||||
|
||||
require('../promisify')(SocketAdmin);
|
||||
|
||||
@@ -219,3 +219,11 @@ User.saveCustomFields = async function (socket, fields) {
|
||||
await user.reloadCustomFieldWhitelist();
|
||||
};
|
||||
|
||||
User.saveBanReasons = async function (socket, reasons) {
|
||||
const keys = await db.getSortedSetRange('ban-reasons', 0, -1);
|
||||
await db.delete('ban-reasons');
|
||||
await db.deleteAll(keys.map(k => `ban-reason:${k}`));
|
||||
const ids = reasons.map((f, i) => i);
|
||||
await db.sortedSetAdd(`ban-reasons`, ids, ids);
|
||||
await db.setObjectBulk(reasons.map((reason, i) => [`ban-reason:${i}`, reason]));
|
||||
};
|
||||
|
||||
@@ -174,6 +174,14 @@ SocketUser.editModerationNote = async function (socket, data) {
|
||||
return await user.getModerationNotesByIds(data.uid, [data.id]);
|
||||
};
|
||||
|
||||
SocketUser.getBanReasons = async function (socket) {
|
||||
const canBan = await privileges.users.hasBanPrivilege(socket.uid);
|
||||
if (!canBan) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
return await user.bans.getBanReasons();
|
||||
};
|
||||
|
||||
SocketUser.deleteUpload = async function (socket, data) {
|
||||
if (!data || !data.name || !data.uid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
|
||||
@@ -7,6 +7,8 @@ const emailer = require('../emailer');
|
||||
const db = require('../database');
|
||||
const groups = require('../groups');
|
||||
const privileges = require('../privileges');
|
||||
const plugins = require('../plugins');
|
||||
const translator = require('../translator');
|
||||
|
||||
module.exports = function (User) {
|
||||
User.bans = {};
|
||||
@@ -50,20 +52,27 @@ module.exports = function (User) {
|
||||
}
|
||||
|
||||
// Email notification of ban
|
||||
const username = await User.getUserField(uid, 'username');
|
||||
const [username, settings] = await Promise.all([
|
||||
User.getUserField(uid, 'username'),
|
||||
User.getSettings(uid),
|
||||
]);
|
||||
const siteTitle = meta.config.title || 'NodeBB';
|
||||
|
||||
const data = {
|
||||
await emailer.send('banned', uid, {
|
||||
subject: `[[email:banned.subject, ${siteTitle}]]`,
|
||||
username: username,
|
||||
until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false,
|
||||
reason: reason,
|
||||
};
|
||||
await emailer.send('banned', uid, data).catch(err => winston.error(`[emailer.send] ${err.stack}`));
|
||||
reason: await parseReason(reason, settings.userLang),
|
||||
}).catch(err => winston.error(`[emailer.send] ${err.stack}`));
|
||||
|
||||
return banData;
|
||||
};
|
||||
|
||||
async function parseReason(reason, lang) {
|
||||
const parsed = await plugins.hooks.fire('filter:parse.raw', reason);
|
||||
return await translator.translate(parsed, lang);
|
||||
}
|
||||
|
||||
User.bans.unban = async function (uids, reason = '') {
|
||||
const isArray = Array.isArray(uids);
|
||||
uids = isArray ? uids : [uids];
|
||||
@@ -155,4 +164,15 @@ module.exports = function (User) {
|
||||
const banObj = await db.getObject(keys[0]);
|
||||
return banObj && banObj.reason ? banObj.reason : '';
|
||||
};
|
||||
|
||||
User.bans.getBanReasons = async function () {
|
||||
const keys = await db.getSortedSetRange('ban-reasons', 0, -1);
|
||||
const reasons = (await db.getObjects(keys.map(k => `ban-reason:${k}`))).filter(Boolean);
|
||||
await Promise.all(reasons.map(async (reason, i) => {
|
||||
reason.key = i;
|
||||
reason.parsedBody = translator.escape(await plugins.hooks.fire('filter:parse.raw', reason.body || ''));
|
||||
reason.body = translator.escape(reason.body);
|
||||
}));
|
||||
return reasons;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -83,6 +83,8 @@
|
||||
<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>
|
||||
<li><a class="dropdown-item rounded-1" href="{relative_path}/admin/manage/users/ban-reasons">[[admin/manage/users:ban-reasons]]</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
47
src/views/admin/manage/users/ban-reasons.tpl
Normal file
47
src/views/admin/manage/users/ban-reasons.tpl
Normal file
@@ -0,0 +1,47 @@
|
||||
<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="">
|
||||
<h4 class="fw-bold tracking-tight mb-0">[[admin/manage/ban-reasons:title]]</h4>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<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]]
|
||||
</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 class="row flex-grow-1">
|
||||
<div class="col-lg-12 d-flex flex-column gap-2">
|
||||
<div class="table-responsive flex-grow-1">
|
||||
<table class="table text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="text-muted">[[admin/manage/ban-reasons:reason-title]]</th>
|
||||
<th class="text-muted">[[admin/manage/ban-reasons:reason-body]]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each reasons }}}
|
||||
<tr data-key="{./key}" data-title="{./title}" data-body="{./body}">
|
||||
<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>
|
||||
</td>
|
||||
<td class="text-nowrap ">{./title}</td>
|
||||
<td class="">{./parsedBody}</td>
|
||||
<td class="">
|
||||
<div class="d-flex justify-content-end gap-1">
|
||||
<button data-action="edit" data-key="{./key}" class="btn btn-light btn-sm">[[admin/admin:edit]]</button>
|
||||
<button data-action="delete" data-key="{./key}" class="btn btn-light btn-sm"><i class="fa fa-trash text-danger"></i></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
11
src/views/admin/partials/manage-ban-reasons-modal.tpl
Normal file
11
src/views/admin/partials/manage-ban-reasons-modal.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
<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>
|
||||
@@ -22,9 +22,9 @@
|
||||
<p style="margin: 0;">
|
||||
[[email:banned.text3]]
|
||||
</p>
|
||||
<p style="margin: 0; padding: 6px 0px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; font-size: 13px; line-height: 26px; color: #666666;">
|
||||
<div style="margin: 0; padding: 6px 0px; font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol; font-size: 13px; line-height: 26px; color: #666666;">
|
||||
{reason}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{{ end }}}
|
||||
|
||||
@@ -9,24 +9,38 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>
|
||||
<input class="form-check-input" type="radio" id="unit-hours" name="unit" value="0" checked />
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="unit-days">[[admin/manage/users:temp-ban.days]]</label>
|
||||
<input class="form-check-input" type="radio" id="unit-days" name="unit" value="1" />
|
||||
<div class="d-flex gap-1">
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||
<select class="form-select" id="unit" name="unit">
|
||||
<option value="0">[[admin/manage/users:temp-ban.hours]]</option>
|
||||
<option value="1">[[admin/manage/users:temp-ban.days]]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||
<input type="text" class="form-control" id="reason" name="reason" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
<form class="form">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="1" />
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="unit-hours">[[admin/manage/users:temp-ban.hours]]</label>
|
||||
<input class="form-check-input" type="radio" id="unit-hours" name="unit" value="0" checked />
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<label class="form-check-label" for="unit-days">[[admin/manage/users:temp-ban.days]]</label>
|
||||
<input class="form-check-input" type="radio" id="unit-days" name="unit" value="1" />
|
||||
<p class="form-text">
|
||||
[[admin/manage/users:temp-mute.explanation]]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<div class="">
|
||||
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
|
||||
<input type="text" class="form-control" id="reason" name="reason" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="length">[[admin/manage/users:temp-ban.length]]</label>
|
||||
<div class="d-flex gap-1">
|
||||
<input class="form-control" id="length" name="length" type="number" min="0" value="0" />
|
||||
<select class="form-select" id="unit" name="unit">
|
||||
<option value="0">[[admin/manage/users:temp-ban.hours]]</option>
|
||||
<option value="1">[[admin/manage/users:temp-ban.days]]</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user