nw-skeleton

Source: app-wrapper/js/lib/appConfig.js

/**
 * @fileOverview AppConfig class file
 * @author Dino Ivankov <dinoivankov@gmail.com>
 * @version 1.3.1
 */

const _ = require('lodash');
const AppBaseClass = require('./appBase').AppBaseClass;

var _appWrapper;
var appState;

/**
 * A class for app config operations and manipulation
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberOf appWrapper
 *
 * @property {Object}           initialAppConfig    Object that stores initial app config that wrapper was initialized with
 * @property {Object}           baseConfig          Object that stores base app config
 * @property {Object}           appStateConfig      Object that stores app state config
 * @property {Object}           defaultConfig       Object that stores default config
 * @property {Object}           config              Object that stores app config
 * @property {Object}           userConfig          Object that stores user config
 * @property {Object}           previousConfig      Object that stores previous app config
 * @property {Boolean}          watchConfig         Flag to indicate whether to watch for config changes
 */
class AppConfig extends AppBaseClass {

    /**
     * Creates AppConfig instance using initial app config object
     *
     * @constructor
     * @param  {Object} initialAppConfig Initial app config object
     * @return {AppConfig}              Instance of AppConfig class
     */
    constructor(initialAppConfig) {
        super();

        if (window && window.getAppWrapper && _.isFunction(window.getAppWrapper)){
            _appWrapper = window.getAppWrapper();
            appState = _appWrapper.getAppState();
        }

        this.forceDebug = true;
        this.forceUserMessages = true;

        this.initialAppConfig = initialAppConfig;
        this.baseConfig = {};
        this.appStateConfig = {};
        this.defaultConfig = {};
        this.config = {};
        this.userConfig = {};
        this.previousConfig = {};
        this.watchConfig = true;

        this.boundMethods = {
            clearUserConfig: null
        };

        this.needsConfig = false;

        return this;
    }

    /**
     * Initializes application configuration
     *
     * @async
     * @return {Object} Application configuration object
     */
    async initializeConfig () {

        this.appStateConfig = require('../../../config/appWrapperConfig').config;

        let theConfig = _appWrapper.mergeDeep({}, this.appStateConfig, this.initialAppConfig);
        _.each(theConfig.configData.vars, function(value, key){
            if (!value.editable){
                theConfig.configData.uneditableConfig.push(key);
            } else {
                theConfig.configData.editableConfig.push(key);
            }
            if (!value.reload){
                theConfig.configData.noReloadConfig.push(key);
            } else {
                theConfig.configData.reloadConfig.push(key);
            }
        });
        this.config = _.cloneDeep(theConfig);
        this.baseConfig = _.cloneDeep(theConfig);
        this.defaultConfig = _.cloneDeep(theConfig);
        return theConfig;
    }

    /**
     * Returns storage variable name under which user config for this app is stored
     *
     * @return {string} storage config variable name
     */
    getConfigStorageName(){
        let mindOsUsers = this.getConfig('mindOsUsers');
        let configName = this.getConfig('appInfo.name') + '_config';

        let username = '';
        if (mindOsUsers){
            username = _appWrapper.getPlatformData().platform.userInfo.username;
        }
        if (username){
            configName += '_' + username;
        }
        configName = configName.replace(/[^A-Za-z0-9]+/g, '_');
        return configName;
    }


    /**
     * Loads user config from storage
     *
     * @async
     * @return {Object} Application config with user config data (if found)
     */
    async loadUserConfig () {
        let configName = this.getConfigStorageName();
        this.log('Loading user config...', 'group', []);
        let userConfig = await _appWrapper.getHelper('storage').get(configName);
        if (userConfig && _.keys(userConfig).length){
            this.log('Loaded {1} user config keys.', 'info', [_.keys(userConfig).length]);
            this.log('User config keys: "{1}".', 'debug', [_.keys(userConfig).join('", "')]);
            appState.hasUserConfig = true;
        } else {
            this.log('User config empty.', 'info', []);
            appState.hasUserConfig = false;
            userConfig = {};
        }
        this.userConfig = _.cloneDeep(userConfig);
        appState.userConfig = _.cloneDeep(userConfig);
        userConfig = _.merge({}, appState.config, userConfig);
        this.previousConfig = _.cloneDeep(userConfig);
        this.log('Loading user config...', 'groupend', []);
        return userConfig;
    }

