<commit_before>(function () {
    'use strict';

    angular.module('ariaNg').factory('aria2WebSocketRpcService', ['$q', '$websocket', '$timeout', 'ariaNgConstants', 'ariaNgSettingService', 'ariaNgLogService', function ($q, $websocket, $timeout, ariaNgConstants, ariaNgSettingService, ariaNgLogService) {
        var websocketStatusConnecting = 0;
        var websocketStatusOpen = 1;

        var rpcUrl = ariaNgSettingService.getCurrentRpcUrl();
        var socketClient = null;
        var pendingReconnect = null;

        var sendIdStates = {};
        var eventCallbacks = {};

        var processMethodCallback = function (content) {
            var uniqueId = content.id;

            if (!uniqueId) {
                return;
            }

            var state = sendIdStates[uniqueId];

            if (!state) {
                return;
            }

            var context = state.context;

            state.deferred.resolve({
                success: true,
                context: context
            });

            if (content.result && context.connectionSuccessCallback) {
                context.connectionSuccessCallback({
                    rpcUrl: rpcUrl
                });
            }

            if (content.result && context.successCallback) {
                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response success', content);

                context.successCallback(context.id, content.result);
            }

            if (content.error && context.errorCallback) {
                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response error', content);

                context.errorCallback(context.id, content.error);
            }

            delete sendIdStates[uniqueId];
        };

        var processEventCallback = function (content) {
            var method = content.method;

            if (!method) {
                return;
            }

            var callbacks = eventCallbacks[method];

            if (!angular.isArray(callbacks) || callbacks.length < 1) {
                return;
            }

            for (var i = 0; i < callbacks.length; i++) {
                var callback = callbacks[i];
                var context = (angular.isArray(content.params) && content.params.length > 0 ? content.params[0] : null);
                callback(context);
            }
        };

        var getSocketClient = function (context) {
            if (socketClient === null) {
                try {
                    socketClient = $websocket(rpcUrl, {
                        maxTimeout: 1 // ms
                    });

                    socketClient.onMessage(function (message) {
                        if (!message || !message.data) {
                            return;
                        }

                        var content = angular.fromJson(message.data);

                        if (!content) {
                            return;
                        }

                        if (content.id) {
                            processMethodCallback(content);
                        } else if (content.method) {
                            processEventCallback(content);
                        }
                    });

                    socketClient.onOpen(function (e) {
                        ariaNgLogService.debug('[aria2WebSocketRpcService.onOpen] websocket is opened', e);

                        if (context && context.connectionSuccessCallback) {
                            context.connectionSuccessCallback({
                                rpcUrl: rpcUrl
                            });
                        }
                    });

                    socketClient.onClose(function (e) {
                        ariaNgLogService.warn('[aria2WebSocketRpcService.onClose] websocket is closed', e);

                        if (ariaNgSettingService.getWebSocketReconnectInterval() > 0 && context && context.connectionWaitingToReconnectCallback) {
                            context.connectionWaitingToReconnectCallback({
                                rpcUrl: rpcUrl
                            });
                            planToReconnect(context);
                        } else if (context && context.connectionFailedCallback) {
                            context.connectionFailedCallback({
                                rpcUrl: rpcUrl
                            });
                        }
                    });
                } catch (ex) {
                    return {
                        success: false,
                        error: 'Cannot initialize WebSocket!',
                        exception: ex
                    }
                }
            }

            return {
                success: true,
                instance: socketClient
            };
        };

        var reconnect = function (context) {
            if (!context || !socketClient) {
                return;
            }

            if (context.connectionReconnectingCallback) {
                context.connectionReconnectingCallback({
                    rpcUrl: rpcUrl
                });
            }

            socketClient.reconnect();
        };

        var planToReconnect = function (context) {
            if (pendingReconnect) {
                ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] another reconnection is pending');
                return;
            }

            pendingReconnect = $timeout(function () {
                if (socketClient == null) {
                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket is null');
                    pendingReconnect = null;
                    return;
                }

                if (socketClient.readyState === websocketStatusConnecting || socketClient.readyState === websocketStatusOpen) {
                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket current state is already ' + socketClient.readyState);
                    pendingReconnect = null;
                    return;
                }

                reconnect(context);
                pendingReconnect = null;
            }, ariaNgSettingService.getWebSocketReconnectInterval());

            ariaNgLogService.debug('[aria2WebSocketRpcService.planToReconnect] next reconnection is pending in ' + ariaNgSettingService.getWebSocketReconnectInterval() + "ms");
        }

        return {
            request: function (context) {
                if (!context) {
                    return;
                }

                var client = getSocketClient({
                    connectionSuccessCallback: context.connectionSuccessCallback,
                    connectionFailedCallback: context.connectionFailedCallback,
                    connectionReconnectingCallback: context.connectionReconnectingCallback,
                    connectionWaitingToReconnectCallback: context.connectionWaitingToReconnectCallback
                });
                var uniqueId = context.uniqueId;
                var requestBody = angular.toJson(context.requestBody);

                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'request start', context);

                var deferred = $q.defer();

                if (client.instance) {
                    sendIdStates[uniqueId] = {
                        context: context,
                        deferred: deferred
                    };

                    client.instance.send(requestBody);
                } else {
                    deferred.reject({
                        success: false,
                        context: context
                    });

                    ariaNgLogService.debug('[aria2WebSocketRpcService.request] client error', client);
                    context.errorCallback(context.id, { message: client.error });
                }

                return deferred.promise;
            },
            reconnect: function (context) {
                reconnect(context);
            },
            on: function (eventName, callback) {
                var callbacks = eventCallbacks[eventName];

                if (!angular.isArray(callbacks)) {
                    callbacks = eventCallbacks[eventName] = [];
                }

                callbacks.push(callback);
            }
        };
    }]);
}());
<commit_msg>remove unused state id cache after reconnecting<commit_after>(function () {
    'use strict';

    angular.module('ariaNg').factory('aria2WebSocketRpcService', ['$q', '$websocket', '$timeout', 'ariaNgConstants', 'ariaNgSettingService', 'ariaNgLogService', function ($q, $websocket, $timeout, ariaNgConstants, ariaNgSettingService, ariaNgLogService) {
        var websocketStatusConnecting = 0;
        var websocketStatusOpen = 1;

        var rpcUrl = ariaNgSettingService.getCurrentRpcUrl();
        var socketClient = null;
        var pendingReconnect = null;

        var sendIdStates = {};
        var eventCallbacks = {};

        var processMethodCallback = function (content) {
            var uniqueId = content.id;

            if (!uniqueId) {
                return;
            }

            var state = sendIdStates[uniqueId];

            if (!state) {
                return;
            }

            var context = state.context;

            state.deferred.resolve({
                success: true,
                context: context
            });

            if (content.result && context.connectionSuccessCallback) {
                context.connectionSuccessCallback({
                    rpcUrl: rpcUrl
                });
            }

            if (content.result && context.successCallback) {
                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response success', content);

                context.successCallback(context.id, content.result);
            }

            if (content.error && context.errorCallback) {
                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'response error', content);

                context.errorCallback(context.id, content.error);
            }

            delete sendIdStates[uniqueId];
        };

        var processEventCallback = function (content) {
            var method = content.method;

            if (!method) {
                return;
            }

            var callbacks = eventCallbacks[method];

            if (!angular.isArray(callbacks) || callbacks.length < 1) {
                return;
            }

            for (var i = 0; i < callbacks.length; i++) {
                var callback = callbacks[i];
                var context = (angular.isArray(content.params) && content.params.length > 0 ? content.params[0] : null);
                callback(context);
            }
        };

        var getSocketClient = function (context) {
            if (socketClient === null) {
                try {
                    socketClient = $websocket(rpcUrl, {
                        maxTimeout: 1 // ms
                    });

                    socketClient.onMessage(function (message) {
                        if (!message || !message.data) {
                            return;
                        }

                        var content = angular.fromJson(message.data);

                        if (!content) {
                            return;
                        }

                        if (content.id) {
                            processMethodCallback(content);
                        } else if (content.method) {
                            processEventCallback(content);
                        }
                    });

                    socketClient.onOpen(function (e) {
                        ariaNgLogService.debug('[aria2WebSocketRpcService.onOpen] websocket is opened', e);

                        if (context && context.connectionSuccessCallback) {
                            context.connectionSuccessCallback({
                                rpcUrl: rpcUrl
                            });
                        }
                    });

                    socketClient.onClose(function (e) {
                        ariaNgLogService.warn('[aria2WebSocketRpcService.onClose] websocket is closed', e);

                        if (ariaNgSettingService.getWebSocketReconnectInterval() > 0 && context && context.connectionWaitingToReconnectCallback) {
                            context.connectionWaitingToReconnectCallback({
                                rpcUrl: rpcUrl
                            });
                            planToReconnect(context);
                        } else if (context && context.connectionFailedCallback) {
                            context.connectionFailedCallback({
                                rpcUrl: rpcUrl
                            });
                        }
                    });
                } catch (ex) {
                    return {
                        success: false,
                        error: 'Cannot initialize WebSocket!',
                        exception: ex
                    }
                }
            }

            return {
                success: true,
                instance: socketClient
            };
        };

        var reconnect = function (context) {
            if (!context || !socketClient) {
                return;
            }

            for (var uniqueId in sendIdStates) {
                if (!sendIdStates.hasOwnProperty(uniqueId)) {
                    continue;
                }

                var state = sendIdStates[uniqueId];

                if (!state) {
                    delete sendIdStates[uniqueId];
                    continue;
                }

                state.deferred.reject({
                    success: false,
                    context: state.context
                });

                ariaNgLogService.debug('[aria2WebSocketRpcService.reconnect] reject old request', state.context);
                state.context.errorCallback(state.context.id, { message: 'Cannot connect to aria2!' });

                delete sendIdStates[uniqueId];
            }

            if (context.connectionReconnectingCallback) {
                context.connectionReconnectingCallback({
                    rpcUrl: rpcUrl
                });
            }

            socketClient.reconnect();
        };

        var planToReconnect = function (context) {
            if (pendingReconnect) {
                ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] another reconnection is pending');
                return;
            }

            pendingReconnect = $timeout(function () {
                if (socketClient == null) {
                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket is null');
                    pendingReconnect = null;
                    return;
                }

                if (socketClient.readyState === websocketStatusConnecting || socketClient.readyState === websocketStatusOpen) {
                    ariaNgLogService.warn('[aria2WebSocketRpcService.planToReconnect] websocket current state is already ' + socketClient.readyState);
                    pendingReconnect = null;
                    return;
                }

                reconnect(context);
                pendingReconnect = null;
            }, ariaNgSettingService.getWebSocketReconnectInterval());

            ariaNgLogService.debug('[aria2WebSocketRpcService.planToReconnect] next reconnection is pending in ' + ariaNgSettingService.getWebSocketReconnectInterval() + "ms");
        }

        return {
            request: function (context) {
                if (!context) {
                    return;
                }

                var client = getSocketClient({
                    connectionSuccessCallback: context.connectionSuccessCallback,
                    connectionFailedCallback: context.connectionFailedCallback,
                    connectionReconnectingCallback: context.connectionReconnectingCallback,
                    connectionWaitingToReconnectCallback: context.connectionWaitingToReconnectCallback
                });
                var uniqueId = context.uniqueId;
                var requestBody = angular.toJson(context.requestBody);

                ariaNgLogService.debug('[aria2WebSocketRpcService.request] ' + (context && context.requestBody && context.requestBody.method ? context.requestBody.method + ' ' : '') + 'request start', context);

                var deferred = $q.defer();

                if (client.instance) {
                    sendIdStates[uniqueId] = {
                        context: context,
                        deferred: deferred
                    };

                    client.instance.send(requestBody);
                } else {
                    deferred.reject({
                        success: false,
                        context: context
                    });

                    ariaNgLogService.debug('[aria2WebSocketRpcService.request] client error', client);
                    context.errorCallback(context.id, { message: client.error });
                }

                return deferred.promise;
            },
            reconnect: function (context) {
                reconnect(context);
            },
            on: function (eventName, callback) {
                var callbacks = eventCallbacks[eventName];

                if (!angular.isArray(callbacks)) {
                    callbacks = eventCallbacks[eventName] = [];
                }

                callbacks.push(callback);
            }
        };
    }]);
}());
<|endoftext|>


