diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index ab4ead6158..106cf7ad6a 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -10,6 +10,7 @@ define('settings', function () { 'settings/array', 'settings/key', 'settings/object', + 'settings/sorted-list', ]; var Settings; @@ -271,6 +272,25 @@ define('settings', function () { 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. @param hash The hash to use as settings-id. @@ -456,7 +476,6 @@ define('settings', function () { if (err) { return callback(err); } - // multipe selects are saved as json arrays, parse them here $(formEl).find('select[multiple]').each(function (idx, selectEl) { var key = $(selectEl).attr('name'); @@ -472,6 +491,12 @@ define('settings', function () { // Save loaded settings into ajaxify.data for use client-side 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).find('input[type="checkbox"]').each(function () { $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked')); @@ -489,23 +514,17 @@ define('settings', function () { }, save: function (hash, formEl, callback) { formEl = $(formEl); + if (formEl.length) { - var values = formEl.serializeObject(); + var values = helper.serializeForm(formEl); - // "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'; + helper.whenReady(function () { + var list = formEl.find('[data-sorted-list]'); + if (list.length) { + getHook(list, 'set').call(Settings, list, values); } }); - // 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', { hash: hash, values: values, diff --git a/public/src/modules/settings/sorted-list.js b/public/src/modules/settings/sorted-list.js new file mode 100644 index 0000000000..39695202d7 --- /dev/null +++ b/public/src/modules/settings/sorted-list.js @@ -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.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.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; +}); diff --git a/src/meta/settings.js b/src/meta/settings.js index 6e0b151801..17ae969043 100644 --- a/src/meta/settings.js +++ b/src/meta/settings.js @@ -1,73 +1,109 @@ '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'); -var plugins = require('../plugins'); -var Meta = require('../meta'); -var pubsub = require('../pubsub'); +const Settings = module.exports; -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 || {}); - }); + + await Promise.all(sortedLists.map(async function (list) { + 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) { - db.getObjectField('settings:' + hash, field, callback); +Settings.getOne = async function (hash, field) { + const data = await Settings.get(hash); + return data[field]; }; -Settings.set = function (hash, values, quiet, callback) { - if (!callback && typeof quiet === 'function') { - callback = quiet; - quiet = false; - } else { - quiet = quiet || false; +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); + } + } } - async.waterfall([ - function (next) { - db.setObject('settings:' + hash, values, next); - }, - function (next) { - plugins.fireHook('action:settings.set', { - plugin: hash, - settings: values, + 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)); }); - pubsub.publish('action:settings.set.' + hash, values); - Meta.reloadRequired = !quiet; - next(); - }, - ], callback); + }); + + await Promise.all(ops); + } + + 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) { - var data = {}; +Settings.setOne = async function (hash, field, value) { + const data = {}; data[field] = value; - Settings.set(hash, data, callback); + await Settings.set(hash, data); }; -Settings.setOnEmpty = function (hash, values, callback) { - async.waterfall([ - function (next) { - 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]; - } - }); +Settings.setOnEmpty = async function (hash, values) { + const settings = await Settings.get(hash) || {}; + const empty = {}; - if (Object.keys(empty).length) { - Settings.set(hash, empty, next); - } else { - next(); - } - }, - ], callback); + Object.keys(values).forEach(function (key) { + if (!settings.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); + + + if (Object.keys(empty).length) { + await Settings.set(hash, empty); + } }; diff --git a/test/meta.js b/test/meta.js index 502cafbc95..097fb7480e 100644 --- a/test/meta.js +++ b/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(); + }); + }); + }); });