    /**
     * Saves user config data to storage
     *
     * @async
     * @return {undefined}
     */
    async saveUserConfig () {
        let configName = this.getConfigStorageName();
        let utilHelper = _appWrapper.getHelper('util');
        let userConfig = utilHelper.difference(this.baseConfig, appState.config);

        let ignoreUserConfig = this.getConfig('configData.ignoreUserConfig');
        if (ignoreUserConfig && ignoreUserConfig.length){
            userConfig = _.omit(userConfig, ignoreUserConfig);
        }

        userConfig = _.omitBy(userConfig, (value) => {
            let returnValue = false;
            if (_.isObject(value) && Object.keys(value).length == 0){
                returnValue = true;
            }
            return returnValue;
        });

        let userConfigKeys = _.keys(userConfig);
        let userConfigKeyMap = utilHelper.propertyMap(userConfig);
        let oldUserConfigKeyMap = utilHelper.propertyMap(this.userConfig);
        let keyMapDiff = _.difference(userConfigKeyMap, oldUserConfigKeyMap);
        keyMapDiff = _.union(keyMapDiff, _.difference(oldUserConfigKeyMap, userConfigKeyMap));



        let reloadConfig = this.getConfig('configData.reloadConfig');
        let reloadChanges = _.intersection(keyMapDiff, reloadConfig);
        let shouldReload = reloadChanges.length > 0;

        try {
            if (userConfig && Object.keys(userConfigKeys).length){
                this.log('Saving user config...', 'debug', []);
                await _appWrapper.getHelper('storage').set(configName, userConfig);
                this.addUserMessage('Configuration data saved', 'debug', []);
                appState.hasUserConfig = true;
                this.userConfig = _.cloneDeep(userConfig);
                appState.userConfig = _.cloneDeep(userConfig);
                if (shouldReload){
                    _appWrapper.getHelper('modal').closeCurrentModal(true);
                    await _appWrapper.wait(appState.config.mediumPauseDuration);
                    _appWrapper.windowManager.reloadWindow(null, false);
                } else {
                    this.config = _.cloneDeep(appState.config);
                }
            } else {
                this.log('Removing user config...', 'debug', []);
                await _appWrapper.getHelper('storage').delete(configName);
                this.addUserMessage('Removed user config', 'debug', []);
                appState.hasUserConfig = false;
                this.userConfig = {};
                appState.userConfig = {};
                if (shouldReload){
                    _appWrapper.getHelper('modal').closeCurrentModal(true);
                    await _appWrapper.wait(appState.config.mediumPauseDuration);
                    _appWrapper.windowManager.reloadWindow(null, false);
                } else {
                    this.config = _.cloneDeep(appState.config);
                }
            }
        } catch (ex) {
            this.addUserMessage('Configuration data could not be saved - "{1}"', 'error', [ex.message], false, false);
        }
    }

    /**
     * Clears user config data from storage
     *
     * @async
     * @param  {Boolean} noReload Flag to indicate whether to prevent app window reload
     * @return {undefined}
     */
    async clearUserConfig (noReload) {
        let configName = this.getConfigStorageName();
        this.log('Clearing user config...', 'debug', []);
        try {
            await _appWrapper.getHelper('storage').delete(configName);
            this.addUserMessage('Configuration data cleared', 'debug', [], false,  false, true);
            if (!noReload){
                this.userConfig = {};
                appState.userConfig = {};
                appState.hasUserConfig = false;
                _appWrapper.getHelper('modal').closeCurrentModal(true);
                appState.appShuttingDown = true;
                appState.mainLoaderTitle = _appWrapper.appTranslations.translate('Please wait while application restarts...');
                setTimeout(() => {
                    _appWrapper.windowManager.reloadWindow(null, true, appState.mainLoaderTitle);
                }, 500);
            } else {
                appState.hasUserConfig = false;
                this.previousConfig = _.cloneDeep(appState.config);
                appState.config = _.cloneDeep(this.defaultConfig);
                this.userConfig = {};
                appState.userConfig = {};
                appState.hasUserConfig = false;
                _appWrapper.getHelper('modal').closeCurrentModal(true);
            }
        } catch (ex) {
            this.addUserMessage('Configuration data could not be cleared - "{1}"', 'error', [ex], false,  false);
        }
    }

