<commit_before>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Dominik
// Date:      02.12.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

/**
 * A constant value for horizontal alignment.
 *
 * @type String
 */
M.HORIZONTAL = 'horizontal';

/**
 * A constant value for vertical alignment.
 *
 * @type String
 */
M.VERTICAL = 'vertical';

/**
 * @class
 *
 * A button group is a vertically or / and horizontally aligned group of buttons. There
 * are basically three different types of a button group:
 *
 * - horizontally aligned buttons
 *     1 - 2 - 3
 *
 * - vertically aligned buttons
 *     1
 *     |
 *     2
 *     |
 *     3
 *
 * - horizontally and vertically aligned buttons
 *     1 - 2
 *     |   |
 *     3 - 4
 *
 * @extends M.View
 */
M.ButtonGroupView = M.View.extend(
/** @scope M.ButtonGroupView.prototype */ {

    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.ButtonGroupView',

    /**
     * This property determines whether to render the button group horizontally
     * or vertically. Default: horizontal.
     *
     * Possible values are:
     * - M.HORIZONTAL: horizontal
     * - M.VERTICAL: vertical
     *
     * @type String
     */
    direction: M.HORIZONTAL,

    /**
     * Determines whether to display the button group view 'inset' or at full width.
     *
     * @type Boolean
     */
    isInset: YES,

    /**
     * This property, if set, defines how many buttons are rendered per line. If there
     * are more buttons defined that fitting into one line, the following buttons are
     * rendered into a new line. Make sure, the number of your buttons is divisible by
     * the number of buttons per line, since only full lines are displayed. So if you
     * for example specify 5 buttons and 2 buttons per line, the fifth button won't be
     * visible.
     *
     * If e.g. 4 buttons are specified and this property is set to 2, the rendering will
     * be as follows:
     *
     *     1 -- 2
     *     3 -- 4
     *
     * @type Number
     */
    buttonsPerLine: null,

    /**
     * This property is used to internally store the number of lines that are necessary
     * to render all buttons according to the buttonsPerLine property.
     *
     * @private
     * @type Number
     */
    numberOfLines: null,

    /**
     * This property refers to the currently rendered line, if there is more than one.
     *
     * @private
     * @type Number
     */
    currentLine: null,

    /**
     * This property contains an array of html ids referring to the several lines of grouped
     * buttons, if there is more than one at all.
     *
     * @private
     * @type Array
     */
    lines: null,

    /**
     * This property contains a reference to the currently selected button.
     *
     * @private
     * @type Object
     */
    activeButton: null,

    /**
     * Renders a button group as a div container and calls the renderChildViews
     * method to render the included buttons.
     *
     * @private
     * @returns {String} The button group view's html representation.
     */
    render: function() {
        /* check if multiple lines are necessary before rendering */
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            if(this.buttonsPerLine && this.buttonsPerLine < childViews.length) {
                var numberOfButtons = 0;
                for(var i in childViews) {
                    if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                        numberOfButtons = numberOfButtons + 1;
                    }
                }
                if(this.buttonsPerLine < numberOfButtons) {
                    this.numberOfLines = M.Math.round(numberOfButtons / this.buttonsPerLine, M.FLOOR);
                }
            }
        }

        /* if there are multiple lines, render multiple horizontally aligned button groups */
        if(this.numberOfLines) {
            /* set the direction to horizontally, no matter what it was set to before */
            this.direction = M.HORIZONTAL;

            /* this is a wrapper for the multiple button groups.
               if it is not inset, assign css class 'ui-listview' for clearing the padding of the surrounding element */
            this.html += '
';

            /* create a button group for every line */
            this.lines = [];
            for(var i = 0; i < this.numberOfLines; i++) {
                this.currentLine = i + 1;
                /* store current line in lines property for use in renderChildViews() */
                this.lines.push(this.id + '_' + i);

                this.html += '
 0 && i < this.numberOfLines - 1) {
                    this.html += this.isInset ? ' style="margin:0px 0px 0px 0px"' : ' style="margin:0px"';
                } else if(i < this.numberOfLines) {
                    this.html += this.isInset ? ' style="margin-top:0px"' : ' style="margin:0px"';
                }

                this.html += '>';

                /* render the buttons for the current line */
                this.renderChildViews();

                this.html += '
';
            }
            this.html += '
';
        } else {
            this.html += '
';

            this.renderChildViews();

            this.html += '
';
        }

        return this.html;
    },

    /**
     * Triggers render() on all children of type M.ButtonGroupView.
     *
     * @private
     */
    renderChildViews: function() {
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            var currentButtonIndex = 0;

            for(var i in childViews) {
                if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                    currentButtonIndex = currentButtonIndex + 1;

                    if(!this.numberOfLines || M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) === this.currentLine) {

                        var button = this[childViews[i]];
                        /* reset buttons html, to make sure it doesn't get rendered twice if this is multi button group */
                        button.html = '';

                        button.parentView = this;
                        button.internalTarget = this;
                        button.internalAction = 'setActiveButton';

                        /* check if button has own target / action, otherwise use target / action from button group */
                        if(!button.target) {
                            if(this.target && this.action) {
                                button.target = this.target;
                                button.action = this.action;
                            }
                        }

                        /* if the buttons are horizontally aligned, compute their width depending on the number of buttons
                           and set the right margin to '-2px' since the jQuery mobile default would cause an ugly gap to
                           the right of the button group */
                        if(this.direction === M.HORIZONTAL) {
                            button.cssStyle = 'margin-right:-2px;width:' + 100 / (this.numberOfLines ? this.buttonsPerLine : childViews.length) + '%';
                        }

                        /* finally render the button and add it to the button groups html */
                        this.html += this[childViews[i]].render();
                    }
                } else {
                    M.Logger.log('childview of button group is no button.', M.WARN);
                }
            }
        }
    },

    /**
     * This method themes the button group and activates one of the included buttons
     * if its isActive property is set.
     *
     * @private
     */
    theme: function() {
        /* if there are multiple lines of buttons, it's getting heavy */
        if(this.numberOfLines) {

            /* iterate through all lines */
            for(var line in this.lines) {
                line = parseInt(line);

                /* style the current line */
                $('#' + this.lines[line]).controlgroup();
                var childViews = $.trim(this.childViews).split(' ');
                var currentButtonIndex = 0;

                /* iterate through all buttons */
                for(var i in childViews) {
                    i = parseInt(i);
                    if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                        currentButtonIndex = currentButtonIndex + 1;
                        var currentLine = M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) - 1;
                        var button = this[childViews[i]];

                        /* if the button belongs to the current line adjust its styling according to its position,
                           e.g. the first button in the first row gets the css class 'ui-corner-tl' (top left). */
                        if(line === currentLine) {

                            /* first line */
                            if(line === 0) {
                                /* first button */
                                if(currentButtonIndex === 1) {
                                    $('#' + button.id).removeClass('ui-corner-left');
                                    if(this.isInset) {
                                        $('#' + button.id).addClass('ui-corner-tl');
                                    }
                                /* last button */
                                } else if(currentButtonIndex === this.buttonsPerLine) {
                                    $('#' + button.id).removeClass('ui-corner-right');
                                    if(this.isInset) {
                                        $('#' + button.id).addClass('ui-corner-tr');
                                    }
                                }
                            /* last line */
                            } else if(line === this.numberOfLines - 1) {
                                /* first button */
                                if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) {
                                    $('#' + button.id).removeClass('ui-corner-left');
                                    $('#' + button.id).addClass('ui-corner-bl');
                                /* last button */
                                } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) {
                                    $('#' + button.id).removeClass('ui-corner-right');
                                    $('#' + button.id).addClass('ui-corner-br');
                                }
                            /* all other lines */
                            } else {
                                /* first button */
                                if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) {
                                    $('#' + button.id).removeClass('ui-corner-left');
                                /* last button */
                                } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) {
                                    $('#' + button.id).removeClass('ui-corner-right');
                                }
                            }
                        }
                    }
                }
            }
        /* if there is only on row, simply style that button group */
        } else {
            $('#' + this.id).controlgroup();
        }

        /* iterate through all buttons and activate on of them, according to the button's isActive property */
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            for(var i in childViews) {
                if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                    var button = this[childViews[i]];
                    if(button.isActive) {
                        this.setActiveButton(button.id);
                        break;
                    }
                }
            }
        }
    },

    /**
     * This method returns the currently selected button of this button group. If no
     * button is selected, null is returned.
     *
     * @returns {M.ButtonView} The currently active button of this button group.
     */
    getActiveButton: function() {
        return this.activeButton;
    },

    /**
     * This method activates one button within the button group.
     *
     * @param {M.ButtonView, String} id The button to be set active or its id.
     */
    setActiveButton: function(id) {
        this.activeButton = null;
        $('#' + this.id).find('a').each(function() {
            var button = M.ViewManager.getViewById($(this).attr('id'));
            button.removeCssClass('ui-btn-active');
            button.isActive = NO;
        });

        var button = M.ViewManager.getViewById(id);
        if(!button) {
            if(id && typeof(id) === 'object' && id.type === 'M.ButtonView') {
                button = id;
            }
        }
        if(button) {
            button.addCssClass('ui-btn-active');
            button.isActive = YES;
            this.activeButton = button;
        }
    }

});<commit_msg>Added isCompact property and refactored rendering / theming to make use of this property<commit_after>// ==========================================================================
// Project:   The M-Project - Mobile HTML5 Application Framework
// Copyright: (c) 2010 M-Way Solutions GmbH. All rights reserved.
// Creator:   Dominik
// Date:      02.12.2010
// License:   Dual licensed under the MIT or GPL Version 2 licenses.
//            http://github.com/mwaylabs/The-M-Project/blob/master/MIT-LICENSE
//            http://github.com/mwaylabs/The-M-Project/blob/master/GPL-LICENSE
// ==========================================================================

