<commit_before>/**
 * Based on example code:
 * https://github.com/kayjtea/ev3-direct
 *
 * Wheel, copyright (c) 2019 - present by Arno van der Vegt
 *
 * Added performance improvements by sending out multiple messages at once with an ID
 * and waiting for their response id.
**/
const sensorModuleConstants = require('../../vm/modules/sensorModuleConstants');
const messageEncoder        = require('./messageEncoder');
const Message               = require('./Message').Message;
const constants             = require('./constants');

const MESSAGE_TIME_OUT_TIME = 25;

exports.CommandQueue = class {
    constructor(ev3, sendFunction) {
        this._sentTime        = Date.now();
        this._sending         = false;
        this._ev3             = ev3;
        this._battery         = 0;
        this._sendFunction    = sendFunction;
        this._pending         = {time: null, command: null};
        this._queue           = [];
        this._id              = 0;
        this._foundDisconnect = false;
        this._layers          = [];
        for (let i = 0; i < 4; i++) {
            this._layers.push(this.initLayer(i));
        }
        setInterval(this.execute.bind(this), 1);
    }

    initLayer(layer) {
        let result = [];
        let i      = 0;
        for (i = 0; i < 4; i++) {
            result.push({
                sending:         true,
                id:              0,
                layer:           layer,
                port:            i,
                mode:            null,
                message:         '',
                type:            null,
                response:        false,
                time:            0,
                value:           0,
                assigned:        null,
                assignedTimeout: null
            });
        }
        for (i = 0; i < 4; i++) {
            result.push({
                ready:           false,
                assigned:        null,
                assignedTimeout: null,
                value:           0,
                reset:           false,
                degrees:         0,
                resetDegrees:    0,
                startDegrees:    null,
                endDegrees:      null
            });
        }
        return result;
    }

    getMode(layer, port) {
        return this._layers[layer][port].mode;
    }

    setMode(layer, port, mode) {
        this._layers[layer][port].mode = mode;
    }

    setMotorMove(layer, port, degrees) {
        layer             = this._layers[layer];
        port              = layer[port + 4];
        port.startDegrees = port.value - port.resetDegrees;
        port.endDegrees   = degrees;
        return this;
    }

    getDefaultModeForType(type) {
        switch (type) {
            case sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH:
            case sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT:
            case sensorModuleConstants.SENSOR_TYPE_NXT_SOUND:
            case sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC:
            case sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE:
            case sensorModuleConstants.SENSOR_TYPE_TOUCH:
                return constants.MODE0;
            case sensorModuleConstants.SENSOR_TYPE_NXT_COLOR:
            case sensorModuleConstants.SENSOR_TYPE_COLOR:
                return sensorModuleConstants.COLOR_COLOR;
            case sensorModuleConstants.SENSOR_TYPE_ULTRASONIC:
                return sensorModuleConstants.ULTRASONIC_CM;
            case sensorModuleConstants.SENSOR_TYPE_GYRO:
                return sensorModuleConstants.GYRO_ANGLE;
            case sensorModuleConstants.SENSOR_TYPE_INFRARED:
                return sensorModuleConstants.IR_REMOTE;
        }
        return constants.MODE0;
    }

    getDefaultMode(layer, port) {
        let item = this._layers[layer][port];
        return this.getDefaultModeForType(item.assigned);
    }

    getLayers() {
        let layers = this._layers;
        layers.forEach((layer) => {
            for (let i = 4; i < 8; i++) {
                let port = layer[i];
                if (port.assigned !== null) {
                    if (port.endDegrees !== null) {
                        if (port.startDegrees < port.endDegrees) {
                            port.ready = (Math.abs(port.degrees - port.endDegrees) < 45) || (port.degrees >= port.endDegrees);
                        } else {
                            port.ready = (Math.abs(port.degrees - port.endDegrees) < 45) || (port.degrees <= port.endDegrees);
                        }
                    }
                }
            }
        });
        return layers;
    }

    getId() {
        this._id = (this._id + 1) & 255;
        return this._id;
    }

    getLength() {
        return this._queue.length;
    }

    getPendingCommand() {
        return this._pending.command;
    }

    getBattery() {
        return this._battery;
    }

    getAssignedPortCount() {
        let count  = 0;
        let layers = this._layers;
        const findAssignedPortCountForLayer = (layer) => {
                layer.forEach((port, index) => {
                    if (index < 4) {
                        count += (this.isValidAssignedSensor(port.assigned) ? 1 : 0);
                    } else if (index < 8) {
                        count += (this.isValidAssignedMotor(port.assigned) ? 1 : 0);
                    }
                });
            };
        for (let i = 0, j = this._ev3.getActiveLayerCount(); i < j; i++) {
            findAssignedPortCountForLayer(layers[i]);
        }
        return count;
    }

    getFloatResult(inputData) {
        let buf  = new ArrayBuffer(4);
        let view = new DataView(buf);
        view.setUint8(0, inputData[8]);
        view.setUint8(1, inputData[7]);
        view.setUint8(2, inputData[6]);
        view.setUint8(3, inputData[5]);
        let result = view.getFloat32(0);
        return isNaN(result) ? 0 : result;
    }

    getFoundDisconnect() {
        return this._foundDisconnect;
    }

    setFoundDisconnect(foundDisconnect) {
        this._foundDisconnect = foundDisconnect;
    }

    shouldChunkTranfers() {
        return false;
    }

    execute() {
        let time = Date.now();
        if (this._sending && ((this._sentTime === null) || (time > this._sentTime))) {
            return;
        }
        let pending = this._pending;
        let queue   = this._queue;
        if ((pending.command !== null) && (time > pending.time)) {
            pending.command.id   = this.getId();
            pending.command.time = time + MESSAGE_TIME_OUT_TIME;
            queue.unshift(pending.command);
            pending.command = null;
        }
        if (!queue.length || pending.command) {
            return;
        }
        this._sentTime = null;
        let command = queue.shift();
        if (command.response) {
            pending.time    = Date.now() + MESSAGE_TIME_OUT_TIME;
            pending.command = command;
            this._sending   = true;
        } else if (command.wait) {
            this._sending   = true;
        }
        this._sentTime = time + 50;
        this._sendFunction(messageEncoder.packMessageForSending(command.id, command.message.get()));
        command.callback && command.callback();
    }

    addToCommandQueue(command) {
        let index = null;
        if (('layer' in command) && ('port' in command)) {
            if (this.isValidAssignedSensor(command.type)) {
                index = command.port;
            } else if (command.type === constants.READ_FROM_MOTOR) {
                index = command.port + 4;
            }
        }
        let time = Date.now();
        if (index !== null) {
            let layer = command.layer;
            let item  = this._layers[layer][index];
            if (this._pending.command || item.sending) {
                if (item.sending && (time > item.time)) { // Timeout, send next time!
                    item.sending = false;
                }
                return;
            }
            item.wait     = true;
            item.id       = this.getId();
            item.time     = time + MESSAGE_TIME_OUT_TIME;
            item.response = command.response;
            item.mode     = command.mode;
            item.type     = command.type;
            item.message  = command.message;
            item.sending  = true;
            this._queue.push(item);
        } else {
            command.wait  = false;
            command.id    = this.getId();
            command.time  = time + MESSAGE_TIME_OUT_TIME;
            this._queue.push(command);
        }
    }

    isValidAssignedMotor(assigned) {
        return (constants.MOTORS.indexOf(assigned) !== -1);
    }

    isValidAssignedSensor(assigned) {
        return ([
            sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH,
            sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT,
            sensorModuleConstants.SENSOR_TYPE_NXT_SOUND,
            sensorModuleConstants.SENSOR_TYPE_NXT_COLOR,
            sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC,
            sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE,
            sensorModuleConstants.SENSOR_TYPE_TOUCH,
            sensorModuleConstants.SENSOR_TYPE_COLOR,
            sensorModuleConstants.SENSOR_TYPE_ULTRASONIC,
            sensorModuleConstants.SENSOR_TYPE_GYRO,
            sensorModuleConstants.SENSOR_TYPE_INFRARED
        ].indexOf(assigned) !== -1);
    }

    readFilesRecieved(inputData) {
        let files = [];
        let file  = '';
        let i     = 12;
        while (i < inputData.length) {
            let c = inputData[i++];
            if (c === 10) {
                files.push(file);
                file = '';
            } else {
                file += String.fromCharCode(c);
            }
        }
        if (file !== '') {
            files.push(file);
        }
        return files;
    }

    continueDownload(handle, filename, data) {
        let chunkSize = 65535;
        for (let i = 0; i < data.length; i += chunkSize) {
            let chunkData = data.substring(i, i + chunkSize);
            this.addToCommandQueue({
                type:    constants.SYSTEM_COMMAND,
                message: new Message()
                    .addS(constants.CONTINUE_DOWNLOAD)
                    .addS(handle)
                    .addS(chunkData)
            });
        }
    }

    receiveSensorData(inputData, id) {
        let layers           = this._layers;
        let activeLayerCount = this._ev3.getActiveLayerCount();
        let found            = false;
        let time             = Date.now();
        for (let i = 0; i < Math.min(activeLayerCount, layers.length); i++) {
            let layer = layers[i];
            for (let j = 0; j < 8; j++) {
                let item = layer[j];
                if ((item.id !== id) || !item.sending) {
                    continue;
                }
                if ((j > 3) && (time > item.time)) {
                    item.sending = false; // Message timed out, reset...
                }
                found = true;
                switch (item.type) {
                    case sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH:
                    case sensorModuleConstants.SENSOR_TYPE_TOUCH:
                        item.value = (inputData[5] === 100) ? 1 : 0;
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_NXT_COLOR:
                    case sensorModuleConstants.SENSOR_TYPE_COLOR:
                        let num = Math.floor(this.getFloatResult(inputData));
                        if (item.mode === sensorModuleConstants.COLOR_COLOR) {
                            item.value = ((num >= 0) && (num < 7)) ? num : 0;
                        } else {
                            item.value = num;
                        }
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_INFRARED:
                        if ((item.mode === sensorModuleConstants.IR_REMOTE) ||
                            (item.mode === sensorModuleConstants.IR_REMOTE_ADVANCED)) {
                            item.value = Math.min(Math.max(Math.floor(this.getFloatResult(inputData)), 0), 11);
                        } else {
                            item.value = this.getFloatResult(inputData);
                        }
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_SOUND:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE:
                    case sensorModuleConstants.SENSOR_TYPE_ULTRASONIC:
                    case sensorModuleConstants.SENSOR_TYPE_GYRO:
                        item.value = this.getFloatResult(inputData);
                        break;
                    case constants.READ_FROM_MOTOR:
                        item.value = Math.round(this.getFloatResult(inputData) * 360); // Round to nearest degree
                        if (item.reset) {
                            item.reset        = false;
                            item.resetDegrees = item.value;
                        }
                        item.degrees = item.value - item.resetDegrees;
                        break;
                }
                item.sending = false;
            }
        }
        return found;
    }

    /**
     * Process the inputData for the connected device information.
     *
     * Return true if there was an earlier connection but no connections are found now.
    **/
    receiveTypeMode(inputData) {
        const isAssigned = function(assigned) {
                // None, Port Error, Unknown, Initializing
                return [0x7E, 0x7F, 0xFF, 0x7D].indexOf(assigned) === -1;
            };
        let assignedCount = 0;
        let hadAssignment = false;
        let p             = this._layers[this._pending.command.layer || 0];
        const updateAssigned = (port, assigned, valid, i) => {
                if (valid && (port.assigned === assigned)) {
                    return;
                }
                if (valid) {
                    if (port.assignedTimeout) {
                        clearTimeout(port.assignedTimeout);
                    }
                    port.assigned = assigned;
                } else if (port.assignedTimeout) {
                    this._foundDisconnect = true;
                    if (port.assignedTimeout) {
                        clearTimeout(port.assignedTimeout);
                    }
                    port.assigned = assigned;
                } else {
                    this._foundDisconnect = true;
                    port.assignedTimeout = setTimeout(
                        () => {
                            port.assignedTimeout = null;
                            port.assigned        = assigned;
                        },
                        5000
                    );
                }
            };
        for (let i = 0; i < 4; i++) {
            let value    = inputData[5 + (i * 2)] || 0;
            let assigned = parseInt(messageEncoder.byteString(value), 16);
            if (!isAssigned(assigned)) {
                p[i].mode = null;
            }
            updateAssigned(p[i], assigned, this.isValidAssignedSensor(assigned), i);
            let j = i + 4;
            assigned = inputData[5 + (j * 2)] || 0;
            updateAssigned(p[j], assigned, this.isValidAssignedMotor(assigned));
        }
        return hadAssignment && (assignedCount === 0);
    }

    receiveHandler(data) {
        this._sending = false;
        let inputData = new Uint8Array(data);
        let id        = inputData[2];
        if (this.receiveSensorData(inputData, id)) {
            return;
        }
        if (!this._pending.command) {
            // Received Data and didn't expect it...
            return;
        }
        let type     = this._pending.command.type;
        let mode     = this._pending.command.mode;
        let callback = this._pending.command.responseCallback;
        let handle   = null;
        let result   = null;
        switch (type) {
            case constants.UIREAD_BATTERY:
                this._battery = inputData[5];
                if (callback) {
                    callback(result);
                }
                break;
            case constants.BEGIN_DOWNLOAD:
                result = inputData[6];
                handle = inputData[7];
                if (result === 0) {
                    let filename = this._pending.command.filename;
                    let data     = this._pending.command.data;
                    this.continueDownload(messageEncoder.decimalToLittleEndianHex(handle, 2), filename, data);
                    callback && callback(false);
                } else {
                    console.error('Download failed, state:' + result + ' handle: ' + handle);
                    callback && callback(true);
                }
                break;
            case constants.BEGIN_LIST_FILES:
                let files = this.readFilesRecieved(inputData);
                if (callback) {
                    callback(files);
                }
                break;
            case constants.INPUT_DEVICE_GET_TYPE_MODE:
                this.receiveTypeMode(inputData);
                break;
            case constants.SYSTEM_COMMAND:
                if (callback) {
                    callback(inputData[4] === 3);
                }
                break;
        }
        this._pending.command = null;
    }
};
<commit_msg>Fixed timeout for downloading files to EV3.<commit_after>/**
 * Based on example code:
 * https://github.com/kayjtea/ev3-direct
 *
 * Wheel, copyright (c) 2019 - present by Arno van der Vegt
 *
 * Added performance improvements by sending out multiple messages at once with an ID
 * and waiting for their response id.
**/
const sensorModuleConstants = require('../../vm/modules/sensorModuleConstants');
const messageEncoder        = require('./messageEncoder');
const Message               = require('./Message').Message;
const constants             = require('./constants');

