<commit_before>define(function(require){
    'use strict';
    require('./path-finder');
    var JsPlumbOverlayManager = require('./jsplumb-overlay-manager');

    function matchEndpoints(paintInfo1, paintInfo2) {
        var keys = ['sx', 'sy', 'tx', 'ty'];
        return _.isEqual(_.pick(paintInfo1, keys), _.pick(paintInfo2, keys));
    }

    function JsPlumbSmartlineManager(jsPlumbInstance) {
        this.jsPlumbInstance = jsPlumbInstance;
        this.jsPlumbOverlayManager = new JsPlumbOverlayManager(this);
        this.cache = {};
        this.debouncedCalculateOverlays = _.debounce(
                _.bind(this.jsPlumbOverlayManager.calculate, this.jsPlumbOverlayManager),
            50);
        this.debouncedRepaintEverything = _.debounce(
            _.bind(function () {
                this.jsPlumbInstance.repaintEverything();
            }, this),
            0
        );
    }

    window.getLastRequest = function () {
        return JSON.stringify(JsPlumbSmartlineManager.lastRequest);
    }

    JsPlumbSmartlineManager.prototype = {

        getNaivePathLength: function (fromRect, toRect) {
            if (fromRect == toRect) {
                return 0;
            }
            return Math.abs(fromRect.bottom - toRect.top)
                + Math.max(0, fromRect.left - toRect.right, toRect.left - fromRect.right)
                + ((fromRect.bottom - toRect.top > 0) ? 1200 : 0);
        },

        calculate: function (connector, paintInfo) {
            var invalidateConnection;
            var cache = {};
            var connections = [];
            var rects = {};
            var graph = new Graph();
            var endPoints = this.jsPlumbInstance.sourceEndpointDefinitions;
            _.each(this.jsPlumbInstance.sourceEndpointDefinitions, function(endPoint, id) {
                var clientRect;
                var el = document.getElementById(id);
                if (el) {
                    clientRect = new Rectangle(el.offsetLeft, el.offsetTop, el.offsetWidth, el.offsetHeight);
                    rects[id] = clientRect;
                    clientRect.cid = id;
                    graph.rectangles.push(clientRect);
                }
            });
            if (graph.rectangles.length < 1) {
                this.cache = {};
                return;
            }
            graph.build();

            _.each(this.jsPlumbInstance.getConnections(), function (conn) {
                if (conn.sourceId in rects && conn.targetId in rects) {
                    connections.push([conn.sourceId, conn.targetId, this.getNaivePathLength(rects[conn.sourceId], rects[conn.targetId]), conn]);
                }
            }, this);

            connections.sort(function (a, b) {
                return a[2] - b[2];
            });

            JsPlumbSmartlineManager.lastRequest = {
                sources: graph.rectangles.map(function (item) {
                    return [item.cid, item.left, item.top, item.width, item.height];
                }),
                connections: connections.map(function (item) {return item.slice(0,2);})
            };

            _.each(connections, function (conn) {
                var finder = new Finder(graph);

                finder.addTo(graph.getPathFromCid(conn[1], Direction2d.BOTTOM_TO_TOP));
                finder.addFrom(graph.getPathFromCid(conn[0], Direction2d.TOP_TO_BOTTOM));

                var newCacheItem;
                var path = finder.find();
                var cacheKey = conn[3].connector.getId();
                if (!path) {
                    console.warn("Cannot find path");
                } else {
                    graph.updateWithPath(path);
                    newCacheItem = {
                        connection: conn[3],
                        path: path,
                        paintInfo: conn[3].connector === connector ? paintInfo : undefined
                    };
                    if (conn[3].connector === connector) {
                        newCacheItem.paintInfo = paintInfo;
                    }
                    cache[cacheKey] = newCacheItem;
                }
            });

            _.each(cache, function (item) {
                var points =  item.path.points.reverse();
                item.points = points;
            })

            invalidateConnection = _.find(cache, function(item, cacheKey) {
                return cacheKey in this.cache && item.connector !== connector &&
                    !_.isEqual(this.cache[cacheKey].points, item.points);
            }, this);

            _.extend(this.cache, cache);

            if (invalidateConnection) {
                this.debouncedRepaintEverything();
            }
            this.debouncedCalculateOverlays();
        },

        getConnectionPath: function (connector, paintInfo) {
            var connectorId = connector.getId();
            var cached = this.retrieveCacheItem(connectorId, paintInfo);

            if (cached === false) {
                this.calculate(connector, paintInfo);
                cached = this.retrieveCacheItem(connectorId, paintInfo);
            }

            if (cached !== false) {
                return _.clone(cached.points);
            }

            console.warn("Path not found");
            return [];
        },

        retrieveCacheItem: function (connectorId, paintInfo) {
            var cached = connectorId in this.cache ? this.cache[connectorId] : undefined;
            if (cached) {
                if (cached.points.length && (_.isUndefined(cached.paintInfo) || matchEndpoints(cached.paintInfo, paintInfo))) {
                    cached.paintInfo = paintInfo;
                    return cached;
                } else {
                    delete this.cache[connectorId];
                }
            }
            return false;
        }
    };

    return JsPlumbSmartlineManager;
});
<commit_msg>CRM-3386 code update<commit_after>define(function(require){
    'use strict';
    require('./path-finder');
    var JsPlumbOverlayManager = require('./jsplumb-overlay-manager');

    function matchEndpoints(paintInfo1, paintInfo2) {
        var keys = ['sx', 'sy', 'tx', 'ty'];
        return _.isEqual(_.pick(paintInfo1, keys), _.pick(paintInfo2, keys));
    }

    function JsPlumbSmartlineManager(jsPlumbInstance) {
        this.jsPlumbInstance = jsPlumbInstance;
        this.jsPlumbOverlayManager = new JsPlumbOverlayManager(this);
        this.cache = {};
        this.debouncedCalculateOverlays = _.debounce(
            _.bind(this.jsPlumbOverlayManager.calculate, this.jsPlumbOverlayManager),
        50);
    }

    window.getLastRequest = function () {
        return JSON.stringify(JsPlumbSmartlineManager.lastRequest);
    }

    JsPlumbSmartlineManager.prototype = {

        isCacheValid: function () {
            return _.isEqual(this.getState(), this.cache.state)
        },

        getConnectionPath: function (connector) {
            if (!this.isCacheValid()) {
                this.refreshCache();
            }

            return this.cache.connections[connector.getId()] || [];
        },

        getNaivePathLength: function (fromRect, toRect) {
            if (fromRect == toRect) {
                return 0;
            }
            return Math.abs(fromRect.bottom - toRect.top)
                + Math.max(0, fromRect.left - toRect.right, toRect.left - fromRect.right)
                + ((fromRect.bottom - toRect.top > 0) ? 1200 : 0);
        },

        getState: function () {
            var state = {
                rectangles: [],
                connections: []
            }
            _.each(this.jsPlumbInstance.sourceEndpointDefinitions, function(endPoint, id) {
                var el = document.getElementById(id);
                if (el) {
                    state.rectangles.push([id, el.offsetLeft, el.offsetTop, el.offsetWidth, el.offsetHeight]);
                }
            });

            _.each(this.jsPlumbInstance.getConnections(), function (conn) {
                if (conn.sourceId in rects && conn.targetId in rects) {
                    state.connections.push([conn.sourceId, conn.targetId]);
                }
            }, this);

            return state;
        },

        refreshCache: function () {
            this.cache.state = this.getState();
            var cache = {};
            var connections = [];
            var rects = {};
            var graph = new Graph();

            _.each(this.jsPlumbInstance.sourceEndpointDefinitions, function(endPoint, id) {
                var clientRect;
                var el = document.getElementById(id);
                if (el) {
                    clientRect = new Rectangle(el.offsetLeft, el.offsetTop, el.offsetWidth, el.offsetHeight);
                    rects[id] = clientRect;
                    clientRect.cid = id;
                    graph.rectangles.push(clientRect);
                }
            });

            if (graph.rectangles.length < 1) {
                this.cache.connections = {};
                return;
            }

            graph.build();

            _.each(this.jsPlumbInstance.getConnections(), function (conn) {
                if (conn.sourceId in rects && conn.targetId in rects) {
                    connections.push([conn.sourceId, conn.targetId, this.getNaivePathLength(rects[conn.sourceId], rects[conn.targetId]), conn]);
                }
            }, this);

            connections.sort(function (a, b) {
                return a[2] - b[2];
            });

            JsPlumbSmartlineManager.lastRequest = {
                sources: graph.rectangles.map(function (item) {
                    return [item.cid, item.left, item.top, item.width, item.height];
                }),
                connections: connections.map(function (item) {return item.slice(0,2);})
            };

            _.each(connections, function (conn) {
                var finder = new Finder(graph);

                finder.addTo(graph.getPathFromCid(conn[1], Direction2d.BOTTOM_TO_TOP));
                finder.addFrom(graph.getPathFromCid(conn[0], Direction2d.TOP_TO_BOTTOM));

                var newCacheItem;
                var path = finder.find();
                var cacheKey = conn[3].connector.getId();
                if (!path) {
                    console.warn("Cannot find path");
                } else {
                    graph.updateWithPath(path);
                    newCacheItem = {
                        connection: conn[3],
                        path: path,
                        paintInfo: conn[3].connector === connector ? paintInfo : undefined
                    };
                    if (conn[3].connector === connector) {
                        newCacheItem.paintInfo = paintInfo;
                    }
                    cache[cacheKey] = newCacheItem;
                }
            });

            _.each(cache, function (item) {
                var points =  item.path.points.reverse();
                item.points = points;
            });

            this.jsPlumbInstance.repaintEverything();
            this.debouncedCalculateOverlays();
        }
    };

    return JsPlumbSmartlineManager;
});
<|endoftext|>


<commit_before>import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import * as actions from './../template/actions';
import Container from './../bootstrap/container';
import Header from './../bootstrap/header';
import PrimaryButton from './../bootstrap/primaryButton';
import Form from './../bootstrap/form';
import FormItem from './../bootstrap/formItem';
import Input from './../bootstrap/input';

const AddItemForm = React.createClass({
    mixins: [PureRenderMixin],

    backHandler() {
        actions.showAddItem(false);
    },

    handler() {

    },

    render() {
        return (
            <Container>

                    <PrimaryButton onClick={this.backHandler}>{'< Back'}</PrimaryButton>

Add new item

                <Form>

                    <FormItem id="add-item-name" title="Name:" sizeLabel={1} size={10}>
                        <Input type="text" id="add-item-name" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-url" title="URL:" sizeLabel={1} size={10}>
                        <Input type="text" id="add-item-url" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-refresh-rate" title="Refresh rate:" sizeLabel={1} size={10}>
                        <Input type="text" id="add-item-refresh-rate" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-height" title="Height:" sizeLabel={1} size={10}>
                        <Input type="text" id="add-item-height" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-width" title="Width:" sizeLabel={1} size={10}>
                        <Input type="text" id="add-item-width" onChange={this.handler}/>
                    </FormItem>

                </Form>

                    ... preview ...

            </Container>
        );
    }
});