    /**
     * Handler for clearUserConfig method
     *
     * @param  {Event} e    Event that triggered the handler
     * @return {undefined}
     */
    clearUserConfigHandler (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        _appWrapper.getHelper('modal').confirm(_appWrapper.appTranslations.translate('Are you sure?'), _appWrapper.appTranslations.translate('This will delete your saved configuration data.'), '', '', this.boundMethods.clearUserConfig);
    }

    /**
     * Checks whether given config var exists and returns true or false
     *
     * @param  {string}  name Name of config var
     * @return {Boolean}      True if var exists, false otherwise
     */
    hasConfigVar(name){
        return !_.isUndefined(appState.config[name]);
    }

    /**
     * Sets config var
     *
     * @async
     * @param {string}  name   Name of config variable
     * @param {mixed}   value  Value of config variable
     * @param {Boolean} noSave Prevents auto-saving of user config
     * @return {undefined}
     */
    async setConfigVar(name, value, noSave){
        this.watchConfig = false;
        _.set(appState.config, name, value);
        // appState.config[name] = value;
        if (!noSave){
            await this.saveUserConfig();
        }
        this.watchConfig = true;
    }

    /**
     * Sets entire app configuration to value from argument
     *
     * @async
     * @param {Object} data   New app configuration object
     * @param {Boolean} noSave Prevents auto-saving of user config
     * @return {undefined}
     */
    async setConfig(data, noSave){
        if (data && _.isObject(data)){
            this.watchConfig = false;
            appState.config = _.merge(appState.config, data);
            if (!noSave){
                await this.saveUserConfig();
            }
            this.watchConfig = true;
        }
    }

    /**
     * Event handler for opening config editor
     *
     * @async
     * @param  {Event} e    Event that triggered the method
     * @return {undefined}
     */
    async openConfigEditorHandler (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        this.openConfigEditor();
    }

    /**
     * Prepares config editor data object to be used in config-editor component
     *
     * @async
     * @return {Object} Config editor data
     */
    async prepareConfigEditorData () {
        appState.configEditorData = {};
        let configData = {};
        let keys = _.keys(appState.config);
        for(let i=0; i<keys.length; i++){
            let key = keys[i];
            let value = appState.config[key];
            if (key !== 'configData'){
                if (!_.includes(appState.config.configData.uneditableConfig, key)){
                    configData[key] = await this.prepareConfigEditorDataItem(value, key);
                }
            }
        }
        appState.configEditorData = _appWrapper.getHelper('util').sortObject(configData, true);
    }

    /**
     * Prepares single config editor data value for passing to config-editor component
     * Used recursively for config variables of type object or array
     *
     * @async
     * @param  {(array|Object)} value   Config var value
     * @return {(array|Object)}         Prepared config editor data value
     */
    async prepareConfigEditorDataItem (value) {
        if (_.isArray(value)){
            for(let i=0; i<value.length; i++) {
                let innerValue = value[i];
                let innerKey = i;
                value[innerKey] = await this.prepareConfigEditorDataItem(innerValue, innerKey);
            }
        } else if (_.isObject(value)){
            let keys = _.keys(value);
            for(let i=0; i<keys.length; i++){
                let innerKey = keys[i];
                let innerValue = value[innerKey];
                value[innerKey] = await this.prepareConfigEditorDataItem(innerValue, innerKey);
            }
        }
        return value;
    }

