mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-10-26 16:46:12 +01:00
feat: settings sorted list (#8170)
* feat: settings sorted list see https://github.com/NodeBB/nodebb-plugin-quickstart/pull/9/files for sample
This commit is contained in:
@@ -10,6 +10,7 @@ define('settings', function () {
|
|||||||
'settings/array',
|
'settings/array',
|
||||||
'settings/key',
|
'settings/key',
|
||||||
'settings/object',
|
'settings/object',
|
||||||
|
'settings/sorted-list',
|
||||||
];
|
];
|
||||||
|
|
||||||
var Settings;
|
var Settings;
|
||||||
@@ -271,6 +272,25 @@ define('settings', function () {
|
|||||||
onReady.push(callback);
|
onReady.push(callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
serializeForm: function (formEl) {
|
||||||
|
var values = formEl.serializeObject();
|
||||||
|
|
||||||
|
// "Fix" checkbox values, so that unchecked options are not omitted
|
||||||
|
formEl.find('input[type="checkbox"]').each(function (idx, inputEl) {
|
||||||
|
inputEl = $(inputEl);
|
||||||
|
if (!inputEl.is(':checked')) {
|
||||||
|
values[inputEl.attr('name')] = 'off';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// save multiple selects as json arrays
|
||||||
|
formEl.find('select[multiple]').each(function (idx, selectEl) {
|
||||||
|
selectEl = $(selectEl);
|
||||||
|
values[selectEl.attr('name')] = JSON.stringify(selectEl.val());
|
||||||
|
});
|
||||||
|
|
||||||
|
return values;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
Persists the given settings with given hash.
|
Persists the given settings with given hash.
|
||||||
@param hash The hash to use as settings-id.
|
@param hash The hash to use as settings-id.
|
||||||
@@ -456,7 +476,6 @@ define('settings', function () {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// multipe selects are saved as json arrays, parse them here
|
// multipe selects are saved as json arrays, parse them here
|
||||||
$(formEl).find('select[multiple]').each(function (idx, selectEl) {
|
$(formEl).find('select[multiple]').each(function (idx, selectEl) {
|
||||||
var key = $(selectEl).attr('name');
|
var key = $(selectEl).attr('name');
|
||||||
@@ -472,6 +491,12 @@ define('settings', function () {
|
|||||||
// Save loaded settings into ajaxify.data for use client-side
|
// Save loaded settings into ajaxify.data for use client-side
|
||||||
ajaxify.data.settings = values;
|
ajaxify.data.settings = values;
|
||||||
|
|
||||||
|
helper.whenReady(function () {
|
||||||
|
$(formEl).find('[data-sorted-list]').each(function (idx, el) {
|
||||||
|
getHook(el, 'get').call(Settings, $(el), hash);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$(formEl).deserialize(values);
|
$(formEl).deserialize(values);
|
||||||
$(formEl).find('input[type="checkbox"]').each(function () {
|
$(formEl).find('input[type="checkbox"]').each(function () {
|
||||||
$(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked'));
|
$(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked'));
|
||||||
@@ -489,23 +514,17 @@ define('settings', function () {
|
|||||||
},
|
},
|
||||||
save: function (hash, formEl, callback) {
|
save: function (hash, formEl, callback) {
|
||||||
formEl = $(formEl);
|
formEl = $(formEl);
|
||||||
|
|
||||||
if (formEl.length) {
|
if (formEl.length) {
|
||||||
var values = formEl.serializeObject();
|
var values = helper.serializeForm(formEl);
|
||||||
|
|
||||||
// "Fix" checkbox values, so that unchecked options are not omitted
|
helper.whenReady(function () {
|
||||||
formEl.find('input[type="checkbox"]').each(function (idx, inputEl) {
|
var list = formEl.find('[data-sorted-list]');
|
||||||
inputEl = $(inputEl);
|
if (list.length) {
|
||||||
if (!inputEl.is(':checked')) {
|
getHook(list, 'set').call(Settings, list, values);
|
||||||
values[inputEl.attr('name')] = 'off';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// save multiple selects as json arrays
|
|
||||||
formEl.find('select[multiple]').each(function (idx, selectEl) {
|
|
||||||
selectEl = $(selectEl);
|
|
||||||
values[selectEl.attr('name')] = JSON.stringify(selectEl.val());
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.emit('admin.settings.set', {
|
socket.emit('admin.settings.set', {
|
||||||
hash: hash,
|
hash: hash,
|
||||||
values: values,
|
values: values,
|
||||||
|
|||||||
125
public/src/modules/settings/sorted-list.js
Normal file
125
public/src/modules/settings/sorted-list.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
define('settings/sorted-list', ['benchpress', 'jqueryui'], function (benchpress) {
|
||||||
|
var SortedList;
|
||||||
|
var Settings;
|
||||||
|
|
||||||
|
|
||||||
|
SortedList = {
|
||||||
|
types: ['sorted-list'],
|
||||||
|
use: function () {
|
||||||
|
Settings = this;
|
||||||
|
},
|
||||||
|
set: function ($container, values) {
|
||||||
|
var key = $container.attr('data-sorted-list');
|
||||||
|
|
||||||
|
values[key] = [];
|
||||||
|
$container.find('[data-type="item"]').each(function (idx, item) {
|
||||||
|
var itemUUID = $(item).attr('data-sorted-list-uuid');
|
||||||
|
|
||||||
|
var formData = $('[data-sorted-list-object="' + key + '"][data-sorted-list-uuid="' + itemUUID + '"]');
|
||||||
|
values[key].push(Settings.helper.serializeForm(formData));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
get: function ($container) {
|
||||||
|
var $list = $container.find('[data-type="list"]');
|
||||||
|
var key = $container.attr('data-sorted-list');
|
||||||
|
var formTpl = $container.attr('data-form-template');
|
||||||
|
|
||||||
|
benchpress.parse(formTpl, {}, function (formHtml) {
|
||||||
|
var addBtn = $('[data-sorted-list="' + key + '"] [data-type="add"]');
|
||||||
|
|
||||||
|
addBtn.on('click', function () {
|
||||||
|
var modal = bootbox.confirm(formHtml, function (save) {
|
||||||
|
if (save) {
|
||||||
|
var itemUUID = utils.generateUUID();
|
||||||
|
var form = $('<form class="" data-sorted-list-uuid="' + itemUUID + '" data-sorted-list-object="' + key + '" />');
|
||||||
|
form.append(modal.find('form').children());
|
||||||
|
|
||||||
|
$('#content').append(form.hide());
|
||||||
|
|
||||||
|
|
||||||
|
var data = Settings.helper.serializeForm(form);
|
||||||
|
parse($container, itemUUID, data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var list = ajaxify.data.settings[key];
|
||||||
|
if (Array.isArray(list) && typeof list[0] !== 'string') {
|
||||||
|
list.forEach(function (item) {
|
||||||
|
var itemUUID = utils.generateUUID();
|
||||||
|
var form = $(formHtml).deserialize(item);
|
||||||
|
form.attr('data-sorted-list-uuid', itemUUID);
|
||||||
|
form.attr('data-sorted-list-object', key);
|
||||||
|
$('#content').append(form.hide());
|
||||||
|
|
||||||
|
parse($container, itemUUID, item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$list.sortable().addClass('pointer');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function setupRemoveButton($container, itemUUID) {
|
||||||
|
var key = $container.attr('data-sorted-list');
|
||||||
|
|
||||||
|
var removeBtn = $('[data-sorted-list="' + key + '"] [data-type="remove"]');
|
||||||
|
removeBtn.on('click', function () {
|
||||||
|
$('[data-sorted-list-uuid="' + itemUUID + '"]').remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupEditButton($container, itemUUID) {
|
||||||
|
var $list = $container.find('[data-type="list"]');
|
||||||
|
var key = $container.attr('data-sorted-list');
|
||||||
|
var itemTpl = $container.attr('data-item-template');
|
||||||
|
var editBtn = $('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="edit"]');
|
||||||
|
|
||||||
|
editBtn.on('click', function () {
|
||||||
|
var form = $('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]').clone(true).show();
|
||||||
|
|
||||||
|
var modal = bootbox.confirm(form, function (save) {
|
||||||
|
if (save) {
|
||||||
|
var form = $('<form class="" data-sorted-list-uuid="' + itemUUID + '" data-sorted-list-object="' + key + '" />');
|
||||||
|
form.append(modal.find('form').children());
|
||||||
|
|
||||||
|
$('#content').find('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]').remove();
|
||||||
|
$('#content').append(form.hide());
|
||||||
|
|
||||||
|
|
||||||
|
var data = Settings.helper.serializeForm(form);
|
||||||
|
|
||||||
|
benchpress.parse(itemTpl, data, function (itemHtml) {
|
||||||
|
itemHtml = $(itemHtml);
|
||||||
|
var oldItem = $list.find('[data-sorted-list-uuid="' + itemUUID + '"]');
|
||||||
|
oldItem.after(itemHtml);
|
||||||
|
oldItem.remove();
|
||||||
|
itemHtml.attr('data-sorted-list-uuid', itemUUID);
|
||||||
|
|
||||||
|
setupRemoveButton($container, itemUUID);
|
||||||
|
setupEditButton($container, itemUUID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parse($container, itemUUID, data) {
|
||||||
|
var $list = $container.find('[data-type="list"]');
|
||||||
|
var itemTpl = $container.attr('data-item-template');
|
||||||
|
|
||||||
|
benchpress.parse(itemTpl, data, function (itemHtml) {
|
||||||
|
itemHtml = $(itemHtml);
|
||||||
|
$list.append(itemHtml);
|
||||||
|
itemHtml.attr('data-sorted-list-uuid', itemUUID);
|
||||||
|
|
||||||
|
setupRemoveButton($container, itemUUID);
|
||||||
|
setupEditButton($container, itemUUID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return SortedList;
|
||||||
|
});
|
||||||
@@ -1,73 +1,109 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
const db = require('../database');
|
||||||
|
const plugins = require('../plugins');
|
||||||
|
const Meta = require('../meta');
|
||||||
|
const pubsub = require('../pubsub');
|
||||||
|
|
||||||
var db = require('../database');
|
const Settings = module.exports;
|
||||||
var plugins = require('../plugins');
|
|
||||||
var Meta = require('../meta');
|
|
||||||
var pubsub = require('../pubsub');
|
|
||||||
|
|
||||||
var Settings = module.exports;
|
Settings.get = async function (hash) {
|
||||||
|
const data = await db.getObject('settings:' + hash) || {};
|
||||||
|
const sortedLists = await db.getSetMembers('settings:' + hash + ':sorted-lists');
|
||||||
|
|
||||||
Settings.get = function (hash, callback) {
|
|
||||||
db.getObject('settings:' + hash, function (err, settings) {
|
|
||||||
callback(err, settings || {});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Settings.getOne = function (hash, field, callback) {
|
await Promise.all(sortedLists.map(async function (list) {
|
||||||
db.getObjectField('settings:' + hash, field, callback);
|
const members = await db.getSortedSetRange('settings:' + hash + ':sorted-list:' + list, 0, -1) || [];
|
||||||
};
|
const keys = [];
|
||||||
|
|
||||||
Settings.set = function (hash, values, quiet, callback) {
|
data[list] = [];
|
||||||
if (!callback && typeof quiet === 'function') {
|
for (const order of members) {
|
||||||
callback = quiet;
|
keys.push('settings:' + hash + ':sorted-list:' + list + ':' + order);
|
||||||
quiet = false;
|
}
|
||||||
} else {
|
|
||||||
quiet = quiet || false;
|
const objects = await db.getObjects(keys);
|
||||||
|
objects.forEach(function (obj) {
|
||||||
|
data[list].push(obj);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
Settings.getOne = async function (hash, field) {
|
||||||
|
const data = await Settings.get(hash);
|
||||||
|
return data[field];
|
||||||
|
};
|
||||||
|
|
||||||
|
Settings.set = async function (hash, values, quiet) {
|
||||||
|
quiet = quiet || false;
|
||||||
|
|
||||||
|
const sortedLists = [];
|
||||||
|
|
||||||
|
for (const key in values) {
|
||||||
|
if (values.hasOwnProperty(key)) {
|
||||||
|
if (Array.isArray(values[key]) && typeof values[key][0] !== 'string') {
|
||||||
|
sortedLists.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortedLists.length) {
|
||||||
|
await db.delete('settings:' + hash + ':sorted-lists');
|
||||||
|
await db.setAdd('settings:' + hash + ':sorted-lists', sortedLists);
|
||||||
|
|
||||||
|
await Promise.all(sortedLists.map(async function (list) {
|
||||||
|
await db.delete('settings:' + hash + ':sorted-list:' + list);
|
||||||
|
await Promise.all(values[list].map(async function (data, order) {
|
||||||
|
await db.delete('settings:' + hash + ':sorted-list:' + list + ':' + order);
|
||||||
|
}));
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ops = [];
|
||||||
|
sortedLists.forEach(function (list) {
|
||||||
|
const arr = values[list];
|
||||||
|
delete values[list];
|
||||||
|
|
||||||
|
arr.forEach(function (data, order) {
|
||||||
|
ops.push(db.sortedSetAdd('settings:' + hash + ':sorted-list:' + list, order, order));
|
||||||
|
ops.push(db.setObject('settings:' + hash + ':sorted-list:' + list + ':' + order, data));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(ops);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(values).length) {
|
||||||
|
await db.setObject('settings:' + hash, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function (next) {
|
|
||||||
db.setObject('settings:' + hash, values, next);
|
|
||||||
},
|
|
||||||
function (next) {
|
|
||||||
plugins.fireHook('action:settings.set', {
|
plugins.fireHook('action:settings.set', {
|
||||||
plugin: hash,
|
plugin: hash,
|
||||||
settings: values,
|
settings: values,
|
||||||
});
|
});
|
||||||
|
|
||||||
pubsub.publish('action:settings.set.' + hash, values);
|
pubsub.publish('action:settings.set.' + hash, values);
|
||||||
Meta.reloadRequired = !quiet;
|
Meta.reloadRequired = !quiet;
|
||||||
next();
|
|
||||||
},
|
|
||||||
], callback);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Settings.setOne = function (hash, field, value, callback) {
|
Settings.setOne = async function (hash, field, value) {
|
||||||
var data = {};
|
const data = {};
|
||||||
data[field] = value;
|
data[field] = value;
|
||||||
Settings.set(hash, data, callback);
|
await Settings.set(hash, data);
|
||||||
};
|
};
|
||||||
|
|
||||||
Settings.setOnEmpty = function (hash, values, callback) {
|
Settings.setOnEmpty = async function (hash, values) {
|
||||||
async.waterfall([
|
const settings = await Settings.get(hash) || {};
|
||||||
function (next) {
|
const empty = {};
|
||||||
db.getObject('settings:' + hash, next);
|
|
||||||
},
|
|
||||||
function (settings, next) {
|
|
||||||
settings = settings || {};
|
|
||||||
var empty = {};
|
|
||||||
Object.keys(values).forEach(function (key) {
|
Object.keys(values).forEach(function (key) {
|
||||||
if (!settings.hasOwnProperty(key)) {
|
if (!settings.hasOwnProperty(key)) {
|
||||||
empty[key] = values[key];
|
empty[key] = values[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (Object.keys(empty).length) {
|
if (Object.keys(empty).length) {
|
||||||
Settings.set(hash, empty, next);
|
await Settings.set(hash, empty);
|
||||||
} else {
|
|
||||||
next();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
], callback);
|
|
||||||
};
|
};
|
||||||
|
|||||||
85
test/meta.js
85
test/meta.js
@@ -93,6 +93,91 @@ describe('meta', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const someList = [
|
||||||
|
{ name: 'andrew', status: 'best' },
|
||||||
|
{ name: 'baris', status: 'wurst' },
|
||||||
|
];
|
||||||
|
const anotherList = [];
|
||||||
|
|
||||||
|
it('should set setting with sorted list', function (done) {
|
||||||
|
socketAdmin.settings.set({ uid: fooUid }, { hash: 'another:hash', values: { foo: '1', derp: 'value', someList: someList, anotherList: anotherList } }, function (err) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.getObject('settings:another:hash', function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(data.foo, '1');
|
||||||
|
assert.equal(data.derp, 'value');
|
||||||
|
assert.equal(data.someList, undefined);
|
||||||
|
assert.equal(data.anotherList, undefined);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get setting with sorted list', function (done) {
|
||||||
|
socketAdmin.settings.get({ uid: fooUid }, { hash: 'another:hash' }, function (err, data) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(data.foo, '1');
|
||||||
|
assert.equal(data.derp, 'value');
|
||||||
|
assert.deepEqual(data.someList, someList);
|
||||||
|
assert.deepEqual(data.anotherList, anotherList);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set setting if not empty', function (done) {
|
||||||
|
meta.settings.setOnEmpty('some:hash', { foo: 2 }, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
db.getObject('settings:some:hash', function (err, data) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(data.foo, '1');
|
||||||
|
assert.equal(data.derp, 'value');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set setting with sorted list if not empty', function (done) {
|
||||||
|
meta.settings.setOnEmpty('another:hash', { foo: anotherList }, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
socketAdmin.settings.get({ uid: fooUid }, { hash: 'another:hash' }, function (err, data) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(data.foo, '1');
|
||||||
|
assert.equal(data.derp, 'value');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set setting with sorted list if empty', function (done) {
|
||||||
|
meta.settings.setOnEmpty('another:hash', { empty: someList }, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
socketAdmin.settings.get({ uid: fooUid }, { hash: 'another:hash' }, function (err, data) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.equal(data.foo, '1');
|
||||||
|
assert.equal(data.derp, 'value');
|
||||||
|
assert.deepEqual(data.empty, someList);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set one and get one sorted list', function (done) {
|
||||||
|
meta.settings.setOne('another:hash', 'someList', someList, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
meta.settings.getOne('another:hash', 'someList', function (err, _someList) {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert.deepEqual(_someList, someList);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user