export default AddItemForm;
<commit_msg>Placeholders and save button<commit_after>import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import * as actions from './../template/actions';
import Container from './../bootstrap/container';
import Header from './../bootstrap/header';
import PrimaryButton from './../bootstrap/primaryButton';
import Form from './../bootstrap/form';
import FormItem from './../bootstrap/formItem';
import Input from './../bootstrap/input';

const AddItemForm = React.createClass({
    mixins: [PureRenderMixin],

    backHandler() {
        actions.showAddItem(false);
    },

    handler() {

    },

    render() {
        return (
            <Container>

                    <PrimaryButton onClick={this.backHandler}>{'< Back'}</PrimaryButton>

Add new item

                <Form>

                    <FormItem id="add-item-name" title="Name:" sizeLabel={1} size={10}>
                        <Input type="text" placeholder="Item name" id="add-item-name" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-url" title="URL:" sizeLabel={1} size={10}>
                        <Input type="text" placeholder="Item URL" id="add-item-url" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-refresh-rate" title="Refresh rate:" sizeLabel={1} size={10}>
                        <Input type="text" placeholder="Item refresh rate" id="add-item-refresh-rate" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-height" title="Height:" sizeLabel={1} size={10}>
                        <Input type="text" placeholder="Item height" id="add-item-height" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="add-item-width" title="Width:" sizeLabel={1} size={10}>
                        <Input type="text" placeholder="Item width" id="add-item-width" onChange={this.handler}/>
                    </FormItem>

                    <FormItem id="" title="" sizeLabel={1} size={10}>
                        <PrimaryButton onClick={this.handler}>Save item</PrimaryButton>
                    </FormItem>

                </Form>

                    ... preview ...

            </Container>
        );
    }
});

export default AddItemForm;
<|endoftext|>


<commit_before>'use strict';

var Promise = require('bluebird');
var _ = require('lodash');
var parseError = require('../utils/error_utils').parseError;
var messagingFn = require('./services/messaging');

