mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-02 12:05:57 +01:00
Squashed commit of the following:
commitafd96a00b1Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Thu Nov 7 10:42:33 2024 -0500 chore: up themes commitb40e530434Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Wed Nov 6 19:16:44 2024 -0500 feat: add min:rep to custom fields add validation in profile.update commit4b5cb613e5Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Wed Nov 6 12:03:22 2024 -0500 test: add openapi spec, move menu button commit0c551fa240Merge:13f39053c9bc00df3cd9Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Wed Nov 6 11:48:05 2024 -0500 Merge branch 'develop' into custom-user-fields commit13f39053c9Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Wed Jul 31 00:23:39 2024 -0400 refactor: dont need delete function commitf33c8849d8Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Tue Jul 30 21:30:00 2024 -0400 feat: show custom fields on edit/profile commit5e1d8769d4Author: Barış Soner Uşaklı <barisusakli@gmail.com> Date: Tue Jul 30 17:08:25 2024 -0400 feat: add custom user fields acp page
This commit is contained in:
@@ -107,10 +107,10 @@
|
||||
"nodebb-plugin-ntfy": "1.7.7",
|
||||
"nodebb-plugin-spam-be-gone": "2.2.2",
|
||||
"nodebb-rewards-essentials": "1.0.0",
|
||||
"nodebb-theme-harmony": "1.2.78",
|
||||
"nodebb-theme-harmony": "1.2.79",
|
||||
"nodebb-theme-lavender": "7.1.10",
|
||||
"nodebb-theme-peace": "2.2.8",
|
||||
"nodebb-theme-persona": "13.3.41",
|
||||
"nodebb-theme-persona": "13.3.42",
|
||||
"nodebb-widget-essentials": "7.0.30",
|
||||
"nodemailer": "6.9.16",
|
||||
"nprogress": "0.2.0",
|
||||
|
||||
21
public/language/en-GB/admin/manage/user-custom-fields.json
Normal file
21
public/language/en-GB/admin/manage/user-custom-fields.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"title": "Manage Custom User Fields",
|
||||
"create-field": "Create Field",
|
||||
"edit-field": "Edit Field",
|
||||
"manage-custom-fields": "Manage Custom Fields",
|
||||
"type-of-input": "Type of input",
|
||||
"key": "Key",
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"min-rep": "Minimum Reputation",
|
||||
"input-type-text": "Input (Text)",
|
||||
"input-type-link": "Input (Link)",
|
||||
"input-type-number": "Input (Number)",
|
||||
"input-type-select": "Select",
|
||||
"select-options": "Options",
|
||||
"select-options-help": "Add one option per line for the select element",
|
||||
"minimum-reputation": "Minimum reputation",
|
||||
"minimum-reputation-help": "If a user has less than this value they won't be able to use this field",
|
||||
"delete-field-confirm-x": "Do you really want to delete custom field \"%1\"?",
|
||||
"custom-fields-saved": "Custom fields saved"
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
"delete-content": "Delete User(s) <strong>Content</strong>",
|
||||
"purge": "Delete <strong>User(s)</strong> and <strong>Content</strong>",
|
||||
"download-csv": "Download CSV",
|
||||
"custom-user-fields": "Custom User Fields",
|
||||
"manage-groups": "Manage Groups",
|
||||
"set-reputation": "Set Reputation",
|
||||
"add-group": "Add Group",
|
||||
|
||||
@@ -208,6 +208,11 @@
|
||||
"not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature",
|
||||
"not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture",
|
||||
"not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture",
|
||||
"not-enough-reputation-custom-field": "You need %1 reputation for %2",
|
||||
"custom-user-field-value-too-long": "Custom field value too long, %1",
|
||||
"custom-user-field-select-value-invalid": "Custom field selected option is invalid, %1",
|
||||
"custom-user-field-invalid-link": "Custom field link is invalid, %1",
|
||||
"custom-user-field-invalid-number": "Custom field number is invalid, %1",
|
||||
"post-already-flagged": "You have already flagged this post",
|
||||
"user-already-flagged": "You have already flagged this user",
|
||||
"post-flagged-too-many-times": "This post has been flagged by others already",
|
||||
|
||||
@@ -106,6 +106,8 @@ paths:
|
||||
$ref: 'read/admin/manage/tags.yaml'
|
||||
/api/admin/manage/users:
|
||||
$ref: 'read/admin/manage/users.yaml'
|
||||
/api/admin/manage/users/custom-fields:
|
||||
$ref: 'read/admin/manage/users/custom-fields.yaml'
|
||||
/api/admin/manage/registration:
|
||||
$ref: 'read/admin/manage/registration.yaml'
|
||||
/api/admin/manage/admins-mods:
|
||||
|
||||
27
public/openapi/read/admin/manage/users/custom-fields.yaml
Normal file
27
public/openapi/read/admin/manage/users/custom-fields.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
get:
|
||||
tags:
|
||||
- admin
|
||||
summary: Manage custom fields for users
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
fields:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
key:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
select-options:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
- $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
|
||||
@@ -31,6 +31,8 @@ get:
|
||||
type: string
|
||||
allowCoverPicture:
|
||||
type: boolean
|
||||
customUserFields:
|
||||
type: array
|
||||
selectedGroup:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -66,6 +66,8 @@ get:
|
||||
type: number
|
||||
title:
|
||||
type: string
|
||||
customUserFields:
|
||||
type: array
|
||||
editButtons:
|
||||
type: array
|
||||
items:
|
||||
|
||||
100
public/src/admin/manage/users/custom-fields.js
Normal file
100
public/src/admin/manage/users/custom-fields.js
Normal file
@@ -0,0 +1,100 @@
|
||||
define('admin/manage/user/custom-fields', [
|
||||
'bootbox', 'alerts', 'jquery-ui/widgets/sortable',
|
||||
], function (bootbox, alerts) {
|
||||
const manageUserFields = {};
|
||||
|
||||
manageUserFields.init = function () {
|
||||
const table = $('table');
|
||||
|
||||
table.on('click', '[data-action="edit"]', function () {
|
||||
const row = $(this).parents('[data-key]');
|
||||
showModal(getDataFromEl(row));
|
||||
});
|
||||
|
||||
table.on('click', '[data-action="delete"]', function () {
|
||||
const key = $(this).attr('data-key');
|
||||
const row = $(this).parents('[data-key]');
|
||||
bootbox.confirm(`[[admin/manage/user-custom-fields:delete-field-confirm-x, ${key}]]`, function (ok) {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
row.remove();
|
||||
});
|
||||
});
|
||||
|
||||
$('tbody').sortable({
|
||||
handle: '[component="sort/handle"]',
|
||||
axis: 'y',
|
||||
zIndex: 9999,
|
||||
});
|
||||
|
||||
$('#new').on('click', () => showModal());
|
||||
|
||||
$('#save').on('click', () => {
|
||||
const fields = [];
|
||||
$('tbody tr[data-key]').each((index, el) => {
|
||||
fields.push(getDataFromEl($(el)));
|
||||
});
|
||||
socket.emit('admin.user.saveCustomFields', fields, function (err) {
|
||||
if (err) {
|
||||
alerts.error(err);
|
||||
}
|
||||
alerts.success('[[admin/manage/user-custom-fields:custom-fields-saved]]');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getDataFromEl(el) {
|
||||
return {
|
||||
key: el.attr('data-key'),
|
||||
name: el.attr('data-name'),
|
||||
type: el.attr('data-type'),
|
||||
'select-options': el.attr('data-select-options'),
|
||||
'min:rep': el.attr('data-min-rep'),
|
||||
};
|
||||
}
|
||||
|
||||
async function showModal(field = null) {
|
||||
const html = await app.parseAndTranslate('admin/partials/manage-custom-user-fields-modal', field);
|
||||
|
||||
const modal = bootbox.dialog({
|
||||
message: html,
|
||||
onEscape: true,
|
||||
title: field ?
|
||||
'[[admin/manage/user-custom-fields:edit-field]]' :
|
||||
'[[admin/manage/user-custom-fields:create-field]]',
|
||||
buttons: {
|
||||
submit: {
|
||||
label: '[[global:save]]',
|
||||
callback: function () {
|
||||
const formData = modal.find('form').serializeObject();
|
||||
if (formData.type === 'select') {
|
||||
formData.selectOptionsFormatted = formData['select-options'].trim().split('\n').join(', ');
|
||||
}
|
||||
|
||||
app.parseAndTranslate('admin/manage/users/custom-fields', 'fields', {
|
||||
fields: [formData],
|
||||
}, (html) => {
|
||||
if (field) {
|
||||
const oldKey = field.key;
|
||||
$(`tbody [data-key="${oldKey}"]`).replaceWith(html);
|
||||
} else {
|
||||
$('tbody').append(html);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
modal.find('#type-select').on('change', function () {
|
||||
const type = $(this).val();
|
||||
modal.find(`[data-input-type]`).addClass('hidden');
|
||||
modal.find(`[data-input-type="${type}"]`).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
return manageUserFields;
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const groups = require('../../groups');
|
||||
const privileges = require('../../privileges');
|
||||
const plugins = require('../../plugins');
|
||||
const file = require('../../file');
|
||||
const accountHelpers = require('./helpers');
|
||||
|
||||
const editController = module.exports;
|
||||
|
||||
@@ -25,11 +26,13 @@ editController.get = async function (req, res, next) {
|
||||
allowMultipleBadges,
|
||||
} = userData;
|
||||
|
||||
const [canUseSignature, canManageUsers] = await Promise.all([
|
||||
const [canUseSignature, canManageUsers, customUserFields] = await Promise.all([
|
||||
privileges.global.can('signature', req.uid),
|
||||
privileges.admin.can('admin:users', req.uid),
|
||||
accountHelpers.getCustomUserFields(userData),
|
||||
]);
|
||||
|
||||
userData.customUserFields = customUserFields;
|
||||
userData.maximumSignatureLength = meta.config.maximumSignatureLength;
|
||||
userData.maximumAboutMeLength = meta.config.maximumAboutMeLength;
|
||||
userData.maximumProfileImageSize = meta.config.maximumProfileImageSize;
|
||||
|
||||
@@ -134,6 +134,29 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
|
||||
return hookData.userData;
|
||||
};
|
||||
|
||||
helpers.getCustomUserFields = async function (userData) {
|
||||
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
||||
const allFields = (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
|
||||
|
||||
const fields = allFields.filter((field) => {
|
||||
const minRep = field['min:rep'] || 0;
|
||||
return userData.reputation >= minRep || meta.config['reputation:disabled'];
|
||||
});
|
||||
|
||||
fields.forEach((f) => {
|
||||
f['select-options'] = f['select-options'].split('\n').filter(Boolean).map(
|
||||
opt => ({
|
||||
value: opt,
|
||||
selected: opt === userData[f.key],
|
||||
})
|
||||
);
|
||||
if (userData[f.key]) {
|
||||
f.value = validator.escape(String(userData[f.key]));
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
function escape(value) {
|
||||
return translator.escape(validator.escape(String(value || '')));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const categories = require('../../categories');
|
||||
const plugins = require('../../plugins');
|
||||
const privileges = require('../../privileges');
|
||||
const helpers = require('../helpers');
|
||||
const accountHelpers = require('./helpers');
|
||||
const utils = require('../../utils');
|
||||
|
||||
const profileController = module.exports;
|
||||
@@ -21,12 +22,13 @@ profileController.get = async function (req, res, next) {
|
||||
|
||||
await incrementProfileViews(req, userData);
|
||||
|
||||
const [latestPosts, bestPosts] = await Promise.all([
|
||||
const [latestPosts, bestPosts, customUserFields] = await Promise.all([
|
||||
getLatestPosts(req.uid, userData),
|
||||
getBestPosts(req.uid, userData),
|
||||
accountHelpers.getCustomUserFields(userData),
|
||||
posts.parseSignature(userData, req.uid),
|
||||
]);
|
||||
|
||||
userData.customUserFields = customUserFields;
|
||||
userData.posts = latestPosts; // for backwards compat.
|
||||
userData.latestPosts = latestPosts;
|
||||
userData.bestPosts = bestPosts;
|
||||
|
||||
@@ -294,3 +294,15 @@ usersController.getCSV = async function (req, res, next) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
usersController.customFields = async function (req, res) {
|
||||
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
||||
const fields = (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
|
||||
fields.forEach((field) => {
|
||||
if (field['select-options']) {
|
||||
field.selectOptionsFormatted = field['select-options'].trim().split('\n').join(', ');
|
||||
}
|
||||
field['min:rep'] = field['min:rep'] || 0;
|
||||
});
|
||||
res.render('admin/manage/users/custom-fields', { fields: fields });
|
||||
};
|
||||
|
||||
@@ -96,6 +96,9 @@ privsAdmin.socketMap = {
|
||||
'admin.user.removeAdmins': 'admin:admins-mods',
|
||||
|
||||
'admin.user.loadGroups': 'admin:users',
|
||||
'admin.user.addCustomField': 'admin:users',
|
||||
'admin.user.editCustomField': 'admin:users',
|
||||
'admin.user.deleteCustomField': 'admin:users',
|
||||
'admin.groups.join': 'admin:users',
|
||||
'admin.groups.leave': 'admin:users',
|
||||
'admin.user.resetLockouts': 'admin:users',
|
||||
|
||||
@@ -21,6 +21,7 @@ module.exports = function (app, name, middleware, controllers) {
|
||||
helpers.setupAdminPageRoute(app, `/${name}/manage/tags`, middlewares, controllers.admin.tags.get);
|
||||
|
||||
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/registration`, middlewares, controllers.admin.users.registrationQueue);
|
||||
|
||||
helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get);
|
||||
|
||||
@@ -187,3 +187,20 @@ User.exportUsersCSV = async function (socket, data) {
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
User.saveCustomFields = async function (socket, fields) {
|
||||
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
||||
await db.delete('user-custom-fields');
|
||||
await db.deleteAll(keys.map(k => `user-custom-field:${k}`));
|
||||
|
||||
await db.sortedSetAdd(
|
||||
`user-custom-fields`,
|
||||
fields.map((f, i) => i),
|
||||
fields.map(f => f.key)
|
||||
);
|
||||
await db.setObjectBulk(
|
||||
fields.map(field => [`user-custom-field:${field.key}`, field])
|
||||
);
|
||||
await user.reloadCustomFieldWhitelist();
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ module.exports = function (User) {
|
||||
'cover:position', 'groupTitle', 'mutedUntil', 'mutedReason',
|
||||
];
|
||||
|
||||
let customFieldWhiteList = null;
|
||||
|
||||
User.guestData = {
|
||||
uid: 0,
|
||||
username: '[[global:guest]]',
|
||||
@@ -46,6 +48,10 @@ module.exports = function (User) {
|
||||
|
||||
let iconBackgrounds;
|
||||
|
||||
User.reloadCustomFieldWhitelist = async () => {
|
||||
customFieldWhiteList = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
||||
};
|
||||
|
||||
User.getUsersFields = async function (uids, fields) {
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
return [];
|
||||
@@ -58,10 +64,12 @@ module.exports = function (User) {
|
||||
ensureRequiredFields(fields, fieldsToRemove);
|
||||
|
||||
const uniqueUids = _.uniq(uids).filter(uid => uid > 0);
|
||||
|
||||
if (!customFieldWhiteList) {
|
||||
await User.reloadCustomFieldWhitelist();
|
||||
}
|
||||
const results = await plugins.hooks.fire('filter:user.whitelistFields', {
|
||||
uids: uids,
|
||||
whitelist: fieldWhitelist.slice(),
|
||||
whitelist: _.uniq(fieldWhitelist.concat(customFieldWhiteList)),
|
||||
});
|
||||
if (!fields.length) {
|
||||
fields = results.whitelist;
|
||||
|
||||
@@ -11,12 +11,14 @@ const meta = require('../meta');
|
||||
const db = require('../database');
|
||||
const groups = require('../groups');
|
||||
const plugins = require('../plugins');
|
||||
const tx = require('../translator');
|
||||
|
||||
module.exports = function (User) {
|
||||
User.updateProfile = async function (uid, data, extraFields) {
|
||||
let fields = [
|
||||
'username', 'email', 'fullname', 'website', 'location',
|
||||
'groupTitle', 'birthday', 'signature', 'aboutme',
|
||||
...await db.getSortedSetRange('user-custom-fields', 0, -1),
|
||||
];
|
||||
if (Array.isArray(extraFields)) {
|
||||
fields = _.uniq(fields.concat(extraFields));
|
||||
@@ -82,6 +84,49 @@ module.exports = function (User) {
|
||||
isLocationValid(data);
|
||||
isBirthdayValid(data);
|
||||
isGroupTitleValid(data);
|
||||
await validateCustomFields(data);
|
||||
}
|
||||
|
||||
async function validateCustomFields(data) {
|
||||
const keys = await db.getSortedSetRange('user-custom-fields', 0, -1);
|
||||
const fields = (await db.getObjects(keys.map(k => `user-custom-field:${k}`))).filter(Boolean);
|
||||
const reputation = await User.getUserField(data.uid, 'reputation');
|
||||
|
||||
fields.forEach((field) => {
|
||||
const { key, type } = field;
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const value = data[key];
|
||||
const minRep = field['min:rep'] || 0;
|
||||
if (reputation < minRep && !meta.config['reputation:disabled']) {
|
||||
throw new Error(tx.compile(
|
||||
'error:not-enough-reputation-custom-field', minRep, field.name
|
||||
));
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.length > 255) {
|
||||
throw new Error(tx.compile(
|
||||
'error:custom-user-field-value-too-long', field.name
|
||||
));
|
||||
}
|
||||
|
||||
if (type === 'input-number' && !utils.isNumber(value)) {
|
||||
throw new Error(tx.compile(
|
||||
'error:custom-user-field-invalid-number', field.name
|
||||
));
|
||||
} else if (value && field.type === 'input-link' && !validator.isURL(String(value))) {
|
||||
throw new Error(tx.compile(
|
||||
'error:custom-user-field-invalid-link', field.name
|
||||
));
|
||||
} else if (field.type === 'select') {
|
||||
const opts = field['select-options'].split('\n').filter(Boolean);
|
||||
if (!opts.includes(value)) {
|
||||
throw new Error(tx.compile(
|
||||
'error:custom-user-field-select-value-invalid', field.name
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function isEmailValid(data) {
|
||||
|
||||
@@ -81,6 +81,8 @@
|
||||
<li><a class="dropdown-item rounded-1" href="#" data-action="create" role="menuitem">[[admin/manage/users:create]]</a></li>
|
||||
{{{ if showInviteButton }}}<li><a class="dropdown-item rounded-1" href="#" component="user/invite" role="menuitem">[[admin/manage/users:invite]]</a></li>{{{ end }}}
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
60
src/views/admin/manage/users/custom-fields.tpl
Normal file
60
src/views/admin/manage/users/custom-fields.tpl
Normal file
@@ -0,0 +1,60 @@
|
||||
<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/user-custom-fields: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/user-custom-fields:create-field]]
|
||||
</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/user-custom-fields:key]]</th>
|
||||
<th class="text-muted">[[admin/manage/user-custom-fields:name]]</th>
|
||||
<th class="text-muted">[[admin/manage/user-custom-fields:type]]</th>
|
||||
<th class="text-muted text-end">[[admin/manage/user-custom-fields:min-rep]]</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{{ each fields }}}
|
||||
<tr data-key="{./key}" data-name="{./name}" data-type="{./type}" data-min-rep="{./min:rep}" data-select-options="{./select-options}" class="align-middle">
|
||||
<td 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">{./key}</td>
|
||||
<td class="text-nowrap">{./name}</td>
|
||||
<td>
|
||||
{./type}
|
||||
{{{ if (./type == "select") }}}
|
||||
<div class="text-muted">
|
||||
({./selectOptionsFormatted})
|
||||
</div>
|
||||
{{{ end }}}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{./min:rep}
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
33
src/views/admin/partials/manage-custom-user-fields-modal.tpl
Normal file
33
src/views/admin/partials/manage-custom-user-fields-modal.tpl
Normal file
@@ -0,0 +1,33 @@
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">[[admin/manage/user-custom-fields:type-of-input]]</label>
|
||||
<select class="form-select" id="type-select" name="type">
|
||||
<option value="input-text" {{{ if (type == "input-text") }}}selected{{{ end }}}>[[admin/manage/user-custom-fields:input-type-text]]</option>
|
||||
<option value="input-link" {{{ if (type == "input-link") }}}selected{{{ end }}}>[[admin/manage/user-custom-fields:input-type-link]]</option>
|
||||
<option value="input-number" {{{ if (type == "input-number") }}}selected{{{ end }}}>[[admin/manage/user-custom-fields:input-type-number]]</option>
|
||||
<option value="select" {{{ if (type == "select") }}}selected{{{ end }}}>[[admin/manage/user-custom-fields:input-type-select]]</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">[[admin/manage/user-custom-fields:key]]</label>
|
||||
<input class="form-control" type="text" name="key" value="{./key}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">[[admin/manage/user-custom-fields:name]]</label>
|
||||
<input class="form-control" type="text" name="name" value="{./name}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">[[admin/manage/user-custom-fields:minimum-reputation]]</label>
|
||||
<input class="form-control" type="number" name="min:rep" value="{./min:rep}" placeholder="0">
|
||||
<p class="form-text">[[admin/manage/user-custom-fields:minimum-reputation-help]]</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 {{{ if (type != "select") }}}hidden{{{ end }}}" data-input-type="select">
|
||||
<label class="form-label">[[admin/manage/user-custom-fields:select-options]]</label>
|
||||
<textarea class="form-control" name="select-options" rows="6">{./select-options}</textarea>
|
||||
<p class="form-text">[[admin/manage/user-custom-fields:select-options-help]]</p>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user