nw-skeleton

Source: app-wrapper/js/helper/system/utilHelper.js

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

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

var _appWrapper;
var appState;

/**
 * UtilHelper class - contains various utility methods
 *
 * @class
 * @extends {appWrapper.AppBaseClass}
 * @memberof appWrapper.helpers.systemHelpers
 */
class UtilHelper extends AppBaseClass {

    /**
     * Creates UtilHelper instance
     *
     * @constructor
     * @return {UtilHelper}              Instance of UtilHelper class
     */
    constructor() {
        super();

        _appWrapper = window.getAppWrapper();
        appState = _appWrapper.getAppState();

        _.noop(_appWrapper);
        _.noop(appState);

        this.boundMethods = {
            prevent: null
        };

        return this;
    }

    /**
     * Returns random number between min and max
     *
     * @param  {Number} min Minimum value for random number
     * @param  {Number} max Maximum value for random number
     * @return {Number}     Random number
     */
    getRandom (min, max){
        let random = Math.floor(Math.random() * (max - min + 1)) + min;
        return random;
    }

    /**
     * Returns random string
     *
     * @param  {Number} size Size of the string (default is 4)
     * @return {string}      Random string
     */
    getRandomString (size) {
        if (!size){
            size = 4;
        }
        let randomString = '';
        do {
            randomString += Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
        } while (randomString.length < size);

        return randomString.substr(0, size);
    }

    /**
     * Converts object to JSON and returns it
     *
     * @param  {mixed} value        Value to convert
     * @param  {boolean} minified   Flag to minify JSON output
     * @return {string}             JSON representation of value from the argument
     */
    toJson (value, minified){
        if (!value){
            return '';
        }

        let cache = [];
        let replacer = function(key, val) {
            if (_.isObject(val) && val !== null) {
                if (cache.indexOf(val) !== -1) {
                    return '__circular__';
                }
                cache.push(val);
            }
            return val;
        };
        value = JSON.stringify(value, replacer);
        if (!minified){
            value = JSON.parse(value);
            value = JSON.stringify(value, ' ', 4);
        }
        cache = null;
        return value;
    }

    /**
     * Preloads image on page and calls callback when finished
     *
     * @param  {string}   imgSrc   Image URL
     * @param  {Function} callback Callback function
     * @return {undefined}
     */
    preloadImageCallback (imgSrc, callback){
        let imgEl = document.createElement('img');
        if (callback && _.isFunction(callback)){
            imgEl.onload = () => {
                imgEl.onload = null;
                imgEl = null;
                callback();
            };
        }
        imgEl.src = imgSrc;
    }

    /**
     * Finds duplicates in array and returns them
     *
     * @param  {array} arr Array to search
     * @return {array}     Array of duplicate values
     */
    getArrayDuplicates (arr){
        let sorted_arr = arr.slice().sort();
        let results = [];
        for (let i = 0; i < arr.length - 1; i++) {
            if (sorted_arr[i + 1] == sorted_arr[i]) {
                results.push(sorted_arr[i]);
            }
        }
        return results;
    }