<commit_before>/**
 * @file DraggableGrid component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {DragDropContext, Droppable} from 'react-beautiful-dnd';
import isArray from 'lodash/isArray';

import DraggableGridItem from '../_DraggableGridItem';
import Tip from '../Tip';
import Theme from '../Theme';

import SelectMode from '../_statics/SelectMode';

import Util from '../_vendors/Util';
import Event from '../_vendors/Event';
import Calculation from '../_vendors/Calculation';

class DraggableGrid extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            data: props.data,
            value: Calculation.getInitValue(props)
        };

    }

    listItemSelectHandler = (item, index) => {

        const {selectMode} = this.props;

        let {value} = this.state;

        if (selectMode === SelectMode.MULTI_SELECT) {

            if (!value || !isArray(value)) {
                value = [];
            }

            value.push(item);

        } else if (selectMode === SelectMode.SINGLE_SELECT) {
            value = item;
        }

        this.setState({
            value
        }, () => {
            const {onItemSelect, onChange} = this.props;
            onItemSelect && onItemSelect(item, index);
            onChange && onChange(value, index);
        });

    };

    listItemDeselectHandler = (item, index) => {

        const {selectMode} = this.props;

        if (selectMode !== SelectMode.MULTI_SELECT) {
            return;
        }

        const {valueField, displayField} = this.props;
        let {value} = this.state;

        if (!value || !isArray(value)) {
            value = [];
        } else {
            value = value.filter(valueItem => {
                return Util.getValueByValueField(valueItem, valueField, displayField)
                    != Util.getValueByValueField(item, valueField, displayField);
            });
        }

        this.setState({
            value
        }, () => {
            const {onItemDeselect, onChange} = this.props;
            onItemDeselect && onItemDeselect(item, index);
            onChange && onChange(value, index);
        });

    };

    onNodeDragEnd = result => {

        /**
         *  result: {
         *      draggableId,
         *      type,
         *      source: {
         *          droppableId,
         *          index
         *      },
         *      destination: {
         *          droppableId,
         *          index
         *      }
         *  }
         */

        if (!result || !('draggableId' in result)
            || !result.source || !('index' in result.source)
            || !result.destination || !('index' in result.destination)) {
            return;
        }

        const {data} = this.state;

        Util.reorder(data, result.source.index, result.destination.index);

        this.setState({
            data
        }, () => {
            const {onNodeDragEnd, onSequenceChange} = this.props;
            onNodeDragEnd && onNodeDragEnd(result);
            onSequenceChange && onSequenceChange(data);
        });

    };

    componentWillReceiveProps(nextProps) {

        let state;

        if (nextProps.data !== this.state.data) {
            state = state ? state : {};
            state.data = nextProps.data;
        }
        if (nextProps.value !== this.state.value) {
            state = state ? state : {};
            state.value = Calculation.getInitValue(nextProps);
        }

        if (state) {
            this.setState(state);
        }

    }

    renderGridItem = (item, index) => {

        if (!item) {
            return;
        }

        const {

                theme, itemHeight, col,

                selectTheme, selectMode, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                idField, valueField, displayField, descriptionField, disabled, isLoading, renderer, onItemClick

            } = this.props,
            {value} = this.state;

        return typeof item === 'object' ?
            <DraggableGridItem key={(idField in item && item[idField]) || index}
                               {...item}
                               index={index}
                               style={{height: itemHeight}}
                               theme={item.theme || theme}
                               col={col}
                               selectTheme={item.selectTheme || selectTheme}
                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                               data={item}
                               checked={Calculation.isItemChecked(item, value, this.props)}
                               value={Util.getValueByValueField(item, valueField, displayField)}
                               text={Util.getTextByDisplayField(item, displayField, valueField)}
                               desc={item[descriptionField] || null}
                               disabled={disabled || item.disabled}
                               isLoading={isLoading || item.isLoading}
                               selectMode={selectMode}
                               renderer={renderer}
                               onClick={e => {
                                   onItemClick && onItemClick(item, index, e);
                                   item.onClick && item.onClick(e);
                               }}
                               onSelect={() => this.listItemSelectHandler(item, index)}
                               onDeselect={() => this.listItemDeselectHandler(item, index)}/>
            :
            <DraggableGridItem key={index}
                               index={index}
                               style={{height: itemHeight}}
                               theme={item.theme || theme}
                               col={col}
                               selectTheme={item.selectTheme || selectTheme}
                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                               data={item}
                               checked={Calculation.isItemChecked(item, value, this.props)}
                               value={item}
                               text={item}
                               disabled={disabled}
                               isLoading={isLoading}
                               selectMode={selectMode}
                               renderer={renderer}
                               onClick={e => onItemClick && onItemClick(item, index, e)}
                               onSelect={() => this.listItemSelectHandler(item, index)}
                               onDeselect={() => this.listItemDeselectHandler(item, index)}/>;

    };

    render() {

        const {

                children, className, style, data, disabled,

                onNodeDragStart

            } = this.props,

            listClassName = (className ? ' ' + className : '');

        return (
            <DragDropContext onDragStart={onNodeDragStart}
                             onDragEnd={this.onNodeDragEnd}>

                <Droppable droppableId="droppable"
                           direction="horizontal">

                    {
                        dropProvided => (
                            <div ref={dropProvided.innerRef}
                                 className={'draggable-grid' + listClassName}
                                 disabled={disabled}
                                 style={style}
                                 onWheel={e => Event.wheelHandler(e, this.props)}>

                                {data && data.map((item, index) => this.renderGridItem(item, index))}

                                {dropProvided.placeholder}

                                {children}

                            </div>
                        )
                    }

                </Droppable>

            </DragDropContext>
        );
    }
}

