From 00d909f16ca31af1fb96b9d1535570e7a16fc615 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 4 Mar 2018 00:22:05 +0000 Subject: [PATCH] Async setting and category events - Async setting and category events - Added deepfreeze utility - Fix dropdown staying open after selecting an option --- client/src/modules/contentmanager.js | 36 ++++++---- client/src/modules/pluginapi.js | 3 +- .../src/structs/settings/settingscategory.js | 64 +++++++++++++----- client/src/structs/settings/settingsset.js | 67 ++++++++++++++----- client/src/structs/settings/types/array.js | 14 ++-- .../src/structs/settings/types/basesetting.js | 55 ++++++++++++--- client/src/structs/settings/types/custom.js | 4 +- client/src/structs/settings/types/dropdown.js | 4 +- client/src/structs/settings/types/radio.js | 4 +- client/src/ui/components/common/Dropdown.vue | 2 +- common/modules/utils.js | 14 ++++ 11 files changed, 195 insertions(+), 72 deletions(-) diff --git a/client/src/modules/contentmanager.js b/client/src/modules/contentmanager.js index d726722e..bf3d3a58 100644 --- a/client/src/modules/contentmanager.js +++ b/client/src/modules/contentmanager.js @@ -9,7 +9,7 @@ */ import Globals from './globals'; -import { FileUtils, ClientLogger as Logger } from 'common'; +import { Utils, FileUtils, ClientLogger as Logger } from 'common'; import path from 'path'; import { Events } from 'modules'; import { SettingsSet, ErrorEvent } from 'structs'; @@ -176,33 +176,41 @@ export default class { const readConfig = await this.readConfig(contentPath); const mainPath = path.join(contentPath, readConfig.main); + const defaultConfig = new SettingsSet({ + settings: readConfig.defaultConfig, + schemes: readConfig.configSchemes + }); + const userConfig = { enabled: false, - config: new SettingsSet({ - settings: readConfig.defaultConfig, - schemes: readConfig.configSchemes - }) + config: undefined, + data: {} }; - for (let category of userConfig.config.settings) { - for (let setting of category.settings) { - setting.setContentPath(contentPath); - } - } - try { const readUserConfig = await this.readUserConfig(contentPath); userConfig.enabled = readUserConfig.enabled || false; - userConfig.config.merge({ settings: readUserConfig.config }); - userConfig.config.setSaved(); + // await userConfig.config.merge({ settings: readUserConfig.config }); + // userConfig.config.setSaved(); + // userConfig.config = userConfig.config.clone({ settings: readUserConfig.config }); + userConfig.config = readUserConfig.config; userConfig.data = readUserConfig.data || {}; } catch (err) { /*We don't care if this fails it either means that user config doesn't exist or there's something wrong with it so we revert to default config*/ console.info(`Failed reading config for ${this.contentType} ${readConfig.info.name} in ${dirName}`); console.error(err); } + userConfig.config = defaultConfig.clone({ settings: userConfig.config }); + userConfig.config.setSaved(); + + for (let setting of userConfig.config.findSettings(() => true)) { + setting.setContentPath(contentPath); + } + + Utils.deepfreeze(defaultConfig); + const configs = { - defaultConfig: readConfig.defaultConfig, + defaultConfig, schemes: userConfig.schemes, userConfig }; diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js index 6977046c..d555117f 100644 --- a/client/src/modules/pluginapi.js +++ b/client/src/modules/pluginapi.js @@ -83,7 +83,8 @@ export default class PluginApi { tryParseJson: () => Utils.tryParseJson.apply(Utils, arguments), toCamelCase: () => Utils.toCamelCase.apply(Utils, arguments), compare: () => Utils.compare.apply(Utils, arguments), - deepclone: () => Utils.deepclone.apply(Utils, arguments) + deepclone: () => Utils.deepclone.apply(Utils, arguments), + deepfreeze: () => Utils.deepfreeze.apply(Utils, arguments) }; } diff --git a/client/src/structs/settings/settingscategory.js b/client/src/structs/settings/settingscategory.js index 9614acb3..e5cce144 100644 --- a/client/src/structs/settings/settingscategory.js +++ b/client/src/structs/settings/settingscategory.js @@ -9,30 +9,31 @@ */ import Setting from './setting'; -import EventEmitter from 'events'; -import { ClientLogger as Logger } from 'common'; +import { ClientLogger as Logger, AsyncEventEmitter } from 'common'; import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs'; export default class SettingsCategory { - constructor(args) { - this.emitter = new EventEmitter(); + constructor(args, ...merge) { + this.emitter = new AsyncEventEmitter(); this.args = args.args || args; this.args.settings = this.settings.map(setting => new Setting(setting)); + for (let newCategory of merge) { + this._merge(newCategory); + } + for (let setting of this.settings) { - setting.on('setting-updated', ({ value, old_value }) => { - this.emit('setting-updated', new SettingUpdatedEvent({ - category: this, category_id: this.id, - setting, setting_id: setting.id, - value, old_value - })); - }); + setting.on('setting-updated', ({ value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({ + category: this, category_id: this.id, + setting, setting_id: setting.id, + value, old_value + }))); setting.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({ - updatedSettings: updatedSettings.map(updatedSetting => Object.assign({ + updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({ category: this, category_id: this.id - }, updatedSetting)) + }, updatedSetting))) }))); } } @@ -78,7 +79,11 @@ export default class SettingsCategory { return this.findSetting(setting => setting.id === id); } - merge(newCategory, emit_multi = true) { + /** + * Merges a category into this category without emitting events (and therefore synchronously). + * Only exists for use by SettingsSet. + */ + _merge(newCategory) { let updatedSettings = []; for (let newSetting of newCategory.settings) { @@ -88,7 +93,29 @@ export default class SettingsCategory { continue; } - const updatedSetting = setting.merge(newSetting, false); + const updatedSetting = setting._merge(newSetting); + if (!updatedSetting) continue; + updatedSettings = updatedSettings.concat(updatedSetting.map(({ setting, value, old_value }) => ({ + category: this, category_id: this.id, + setting, setting_id: setting.id, + value, old_value + }))); + } + + return updatedSettings; + } + + async merge(newCategory, emit_multi = true) { + let updatedSettings = []; + + for (let newSetting of newCategory.settings) { + const setting = this.settings.find(setting => setting.id === newSetting.id); + if (!setting) { + Logger.warn('SettingsCategory', `Trying to merge setting ${this.id}/${newSetting.id}, which does not exist.`); + continue; + } + + const updatedSetting = await setting._merge(newSetting, false); if (!updatedSetting) continue; updatedSettings = updatedSettings.concat(updatedSetting.map(({ setting, value, old_value }) => ({ category: this, category_id: this.id, @@ -98,9 +125,10 @@ export default class SettingsCategory { } if (emit_multi) - this.emit('settings-updated', new SettingsUpdatedEvent({ + await this.emit('settings-updated', new SettingsUpdatedEvent({ updatedSettings })); + return updatedSettings; } @@ -117,7 +145,7 @@ export default class SettingsCategory { }; } - clone() { + clone(...merge) { return new SettingsCategory({ id: this.id, category: this.id, @@ -125,7 +153,7 @@ export default class SettingsCategory { category_name: this.category_name, type: this.type, settings: this.settings.map(setting => setting.clone()) - }); + }, ...merge); } on(...args) { return this.emitter.on(...args); } diff --git a/client/src/structs/settings/settingsset.js b/client/src/structs/settings/settingsset.js index 8e21566a..e6ad395c 100644 --- a/client/src/structs/settings/settingsset.js +++ b/client/src/structs/settings/settingsset.js @@ -16,14 +16,18 @@ import { Modals } from 'ui'; export default class SettingsSet { - constructor(args) { + constructor(args, ...merge) { this.emitter = new AsyncEventEmitter(); this.args = args.args || args; - this.args.settings = this.settings.map(category => new SettingsCategory(category)); + this.args.categories = this.categories.map(category => new SettingsCategory(category)); this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme)); - for (let category of this.settings) { + for (let newSet of merge) { + this._merge(newSet); + } + + for (let category of this.categories) { category.on('setting-updated', ({ setting, value, old_value }) => this.emit('setting-updated', new SettingUpdatedEvent({ set: this, set_id: this.id, category, category_id: category.id, @@ -31,9 +35,9 @@ export default class SettingsSet { value, old_value }))); category.on('settings-updated', ({ updatedSettings }) => this.emit('settings-updated', new SettingsUpdatedEvent({ - updatedSettings: updatedSettings.map(updatedSetting => Object.assign({ + updatedSettings: updatedSettings.map(updatedSetting => new SettingUpdatedEvent(Object.assign({ set: this, set_id: this.id - }, updatedSetting)) + }, updatedSetting))) }))); } } @@ -55,7 +59,7 @@ export default class SettingsSet { } get categories() { - return this.args.settings || []; + return this.args.categories || this.args.settings || []; } get settings() { @@ -91,10 +95,7 @@ export default class SettingsSet { } findSettings(f) { - for (let category of this.categories) { - const setting = category.findSettings(f); - if (setting) return setting; - } + return this.findSettingsInCategory(() => true, f); } findSettingInCategory(cf, f) { @@ -105,10 +106,11 @@ export default class SettingsSet { } findSettingsInCategory(cf, f) { + let settings = []; for (let category of this.categories.filter(cf)) { - const setting = category.findSettings(f); - if (setting) return setting; + settings = settings.concat(category.findSettings(f)); } + return settings; } getSetting(id, sid) { @@ -125,9 +127,14 @@ export default class SettingsSet { Modals.settings(this, headertext ? headertext : this.headertext); } - async merge(newSet, emit_multi = true) { + /** + * Merges a set into this set without emitting events (and therefore synchronously). + * Only exists for use by the constructor. + */ + _merge(newSet, emit_multi = true) { let updatedSettings = []; - const categories = newSet instanceof Array ? newSet : newSet.settings; + // const categories = newSet instanceof Array ? newSet : newSet.settings; + const categories = newSet && newSet.args ? newSet.args.settings : newSet ? newSet.settings : newSet; if (!categories) return []; for (let newCategory of categories) { @@ -137,7 +144,33 @@ export default class SettingsSet { continue; } - const updatedSetting = category.merge(newCategory, false); + const updatedSetting = category._merge(newCategory, false); + if (!updatedSetting) continue; + updatedSettings = updatedSettings.concat(updatedSetting.map(({ category, setting, value, old_value }) => ({ + set: this, set_id: this.id, + category, category_id: category.id, + setting, setting_id: setting.id, + value, old_value + }))); + } + + return updatedSettings; + } + + async merge(newSet, emit_multi = true) { + let updatedSettings = []; + // const categories = newSet instanceof Array ? newSet : newSet.settings; + const categories = newSet && newSet.args ? newSet.args.settings : newSet ? newSet.settings : newSet; + if (!categories) return []; + + for (let newCategory of categories) { + const category = this.find(category => category.category === newCategory.category); + if (!category) { + Logger.warn('SettingsCategory', `Trying to merge category ${newCategory.id}, which does not exist.`); + continue; + } + + const updatedSetting = await category.merge(newCategory, false); if (!updatedSetting) continue; updatedSettings = updatedSettings.concat(updatedSetting.map(({ category, setting, value, old_value }) => ({ set: this, set_id: this.id, @@ -168,14 +201,14 @@ export default class SettingsSet { return stripped; } - clone() { + clone(...merge) { return new SettingsSet({ id: this.id, text: this.text, headertext: this.headertext, settings: this.categories.map(category => category.clone()), schemes: this.schemes.map(scheme => scheme.clone()) - }); + }, ...merge); } on(...args) { return this.emitter.on(...args); } diff --git a/client/src/structs/settings/types/array.js b/client/src/structs/settings/types/array.js index 146147dd..73f62373 100644 --- a/client/src/structs/settings/types/array.js +++ b/client/src/structs/settings/types/array.js @@ -17,8 +17,8 @@ import SettingsScheme from '../settingsscheme'; export default class ArraySetting extends Setting { - constructor(args) { - super(args); + constructor(args, ...merge) { + super(args, ...merge); this.args.settings = this.settings.map(category => new SettingsCategory(category)); this.args.schemes = this.schemes.map(scheme => new SettingsScheme(scheme)); @@ -85,9 +85,9 @@ export default class ArraySetting extends Setting { const set = new SettingsSet({ settings: Utils.deepclone(this.settings), schemes: this.schemes - }); + }, item ? item.args || item : undefined); - if (item) set.merge(item.args || item); + // if (item) set.merge(item.args || item); set.setSaved(); set.on('settings-updated', () => this.updateValue()); return set; @@ -124,8 +124,8 @@ export default class ArraySetting extends Setting { return maps.length ? maps.join(', ') + ',' : '()'; } - clone() { - return new ArraySetting(Utils.deepclone(this.args)); - } + // clone() { + // return new ArraySetting(Utils.deepclone(this.args)); + // } } diff --git a/client/src/structs/settings/types/basesetting.js b/client/src/structs/settings/types/basesetting.js index a7535253..09347136 100644 --- a/client/src/structs/settings/types/basesetting.js +++ b/client/src/structs/settings/types/basesetting.js @@ -9,13 +9,12 @@ */ import { ThemeManager } from 'modules'; -import { Utils } from 'common'; +import { Utils, AsyncEventEmitter } from 'common'; import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs'; -import EventEmitter from 'events'; export default class Setting { - constructor(args) { + constructor(args, ...merge) { this.args = args.args || args; if (!this.args.hasOwnProperty('value')) @@ -23,7 +22,11 @@ export default class Setting { if (!this.args.hasOwnProperty('saved_value')) this.args.saved_value = this.args.value; - this.emitter = new EventEmitter(); + for (let newSetting of merge) { + this._merge(newSetting); + } + + this.emitter = new AsyncEventEmitter(); this.changed = !Utils.compare(this.args.value, this.args.saved_value); } @@ -67,8 +70,44 @@ export default class Setting { return this.args.fullwidth || false; } - merge(newSetting, emit_multi = true) { - return this.setValue(newSetting.args ? newSetting.args.value : newSetting.value, emit_multi); + /** + * Merges a setting into this setting without emitting events (and therefore synchronously). + * Only exists for use by SettingsCategory. + */ + _merge(newSetting) { + const value = newSetting.args ? newSetting.args.value : newSetting.value; + const old_value = this.args.value; + if (Utils.compare(value, old_value)) return []; + this.args.value = value; + this.changed = !Utils.compare(this.args.value, this.args.saved_value); + + return [{ + setting: this, setting_id: this.id, + value, old_value + }]; + } + + async merge(newSetting, emit_multi = true) { + const value = newSetting.args ? newSetting.args.value : newSetting.value; + const old_value = this.args.value; + if (Utils.compare(value, old_value)) return []; + this.args.value = value; + this.changed = !Utils.compare(this.args.value, this.args.saved_value); + + const updatedSetting = { + setting: this, setting_id: this.id, + value, old_value + }; + + if (emit) + await this.emit('setting-updated', new SettingUpdatedEvent(updatedSetting)); + + if (emit_multi) + await this.emit('settings-updated', new SettingsUpdatedEvent({ + updatedSettings: [updatedSetting] + })); + + return [updatedSetting]; } setValue(value, emit_multi = true, emit = true) { @@ -109,8 +148,8 @@ export default class Setting { }; } - clone() { - return new this.constructor(Utils.deepclone(this.args)); + clone(...merge) { + return new this.constructor(Utils.deepclone(this.args), ...merge); } toSCSS() { diff --git a/client/src/structs/settings/types/custom.js b/client/src/structs/settings/types/custom.js index eb95de93..83e96134 100644 --- a/client/src/structs/settings/types/custom.js +++ b/client/src/structs/settings/types/custom.js @@ -15,8 +15,8 @@ import path from 'path'; export default class CustomSetting extends Setting { - constructor(args) { - super(args); + constructor(args, ...merge) { + super(args, ...merge); if (this.args.class_file && this.path) this.setClass(this.args.class_file, this.args.class); diff --git a/client/src/structs/settings/types/dropdown.js b/client/src/structs/settings/types/dropdown.js index cf889b89..87be413c 100644 --- a/client/src/structs/settings/types/dropdown.js +++ b/client/src/structs/settings/types/dropdown.js @@ -13,8 +13,8 @@ import MultipleChoiceOption from '../multiplechoiceoption'; export default class DropdownSetting extends Setting { - constructor(args) { - super(args); + constructor(args, ...merge) { + super(args, ...merge); this.args.options = this.options.map(option => new MultipleChoiceOption(option)); } diff --git a/client/src/structs/settings/types/radio.js b/client/src/structs/settings/types/radio.js index 9943ae5b..bb96fcc7 100644 --- a/client/src/structs/settings/types/radio.js +++ b/client/src/structs/settings/types/radio.js @@ -13,8 +13,8 @@ import MultipleChoiceOption from '../multiplechoiceoption'; export default class RadioSetting extends Setting { - constructor(args) { - super(args); + constructor(args, ...merge) { + super(args, ...merge); this.args.options = this.options.map(option => new MultipleChoiceOption(option)); } diff --git a/client/src/ui/components/common/Dropdown.vue b/client/src/ui/components/common/Dropdown.vue index b7ddcc62..c0cb984d 100644 --- a/client/src/ui/components/common/Dropdown.vue +++ b/client/src/ui/components/common/Dropdown.vue @@ -17,7 +17,7 @@
-
{{ option.text }}
+
{{ option.text }}
diff --git a/common/modules/utils.js b/common/modules/utils.js index 219d2eef..c308b847 100644 --- a/common/modules/utils.js +++ b/common/modules/utils.js @@ -83,6 +83,20 @@ export class Utils { return value; } + + static deepfreeze(object) { + if (typeof object === 'object' && object !== null) { + const properties = Object.getOwnPropertyNames(object); + + for (let property of properties) { + this.deepfreeze(object[property]); + } + + Object.freeze(object); + } + + return object; + } } export class FileUtils {