const MESSAGE_TIME_OUT_TIME      = 25;
const MESSAGE_TIME_OUT_TIME_LONG = 500;

exports.CommandQueue = class {
    constructor(ev3, sendFunction) {
        this._sentTime        = Date.now();
        this._sending         = false;
        this._ev3             = ev3;
        this._battery         = 0;
        this._sendFunction    = sendFunction;
        this._pending         = {time: null, command: null};
        this._queue           = [];
        this._id              = 0;
        this._foundDisconnect = false;
        this._layers          = [];
        for (let i = 0; i < 4; i++) {
            this._layers.push(this.initLayer(i));
        }
        setInterval(this.execute.bind(this), 1);
    }

    initLayer(layer) {
        let result = [];
        let i      = 0;
        for (i = 0; i < 4; i++) {
            result.push({
                sending:         true,
                id:              0,
                layer:           layer,
                port:            i,
                mode:            null,
                message:         '',
                type:            null,
                response:        false,
                time:            0,
                value:           0,
                assigned:        null,
                assignedTimeout: null
            });
        }
        for (i = 0; i < 4; i++) {
            result.push({
                ready:           false,
                assigned:        null,
                assignedTimeout: null,
                value:           0,
                reset:           false,
                degrees:         0,
                resetDegrees:    0,
                startDegrees:    null,
                endDegrees:      null
            });
        }
        return result;
    }

    getMode(layer, port) {
        return this._layers[layer][port].mode;
    }

    setMode(layer, port, mode) {
        this._layers[layer][port].mode = mode;
    }

    setMotorMove(layer, port, degrees) {
        layer             = this._layers[layer];
        port              = layer[port + 4];
        port.startDegrees = port.value - port.resetDegrees;
        port.endDegrees   = degrees;
        return this;
    }

    getDefaultModeForType(type) {
        switch (type) {
            case sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH:
            case sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT:
            case sensorModuleConstants.SENSOR_TYPE_NXT_SOUND:
            case sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC:
            case sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE:
            case sensorModuleConstants.SENSOR_TYPE_TOUCH:
                return constants.MODE0;
            case sensorModuleConstants.SENSOR_TYPE_NXT_COLOR:
            case sensorModuleConstants.SENSOR_TYPE_COLOR:
                return sensorModuleConstants.COLOR_COLOR;
            case sensorModuleConstants.SENSOR_TYPE_ULTRASONIC:
                return sensorModuleConstants.ULTRASONIC_CM;
            case sensorModuleConstants.SENSOR_TYPE_GYRO:
                return sensorModuleConstants.GYRO_ANGLE;
            case sensorModuleConstants.SENSOR_TYPE_INFRARED:
                return sensorModuleConstants.IR_REMOTE;
        }
        return constants.MODE0;
    }

    getDefaultMode(layer, port) {
        let item = this._layers[layer][port];
        return this.getDefaultModeForType(item.assigned);
    }

    getLayers() {
        let layers = this._layers;
        layers.forEach((layer) => {
            for (let i = 4; i < 8; i++) {
                let port = layer[i];
                if (port.assigned !== null) {
                    if (port.endDegrees !== null) {
                        if (port.startDegrees < port.endDegrees) {
                            port.ready = (Math.abs(port.degrees - port.endDegrees) < 45) || (port.degrees >= port.endDegrees);
                        } else {
                            port.ready = (Math.abs(port.degrees - port.endDegrees) < 45) || (port.degrees <= port.endDegrees);
                        }
                    }
                }
            }
        });
        return layers;
    }

    getId() {
        this._id = (this._id + 1) & 255;
        return this._id;
    }

    getLength() {
        return this._queue.length;
    }

    getPendingCommand() {
        return this._pending.command;
    }

    getBattery() {
        return this._battery;
    }

    getAssignedPortCount() {
        let count  = 0;
        let layers = this._layers;
        const findAssignedPortCountForLayer = (layer) => {
                layer.forEach((port, index) => {
                    if (index < 4) {
                        count += (this.isValidAssignedSensor(port.assigned) ? 1 : 0);
                    } else if (index < 8) {
                        count += (this.isValidAssignedMotor(port.assigned) ? 1 : 0);
                    }
                });
            };
        for (let i = 0, j = this._ev3.getActiveLayerCount(); i < j; i++) {
            findAssignedPortCountForLayer(layers[i]);
        }
        return count;
    }

    getFloatResult(inputData) {
        let buf  = new ArrayBuffer(4);
        let view = new DataView(buf);
        view.setUint8(0, inputData[8]);
        view.setUint8(1, inputData[7]);
        view.setUint8(2, inputData[6]);
        view.setUint8(3, inputData[5]);
        let result = view.getFloat32(0);
        return isNaN(result) ? 0 : result;
    }

    getFoundDisconnect() {
        return this._foundDisconnect;
    }

    setFoundDisconnect(foundDisconnect) {
        this._foundDisconnect = foundDisconnect;
    }

    shouldChunkTranfers() {
        return false;
    }

    execute() {
        let time = Date.now();
        if (this._sending && ((this._sentTime === null) || (time > this._sentTime))) {
            return;
        }
        let pending = this._pending;
        let queue   = this._queue;
        if ((pending.command !== null) && (time > pending.time)) {
            pending.command.id   = this.getId();
            pending.command.time = time + MESSAGE_TIME_OUT_TIME;
            queue.unshift(pending.command);
            pending.command = null;
        }
        if (!queue.length || pending.command) {
            return;
        }
        this._sentTime = null;
        let command = queue.shift();
        if (command.response) {
            // This is a different timeout because it's for messages like downloading or requesting file info!
            pending.time    = Date.now() + MESSAGE_TIME_OUT_TIME_LONG;
            pending.command = command;
            this._sending   = true;
        } else if (command.wait) {
            this._sending   = true;
        }
        this._sentTime = time + 50;
        this._sendFunction(messageEncoder.packMessageForSending(command.id, command.message.get()));
        command.callback && command.callback();
    }

    addToCommandQueue(command) {
        let index = null;
        if (('layer' in command) && ('port' in command)) {
            if (this.isValidAssignedSensor(command.type)) {
                index = command.port;
            } else if (command.type === constants.READ_FROM_MOTOR) {
                index = command.port + 4;
            }
        }
        let time = Date.now();
        if (index !== null) {
            let layer = command.layer;
            let item  = this._layers[layer][index];
            if (this._pending.command || item.sending) {
                if (item.sending && (time > item.time)) { // Timeout, send next time!
                    item.sending = false;
                }
                return;
            }
            item.wait     = true;
            item.id       = this.getId();
            item.time     = time + MESSAGE_TIME_OUT_TIME;
            item.response = command.response;
            item.mode     = command.mode;
            item.type     = command.type;
            item.message  = command.message;
            item.sending  = true;
            this._queue.push(item);
        } else {
            command.wait  = false;
            command.id    = this.getId();
            command.time  = time + MESSAGE_TIME_OUT_TIME;
            this._queue.push(command);
        }
    }

    isValidAssignedMotor(assigned) {
        return (constants.MOTORS.indexOf(assigned) !== -1);
    }

    isValidAssignedSensor(assigned) {
        return ([
            sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH,
            sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT,
            sensorModuleConstants.SENSOR_TYPE_NXT_SOUND,
            sensorModuleConstants.SENSOR_TYPE_NXT_COLOR,
            sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC,
            sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE,
            sensorModuleConstants.SENSOR_TYPE_TOUCH,
            sensorModuleConstants.SENSOR_TYPE_COLOR,
            sensorModuleConstants.SENSOR_TYPE_ULTRASONIC,
            sensorModuleConstants.SENSOR_TYPE_GYRO,
            sensorModuleConstants.SENSOR_TYPE_INFRARED
        ].indexOf(assigned) !== -1);
    }

    readFilesRecieved(inputData) {
        let files = [];
        let file  = '';
        let i     = 12;
        while (i < inputData.length) {
            let c = inputData[i++];
            if (c === 10) {
                files.push(file);
                file = '';
            } else {
                file += String.fromCharCode(c);
            }
        }
        if (file !== '') {
            files.push(file);
        }
        return files;
    }

    continueDownload(handle, filename, data) {
        let chunkSize = 65535;
        for (let i = 0; i < data.length; i += chunkSize) {
            let chunkData = data.substring(i, i + chunkSize);
            this.addToCommandQueue({
                type:    constants.SYSTEM_COMMAND,
                message: new Message()
                    .addS(constants.CONTINUE_DOWNLOAD)
                    .addS(handle)
                    .addS(chunkData)
            });
        }
    }

    receiveSensorData(inputData, id) {
        let layers           = this._layers;
        let activeLayerCount = this._ev3.getActiveLayerCount();
        let found            = false;
        let time             = Date.now();
        for (let i = 0; i < Math.min(activeLayerCount, layers.length); i++) {
            let layer = layers[i];
            for (let j = 0; j < 8; j++) {
                let item = layer[j];
                if ((item.id !== id) || !item.sending) {
                    continue;
                }
                if ((j > 3) && (time > item.time)) {
                    item.sending = false; // Message timed out, reset...
                }
                found = true;
                switch (item.type) {
                    case sensorModuleConstants.SENSOR_TYPE_NXT_TOUCH:
                    case sensorModuleConstants.SENSOR_TYPE_TOUCH:
                        item.value = (inputData[5] === 100) ? 1 : 0;
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_NXT_COLOR:
                    case sensorModuleConstants.SENSOR_TYPE_COLOR:
                        let num = Math.floor(this.getFloatResult(inputData));
                        if (item.mode === sensorModuleConstants.COLOR_COLOR) {
                            item.value = ((num >= 0) && (num < 7)) ? num : 0;
                        } else {
                            item.value = num;
                        }
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_INFRARED:
                        if ((item.mode === sensorModuleConstants.IR_REMOTE) ||
                            (item.mode === sensorModuleConstants.IR_REMOTE_ADVANCED)) {
                            item.value = Math.min(Math.max(Math.floor(this.getFloatResult(inputData)), 0), 11);
                        } else {
                            item.value = this.getFloatResult(inputData);
                        }
                        break;
                    case sensorModuleConstants.SENSOR_TYPE_NXT_LIGHT:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_SOUND:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_ULTRASONIC:
                    case sensorModuleConstants.SENSOR_TYPE_NXT_TEMPERATURE:
                    case sensorModuleConstants.SENSOR_TYPE_ULTRASONIC:
                    case sensorModuleConstants.SENSOR_TYPE_GYRO:
                        item.value = this.getFloatResult(inputData);
                        break;
                    case constants.READ_FROM_MOTOR:
                        item.value = Math.round(this.getFloatResult(inputData) * 360); // Round to nearest degree
                        if (item.reset) {
                            item.reset        = false;
                            item.resetDegrees = item.value;
                        }
                        item.degrees = item.value - item.resetDegrees;
                        break;
                }
                item.sending = false;
            }
        }
        return found;
    }

    /**
     * Process the inputData for the connected device information.
     *
     * Return true if there was an earlier connection but no connections are found now.
    **/
    receiveTypeMode(inputData) {
        const isAssigned = function(assigned) {
                // None, Port Error, Unknown, Initializing
                return [0x7E, 0x7F, 0xFF, 0x7D].indexOf(assigned) === -1;
            };
        let assignedCount = 0;
        let hadAssignment = false;
        let p             = this._layers[this._pending.command.layer || 0];
        const updateAssigned = (port, assigned, valid, i) => {
                if (valid && (port.assigned === assigned)) {
                    return;
                }
                if (valid) {
                    if (port.assignedTimeout) {
                        clearTimeout(port.assignedTimeout);
                    }
                    port.assigned = assigned;
                } else if (port.assignedTimeout) {
                    this._foundDisconnect = true;
                    if (port.assignedTimeout) {
                        clearTimeout(port.assignedTimeout);
                    }
                    port.assigned = assigned;
                } else {
                    this._foundDisconnect = true;
                    port.assignedTimeout = setTimeout(
                        () => {
                            port.assignedTimeout = null;
                            port.assigned        = assigned;
                        },
                        5000
                    );
                }
            };
        for (let i = 0; i < 4; i++) {
            let value    = inputData[5 + (i * 2)] || 0;
            let assigned = parseInt(messageEncoder.byteString(value), 16);
            if (!isAssigned(assigned)) {
                p[i].mode = null;
            }
            updateAssigned(p[i], assigned, this.isValidAssignedSensor(assigned), i);
            let j = i + 4;
            assigned = inputData[5 + (j * 2)] || 0;
            updateAssigned(p[j], assigned, this.isValidAssignedMotor(assigned));
        }
        return hadAssignment && (assignedCount === 0);
    }

    receiveHandler(data) {
        this._sending = false;
        let inputData = new Uint8Array(data);
        let id        = inputData[2];
        if (this.receiveSensorData(inputData, id)) {
            return;
        }
        if (!this._pending.command) {
            // Received Data and didn't expect it...
            return;
        }
        let type     = this._pending.command.type;
        let mode     = this._pending.command.mode;
        let callback = this._pending.command.responseCallback;
        let handle   = null;
        let result   = null;
        switch (type) {
            case constants.UIREAD_BATTERY:
                this._battery = inputData[5];
                if (callback) {
                    callback(result);
                }
                break;
            case constants.BEGIN_DOWNLOAD:
                result = inputData[6];
                handle = inputData[7];
                if (result === 0) {
                    let filename = this._pending.command.filename;
                    let data     = this._pending.command.data;
                    this.continueDownload(messageEncoder.decimalToLittleEndianHex(handle, 2), filename, data);
                    callback && callback(false);
                } else {
                    console.error('Download failed, state:' + result + ' handle: ' + handle);
                    callback && callback(true);
                }
                break;
            case constants.BEGIN_LIST_FILES:
                let files = this.readFilesRecieved(inputData);
                if (callback) {
                    callback(files);
                }
                break;
            case constants.INPUT_DEVICE_GET_TYPE_MODE:
                this.receiveTypeMode(inputData);
                break;
            case constants.SYSTEM_COMMAND:
                if (callback) {
                    callback(inputData[4] === 3);
                }
                break;
        }
        this._pending.command = null;
    }
};
<|endoftext|>