DraggableGrid.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The theme of the grid item.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The theme of the grid item select radio or checkbox.
     */
    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The mode of listItem.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * Children passed into the GridItem.
     */
    data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the grid button.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the grid button.
         */
        style: PropTypes.object,

        /**
         * The theme of the grid button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the grid button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The grid item's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the grid button. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the grid item will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * If true,the element's ripple effect will be disabled.
         */
        disableTouchRipple: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        tip: PropTypes.string,

        /**
         * The position of tip.
         */
        tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

        /**
         * If true,the item will have center displayed ripple effect.
         */
        rippleDisplayCenter: PropTypes.bool,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a grid item touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.string, PropTypes.number, PropTypes.symbol])),

    value: PropTypes.any,

    /**
     * The id field name in data. (default: "id")
     */
    idField: PropTypes.string,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * If true, the grid will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * If true, the grid will be at loading status.
     */
    isLoading: PropTypes.bool,

    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    col: PropTypes.number,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * Callback function fired when the grid-item touch tap.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the grid-item select.
     */
    onItemSelect: PropTypes.func,

    /**
     * Callback function fired when the grid-item deselect.
     */
    onItemDeselect: PropTypes.func,

    /**
     * Callback function fired when the grid changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func,

    onNodeDragStart: PropTypes.func,
    onNodeDragEnd: PropTypes.func,

    onSequenceChange: PropTypes.func

};

DraggableGrid.defaultProps = {

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',
    disabled: false,

    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square',

    col: 3

};

export default DraggableGrid;
<commit_msg>update DraggableGrid<commit_after>/**
 * @file DraggableGrid component
 * @author liangxiaojun(liangxiaojun@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {DragDropContext, Droppable} from 'react-beautiful-dnd';
import isArray from 'lodash/isArray';

import DraggableGridItem from '../_DraggableGridItem';
import Tip from '../Tip';

import Theme from '../Theme';
import SelectMode from '../_statics/SelectMode';

import Util from '../_vendors/Util';
import Event from '../_vendors/Event';
import Calculation from '../_vendors/Calculation';
import ComponentUtil from '../_vendors/ComponentUtil';

class DraggableGrid extends Component {

    static SelectMode = SelectMode;
    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            data: props.data,
            value: Calculation.getInitValue(props)
        };

    }

    handleListItemSelect = (item, index) => {

        const {selectMode} = this.props;

        let {value} = this.state;

        if (selectMode === SelectMode.MULTI_SELECT) {

            if (!value || !isArray(value)) {
                value = [];
            }

            value.push(item);

        } else if (selectMode === SelectMode.SINGLE_SELECT) {
            value = item;
        }

        this.setState({
            value
        }, () => {
            const {onItemSelect, onChange} = this.props;
            onItemSelect && onItemSelect(item, index);
            onChange && onChange(value, index);
        });

    };

    handleListItemDeselect = (item, index) => {

        const {selectMode} = this.props;

        if (selectMode !== SelectMode.MULTI_SELECT) {
            return;
        }

        const {valueField, displayField} = this.props;
        let {value} = this.state;

        if (!value || !isArray(value)) {
            value = [];
        } else {
            value = value.filter(valueItem => {
                return Util.getValueByValueField(valueItem, valueField, displayField)
                    != Util.getValueByValueField(item, valueField, displayField);
            });
        }

        this.setState({
            value
        }, () => {
            const {onItemDeselect, onChange} = this.props;
            onItemDeselect && onItemDeselect(item, index);
            onChange && onChange(value, index);
        });

    };

    handleNodeDragEnd = result => {

        /**
         *  result: {
         *      draggableId,
         *      type,
         *      source: {
         *          droppableId,
         *          index
         *      },
         *      destination: {
         *          droppableId,
         *          index
         *      }
         *  }
         */

        if (!result || !('draggableId' in result)
            || !result.source || !('index' in result.source)
            || !result.destination || !('index' in result.destination)) {
            return;
        }

        const {data} = this.state;

        Util.reorder(data, result.source.index, result.destination.index);

        this.setState({
            data
        }, () => {
            const {handleNodeDragEnd, onSequenceChange} = this.props;
            handleNodeDragEnd && handleNodeDragEnd(result);
            onSequenceChange && onSequenceChange(data);
        });

    };

    static getDerivedStateFromProps(props, state) {
        return {
            prevProps: props,
            data: ComponentUtil.getDerivedState(props, state, 'data'),
            value: Calculation.getInitValue({
                value: ComponentUtil.getDerivedState(props, state, 'value'),
                selectMode: props.selectMode
            })
        };
    }

    /* eslint-disable complexity */
    renderGridItem = (item, index) => {

        if (!item) {
            return;
        }

        const {

                theme, itemHeight, col,

                selectTheme, selectMode, radioUncheckedIconCls, radioCheckedIconCls,
                checkboxUncheckedIconCls, checkboxCheckedIconCls, checkboxIndeterminateIconCls,

                idField, valueField, displayField, descriptionField, disabled, isLoading, renderer, onItemClick

            } = this.props,
            {value} = this.state;

        return typeof item === 'object' ?
            <DraggableGridItem key={(idField in item && item[idField]) || index}
                               {...item}
                               index={index}
                               style={{height: itemHeight}}
                               theme={item.theme || theme}
                               col={col}
                               selectTheme={item.selectTheme || selectTheme}
                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                               data={item}
                               checked={Calculation.isItemChecked(item, value, this.props)}
                               value={Util.getValueByValueField(item, valueField, displayField)}
                               text={Util.getTextByDisplayField(item, displayField, valueField)}
                               desc={item[descriptionField] || null}
                               disabled={disabled || item.disabled}
                               isLoading={isLoading || item.isLoading}
                               selectMode={selectMode}
                               renderer={renderer}
                               onClick={e => {
                                   onItemClick && onItemClick(item, index, e);
                                   item.onClick && item.onClick(e);
                               }}
                               onSelect={() => this.handleListItemSelect(item, index)}
                               onDeselect={() => this.handleListItemDeselect(item, index)}/>
            :
            <DraggableGridItem key={index}
                               index={index}
                               style={{height: itemHeight}}
                               theme={item.theme || theme}
                               col={col}
                               selectTheme={item.selectTheme || selectTheme}
                               radioUncheckedIconCls={item.radioUncheckedIconCls || radioUncheckedIconCls}
                               radioCheckedIconCls={item.radioCheckedIconCls || radioCheckedIconCls}
                               checkboxUncheckedIconCls={item.checkboxUncheckedIconCls || checkboxUncheckedIconCls}
                               checkboxCheckedIconCls={item.checkboxCheckedIconCls || checkboxCheckedIconCls}
                               checkboxIndeterminateIconCls={item.checkboxIndeterminateIconCls || checkboxIndeterminateIconCls}
                               data={item}
                               checked={Calculation.isItemChecked(item, value, this.props)}
                               value={item}
                               text={item}
                               disabled={disabled}
                               isLoading={isLoading}
                               selectMode={selectMode}
                               renderer={renderer}
                               onClick={e => onItemClick && onItemClick(item, index, e)}
                               onSelect={() => this.handleListItemSelect(item, index)}
                               onDeselect={() => this.handleListItemDeselect(item, index)}/>;

    };

    render() {

        const {

                children, className, style, data, disabled,

                onNodeDragStart

            } = this.props,

            listClassName = (className ? ' ' + className : '');

        return (
            <DragDropContext onDragStart={onNodeDragStart}
                             onDragEnd={this.handleNodeDragEnd}>

                <Droppable droppableId="droppable"
                           direction="horizontal">

                    {
                        dropProvided => (
                            <div ref={dropProvided.innerRef}
                                 className={'draggable-grid' + listClassName}
                                 disabled={disabled}
                                 style={style}
                                 onWheel={e => Event.wheelHandler(e, this.props)}>

                                {data && data.map((item, index) => this.renderGridItem(item, index))}

                                {dropProvided.placeholder}

                                {children}

                            </div>
                        )
                    }

                </Droppable>

            </DragDropContext>
        );
    }
}