    /**
     * Opens config editor modal
     *
     * @async
     * @return {undefined}
     */
    async openConfigEditor () {
        // appState.status.noHandlingKeys = true;

        await this.prepareConfigEditorData();

        let modalHelper = _appWrapper.getHelper('modal');
        let modalOptions = {
            title: _appWrapper.appTranslations.translate('Config editor'),
            confirmButtonText: _appWrapper.appTranslations.translate('Save'),
            cancelButtonText: _appWrapper.appTranslations.translate('Cancel'),
        };
        appState.modalData.currentModal = modalHelper.getModalObject('configEditorModal', modalOptions);
        _appWrapper.getHelper('modal').modalBusy(_appWrapper.appTranslations.translate('Please wait...'));
        _appWrapper._confirmModalAction = this.saveConfig.bind(this);
        _appWrapper._cancelModalAction = function(evt){
            if (evt && evt.preventDefault && _.isFunction(evt.preventDefault)){
                evt.preventDefault();
            }
            _appWrapper.getHelper('modal').modalNotBusy();
            _appWrapper._cancelModalAction = _appWrapper.__cancelModalAction;
            return _appWrapper.__cancelModalAction();
        };
        _appWrapper.getHelper('modal').openCurrentModal();
    }

    /**
     * Saves current user config (if it has changed) to storage
     *
     * @async
     * @param  {Event} e  Event that triggered the method
     * @return {undefined}
     */
    async saveConfig (e) {
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        let form = appState.modalData.modalElement.querySelector('form');
        let newConfig = {};
        _.each(form, function(input){
            let currentConfig = newConfig;
            let appConfig = _.cloneDeep(appState.config);
            let dataPath = input.getAttribute('data-path');
            if (dataPath && dataPath.split){
                let pathChunks = _.drop(dataPath.split('.'), 1);
                let chunkCount = pathChunks.length - 1;
                _.each(pathChunks, function(pathChunk, i){
                    if (i == chunkCount){
                        if (input.getAttribute('type') == 'checkbox'){
                            currentConfig[pathChunk] = input.checked;
                        } else {
                            currentConfig[pathChunk] = input.value;
                        }
                    } else {
                        if (_.isUndefined(currentConfig[pathChunk])){
                            if (!_.isNaN(parseInt(pathChunks[i+1], 10))){
                                currentConfig[pathChunk] = [];
                            } else {
                                currentConfig[pathChunk] = {};
                            }
                        }
                    }
                    currentConfig = currentConfig[pathChunk];
                    appConfig = appConfig[pathChunk];
                });
            }
        });
        let oldConfig = _.cloneDeep(appState.config);
        let difference = _appWrapper.getHelper('util').difference(oldConfig, newConfig);

        if (difference && _.isObject(difference) && _.keys(difference).length){
            let finalConfig = _appWrapper.mergeDeep({}, appState.config, difference);
            appState.config = _.cloneDeep(finalConfig);
            await this.saveUserConfig();
            _appWrapper.getHelper('modal').closeCurrentModal();
        } else {
            _appWrapper.getHelper('modal').closeCurrentModal();
        }
    }

    /**
     * Watcher for config variable changes
     *
     * @async
     * @param  {mixed} oldValue     Old config var value
     * @param  {mixed} newValue     New config var value
     * @return {undefined}
     */
    async configChanged (oldValue, newValue){
        if (!appState.isDebugWindow){
            await this.asyncMessage({instruction: 'setConfig', data: {config: appState.config}});
            if (this.watchConfig){
                let utilHelper = _appWrapper.getHelper('util');
                let difference = utilHelper.difference(this.previousConfig, newValue);
                let diffKeys = Object.keys(difference);
                if (diffKeys && diffKeys.length){
                    this.log('{1} config variables have changed: "{2}"', 'debug', [diffKeys.length, diffKeys.join(', ')]);
                    await this.saveUserConfig();
                }
            }
            this.previousConfig = _.cloneDeep(appState.config);
        }
    }
}

exports.AppConfig = AppConfig;