<commit_before>define(['logger', 'tracker', 'underscore', 'jquery', 'backbone', 'spin', 'voc',
        'text!templates/toolbar/search.tpl',
        'data/EntityData', 'data/episode/UserData', 'view/sss/EntityView'], function(Logger, tracker, _, $, Backbone, Spinner, Voc, SearchTemplate, EntityData, UserData, EntityView){
    return Backbone.View.extend({
        searchResultSet : [],
        tagCloud: {},
        events: {
            'keypress .search input' : 'updateOnEnter',
            'click .filter .clearDatepicker' : 'clearDatepicker',
            'click .tagcloud a' : 'tagCloudTagClicked',
            'click .search a' : '_clearLabelSearchAndRestart',
            'click .results button' : 'loadNextPage'
        },
        LOG: Logger.get('SearchToolbarView'),
        initialize: function() {
            var that = this,
                promise = UserData.getTagFrequencies();
            this.loadMoreResultsSelector = '.results button';
            this.resultsSelector = '.results';
            this.resultSetSelector = '.results .resultSet';
            this.tagCloudSelector = '.tagcloud';
            this.searchInputSelector = '.search input';
            this.tagSearchSelector = '.tagSearch';

            promise.then(
                function(data) {
                    that.$el.find(that.tagSearchSelector).show();
                    that.tagCloud = data;
                    that.displayTagCloud();
                },
                function(f) {
                    that.LOG.debug('Failed to load tags cloud frequencies', f);
                }
            );
        },
        render: function() {
            var search = _.template(SearchTemplate);
            this.$el.html(search);

            this.$el.find('.filter input.datepicker').datepicker();
            this.$el.find(this.tagSearchSelector).hide();
        },
        updateOnEnter: function(e) {
            if (e.keyCode == 13) {
                this.search($(e.currentTarget).val());

                tracker.info(tracker.SEARCHWITHKEYWORD, tracker.SEARCHTAB, null, $(e.currentTarget).val());
            }
        },
        _addToResultSet: function(results) {
            var that = this;
                box = that.$el.find(this.resultSetSelector),
                combinedSearchTerms = that.searchedTags.concat(that.searchedKeywords),
                combinedSearchTermsText = combinedSearchTerms.join(', ');

            _.each(results, function(result) {
                var view = new EntityView({
                    model : result
                });
                box.append(view.render().$el);
                that.searchResultSet.push(view);

                view.toolContext = tracker.SEARCHTAB;
                view.trackerEvtContent = combinedSearchTermsText;
            });
        },
        //@unused Probably is unused
        _resetTagCloud: function() {
            this.tagCloud = {};
        },
        _clearResultSet: function() {
           _.each(this.searchResultSet, function(view) {
               view.remove();
           });
           this.searchResultSet = [];
           this.pageNumber = null;
           this.pageNumbers = null;
           this.pagesID = null;
        },
        _clearLabelSearchAndRestart: function(e) {
           e.preventDefault();
           this.$el.find('.search input').val('');
           this.$el.find(this.resultsSelector).hide();
           this._clearResultSet();
           // Trigger a search in case there were any selected tags
           if ( !_.isEmpty(this._getSelectedTags()) ) {
               this.search( this.$el.find(this.searchInputSelector).val() );
           }
        },
        //@unused
        _clearSearch: function(e) {
           e.preventDefault();
           this.$el.find('.search input').val('');
           this.$el.find(this.resultsSelector).hide();
           this.$el.find(this.tagCloudSelector).find('span.selected').removeClass('selected');
           this._clearResultSet();
        },
        _getSelectedTags: function() {
            var selectedTags = [],
                selectedTagElements = this.$el.find(this.tagCloudSelector).find('span.selected');

            if ( selectedTagElements && selectedTagElements.length > 0 ) {
                _.each(selectedTagElements, function(element) {
                    selectedTags.push($(element).find('a').data('tag'));
                });
            }

            return selectedTags;
        },
        addAjaxLoader: function(element) {
            if ( !this.spinner ) {
                this.spinner = new Spinner({
                    radius : 5,
                    length : 5,
                    width : 2
                });
            }
            var wrapper = document.createElement('div');
            wrapper.className = 'ajaxLoader';
            element.append(wrapper);
            this.spinner.spin(wrapper);
        },
        removeAjaxLoader: function(element) {
            this.spinner.stop();
            element.find('.ajaxLoader').remove();
        },
        search: function(searchString) {
            var that = this,
                keywords = [],
                tags = this._getSelectedTags();

            // Replace multiple spaces and split by space
            searchString = searchString.replace(/\s{2,}/g, '');
            keywords = searchString.split(' ');

            EntityData.search(keywords, tags, function(results, passThrough) {
                that.searchedKeywords = keywords;
                that.searchedTags = tags;

                that.$el.find(that.loadMoreResultsSelector).hide();
                that._clearResultSet();

                that.pageNumber = passThrough.pageNumber;
                that.pageNumbers = passThrough.pageNumbers;
                that.pagesID = passThrough.pagesID;

                // Show or hide results holder
                if ( !_.isEmpty(results) ) {
                    that.$el.find(that.resultsSelector).show();
                } else {
                    that.$el.find(that.resultsSelector).hide();
                }

                that._addToResultSet(results);

                if ( that.pageNumbers > that.pageNumber ) {
                    that.$el.find(that.loadMoreResultsSelector).show();
                }
            });
        },
        loadNextPage: function(e) {
            var that = this,
                box = this.$el.find(this.resultSetSelector),
                moreResultsElement = this.$el.find(this.loadMoreResultsSelector);

            moreResultsElement.hide().blur();
            this.addAjaxLoader(box);
            if ( this.pageNumber >= this.pageNumbers ) {
                this.removeAjaxLoader(box);
                return;
            }
            EntityData.loadSearchNextPage(this.pagesID, this.pageNumber + 1, function(results, passThrough) {
                that.pageNumber = passThrough.pageNumber;

                that._addToResultSet(results);
                that.removeAjaxLoader(box);
                if ( that.pageNumbers > that.pageNumber ) {
                    moreResultsElement.show();
                }
            });
        },
        clearDatepicker: function(e) {
            e.preventDefault();
            this.$el.find('.filter input.datepicker').val('');
        },
        displayTagCloud: function(data) {
            var that = this,
                tagCloudElement = this.$el.find(this.tagCloudSelector),
                fontMin = 10,
                fontMax = 14,
                frequMin = null,
                frequMax = null;

            tagCloudElement.empty();

            if ( _.isEmpty(that.tagCloud) ) {
                return;
            }

            frequMin = _.min(that.tagCloud, function(tag) { return tag.frequ; }).frequ;
            frequMax = _.max(that.tagCloud, function(tag) { return tag.frequ; }).frequ;

            _.each(that.tagCloud, function(single) {
                var tag = single.label,
                    frequ = single.frequ,
                    fontSize = (frequ === frequMin) ? fontMin : (frequ / frequMax) * (fontMax - fontMin) + fontMin,
                    tagClass = 'badge';
                tagCloudElement.append(' ' + tag + '');
            });
        },
        tagCloudTagClicked: function(e) {
            e.preventDefault();
            var currentTarget = $(e.currentTarget);

            currentTarget.parent().toggleClass('selected');
            this.search( this.$el.find(this.searchInputSelector).val() );

            tracker.info(tracker.CLICKTAG, tracker.SEARCHTAB, null, currentTarget.data('tag'));
        }
    });
});
<commit_msg>LL-1105 #resolve Added sorting functionality.<commit_after>define(['logger', 'tracker', 'underscore', 'jquery', 'backbone', 'spin', 'voc',
        'text!templates/toolbar/search.tpl',
        'data/EntityData', 'data/episode/UserData', 'view/sss/EntityView'], function(Logger, tracker, _, $, Backbone, Spinner, Voc, SearchTemplate, EntityData, UserData, EntityView){
    return Backbone.View.extend({
        searchResultSet : [],
        tagCloud: {},
        events: {
            'keypress .search input' : 'updateOnEnter',
            'click .filter .clearDatepicker' : 'clearDatepicker',
            'click .tagcloud a' : 'tagCloudTagClicked',
            'click .search a' : '_clearLabelSearchAndRestart',
            'click .results button' : 'loadNextPage'
        },
        LOG: Logger.get('SearchToolbarView'),
        initialize: function() {
            var that = this,
                promise = UserData.getTagFrequencies();
            this.loadMoreResultsSelector = '.results button';
            this.resultsSelector = '.results';
            this.resultSetSelector = '.results .resultSet';
            this.tagCloudSelector = '.tagcloud';
            this.searchInputSelector = '.search input';
            this.tagSearchSelector = '.tagSearch';

            promise.then(
                function(data) {
                    that.$el.find(that.tagSearchSelector).show();
                    that.tagCloud = data;
                    that.displayTagCloud();
                },
                function(f) {
                    that.LOG.debug('Failed to load tags cloud frequencies', f);
                }
            );
        },
        render: function() {
            var search = _.template(SearchTemplate);
            this.$el.html(search);

            this.$el.find('.filter input.datepicker').datepicker();
            this.$el.find(this.tagSearchSelector).hide();
        },
        updateOnEnter: function(e) {
            if (e.keyCode == 13) {
                this.search($(e.currentTarget).val());

                tracker.info(tracker.SEARCHWITHKEYWORD, tracker.SEARCHTAB, null, $(e.currentTarget).val());
            }
        },
        _addToResultSet: function(results) {
            var that = this;
                box = that.$el.find(this.resultSetSelector),
                combinedSearchTerms = that.searchedTags.concat(that.searchedKeywords),
                combinedSearchTermsText = combinedSearchTerms.join(', ');

            _.each(results, function(result) {
                var view = new EntityView({
                    model : result
                });
                box.append(view.render().$el);
                that.searchResultSet.push(view);

                view.toolContext = tracker.SEARCHTAB;
                view.trackerEvtContent = combinedSearchTermsText;
            });
        },
        //@unused Probably is unused
        _resetTagCloud: function() {
            this.tagCloud = {};
        },
        _sortTagCloud: function() {
          this.tagCloud = _.sortBy(this.tagCloud, function(tag) {
              return tag.label;
          });
        },
        _clearResultSet: function() {
           _.each(this.searchResultSet, function(view) {
               view.remove();
           });
           this.searchResultSet = [];
           this.pageNumber = null;
           this.pageNumbers = null;
           this.pagesID = null;
        },
        _clearLabelSearchAndRestart: function(e) {
           e.preventDefault();
           this.$el.find('.search input').val('');
           this.$el.find(this.resultsSelector).hide();
           this._clearResultSet();
           // Trigger a search in case there were any selected tags
           if ( !_.isEmpty(this._getSelectedTags()) ) {
               this.search( this.$el.find(this.searchInputSelector).val() );
           }
        },
        //@unused
        _clearSearch: function(e) {
           e.preventDefault();
           this.$el.find('.search input').val('');
           this.$el.find(this.resultsSelector).hide();
           this.$el.find(this.tagCloudSelector).find('span.selected').removeClass('selected');
           this._clearResultSet();
        },
        _getSelectedTags: function() {
            var selectedTags = [],
                selectedTagElements = this.$el.find(this.tagCloudSelector).find('span.selected');

            if ( selectedTagElements && selectedTagElements.length > 0 ) {
                _.each(selectedTagElements, function(element) {
                    selectedTags.push($(element).find('a').data('tag'));
                });
            }

            return selectedTags;
        },
        addAjaxLoader: function(element) {
            if ( !this.spinner ) {
                this.spinner = new Spinner({
                    radius : 5,
                    length : 5,
                    width : 2
                });
            }
            var wrapper = document.createElement('div');
            wrapper.className = 'ajaxLoader';
            element.append(wrapper);
            this.spinner.spin(wrapper);
        },
        removeAjaxLoader: function(element) {
            this.spinner.stop();
            element.find('.ajaxLoader').remove();
        },
        search: function(searchString) {
            var that = this,
                keywords = [],
                tags = this._getSelectedTags();

            // Replace multiple spaces and split by space
            searchString = searchString.replace(/\s{2,}/g, '');
            keywords = searchString.split(' ');

            EntityData.search(keywords, tags, function(results, passThrough) {
                that.searchedKeywords = keywords;
                that.searchedTags = tags;

                that.$el.find(that.loadMoreResultsSelector).hide();
                that._clearResultSet();

                that.pageNumber = passThrough.pageNumber;
                that.pageNumbers = passThrough.pageNumbers;
                that.pagesID = passThrough.pagesID;

                // Show or hide results holder
                if ( !_.isEmpty(results) ) {
                    that.$el.find(that.resultsSelector).show();
                } else {
                    that.$el.find(that.resultsSelector).hide();
                }

                that._addToResultSet(results);

                if ( that.pageNumbers > that.pageNumber ) {
                    that.$el.find(that.loadMoreResultsSelector).show();
                }
            });
        },
        loadNextPage: function(e) {
            var that = this,
                box = this.$el.find(this.resultSetSelector),
                moreResultsElement = this.$el.find(this.loadMoreResultsSelector);

            moreResultsElement.hide().blur();
            this.addAjaxLoader(box);
            if ( this.pageNumber >= this.pageNumbers ) {
                this.removeAjaxLoader(box);
                return;
            }
            EntityData.loadSearchNextPage(this.pagesID, this.pageNumber + 1, function(results, passThrough) {
                that.pageNumber = passThrough.pageNumber;

                that._addToResultSet(results);
                that.removeAjaxLoader(box);
                if ( that.pageNumbers > that.pageNumber ) {
                    moreResultsElement.show();
                }
            });
        },
        clearDatepicker: function(e) {
            e.preventDefault();
            this.$el.find('.filter input.datepicker').val('');
        },
        displayTagCloud: function(data) {
            var that = this,
                tagCloudElement = this.$el.find(this.tagCloudSelector),
                fontMin = 10,
                fontMax = 14,
                frequMin = null,
                frequMax = null;

            tagCloudElement.empty();

            if ( _.isEmpty(that.tagCloud) ) {
                return;
            }

            frequMin = _.min(that.tagCloud, function(tag) { return tag.frequ; }).frequ;
            frequMax = _.max(that.tagCloud, function(tag) { return tag.frequ; }).frequ;

            this._sortTagCloud();

            _.each(that.tagCloud, function(single) {
                var tag = single.label,
                    frequ = single.frequ,
                    fontSize = (frequ === frequMin) ? fontMin : (frequ / frequMax) * (fontMax - fontMin) + fontMin,
                    tagClass = 'badge';
                tagCloudElement.append(' ' + tag + '');
            });
        },
        tagCloudTagClicked: function(e) {
            e.preventDefault();
            var currentTarget = $(e.currentTarget);

            currentTarget.parent().toggleClass('selected');
            this.search( this.$el.find(this.searchInputSelector).val() );

            tracker.info(tracker.CLICKTAG, tracker.SEARCHTAB, null, currentTarget.data('tag'));
        }
    });
});
<|endoftext|>


