feat: settings sorted list (#8170)

* feat: settings sorted list

see https://github.com/NodeBB/nodebb-plugin-quickstart/pull/9/files for sample
v1.18.x
Andrew Rodrigues 5 years ago committed by GitHub
parent 7cc63f7d2c
commit 3c9689a5ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -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) { await Promise.all(sortedLists.map(async function (list) {
callback(err, settings || {}); const members = await db.getSortedSetRange('settings:' + hash + ':sorted-list:' + list, 0, -1) || [];
}); const keys = [];
data[list] = [];
for (const order of members) {
keys.push('settings:' + hash + ':sorted-list:' + list + ':' + order);
}
const objects = await db.getObjects(keys);
objects.forEach(function (obj) {
data[list].push(obj);
});
}));
return data;
}; };
Settings.getOne = function (hash, field, callback) { Settings.getOne = async function (hash, field) {
db.getObjectField('settings:' + hash, field, callback); const data = await Settings.get(hash);
return data[field];
}; };
Settings.set = function (hash, values, quiet, callback) { Settings.set = async function (hash, values, quiet) {
if (!callback && typeof quiet === 'function') { quiet = quiet || false;
callback = quiet;
quiet = false; const sortedLists = [];
} else {
quiet = quiet || false; for (const key in values) {
if (values.hasOwnProperty(key)) {
if (Array.isArray(values[key]) && typeof values[key][0] !== 'string') {
sortedLists.push(key);
}
}
} }
async.waterfall([ if (sortedLists.length) {
function (next) { await db.delete('settings:' + hash + ':sorted-lists');
db.setObject('settings:' + hash, values, next); await db.setAdd('settings:' + hash + ':sorted-lists', sortedLists);
},
function (next) { await Promise.all(sortedLists.map(async function (list) {
plugins.fireHook('action:settings.set', { await db.delete('settings:' + hash + ':sorted-list:' + list);
plugin: hash, await Promise.all(values[list].map(async function (data, order) {
settings: values, 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));
}); });
pubsub.publish('action:settings.set.' + hash, values); });
Meta.reloadRequired = !quiet;
next(); await Promise.all(ops);
}, }
], callback);
if (Object.keys(values).length) {
await db.setObject('settings:' + hash, values);
}
plugins.fireHook('action:settings.set', {
plugin: hash,
settings: values,
});
pubsub.publish('action:settings.set.' + hash, values);
Meta.reloadRequired = !quiet;
}; };
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) {
if (!settings.hasOwnProperty(key)) {
empty[key] = values[key];
}
});
if (Object.keys(empty).length) { Object.keys(values).forEach(function (key) {
Settings.set(hash, empty, next); if (!settings.hasOwnProperty(key)) {
} else { empty[key] = values[key];
next(); }
} });
},
], callback);
if (Object.keys(empty).length) {
await Settings.set(hash, empty);
}
}; };

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

Loading…
Cancel
Save