/**
 * A constant value for horizontal alignment.
 *
 * @type String
 */
M.HORIZONTAL = 'horizontal';

/**
 * A constant value for vertical alignment.
 *
 * @type String
 */
M.VERTICAL = 'vertical';

/**
 * @class
 *
 * A button group is a vertically or / and horizontally aligned group of buttons. There
 * are basically three different types of a button group:
 *
 * - horizontally aligned buttons
 *     1 - 2 - 3
 *
 * - vertically aligned buttons
 *     1
 *     |
 *     2
 *     |
 *     3
 *
 * - horizontally and vertically aligned buttons
 *     1 - 2
 *     |   |
 *     3 - 4
 *
 * @extends M.View
 */
M.ButtonGroupView = M.View.extend(
/** @scope M.ButtonGroupView.prototype */ {

    /**
     * The type of this object.
     *
     * @type String
     */
    type: 'M.ButtonGroupView',

    /**
     * This property determines whether to render the button group horizontally
     * or vertically. Default: horizontal.
     *
     * Possible values are:
     * - M.HORIZONTAL: horizontal
     * - M.VERTICAL: vertical
     *
     * @type String
     */
    direction: M.HORIZONTAL,

    /**
     * Determines whether to display the button group view 'inset' or at full width.
     *
     * @type Boolean
     */
    isInset: YES,

    /**
     * Determines whether to display the button group compact, i.e. without top/bottom
     * margin. This property only is relevant in combination with multiple lines of
     * buttons (c.p.: buttonsPerLine property).
     *
     * @type Boolean
     */
    isCompact: YES,

    /**
     * This property, if set, defines how many buttons are rendered per line. If there
     * are more buttons defined that fitting into one line, the following buttons are
     * rendered into a new line. Make sure, the number of your buttons is divisible by
     * the number of buttons per line, since only full lines are displayed. So if you
     * for example specify 5 buttons and 2 buttons per line, the fifth button won't be
     * visible.
     *
     * If e.g. 4 buttons are specified and this property is set to 2, the rendering will
     * be as follows:
     *
     *     1 -- 2
     *     3 -- 4
     *
     * @type Number
     */
    buttonsPerLine: null,

    /**
     * This property is used to internally store the number of lines that are necessary
     * to render all buttons according to the buttonsPerLine property.
     *
     * @private
     * @type Number
     */
    numberOfLines: null,

    /**
     * This property refers to the currently rendered line, if there is more than one.
     *
     * @private
     * @type Number
     */
    currentLine: null,

    /**
     * This property contains an array of html ids referring to the several lines of grouped
     * buttons, if there is more than one at all.
     *
     * @private
     * @type Array
     */
    lines: null,

    /**
     * This property contains a reference to the currently selected button.
     *
     * @private
     * @type Object
     */
    activeButton: null,

    /**
     * Renders a button group as a div container and calls the renderChildViews
     * method to render the included buttons.
     *
     * @private
     * @returns {String} The button group view's html representation.
     */
    render: function() {
        /* check if multiple lines are necessary before rendering */
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            if(this.buttonsPerLine && this.buttonsPerLine < childViews.length) {
                var numberOfButtons = 0;
                for(var i in childViews) {
                    if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                        numberOfButtons = numberOfButtons + 1;
                    }
                }
                if(this.buttonsPerLine < numberOfButtons) {
                    this.numberOfLines = M.Math.round(numberOfButtons / this.buttonsPerLine, M.FLOOR);
                }
            }
        }

        /* if there are multiple lines, render multiple horizontally aligned button groups */
        if(this.numberOfLines) {
            /* set the direction to horizontally, no matter what it was set to before */
            this.direction = M.HORIZONTAL;

            /* this is a wrapper for the multiple button groups.
               if it is not inset, assign css class 'ui-listview' for clearing the padding of the surrounding element */
            this.html += '
';

            /* create a button group for every line */
            this.lines = [];
            for(var i = 0; i < this.numberOfLines; i++) {
                this.currentLine = i + 1;
                /* store current line in lines property for use in renderChildViews() */
                this.lines.push(this.id + '_' + i);

                this.html += '
 0 && i < this.numberOfLines - 1) {
                        this.html += this.isInset ? ' style="margin:0px 0px 0px 0px"' : ' style="margin:0px"';
                    } else if(i < this.numberOfLines) {
                        this.html += this.isInset ? ' style="margin-top:0px"' : ' style="margin:0px"';
                    }
                }

                this.html += '>';

                /* render the buttons for the current line */
                this.renderChildViews();

                this.html += '
';
            }
            this.html += '
';
        } else {
            this.html += '
';

            this.renderChildViews();

            this.html += '
';
        }

        return this.html;
    },

    /**
     * Triggers render() on all children of type M.ButtonGroupView.
     *
     * @private
     */
    renderChildViews: function() {
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            var currentButtonIndex = 0;

            for(var i in childViews) {
                if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                    currentButtonIndex = currentButtonIndex + 1;

                    if(!this.numberOfLines || M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) === this.currentLine) {

                        var button = this[childViews[i]];
                        /* reset buttons html, to make sure it doesn't get rendered twice if this is multi button group */
                        button.html = '';

                        button.parentView = this;
                        button.internalTarget = this;
                        button.internalAction = 'setActiveButton';

                        /* check if button has own target / action, otherwise use target / action from button group */
                        if(!button.target) {
                            if(this.target && this.action) {
                                button.target = this.target;
                                button.action = this.action;
                            }
                        }

                        /* if the buttons are horizontally aligned, compute their width depending on the number of buttons
                           and set the right margin to '-2px' since the jQuery mobile default would cause an ugly gap to
                           the right of the button group */
                        if(this.direction === M.HORIZONTAL) {
                            button.cssStyle = 'margin-right:-2px;width:' + 100 / (this.numberOfLines ? this.buttonsPerLine : childViews.length) + '%';
                        }

                        /* finally render the button and add it to the button groups html */
                        this.html += this[childViews[i]].render();
                    }
                } else {
                    M.Logger.log('childview of button group is no button.', M.WARN);
                }
            }
        }
    },

    /**
     * This method themes the button group and activates one of the included buttons
     * if its isActive property is set.
     *
     * @private
     */
    theme: function() {
        /* if there are multiple lines of buttons, it's getting heavy */
        if(this.numberOfLines) {

            /* iterate through all lines */
            for(var line in this.lines) {
                line = parseInt(line);

                /* style the current line */
                $('#' + this.lines[line]).controlgroup();
                var childViews = $.trim(this.childViews).split(' ');
                var currentButtonIndex = 0;

                /* if isCompact, iterate through all buttons */
                if(this.isCompact) {
                    for(var i in childViews) {
                        i = parseInt(i);
                        if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                            currentButtonIndex = currentButtonIndex + 1;
                            var currentLine = M.Math.round(currentButtonIndex / this.buttonsPerLine, M.CEIL) - 1;
                            var button = this[childViews[i]];

                            /* if the button belongs to the current line adjust its styling according to its position,
                               e.g. the first button in the first row gets the css class 'ui-corner-tl' (top left). */
                            if(line === currentLine) {

                                /* first line */
                                if(line === 0) {
                                    /* first button */
                                    if(currentButtonIndex === 1) {
                                        $('#' + button.id).removeClass('ui-corner-left');
                                        if(this.isInset) {
                                            $('#' + button.id).addClass('ui-corner-tl');
                                        }
                                    /* last button */
                                    } else if(currentButtonIndex === this.buttonsPerLine) {
                                        $('#' + button.id).removeClass('ui-corner-right');
                                        if(this.isInset) {
                                            $('#' + button.id).addClass('ui-corner-tr');
                                        }
                                    }
                                /* last line */
                                } else if(line === this.numberOfLines - 1) {
                                    /* first button */
                                    if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) {
                                        $('#' + button.id).removeClass('ui-corner-left');
                                        $('#' + button.id).addClass('ui-corner-bl');
                                    /* last button */
                                    } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) {
                                        $('#' + button.id).removeClass('ui-corner-right');
                                        $('#' + button.id).addClass('ui-corner-br');
                                    }
                                /* all other lines */
                                } else {
                                    /* first button */
                                    if(currentButtonIndex === (currentLine * this.buttonsPerLine) + 1) {
                                        $('#' + button.id).removeClass('ui-corner-left');
                                    /* last button */
                                    } else if(currentButtonIndex === ((currentLine + 1) * this.buttonsPerLine)) {
                                        $('#' + button.id).removeClass('ui-corner-right');
                                    }
                                }
                            }
                        }
                    }
                }
            }
        /* if there is only on row, simply style that button group */
        } else {
            $('#' + this.id).controlgroup();
        }

        /* iterate through all buttons and activate on of them, according to the button's isActive property */
        if(this.childViews) {
            var childViews = $.trim(this.childViews).split(' ');
            for(var i in childViews) {
                if(this[childViews[i]] && this[childViews[i]].type === 'M.ButtonView') {
                    var button = this[childViews[i]];
                    if(button.isActive) {
                        this.setActiveButton(button.id);
                        break;
                    }
                }
            }
        }
    },

    /**
     * This method returns the currently selected button of this button group. If no
     * button is selected, null is returned.
     *
     * @returns {M.ButtonView} The currently active button of this button group.
     */
    getActiveButton: function() {
        return this.activeButton;
    },

    /**
     * This method activates one button within the button group.
     *
     * @param {M.ButtonView, String} id The button to be set active or its id.
     */
    setActiveButton: function(id) {
        this.activeButton = null;
        $('#' + this.id).find('a').each(function() {
            var button = M.ViewManager.getViewById($(this).attr('id'));
            button.removeCssClass('ui-btn-active');
            button.isActive = NO;
        });

        var button = M.ViewManager.getViewById(id);
        if(!button) {
            if(id && typeof(id) === 'object' && id.type === 'M.ButtonView') {
                button = id;
            }
        }
        if(button) {
            button.addCssClass('ui-btn-active');
            button.isActive = YES;
            this.activeButton = button;
        }
    }

});<|endoftext|>