module.exports = function(context) {
    var events = context.foundation.getEventEmitter();
    var cluster = context.cluster;
    var config = context.sysconfig.teraslice;
    var ex_id = process.env.ex_id;
    var job_id = process.env.job_id;
    var isDone = true;
    var isShuttingDown = false;
    var ID = context.sysconfig.teraslice.hostname + "__" + cluster.worker.id;
    var job_runner = require('./runners/job')(context);
    var analytics_store;
    var queue;
    var max_retries;
    var job;
    var recycle;

    //this will be used to keep track of the previously sent message just in case of a disconnect
    var sentMessage = false;

    var state_store;

    //this will store errors and the number of retries
    var errorLog = {};

    var logger = context.foundation.makeLogger({
        ex_id: ex_id,
        job_id: job_id,
        module: 'worker',
        worker_id: ID
    });
    //need ipc channels open before job construction
    var messaging = messagingFn(context, logger);

    //if worker cannot make client, job needs to shutdown, needs to be setup before job_runner
    events.on('getClient:config_error', terminalShutdown);

    var host = messaging.getHostUrl();

    logger.info(`worker ${ID} is online, communicating with host: ${host}`);

    messaging.register('assets:loaded', function(ipcMessage) {
        events.emit('worker:assets_loaded', ipcMessage)
    });

    //set up listener
    messaging.register('slicer:slice:new', function(msg) {
        isDone = false;
        //getting a slice means the previous message was handled
        sentMessage = false;
        logger.info(`received slice: ${msg.data.slice_id}`);
        var sliceLogger = context.foundation.makeLogger({
            ex_id: ex_id,
            job_id: job_id,
            module: 'slice',
            worker_id: ID,
            slice_id: msg.data.slice_id
        });

        runSlice(msg.data, sliceLogger);
    });

    messaging.register('slicer:slice:recorded', function() {
        //the sent message has been processed, so set it to false;
        logger.debug('slice has been marked as completed by slicer');
        sentMessage = false;
    });

    messaging.register('network:error', function(err) {
        events.emit('network:error');
        logger.error('Error in worker socket: ', err)
    });

    messaging.register('network:disconnect', function(e) {
        events.emit('network:disconnect');
        logger.error(`worker ${ID} has disconnected from slicer ex_id ${ex_id}`, e);
    });

    messaging.register('network:connect', function() {
        if (sentMessage) {
            logger.warn('reconnecting to slicer, previous slice: ', sentMessage);
            sentMessage.retry = true;
            messaging.send('worker:slice:complete', sentMessage);
        }
        else {
            logger.debug(`a worker has made the initial connection to slicer ex_id: ${ex_id}`);
            messaging.send('worker:ready', {worker_id: ID});
        }
    });

    var finalShutdown = _.once(shutdown);

    messaging.register('worker:shutdown', finalShutdown);
    //to catch signal propagation, but cleanup through msg sent from master
    function noOP() {
    }

    messaging.register('process:SIGTERM', noOP);
    messaging.register('process:SIGINT', noOP);

    Promise.resolve(job_runner.initialize(events, logger))
        .then(function(_job) {
            job = _job;
            queue = _job.queue;
            max_retries = _job.max_retries;
            recycle = recycleFn(job);
            return require('./storage/state')(context)
        })
        .then(function(store) {
            state_store = store;
            return require('./storage/analytics')(context)
        })
        .then(function(store) {
            analytics_store = store;
            logger.info(`connecting to host: ${host}`);
            messaging.initialize();
        })
        .catch(function(err) {
            var errMsg = parseError(err);
            logger.error(`could not instantiate for job execution: ${ex_id}, error: ${errMsg}`);
            logger.flush()
                .then(function() {
                    process.exit()
                })
                .catch(function(err) {
                    var errMsg = parseError(err);
                    logger.error(errMsg);
                    process.exit();
                });
        });

    function isReady(res, slice, msg, specData, sliceLogger) {
        //res may return null if no data was received
        return state_store.updateState(slice, 'completed', slice.index)
            .then(function(results) {
                events.emit('slice:success', slice);
                sentMessage = {worker_id: ID, slice: slice, analytics: specData};
                sliceLogger.info(`completed slice: `, slice);
                if (isShuttingDown) {
                    sentMessage.isShuttingDown = true;
                }

                if (specData) {
                    if (job.reporter) {
                        job.reporter(context, job.jobConfig, specData);
                        logMessage(sliceLogger, msg);
                    }
                    else {
                        logMessage(sliceLogger, msg, specData);
                        return analytics_store.log(job, slice, specData)
                            .catch(function(err) {
                                var errMsg = parseError(err);
                                logger.error(`Failure when storing analytics: ${errMsg}`)
                            });
                    }
                }
            })
            .finally(function() {
                events.emit('slice:finalize');
                isDone = true;
                recycle();
            });

    }

    function runSlice(slice, sliceLogger) {

        function sliceFailed(err) {
            var errMsg = parseError(err);
            sentMessage = {worker_id: ID, slice: slice, error: errMsg};
            events.emit('slice:failure', sentMessage);
            return state_store.updateState(slice, 'error', errMsg)
                .then(function() {
                    sliceLogger.error(`failed to process`, sentMessage, `and has slice state is marked as error`);
                })
                .catch(function(err) {
                    var errMsg = parseError(err);
                    sliceLogger.error(`An error has occurred: ${errMsg} on marking slice as failed, message: `, slice);
                });
        }

        var msg = slice.request;
        var slice_id = slice.slice_id;

        var finalQueue = queue;
        var specData;

        if (job.analytics) {
            specData = {time: [], size: [], memory: []};

            finalQueue = queue.map(function(fn) {
                return fn.bind(null, specData);
            });
        }

        var work = Promise.reduce(finalQueue, function(prev, fn) {
            return Promise.resolve(prev).then(function(data) {
               // return Promise.reject('an error')
                return fn(data, sliceLogger, msg);
            });
        }, msg);

        work.then(function(res) {
            return isReady(res, slice, msg, specData, sliceLogger)
        })
            .catch(function(err) {
                var errMsg = parseError(err);
                sliceLogger.error(`An error has occurred: ${errMsg}, message: `, slice);
                if (max_retries) {
                    //checking if error has occurred before
                    if (errorLog[slice_id]) {
                        errorLog[slice_id]++;

                        if (errorLog[slice_id] >= max_retries) {
                            sliceLogger.error('Max retires has been reached for: ', slice);

                            sliceFailed(err)
                                .finally(function() {
                                    events.emit('slice:finalize');
                                    isDone = true;
                                    recycle();
                                });
                        }
                        else {
                            runSlice(slice, sliceLogger);
                        }
                    }
                    else {
                        errorLog[slice_id] = 1;
                        runSlice(slice, sliceLogger);
                    }
                }
                //no retries, proceed to next slice
                else {
                    sliceFailed(err)
                        .finally(function() {
                            events.emit('slice:finalize');
                            isDone = true;
                            recycle();
                        });
                }
            });
    }

    function shutdown() {
        var counter = config.shutdown_timeout;
        isShuttingDown = true;
        events.emit('worker:shutdown');
        setInterval(function() {
            logger.trace(`is done with current slice: ${isDone}, shutdown counter: ${counter}`);
            if (isDone || counter <= 0) {
                logger.info(`shutting down`);
                //just in case analytic or state store are not initialized before it shuts down, could happen if slicer immediately fails
                if (analytics_store === undefined) {
                    analytics_store = {
                        shutdown: function() {
                            return Promise.resolve(true)
                        }
                    }
                }
                if (state_store === undefined) {
                    state_store = {
                        shutdown: function() {
                            return Promise.resolve(true)
                        }
                    }
                }

                let stores = [state_store.shutdown, analytics_store.shutdown, logger.flush];

                var shutdownStores = Promise.reduce(stores, function(prev, curr) {
                    return Promise.resolve(prev)
                        .then(function() {
                            return curr()
                        })
                        .catch(function(err) {
                            var errMsg = parseError(err);
                            logger.error(`Error shutting down store ${prev.name}`, errMsg);
                            return Promise.resolve(curr)
                        });
                });

                shutdownStores
                    .then(function() {
                        process.exit()
                    })
                    .catch(function(err) {
                        var errMsg = parseError(err);
                        logger.error(errMsg);
                        //todo the log might never be called to stdout with process.exit here
                        process.exit();
                    });
            }
            else {
                if (counter % 6000 === 0) {
                    logger.info(`shutdown sequence initiated, but is still processing. Will force shutdown in ${counter / 1000} seconds`);
                }

                counter -= 1000;
            }
        }, 1000);

    }

    function terminalShutdown(errEV) {
        logger.error(`Terminal error, shutting down job ${ex_id}`);
        events.emit('worker:shutdown');
        messaging.respond({message: 'job:error:terminal', error: errEV.err, ex_id: ex_id})
    }

    function recycleFn(job) {
        var sliceCount = 0;
        return function() {
            if (job.jobConfig.recycle_worker) {
                sliceCount += 1;
                if (sliceCount >= job.jobConfig.recycle_worker && !isShuttingDown) {
                    logger.info(`worker: ${ID} will now reinstantiate`);
                    //still need to signal that slice completed and not be enqueued
                    sentMessage.isShuttingDown = true;
                    messaging.send('worker:slice:complete', sentMessage);
                    setTimeout(function() {
                        process.exit(42);
                    }, 100)
                }
                else {
                    messaging.send('worker:slice:complete', sentMessage);
                }
            }
            else {
                messaging.send('worker:slice:complete', sentMessage);
            }
        }
    }

    function logMessage(sliceLogger, msg, specData) {
        var str = `analytics for slice `;
        var dataStr = '';

        if (typeof msg === 'string') {
            dataStr = msg + ', '
        }
        else {
            _.forOwn(msg, function(value, key) {
                dataStr += `${key} : ${value} `;
            });
        }
        _.forOwn(specData, function(value, key) {
            dataStr += `${key} : ${value} `;
        });

        sliceLogger.info(str + dataStr);
    }

};
<commit_msg>Stagger worker recycle (#575)<commit_after>'use strict';

var Promise = require('bluebird');
var _ = require('lodash');
var parseError = require('../utils/error_utils').parseError;
var messagingFn = require('./services/messaging');

module.exports = function(context) {
    var events = context.foundation.getEventEmitter();
    var cluster = context.cluster;
    var config = context.sysconfig.teraslice;
    var ex_id = process.env.ex_id;
    var job_id = process.env.job_id;
    var isDone = true;
    var isShuttingDown = false;
    var ID = context.sysconfig.teraslice.hostname + "__" + cluster.worker.id;
    var job_runner = require('./runners/job')(context);
    var analytics_store;
    var queue;
    var max_retries;
    var job;
    var recycle;
    var recycleWorkerRandomFactor = _.random(75, 100)

    //this will be used to keep track of the previously sent message just in case of a disconnect
    var sentMessage = false;

    var state_store;

    //this will store errors and the number of retries
    var errorLog = {};

    var logger = context.foundation.makeLogger({
        ex_id: ex_id,
        job_id: job_id,
        module: 'worker',
        worker_id: ID
    });
    //need ipc channels open before job construction
    var messaging = messagingFn(context, logger);

    //if worker cannot make client, job needs to shutdown, needs to be setup before job_runner
    events.on('getClient:config_error', terminalShutdown);

    var host = messaging.getHostUrl();

    logger.info(`worker ${ID} is online, communicating with host: ${host}`);

    messaging.register('assets:loaded', function(ipcMessage) {
        events.emit('worker:assets_loaded', ipcMessage)
    });

    //set up listener
    messaging.register('slicer:slice:new', function(msg) {
        isDone = false;
        //getting a slice means the previous message was handled
        sentMessage = false;
        logger.info(`received slice: ${msg.data.slice_id}`);
        var sliceLogger = context.foundation.makeLogger({
            ex_id: ex_id,
            job_id: job_id,
            module: 'slice',
            worker_id: ID,
            slice_id: msg.data.slice_id
        });

        runSlice(msg.data, sliceLogger);
    });

    messaging.register('slicer:slice:recorded', function() {
        //the sent message has been processed, so set it to false;
        logger.debug('slice has been marked as completed by slicer');
        sentMessage = false;
    });

    messaging.register('network:error', function(err) {
        events.emit('network:error');
        logger.error('Error in worker socket: ', err)
    });

    messaging.register('network:disconnect', function(e) {
        events.emit('network:disconnect');
        logger.error(`worker ${ID} has disconnected from slicer ex_id ${ex_id}`, e);
    });

    messaging.register('network:connect', function() {
        if (sentMessage) {
            logger.warn('reconnecting to slicer, previous slice: ', sentMessage);
            sentMessage.retry = true;
            messaging.send('worker:slice:complete', sentMessage);
        }
        else {
            logger.debug(`a worker has made the initial connection to slicer ex_id: ${ex_id}`);
            messaging.send('worker:ready', {worker_id: ID});
        }
    });

    var finalShutdown = _.once(shutdown);

    messaging.register('worker:shutdown', finalShutdown);
    //to catch signal propagation, but cleanup through msg sent from master
    function noOP() {
    }

    messaging.register('process:SIGTERM', noOP);
    messaging.register('process:SIGINT', noOP);

    Promise.resolve(job_runner.initialize(events, logger))
        .then(function(_job) {
            job = _job;
            queue = _job.queue;
            max_retries = _job.max_retries;
            recycle = recycleFn(job);
            return require('./storage/state')(context)
        })
        .then(function(store) {
            state_store = store;
            return require('./storage/analytics')(context)
        })
        .then(function(store) {
            analytics_store = store;
            logger.info(`connecting to host: ${host}`);
            messaging.initialize();
        })
        .catch(function(err) {
            var errMsg = parseError(err);
            logger.error(`could not instantiate for job execution: ${ex_id}, error: ${errMsg}`);
            logger.flush()
                .then(function() {
                    process.exit()
                })
                .catch(function(err) {
                    var errMsg = parseError(err);
                    logger.error(errMsg);
                    process.exit();
                });
        });

    function isReady(res, slice, msg, specData, sliceLogger) {
        //res may return null if no data was received
        return state_store.updateState(slice, 'completed', slice.index)
            .then(function(results) {
                events.emit('slice:success', slice);
                sentMessage = {worker_id: ID, slice: slice, analytics: specData};
                sliceLogger.info(`completed slice: `, slice);
                if (isShuttingDown) {
                    sentMessage.isShuttingDown = true;
                }

                if (specData) {
                    if (job.reporter) {
                        job.reporter(context, job.jobConfig, specData);
                        logMessage(sliceLogger, msg);
                    }
                    else {
                        logMessage(sliceLogger, msg, specData);
                        return analytics_store.log(job, slice, specData)
                            .catch(function(err) {
                                var errMsg = parseError(err);
                                logger.error(`Failure when storing analytics: ${errMsg}`)
                            });
                    }
                }
            })
            .finally(function() {
                events.emit('slice:finalize');
                isDone = true;
                recycle();
            });

    }

    function runSlice(slice, sliceLogger) {

        function sliceFailed(err) {
            var errMsg = parseError(err);
            sentMessage = {worker_id: ID, slice: slice, error: errMsg};
            events.emit('slice:failure', sentMessage);
            return state_store.updateState(slice, 'error', errMsg)
                .then(function() {
                    sliceLogger.error(`failed to process`, sentMessage, `and has slice state is marked as error`);
                })
                .catch(function(err) {
                    var errMsg = parseError(err);
                    sliceLogger.error(`An error has occurred: ${errMsg} on marking slice as failed, message: `, slice);
                });
        }

        var msg = slice.request;
        var slice_id = slice.slice_id;

        var finalQueue = queue;
        var specData;

        if (job.analytics) {
            specData = {time: [], size: [], memory: []};

            finalQueue = queue.map(function(fn) {
                return fn.bind(null, specData);
            });
        }

        var work = Promise.reduce(finalQueue, function(prev, fn) {
            return Promise.resolve(prev).then(function(data) {
               // return Promise.reject('an error')
                return fn(data, sliceLogger, msg);
            });
        }, msg);

        work.then(function(res) {
            return isReady(res, slice, msg, specData, sliceLogger)
        })
            .catch(function(err) {
                var errMsg = parseError(err);
                sliceLogger.error(`An error has occurred: ${errMsg}, message: `, slice);
                if (max_retries) {
                    //checking if error has occurred before
                    if (errorLog[slice_id]) {
                        errorLog[slice_id]++;

                        if (errorLog[slice_id] >= max_retries) {
                            sliceLogger.error('Max retires has been reached for: ', slice);

                            sliceFailed(err)
                                .finally(function() {
                                    events.emit('slice:finalize');
                                    isDone = true;
                                    recycle();
                                });
                        }
                        else {
                            runSlice(slice, sliceLogger);
                        }
                    }
                    else {
                        errorLog[slice_id] = 1;
                        runSlice(slice, sliceLogger);
                    }
                }
                //no retries, proceed to next slice
                else {
                    sliceFailed(err)
                        .finally(function() {
                            events.emit('slice:finalize');
                            isDone = true;
                            recycle();
                        });
                }
            });
    }

    function shutdown() {
        var counter = config.shutdown_timeout;
        isShuttingDown = true;
        events.emit('worker:shutdown');
        setInterval(function() {
            logger.trace(`is done with current slice: ${isDone}, shutdown counter: ${counter}`);
            if (isDone || counter <= 0) {
                logger.info(`shutting down`);
                //just in case analytic or state store are not initialized before it shuts down, could happen if slicer immediately fails
                if (analytics_store === undefined) {
                    analytics_store = {
                        shutdown: function() {
                            return Promise.resolve(true)
                        }
                    }
                }
                if (state_store === undefined) {
                    state_store = {
                        shutdown: function() {
                            return Promise.resolve(true)
                        }
                    }
                }

                let stores = [state_store.shutdown, analytics_store.shutdown, logger.flush];

                var shutdownStores = Promise.reduce(stores, function(prev, curr) {
                    return Promise.resolve(prev)
                        .then(function() {
                            return curr()
                        })
                        .catch(function(err) {
                            var errMsg = parseError(err);
                            logger.error(`Error shutting down store ${prev.name}`, errMsg);
                            return Promise.resolve(curr)
                        });
                });

                shutdownStores
                    .then(function() {
                        process.exit()
                    })
                    .catch(function(err) {
                        var errMsg = parseError(err);
                        logger.error(errMsg);
                        //todo the log might never be called to stdout with process.exit here
                        process.exit();
                    });
            }
            else {
                if (counter % 6000 === 0) {
                    logger.info(`shutdown sequence initiated, but is still processing. Will force shutdown in ${counter / 1000} seconds`);
                }

                counter -= 1000;
            }
        }, 1000);

    }

    function terminalShutdown(errEV) {
        logger.error(`Terminal error, shutting down job ${ex_id}`);
        events.emit('worker:shutdown');
        messaging.respond({message: 'job:error:terminal', error: errEV.err, ex_id: ex_id})
    }

    function recycleFn(job) {
        var sliceCount = 0;
        return function() {
            if (job.jobConfig.recycle_worker) {
                sliceCount += 1;
                // recycle worker between 75% and 100% of job.jobConfig.recycle_worker
                var recycleCount = Math.trunc(recycleWorkerRandomFactor/100 * job.jobConfig.recycle_worker);
                if (sliceCount >= recycleCount && !isShuttingDown) {
                    logger.info(`worker: ${ID} recycled after processing ${sliceCount} slices`);
                    //still need to signal that slice completed and not be enqueued
                    sentMessage.isShuttingDown = true;
                    messaging.send('worker:slice:complete', sentMessage);
                    setTimeout(function() {
                        process.exit(42);
                    }, 100)
                }
                else {
                    messaging.send('worker:slice:complete', sentMessage);
                }
            }
            else {
                messaging.send('worker:slice:complete', sentMessage);
            }
        }
    }

    function logMessage(sliceLogger, msg, specData) {
        var str = `analytics for slice `;
        var dataStr = '';

        if (typeof msg === 'string') {
            dataStr = msg + ', '
        }
        else {
            _.forOwn(msg, function(value, key) {
                dataStr += `${key} : ${value} `;
            });
        }
        _.forOwn(specData, function(value, key) {
            dataStr += `${key} : ${value} `;
        });

        sliceLogger.info(str + dataStr);
    }

};
<|endoftext|>


<commit_before>
const ROT = require('../../lib/rot');
const RG = require('./rg');

const Path = {};

/* NOTE: This has problem that if x0,y0 or x1,y1 have actors, returns no path at
 * all. */
Path.getShortestPassablePath = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCallback = (x, y) => map.isPassable(x, y);
    const finder = new ROT.Path.AStar(x1, y1, passableCallback);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest actor to actor path. Returns shortest path between two
 * actors excluding the source and destination points. */
Path.getActorToActorPath = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCb = (x, y) => {
        if (map.hasXY(x, y)) {
            return (
                map.isPassable(x, y) || (x === x0 && y === y0)
                || (x === x1 && y === y1)
            );
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCb);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });

    if (coords.length > 1) {
        coords.shift(); // Remove source x,y
        coords.pop(); // Remove target x,y
    }
    return coords;
};