<commit_before>'use strict';

/*
    definition: a schedule is an array of games
    public methods:
        * wins: returns int of wins
        * losses: returns int of losses
        * ties: returns int of ties
*/
Laker.Schedule = function (data) {

    var sched = [];

    sched.wins = 0;
    sched.losses = 0;
    sched.ties = 0;
    sched.goalsFor = 0;
    sched.goalsAgainst = 0;

    function validate(dtString) {

        var d = moment(dtString, "M/D/YYYY");

        return d.isValid() ? d : moment(Laker.utility.defaultDate);
    }

    function gameSort(g1, g2) {

      return g1.gamedate - g2.gamedate;
    }

    $.each(data.feed.entry, function (idx, val) {

        sched.push(new Laker.Game(validate(val.title.$t).toDate(), val.content.$t));

        switch (sched[idx].result) {

        case 'W':
            sched.wins += 1;
            break;

        case 'L':
            sched.losses += 1;
            break;

        default:
            sched.ties += 1;
            break;
        }

        if (sched[idx].played) {
          sched[idx].goalsFor += sched[idx].lscore;
          sched[idx].goalsAgainst += sched[idx].oscore;
        }

    });

    sched.sort(gameSort);

    return sched;
};
<commit_msg>Updated schedule<commit_after>'use strict';