<commit_before>'use strict';
const path = require('path');
const _ = require('lodash');
const childProcess = require('child_process');
const { app, ipcMain } = require('electron');
const Config = require('electron-config');
const notifier = require('node-notifier');
const sendStat = require('./send_stat');

const spawn = childProcess.spawn;
const exec = childProcess.exec;
const Convert = require('ansi-to-html');

const convert = new Convert();
const config = new Config();

const isWin = process.platform === 'win32';

const iconSuccessPath = path.join(__dirname, '../images/iconSuccess.png');
const iconErrorPath = path.join(__dirname, '../images/iconError.png');
// const iconSuccess = nativeImage.createFromPath(path.join(__dirname, '../images/iconSuccess.png'));
// const iconError = nativeImage.createFromPath(path.join(__dirname, '../images/iconError.png'));

notifier.on('click', () => {
  console.log('notifier on click');
  global.CP_WIN.show();
});

function guid() {
  return 'xyxyxyxy'.replace(/[xy]/g, (c) => {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); // eslint-disable-line
    return v.toString(16);
  });
}

// Ensure immutable, don't know if config.get always returns new data.
const cmds = _.cloneDeep(config.get('cmds') || []);
for (const c of cmds) {
  c.outputs = [];
}

const cmdHash = {};
cmds.forEach(cmd => cmdHash[cmd.id] = cmd); // eslint-disable-line

function stopCmd(cmd) {
  console.log('stopping cmd2: ', cmd.id);
  if (!cmd || !cmd.process) return;
  try {
    if (isWin) {
      exec('taskkill /pid ' + cmd.process.pid + ' /T /F');
    } else {
      cmd.process.destroy();
    }
  } catch (e) {
    console.log('failed to kill the process: ', e);
  }
}

function stopAllCmds() {
  console.log('app will quit.');
  for (const cmd of cmds) {
    if (cmd.process) {
      stopCmd(cmd);
    }
  }
}

function getEnvPath() {
  let envPath = config.get('envPath');
  if (envPath) {
    if (isWin) {
      envPath = ';' + envPath;
    } else {
      envPath = ':/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/local/share/node/bin:' + envPath;
    }
  } else {
    envPath = ':/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/local/share/node/bin';
  }
  return envPath;
}

function getOutputRowsLimit() {
  return config.get('outputRowsLimit') || 100;
}
/* ==================== Get Init Data ============================== */
ipcMain.on('GET_INIT_DATA', (evt) => {
  evt.sender.send('SET_INIT_DATA', {
    appVersion: app.getVersion(),
    envPath: config.get('envPath'),
    outputRowsLimit: getOutputRowsLimit(),
    isWin,
    cmds: cmds.map(c => Object.assign(_.pick(c, ['id', 'name', 'cmd', 'cwd', 'sudo', 'outputs', 'url', 'finishPrompt']), { status: c.process ? 'running' : 'stopped' })),
  });
});

/* ==================== Save Command ============================== */
ipcMain.on('SAVE_CMD', (evt, data, cmdId) => {
  console.log('saving cmd');
  const cmd = { id: cmdId || guid() };
  Object.assign(cmd, {
    name: data.name,
    cmd: data.cmd,
    sudo: data.sudo,
    finishPrompt: data.finishPrompt,
    cwd: data.cwd || null,
    url: data.url || null,
  });
  const newCmds = config.get('cmds') || [];
  if (cmdId) {
    const i = newCmds.findIndex(c => c.id === cmdId);
    newCmds.splice(i, 1, cmd);
  } else {
    newCmds.push(cmd);
  }
  config.set('cmds', newCmds);

  // Notify UI
  const curr = cmdId ? cmds.find(c => c.id === cmdId) : { status: 'stopped', outputs: [] };
  Object.assign(curr, cmd);
  if (!cmdId) {
    cmds.push(curr);
  }

  cmdHash[cmd.id] = curr;
  evt.sender.send('SAVE_CMD_SUCCESS', cmd);

  sendStat({ type: cmdId ? 'update_cmd' : 'add_cmd', cmd_count: cmds.length });
});

/* ==================== Delete Command ============================== */
ipcMain.on('DELETE_CMD', (evt, cmdId) => {
  console.log('deleting cmd');
  // Stop the process if needed
  const cmd = cmdHash[cmdId];
  if (cmd.process) {
    stopCmd(cmd);
  }
  // Save to config
  const newCmds = config.get('cmds') || [];
  _.remove(newCmds, c => c.id === cmdId);
  config.set('cmds', newCmds);

  // Remove from store
  _.remove(cmds, c => c.id === cmdId);
  delete cmdHash[cmdId];

  evt.sender.send('DELETE_CMD_SUCCESS', cmdId);

  sendStat({ type: 'del_cmd', cmd_count: cmds.length });
});

/* ==================== Reorder Commands ============================== */
ipcMain.on('REORDER_CMDS', (evt, cmdIds) => {
  console.log('reordering cmds');

  // Save to config
  let newCmds = config.get('cmds') || [];
  const groups = _.groupBy(newCmds, 'id');
  newCmds = cmdIds.map(id => groups[id][0]);
  config.set('cmds', newCmds);

  // Update store order
  for (let i = 0; i < cmds.length; i++) {
    cmds[i] = cmdHash[cmdIds[i]];
  }

  evt.sender.send('REORDER_CMDS_SUCCESS', cmdIds);

  sendStat({ type: 'reorder_cmds', cmd_count: cmds.length });
});