    /**
     * Returns control object to be used in form-control component
     *
     * @param  {mixed}  configValue     Value of variable
     * @param  {string}  configName     Name of variable
     * @param  {string}  path           Path to variable in configuration
     * @param  {Object}  options        Object with additional control options
     * @param  {Boolean} isInner        Flag to indicate whether method called itself for complex vars
     * @return {Object}                 Form control object for the var
     */
    getControlObject (configValue, configName, path, options, isInner){
        if (!options){
            options = {};
        }

        options = _.defaults(options, {
            disabled: false,
            required: false,
            readonly: false,
            rowErrorText: ''
        });

        if (!path){
            path = configName;
        } else {
            path += '.' + configName;
        }
        if (!isInner){
            isInner = false;
        }
        let objValue;
        let configVar = {
            fullPath: 'appState.' + path,
            path: path,
            readonly: options.readonly,
            disabled: options.disabled,
            required: options.required,
            rowErrorText: options.rowErrorText,
            error: false,
            name: configName,
            value: _.cloneDeep(configValue),
            controlData: null
        };
        let innerPath = path.replace(/^config\./, '');
        if (appState.config.configData.vars[innerPath] && appState.config.configData.vars[innerPath]['control']){
            configVar.formControl = 'form-control-' + appState.config.configData.vars[innerPath]['control'];
            configVar.type = appState.config.configData.vars[innerPath]['type'];
            if (appState.config.configData.vars[innerPath]['controlData']){
                configVar.controlData = appState.config.configData.vars[innerPath]['controlData'];
            }
        } else {
            if (_.isBoolean(configValue)){
                configVar.formControl = 'form-control-checkbox';
                configVar.type = 'boolean';
            } else if (_.isString(configValue)){
                configVar.formControl = 'form-control-text';
                configVar.type = 'string';
            } else if (_.isArray(configValue)){
                configVar.formControl = 'form-control-array';
                configVar.type = 'array';
                objValue = [];
                let values = _.cloneDeep(configValue);
                _.each(values, (value, name) => {
                    objValue.push(this.getControlObject(value, name, path));
                });
                configVar.value = _.cloneDeep(objValue);
            } else if (_.isObject(configValue) && configValue instanceof RegExp){
                configVar.formControl = 'form-control-text';
                configVar.type = 'string';
            } else if (_.isObject(configValue)){
                configVar.formControl = 'form-control-object';
                configVar.type = 'object';
                objValue = [];
                let keys = _.keys(configValue);
                for(let i=0; i<keys.length;i++){
                    let name = keys[i];
                    let value;
                    try {
                        value = configValue[keys[i]];
                    } catch (ex){
                        value = configValue[keys[i]];
                    }
                    let newObjValue = this.getControlObject(value, name, path, {}, true);
                    objValue.push(newObjValue);
                }
                configVar.value = _.cloneDeep(objValue);
            } else {
                configVar.formControl = 'form-control-text';
                configVar.type = 'unknown';
            }
        }
        return configVar;
    }

    /**
     * Calculates deep difference between objects or arrays
     *
     * @param  {(Object|array)} original Original array or object
     * @param  {(Object|array)} modified Modified array or object
     * @return {(Object|array)}          Differences in arrays or objects
     */
    difference (original, modified) {
        let ret = {};
        let diff;
        for (let name in modified) {
            if (name in original) {
                if (_.isObject(modified[name]) && !_.isArray(modified[name])) {
                    diff = this.difference(original[name], modified[name]);
                    if (!_.isEmpty(diff)) {
                        ret[name] = diff;
                    }
                } else if (_.isArray(modified[name])) {
                    diff = this.difference(original[name], modified[name]);
                    if (!_.isEmpty(diff)) {
                        ret[name] = diff;
                    }
                } else if (!_.isEqualWith(original[name], modified[name], function(originalValue, modifiedValue){ return originalValue == modifiedValue; })) {
                    ret[name] = modified[name];
                }
            } else {
                ret[name] = modified[name];
            }
        }
        return ret;
    }

    /**
     * Returns var value using path and context from arguments
     *
     * @param  {string}     varPath     Path to the var (i.e. 'appConfig.theme')
     * @param  {Object}     context     Object that is base context for search (default: global)
     * @return {mixed}                  Found var value or undefined if no var found
     */
    getVarParent(varPath, context){
        if (!context){
            context = global;
        }
        let varChunks = varPath.split('.');
        let currentVar = false;
        if (varChunks && varChunks.length){
            currentVar = context[varChunks[0]];
        }
        if (!_.isUndefined(currentVar) && currentVar){
            for (let i=1; i<varChunks.length-1;i++){
                if (!_.isUndefined(currentVar[varChunks[i]])){
                    currentVar = currentVar[varChunks[i]];
                } else {
                    currentVar = false;
                }
            }
        }
        return currentVar;
    }

    /**
     * Returns var value using path and context from arguments
     *
     * @param  {string}     varPath     Path to the var (i.e. 'appConfig.theme')
     * @param  {Object}     context     Object that is base context for search (default: global)
     * @return {mixed}                  Found var value or undefined if no var found
     */
    getVar(varPath, context){
        if (!context){
            context = global;
        }
        let varChunks = varPath.split('.');
        let currentVar = false;
        if (varChunks && varChunks.length){
            currentVar = context[varChunks[0]];
        }
        if (!_.isUndefined(currentVar) && currentVar){
            for (let i=1; i<varChunks.length;i++){
                if (!_.isUndefined(currentVar[varChunks[i]])){
                    currentVar = currentVar[varChunks[i]];
                } else {
                    currentVar = false;
                }
            }
        }
        return currentVar;
    }

