feat: add custom user fields acp page

This commit is contained in:
Barış Soner Uşaklı
2024-07-30 17:08:25 -04:00
parent c9de0e519e
commit 5e1d8769d4
10 changed files with 239 additions and 0 deletions

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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;
});

View File

@@ -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 });
};

View File

@@ -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',

View File

@@ -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);

View File

@@ -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}`);
};

View File

@@ -0,0 +1,56 @@
<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></th>
</tr>
</thead>
<tbody>
{{{ each fields }}}
<tr data-key="{./key}" data-name="{./name}" data-type="{./type}" 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>
<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>

View File

@@ -0,0 +1,24 @@
<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-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 {{{ 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>

View File

@@ -38,6 +38,7 @@
{{{ end }}}
{{{ if user.privileges.admin:users }}}
<a class="btn-ghost-sm justify-content-start text-decoration-none" id="manage-users" href="{relative_path}/admin/manage/users">[[admin/menu:manage/users]]</a>
<a class="btn-ghost-sm justify-content-start text-decoration-none" id="manage-custom-user-fields" href="{relative_path}/admin/manage/users/custom-fields">[[admin/menu:manage/custom-user-fields]]</a>
{{{ end }}}
{{{ if user.privileges.admin:groups }}}
<a class="btn-ghost-sm justify-content-start text-decoration-none" href="{relative_path}/admin/manage/groups">[[admin/menu:manage/groups]]</a>