DraggableGrid.propTypes = {

    children: PropTypes.any,

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The theme of the grid item.
     */
    theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The theme of the grid item select radio or checkbox.
     */
    selectTheme: PropTypes.oneOf(Util.enumerateValue(Theme)),

    /**
     * The mode of listItem.
     */
    selectMode: PropTypes.oneOf(Util.enumerateValue(SelectMode)),

    /**
     * Children passed into the GridItem.
     */
    data: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

        /**
         * The CSS class name of the grid button.
         */
        className: PropTypes.string,

        /**
         * Override the styles of the grid button.
         */
        style: PropTypes.object,

        /**
         * The theme of the grid button.
         */
        theme: PropTypes.oneOf(Util.enumerateValue(Theme)),

        /**
         * The text value of the grid button.Type can be string or number.
         */
        value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The grid item's display text. Type can be string, number or bool.
         */
        text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * The desc value of the grid button. Type can be string or number.
         */
        desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

        /**
         * If true,the grid item will be disabled.
         */
        disabled: PropTypes.bool,

        /**
         * If true,the button will be have loading effect.
         */
        isLoading: PropTypes.bool,

        /**
         * If true,the element's ripple effect will be disabled.
         */
        disableTouchRipple: PropTypes.bool,

        /**
         * Use this property to display an icon. It will display on the left.
         */
        iconCls: PropTypes.string,

        /**
         * Use this property to display an icon. It will display on the right.
         */
        rightIconCls: PropTypes.string,

        /**
         * The message of tip.
         */
        tip: PropTypes.string,

        /**
         * The position of tip.
         */
        tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

        /**
         * If true,the item will have center displayed ripple effect.
         */
        rippleDisplayCenter: PropTypes.bool,

        /**
         * You can create a complicated renderer callback instead of value and desc prop.
         */
        itemRenderer: PropTypes.func,

        /**
         * Callback function fired when a grid item touch-tapped.
         */
        onClick: PropTypes.func

    }), PropTypes.string, PropTypes.number, PropTypes.symbol])),

    value: PropTypes.any,

    /**
     * The id field name in data. (default: "id")
     */
    idField: PropTypes.string,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The description field name in data. (default: "desc")
     */
    descriptionField: PropTypes.string,

    /**
     * If true, the grid will be disabled.
     */
    disabled: PropTypes.bool,

    /**
     * If true, the grid will be at loading status.
     */
    isLoading: PropTypes.bool,

    radioUncheckedIconCls: PropTypes.string,
    radioCheckedIconCls: PropTypes.string,
    checkboxUncheckedIconCls: PropTypes.string,
    checkboxCheckedIconCls: PropTypes.string,
    checkboxIndeterminateIconCls: PropTypes.string,

    col: PropTypes.number,
    itemHeight: PropTypes.number,

    /**
     * You can create a complicated renderer callback instead of value and desc prop.
     */
    renderer: PropTypes.func,

    /**
     * Callback function fired when the grid-item touch tap.
     */
    onItemClick: PropTypes.func,

    /**
     * Callback function fired when the grid-item select.
     */
    onItemSelect: PropTypes.func,

    /**
     * Callback function fired when the grid-item deselect.
     */
    onItemDeselect: PropTypes.func,

    /**
     * Callback function fired when the grid changed.
     */
    onChange: PropTypes.func,

    /**
     * Callback function fired when wrapper wheeled.
     */
    onWheel: PropTypes.func,

    onNodeDragStart: PropTypes.func,
    handleNodeDragEnd: PropTypes.func,

    onSequenceChange: PropTypes.func

};

DraggableGrid.defaultProps = {

    theme: Theme.DEFAULT,

    selectTheme: Theme.DEFAULT,
    selectMode: SelectMode.SINGLE_SELECT,

    idField: 'id',
    valueField: 'value',
    displayField: 'text',
    descriptionField: 'desc',
    disabled: false,

    checkboxUncheckedIconCls: 'far fa-square',
    checkboxCheckedIconCls: 'fas fa-check-square',
    checkboxIndeterminateIconCls: 'fas fa-minus-square',

    col: 3

};

export default DraggableGrid;
<|endoftext|>


<commit_before>// ==UserScript==
// @name       Youtube video links inserter
// @namespace  http://gubnota.github.io/fenki_js_helpers/plugin/monkey/
// @version    0.1
// @downloadURL http://gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/main.user.js
// @description removes adds block in Vk.com
// @include        https://*.youtube.com/*
// @include        https://youtube.com/*
// @copyright  2015+, Vladislav Muravyev
// @grant          none
// @icon http://gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/yt.png
// @run-at document-end
// ==/UserScript==
(function(window, undefined){
var w = window;
    // Пример: подключение jquery.min.js
    // (function(a,b){function ci(a) ... a.jQuery=a.$=d})(w);

    // [3] не запускаем скрипт во фреймах
    // без этого условия скрипт будет запускаться несколько раз на странице с фреймами
    if (w.self != w.top) {
        return;
    }
    // [4] дополнительная проверка наряду с @include
    if (/:\/\/.*youtube\.com\//.test(w.location.href)) {
        //Ниже идёт непосредственно код скрипта
        var el = document.getElementById('youtube_download_links_window_helper_script');
        if (el === null) {
(function(f,e,n,k,i){
var n=e.createElement(n),y=e.getElementsByTagName(i)[0];
n.setAttribute('id','youtube_download_links_window_helper_script');
n.async=1;n.src=k;y.appendChild(n);

if(typeof(IFrameWindowHelper) == 'undefined'){
    n.onload = function() {ShutterPlacesWindowHelperScript.Button();}
}

})(window,document,'script','//gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/window_helper.js','head');
        }//undefined el
    }//url pattern
})(window);
<commit_msg>youtube<commit_after>// ==UserScript==
// @name       Youtube video links inserter
// @namespace  http://gubnota.github.io/fenki_js_helpers/plugin/monkey/
// @version    0.1
// @downloadURL http://gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/main.user.js
// @description adds links to download video at Youtube.com
// @include        https://*.youtube.com/*
// @include        https://youtube.com/*
// @copyright  2015+, Vladislav Muravyev
// @grant          none
// @icon http://gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/yt.png
// @run-at document-end
// ==/UserScript==
(function(window, undefined){
var w = window;
    // Пример: подключение jquery.min.js
    // (function(a,b){function ci(a) ... a.jQuery=a.$=d})(w);

    // [3] не запускаем скрипт во фреймах
    // без этого условия скрипт будет запускаться несколько раз на странице с фреймами
    if (w.self != w.top) {
        return;
    }
    // [4] дополнительная проверка наряду с @include
    if (/:\/\/.*youtube\.com\//.test(w.location.href)) {
        //Ниже идёт непосредственно код скрипта
        var el = document.getElementById('youtube_download_links_window_helper_script');
        if (el === null) {
(function(f,e,n,k,i){
var n=e.createElement(n),y=e.getElementsByTagName(i)[0];
n.setAttribute('id','youtube_download_links_window_helper_script');
n.async=1;n.src=k;y.appendChild(n);

if(typeof(IFrameWindowHelper) == 'undefined'){
    n.onload = function() {ShutterPlacesWindowHelperScript.Button();}
}

})(window,document,'script','//gubnota.github.io/fenki_js_helpers/plugin/monkey/youtube_download_links/window_helper.js','head');
        }//undefined el
    }//url pattern
})(window);
<|endoftext|>