    /**
     * Sets var value based on path and context
     *
     * @param  {string}     varPath     Path to the var (i.e. 'appConfig.theme')
     * @param  {mixed}      value       New var value
     * @param  {Object}     context     Object that is base context for search (default: global)
     * @return {undefined}
     */
    setVar(varPath, value, context){
        if (!context){
            context = global;
        }
        let varChunks = varPath.split('.');
        let currentVar;
        let found = false;
        if (varChunks && varChunks.length){
            currentVar = context[varChunks[0]];
            if (!_.isUndefined(currentVar) && currentVar){
                if (varChunks.length > 1){
                    for (let i=1; i<varChunks.length-1;i++){
                        if (!_.isUndefined(currentVar[varChunks[i]])){
                            found = true;
                            currentVar = currentVar[varChunks[i]];
                        }
                    }
                    if (found){
                        currentVar[varChunks[varChunks.length-1]] = value;
                        return true;
                    }
                } else {
                    currentVar = value;
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Prevents default for passed event
     *
     * @param  {Event} e Event that should be prevented
     * @return {undefined}
     */
    prevent (e){
        e.preventDefault();
    }

    /**
     * Quotes string so it can be used in regex safely
     *
     * @param  {string} string String to quote
     * @return {string}        Save regex-quoted string
     */
    quoteRegex (string) {
        return string.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&').replace(/\s/g, '\\s');
    }

    /**
     * Handler for opening log file for log-viewer component picking dialog
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async pickLogFile (e) {
        let fileEl = e.target.parentNode.querySelector('.log-file-picker-input');
        fileEl.click();
    }

    /**
     * Handler for loading log file for log-viewer component
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async pickLogViewerFile (e) {
        let fileName = e.target.value;
        e.target.value = '';
        return await this.loadLogViewerFile(fileName);
    }

    /**
     * Loads log viewer file and displays it in modal log-viewer component
     *
     * @async
     * @param  {string} fileName Absolute path to log file
     * @return {undefined}
     */
    async loadLogViewerFile (fileName) {
        let fileValid = true;
        let messages;
        if (!fileName){
            this.addUserMessage(this.translate('Please pick file'), 'error', []);
            fileValid = false;
        } else {
            if (!await _appWrapper.fileManager.isFile(fileName)){
                this.addUserMessage(this.translate('File is not valid'), 'error', []);
                fileValid = false;
            } else {
                let fileContents = await _appWrapper.fileManager.loadFile(fileName);
                if (!fileContents){
                    this.addUserMessage(this.translate('Problem reading file'), 'error', []);
                    fileValid = false;
                } else {
                    try {
                        messages = JSON.parse(fileContents);
                    } catch (ex) {
                        this.addUserMessage(this.translate('Problem parsing file: "{1}"'), 'error', [ex.message]);
                        fileValid = false;
                    }
                }
            }
        }
        if (fileValid && messages && messages.length){
            let modalHelper = _appWrapper.getHelper('modal');
            let types = _.uniq(_.map(messages, (msg) => {
                return msg.type;
            }));
            let displayTypes = {};
            for (let i=0; i< types.length; i++){
                displayTypes[types[i]] = true;
            }

            let modalOptions = {
                title: this.translate('Log viewer'),
                confirmButtonText: this.translate('Close'),
                busy: true,
                file: fileName,
                fileMessages: _.map(messages, (msg) => {
                    if (msg.type == 'group' || msg.type == 'groupend' || msg.type == 'groupcollapsed'){
                        msg.type = 'info';
                    }
                    return msg;
                }),
                displayTypes: displayTypes,
                dataLoaded: true,
            };
            _appWrapper._confirmModalAction = this.confirmLogViewerModalAction;

            modalHelper.openModal('logViewerModal', modalOptions);
        }
    }

    /**
     * Log viewer modal action confirm
     *
     * @return {undefined}
     */
    confirmLogViewerModalAction () {
        _appWrapper.cancelModalAction();
    }

    /**
     * Gets stack counts for messages
     *
     * @param {Object} messages Message objects to count
     * @return {Number} Number of messages with stack data
     */
    getMessageStacksCount (messages) {
        let stackCount = 0;
        for(let i=0; i<messages.length; i++){
            if (messages[i].stack && messages[i].stack.length){
                stackCount++;
            }
        }
        return stackCount;
    }

    /**
     * Gets current stack state for messages
     *
     * @param {Object[]} messages Messages to get state for
     * @return {Number} Number of unopened stack messages
     */
    getMessageStacksState (messages) {
        let stacksCount = this.getMessageStacksCount(messages);
        let stacksOpen = 0;
        for(let i=0; i<messages.length; i++){
            if (messages[i].stack && messages[i].stack.length){
                if (messages[i].stackVisible){
                    stacksOpen++;
                }
            }
        }
        return stacksOpen >= stacksCount;
    }

    /**
     * Returns deep property map for object
     *
     * @param  {Object} obj     Object for mapping
     * @param  {string} prepend String to prepend for property map items
     * @return {string[]}       An array of property paths (i.e. ['a','a.b','a.c'])
     */
    propertyMap (obj, prepend){
        let keyMap = [];
        let objKeys = Object.keys(obj);
        if (!prepend){
            prepend = '';
        } else {
            prepend += '.';
        }
        for (let i=0; i<objKeys.length;i++){
            if (_.isArray(obj[objKeys[i]]) || _.isObject(obj[objKeys[i]])){
                keyMap = _.union(keyMap, this.propertyMap(obj[objKeys[i]], prepend + objKeys[i]));
            } else {
                keyMap.push(prepend + objKeys[i]);
            }
        }
        return keyMap;
    }

    /**
     * Gets deep property map with values
     *
     * @param  {Object} obj     Object for mapping
     * @param  {string} prepend String to prepend for property map items
     * @return {Object}         Property map with values (i.e {'a.b': 'c','d':'e'})
     */
    propertyValuesMap (obj, prepend){
        let keyMap = [];
        if (obj && _.isObject(obj)){
            let objKeys = Object.keys(obj);
            if (!prepend){
                prepend = '';
            } else {
                prepend += '.';
            }
            for (let i=0; i<objKeys.length;i++){
                if (_.isArray(obj[objKeys[i]]) || _.isObject(obj[objKeys[i]])){
                    keyMap = _.merge(keyMap, this.propertyValuesMap(obj[objKeys[i]], objKeys[i]));
                } else {
                    keyMap[prepend + objKeys[i]] = obj[objKeys[i]];
                }
            }
        }
        return keyMap;
    }

    /**
     * Empty method placeholder
     *
     * @return {string} Empty string
     */
    noop () {
        return '';
    }

    /**
     * Sets object values using form data from passed form element
     *
     * @async
     * @param {HTMLElement} form            Form element
     * @param {Object}      source          Source object to use for final data
     * @param {boolean}     keepFirstChunk  Flag to indicate whether to keep first chunk from form elements data-path attributes
     * @return {Object}                     Object with populated data from form
     */
    async setObjectValuesFromForm (form, source, keepFirstChunk) {
        if (!source){
            source = {};
        }
        let newObject = _.cloneDeep(source);
        let finalObject = {};
        _.each(form, (input) => {
            if (!input.hasClass('modal-dialog-button') && input.hasAttribute('data-path')){
                let path = input.getAttribute('data-path');
                var currentObject = newObject;
                var dataPath = path;
                if (dataPath && dataPath.split){
                    var pathChunks;
                    if (keepFirstChunk){
                        pathChunks = dataPath.split('.');
                    } else {
                        pathChunks = _.drop(dataPath.split('.'), 1);
                    }
                    var chunkCount = pathChunks.length - 1;
                    _.each(pathChunks, (pathChunk, i) => {
                        if (i == chunkCount){
                            if (input.getAttribute('type') == 'checkbox'){
                                currentObject[pathChunk] = input.checked;
                            } else {
                                currentObject[pathChunk] = input.value;
                            }
                        } else {
                            if (_.isUndefined(currentObject[pathChunk])){
                                currentObject[pathChunk] = {};
                            }
                        }
                        currentObject = currentObject[pathChunk];
                    });
                }
            }
        });
        var oldObject = _.cloneDeep(source);
        var difference = this.difference(oldObject, newObject);

        if (difference && _.isObject(difference) && _.keys(difference).length){
            finalObject = _appWrapper.mergeDeep({}, source, difference);
        } else {
            finalObject = _.cloneDeep(source);
        }
        return finalObject;
    }

    /**
     * Handler that saves user message or debug logs
     *
     * @async
     * @param  {Event} e Event that triggered the method
     * @return {undefined}
     */
    async confirmSaveLogAction (e){
        if (e && e.preventDefault && _.isFunction(e.preventDefault)){
            e.preventDefault();
        }
        let modalHelper = _appWrapper.getHelper('modal');
        modalHelper.setModalVar('saveFileError', false);
        modalHelper.clearModalMessages();

        var filePath = modalHelper.getModalVar('file');
        var saveAll = modalHelper.getModalVar('saveAll');
        let overwriteAction = modalHelper.getModalVar('overwriteAction');
        let append = overwriteAction == 'append';

        if (filePath){
            modalHelper.modalBusy();
            await _appWrapper.wait(this.getConfig('shortPauseDuration'));
            var saved = true;
            var writeMode = 'w';

            let previousMessages = [];
            let canAppend = true;
            if (append){
                let fileContents = await _appWrapper.fileManager.readFileSync(filePath, {encoding:'utf8'});
                if (fileContents){
                    try {
                        previousMessages = JSON.parse(fileContents);
                    } catch (ex) {
                        canAppend = false;
                        this.addModalMessage('Can not parse file contents for appending!', 'error', []);
                        this.log('Can not parse file contents for appending!', 'error', []);
                    }
                }
            }

            if (append && !canAppend){
                modalHelper.modalNotBusy();
                return;
            }

            let modalName = modalHelper.getModalVar('name');
            let messages;
            let saveStacks;
            if (modalName == 'save-user-messages'){
                messages = _.cloneDeep(appState.userMessages);
                if (saveAll){
                    messages = _.cloneDeep(appState.allUserMessages);
                }
                saveStacks = this.getConfig('userMessages.saveStacksToFile', false);
            } else {
                messages = _.cloneDeep(appState.debugMessages);
                if (saveAll){
                    messages = _.cloneDeep(appState.allDebugMessages);
                }
                saveStacks = this.getConfig('debug.saveStacksToFile', false);
            }



            let processedMessages = _.map(messages, (message) => {
                if (message.stackVisible){
                    message.stackVisible = false;
                }
                delete message.force;
                delete message.active;
                if (!saveStacks){
                    delete message.stackVisible;
                    delete message.stack;
                }
                return message;
            });

            processedMessages = _.union(previousMessages, processedMessages);

            var data = JSON.stringify(processedMessages, ' ', 4);

            try {
                await _appWrapper.fileManager.writeFileSync(filePath, data, {
                    encoding: 'utf8',
                    mode: 0o775,
                    flag: writeMode
                });
            } catch (e) {
                saved = false;
                this.log('Problem saving log file "{1}" - {2}', 'error', [filePath, e]);
            }

            if (saved){
                if (appState.isDebugWindow){
                    this.log('Log file saved successfully', 'info', [], true);
                } else {
                    this.addUserMessage('Log file saved successfully', 'info', [], true,  false, true);
                }
                _appWrapper._confirmModalAction = () => {
                    modalHelper.closeCurrentModal();
                };
                modalHelper.setModalVars({
                    title: this.translate('Operation successful'),
                    body: this.translate('Log file saved successfully'),
                    bodyComponent: 'modal-body',
                    confirmButtonText: this.translate('Close'),
                    autoCloseTime: 5000,
                });
                modalHelper.autoCloseModal();
            } else {
                if (appState.isDebugWindow){
                    this.log('Log saving failed', 'error', [], true);
                } else {
                    this.addUserMessage('Log saving failed', 'error', [], false,  false);
                }
                this.addModalMessage('Log saving failed', 'error', [], false,  false);
            }
            modalHelper.modalNotBusy();
        }
    }

    /**
     * Returns UUID string
     *
     * @return {string} UUID string
     */
    uuid () {
        return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
    }

    /**
     * Opens external url in system default browser
     *
     * @param  {string} url Url to open
     * @return {undefined}
     */
    openExternalUrl (url){
        nw.Shell.openExternal(url);
    }

    /**
     * Sorts object properties by key and returns sorted copy
     *
     * @param  {Object}  object Object to sort
     * @param  {Boolean} deep   Perform deep sorting
     * @return {Object}         Sorted object copy
     */
    sortObject(object, deep) {
        let sorted = {};
        let key;
        let array = [];

        for (key in object) {
            if (object.hasOwnProperty(key)) {
                array.push(key);
            }
        }

        array.sort();

        for (key = 0; key < array.length; key++) {
            sorted[array[key]] = object[array[key]];
            if (deep && _.isObject(sorted[array[key]])){
                sorted[array[key]] = this.sortObject(sorted[array[key]], deep);
            }
        }
        return sorted;
    }
}

exports.UtilHelper = UtilHelper;