/* Returns shortest path for actor in x0,y0, excluding the source point. If
 * destination point is impassable, returns an empty array. */
Path.getShortestActorPath = function(map, x0, y0, x1, y1, cb) {
    const coords = [];
    const passableCb = (x, y) => {
        if (map.hasXY(x, y)) {
            if (cb) {
                return cb(x, y) || (x === x0 && y === y0);
            }
            return (
                map.isPassable(x, y) || (x === x0 && y === y0)
            );
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCb);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });

    if (coords.length > 0) {
        coords.shift(); // Remove source x,y
    }
    return coords;
};

Path.getShortestPassablePathWithDoors = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCbDoor = (x, y) => {
        if (map.hasXY(x, y)) {
            return map.isPassable(x, y) || map.getCell(x, y).hasDoor();
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCbDoor);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest path (array of x,y pairs) between two points. Does not
* check if any of the cells are passable. */
Path.getShortestPath = function(x0, y0, x1, y1, cb = () => true) {
    const coords = [];
    const passableCallback = cb;
    const finder = new ROT.Path.AStar(x1, y1, passableCallback);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest distance (in cells) between two points.*/
Path.shortestDist = function(x0, y0, x1, y1) {
    const coords = Path.getShortestPath(x0, y0, x1, y1);
    return coords.length - 1; // Subtract one because starting cell included
};

Path.getPathWeight = (map, coord) => {
    let w = 0;
    coord.forEach(c => {
        if (map.hasXY(c.x, c.y)) {
            const elem = map.getBaseElemXY(c.x, c.y);
            switch (elem.getType()) {
                case 'floor': w += 1; break;
                case 'forest': w += 2; break;
                case 'stone': w += 2; break;
                case 'water': w += 4; break;
                case 'highrock': w += 4; break;
                case 'chasm': w += 5; break;
                case 'wall': w += 20; break;
                default: w += 0; break;
            }
        }
    });
    return w;
};

/* Gets the minimum weight path between x0,y0 and x1,y1 of two options.
 * This algorithm will
 * tunnel through any obstacles eventually. Optionally takes the path function
 * as last argument or uses the default getShortestPassablePath. */
Path.getMinWeightPath = function(map, x0, y0, x1, y1, pathFunc) {
    let coordPassable = [];
    if (pathFunc) {
        coordPassable = pathFunc(map, x0, y0, x1, y1);
    }
    else {
        coordPassable = Path.getShortestPassablePath(map, x0, y0, x1, y1);
    }

    const coordShortest = Path.getShortestPath(x0, y0, x1, y1);
    const passableWeight = Path.getPathWeight(map, coordPassable);
    const shortestWeight = Path.getPathWeight(map, coordShortest);

    let coord = null;
    if (coordPassable.length === 0) {
        coord = coordShortest;
    }
    else {
        coord = passableWeight >= shortestWeight ? coordShortest
            : coordPassable;
    }
    return coord;
};

/* Given map and two x,y points, calculates min paths between these points using
 * the list of path functions. */
Path.getMinWeightOrShortest = function(map, x0, y0, x1, y1, passableFuncs) {
    const coordShortest = Path.getShortestPath(x0, y0, x1, y1);
    const paths = [];
    passableFuncs.forEach(passableCb => {
        const path = Path.getShortestPath(x0, y0, x1, y1, passableCb);
        if (path.length > 0) {
            paths.push(path);
        }
    });
    paths.push(coordShortest);

    let minPath = null;
    let minWeight = -1;
    console.log('Found paths for calc', paths.length);
    paths.forEach(path => {
        const pathWeight = Path.getPathWeight(map, path);
        if (minWeight === -1 || pathWeight < minWeight) {
            minWeight = pathWeight;
            minPath = path;
        }
    });
    return minPath;
};

/* This algorithm divides the path into nSegments, then computes minimum
 * weighted path for each of those segments. This makes the
 * path look more realistic, but of course less optimal.
 */
Path.getWeightPathSegmented = function(map, x0, y0, x1, y1, nSeg, pathFunc) {
    const dX = x1 - x0;
    const dY = y1 - y0;
    const segX = RG.Path.getPathSeg(dX, nSeg);
    const segY = RG.Path.getPathSeg(dY, nSeg);
    let finalPath = [];

    let [startX, startY] = [x0, y0];
    for (let i = 0; i < nSeg; i++) {
        const [endX, endY] = [startX + segX[i], startY + segY[i]];
        const segmentPath = Path.getMinWeightPath(map, startX, startY,
            endX, endY, pathFunc);
        [startX, startY] = [endX, endY];
        finalPath = finalPath.concat(segmentPath);
    }
    return finalPath;
};

Path.addPathToMap = function(map, coord) {
    const chosenCoord = [];
    for (let j = 0; j < coord.length; j++) {
        const c = coord[j];
        if (map.hasXY(c.x, c.y)) {
            const baseElem = map.getBaseElemXY(c.x, c.y);
            if (baseElem.getType().match(/(chasm|water)/)) {
                map.setBaseElemXY(c.x, c.y, RG.ELEM.BRIDGE);
            }
            else if (baseElem.getType() === 'stone') {
                // TODO add mountain path
                map.setBaseElemXY(c.x, c.y, RG.ELEM.ROAD);
            }
            else {
                map.setBaseElemXY(c.x, c.y, RG.ELEM.ROAD);
            }
            chosenCoord.push(c);
        }
    }
    return chosenCoord;
};

/* Returns the path segment sizes. For example, dist=17, nSeg=4,
 * produces [4, 4, 4, 5] */
Path.getPathSeg = function(dist, nSeg) {
    let remain = dist;
    const result = [];
    const segSize = Math.floor(dist / nSeg);
    for (let i = 0; i < nSeg - 1; i++) {
        result.push(segSize);
        remain -= segSize;
    }
    result.push(remain);
    return result;
};

module.exports = Path;
<commit_msg>Cleaned up console.logs.<commit_after>
const ROT = require('../../lib/rot');
const RG = require('./rg');

const Path = {};

/* NOTE: This has problem that if x0,y0 or x1,y1 have actors, returns no path at
 * all. */
Path.getShortestPassablePath = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCallback = (x, y) => map.isPassable(x, y);
    const finder = new ROT.Path.AStar(x1, y1, passableCallback);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest actor to actor path. Returns shortest path between two
 * actors excluding the source and destination points. */
Path.getActorToActorPath = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCb = (x, y) => {
        if (map.hasXY(x, y)) {
            return (
                map.isPassable(x, y) || (x === x0 && y === y0)
                || (x === x1 && y === y1)
            );
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCb);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });

    if (coords.length > 1) {
        coords.shift(); // Remove source x,y
        coords.pop(); // Remove target x,y
    }
    return coords;
};

/* Returns shortest path for actor in x0,y0, excluding the source point. If
 * destination point is impassable, returns an empty array. */
Path.getShortestActorPath = function(map, x0, y0, x1, y1, cb) {
    const coords = [];
    const passableCb = (x, y) => {
        if (map.hasXY(x, y)) {
            if (cb) {
                return cb(x, y) || (x === x0 && y === y0);
            }
            return (
                map.isPassable(x, y) || (x === x0 && y === y0)
            );
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCb);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });

    if (coords.length > 0) {
        coords.shift(); // Remove source x,y
    }
    return coords;
};