<commit_before>/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const assert = require('assert');
const {acceptDialog} = require('./dialog');

async function setSettingsToggle(session, toggle, enabled) {
    const className = await session.getElementProperty(toggle, "className");
    const checked = className.includes("mx_ToggleSwitch_on");
    if (checked !== enabled) {
        await toggle.click();
        session.log.done();
        return true;
    } else {
        session.log.done("already set");
    }
}

async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
    const className = await session.getElementProperty(toggle, "className");
    const checked = className.includes("mx_ToggleSwitch_on");
    if (checked === shouldBeEnabled) {
        session.log.done('set as expected');
    } else {
        // other logs in the area should give more context as to what this means.
        throw new Error("settings toggle value didn't match expectation");
    }
}

async function findTabs(session) {
    /// XXX delay is needed here, possibly because the header is being rerendered
    /// click doesn't do anything otherwise
    await session.delay(1000);
    const settingsButton = await session.query(".mx_RoomHeader .mx_AccessibleButton[aria-label=Settings]");
    await settingsButton.click();

    //find tabs
    const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel");
    const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t)));
    const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))];

    return {securityTabButton};
}

async function checkRoomSettings(session, expectedSettings) {
    session.log.startGroup(`checks the room settings`);

    const {securityTabButton} = await findTabs(session);
    const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const isDirectory = generalSwitches[0];

    if (typeof expectedSettings.directory === 'boolean') {
        session.log.step(`checks directory listing is ${expectedSettings.directory}`);
        await checkSettingsToggle(session, isDirectory, expectedSettings.directory);
    }

    if (expectedSettings.alias) {
        session.log.step(`checks for local alias of ${expectedSettings.alias}`);
        const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
        await summary.click();
        const localAliases = await session.query('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
        const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a)));
        if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) {
            session.log.done("present");
        } else {
            throw new Error(`could not find local alias ${expectedSettings.alias}`);
        }
    }

    securityTabButton.click();
    await session.delay(500);
    const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const e2eEncryptionToggle = securitySwitches[0];

    if (typeof expectedSettings.encryption === "boolean") {
        session.log.step(`checks room e2e encryption is ${expectedSettings.encryption}`);
        await checkSettingsToggle(session, e2eEncryptionToggle, expectedSettings.encryption);
    }

    if (expectedSettings.visibility) {
        session.log.step(`checks visibility is ${expectedSettings.visibility}`);
        const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
        assert.equal(radios.length, 7);
        const inviteOnly = radios[0];
        const publicNoGuests = radios[1];
        const publicWithGuests = radios[2];

        let expectedRadio = null;
        if (expectedSettings.visibility === "invite_only") {
            expectedRadio = inviteOnly;
        } else if (expectedSettings.visibility === "public_no_guests") {
            expectedRadio = publicNoGuests;
        } else if (expectedSettings.visibility === "public_with_guests") {
            expectedRadio = publicWithGuests;
        } else {
            throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
        }
        if (await session.isChecked(expectedRadio)) {
            session.log.done();
        } else {
            throw new Error("room visibility is not as expected");
        }
    }

    const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
    await closeButton.click();

    session.log.endGroup();
}

async function changeRoomSettings(session, settings) {
    session.log.startGroup(`changes the room settings`);

    const {securityTabButton} = await findTabs(session);
    const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const isDirectory = generalSwitches[0];

    if (typeof settings.directory === "boolean") {
        session.log.step(`sets directory listing to ${settings.directory}`);
        await setSettingsToggle(session, isDirectory, settings.directory);
    }

    if (settings.alias) {
        session.log.step(`sets alias to ${settings.alias}`);
        const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
        await summary.click();
        const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
        await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
        const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
        await addButton.click();
        await session.delay(10); // delay to give time for the validator to run and check the alias
        session.log.done();
    }

    securityTabButton.click();
    await session.delay(500);
    const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const e2eEncryptionToggle = securitySwitches[0];

    if (typeof settings.encryption === "boolean") {
        session.log.step(`sets room e2e encryption to ${settings.encryption}`);
        const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption);
        // if enabling, accept beta warning dialog
        if (clicked && settings.encryption) {
            await acceptDialog(session, "Enable encryption?");
        }
    }

    if (settings.visibility) {
        session.log.step(`sets visibility to ${settings.visibility}`);
        const radios = await session.queryAll(".mx_RoomSettingsDialog label");
        assert.equal(radios.length, 7);
        const inviteOnly = radios[0];
        const publicNoGuests = radios[1];
        const publicWithGuests = radios[2];

        if (settings.visibility === "invite_only") {
            await inviteOnly.click();
        } else if (settings.visibility === "public_no_guests") {
            await publicNoGuests.click();
        } else if (settings.visibility === "public_with_guests") {
            await publicWithGuests.click();
        } else {
            throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
        }
        session.log.done();
    }

    const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
    await closeButton.click();

    session.log.endGroup();
}

module.exports = {checkRoomSettings, changeRoomSettings};
<commit_msg>Fix e2e tests<commit_after>/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

const assert = require('assert');
const {acceptDialog} = require('./dialog');

async function setSettingsToggle(session, toggle, enabled) {
    const className = await session.getElementProperty(toggle, "className");
    const checked = className.includes("mx_ToggleSwitch_on");
    if (checked !== enabled) {
        await toggle.click();
        session.log.done();
        return true;
    } else {
        session.log.done("already set");
    }
}

async function checkSettingsToggle(session, toggle, shouldBeEnabled) {
    const className = await session.getElementProperty(toggle, "className");
    const checked = className.includes("mx_ToggleSwitch_on");
    if (checked === shouldBeEnabled) {
        session.log.done('set as expected');
    } else {
        // other logs in the area should give more context as to what this means.
        throw new Error("settings toggle value didn't match expectation");
    }
}

async function findTabs(session) {
    /// XXX delay is needed here, possibly because the header is being rerendered
    /// click doesn't do anything otherwise
    await session.delay(1000);
    const roomSummaryButton = await session.query('.mx_RoomHeader .mx_AccessibleButton[aria-label="Room Info"]');
    await roomSummaryButton.click();
    const settingsButton = await session.query(".mx_RoomSummaryCard_icon_settings");
    await settingsButton.click();

    //find tabs
    const tabButtons = await session.queryAll(".mx_RoomSettingsDialog .mx_TabbedView_tabLabel");
    const tabLabels = await Promise.all(tabButtons.map(t => session.innerText(t)));
    const securityTabButton = tabButtons[tabLabels.findIndex(l => l.toLowerCase().includes("security"))];

    return {securityTabButton};
}