/*
    definition: a schedule is an array of games
    public methods:
        * wins: returns int of wins
        * losses: returns int of losses
        * ties: returns int of ties
*/
Laker.Schedule = function (data) {

    var sched = [];

    sched.wins = 0;
    sched.losses = 0;
    sched.ties = 0;
    sched.goalsFor = 0;
    sched.goalsAgainst = 0;

    function validate(dtString) {

        var d = moment(dtString, "M/D/YYYY");

        return d.isValid() ? d : moment(Laker.utility.defaultDate);
    }

    function gameSort(g1, g2) {

      return g1.gamedate - g2.gamedate;
    }

    $.each(data.feed.entry, function (idx, val) {

        sched.push(new Laker.Game(validate(val.title.$t).toDate(), val.content.$t));

        switch (sched[idx].result) {

        case 'W':
            sched.wins += 1;
            break;

        case 'L':
            sched.losses += 1;
            break;

        case 'T':
            sched.ties += 1;
            break;
        }

        if (sched[idx].played) {
          sched.goalsFor += sched[idx].lscore;
          sched.goalsAgainst += sched[idx].oscore;
        }

    });

    sched.sort(gameSort);

    return sched;
};
<|endoftext|>


<commit_before>Template.lsiSearchResults.matchingLSI = function(){
	var lang = Session.get('lsiLangSearch');
	var algo = Session.get('lastAlgoSearch');
	return LSIs.find({pAiD:algo.AiD, Language:lang.Name},{sort:{When:-1}});
}

Template.lsiSearchResults.everyLSI = function(){
	var algo = Session.get('lastAlgoSearch');
	return LSIs.find({pAiD:algo.AiD},{sort:{When:-1}});
}

Template.lsiSearchResults.isntSet = function(){
	return Session.equals('lsiLangSearch',null);
}
<commit_msg>undefined langs no longer called for showAll<commit_after>Template.lsiSearchResults.matchingLSI = function(){
	var lang = Session.get('lsiLangSearch');
	var algo = Session.get('lastAlgoSearch');
	if(lang===undefined){
		return LSIs.find({pAiD:algo.AiD},{sort:{When:-1}});
	}
	return LSIs.find({pAiD:algo.AiD, Language:lang.Name},{sort:{When:-1}});
}

Template.lsiSearchResults.everyLSI = function(){
	var algo = Session.get('lastAlgoSearch');
	return LSIs.find({pAiD:algo.AiD},{sort:{When:-1}});
}

Template.lsiSearchResults.isntSet = function(){
	return Session.equals('lsiLangSearch',null);
}
<|endoftext|>


<commit_before>'use strict';
var login = require('./helpers/utils').login;
var openBlog = require('./helpers/utils').openBlog;
var randomString = require('./helpers/utils').randomString;

describe('timeline add to top and edit', function() {
    beforeEach(function(done) {login().then(done);});
    it('can add item to top of the timeline', function() {
        openBlog(0);
        var randomText = randomString(10);
        //type the random text
        element(by.css('[class="st-required st-text-block"]')).sendKeys(randomText);
        //click the publish button
        element(by.css('[ng-click="publish()"]')).click();
        browser.waitForAngular();
        //go and check the timeline
        element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
            var textElement = posts[0].element(by.css('span[medium-editable]'));
            //first element should have the new entered value
            textElement.getText().then(function(text) {
                expect(text).toEqual(randomText);
            });
        });
    });
    it('can edit an item on the timeline', function() {

        function getFirstPostText() {
            return element(by.repeater('post in postsList.posts').row(0))
                .element(by.css('.lb-post__list')).getText();
        }

        var randomText = randomString(10);
        openBlog(2);
        element(by.repeater('post in posts').row(0))
            .element(by.css('[ng-click="onEditClick(post)"]')).click();
        element(by.css('.editor .st-text-block')).clear().sendKeys(randomText);
        element(by.css('[ng-click="publish()"]')).click();
        browser.waitForAngular();
        getFirstPostText().then(function (old_text) {
                // wait a change in the post
                browser.wait(function() {
                    return getFirstPostText() !== old_text;
                });
                expect(getFirstPostText()).toBe(randomText);
            });
    });
    it('can edit an item on the timeline (quick edit mode)', function() {
        openBlog(2);
        var randomText = randomString(10);
        //go and check the timeline
        element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
            posts[0].isElementPresent(by.css('.lb-post__expander-holder')).then(function(present) {
                if (present) {
                    posts[0].element(by.css('.lb-post__expander-holder')).click();
                }
            });
            var textElement = posts[0].element(by.css('span[medium-editable]'));
            //add some text
            textElement.sendKeys(randomText);
            //click the save button
            posts[0].element(by.css('[ng-click="updateMedium()"]')).click();
            //get the new edited text
            textElement.getText().then(function(editedText) {
                //click on back to liveblog list
                element(by.css('[class="icon-th-large"]')).click();
                //open first blog
                openBlog(2);
                //go and check the timeline
                element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
                    posts[0].isElementPresent(by.css('.lb-post__expander-holder')).then(function(present) {
                        if (present) {
                            posts[0].element(by.css('.lb-post__expander-holder')).click();
                        }
                    });
                    var textElement = posts[0].element(by.css('span[medium-editable]'));
                    textElement.getText().then(function(text) {
                        //first element should have the new edited value
                        expect(text).toEqual(editedText);
                    });
                });
            });
        });
    });
    it('can unpublish your own post', function() {

        function getFirstPost(column) {
            return element(by.css('.column-' + column))
                .element(by.repeater('post in posts').row(0));
        }

        // unpublish the first post of the timeline and
        // check if it appears on the top of the draft posts list
        function unpublishAndTest() {
            getFirstPost('timeline')
                .element(by.css('.lb-post__list'))
                .getText().then(function(firstTimelinePostContent) {
                    getFirstPost('timeline')
                        .element(by.css('[ng-click="unpublishPost(post)"]')).click().then(function () {
                            expect(getFirstPost('timeline').element(by.css('.lb-post__list')).getText()).toNotBe(firstTimelinePostContent);
                            expect(getFirstPost('draft-posts').element(by.css('.lb-post__list')).getText()).toBe(firstTimelinePostContent);
                        });
                });
        }

        function publishPost() {
            var randomText = randomString(10);
            //type the random text
            element(by.css('[class="st-required st-text-block"]')).sendKeys(randomText);
            //click the publish button
            element(by.css('[ng-click="publish()"]')).click();
        }

        openBlog(2);
        publishPost();
        publishPost();
        // open draft posts panel
        element(by.css('[ng-click="toggleDraftPanel()"]')).click();
        unpublishAndTest();
        // twice to ensure the order
        unpublishAndTest();
    });
});
<commit_msg>spec/timeline_add_edit_spec.js factorized<commit_after>'use strict';
var login = require('./helpers/utils').login;
var openBlog = require('./helpers/utils').openBlog;
var randomString = require('./helpers/utils').randomString;