Path.getShortestPassablePathWithDoors = function(map, x0, y0, x1, y1) {
    const coords = [];
    const passableCbDoor = (x, y) => {
        if (map.hasXY(x, y)) {
            return map.isPassable(x, y) || map.getCell(x, y).hasDoor();
        }
        return false;
    };
    const finder = new ROT.Path.AStar(x1, y1, passableCbDoor);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest path (array of x,y pairs) between two points. Does not
* check if any of the cells are passable. */
Path.getShortestPath = function(x0, y0, x1, y1, cb = () => true) {
    const coords = [];
    const passableCallback = cb;
    const finder = new ROT.Path.AStar(x1, y1, passableCallback);
    finder.compute(x0, y0, (x, y) => {
        coords.push({x, y});
    });
    return coords;
};

/* Returns shortest distance (in cells) between two points.*/
Path.shortestDist = function(x0, y0, x1, y1) {
    const coords = Path.getShortestPath(x0, y0, x1, y1);
    return coords.length - 1; // Subtract one because starting cell included
};

Path.getPathWeight = (map, coord) => {
    let w = 0;
    coord.forEach(c => {
        if (map.hasXY(c.x, c.y)) {
            const elem = map.getBaseElemXY(c.x, c.y);
            switch (elem.getType()) {
                case 'floor': w += 1; break;
                case 'forest': w += 2; break;
                case 'stone': w += 2; break;
                case 'water': w += 4; break;
                case 'highrock': w += 4; break;
                case 'chasm': w += 5; break;
                case 'wall': w += 20; break;
                default: w += 0; break;
            }
        }
    });
    return w;
};

/* Gets the minimum weight path between x0,y0 and x1,y1 of two options.
 * This algorithm will
 * tunnel through any obstacles eventually. Optionally takes the path function
 * as last argument or uses the default getShortestPassablePath. */
Path.getMinWeightPath = function(map, x0, y0, x1, y1, pathFunc) {
    let coordPassable = [];
    if (pathFunc) {
        coordPassable = pathFunc(map, x0, y0, x1, y1);
    }
    else {
        coordPassable = Path.getShortestPassablePath(map, x0, y0, x1, y1);
    }

    const coordShortest = Path.getShortestPath(x0, y0, x1, y1);
    const passableWeight = Path.getPathWeight(map, coordPassable);
    const shortestWeight = Path.getPathWeight(map, coordShortest);

    let coord = null;
    if (coordPassable.length === 0) {
        coord = coordShortest;
    }
    else {
        coord = passableWeight >= shortestWeight ? coordShortest
            : coordPassable;
    }
    return coord;
};

/* Given map and two x,y points, calculates min paths between these points using
 * the list of path functions. */
Path.getMinWeightOrShortest = function(map, x0, y0, x1, y1, passableFuncs) {
    const coordShortest = Path.getShortestPath(x0, y0, x1, y1);
    const paths = [];
    passableFuncs.forEach(passableCb => {
        const path = Path.getShortestPath(x0, y0, x1, y1, passableCb);
        if (path.length > 0) {
            paths.push(path);
        }
    });
    paths.push(coordShortest);

    let minPath = null;
    let minWeight = -1;
    paths.forEach(path => {
        const pathWeight = Path.getPathWeight(map, path);
        if (minWeight === -1 || pathWeight < minWeight) {
            minWeight = pathWeight;
            minPath = path;
        }
    });
    return minPath;
};

/* This algorithm divides the path into nSegments, then computes minimum
 * weighted path for each of those segments. This makes the
 * path look more realistic, but of course less optimal.
 */
Path.getWeightPathSegmented = function(map, x0, y0, x1, y1, nSeg, pathFunc) {
    const dX = x1 - x0;
    const dY = y1 - y0;
    const segX = RG.Path.getPathSeg(dX, nSeg);
    const segY = RG.Path.getPathSeg(dY, nSeg);
    let finalPath = [];

    let [startX, startY] = [x0, y0];
    for (let i = 0; i < nSeg; i++) {
        const [endX, endY] = [startX + segX[i], startY + segY[i]];
        const segmentPath = Path.getMinWeightPath(map, startX, startY,
            endX, endY, pathFunc);
        [startX, startY] = [endX, endY];
        finalPath = finalPath.concat(segmentPath);
    }
    return finalPath;
};

Path.addPathToMap = function(map, coord) {
    const chosenCoord = [];
    for (let j = 0; j < coord.length; j++) {
        const c = coord[j];
        if (map.hasXY(c.x, c.y)) {
            const baseElem = map.getBaseElemXY(c.x, c.y);
            if (baseElem.getType().match(/(chasm|water)/)) {
                map.setBaseElemXY(c.x, c.y, RG.ELEM.BRIDGE);
            }
            else if (baseElem.getType() === 'stone') {
                // TODO add mountain path
                map.setBaseElemXY(c.x, c.y, RG.ELEM.ROAD);
            }
            else {
                map.setBaseElemXY(c.x, c.y, RG.ELEM.ROAD);
            }
            chosenCoord.push(c);
        }
    }
    return chosenCoord;
};

/* Returns the path segment sizes. For example, dist=17, nSeg=4,
 * produces [4, 4, 4, 5] */
Path.getPathSeg = function(dist, nSeg) {
    let remain = dist;
    const result = [];
    const segSize = Math.floor(dist / nSeg);
    for (let i = 0; i < nSeg - 1; i++) {
        result.push(segSize);
        remain -= segSize;
    }
    result.push(remain);
    return result;
};

module.exports = Path;
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Kagilum SAS.
 *
 * This file is part of iceScrum.
 *
 * iceScrum is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License.
 *
 * iceScrum is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with iceScrum.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *
 * Vincent Barrier (vbarrier@kagilum.com)
 * Nicolas Noullet (nnoullet@kagilum.com)
 *
 */

extensibleController('backlogDetailsCtrl', ['$scope', 'StoryService', 'BacklogService', 'backlog', function($scope, StoryService, BacklogService, backlog) {
    // Functions
    $scope.refreshBacklog = function(backlog) {
        $scope.stories = BacklogService.filterStories(backlog, $scope.project.stories);
    };
    // Init
    $scope.backlog = backlog;
    $scope.refreshBacklog(backlog);
    // Ensures that the stories list of displayed backlogs are up to date
    $scope.$on('is:backlogsUpdated', function(event, backlogCodes) {
        if (_.includes(backlogCodes, backlog.code)) {
            $scope.refreshBacklog(backlog);
        }
    });
}]);

extensibleController('backlogChartCtrl', ['$scope', '$controller', '$element', function($scope, $controller, $element) {
    $controller('chartCtrl', {$scope: $scope, $element: $element});
    $scope.$watchGroup(['backlog.chartType', 'backlog.chartUnit'], function(newValues) {
        var unit = newValues[1];
        var chartName = newValues[0] + (unit ? '-' + unit : ''); // Hack to preserve the chartLoaderInterface while using an additional parameter
        $scope.openChart('backlog', chartName, $scope.backlog);
    }, true);
}]);

extensibleController('backlogCtrl', ['$controller', '$scope', 'window', '$filter', '$timeout', '$state', 'StoryService', 'BacklogService', 'BacklogCodes', 'StoryStatesByName', 'project', 'backlogs', function($controller, $scope, window, $filter, $timeout, $state, StoryService, BacklogService, BacklogCodes, StoryStatesByName, project, backlogs) {
    $controller('windowCtrl', {$scope: $scope, window: window}); // inherit from windowCtrl
    // Functions
    $scope.authorizedStory = StoryService.authorizedStory;
    $scope.isSelected = function(selectable) {
        if ($state.params.storyId) {
            return $state.params.storyId === selectable.id;
        } else if ($state.params.storyListId) {
            return _.includes($state.params.storyListId.split(','), selectable.id.toString());
        } else {
            return false;
        }
    };
    $scope.hasSelected = function() {
        return $state.params.storyId !== undefined || $state.params.storyListId !== undefined;
    };
    $scope.toggleSelectableMultiple = function() {
        $scope.selectableOptions.selectingMultiple = !$scope.selectableOptions.selectingMultiple;
        if ($state.params.storyListId !== undefined) {
            var currentStateName = $state.current.name;
            var storyIndexInStateName = currentStateName.indexOf('story');
            $state.go(currentStateName.slice(0, storyIndexInStateName - 1));
        }
    };
    $scope.refreshSingleBacklog = function(backlogContainer) {
        var backlog = backlogContainer.backlog;
        var filteredStories = BacklogService.filterStories(backlog, $scope.project.stories);
        backlog.stories = $filter('orderBy')(filteredStories, 'rank'); // This will be the model for the sortable directive so it must be in sort order, regardless of current display order
        if (backlog.stories && backlog.stories.length > 0) {
            backlogContainer.storiesLoaded = true; // Render stories already there in the client cache
        }
        backlogContainer.sortable = StoryService.authorizedStory('rank') && (BacklogService.isBacklog(backlog) || BacklogService.isSandbox(backlog));
        $timeout(function() { // Timeout to wait for story rendering
            $scope.$emit('selectable-refresh');
        }, 0);
    };
    var getValueEffortRateForSorting = function(story) {
        var rate = -3; // Rate = -3 when no effort (null) & no value (0)
        if (story.value === 0) {
            if (story.effort !== null) {
                rate = -story.effort / 10000; // Rate spans from 0 to -1 (unless effort is > 1000, very unlikely), higher effort => lower rate
            }
        } else {
            if (story.effort === null) {
                rate = -1 / story.value - 1; // Rate spans from -2 to -1, higher value => higher rate
            } else {
                rate = story.value / story.effort; // Rate spans from 0 to Infinity, higher value compared to effort => higher rate
            }
        }
        return rate;
    };
    $scope.orderBacklogByRank = function(backlogContainer) {
        backlogContainer.orderBy.reverse = false;
        $scope.changeBacklogOrder(backlogContainer, _.find(backlogContainer.orderBy.values, {id: 'rank'}));
    };
    $scope.changeBacklogOrder = function(backlogContainer, order) {
        var newOrder = _.clone(order);
        newOrder.value = [order.value];
        if (order.id !== 'rank') {
            newOrder.value.push('id'); // Order by id is crucial to ensure stable order when duplicate value (e.g. same effort) regardless of storyService.list order which itself depends on navigation order
        }
        if (BacklogService.isAll(backlogContainer.backlog) && order.id === 'rank') { // Hack to ensure that rank sort in "All" backlog is consistent with individual backlog ranking
            var sortByStateGroupingByBacklogState = function(story) {
                var orderCriteria = story.state === StoryStatesByName.ESTIMATED ? StoryStatesByName.ACCEPTED : story.state; // Ignore the differences betweed accepted and estimated
                return -orderCriteria; // "minus" the state to make done stories more prioritary
            };
            newOrder.value.unshift(sortByStateGroupingByBacklogState);
        }
        backlogContainer.orderBy.current = newOrder;
    };
    $scope.reverseBacklogOrder = function(backlogContainer) {
        backlogContainer.orderBy.reverse = !backlogContainer.orderBy.reverse;
    };
    $scope.isSortingBacklog = function(backlogContainer) {
        return backlogContainer.sortable && backlogContainer.orderBy.current.id === 'rank' && !backlogContainer.orderBy.reverse && !$scope.hasContextOrSearch();
    };
    $scope.enableSortable = function(backlogContainer) {
        $scope.clearContextAndSearch();
        $scope.orderBacklogByRank(backlogContainer)
    };
    $scope.openStoryUrl = function(storyId) {
        return '#/' + $scope.viewName + '/' + $state.params.elementId + '/story/' + storyId;
    };
    $scope.closeBacklogUrl = function(backlog) {
        var stateParams;
        if (backlog.code === $state.params.pinnedElementId) {
            stateParams = {pinnedElementId: $state.params.elementId, elementId: null};
        } else {
            stateParams = {elementId: null};
        }
        return $state.href('.', stateParams);
    };
    $scope.getBacklogContainer = function(backlogCode) {
        return _.find($scope.backlogContainers, function(backlogContainer) {
            return backlogContainer.backlog.code === backlogCode;
        });
    };
    $scope.showBacklog = function(backlogCode) {
        var backlogContainer = $scope.getBacklogContainer(backlogCode);
        if (!backlogContainer) {
            backlogContainer = {
                backlog: _.find($scope.availableBacklogs, {code: backlogCode}),
                storiesLoaded: false,
                orderBy: {
                    values: _.sortBy($scope.sortOptions, 'name')
                }
            };
            $scope.orderBacklogByRank(backlogContainer);
            $scope.refreshSingleBacklog(backlogContainer); // Init the backlog from client data (storyService.list) + init sortable variable
            var setStoriesLoaded = function() {
                backlogContainer.storiesLoaded = true;
            };
            var retrieveServerStories = function() { // Retrieve server data, stories that were missing will be automatically added
                StoryService.listByBacklog(backlogContainer.backlog, project.id).then(function(stories) {
                    if (stories.length === 0) {
                        setStoriesLoaded();
                    }
                });
            };
            if (backlogContainer.backlog.count > 500) {
                $scope.confirm({message: $scope.message('todo.is.ui.backlog.load.confirm'), callback: retrieveServerStories, closeCallback: setStoriesLoaded});
            } else {
                retrieveServerStories();
            }
            $scope.backlogContainers.push(backlogContainer);
            var savedBacklogsOrder = $scope.getWindowSetting('elementsListOrder');
            if (savedBacklogsOrder) {
                $scope.backlogContainers.sort(function(a, b) {
                    return savedBacklogsOrder.indexOf(a.backlog.code) - savedBacklogsOrder.indexOf(b.backlog.code)
                });
            } else {
                // Keep old fashion way for all users that never reordered their backlog at least one time
                $scope.backlogContainers = _.sortBy($scope.backlogContainers, function(backlogContainer) {
                    return backlogContainer.backlog.id;
                });
            }
        }
    };
    $scope.$watchGroup([function() { return $state.$current.self.name; }, function() { return $state.params.pinnedElementId; }, function() { return $state.params.elementId; }], function(newValues) {
        var stateName = newValues[0];
        var pinnedElementId = newValues[1];
        var elementId = newValues[2];
        if (stateName === 'backlog') {
            var visibleElementsListOrder = $scope.getWindowSetting('elementsListOrder');
            var defaultBacklogCode = _.head($scope.availableBacklogs).code;
            if (visibleElementsListOrder && visibleElementsListOrder.length > 0 && _.find($scope.availableBacklogs, {code: _.head(visibleElementsListOrder)})) {
                defaultBacklogCode = _.head(visibleElementsListOrder);
            }
            $state.go('backlog.backlog', {elementId: defaultBacklogCode}, {location: 'replace'});
        } else if (_.startsWith(stateName, 'backlog')) {
            if (pinnedElementId) {
                $scope.showBacklog(pinnedElementId);
            }
            if (elementId) {
                $scope.showBacklog(elementId);
            }
            _.remove($scope.backlogContainers, function(backlogContainer) {
                return !_.includes([pinnedElementId, elementId], backlogContainer.backlog.code);
            });
        }
    });
    $scope.openBacklogUrl = function(backlog) {
        var stateName = 'backlog.backlog';
        if ($state.current.name != 'backlog.backlog.details') {
            stateName += '.details';
        }
        return $state.href(stateName, {elementId: backlog.code});
    };
    // Init
    $scope.viewName = 'backlog';
    $scope.project = project;
    $scope.sortOptions = [
        {id: 'effort', value: 'effort', name: $scope.message('todo.is.ui.sort.effort')},
        {id: 'rank', value: 'rank', name: $scope.message('todo.is.ui.sort.rank')},
        {id: 'name', value: 'name', name: $scope.message('todo.is.ui.sort.name')},
        {id: 'tasks_count', value: 'tasks_count', name: $scope.message('todo.is.ui.sort.tasks')},
        {id: 'suggestedDate', value: 'suggestedDate', name: $scope.message('todo.is.ui.sort.date')},
        {id: 'feature.id', value: 'feature.id', name: $scope.message('todo.is.ui.sort.feature')},
        {id: 'value', value: 'value', name: $scope.message('todo.is.ui.sort.value')},
        {id: 'type', value: 'type', name: $scope.message('todo.is.ui.sort.type')},
        {id: 'state', value: 'state', name: $scope.message('todo.is.ui.sort.state')},
        {id: 'value/effort', value: getValueEffortRateForSorting, name: $scope.message('todo.is.ui.sort.value.effort.rate')}
    ];
    $scope.backlogSortableOptions = {
        itemMoved: function(event) {
            var story = event.source.itemScope.modelValue;
            var newRank = event.dest.index + 1;
            var sourceScope = event.source.sortableScope;
            var destScope = event.dest.sortableScope;
            var promise;
            if (BacklogService.isBacklog(sourceScope.backlogContainer.backlog) && BacklogService.isSandbox(destScope.backlogContainer.backlog)) {
                promise = StoryService.updateState(story, 'returnToSandbox', newRank);
            } else if (BacklogService.isSandbox(sourceScope.backlogContainer.backlog) && BacklogService.isBacklog(destScope.backlogContainer.backlog)) {
                promise = StoryService.updateState(story, 'accept', newRank);
            }
            if (promise) {
                promise.catch(function() {
                    $scope.revertSortable(event);
                });
            }
        },
        orderChanged: function(event) {
            var story = event.source.itemScope.modelValue;
            var newRank = event.dest.index + 1;
            story.rank = newRank;
            StoryService.update(story).then(function(updatedStory) {
                if (updatedStory.rank !== newRank) {
                    $scope.notifyWarning('is.ui.story.warning.rank.dependsOn');
                }
            }).catch(function() {
                $scope.revertSortable(event);
            });
        },
        accept: function(sourceItemHandleScope, destSortableScope) {
            var sameSortable = sourceItemHandleScope.itemScope.sortableScope.sortableId === destSortableScope.sortableId;
            // We don't check more that the fact that the dest backlog is also sorting
            // because we know that the only backlogs that can be sorted (Sandbox & Backlog) can always be inter-sorted (accept <-> return to backlog)
            return sameSortable && destSortableScope.isSortingBacklog(destSortableScope.backlogContainer);
        }
    };
    $scope.sortableId = 'backlog';
    $scope.selectableOptions = {
        notSelectableSelector: '.action, button, a',
        allowMultiple: true,
        selectionUpdated: function(selectedIds) {
            var currentStateName = $state.current.name;
            var storyIndexInStateName = currentStateName.indexOf('story');
            if (selectedIds.length === 0) {
                if (storyIndexInStateName != -1) {
                    $state.go(currentStateName.slice(0, storyIndexInStateName - 1));
                }
            } else {
                var stateName;
                var stateParams;
                if (_.startsWith(currentStateName, 'backlog.backlog')) {
                    stateName = 'backlog.backlog.story'
                } else if (_.startsWith(currentStateName, 'backlog.multiple')) {
                    stateName = 'backlog.multiple.story'
                }
                if (selectedIds.length === 1) {
                    stateName += '.details' + ($state.params.storyTabId ? '.tab' : '');
                    stateParams = {storyId: selectedIds};
                } else {
                    stateName += '.multiple';
                    stateParams = {storyListId: selectedIds.join(",")};
                }
                $state.go(stateName, stateParams);
            }
        }
    };
    $scope.backlogContainers = [];
    $scope.availableBacklogs = backlogs;
    $scope.backlogCodes = BacklogCodes;
    var backlogsPendingUpdate = [];
    // Ensures that the stories list of displayed backlogs are up to date
    $scope.$on('is:backlogsUpdated', function(event, backlogCodes) {
        _.each(backlogCodes, function(backlogCode) {
            var backlogContainer = $scope.getBacklogContainer(backlogCode);
            if (backlogContainer && !_.includes(backlogsPendingUpdate, backlogCode)) {
                backlogsPendingUpdate.push(backlogCode);
                $timeout(function() {
                    _.pull(backlogsPendingUpdate, backlogCode);
                    $scope.refreshSingleBacklog(backlogContainer);
                }, 25);
            }
        });
    });
    $scope.storyListGetters = []; // Mandatory to share code between two separate substates
    $scope.findPreviousOrNextStory = StoryService.findPreviousOrNextStory($scope.storyListGetters);
}]);<commit_msg>Avoid js error<commit_after>/*
 * Copyright (c) 2014 Kagilum SAS.
 *
 * This file is part of iceScrum.
 *
 * iceScrum is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License.
 *
 * iceScrum is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with iceScrum.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *
 * Vincent Barrier (vbarrier@kagilum.com)
 * Nicolas Noullet (nnoullet@kagilum.com)
 *
 */

extensibleController('backlogDetailsCtrl', ['$scope', 'StoryService', 'BacklogService', 'backlog', function($scope, StoryService, BacklogService, backlog) {
    // Functions
    $scope.refreshBacklog = function(backlog) {
        $scope.stories = BacklogService.filterStories(backlog, $scope.project.stories);
    };
    // Init
    $scope.backlog = backlog;
    $scope.refreshBacklog(backlog);
    // Ensures that the stories list of displayed backlogs are up to date
    $scope.$on('is:backlogsUpdated', function(event, backlogCodes) {
        if (_.includes(backlogCodes, backlog.code)) {
            $scope.refreshBacklog(backlog);
        }
    });
}]);

extensibleController('backlogChartCtrl', ['$scope', '$controller', '$element', function($scope, $controller, $element) {
    $controller('chartCtrl', {$scope: $scope, $element: $element});
    var displayChart = function() {
        var unit = $scope.backlog.chartUnit;
        var chartName = $scope.backlog.chartType + (unit ? '-' + unit : ''); // Hack to preserve the chartLoaderInterface while using an additional parameter
        $scope.openChart('backlog', chartName, $scope.backlog);
    };
    displayChart();
    $scope.$watchGroup(['backlog.chartType', 'backlog.chartUnit'], displayChart, true);
}]);

extensibleController('backlogCtrl', ['$controller', '$scope', 'window', '$filter', '$timeout', '$state', 'StoryService', 'BacklogService', 'BacklogCodes', 'StoryStatesByName', 'project', 'backlogs', function($controller, $scope, window, $filter, $timeout, $state, StoryService, BacklogService, BacklogCodes, StoryStatesByName, project, backlogs) {
    $controller('windowCtrl', {$scope: $scope, window: window}); // inherit from windowCtrl
    // Functions
    $scope.authorizedStory = StoryService.authorizedStory;
    $scope.isSelected = function(selectable) {
        if ($state.params.storyId) {
            return $state.params.storyId === selectable.id;
        } else if ($state.params.storyListId) {
            return _.includes($state.params.storyListId.split(','), selectable.id.toString());
        } else {
            return false;
        }
    };
    $scope.hasSelected = function() {
        return $state.params.storyId !== undefined || $state.params.storyListId !== undefined;
    };
    $scope.toggleSelectableMultiple = function() {
        $scope.selectableOptions.selectingMultiple = !$scope.selectableOptions.selectingMultiple;
        if ($state.params.storyListId !== undefined) {
            var currentStateName = $state.current.name;
            var storyIndexInStateName = currentStateName.indexOf('story');
            $state.go(currentStateName.slice(0, storyIndexInStateName - 1));
        }
    };
    $scope.refreshSingleBacklog = function(backlogContainer) {
        var backlog = backlogContainer.backlog;
        var filteredStories = BacklogService.filterStories(backlog, $scope.project.stories);
        backlog.stories = $filter('orderBy')(filteredStories, 'rank'); // This will be the model for the sortable directive so it must be in sort order, regardless of current display order
        if (backlog.stories && backlog.stories.length > 0) {
            backlogContainer.storiesLoaded = true; // Render stories already there in the client cache
        }
        backlogContainer.sortable = StoryService.authorizedStory('rank') && (BacklogService.isBacklog(backlog) || BacklogService.isSandbox(backlog));
        $timeout(function() { // Timeout to wait for story rendering
            $scope.$emit('selectable-refresh');
        }, 0);
    };
    var getValueEffortRateForSorting = function(story) {
        var rate = -3; // Rate = -3 when no effort (null) & no value (0)
        if (story.value === 0) {
            if (story.effort !== null) {
                rate = -story.effort / 10000; // Rate spans from 0 to -1 (unless effort is > 1000, very unlikely), higher effort => lower rate
            }
        } else {
            if (story.effort === null) {
                rate = -1 / story.value - 1; // Rate spans from -2 to -1, higher value => higher rate
            } else {
                rate = story.value / story.effort; // Rate spans from 0 to Infinity, higher value compared to effort => higher rate
            }
        }
        return rate;
    };
    $scope.orderBacklogByRank = function(backlogContainer) {
        backlogContainer.orderBy.reverse = false;
        $scope.changeBacklogOrder(backlogContainer, _.find(backlogContainer.orderBy.values, {id: 'rank'}));
    };
    $scope.changeBacklogOrder = function(backlogContainer, order) {
        var newOrder = _.clone(order);
        newOrder.value = [order.value];
        if (order.id !== 'rank') {
            newOrder.value.push('id'); // Order by id is crucial to ensure stable order when duplicate value (e.g. same effort) regardless of storyService.list order which itself depends on navigation order
        }
        if (BacklogService.isAll(backlogContainer.backlog) && order.id === 'rank') { // Hack to ensure that rank sort in "All" backlog is consistent with individual backlog ranking
            var sortByStateGroupingByBacklogState = function(story) {
                var orderCriteria = story.state === StoryStatesByName.ESTIMATED ? StoryStatesByName.ACCEPTED : story.state; // Ignore the differences betweed accepted and estimated
                return -orderCriteria; // "minus" the state to make done stories more prioritary
            };
            newOrder.value.unshift(sortByStateGroupingByBacklogState);
        }
        backlogContainer.orderBy.current = newOrder;
    };
    $scope.reverseBacklogOrder = function(backlogContainer) {
        backlogContainer.orderBy.reverse = !backlogContainer.orderBy.reverse;
    };
    $scope.isSortingBacklog = function(backlogContainer) {
        return backlogContainer.sortable && backlogContainer.orderBy.current.id === 'rank' && !backlogContainer.orderBy.reverse && !$scope.hasContextOrSearch();
    };
    $scope.enableSortable = function(backlogContainer) {
        $scope.clearContextAndSearch();
        $scope.orderBacklogByRank(backlogContainer)
    };
    $scope.openStoryUrl = function(storyId) {
        return '#/' + $scope.viewName + '/' + $state.params.elementId + '/story/' + storyId;
    };
    $scope.closeBacklogUrl = function(backlog) {
        var stateParams;
        if (backlog.code === $state.params.pinnedElementId) {
            stateParams = {pinnedElementId: $state.params.elementId, elementId: null};
        } else {
            stateParams = {elementId: null};
        }
        return $state.href('.', stateParams);
    };
    $scope.getBacklogContainer = function(backlogCode) {
        return _.find($scope.backlogContainers, function(backlogContainer) {
            return backlogContainer.backlog.code === backlogCode;
        });
    };
    $scope.showBacklog = function(backlogCode) {
        var backlogContainer = $scope.getBacklogContainer(backlogCode);
        if (!backlogContainer) {
            backlogContainer = {
                backlog: _.find($scope.availableBacklogs, {code: backlogCode}),
                storiesLoaded: false,
                orderBy: {
                    values: _.sortBy($scope.sortOptions, 'name')
                }
            };
            $scope.orderBacklogByRank(backlogContainer);
            $scope.refreshSingleBacklog(backlogContainer); // Init the backlog from client data (storyService.list) + init sortable variable
            var setStoriesLoaded = function() {
                backlogContainer.storiesLoaded = true;
            };
            var retrieveServerStories = function() { // Retrieve server data, stories that were missing will be automatically added
                StoryService.listByBacklog(backlogContainer.backlog, project.id).then(function(stories) {
                    if (stories.length === 0) {
                        setStoriesLoaded();
                    }
                });
            };
            if (backlogContainer.backlog.count > 500) {
                $scope.confirm({message: $scope.message('todo.is.ui.backlog.load.confirm'), callback: retrieveServerStories, closeCallback: setStoriesLoaded});
            } else {
                retrieveServerStories();
            }
            $scope.backlogContainers.push(backlogContainer);
            var savedBacklogsOrder = $scope.getWindowSetting('elementsListOrder');
            if (savedBacklogsOrder) {
                $scope.backlogContainers.sort(function(a, b) {
                    return savedBacklogsOrder.indexOf(a.backlog.code) - savedBacklogsOrder.indexOf(b.backlog.code)
                });
            } else {
                // Keep old fashion way for all users that never reordered their backlog at least one time
                $scope.backlogContainers = _.sortBy($scope.backlogContainers, function(backlogContainer) {
                    return backlogContainer.backlog.id;
                });
            }
        }
    };
    $scope.$watchGroup([function() { return $state.$current.self.name; }, function() { return $state.params.pinnedElementId; }, function() { return $state.params.elementId; }], function(newValues) {
        var stateName = newValues[0];
        var pinnedElementId = newValues[1];
        var elementId = newValues[2];
        if (stateName === 'backlog') {
            var visibleElementsListOrder = $scope.getWindowSetting('elementsListOrder');
            var defaultBacklogCode = _.head($scope.availableBacklogs).code;
            if (visibleElementsListOrder && visibleElementsListOrder.length > 0 && _.find($scope.availableBacklogs, {code: _.head(visibleElementsListOrder)})) {
                defaultBacklogCode = _.head(visibleElementsListOrder);
            }
            $state.go('backlog.backlog', {elementId: defaultBacklogCode}, {location: 'replace'});
        } else if (_.startsWith(stateName, 'backlog')) {
            if (pinnedElementId) {
                $scope.showBacklog(pinnedElementId);
            }
            if (elementId) {
                $scope.showBacklog(elementId);
            }
            _.remove($scope.backlogContainers, function(backlogContainer) {
                return !_.includes([pinnedElementId, elementId], backlogContainer.backlog.code);
            });
        }
    });
    $scope.openBacklogUrl = function(backlog) {
        var stateName = 'backlog.backlog';
        if ($state.current.name != 'backlog.backlog.details') {
            stateName += '.details';
        }
        return $state.href(stateName, {elementId: backlog.code});
    };
    // Init
    $scope.viewName = 'backlog';
    $scope.project = project;
    $scope.sortOptions = [
        {id: 'effort', value: 'effort', name: $scope.message('todo.is.ui.sort.effort')},
        {id: 'rank', value: 'rank', name: $scope.message('todo.is.ui.sort.rank')},
        {id: 'name', value: 'name', name: $scope.message('todo.is.ui.sort.name')},
        {id: 'tasks_count', value: 'tasks_count', name: $scope.message('todo.is.ui.sort.tasks')},
        {id: 'suggestedDate', value: 'suggestedDate', name: $scope.message('todo.is.ui.sort.date')},
        {id: 'feature.id', value: 'feature.id', name: $scope.message('todo.is.ui.sort.feature')},
        {id: 'value', value: 'value', name: $scope.message('todo.is.ui.sort.value')},
        {id: 'type', value: 'type', name: $scope.message('todo.is.ui.sort.type')},
        {id: 'state', value: 'state', name: $scope.message('todo.is.ui.sort.state')},
        {id: 'value/effort', value: getValueEffortRateForSorting, name: $scope.message('todo.is.ui.sort.value.effort.rate')}
    ];
    $scope.backlogSortableOptions = {
        itemMoved: function(event) {
            var story = event.source.itemScope.modelValue;
            var newRank = event.dest.index + 1;
            var sourceScope = event.source.sortableScope;
            var destScope = event.dest.sortableScope;
            var promise;
            if (BacklogService.isBacklog(sourceScope.backlogContainer.backlog) && BacklogService.isSandbox(destScope.backlogContainer.backlog)) {
                promise = StoryService.updateState(story, 'returnToSandbox', newRank);
            } else if (BacklogService.isSandbox(sourceScope.backlogContainer.backlog) && BacklogService.isBacklog(destScope.backlogContainer.backlog)) {
                promise = StoryService.updateState(story, 'accept', newRank);
            }
            if (promise) {
                promise.catch(function() {
                    $scope.revertSortable(event);
                });
            }
        },
        orderChanged: function(event) {
            var story = event.source.itemScope.modelValue;
            var newRank = event.dest.index + 1;
            story.rank = newRank;
            StoryService.update(story).then(function(updatedStory) {
                if (updatedStory.rank !== newRank) {
                    $scope.notifyWarning('is.ui.story.warning.rank.dependsOn');
                }
            }).catch(function() {
                $scope.revertSortable(event);
            });
        },
        accept: function(sourceItemHandleScope, destSortableScope) {
            var sameSortable = sourceItemHandleScope.itemScope.sortableScope.sortableId === destSortableScope.sortableId;
            // We don't check more that the fact that the dest backlog is also sorting
            // because we know that the only backlogs that can be sorted (Sandbox & Backlog) can always be inter-sorted (accept <-> return to backlog)
            return sameSortable && destSortableScope.isSortingBacklog(destSortableScope.backlogContainer);
        }
    };
    $scope.sortableId = 'backlog';
    $scope.selectableOptions = {
        notSelectableSelector: '.action, button, a',
        allowMultiple: true,
        selectionUpdated: function(selectedIds) {
            var currentStateName = $state.current.name;
            var storyIndexInStateName = currentStateName.indexOf('story');
            if (selectedIds.length === 0) {
                if (storyIndexInStateName != -1) {
                    $state.go(currentStateName.slice(0, storyIndexInStateName - 1));
                }
            } else {
                var stateName;
                var stateParams;
                if (_.startsWith(currentStateName, 'backlog.backlog')) {
                    stateName = 'backlog.backlog.story'
                } else if (_.startsWith(currentStateName, 'backlog.multiple')) {
                    stateName = 'backlog.multiple.story'
                }
                if (selectedIds.length === 1) {
                    stateName += '.details' + ($state.params.storyTabId ? '.tab' : '');
                    stateParams = {storyId: selectedIds};
                } else {
                    stateName += '.multiple';
                    stateParams = {storyListId: selectedIds.join(",")};
                }
                $state.go(stateName, stateParams);
            }
        }
    };
    $scope.backlogContainers = [];
    $scope.availableBacklogs = backlogs;
    $scope.backlogCodes = BacklogCodes;
    var backlogsPendingUpdate = [];
    // Ensures that the stories list of displayed backlogs are up to date
    $scope.$on('is:backlogsUpdated', function(event, backlogCodes) {
        _.each(backlogCodes, function(backlogCode) {
            var backlogContainer = $scope.getBacklogContainer(backlogCode);
            if (backlogContainer && !_.includes(backlogsPendingUpdate, backlogCode)) {
                backlogsPendingUpdate.push(backlogCode);
                $timeout(function() {
                    _.pull(backlogsPendingUpdate, backlogCode);
                    $scope.refreshSingleBacklog(backlogContainer);
                }, 25);
            }
        });
    });
    $scope.storyListGetters = []; // Mandatory to share code between two separate substates
    $scope.findPreviousOrNextStory = StoryService.findPreviousOrNextStory($scope.storyListGetters);
}]);<|endoftext|>


<commit_before>/*!
 * [react-events](https://github.com/jhudson8/react-events) v0.4.3;  MIT license; Joe Hudson<joehud_AT_gmail.com>
 */
!function(n){"function"==typeof define&&define.amd?define(["react"],n):"undefined"!=typeof exports&&"undefined"!=typeof require?module.exports=function(t){n(t)}:n(React)}(function(n){function t(f,l,s,c){if(!c){var u,h=l;if("object"==typeof l&&(h=l.callback.call(this)),"string"==typeof l&&(u=o.indexOf(l)>=0,h=s[l]),!h)throw'no callback function exists for "'+l+'"';l=function(){return h.apply(s,u?[]:arguments)}}var d=f.match(a);if(d){var p=d[1],v=d[2].split(/\s*,\s*/),m=d[3],g=n.events.specials[p];if(g)return 1===v.length&&""===v[0]&&(v=[]),l=g.call(s,l,v),t(m,l,s,!0);throw new Error('invalid special event handler "'+p+"'")}var y=f.match(r),w=y[1];path=y[2],handler=e[w];for(var x=0;!handler&&x<i.length;x++)w.match(i[x].pattern)&&(handler=i[x].handler);if(!handler)throw'no handler registered for "'+f+'"';return handler.call(s,{key:w,path:path},l)}var e={},i=[],r=/^([^:]+):?(.*)/,a=/^\*([^\(]+)\(([^)]*)\):(.*)/,o=["forceUpdate"],f={standard:function(n){var t={on:n.onKey||"on",off:n.offKey||"off"},e=n.target;return function(i,r){function a(n,i){return function(){var a="function"==typeof e?e.call(i,o):e;a&&a[t[n]](o,r)}}var o=i.path;return{on:a("on",this),off:a("off",this),initialize:n.initialize}}}},l=n.events={specials:{},handle:function(n,t){"function"!=typeof t&&(t=f[t.type||"standard"](t)),n instanceof RegExp?i.push({pattern:n,handler:t}):e[n]=t}};"undefined"!=typeof window&&l.handle("window",{target:window,onKey:"addEventListener",offKey:"removeEventListener"}),l.handle("ref",function(n,t){var e,i,a=n.path.match(r),o=a[1],f=a[2];return{on:function(){var n=this.refs[o];n&&(i=n.state||n,n.on(f,t),e=n)},off:function(){e&&(e.off(f,t),e=void 0,i=void 0)},isStale:function(){if(!e)return!!this.refs[o];var n=this.refs[o];return n&&(n.state||n)===i?void 0:!0}}}),l.handle("dom",function(n,t){var e=n.path.match(r);return{on:function(){$(this.getDOMNode()).on(e[1],e[2],t)},off:function(){$(this.getDOMNode()).off(e[1],e[2],t)}}}),l.handle("repeat",function(n,t){var e,i=parseInt(n.path,10);return{on:function(){e=setInterval(t,i)},off:function(){e=!!clearInterval(e)}}}),l.handle("!repeat",function(n,t){function e(n){n!==!0&&t(),setTimeout(function(){i&&requestAnimationFrame(e)},r)}var i,r=parseInt(n.path,10);return{on:function(){i=!0,e(!0)},off:function(){i=!1}}}),n.mixins.add("events",function(){function n(n,t){return function(){n.apply(t,arguments)}}var e=[{getInitialState:function(){var n=this._eventHandlers=[];if(this.events){var e;for(var i in this.events)e=t(i,this.events[i],this),e.initialize&&e.initialize.call(this),n.push(e)}return null},componentDidUpdate:function(){for(var n,t=this._eventHandlers,e=0;e<t.length;e++)n=t[e],n.isStale&&n.isStale.call(this)&&(n.off.call(this),n.on.call(this))},componentDidMount:function(){for(var n=this._eventHandlers,t=0;t<n.length;t++)n[t].on.call(this)},componentWillUnmount:function(){for(var n=this._eventHandlers,t=0;t<n.length;t++)n[t].off.call(this)}}];if(l.mixin){var i={},r={};for(var a in l.mixin)i[a]=n(l.mixin[a],r);i.getInitialState=function(){return{__events:r}},e.push(i)}return e}),n.mixins.add("triggerWith",{triggerWith:function(){var n=Array.prototype.slice.call(arguments),t=this;return function(){t.trigger.apply(this,n)}}})});<commit_msg>update min<commit_after>/*!
 * [react-events](https://github.com/jhudson8/react-events) v0.4.3;  MIT license; Joe Hudson<joehud_AT_gmail.com>
 */
!function(n){"function"==typeof define&&define.amd?define(["react"],n):"undefined"!=typeof exports&&"undefined"!=typeof require?module.exports=function(t){n(t)}:n(React)}(function(n){function t(f,l,s,c){if(!c){var u,h=l;if("object"==typeof l&&(h=l.callback.call(this)),"string"==typeof l&&(u=o.indexOf(l)>=0,h=s[l]),!h)throw'no callback function exists for "'+l+'"';l=function(){return h.apply(s,u?[]:arguments)}}var d=f.match(a);if(d){var p=d[1],v=d[2].split(/\s*,\s*/),m=d[3],g=n.events.specials[p];if(g)return 1===v.length&&""===v[0]&&(v=[]),l=g.call(s,l,v),t(m,l,s,!0);throw new Error('invalid special event handler "'+p+"'")}var y=f.match(r),w=y[1];path=y[2],handler=e[w];for(var x=0;!handler&&x<i.length;x++)w.match(i[x].pattern)&&(handler=i[x].handler);if(!handler)throw'no handler registered for "'+f+'"';return handler.call(s,{key:w,path:path},l)}var e={},i=[],r=/^([^:]+):?(.*)/,a=/^\*([^\(]+)\(([^)]*)\):(.*)/,o=["forceUpdate"],f={standard:function(n){var t={on:n.onKey||"on",off:n.offKey||"off"},e=n.target;return function(i,r){function a(n,i){return function(){var a="function"==typeof e?e.call(i,o):e;a&&a[t[n]](o,r)}}var o=i.path;return{on:a("on",this),off:a("off",this),initialize:n.initialize}}}},l=n.events={specials:{},handle:function(n,t){"function"!=typeof t&&(t=f[t.type||"standard"](t)),n instanceof RegExp?i.push({pattern:n,handler:t}):e[n]=t}};"undefined"!=typeof window&&l.handle("window",{target:window,onKey:"addEventListener",offKey:"removeEventListener"}),l.handle("ref",function(n,t){var e,i,a=n.path.match(r),o=a[1],f=a[2];return{on:function(){var n=this.refs[o];n&&(i=n.state||n,n.on(f,t),e=n)},off:function(){e&&(e.off(f,t),e=void 0,i=void 0)},isStale:function(){if(!e)return!!this.refs[o];var n=this.refs[o];return n&&(n.state||n)===i?void 0:!0}}}),l.handle("dom",function(n,t){var e=n.path.match(r);return{on:function(){$(this.getDOMNode()).on(e[1],e[2],t)},off:function(){$(this.getDOMNode()).off(e[1],e[2],t)}}}),l.handle("repeat",function(n,t){var e,i=parseInt(n.path,10);return{on:function(){e=setInterval(t,i)},off:function(){e=!!clearInterval(e)}}}),l.handle("!repeat",function(n,t){function e(n){n!==!0&&t(),setTimeout(function(){i&&requestAnimationFrame(e)},r)}var i,r=parseInt(n.path,10);return{on:function(){i=!0,e(!0)},off:function(){i=!1}}}),n.mixins.add("events",function(){function n(n,t){return function(){n.apply(t,arguments)}}var e=[{triggerWith:function(){var n=Array.prototype.slice.call(arguments),t=this;return function(){t.trigger.apply(this,n)}},getInitialState:function(){var n=this._eventHandlers=[];if(this.events){var e;for(var i in this.events)e=t(i,this.events[i],this),e.initialize&&e.initialize.call(this),n.push(e)}return null},componentDidUpdate:function(){for(var n,t=this._eventHandlers,e=0;e<t.length;e++)n=t[e],n.isStale&&n.isStale.call(this)&&(n.off.call(this),n.on.call(this))},componentDidMount:function(){for(var n=this._eventHandlers,t=0;t<n.length;t++)n[t].on.call(this)},componentWillUnmount:function(){for(var n=this._eventHandlers,t=0;t<n.length;t++)n[t].off.call(this)}}];if(l.mixin){var i={},r={};for(var a in l.mixin)i[a]=n(l.mixin[a],r);i.getInitialState=function(){return{__events:r}},e.push(i)}return e})});<|endoftext|>