async function checkRoomSettings(session, expectedSettings) {
    session.log.startGroup(`checks the room settings`);

    const {securityTabButton} = await findTabs(session);
    const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const isDirectory = generalSwitches[0];

    if (typeof expectedSettings.directory === 'boolean') {
        session.log.step(`checks directory listing is ${expectedSettings.directory}`);
        await checkSettingsToggle(session, isDirectory, expectedSettings.directory);
    }

    if (expectedSettings.alias) {
        session.log.step(`checks for local alias of ${expectedSettings.alias}`);
        const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
        await summary.click();
        const localAliases = await session.query('.mx_RoomSettingsDialog .mx_AliasSettings .mx_EditableItem_item');
        const localAliasTexts = await Promise.all(localAliases.map(a => session.innerText(a)));
        if (localAliasTexts.find(a => a.includes(expectedSettings.alias))) {
            session.log.done("present");
        } else {
            throw new Error(`could not find local alias ${expectedSettings.alias}`);
        }
    }

    securityTabButton.click();
    await session.delay(500);
    const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const e2eEncryptionToggle = securitySwitches[0];

    if (typeof expectedSettings.encryption === "boolean") {
        session.log.step(`checks room e2e encryption is ${expectedSettings.encryption}`);
        await checkSettingsToggle(session, e2eEncryptionToggle, expectedSettings.encryption);
    }

    if (expectedSettings.visibility) {
        session.log.step(`checks visibility is ${expectedSettings.visibility}`);
        const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]");
        assert.equal(radios.length, 7);
        const inviteOnly = radios[0];
        const publicNoGuests = radios[1];
        const publicWithGuests = radios[2];

        let expectedRadio = null;
        if (expectedSettings.visibility === "invite_only") {
            expectedRadio = inviteOnly;
        } else if (expectedSettings.visibility === "public_no_guests") {
            expectedRadio = publicNoGuests;
        } else if (expectedSettings.visibility === "public_with_guests") {
            expectedRadio = publicWithGuests;
        } else {
            throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`);
        }
        if (await session.isChecked(expectedRadio)) {
            session.log.done();
        } else {
            throw new Error("room visibility is not as expected");
        }
    }

    const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
    await closeButton.click();

    session.log.endGroup();
}

async function changeRoomSettings(session, settings) {
    session.log.startGroup(`changes the room settings`);

    const {securityTabButton} = await findTabs(session);
    const generalSwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const isDirectory = generalSwitches[0];

    if (typeof settings.directory === "boolean") {
        session.log.step(`sets directory listing to ${settings.directory}`);
        await setSettingsToggle(session, isDirectory, settings.directory);
    }

    if (settings.alias) {
        session.log.step(`sets alias to ${settings.alias}`);
        const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
        await summary.click();
        const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
        await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
        const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
        await addButton.click();
        await session.delay(10); // delay to give time for the validator to run and check the alias
        session.log.done();
    }

    securityTabButton.click();
    await session.delay(500);
    const securitySwitches = await session.queryAll(".mx_RoomSettingsDialog .mx_ToggleSwitch");
    const e2eEncryptionToggle = securitySwitches[0];

    if (typeof settings.encryption === "boolean") {
        session.log.step(`sets room e2e encryption to ${settings.encryption}`);
        const clicked = await setSettingsToggle(session, e2eEncryptionToggle, settings.encryption);
        // if enabling, accept beta warning dialog
        if (clicked && settings.encryption) {
            await acceptDialog(session, "Enable encryption?");
        }
    }

    if (settings.visibility) {
        session.log.step(`sets visibility to ${settings.visibility}`);
        const radios = await session.queryAll(".mx_RoomSettingsDialog label");
        assert.equal(radios.length, 7);
        const inviteOnly = radios[0];
        const publicNoGuests = radios[1];
        const publicWithGuests = radios[2];

        if (settings.visibility === "invite_only") {
            await inviteOnly.click();
        } else if (settings.visibility === "public_no_guests") {
            await publicNoGuests.click();
        } else if (settings.visibility === "public_with_guests") {
            await publicWithGuests.click();
        } else {
            throw new Error(`unrecognized room visibility setting: ${settings.visibility}`);
        }
        session.log.done();
    }

    const closeButton = await session.query(".mx_RoomSettingsDialog .mx_Dialog_cancelButton");
    await closeButton.click();

    session.log.endGroup();
}

module.exports = {checkRoomSettings, changeRoomSettings};
<|endoftext|>


<commit_before>var _ = require('lodash');
var Boom = require('boom');
var Bluebird = require('bluebird');

var mongo;

var valid_actions = {
    put_job: 'PUT',
    get_job: 'GET',
    destroy_job: 'DELETE',
    list_jobs: 'GET',
    job_history: 'GET',
    reserve_jobs: 'POST',
    update_job: 'PUT',
};

return function (context, req, res) {
    var action = validate_method(valid_actions);
    var maxJobsPerContainer = parseInt(context.data.max_jobs_per_container, 10) || 100;
    var now = new Date();

    if (!validate_params(['JOB_COLLECTION', 'LOG_COLLECTION', 'cluster_url']))
        return;

    if (action === 'put_job') {
        // Create or update a scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        if (!validate_body(['schedule', 'token']))
            return;

        var update = {
            $set: {
                state: 'active',
                schedule: context.body.schedule,
                token: context.body.token,
                cluster_url: context.data.cluster_url,
                container: context.data.container,
                name: context.data.name,
                last_scheduled_at: now,
                next_available_at: now,
            },
            $setOnInsert: {
                last_result: null,
                run_count: 0,
                error_count: 0,
            }
        };

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var countExistingCursor = coll.find({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                });
                var countExisting = Bluebird.promisify(countExistingCursor.count, countExistingCursor)();

                var alreadyExistsCursor = coll.find({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                });
                var alreadyExists = Bluebird.promisify(alreadyExistsCursor.count, alreadyExistsCursor)();

                return Bluebird.all([countExisting, alreadyExists])
                    .then(function (counts) {
                        var sameContainerCount = counts[0];
                        var exists = !!counts[1];

                        if (!exists && sameContainerCount >= maxJobsPerContainer) {
                            throw Boom.badRequest('Unable to schedule more than '
                                + maxJobsPerContainer
                                + ' jobs per container.');
                        }

                        return coll.findOneAndUpdateAsync({
                            cluster_url: context.data.cluster_url,
                            container: context.data.container,
                            name: context.data.name,
                        }, update, {
                            returnOriginal: false,
                            upsert: true,
                        });
                    });
            })
            .get('value')
            .then(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'reserve_jobs') {
        // Reserve a set of jobs for execution

        if (!validate_params(['count', 'ttl']))
            return;

        var next_available_at = new Date(now.valueOf() + (parseInt(context.data.ttl, 10) * 1000));

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                return Bluebird.resolve(_.range(context.data.count))
                    // TODO (ggoodman): Refactor this to use reduce to avoid calling N queries if reservation fails before N
                    .map(function (n) {
                        var filter = {
                            cluster_url: context.data.cluster_url,
                            next_available_at: {
                                $lte: now
                            },
                            state: 'active',
                        };
                        var update = {
                            $set: {
                                next_available_at: next_available_at
                            }
                        };

                        return coll.findOneAndUpdateAsync(filter, update, {
                            returnOriginal: false, // Return modified
                        })
                            .get('value'); // Only pull out the value

                    })
                    // Don't prevent one failure from blocking the entire
                    // reservation (that would mean extra waiting on those that
                    // didn't fail)
                    .settle()
                    // Log errors and continue with safe fallback
                    .map(function (result) {
                        if (result.isRejected()) {
                            return null;
                        } else {
                            return result.value();
                        }
                    })
                    // Elminate error queries and queries that did not match
                    // (this is the case when there are fewer than N jobs
                    // available for running now)
                    .filter(Boolean);

            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'list_jobs') {
        // List all scheduled webtasks or only those in a container passed via query

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                };

                // Both admin and user will hit this endpoint. When admin, container can be unset
                if (context.data.container) query.container = context.data.container;

                // TODO (ggoodman): Pagination logic
                return coll.findAsync(query)
                    .then(function (cursor) {
                        return Bluebird.promisify(cursor.toArray, cursor)();
                    });
            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else if (action === 'get_job') {
        // Get a single scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                };

                return coll.findOneAsync(query, {});
            })
            .then(stripMongoId)
            .then(function (data) {
                if (!data) throw Boom.notFound();

                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else if (action === 'destroy_job') {
        // Get a single scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        var filter = {
            cluster_url: context.data.cluster_url,
            container: context.data.container,
            name: context.data.name,
        };

        var deleteJob = withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                return coll.deleteOne(filter, {w: 1});
            });

        var deleteLogs = withMongoCollection(context.data.LOG_COLLECTION)
            .then(function (coll) {
                return coll.deleteMany(filter, {w: 1});
            });

        Bluebird.all([deleteJob, deleteLogs])
            .then(function (data) {
                res.writeHead(204);
                res.end();
            })
            .catch(respondWithError);

    } else if (action === 'update_job') {
        // Update a single scheduled webtask based on criteria

        if (!validate_params(['container', 'name']))
            return;

        if (!validate_body(['criteria', 'updates']))
            return;

        if (!_.isObject(context.body.criteria)) return respondWithError(Boom.badRequest('Expecting criteria to be an object'));
        if (!_.isObject(context.body.updates)) return respondWithError(Boom.badRequest('Expecting updates to be an object'));

        var criteria = context.body.criteria;
        var updates = context.body.updates;
        var result = updates.last_result;

        var makeMongoDates = function (obj, fields) {
            _.forEach(fields, function (field) {
                if (obj[field]) obj[field] = new Date(obj[field]);
            });
        };

        makeMongoDates(criteria, ['last_scheduled_at', 'next_available_at']);
        makeMongoDates(updates, ['last_scheduled_at', 'next_available_at']);

        // This update includes a last_result, so we will add it to our log
        if (result) {
            // Fire and forget this as considered non-critical
            withMongoCollection(context.data.LOG_COLLECTION)
                .then(function (coll) {
                    var logEntry = _.extend(result, {
                        cluster_url: context.data.cluster_url,
                        container: context.data.container,
                        name: context.data.name,
                        created_at: new Date(),
                    });

                    return coll.insertOneAsync(logEntry, {w: 1});
                });
        }

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var filter = _.defaults({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                }, context.body.critera);

                var update = {
                    $set: context.body.updates,
                };

                // Track stats based on result
                if (result) {
                    if (result.type == 'success') update.$inc = { run_count: 1};
                    if (result.type == 'error') update.$inc = { error_count: 1};
                }

                return coll.findOneAndUpdateAsync(filter, update)
                    .then(stripMongoId);
            })
            .then(function (data) {
                // There may be no jobs that match criteria, resulting in
                // data being null

                if (!data) throw Boom.notFound('No job matched the supplied criteria');

                res.writeHead(200);
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'job_history') {
        // List all scheduled webtask's result history

        if (!validate_params(['container', 'name']))
            return;

        return withMongoCollection(context.data.LOG_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                };

                var cursor = coll.find(query)
                    .sort({created_at: -1});

                if (context.data.limit) cursor.limit(parseInt(context.data.limit, 10));
                if (context.data.skip) cursor.skip(parseInt(context.data.skip, 0));

                var fetchResults = Bluebird.promisify(cursor.toArray, cursor);

                // TODO (ggoodman): Pagination logic
                return fetchResults();
            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else {
        // TODO (ggoodman): Consider: https://github.com/auth0/auth0-sandbox-ext/pull/1#discussion_r32170649

        return respondWithError(Boom.methodNotAllowed('Invalid method'));
    }

    // Helper methods

    function stripMongoId (doc) {
        if (doc) {
            if (doc._id) delete doc._id;
            if (doc.last_result && doc.last_result._id) delete doc.last_result._id;
        }

        return doc;
    }

    function withMongoDb () {
        if (mongo) return mongo;

        var MongoClient = require('mongodb').MongoClient;
        var connect = Bluebird.promisify(MongoClient.connect, MongoClient);

        return connect(context.data.MONGO_URL)
            .then(function (db) {
                // Store the settled promise resolving to the db object
                mongo = Bluebird.resolve(db);

                return db;
            })
            .catch(function (err) {
                console.log("Database error:", err);
                console.trace(err);

                throw Boom.wrap(err, 'Database unreachable');
            });
    }

    function withMongoCollection (collName) {
        return withMongoDb()
            .call('collection', collName) // Get a Collection handle
            .then(Bluebird.promisifyAll) // Promisify all methods of Connection
            .catch(function (err) {
                console.log("Database error:", err);
                console.trace(err);

                throw Boom.wrap(err, 'Database collection unreachable');
            })
    }

    function validate_method (valid_actions) {
        var action = context.data.action;
        var expected_method = valid_actions[action];

        if (!action) return respondWithError(Boom.badRequest('Missing action.'));
        if (!expected_method) return respondWithError(Boom.badRequest('Invalid action.'));
        if (expected_method !== req.method) return respondWithError(Boom.methodNotAllowed('Invalid method.'));

        return action;
    }

    function validate_params(required_params) {
        for (var i in required_params) {
            if (typeof context.data[required_params[i]] !== 'string') {
                return respondWithError(Boom.badRequest('Missing query parameter ' + required_params[i] + '.'));
            }
        }
        return true;
    }

    function validate_body(required_fields) {
        for (var i in required_fields) {
            if (!context.body[required_fields[i]]) {
                return respondWithError(Boom.badRequest('Missing payload parameter ' + required_fields[i] + '.'));
            }
        }
        return true;
    }

    function respondWithError (err) {
        if (!err.isBoom) err = Boom.wrap(err);

        res.writeHead(err.output.statusCode, err.output.headers);
        res.end(JSON.stringify(err.output.payload));

        return false;
    }
}<commit_msg>WIP debug max jobs logic<commit_after>var _ = require('lodash');
var Boom = require('boom');
var Bluebird = require('bluebird');

var mongo;

var valid_actions = {
    put_job: 'PUT',
    get_job: 'GET',
    destroy_job: 'DELETE',
    list_jobs: 'GET',
    job_history: 'GET',
    reserve_jobs: 'POST',
    update_job: 'PUT',
};

return function (context, req, res) {
    var action = validate_method(valid_actions);
    var maxJobsPerContainer = parseInt(context.data.max_jobs_per_container, 10) || 100;
    var now = new Date();

    if (!validate_params(['JOB_COLLECTION', 'LOG_COLLECTION', 'MONGO_URL', 'cluster_url']))
        return;

    if (action === 'put_job') {
        // Create or update a scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        if (!validate_body(['schedule', 'token']))
            return;

        var update = {
            $set: {
                state: 'active',
                schedule: context.body.schedule,
                token: context.body.token,
                cluster_url: context.data.cluster_url,
                container: context.data.container,
                name: context.data.name,
                last_scheduled_at: now,
                next_available_at: now,
            },
            $setOnInsert: {
                last_result: null,
                run_count: 0,
                error_count: 0,
            }
        };

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var countExistingCursor = coll.find({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                });
                var countExisting = Bluebird.promisify(countExistingCursor.count, countExistingCursor)();

                var alreadyExistsCursor = coll.find({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                });
                var alreadyExists = Bluebird.promisify(alreadyExistsCursor.count, alreadyExistsCursor)();

                return Bluebird.all([countExisting, alreadyExists])
                    .then(function (counts) {
                        var sameContainerCount = counts[0];
                        var exists = !!counts[1];

                        if (!exists && sameContainerCount >= maxJobsPerContainer) {
                            throw Boom.badRequest('Unable to schedule more than '
                                + maxJobsPerContainer
                                + ' jobs per container.' + ' In container already: ' + sameContainerCount + ', exists: ' + counts[0]);
                        }

                        return coll.findOneAndUpdateAsync({
                            cluster_url: context.data.cluster_url,
                            container: context.data.container,
                            name: context.data.name,
                        }, update, {
                            returnOriginal: false,
                            upsert: true,
                        });
                    });
            })
            .get('value')
            .then(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'reserve_jobs') {
        // Reserve a set of jobs for execution

        if (!validate_params(['count', 'ttl']))
            return;

        var next_available_at = new Date(now.valueOf() + (parseInt(context.data.ttl, 10) * 1000));

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                return Bluebird.resolve(_.range(context.data.count))
                    // TODO (ggoodman): Refactor this to use reduce to avoid calling N queries if reservation fails before N
                    .map(function (n) {
                        var filter = {
                            cluster_url: context.data.cluster_url,
                            next_available_at: {
                                $lte: now
                            },
                            state: 'active',
                        };
                        var update = {
                            $set: {
                                next_available_at: next_available_at
                            }
                        };

                        return coll.findOneAndUpdateAsync(filter, update, {
                            returnOriginal: false, // Return modified
                        })
                            .get('value'); // Only pull out the value

                    })
                    // Don't prevent one failure from blocking the entire
                    // reservation (that would mean extra waiting on those that
                    // didn't fail)
                    .settle()
                    // Log errors and continue with safe fallback
                    .map(function (result) {
                        if (result.isRejected()) {
                            return null;
                        } else {
                            return result.value();
                        }
                    })
                    // Elminate error queries and queries that did not match
                    // (this is the case when there are fewer than N jobs
                    // available for running now)
                    .filter(Boolean);

            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'list_jobs') {
        // List all scheduled webtasks or only those in a container passed via query

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                };

                // Both admin and user will hit this endpoint. When admin, container can be unset
                if (context.data.container) query.container = context.data.container;

                // TODO (ggoodman): Pagination logic
                return coll.findAsync(query)
                    .then(function (cursor) {
                        return Bluebird.promisify(cursor.toArray, cursor)();
                    });
            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else if (action === 'get_job') {
        // Get a single scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                };

                return coll.findOneAsync(query, {});
            })
            .then(stripMongoId)
            .then(function (data) {
                if (!data) throw Boom.notFound();

                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else if (action === 'destroy_job') {
        // Get a single scheduled webtask

        if (!validate_params(['container', 'name']))
            return;

        var filter = {
            cluster_url: context.data.cluster_url,
            container: context.data.container,
            name: context.data.name,
        };

        var deleteJob = withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                return coll.deleteOne(filter, {w: 1});
            });

        var deleteLogs = withMongoCollection(context.data.LOG_COLLECTION)
            .then(function (coll) {
                return coll.deleteMany(filter, {w: 1});
            });

        Bluebird.all([deleteJob, deleteLogs])
            .then(function (data) {
                res.writeHead(204);
                res.end();
            })
            .catch(respondWithError);

    } else if (action === 'update_job') {
        // Update a single scheduled webtask based on criteria

        if (!validate_params(['container', 'name']))
            return;

        if (!validate_body(['criteria', 'updates']))
            return;

        if (!_.isObject(context.body.criteria)) return respondWithError(Boom.badRequest('Expecting criteria to be an object'));
        if (!_.isObject(context.body.updates)) return respondWithError(Boom.badRequest('Expecting updates to be an object'));

        var criteria = context.body.criteria;
        var updates = context.body.updates;
        var result = updates.last_result;

        var makeMongoDates = function (obj, fields) {
            _.forEach(fields, function (field) {
                if (obj[field]) obj[field] = new Date(obj[field]);
            });
        };

        makeMongoDates(criteria, ['last_scheduled_at', 'next_available_at']);
        makeMongoDates(updates, ['last_scheduled_at', 'next_available_at']);

        // This update includes a last_result, so we will add it to our log
        if (result) {
            // Fire and forget this as considered non-critical
            withMongoCollection(context.data.LOG_COLLECTION)
                .then(function (coll) {
                    var logEntry = _.extend(result, {
                        cluster_url: context.data.cluster_url,
                        container: context.data.container,
                        name: context.data.name,
                        created_at: new Date(),
                    });

                    return coll.insertOneAsync(logEntry, {w: 1});
                });
        }

        return withMongoCollection(context.data.JOB_COLLECTION)
            .then(function (coll) {
                var filter = _.defaults({
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                }, context.body.critera);

                var update = {
                    $set: context.body.updates,
                };

                // Track stats based on result
                if (result) {
                    if (result.type == 'success') update.$inc = { run_count: 1};
                    if (result.type == 'error') update.$inc = { error_count: 1};
                }

                return coll.findOneAndUpdateAsync(filter, update)
                    .then(stripMongoId);
            })
            .then(function (data) {
                // There may be no jobs that match criteria, resulting in
                // data being null

                if (!data) throw Boom.notFound('No job matched the supplied criteria');

                res.writeHead(200);
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);
    } else if (action === 'job_history') {
        // List all scheduled webtask's result history

        if (!validate_params(['container', 'name']))
            return;

        return withMongoCollection(context.data.LOG_COLLECTION)
            .then(function (coll) {
                var query = {
                    cluster_url: context.data.cluster_url,
                    container: context.data.container,
                    name: context.data.name,
                };

                var cursor = coll.find(query)
                    .sort({created_at: -1});

                if (context.data.limit) cursor.limit(parseInt(context.data.limit, 10));
                if (context.data.skip) cursor.skip(parseInt(context.data.skip, 0));

                var fetchResults = Bluebird.promisify(cursor.toArray, cursor);

                // TODO (ggoodman): Pagination logic
                return fetchResults();
            })
            .map(stripMongoId)
            .then(function (data) {
                res.writeHead(200, {'Content-Type': 'application/json'});
                res.end(JSON.stringify(data));
            })
            .catch(respondWithError);

    } else {
        // TODO (ggoodman): Consider: https://github.com/auth0/auth0-sandbox-ext/pull/1#discussion_r32170649

        return respondWithError(Boom.methodNotAllowed('Invalid method'));
    }

    // Helper methods

    function stripMongoId (doc) {
        if (doc) {
            if (doc._id) delete doc._id;
            if (doc.last_result && doc.last_result._id) delete doc.last_result._id;
        }

        return doc;
    }

    function withMongoDb () {
        if (mongo) return mongo;

        var MongoClient = require('mongodb').MongoClient;
        var connect = Bluebird.promisify(MongoClient.connect, MongoClient);

        return connect(context.data.MONGO_URL)
            .then(function (db) {
                // Store the settled promise resolving to the db object
                mongo = Bluebird.resolve(db);

                return db;
            })
            .catch(function (err) {
                console.log("Database error:", err);
                console.trace(err);

                throw Boom.wrap(err, 'Database unreachable');
            });
    }

    function withMongoCollection (collName) {
        return withMongoDb()
            .call('collection', collName) // Get a Collection handle
            .then(Bluebird.promisifyAll) // Promisify all methods of Connection
            .catch(function (err) {
                console.log("Database error:", err);
                console.trace(err);

                throw Boom.wrap(err, 'Database collection unreachable');
            })
    }

    function validate_method (valid_actions) {
        var action = context.data.action;
        var expected_method = valid_actions[action];

        if (!action) return respondWithError(Boom.badRequest('Missing action.'));
        if (!expected_method) return respondWithError(Boom.badRequest('Invalid action.'));
        if (expected_method !== req.method) return respondWithError(Boom.methodNotAllowed('Invalid method.'));

        return action;
    }

    function validate_params(required_params) {
        for (var i in required_params) {
            if (typeof context.data[required_params[i]] !== 'string') {
                return respondWithError(Boom.badRequest('Missing query parameter ' + required_params[i] + '.'));
            }
        }
        return true;
    }

    function validate_body(required_fields) {
        for (var i in required_fields) {
            if (!context.body[required_fields[i]]) {
                return respondWithError(Boom.badRequest('Missing payload parameter ' + required_fields[i] + '.'));
            }
        }
        return true;
    }

    function respondWithError (err) {
        if (!err.isBoom) err = Boom.wrap(err);

        res.writeHead(err.output.statusCode, err.output.headers);
        res.end(JSON.stringify(err.output.payload));

        return false;
    }
}<|endoftext|>