diff --git a/public/language/en-GB/admin/manage/user-custom-fields.json b/public/language/en-GB/admin/manage/user-custom-fields.json new file mode 100644 index 0000000000..41e3cb8e0c --- /dev/null +++ b/public/language/en-GB/admin/manage/user-custom-fields.json @@ -0,0 +1,17 @@ +{ + "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", + "input-type-text": "Input (Text)", + "input-type-number": "Input (Number)", + "input-type-select": "Select", + "select-options": "Options", + "select-options-help": "Add one option per line for the select element", + "delete-field-confirm-x": "Do you really want to delete custom field \"%1\"?", + "custom-fields-saved": "Custom fields saved" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 6e30be22b3..9093aad71a 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -12,6 +12,7 @@ "manage/privileges": "Privileges", "manage/tags": "Tags", "manage/users": "Users", + "manage/custom-user-fields": "Custom User Fields", "manage/admins-mods": "Admins & Mods", "manage/registration": "Registration Queue", "manage/flagged-content": "Flagged Content", diff --git a/public/src/admin/manage/users/custom-fields.js b/public/src/admin/manage/users/custom-fields.js new file mode 100644 index 0000000000..3070768d08 --- /dev/null +++ b/public/src/admin/manage/users/custom-fields.js @@ -0,0 +1,105 @@ +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]'); + const field = { + key: row.attr('data-key'), + name: row.attr('data-name'), + type: row.attr('data-type'), + 'select-options': row.attr('data-select-options'), + }; + showModal(field); + }); + + 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; + } + socket.emit('admin.user.deleteCustomField', key, (err) => { + if (err) { + return alerts.error(err); + } + 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) => { + const $el = $(el); + fields.push({ + key: $el.attr('data-key'), + name: $el.attr('data-name'), + type: $el.attr('data-type'), + 'select-options': $el.attr('data-select-options'), + }); + }); + socket.emit('admin.user.saveCustomFields', fields, function (err) { + if (err) { + alerts.error(err); + } + alerts.success('[[admin/manage/user-custom-fields:custom-fields-saved]]'); + }); + }); + }; + + 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; +}); + + diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index aab9045eca..a6e25c18ec 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -294,3 +294,14 @@ usersController.getCSV = async function (req, res, next) { } }); }; + +usersController.customFields = async function (req, res, next) { + const keys = await db.getSortedSetRange('user-custom-fields', 0, -1); + const fields = await db.getObjects(keys.map(k => `user-custom-field:${k}`)); + fields.forEach((field) => { + if (field['select-options']) { + field.selectOptionsFormatted = field['select-options'].trim().split('\n').join(', '); + } + }); + res.render('admin/manage/users/custom-fields', { fields: fields }); +}; diff --git a/src/privileges/admin.js b/src/privileges/admin.js index 35a71e5f02..958b4cfe49 100644 --- a/src/privileges/admin.js +++ b/src/privileges/admin.js @@ -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', diff --git a/src/routes/admin.js b/src/routes/admin.js index 6e6721c13e..9e780d9104 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -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); diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index db9a49ac1f..86ab52b196 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -187,3 +187,23 @@ 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]) + ); +}; + +User.deleteCustomField = async function (socket, key) { + await db.sortedSetRemove(`user-custom-fields`, key); + await db.delete(`user-custom-field:${key}`); +}; diff --git a/src/views/admin/manage/users/custom-fields.tpl b/src/views/admin/manage/users/custom-fields.tpl new file mode 100644 index 0000000000..33fce16806 --- /dev/null +++ b/src/views/admin/manage/users/custom-fields.tpl @@ -0,0 +1,56 @@ +