describe('timeline add to top and edit', function() {

    function publishPost() {
        var randomText = randomString(10);
        //type the random text
        element(by.css('[class="st-required st-text-block"]')).sendKeys(randomText);
        //click the publish button
        element(by.css('[ng-click="publish()"]')).click();
        return randomText;
    }

    beforeEach(function(done) {login().then(done);});

    it('can add item to top of the timeline', function() {
        openBlog(0);
        var randomText = publishPost();
        //go and check the timeline
        element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
            var textElement = posts[0].element(by.css('span[medium-editable]'));
            //first element should have the new entered value
            textElement.getText().then(function(text) {
                expect(text).toEqual(randomText);
            });
        });
    });

    it('can edit an item on the timeline', function() {

        function getFirstPostText() {
            return element(by.repeater('post in postsList.posts').row(0))
                .element(by.css('.lb-post__list')).getText();
        }

        var randomText = randomString(10);
        openBlog(2);
        element(by.repeater('post in posts').row(0))
            .element(by.css('[ng-click="onEditClick(post)"]')).click();
        element(by.css('.editor .st-text-block')).clear().sendKeys(randomText);
        element(by.css('[ng-click="publish()"]')).click();
        browser.waitForAngular();
        getFirstPostText().then(function (old_text) {
                // wait a change in the post
                browser.wait(function() {
                    return getFirstPostText() !== old_text;
                });
                expect(getFirstPostText()).toBe(randomText);
            });
    });

    it('can edit an item on the timeline (quick edit mode)', function() {
        openBlog(2);
        var randomText = randomString(10);
        //go and check the timeline
        element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
            posts[0].isElementPresent(by.css('.lb-post__expander-holder')).then(function(present) {
                if (present) {
                    posts[0].element(by.css('.lb-post__expander-holder')).click();
                }
            });
            var textElement = posts[0].element(by.css('span[medium-editable]'));
            //add some text
            textElement.sendKeys(randomText);
            //click the save button
            posts[0].element(by.css('[ng-click="updateMedium()"]')).click();
            //get the new edited text
            textElement.getText().then(function(editedText) {
                //click on back to liveblog list
                element(by.css('[class="icon-th-large"]')).click();
                //open first blog
                openBlog(2);
                //go and check the timeline
                element(by.css('.column-timeline')).all(by.repeater('post in posts')).then(function(posts) {
                    posts[0].isElementPresent(by.css('.lb-post__expander-holder')).then(function(present) {
                        if (present) {
                            posts[0].element(by.css('.lb-post__expander-holder')).click();
                        }
                    });
                    var textElement = posts[0].element(by.css('span[medium-editable]'));
                    textElement.getText().then(function(text) {
                        //first element should have the new edited value
                        expect(text).toEqual(editedText);
                    });
                });
            });
        });
    });

    it('can unpublish your own post', function() {

        function getFirstPost(column) {
            return element(by.css('.column-' + column))
                .element(by.repeater('post in posts').row(0));
        }

        // unpublish the first post of the timeline and
        // check if it appears on the top of the draft posts list
        function unpublishAndTest() {
            getFirstPost('timeline')
                .element(by.css('.lb-post__list'))
                .getText().then(function(firstTimelinePostContent) {
                    getFirstPost('timeline')
                        .element(by.css('[ng-click="unpublishPost(post)"]')).click().then(function () {
                            expect(getFirstPost('timeline').element(by.css('.lb-post__list')).getText()).toNotBe(firstTimelinePostContent);
                            expect(getFirstPost('draft-posts').element(by.css('.lb-post__list')).getText()).toBe(firstTimelinePostContent);
                        });
                });
        }

        openBlog(2);
        publishPost();
        publishPost();
        // open draft posts panel
        element(by.css('[ng-click="toggleDraftPanel()"]')).click();
        unpublishAndTest();
        // twice to ensure the order
        unpublishAndTest();
    });
});
<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import message from 'antd/lib/message'
import SplitPane from 'react-split-pane'
import { Prompt } from 'react-router-dom'
import fetchJson from '../utilities/fetch-json.js'
import uuid from 'uuid'
import keymaster from 'keymaster'
import QueryResultDataTable from '../common/QueryResultDataTable.js'
import SqlpadTauChart from '../common/SqlpadTauChart.js'
import QueryResultHeader from './QueryResultHeader.js'
import QueryDetailsModal from './QueryDetailsModal'
import EditorNavBar from './EditorNavBar'
import FlexTabPane from './FlexTabPane'
import SchemaSidebar from './SchemaSidebar.js'
import VisSidebar from './VisSidebar'
import SqlEditor from '../common/SqlEditor'
import sqlFormatter from 'sql-formatter'

const NEW_QUERY = {
  _id: '',
  name: '',
  tags: [],
  connectionId: '',
  queryText: '',
  chartConfiguration: {
    chartType: '',
    fields: {} // key value for chart
  }
}

class QueryEditor extends React.Component {
  state = {
    activeTabKey: 'sql',
    availableTags: [],
    cacheKey: uuid.v1(),
    isRunning: false,
    isSaving: false,
    unsavedChanges: false,
    query: Object.assign({}, NEW_QUERY),
    queryResult: undefined,
    runQueryStartTime: undefined,
    showModal: false,
    showValidation: false,
    selectedText: ''
  }

  sqlpadTauChart = undefined

  getTagOptions() {
    const { availableTags, query } = this.state
    const tagOptions = availableTags.slice()
    if (query && query.tags) {
      query.tags.forEach(t => {
        if (tagOptions.indexOf(t) === -1) {
          tagOptions.push(t)
        }
      })
    }
    return tagOptions
  }

  componentDidUpdate(prevProps) {
    const { query } = this.state
    const { connections } = this.props

    // if only 1 connection auto-select it
    if (connections.length === 1 && query && !query.connectionId) {
      query.connectionId = connections[0]._id
      this.setState({ connections, query })
    }
  }

  loadQueryFromServer = queryId => {
    fetchJson('GET', `/api/queries/${queryId}`).then(json => {
      const { error, query } = json
      if (error) {
        message.error(error)
      }
      this.setState({ query })
    })
  }

  loadTagsFromServer = () => {
    fetchJson('GET', '/api/tags').then(json => {
      const { error, tags } = json
      if (error) {
        message.error(error)
      }
      this.setState({ availableTags: tags })
    })
  }

  runQuery = () => {
    const { cacheKey, query, selectedText } = this.state
    this.setState({
      isRunning: true,
      runQueryStartTime: new Date()
    })
    const postData = {
      connectionId: query.connectionId,
      cacheKey,
      queryName: query.name,
      queryText: selectedText || query.queryText
    }
    fetchJson('POST', '/api/query-result', postData).then(json => {
      if (json.error) message.error(json.error)
      this.setState({
        isRunning: false,
        queryError: json.error,
        queryResult: json.queryResult
      })
    })
  }

  handleCloneClick = () => {
    const { config } = this.props
    const { query } = this.state
    delete query._id
    query.name = 'Copy of ' + query.name
    window.history.replaceState({}, query.name, `${config.baseUrl}/queries/new`)
    this.setState({ query, unsavedChanges: true })
  }

  formatQuery = () => {
    const { query } = this.state
    query.queryText = sqlFormatter.format(query.queryText)
    this.setState({
      query,
      unsavedChanges: true
    })
  }

  saveQuery = () => {
    const { query } = this.state
    const { config } = this.props
    if (!query.name) {
      message.error('Query name required')
      this.setState({ showValidation: true })
      return
    }
    this.setState({ isSaving: true })
    if (query._id) {
      fetchJson('PUT', `/api/queries/${query._id}`, query).then(json => {
        const { error, query } = json
        if (error) {
          message.error(error)
          this.setState({ isSaving: false })
          return
        }
        message.success('Query Saved')
        this.setState({ isSaving: false, unsavedChanges: false, query })
      })
    } else {
      fetchJson('POST', `/api/queries`, query).then(json => {
        const { error, query } = json
        if (error) {
          message.error(error)
          this.setState({ isSaving: false })
          return
        }
        window.history.replaceState(
          {},
          query.name,
          `${config.baseUrl}/queries/${query._id}`
        )
        message.success('Query Saved')
        this.setState({ isSaving: false, unsavedChanges: false, query })
      })
    }
  }

  setQueryState = (field, value) => {
    const { query } = this.state
    query[field] = value
    this.setState({ query, unsavedChanges: true })
  }