/* ==================== Run Command ============================== */
ipcMain.on('RUN_CMD', (evt, cmdId, password) => { // eslint-disable-line
  const cmd = cmdHash[cmdId];
  console.log('running cmd: ', cmd.cmd);

  // notify the UI to show command is running. TODO: maybe use sync action is enough
  evt.sender.send('RUN_CMD_SUCCESS', cmdId);
  if (cmd.process) {
    // prevent from running multiple times.
    return;
  }

  let lineId = 0;
  cmd.outputs.length = 0;
  function onData(chunk, term) {
    let out = chunk.toString('utf8');

    if (!isWin) {
      if (/sorry.*try.*again.*/i.test(out)) {
        cmd.outputs.push({
          id: `${cmdId}_${lineId++}`, //eslint-disable-line
          text: 'Password is incorrect.',
        });
        evt.sender.send('CMD_OUTPUT', cmdId, [].concat(cmd.outputs));
        term.destroy();
        return;
      }

      if (cmd.sudo && /password/i.test(out)) {
        term.write(`${password}\r`);
        return;
      }
    }

    out = out.replace(/[\n\r]$/, '');
    const lines = out.split('\n');
    const rowsLimit = getOutputRowsLimit();
    for (const line of lines) {
      let text = line
        .replace(/>/g, '>')
        .replace(/</g, '<')
        .replace(/^\s/g, ' ')
      ;

      if (!isWin) text = convert.toHtml(text);
      cmd.outputs.push({
        id: `${cmdId}_${lineId++}`, //eslint-disable-line
        text,
      });
      if (cmd.outputs.length >= rowsLimit) {
        cmd.outputs.splice(0, cmd.outputs.length - rowsLimit);
      }
    }
    evt.sender.send('CMD_OUTPUT', cmdId, [].concat(cmd.outputs));
  }

  function onExit(code) {
    delete cmd.process;
    delete cmd._manualStop;
    if (!cmd._manualStop && cmd.finishPrompt) {
      notifier.notify({ // eslint-disable-line
        title: 'Command Pad',
        message: `Command ${code > 0 ? 'failed' : 'finished'}: ${cmd.name} .`,
        icon: code > 0 ? iconErrorPath : iconSuccessPath,
        // icon: path.join(__dirname, '../images/logo.png'),
        wait: true,
      });
    }
    evt.sender.send('CMD_FINISHED', cmdId, code);
  }

  let arr = cmd.cmd.match(/'[^']*'|"[^"]*"|[^ ]+/g) || [];
  arr = arr.map(m => m.replace(/^['"]|['"]/g, ''));

  const envPath = getEnvPath();
  if (isWin) {
    // use cmd.exe to execute command
    arr = ['/s', '/c'].concat(arr);
    const child = spawn('cmd', arr, {
      cwd: cmd.cwd || process.env.HOME,
      stdio: 'pipe',
      env: Object.assign({}, process.env, { Path: `${process.env.Path}${envPath}` }),
    });
    child.stdout.pipe(process.stdout);
    cmd.process = child;
    child.stdout.on('data', onData);
    child.stderr.on('data', onData);
    child.on('exit', onExit);
    child.on('error', () => onExit(99));
  } else {
    // use ptyw.js to execute command
    let target;
    if (cmd.sudo) {
      target = 'sudo';
    } else {
      target = arr.shift();
    }
    const ptyw = require('ptyw.js');

    const term = ptyw.spawn(target, arr, {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: cmd.cwd || process.env.HOME,
      detached: true,
      env: Object.assign({}, process.env, { PATH: `${process.env.PATH}${envPath}` }),
    });
    cmd.process = term;
    term.on('data', onData);
    term.on('exit', onExit);
  }

  sendStat({ type: 'run_cmd', cmd_count: cmds.length });
});

/* ==================== Stop Command ============================== */
ipcMain.on('STOP_CMD', (evt, cmdId) => {
  const cmd = cmdHash[cmdId];
  console.log('stopping cmd: ', cmdId, !!cmd.process);

  if (cmd.process) {
    cmd._manualStop = true;
    stopCmd(cmd);
    sendStat({ type: 'stop_cmd', cmd_count: cmds.length });
  }
});

/* ==================== Save Settings ============================== */
ipcMain.on('SAVE_SETTINGS', (evt, data) => {
  console.log('save settings');
  config.set('envPath', data.envPath);
  config.set('outputRowsLimit', parseInt(data.outputRowsLimit, 10) || 100);
  evt.sender.send('SAVE_SETTINGS_SUCCESS');

  sendStat({ type: 'save_settings', cmd_count: cmds.length });
});

/* ==================== Save Settings ============================== */
ipcMain.on('CLEAR_OUTPUT', (evt, cmdId) => {
  console.log('clear output');
  const cmd = cmdHash[cmdId];
  cmd.outputs.length = 0;
  evt.sender.send('CLEAR_OUTPUT_SUCCESS');
  sendStat({ type: 'clear_output', cmd_count: cmds.length });
});

module.exports = {
  appWillQuit: stopAllCmds
};

<commit_msg>Fix sudo support.<commit_after>'use strict';
const path = require('path');
const _ = require('lodash');
const childProcess = require('child_process');
const { app, ipcMain } = require('electron');
const Config = require('electron-config');
const notifier = require('node-notifier');
const sendStat = require('./send_stat');

const spawn = childProcess.spawn;
const exec = childProcess.exec;
const Convert = require('ansi-to-html');

const convert = new Convert();
const config = new Config();

const isWin = process.platform === 'win32';

const iconSuccessPath = path.join(__dirname, '../images/iconSuccess.png');
const iconErrorPath = path.join(__dirname, '../images/iconError.png');
// const iconSuccess = nativeImage.createFromPath(path.join(__dirname, '../images/iconSuccess.png'));
// const iconError = nativeImage.createFromPath(path.join(__dirname, '../images/iconError.png'));

notifier.on('click', () => {
  console.log('notifier on click');
  global.CP_WIN.show();
});

function guid() {
  return 'xyxyxyxy'.replace(/[xy]/g, (c) => {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); // eslint-disable-line
    return v.toString(16);
  });
}

// Ensure immutable, don't know if config.get always returns new data.
const cmds = _.cloneDeep(config.get('cmds') || []);
for (const c of cmds) {
  c.outputs = [];
}

const cmdHash = {};
cmds.forEach(cmd => cmdHash[cmd.id] = cmd); // eslint-disable-line

function stopCmd(cmd) {
  console.log('stopping cmd2: ', cmd.id);
  if (!cmd || !cmd.process) return;
  try {
    if (isWin) {
      exec('taskkill /pid ' + cmd.process.pid + ' /T /F');
    } else {
      cmd.process.destroy();
    }
  } catch (e) {
    console.log('failed to kill the process: ', e);
  }
}

function stopAllCmds() {
  console.log('app will quit.');
  for (const cmd of cmds) {
    if (cmd.process) {
      stopCmd(cmd);
    }
  }
}

function getEnvPath() {
  let envPath = config.get('envPath');
  if (envPath) {
    if (isWin) {
      envPath = ';' + envPath;
    } else {
      envPath = ':/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/local/share/node/bin:' + envPath;
    }
  } else {
    envPath = ':/usr/local/bin:/usr/local/sbin:/usr/local/share/npm/bin:/usr/local/share/node/bin';
  }
  return envPath;
}

function getOutputRowsLimit() {
  return config.get('outputRowsLimit') || 100;
}
/* ==================== Get Init Data ============================== */
ipcMain.on('GET_INIT_DATA', (evt) => {
  evt.sender.send('SET_INIT_DATA', {
    appVersion: app.getVersion(),
    envPath: config.get('envPath'),
    outputRowsLimit: getOutputRowsLimit(),
    isWin,
    cmds: cmds.map(c => Object.assign(_.pick(c, ['id', 'name', 'cmd', 'cwd', 'sudo', 'outputs', 'url', 'finishPrompt']), { status: c.process ? 'running' : 'stopped' })),
  });
});

/* ==================== Save Command ============================== */
ipcMain.on('SAVE_CMD', (evt, data, cmdId) => {
  console.log('saving cmd');
  const cmd = { id: cmdId || guid() };
  Object.assign(cmd, {
    name: data.name,
    cmd: data.cmd,
    sudo: data.sudo,
    finishPrompt: data.finishPrompt,
    cwd: data.cwd || null,
    url: data.url || null,
  });
  const newCmds = config.get('cmds') || [];
  if (cmdId) {
    const i = newCmds.findIndex(c => c.id === cmdId);
    newCmds.splice(i, 1, cmd);
  } else {
    newCmds.push(cmd);
  }
  config.set('cmds', newCmds);

  // Notify UI
  const curr = cmdId ? cmds.find(c => c.id === cmdId) : { status: 'stopped', outputs: [] };
  Object.assign(curr, cmd);
  if (!cmdId) {
    cmds.push(curr);
  }

  cmdHash[cmd.id] = curr;
  evt.sender.send('SAVE_CMD_SUCCESS', cmd);

  sendStat({ type: cmdId ? 'update_cmd' : 'add_cmd', cmd_count: cmds.length });
});

/* ==================== Delete Command ============================== */
ipcMain.on('DELETE_CMD', (evt, cmdId) => {
  console.log('deleting cmd');
  // Stop the process if needed
  const cmd = cmdHash[cmdId];
  if (cmd.process) {
    stopCmd(cmd);
  }
  // Save to config
  const newCmds = config.get('cmds') || [];
  _.remove(newCmds, c => c.id === cmdId);
  config.set('cmds', newCmds);

  // Remove from store
  _.remove(cmds, c => c.id === cmdId);
  delete cmdHash[cmdId];

  evt.sender.send('DELETE_CMD_SUCCESS', cmdId);

  sendStat({ type: 'del_cmd', cmd_count: cmds.length });
});

/* ==================== Reorder Commands ============================== */
ipcMain.on('REORDER_CMDS', (evt, cmdIds) => {
  console.log('reordering cmds');

  // Save to config
  let newCmds = config.get('cmds') || [];
  const groups = _.groupBy(newCmds, 'id');
  newCmds = cmdIds.map(id => groups[id][0]);
  config.set('cmds', newCmds);

  // Update store order
  for (let i = 0; i < cmds.length; i++) {
    cmds[i] = cmdHash[cmdIds[i]];
  }

  evt.sender.send('REORDER_CMDS_SUCCESS', cmdIds);

  sendStat({ type: 'reorder_cmds', cmd_count: cmds.length });
});

/* ==================== Run Command ============================== */
ipcMain.on('RUN_CMD', (evt, cmdId, password) => { // eslint-disable-line
  const cmd = cmdHash[cmdId];
  console.log('running cmd: ', cmd.cmd);

  // notify the UI to show command is running. TODO: maybe use sync action is enough
  evt.sender.send('RUN_CMD_SUCCESS', cmdId);
  if (cmd.process) {
    // prevent from running multiple times.
    return;
  }

  let lineId = 0;
  cmd.outputs.length = 0;
  function onData(chunk, term) {
    let out = chunk.toString('utf8');

    if (!isWin) {
      if (/sorry.*try.*again.*/i.test(out)) {
        cmd.outputs.push({
          id: `${cmdId}_${lineId++}`, //eslint-disable-line
          text: 'Password is incorrect.',
        });
        evt.sender.send('CMD_OUTPUT', cmdId, [].concat(cmd.outputs));
        term.destroy();
        return;
      }

      if (cmd.sudo && /password/i.test(out)) {
        term.write(`${password}\r`);
        return;
      }
    }

    out = out.replace(/[\n\r]$/, '');
    const lines = out.split('\n');
    const rowsLimit = getOutputRowsLimit();
    for (const line of lines) {
      let text = line
        .replace(/>/g, '>')
        .replace(/</g, '<')
        .replace(/^\s/g, ' ')
      ;

      if (!isWin) text = convert.toHtml(text);
      cmd.outputs.push({
        id: `${cmdId}_${lineId++}`, //eslint-disable-line
        text,
      });
      if (cmd.outputs.length >= rowsLimit) {
        cmd.outputs.splice(0, cmd.outputs.length - rowsLimit);
      }
    }
    evt.sender.send('CMD_OUTPUT', cmdId, [].concat(cmd.outputs));
  }

  function onExit(code) {
    delete cmd.process;
    delete cmd._manualStop;
    if (!cmd._manualStop && cmd.finishPrompt) {
      notifier.notify({ // eslint-disable-line
        title: 'Command Pad',
        message: `Command ${code > 0 ? 'failed' : 'finished'}: ${cmd.name} .`,
        icon: code > 0 ? iconErrorPath : iconSuccessPath,
        // icon: path.join(__dirname, '../images/logo.png'),
        wait: true,
      });
    }
    evt.sender.send('CMD_FINISHED', cmdId, code);
  }

  let arr = cmd.cmd.match(/'[^']*'|"[^"]*"|[^ ]+/g) || [];
  arr = arr.map(m => m.replace(/^['"]|['"]/g, ''));

  const envPath = getEnvPath();
  if (isWin) {
    // use cmd.exe to execute command
    arr = ['/s', '/c'].concat(arr);
    const child = spawn('cmd', arr, {
      cwd: cmd.cwd || process.env.HOME,
      stdio: 'pipe',
      env: Object.assign({}, process.env, { Path: `${process.env.Path}${envPath}` }),
    });
    child.stdout.pipe(process.stdout);
    cmd.process = child;
    child.stdout.on('data', onData);
    child.stderr.on('data', onData);
    child.on('exit', onExit);
    child.on('error', () => onExit(99));
  } else {
    // use ptyw.js to execute command
    let target;
    if (cmd.sudo) {
      target = 'sudo';
    } else {
      target = arr.shift();
    }
    const ptyw = require('ptyw.js');

    const term = ptyw.spawn(target, arr, {
      name: 'xterm-color',
      cols: 80,
      rows: 30,
      cwd: cmd.cwd || process.env.HOME,
      detached: true,
      env: Object.assign({}, process.env, { PATH: `${process.env.PATH}${envPath}` }),
    });
    cmd.process = term;
    term.on('data', chunk => onData(chunk, term));
    term.on('exit', onExit);
  }

  sendStat({ type: 'run_cmd', cmd_count: cmds.length });
});

/* ==================== Stop Command ============================== */
ipcMain.on('STOP_CMD', (evt, cmdId) => {
  const cmd = cmdHash[cmdId];
  console.log('stopping cmd: ', cmdId, !!cmd.process);

  if (cmd.process) {
    cmd._manualStop = true;
    stopCmd(cmd);
    sendStat({ type: 'stop_cmd', cmd_count: cmds.length });
  }
});

/* ==================== Save Settings ============================== */
ipcMain.on('SAVE_SETTINGS', (evt, data) => {
  console.log('save settings');
  config.set('envPath', data.envPath);
  config.set('outputRowsLimit', parseInt(data.outputRowsLimit, 10) || 100);
  evt.sender.send('SAVE_SETTINGS_SUCCESS');

  sendStat({ type: 'save_settings', cmd_count: cmds.length });
});

/* ==================== Save Settings ============================== */
ipcMain.on('CLEAR_OUTPUT', (evt, cmdId) => {
  console.log('clear output');
  const cmd = cmdHash[cmdId];
  cmd.outputs.length = 0;
  evt.sender.send('CLEAR_OUTPUT_SUCCESS');
  sendStat({ type: 'clear_output', cmd_count: cmds.length });
});

module.exports = {
  appWillQuit: stopAllCmds
};

<|endoftext|>


<commit_before>/*****************************************************************************/
/* PayrunNew: Event Handlers */
/*****************************************************************************/

let ReportUtils = {}

ReportUtils.getPayTypeHeaders = function(employeePayments) {
    let payTypeHeaders = ['Employee']

    employeePayments.forEach(anEmployeeData => {
        anEmployeeData.payment.forEach(anEmployeePayType => {
            if(anEmployeePayType.id) {
                let doesPayTypeHeaderExist = _.find(payTypeHeaders, function(aPayType) {
                    return aPayType.id && (aPayType.id === anEmployeePayType.id)
                })
                if(!doesPayTypeHeaderExist) {
                    payTypeHeaders.push({
                        id: anEmployeePayType.id,
                        description: anEmployeePayType.description
                    })
                }
            }
        })
    })
    payTypeHeaders.push('Total Deduction')
    payTypeHeaders.push('Net Pay')

    return {payTypeHeaders}
}

ReportUtils.getPayTypeValues = function(employeePayments, payTypeHeaders) {
    let payTypeValues = []

    employeePayments.forEach(anEmployeeData => {
        let aRowOfPayTypeValues = []

        payTypeHeaders.forEach(aPaytypeHeader => {
            if(aPaytypeHeader === 'Employee') {
                let employee = Meteor.users.findOne({_id: anEmployeeData.employeeId});
                aRowOfPayTypeValues.push(employee.profile.fullName)
                return
            }
            //--
            let payDetails = _.find(anEmployeeData.payment, function(aPayType) {
                return aPayType.id && (aPaytypeHeader.id === aPayType.id)
            })
            if(payDetails) {
                let payAmount = payDetails.amountLC
                if(payDetails.type === 'Deduction') {
                    if(payAmount > 0) {
                        payAmount *= -1
                    }
                }
                aRowOfPayTypeValues.push(payAmount)
            } else if(aPaytypeHeader === 'Net Pay') {
                let netPay = _.find(anEmployeeData.payment, function(aPayType) {
                    return (aPayType.code === 'NMP')
                })
                if(netPay) {
                    aRowOfPayTypeValues.push(netPay.amountLC)
                }
            } else if(aPaytypeHeader === 'Total Deduction') {
                let totalDeduction = _.find(anEmployeeData.payment, function(aPayType) {
                    return (aPayType.code === 'TDEDUCT')
                })
                if(totalDeduction) {
                    aRowOfPayTypeValues.push(totalDeduction.amountLC)
                }
            } else {
                aRowOfPayTypeValues.push("---")
            }
        })
        payTypeValues.push(aRowOfPayTypeValues)
    })
    return payTypeValues
}

Template.PayrunNew.events({
    'click #startProcessing': (e,tmpl) => {
        let payGrades = function(){
            let selected = [];
            $("input:checkbox[name=paygrades]:checked").each(function () {
                selected.push($(this).attr("id"));
            });
            return selected;
        };
        let annualPay = function(){
            let selected = [];
            $("input:checkbox[name=selectAnnual]:checked").each(function () {
                selected.push($(this).attr("id"));
            });
            console.log(selected);
            return selected;
        };
        var view = Blaze.render(Template.Loading, document.getElementById('spinner'));
        const params = {
            employees: Core.returnSelection($('[name="employee"]')),
            paygrades: payGrades(),
            period: {
                month: $('[name="paymentPeriod.month"]').val(),
                year: $('[name="paymentPeriod.year"]').val(),

            },
            type: $('input[name=runtype]:checked').val(),
            annuals: annualPay()
        };
        //check if any valid selection is made
        if (params.employees.length > 0 || params.paygrades.length > 0){
            Meteor.call("payrun/process",  params, Session.get('context'), (err, res) => {
                if(res){
                    // console.log(`Payrun results: ${JSON.stringify(res)}`);
                    Session.set('currentPayrunPeriod', res.period);

                    //set result as reactive dict payResult
                    tmpl.dict.set("payResult", res);
                } else{
                    console.log(err);
                }
                Blaze.remove(view);
            })
        } else {
            Blaze.remove(view);
            swal("notice","A valid selection must be made", "error");
        }

    },
    'change [name="annualPay"]': (e, tmpl) => {
        tmpl.includePay.set($(e.target).is(':checked'));

    },
    'change [name="employee"]': (e, tmpl) => {
        let selected = Core.returnSelection($(e.target));
        selected.length > 0 ? tmpl.showPayGrade.set(false):tmpl.showPayGrade.set(true);
        //add employee paygrades
        let employeeGrade = selected.map(x => {
            return Meteor.users.findOne({_id: x, 'employee': true}).employeeProfile.employment.paygrade;
        });
        tmpl.grades.set(employeeGrade);
    },
    'change [name="paygrades"]': (e, tmpl) => {
        let selected = [];
        $("input:checkbox[name=paygrades]:checked").each(function () {
            selected.push($(this).attr("id"));
        });
        tmpl.grades.set(selected);
    },
    'hover .table tbody tr': (e,tmpl) => {
        console.log('hover called');
    },
    selectedMonth: function (val) {
        if(Template.instance().selectedMonth.get()) {
            return Template.instance().selectedMonth.get() === val ? selected="selected" : '';
        }
    },
    selectedYear: function (val) {
        if(Template.instance().selectedYear.get()) {
            return Template.instance().selectedYear.get() === val ? selected="selected" : '';
        }
    },
    'click #export-to-csv': (e,tmpl) => {
        const payResult = Template.instance().dict.get('payResult');
        if (payResult) {
            let payTypeHeadersAndTotal = ReportUtils.getPayTypeHeaders(payResult.payObj.payrun)

            let formattedHeader = payTypeHeadersAndTotal.payTypeHeaders.map(aHeader => {
                return aHeader.description || aHeader
            })
            //--
            let reportData = ReportUtils.getPayTypeValues(payResult.payObj.payrun, payTypeHeadersAndTotal.payTypeHeaders)

            BulkpayExplorer.exportAllData({fields: formattedHeader, data: reportData},
                `Payrun results export`);
        } else {
            swal('Error', 'No payrun results', 'error')
        }
    }
});

/*****************************************************************************/
/* PayrunNew: Helpers */
/*****************************************************************************/
Template.PayrunNew.helpers({
    'month': function(){
        return Core.months()
    },
    'year': function(){
        return Core.years();
    },
    'paygrades': () => {
       return PayGrades.find();
    },
    'employees': () => {
        return Meteor.users.find({"employee": true});
    },
    "showPayGrade": () => {
        return Template.instance().showPayGrade.get();
    },
    "showAnnualPay": () => {
        return Template.instance().includePay.get();
    },
    'annualPay': () => {
        return PayTypes.find().fetch();
    },
    'checkInitial': (index) => {
        return index === 0 ? 'checked': null;
    },
    'nopayresult': () =>{
        const payresult = Template.instance().dict.get('payResult');
        if (!payresult) return true;
        return false;
    },
    'employeeResult': () => {
        const payResult = Template.instance().dict.get('payResult');
        if (payResult) {
            console.log(`pay result`, payResult.payObj.result)
            return payResult.payObj.result;
        }
    },
    'processingError': () => {
        const payresult = Template.instance().dict.get('payResult');
        return payresult.payObj.error.length;
    },
    selectedMonth: function (val) {
        if(Template.instance().selectedMonth.get()) {
            return Template.instance().selectedMonth.get() === val ? selected="selected" : '';
        }
    },
    selectedYear: function (val) {
        if(Template.instance().selectedYear.get()) {
            return Template.instance().selectedYear.get() === val ? selected="selected" : '';
        }
    }
});

/*****************************************************************************/
/* Payments: Lifecycle Hooks */
/*****************************************************************************/
Template.PayrunNew.onCreated(function () {
    let self = this;
    self.subscribe("paygrades", Session.get('context'));
    self.subscribe("activeEmployees", Session.get('context'));
    self.dict = new ReactiveDict();
    self.showPayGrade = new ReactiveVar(true);
    self.grades = new ReactiveVar([]);
    self.includePay = new ReactiveVar(false);
    // if annual payment included, subscribe to all annual pay.

    //--
    self.selectedMonth = new ReactiveVar();
    self.selectedYear = new ReactiveVar();
    //--
    let theMoment = moment();
    self.selectedMonth.set(theMoment.format('MM'))
    self.selectedYear.set(theMoment.format('YYYY'))
    //--

    self.autorun(function(){
        let includeType = self.includePay.get();
        let selectedGrade = self.grades.get();
        if (includeType)
            self.subscribe("AdditionalPayTypes", selectedGrade, Session.get('context'));
    })
});

Template.PayrunNew.onRendered(function () {
     $('select.dropdown').dropdown();
    // //
    // let selected = [];
    // $("input:checkbox[name=paygrades]:checked").each(function () {
    //     selected.push($(this).attr("id"));
    // });
    // Template.instance().grades.set(selected);
    // Show aciton upon row hover
});

Template.PayrunNew.onDestroyed(function () {
});
<commit_msg>Added employer pension contrib to payrun simulation csv export<commit_after>/*****************************************************************************/
/* PayrunNew: Event Handlers */
/*****************************************************************************/

let ReportUtils = {}

ReportUtils.getPayTypeHeaders = function(employeePayments) {
    let payTypeHeaders = ['Employee']

    employeePayments.forEach(anEmployeeData => {
        anEmployeeData.payment.forEach(anEmployeePayType => {
            if(anEmployeePayType.id) {
                let doesPayTypeHeaderExist = _.find(payTypeHeaders, function(aPayType) {
                    return aPayType.id && (aPayType.id === anEmployeePayType.id && aPayType.code === anEmployeePayType.code)
                })
                if(!doesPayTypeHeaderExist) {
                    payTypeHeaders.push({
                        id: anEmployeePayType.id,
                        code: anEmployeePayType.code,
                        description: anEmployeePayType.description
                    })
                }
            }
        })
    })
    payTypeHeaders.push({
        id: 'totalDeduction',
        code: 'totalDeduction',
        description: 'Total Deduction'
    })
    payTypeHeaders.push({
        id: 'netPay',
        code: 'netPay',
        description: 'Net Pay'
    })
    return {payTypeHeaders}
}

ReportUtils.getPayTypeValues = function(employeePayments, payTypeHeaders) {
    let payTypeValues = []

    employeePayments.forEach(anEmployeeData => {
        let aRowOfPayTypeValues = []

        payTypeHeaders.forEach(aPaytypeHeader => {
            if(aPaytypeHeader === 'Employee') {
                let employee = Meteor.users.findOne({_id: anEmployeeData.employeeId});
                aRowOfPayTypeValues.push(employee.profile.fullName)
                return
            }
            //--
            let payDetails = _.find(anEmployeeData.payment, function(aPayType) {
                return aPayType.id && (aPaytypeHeader.id === aPayType.id && aPaytypeHeader.code === aPayType.code)
            })
            if(payDetails) {
                let payAmount = payDetails.amountLC
                if(payDetails.type === 'Deduction') {
                    if(payAmount > 0) {
                        payAmount *= -1
                    }
                }
                aRowOfPayTypeValues.push(payAmount)
            } else if(aPaytypeHeader.id === 'netPay') {
                let netPay = _.find(anEmployeeData.payment, function(aPayType) {
                    return (aPayType.code === 'NMP')
                })
                if(netPay) {
                    aRowOfPayTypeValues.push(netPay.amountLC)
                }
            } else if(aPaytypeHeader.id === 'totalDeduction') {
                let totalDeduction = _.find(anEmployeeData.payment, function(aPayType) {
                    return (aPayType.code === 'TDEDUCT')
                })
                if(totalDeduction) {
                    aRowOfPayTypeValues.push(totalDeduction.amountLC)
                }
            } else {
                aRowOfPayTypeValues.push("---")
            }
        })
        payTypeValues.push(aRowOfPayTypeValues)
    })
    return payTypeValues
}

Template.PayrunNew.events({
    'click #startProcessing': (e,tmpl) => {
        let payGrades = function(){
            let selected = [];
            $("input:checkbox[name=paygrades]:checked").each(function () {
                selected.push($(this).attr("id"));
            });
            return selected;
        };
        let annualPay = function(){
            let selected = [];
            $("input:checkbox[name=selectAnnual]:checked").each(function () {
                selected.push($(this).attr("id"));
            });
            console.log(selected);
            return selected;
        };
        var view = Blaze.render(Template.Loading, document.getElementById('spinner'));
        const params = {
            employees: Core.returnSelection($('[name="employee"]')),
            paygrades: payGrades(),
            period: {
                month: $('[name="paymentPeriod.month"]').val(),
                year: $('[name="paymentPeriod.year"]').val(),

            },
            type: $('input[name=runtype]:checked').val(),
            annuals: annualPay()
        };
        //check if any valid selection is made
        if (params.employees.length > 0 || params.paygrades.length > 0){
            Meteor.call("payrun/process",  params, Session.get('context'), (err, res) => {
                if(res){
                    // console.log(`Payrun results: ${JSON.stringify(res)}`);
                    Session.set('currentPayrunPeriod', res.period);

                    //set result as reactive dict payResult
                    tmpl.dict.set("payResult", res);
                } else{
                    console.log(err);
                }
                Blaze.remove(view);
            })
        } else {
            Blaze.remove(view);
            swal("notice","A valid selection must be made", "error");
        }

    },
    'change [name="annualPay"]': (e, tmpl) => {
        tmpl.includePay.set($(e.target).is(':checked'));

    },
    'change [name="employee"]': (e, tmpl) => {
        let selected = Core.returnSelection($(e.target));
        selected.length > 0 ? tmpl.showPayGrade.set(false):tmpl.showPayGrade.set(true);
        //add employee paygrades
        let employeeGrade = selected.map(x => {
            return Meteor.users.findOne({_id: x, 'employee': true}).employeeProfile.employment.paygrade;
        });
        tmpl.grades.set(employeeGrade);
    },
    'change [name="paygrades"]': (e, tmpl) => {
        let selected = [];
        $("input:checkbox[name=paygrades]:checked").each(function () {
            selected.push($(this).attr("id"));
        });
        tmpl.grades.set(selected);
    },
    'hover .table tbody tr': (e,tmpl) => {
        console.log('hover called');
    },
    selectedMonth: function (val) {
        if(Template.instance().selectedMonth.get()) {
            return Template.instance().selectedMonth.get() === val ? selected="selected" : '';
        }
    },
    selectedYear: function (val) {
        if(Template.instance().selectedYear.get()) {
            return Template.instance().selectedYear.get() === val ? selected="selected" : '';
        }
    },
    'click #export-to-csv': (e,tmpl) => {
        const payResult = Template.instance().dict.get('payResult');
        if (payResult) {
            let payTypeHeaders = ReportUtils.getPayTypeHeaders(payResult.payObj.payrun)

            let formattedHeader = payTypeHeaders.payTypeHeaders.map(aHeader => {
                return aHeader.description || aHeader
            })
            //--
            let reportData = ReportUtils.getPayTypeValues(payResult.payObj.payrun, payTypeHeaders.payTypeHeaders)

            BulkpayExplorer.exportAllData({fields: formattedHeader, data: reportData},
                `Payrun results export`);
        } else {
            swal('Error', 'No payrun results', 'error')
        }
    }
});

/*****************************************************************************/
/* PayrunNew: Helpers */
/*****************************************************************************/
Template.PayrunNew.helpers({
    'month': function(){
        return Core.months()
    },
    'year': function(){
        return Core.years();
    },
    'paygrades': () => {
       return PayGrades.find();
    },
    'employees': () => {
        return Meteor.users.find({"employee": true});
    },
    "showPayGrade": () => {
        return Template.instance().showPayGrade.get();
    },
    "showAnnualPay": () => {
        return Template.instance().includePay.get();
    },
    'annualPay': () => {
        return PayTypes.find().fetch();
    },
    'checkInitial': (index) => {
        return index === 0 ? 'checked': null;
    },
    'nopayresult': () =>{
        const payresult = Template.instance().dict.get('payResult');
        if (!payresult) return true;
        return false;
    },
    'employeeResult': () => {
        const payResult = Template.instance().dict.get('payResult');
        if (payResult) {
            console.log(`pay result`, payResult.payObj.result)
            return payResult.payObj.result;
        }
    },
    'processingError': () => {
        const payresult = Template.instance().dict.get('payResult');
        return payresult.payObj.error.length;
    },
    selectedMonth: function (val) {
        if(Template.instance().selectedMonth.get()) {
            return Template.instance().selectedMonth.get() === val ? selected="selected" : '';
        }
    },
    selectedYear: function (val) {
        if(Template.instance().selectedYear.get()) {
            return Template.instance().selectedYear.get() === val ? selected="selected" : '';
        }
    }
});

/*****************************************************************************/
/* Payments: Lifecycle Hooks */
/*****************************************************************************/
Template.PayrunNew.onCreated(function () {
    let self = this;
    self.subscribe("paygrades", Session.get('context'));
    self.subscribe("activeEmployees", Session.get('context'));
    self.dict = new ReactiveDict();
    self.showPayGrade = new ReactiveVar(true);
    self.grades = new ReactiveVar([]);
    self.includePay = new ReactiveVar(false);
    // if annual payment included, subscribe to all annual pay.

    //--
    self.selectedMonth = new ReactiveVar();
    self.selectedYear = new ReactiveVar();
    //--
    let theMoment = moment();
    self.selectedMonth.set(theMoment.format('MM'))
    self.selectedYear.set(theMoment.format('YYYY'))
    //--

    self.autorun(function(){
        let includeType = self.includePay.get();
        let selectedGrade = self.grades.get();
        if (includeType)
            self.subscribe("AdditionalPayTypes", selectedGrade, Session.get('context'));
    })
});

Template.PayrunNew.onRendered(function () {
     $('select.dropdown').dropdown();
    // //
    // let selected = [];
    // $("input:checkbox[name=paygrades]:checked").each(function () {
    //     selected.push($(this).attr("id"));
    // });
    // Template.instance().grades.set(selected);
    // Show aciton upon row hover
});

Template.PayrunNew.onDestroyed(function () {
});
<|endoftext|>


<commit_before>exports.up = function(knex, Promise) {
  return knex.schema.createTableIfNotExists('games_users', function(table){
    table.increments('id').primary();
    table.integer('user_id').references('users.user_id');
    table.string('game_hash').references('games.game_hash');
    table.boolean('favorites').defaultTo(false);
  })
  .then(function(){
    return knex.schema.createTableIfNotExists('games_users', function(table){
        table.increments('id').primary();
        table.integer('user_id').references('users.user_id');
        table.string('game_hash').references('games.game_hash');
        table.boolean('favorites').defaultTo(false);;
    })
  );
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTable('games_users');
};<commit_msg>migrations 3<commit_after>exports.up = function(knex, Promise) {
  return knex.schema.createTableIfNotExists('games_users', function(table){
    table.increments('id').primary();
    table.integer('user_id').references('users.user_id');
    table.string('game_hash').references('games.game_hash');
    table.boolean('favorites').defaultTo(false);
  })
  .then(function(){
    return knex.schema.createTableIfNotExists('games_users', function(table){
        table.increments('id').primary();
        table.integer('user_id').references('users.user_id');
        table.string('game_hash').references('games.game_hash');
        table.boolean('favorites').defaultTo(false);;
    })
  });
};

exports.down = function(knex, Promise) {
  return knex.schema.dropTable('games_users');
};<|endoftext|>


<commit_before>import apiFetch from './index';
import {toastr} from 'react-redux-toastr'
import { recordStatEvent } from './index';

export const REQUEST_APPLICATION = 'REQUEST_APPLICATION';
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';

export function fetchApplication () {
    return (dispatch) => {
        dispatch(requestApplication());
        return apiFetch('users/me/application')
            .then((response) => response.json())
            .then((json) => dispatch(receiveApplication(json)));
    };
}

function requestApplication () {
    return {
        type: REQUEST_APPLICATION
    };
}

function receiveApplication (json, onlyUpdateNonFormFields = false) {
    if ('error' in json) {
        json = null;
    }
    return {
        type: RECEIVE_APPLICATION,
        json,
        onlyUpdateNonFormFields
    };
}

export function saveApplication(suppressToast = false) {
    return (dispatch, getState) => {
        let data = getState().application.applicationForm;
        return apiFetch('users/me/application',
            {
                method: 'PUT',
                body: JSON.stringify(data)
            })
            .then((response) => response.json())
            .then((json) => {
                if(json.success) {
                    if(!suppressToast) {
                        toastr.success('Success!', json.data.message);
                    }
                    //this api endpoint returns the updated application
                    dispatch(receiveApplication(json,true));
                } else {
                    //something broke, rehydrate the store with fresh application to be safe
                    dispatch(fetchApplication());
                }
            });
    };
}

/*
 * Callback from Dropzone
 */
export function onResumeDrop(accepted, rejected) {
    return (dispatch) => {
        if(rejected.length > 0) {
            toastr.error("Upload error","Resume must be a PDF");
            console.log("wrong filetype");
            recordStatEvent("resumeUploadError",null,{rejected});
            return;
        } else if(accepted.length !== 1) {
            toastr.error("Upload error","please try again");
            recordStatEvent("resumeUploadError",null,{accepted});
            console.log("err");
            return;
        }
        dispatch(initResumeUpload(accepted[0]));
    }
}

/*
 * Resume Redux XHR Black Magic :)
 */
function initResumeUpload(file) {
    return (dispatch, getState) => {
        let URL = getState().application.applicationForm.resume_put_url;
        let fileName = file.name;
        dispatch(startResumeUpload(fileName));
        /*
         * fetch doesn't let us get progress information, so we need to use vanilla XHR
         */
        let xhr = new XMLHttpRequest();

        xhr.onload = () => {
            let { status } = xhr;
            dispatch(finishResumeUpload(fileName,status));
        };

        xhr.upload.addEventListener('progress',(e) => {
            dispatch(resumeUploadProgress(e));
        });

        let fd = new FormData();
        fd.append('Content-Type', file.type);
        fd.append("file",file);
        xhr.open('PUT', URL);
        xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
        xhr.send(fd);
    }
}

export const START_RESUME_UPLOAD = 'START_RESUME_UPLOAD';
export const FINISH_RESUME_UPLOAD = 'FINISH_RESUME_UPLOAD';
export const RESUME_UPLOAD_PROGRESS = 'RESUME_UPLOAD_PROGRESS';

function startResumeUpload(fileName) {
    return (dispatch) => {
        toastr.info("hang tight!",`uploading ${fileName}`);
        dispatch({
            type: START_RESUME_UPLOAD,
            fileName
        })
    }
}

function finishResumeUpload(fileName,status) {
    return (dispatch) => {
        let success = (status===200);

        dispatch({
            type: FINISH_RESUME_UPLOAD,
            fileName
        });

        if(success) {
            dispatch(saveApplication(true));
            toastr.success('Resume Upload Success', `uploaded ${fileName}`);
        } else {
            //todo: ??
            toastr.error('Oops!','something went wrong... try again');
        }
        recordStatEvent("resumeUploadFinished",null,{success, fileName, status});
    };
}

function resumeUploadProgress(event) {
    let progress = event.loaded/event.total*100;
    return {
        type: RESUME_UPLOAD_PROGRESS,
        progress
    }
}

/*
 * Normal application field stuff
 */
export const CHANGE_APPLICATION_FIELD_VALUE = 'CHANGE_APPLICATION_FIELD_VALUE';
export function changeApplicationFieldValue(field, value) {
    return {
        type: CHANGE_APPLICATION_FIELD_VALUE,
        field,
        value
    }
}
export const TOGGLE_APPLICATION_FIELD_VALUE = 'TOGGLE_APPLICATION_FIELD_VALUE';
export function toggleApplicationFieldValue(field) {
    return {
        type: TOGGLE_APPLICATION_FIELD_VALUE,
        field
    }

}

export const REQUEST_SCHOOL_LIST = 'REQUEST_SCHOOL_LIST';
export const RECEIVE_SCHOOL_LIST = 'RECEIVE_SCHOOL_LIST';

export function fetchSchoolList () {
    return (dispatch) => {
        dispatch(requestSchoolList());
        return apiFetch('schools')
            .then((response) => response.json())
            .then((json) => dispatch(receiveSchoolList(json)));
    };
}

function requestSchoolList () {
    return {
        type: REQUEST_SCHOOL_LIST
    };
}

function receiveSchoolList (json) {
    return {
        type: RECEIVE_SCHOOL_LIST,
        json,
    };
}<commit_msg>Fix capitalization in toasts<commit_after>import apiFetch from './index';
import {toastr} from 'react-redux-toastr'
import { recordStatEvent } from './index';

export const REQUEST_APPLICATION = 'REQUEST_APPLICATION';
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';

export function fetchApplication () {
    return (dispatch) => {
        dispatch(requestApplication());
        return apiFetch('users/me/application')
            .then((response) => response.json())
            .then((json) => dispatch(receiveApplication(json)));
    };
}

function requestApplication () {
    return {
        type: REQUEST_APPLICATION
    };
}

function receiveApplication (json, onlyUpdateNonFormFields = false) {
    if ('error' in json) {
        json = null;
    }
    return {
        type: RECEIVE_APPLICATION,
        json,
        onlyUpdateNonFormFields
    };
}

export function saveApplication(suppressToast = false) {
    return (dispatch, getState) => {
        let data = getState().application.applicationForm;
        return apiFetch('users/me/application',
            {
                method: 'PUT',
                body: JSON.stringify(data)
            })
            .then((response) => response.json())
            .then((json) => {
                if(json.success) {
                    if(!suppressToast) {
                        toastr.success('Success!', json.data.message);
                    }
                    //this api endpoint returns the updated application
                    dispatch(receiveApplication(json,true));
                } else {
                    //something broke, rehydrate the store with fresh application to be safe
                    dispatch(fetchApplication());
                }
            });
    };
}

/*
 * Callback from Dropzone
 */
export function onResumeDrop(accepted, rejected) {
    return (dispatch) => {
        if(rejected.length > 0) {
            toastr.error("Upload error","Resume must be a PDF");
            console.log("wrong filetype");
            recordStatEvent("resumeUploadError",null,{rejected});
            return;
        } else if(accepted.length !== 1) {
            toastr.error("Upload error","Please try again");
            recordStatEvent("resumeUploadError",null,{accepted});
            console.log("err");
            return;
        }
        dispatch(initResumeUpload(accepted[0]));
    }
}

/*
 * Resume Redux XHR Black Magic :)
 */
function initResumeUpload(file) {
    return (dispatch, getState) => {
        let URL = getState().application.applicationForm.resume_put_url;
        let fileName = file.name;
        dispatch(startResumeUpload(fileName));
        /*
         * fetch doesn't let us get progress information, so we need to use vanilla XHR
         */
        let xhr = new XMLHttpRequest();

        xhr.onload = () => {
            let { status } = xhr;
            dispatch(finishResumeUpload(fileName,status));
        };

        xhr.upload.addEventListener('progress',(e) => {
            dispatch(resumeUploadProgress(e));
        });

        let fd = new FormData();
        fd.append('Content-Type', file.type);
        fd.append("file",file);
        xhr.open('PUT', URL);
        xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
        xhr.send(fd);
    }
}

export const START_RESUME_UPLOAD = 'START_RESUME_UPLOAD';
export const FINISH_RESUME_UPLOAD = 'FINISH_RESUME_UPLOAD';
export const RESUME_UPLOAD_PROGRESS = 'RESUME_UPLOAD_PROGRESS';

function startResumeUpload(fileName) {
    return (dispatch) => {
        toastr.info("Hang tight!",`Uploading ${fileName}`);
        dispatch({
            type: START_RESUME_UPLOAD,
            fileName
        })
    }
}

function finishResumeUpload(fileName,status) {
    return (dispatch) => {
        let success = (status===200);

        dispatch({
            type: FINISH_RESUME_UPLOAD,
            fileName
        });

        if(success) {
            dispatch(saveApplication(true));
            toastr.success('Resume Upload Success', `Uploaded ${fileName}`);
        } else {
            //todo: ??
            toastr.error('Oops!','Something went wrong... try again');
        }
        recordStatEvent("resumeUploadFinished",null,{success, fileName, status});
    };
}

function resumeUploadProgress(event) {
    let progress = event.loaded/event.total*100;
    return {
        type: RESUME_UPLOAD_PROGRESS,
        progress
    }
}

/*
 * Normal application field stuff
 */
export const CHANGE_APPLICATION_FIELD_VALUE = 'CHANGE_APPLICATION_FIELD_VALUE';
export function changeApplicationFieldValue(field, value) {
    return {
        type: CHANGE_APPLICATION_FIELD_VALUE,
        field,
        value
    }
}
export const TOGGLE_APPLICATION_FIELD_VALUE = 'TOGGLE_APPLICATION_FIELD_VALUE';
export function toggleApplicationFieldValue(field) {
    return {
        type: TOGGLE_APPLICATION_FIELD_VALUE,
        field
    }

}

export const REQUEST_SCHOOL_LIST = 'REQUEST_SCHOOL_LIST';
export const RECEIVE_SCHOOL_LIST = 'RECEIVE_SCHOOL_LIST';

export function fetchSchoolList () {
    return (dispatch) => {
        dispatch(requestSchoolList());
        return apiFetch('schools')
            .then((response) => response.json())
            .then((json) => dispatch(receiveSchoolList(json)));
    };
}

function requestSchoolList () {
    return {
        type: REQUEST_SCHOOL_LIST
    };
}

function receiveSchoolList (json) {
    return {
        type: RECEIVE_SCHOOL_LIST,
        json,
    };
}
<|endoftext|>


<commit_before>/**
 * Created by mgab on 05/05/2017.
 */
import * as types from '../constants/actionTypes'
import { Map } from 'immutable'

export function logIn (username, password) {
  return {
    type: types.AUTH_REQUEST,
    payload: { username, password }
  }
}

export function logOut () {
  return {
    type: types.AUTH_LOGOUT
  }
}

export function getToken (state) {
  const newState = Map(state).getIn(['auth', 'authInfo', 'access_token'], null)
  return newState
}

export function isAuthenticated (state) {
  return getToken(state) !== null
}
<commit_msg>Fixed Immutable check for the access token.<commit_after>/**
 * Created by mgab on 05/05/2017.
 */
import * as types from '../constants/actionTypes'
import { Map } from 'immutable'

export function logIn (username, password) {
  return {
    type: types.AUTH_REQUEST,
    payload: { username, password }
  }
}

export function logOut () {
  return {
    type: types.AUTH_LOGOUT
  }
}

export function getToken (state) {
  const immutableState = Map(state)
  return immutableState.getIn(['auth', 'authInfo', 'access_token'], null)
}

export function isAuthenticated (state) {
  return getToken(state) !== null
}
<|endoftext|>


<commit_before>import * as types from './actionTypes';
import GithubAPI from '../api/githubAPI';

export function reposLoaded(repos){
  return {type: types.REPOS_LOADED, repos};
}

export function loadRepos(){
  return function(dispatch) {
    return GithubAPI.getCurrentUserAllRepos().then(repos => {
      dispatch(reposLoaded(repos));
    }).catch(error => {
      throw(error);
    });

  };
}
<commit_msg>Refactor 'loadRepos' in repoActions.js to invoke getUserOwnedRepos method from GithubAPI<commit_after>import * as types from './actionTypes';
import GithubAPI from '../api/githubAPI';

export function reposLoaded(repos){
  return {type: types.REPOS_LOADED, repos};
}

export function loadRepos(){
  return function(dispatch, getState) {
    const currentState = getState();

    return GithubAPI.getUserOwnedRepos(currentState.oauths.authenticatedUser.login).then(repos => {
      dispatch(reposLoaded(repos));
    }).catch(error => {
      throw(error);
    });

  };
}
<|endoftext|>