  handleChartConfigurationFieldsChange = (chartFieldId, queryResultField) => {
    const { query } = this.state
    query.chartConfiguration.fields[chartFieldId] = queryResultField
    this.setState({ query, unsavedChanges: true })
  }

  handleChartTypeChange = value => {
    const { query } = this.state
    query.chartConfiguration.chartType = value
    this.setState({ query, unsavedChanges: true })
  }

  handleConnectionChange = connectionId => {
    this.setQueryState('connectionId', connectionId)
  }

  handleModalHide = () => {
    this.setState({ showModal: false })
  }

  handleQueryNameChange = name => this.setQueryState('name', name)

  handleMoreClick = () => this.setState({ showModal: true })

  handleQueryTagsChange = values => this.setQueryState('tags', values)

  handleQueryTextChange = queryText =>
    this.setQueryState('queryText', queryText)

  handleQuerySelectionChange = selectedText => {
    this.setState({ selectedText })
  }

  handleSaveImageClick = e => {
    if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
      this.sqlpadTauChart.chart.fire('exportTo', 'png')
    }
  }

  handleTabSelect = e => {
    this.setState({ activeTabKey: e.target.value })
  }

  handleVisualizeClick = () => this.sqlpadTauChart.renderChart(true)

  hasRows = () => {
    const queryResult = this.state.queryResult
    return !!(queryResult && queryResult.rows && queryResult.rows.length)
  }

  isChartable = () => {
    const { isRunning, queryError, activeTabKey } = this.state
    const pending = isRunning || queryError
    return !pending && activeTabKey === 'vis' && this.hasRows()
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.queryId === 'new') {
      return this.setState({
        activeTabKey: 'sql',
        queryResult: undefined,
        query: Object.assign({}, NEW_QUERY),
        unsavedChanges: false
      })
    }
    this.loadQueryFromServer(nextProps.queryId)
  }

  componentDidMount() {
    const { queryId, loadConnections } = this.props

    loadConnections()
    this.loadTagsFromServer()
    if (queryId !== 'new') {
      this.loadQueryFromServer(queryId)
    }

    /*  Shortcuts
    ============================================================================== */
    // keymaster doesn't fire on input/textarea events by default
    // since we are only using command/ctrl shortcuts,
    // we want the event to fire all the time for any element
    keymaster.filter = () => true
    keymaster.unbind('ctrl+s, command+s')
    keymaster('ctrl+s, command+s', e => {
      this.saveQuery()
      e.preventDefault()
      return false
    })
    // there should only ever be 1 QueryEditor on the page,
    // but just in case there isn't unbind anything previously bound
    // rather something previously not run than something run more than once
    keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
    keymaster('ctrl+r, command+r, ctrl+e, command+e', e => {
      message.info('Shortcut changed to ctrl+return / command+return')
      e.preventDefault()
      return false
    })
    keymaster.unbind('ctrl+return, command+return')
    keymaster('ctrl+return, command+return', e => {
      this.runQuery()
      e.preventDefault()
      return false
    })
    keymaster.unbind('alt+r')
    keymaster('alt+r', e => {
      message.info('Shortcut changed to shift+return')
      e.preventDefault()
      return false
    })
    keymaster.unbind('shift+return')
    keymaster('shift+return', e => {
      this.formatQuery()
      e.preventDefault()
      return false
    })
  }

  componentWillUnmount() {
    keymaster.unbind('ctrl+return, command+return')
    keymaster.unbind('ctrl+s, command+s')
    keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
    keymaster.unbind('alt+r')
    keymaster.unbind('shift+return')
  }

  handleFormatClick = () => {
    this.formatQuery()
  }

  handleSqlPaneResize = () => {
    if (this.editor) {
      this.editor.resize()
    }
    if (this.dataTable) {
      this.dataTable.handleResize()
    }
  }

  handleVisPaneResize = () => {
    if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
      this.sqlpadTauChart.chart.resize()
    }
  }

  render() {
    const { config } = this.props
    const {
      activeTabKey,
      cacheKey,
      isRunning,
      isSaving,
      query,
      queryError,
      queryResult,
      runQueryStartTime,
      runSeconds,
      showModal,
      showValidation,
      unsavedChanges
    } = this.state

    document.title = query.name || 'New Query'

    return (

        <EditorNavBar
          activeTabKey={activeTabKey}
          isRunning={isRunning}
          isSaving={isSaving}
          onCloneClick={this.handleCloneClick}
          onMoreClick={this.handleMoreClick}
          onRunClick={this.runQuery}
          onSaveClick={this.saveQuery}
          onFormatClick={this.handleFormatClick}
          onTabSelect={this.handleTabSelect}
          query={query}
          onQueryNameChange={this.handleQueryNameChange}
          showValidation={showValidation}
          unsavedChanges={unsavedChanges}
          connectionId={query.connectionId}
          onConnectionChange={this.handleConnectionChange}
        />

          <FlexTabPane tabKey="sql" activeTabKey={activeTabKey}>
            <SplitPane
              split="vertical"
              minSize={150}
              defaultSize={280}
              maxSize={-100}
              onChange={this.handleSqlPaneResize}
            >
              <SchemaSidebar
                {...this.props}
                connectionId={query.connectionId}
              />
              <SplitPane
                split="horizontal"
                minSize={100}
                defaultSize={'60%'}
                maxSize={-100}
                onChange={this.handleSqlPaneResize}
              >
                <SqlEditor
                  config={config}
                  value={query.queryText}
                  onChange={this.handleQueryTextChange}
                  ref={ref => {
                    this.editor = ref ? ref.editor : null
                  }}
                  onSelectionChange={this.handleQuerySelectionChange}
                />

                  <QueryResultHeader
                    {...this.props}
                    cacheKey={cacheKey}
                    isRunning={isRunning}
                    queryResult={queryResult}
                    runQueryStartTime={runQueryStartTime}
                    runSeconds={runSeconds}
                  />

                    <QueryResultDataTable
                      {...this.props}
                      isRunning={isRunning}
                      queryError={queryError}
                      queryResult={queryResult}
                      ref={ref => (this.dataTable = ref)}
                    />

              </SplitPane>
            </SplitPane>
          </FlexTabPane>
          <FlexTabPane tabKey="vis" activeTabKey={activeTabKey}>
            <SplitPane
              split="vertical"
              minSize={150}
              defaultSize={280}
              maxSize={-100}
              onChange={this.handleVisPaneResize}
            >
              <VisSidebar
                isChartable={this.isChartable()}
                onChartConfigurationFieldsChange={
                  this.handleChartConfigurationFieldsChange
                }
                onChartTypeChange={this.handleChartTypeChange}
                onSaveImageClick={this.handleSaveImageClick}
                onVisualizeClick={this.handleVisualizeClick}
                query={query}
                queryResult={queryResult}
              />

                <SqlpadTauChart
                  config={this.props.config}
                  isRunning={isRunning}
                  query={query}
                  queryError={queryError}
                  queryResult={queryResult}
                  renderChart={this.isChartable()}
                  ref={ref => {
                    this.sqlpadTauChart = ref
                  }}
                />

            </SplitPane>
          </FlexTabPane>

        <QueryDetailsModal
          config={config}
          onHide={this.handleModalHide}
          onQueryTagsChange={this.handleQueryTagsChange}
          query={query}
          showModal={showModal}
          tagOptions={this.getTagOptions()}
        />
        <Prompt
          when={unsavedChanges}
          message={location => `Leave without saving?`}
        />

    )
  }
}

QueryEditor.propTypes = {
  connections: PropTypes.array.isRequired,
  loadConnections: PropTypes.func.isRequired
}

export default QueryEditor
<commit_msg>Prevent unnecessary renders / remove componentWillReceiveProps use<commit_after>import message from 'antd/lib/message'
import keymaster from 'keymaster'
import PropTypes from 'prop-types'
import React from 'react'
import { Prompt } from 'react-router-dom'
import SplitPane from 'react-split-pane'
import sqlFormatter from 'sql-formatter'
import uuid from 'uuid'
import QueryResultDataTable from '../common/QueryResultDataTable.js'
import SqlEditor from '../common/SqlEditor'
import SqlpadTauChart from '../common/SqlpadTauChart.js'
import fetchJson from '../utilities/fetch-json.js'
import EditorNavBar from './EditorNavBar'
import FlexTabPane from './FlexTabPane'
import QueryDetailsModal from './QueryDetailsModal'
import QueryResultHeader from './QueryResultHeader.js'
import SchemaSidebar from './SchemaSidebar.js'
import VisSidebar from './VisSidebar'

const NEW_QUERY = {
  _id: '',
  name: '',
  tags: [],
  connectionId: '',
  queryText: '',
  chartConfiguration: {
    chartType: '',
    fields: {} // key value for chart
  }
}

class QueryEditor extends React.Component {
  state = {
    activeTabKey: 'sql',
    availableTags: [],
    cacheKey: uuid.v1(),
    isRunning: false,
    isSaving: false,
    unsavedChanges: false,
    query: Object.assign({}, NEW_QUERY),
    queryResult: undefined,
    runQueryStartTime: undefined,
    showModal: false,
    showValidation: false,
    selectedText: ''
  }

  sqlpadTauChart = undefined

  getTagOptions() {
    const { availableTags, query } = this.state
    const tagOptions = availableTags.slice()
    if (query && query.tags) {
      query.tags.forEach(t => {
        if (tagOptions.indexOf(t) === -1) {
          tagOptions.push(t)
        }
      })
    }
    return tagOptions
  }

  componentDidUpdate(prevProps) {
    const { query } = this.state
    const { connections, queryId } = this.props

    if (queryId !== prevProps.queryId) {
      if (queryId === 'new') {
        return this.setState({
          activeTabKey: 'sql',
          queryResult: undefined,
          query: Object.assign({}, NEW_QUERY),
          unsavedChanges: false
        })
      }
      return this.loadQueryFromServer(queryId)
    }

    // if only 1 connection auto-select it
    if (connections.length === 1 && query && !query.connectionId) {
      return this.setState({
        query: Object.assign({}, query, {
          connectionId: connections[0]._id
        })
      })
    }
  }

  loadQueryFromServer = queryId => {
    fetchJson('GET', `/api/queries/${queryId}`).then(json => {
      const { error, query } = json
      if (error) {
        message.error(error)
      }
      this.setState({ query })
    })
  }

  loadTagsFromServer = () => {
    fetchJson('GET', '/api/tags').then(json => {
      const { error, tags } = json
      if (error) {
        message.error(error)
      }
      this.setState({ availableTags: tags })
    })
  }

  runQuery = () => {
    const { cacheKey, query, selectedText } = this.state
    this.setState({
      isRunning: true,
      runQueryStartTime: new Date()
    })
    const postData = {
      connectionId: query.connectionId,
      cacheKey,
      queryName: query.name,
      queryText: selectedText || query.queryText
    }
    fetchJson('POST', '/api/query-result', postData).then(json => {
      if (json.error) message.error(json.error)
      this.setState({
        isRunning: false,
        queryError: json.error,
        queryResult: json.queryResult
      })
    })
  }

  handleCloneClick = () => {
    const { config } = this.props
    const { query } = this.state
    delete query._id
    query.name = 'Copy of ' + query.name
    window.history.replaceState({}, query.name, `${config.baseUrl}/queries/new`)
    this.setState({ query, unsavedChanges: true })
  }

  formatQuery = () => {
    const { query } = this.state
    query.queryText = sqlFormatter.format(query.queryText)
    this.setState({
      query,
      unsavedChanges: true
    })
  }

  saveQuery = () => {
    const { query } = this.state
    const { config } = this.props
    if (!query.name) {
      message.error('Query name required')
      this.setState({ showValidation: true })
      return
    }
    this.setState({ isSaving: true })
    if (query._id) {
      fetchJson('PUT', `/api/queries/${query._id}`, query).then(json => {
        const { error, query } = json
        if (error) {
          message.error(error)
          this.setState({ isSaving: false })
          return
        }
        message.success('Query Saved')
        this.setState({ isSaving: false, unsavedChanges: false, query })
      })
    } else {
      fetchJson('POST', `/api/queries`, query).then(json => {
        const { error, query } = json
        if (error) {
          message.error(error)
          this.setState({ isSaving: false })
          return
        }
        window.history.replaceState(
          {},
          query.name,
          `${config.baseUrl}/queries/${query._id}`
        )
        message.success('Query Saved')
        this.setState({ isSaving: false, unsavedChanges: false, query })
      })
    }
  }

  setQueryState = (field, value) => {
    const { query } = this.state
    query[field] = value
    this.setState({ query, unsavedChanges: true })
  }

  handleChartConfigurationFieldsChange = (chartFieldId, queryResultField) => {
    const { query } = this.state
    query.chartConfiguration.fields[chartFieldId] = queryResultField
    this.setState({ query, unsavedChanges: true })
  }

  handleChartTypeChange = value => {
    const { query } = this.state
    query.chartConfiguration.chartType = value
    this.setState({ query, unsavedChanges: true })
  }

  handleConnectionChange = connectionId => {
    this.setQueryState('connectionId', connectionId)
  }

  handleModalHide = () => {
    this.setState({ showModal: false })
  }

  handleQueryNameChange = name => this.setQueryState('name', name)

  handleMoreClick = () => this.setState({ showModal: true })

  handleQueryTagsChange = values => this.setQueryState('tags', values)

  handleQueryTextChange = queryText =>
    this.setQueryState('queryText', queryText)

  handleQuerySelectionChange = selectedText => {
    this.setState({ selectedText })
  }

  handleSaveImageClick = e => {
    if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
      this.sqlpadTauChart.chart.fire('exportTo', 'png')
    }
  }

  handleTabSelect = e => {
    this.setState({ activeTabKey: e.target.value })
  }

  handleVisualizeClick = () => this.sqlpadTauChart.renderChart(true)

  hasRows = () => {
    const queryResult = this.state.queryResult
    return !!(queryResult && queryResult.rows && queryResult.rows.length)
  }

  isChartable = () => {
    const { isRunning, queryError, activeTabKey } = this.state
    const pending = isRunning || queryError
    return !pending && activeTabKey === 'vis' && this.hasRows()
  }

  componentDidMount() {
    const { queryId, loadConnections } = this.props

    loadConnections()
    this.loadTagsFromServer()
    if (queryId !== 'new') {
      this.loadQueryFromServer(queryId)
    }

    /*  Shortcuts
    ============================================================================== */
    // keymaster doesn't fire on input/textarea events by default
    // since we are only using command/ctrl shortcuts,
    // we want the event to fire all the time for any element
    keymaster.filter = () => true
    keymaster.unbind('ctrl+s, command+s')
    keymaster('ctrl+s, command+s', e => {
      this.saveQuery()
      e.preventDefault()
      return false
    })
    // there should only ever be 1 QueryEditor on the page,
    // but just in case there isn't unbind anything previously bound
    // rather something previously not run than something run more than once
    keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
    keymaster('ctrl+r, command+r, ctrl+e, command+e', e => {
      message.info('Shortcut changed to ctrl+return / command+return')
      e.preventDefault()
      return false
    })
    keymaster.unbind('ctrl+return, command+return')
    keymaster('ctrl+return, command+return', e => {
      this.runQuery()
      e.preventDefault()
      return false
    })
    keymaster.unbind('alt+r')
    keymaster('alt+r', e => {
      message.info('Shortcut changed to shift+return')
      e.preventDefault()
      return false
    })
    keymaster.unbind('shift+return')
    keymaster('shift+return', e => {
      this.formatQuery()
      e.preventDefault()
      return false
    })
  }

  componentWillUnmount() {
    keymaster.unbind('ctrl+return, command+return')
    keymaster.unbind('ctrl+s, command+s')
    keymaster.unbind('ctrl+r, command+r, ctrl+e, command+e')
    keymaster.unbind('alt+r')
    keymaster.unbind('shift+return')
  }

  handleFormatClick = () => {
    this.formatQuery()
  }

  handleSqlPaneResize = () => {
    if (this.editor) {
      this.editor.resize()
    }
    if (this.dataTable) {
      this.dataTable.handleResize()
    }
  }

  handleVisPaneResize = () => {
    if (this.sqlpadTauChart && this.sqlpadTauChart.chart) {
      this.sqlpadTauChart.chart.resize()
    }
  }

  render() {
    const { config } = this.props
    const {
      activeTabKey,
      cacheKey,
      isRunning,
      isSaving,
      query,
      queryError,
      queryResult,
      runQueryStartTime,
      runSeconds,
      showModal,
      showValidation,
      unsavedChanges
    } = this.state

    document.title = query.name || 'New Query'

    return (

        <EditorNavBar
          activeTabKey={activeTabKey}
          isRunning={isRunning}
          isSaving={isSaving}
          onCloneClick={this.handleCloneClick}
          onMoreClick={this.handleMoreClick}
          onRunClick={this.runQuery}
          onSaveClick={this.saveQuery}
          onFormatClick={this.handleFormatClick}
          onTabSelect={this.handleTabSelect}
          query={query}
          onQueryNameChange={this.handleQueryNameChange}
          showValidation={showValidation}
          unsavedChanges={unsavedChanges}
          connectionId={query.connectionId}
          onConnectionChange={this.handleConnectionChange}
        />

          <FlexTabPane tabKey="sql" activeTabKey={activeTabKey}>
            <SplitPane
              split="vertical"
              minSize={150}
              defaultSize={280}
              maxSize={-100}
              onChange={this.handleSqlPaneResize}
            >
              <SchemaSidebar
                {...this.props}
                connectionId={query.connectionId}
              />
              <SplitPane
                split="horizontal"
                minSize={100}
                defaultSize={'60%'}
                maxSize={-100}
                onChange={this.handleSqlPaneResize}
              >
                <SqlEditor
                  config={config}
                  value={query.queryText}
                  onChange={this.handleQueryTextChange}
                  ref={ref => {
                    this.editor = ref ? ref.editor : null
                  }}
                  onSelectionChange={this.handleQuerySelectionChange}
                />

                  <QueryResultHeader
                    {...this.props}
                    cacheKey={cacheKey}
                    isRunning={isRunning}
                    queryResult={queryResult}
                    runQueryStartTime={runQueryStartTime}
                    runSeconds={runSeconds}
                  />

                    <QueryResultDataTable
                      {...this.props}
                      isRunning={isRunning}
                      queryError={queryError}
                      queryResult={queryResult}
                      ref={ref => (this.dataTable = ref)}
                    />

              </SplitPane>
            </SplitPane>
          </FlexTabPane>
          <FlexTabPane tabKey="vis" activeTabKey={activeTabKey}>
            <SplitPane
              split="vertical"
              minSize={150}
              defaultSize={280}
              maxSize={-100}
              onChange={this.handleVisPaneResize}
            >
              <VisSidebar
                isChartable={this.isChartable()}
                onChartConfigurationFieldsChange={
                  this.handleChartConfigurationFieldsChange
                }
                onChartTypeChange={this.handleChartTypeChange}
                onSaveImageClick={this.handleSaveImageClick}
                onVisualizeClick={this.handleVisualizeClick}
                query={query}
                queryResult={queryResult}
              />

                <SqlpadTauChart
                  config={this.props.config}
                  isRunning={isRunning}
                  query={query}
                  queryError={queryError}
                  queryResult={queryResult}
                  renderChart={this.isChartable()}
                  ref={ref => {
                    this.sqlpadTauChart = ref
                  }}
                />

            </SplitPane>
          </FlexTabPane>

        <QueryDetailsModal
          config={config}
          onHide={this.handleModalHide}
          onQueryTagsChange={this.handleQueryTagsChange}
          query={query}
          showModal={showModal}
          tagOptions={this.getTagOptions()}
        />
        <Prompt
          when={unsavedChanges}
          message={location => `Leave without saving?`}
        />

    )
  }
}

QueryEditor.propTypes = {
  connections: PropTypes.array.isRequired,
  loadConnections: PropTypes.func.isRequired
}

export default QueryEditor
<|endoftext|>