<commit_before>/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var util = require("util");
var tsd = require("../lib/tsd-metrics-client");
var assert = require("chai").assert;
var testCommon = require("./test-common");
var request = require("request");
var JaySchema = require("jayschema");
var fs = require("fs");
var TEST_LOGFILE_NAME = "test-tsd-query.log";
if (fs.existsSync(TEST_LOGFILE_NAME)) {
    fs.unlinkSync(TEST_LOGFILE_NAME);
}

var log_schema = null;

before(function(done) {
    this.timeout(4000);
    // Get query log schema
    request({
        url: "https://raw.githubusercontent.com/ArpNetworking/metrics-client-doc/master/schema/query-log-schema-2e.json",
        json: true
    }, function(error, response, body) {
        if (!error && response.statusCode === 200) {
            log_schema = body;
        }
        done();
    })
});

var schemaValidator = new JaySchema();

var errorArr = [];
var skipErrorValidation = false;

tsd.onError(function (err) {
    testCommon.printError("Error:" + err);
    errorArr.push(err);
});

function clearErrors() {
    errorArr.length = 0;
    skipValidateSchema = true;
    skipErrorValidation = false;
}

var testSinks = [];
if (testCommon.verbose) {
    testSinks.push(tsd.Sinks.createConsoleSink());
}

function createMetrics() {
    return new tsd.TsdMetrics();
}

function validateSchema(jsonObject) {
    var schemaValidation = schemaValidator.validate(jsonObject, log_schema);
    assert.lengthOf(schemaValidation, 0, "Schema Validation failed: " + JSON.stringify(schemaValidation));
}

describe('Query log sink', function () {
    var queryLogSink = tsd.Sinks.createQueryLogSink(TEST_LOGFILE_NAME, 1000, 2);

    //let's sniff the wire
    var originalInfo = queryLogSink.logger.info;

    function sinkSniffer(replacement) {
        queryLogSink.logger.info = function (serializedEvent) {
            testCommon.print(JSON.stringify(JSON.parse(serializedEvent), null, 2));
            replacement(serializedEvent);
        }
    }

    beforeEach(function () {
        clearErrors();
        tsd.init(testSinks.concat([queryLogSink]));
    });

    afterEach(function () {
        if (!skipErrorValidation) {
            if (errorArr.length > 0) {
                this.test.error(new Error("Errors reported when non is expected.\n" + errorArr.toString()));
            }
        }
    });

    it('should output counters, timers and gauges correctly according to schema', function (done) {
        var helloCounter = Math.floor(Math.random() * 50.0);
        var worldCounter = Math.floor(Math.random() * 50.0);
        var customAnnotation = "HelloWorld";
        var gg0 = Math.floor(Math.random() * 50.0);
        var gg1 = Math.floor(Math.random() * 50.0);
        var ct0 = Date.now();

        var m = createMetrics();

        m.annotate(customAnnotation, customAnnotation);
        m.startTimer("timer1");
        m.incrementCounter("hello", helloCounter);
        m.incrementCounter("hello");
        m.resetCounter("brandNew");
        m.decrementCounter("world");
        m.resetCounter("world");
        m.decrementCounter("world", worldCounter);
        m.setGauge("gg", gg0);
        m.setGauge("gg", gg1);
        m.setTimer("customTimer", ct0, tsd.Units.MILLISECOND);

        var deserializedEvent = undefined;
        //don't run assertions within the sniffer, because the sniffer runs within the try catch block of the
        //metrics objects and within the test error listener, so assertion error would interfere with these and
        //cause global catastrophe that makes knowing what happen not so easy
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });

        setTimeout(function () {
            testCommon.print("stop timer1 after ~750ms");
            m.stopTimer("timer1");
            m.close();
            validateSchema(deserializedEvent);

            assert.property(deserializedEvent.data.annotations, "initTimestamp");
            assert.property(deserializedEvent.data.annotations, "finalTimestamp");
            assert.property(deserializedEvent.data.annotations, customAnnotation);

            assert.equal(deserializedEvent.data.counters.brandNew.values[0].value, 0,
                "resetCounter didn't create counter with value 0");
            assert.equal(deserializedEvent.data.counters.hello.values[0].value, helloCounter + 1,
                "increment counter happy case failed");
            assert.equal(deserializedEvent.data.counters.world.values[0].value, -1,
                "decrement counter happy case failed");
            assert.equal(deserializedEvent.data.counters.world.values[1].value, -worldCounter,
                "decrement additional sample failed");
            assert.equal(deserializedEvent.data.gauges.gg.values[0].value, gg0, "setting gauge failed");
            assert.equal(deserializedEvent.data.gauges.gg.values[1].value, gg1, "setting additional guage failed");
            assert.timerValue(deserializedEvent.data.timers.timer1.values[0].value, 750,
                "log.timers.timer1.values[0] >= 750");
            assert.equal(deserializedEvent.data.timers.customTimer.values[0].value, ct0,
                "setTimer failed to set the correct time duration");
            assert.equal(deserializedEvent.data.timers.customTimer.values[0].unit, "millisecond",
                "setTimer failed to set the correct time unit");
            done();
        }, 750);
    });

    it("should not output counters or gauges that are empty, but output empty timers with non stopped samples", function (done) {
        skipErrorValidation = true;
        var m = createMetrics();

        m.startTimer("TIMER1");
        m.createTimer("Timer2");
        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });
        m.close(); //serialization reports two errors

        validateSchema(deserializedEvent);
        assert.property(deserializedEvent.data.annotations, "initTimestamp");
        assert.property(deserializedEvent.data.annotations, "finalTimestamp");
        assert.notProperty(deserializedEvent.data, "counters");
        assert.notProperty(deserializedEvent.data, "gauges");
        assert.property(deserializedEvent.data, "timers");
        assert.lengthOf(deserializedEvent.data.timers.TIMER1.values, 0);
        assert.lengthOf(deserializedEvent.data.timers.Timer2.values, 0);

        assert.lengthOf(errorArr, 2, "unexpected count of errors");

        assert.include(errorArr[0].toString(), "TIMER1");
        assert.include(errorArr[1].toString(), "Timer2");

        done();
    });

    //TODO: Move to sink tests
    it('should not emit timer sample for non closed samples', function (done) {
        skipErrorValidation = true;
        var m = createMetrics();

        testCommon.print("start timer1");
        m.createTimer("timer1");
        testCommon.print("start timer1 second sample and stop it");
        m.createTimer("timer1").stop();

        testCommon.print("start timer2");
        m.createTimer("timer2");
        testCommon.print("start timer2 second sample");
        m.createTimer("timer2");

        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });

        setTimeout(function () {
            testCommon.print("close (timer 3 is not auto stopped)");
            m.close();
            assert.lengthOf(deserializedEvent.data.timers.timer1.values, 1, "non stopped timer sample was emitted");
            assert.lengthOf(deserializedEvent.data.timers.timer2.values, 0, "timer 2 was auto stopped when not expected to");

            assert.lengthOf(errorArr, 3, "unexpected count of errors. Errors dump: " + errorArr.toString());
            done();
        }, 100);
    });

    it("should not output counters, metrics or gauges if they are empty", function (done) {
        var m = createMetrics();

        testCommon.print("Do nothing");
        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });
        m.close();

        validateSchema(deserializedEvent);
        assert.property(deserializedEvent.data.annotations, "initTimestamp");
        assert.property(deserializedEvent.data.annotations, "finalTimestamp");
        assert.notProperty(deserializedEvent.data, "counters");
        assert.notProperty(deserializedEvent.data, "gauges");
        assert.notProperty(deserializedEvent.data, "timers");
        done();
    });

    it("should test correctly for metrics list and sample", function (done) {
        function TestSink() {}

        util.inherits(TestSink, tsd.Sink);

        var emittedMetricEvent;
        TestSink.prototype.record = function (metricsEvent) {
            emittedMetricEvent = metricsEvent;
        };
        tsd.init(testSinks.concat([new TestSink()]));
        var m = createMetrics();
        m.incrementCounter("test");
        m.close();

        assert.isTrue(tsd.Sink.isMetricsList(emittedMetricEvent.counters.test));
        assert.isFalse(tsd.Sink.isMetricsList(emittedMetricEvent.counters.test[0]));

        assert.isTrue(tsd.Sink.isMetricSample(emittedMetricEvent.counters.test.getValues()[0]));
        assert.isFalse(tsd.Sink.isMetricSample(emittedMetricEvent.counters.test.getValues()));
        done();
    });
});

describe('Console log sink', function () {
    var consoleLogSink = tsd.Sinks.createConsoleSink();

    function sinkSniffer(replacement) {
        consoleLogSink._console = {};
        consoleLogSink._console.log = function (serializedEvent) {
            replacement(serializedEvent);
        }
    }

    function TestSink() {
    }

    util.inherits(TestSink, tsd.Sink);

    var emittedMetricEvent;
    TestSink.prototype.record = function (metricsEvent) {
        emittedMetricEvent = metricsEvent;
    };

    beforeEach(function () {
        clearErrors();
        tsd.init(testSinks.concat([new TestSink(), consoleLogSink]));
    });

    afterEach(function () {
        if (!skipErrorValidation) {
            if (errorArr.length > 0) {
                this.test.error(new Error("Errors reported when non is expected.\n" + errorArr.toString()));
            }
        }
    });

    it('should work', function (done) {
        var helloCounter = Math.floor(Math.random() * 50.0);
        var worldCounter = Math.floor(Math.random() * 50.0);
        var customAnnotation = "HelloWorld";
        var gg0 = Math.floor(Math.random() * 50.0);
        var gg1 = Math.floor(Math.random() * 50.0);
        var ct0 = Date.now();

        var m = createMetrics();

        m.annotate(customAnnotation, customAnnotation);
        m.startTimer("timer1");
        m.incrementCounter("hello", helloCounter);
        m.incrementCounter("hello");
        m.resetCounter("brandNew");
        m.decrementCounter("world");
        m.resetCounter("world");
        m.decrementCounter("world", worldCounter);
        m.setGauge("gg", gg0);
        m.setGauge("gg", gg1);
        m.setTimer("customTimer", ct0, tsd.Units.MILLISECOND);

        var serializedEvent = undefined;

        sinkSniffer(function (data) {
            serializedEvent = data;
        });

        setTimeout(function () {
            testCommon.print("stop timer1 after ~750ms");
            m.stopTimer("timer1");
            m.close();

            assert.equal(JSON.stringify(emittedMetricEvent, function (key, value) {
                if (tsd.Sink.isMetricSample(value)) {
                    return {
                        value: value.getValue(),
                        unit: value.getUnit()
                    };
                }
                if (tsd.Sink.isMetricsList(value)) {
                    var listValue = {};
                    listValue[key] = {
                        values: value.getValues()
                    };
                    return {
                        values: value.getValues()
                    };
                }
                if (key[0] !== "_") {
                    return value;
                }
            }, 2), serializedEvent);
            done();
        }, 750);
    });
});
<commit_msg>Cache the test query schema locally in lib/<commit_after>/*
 * Copyright 2014 Groupon.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var util = require("util");
var tsd = require("../lib/tsd-metrics-client");
var assert = require("chai").assert;
var testCommon = require("./test-common");
var request = require("request");
var JaySchema = require("jayschema");
var fs = require("fs");
var path = require("path");

var SCHEMA_CACHE_PATH = path.join(__dirname, "..", "lib", "query-log-schema-2e.json");
var TEST_LOGFILE_NAME = "test-tsd-query.log";

if (fs.existsSync(TEST_LOGFILE_NAME)) {
    fs.unlinkSync(TEST_LOGFILE_NAME);
}

var log_schema = null;

before(function(done) {
    this.timeout(4000);
    // Check for cached schema locally
    fs.readFile(SCHEMA_CACHE_PATH, {encoding: 'utf8'}, function(err, data) {
        if (err == null) {
            log_schema = JSON.parse(data);
            done();
            return;
        }

        // Fetch query log schema
        request({
            url: "https://raw.githubusercontent.com/ArpNetworking/metrics-client-doc/master/schema/query-log-schema-2e.json",
            json: true
        }, function(err, response, body) {
            if (err != null) {
                throw err;
            }
            if (response.statusCode !== 200) {
                throw new Error("Failed to fetch query schema from github");
            }
            log_schema = body;
            fs.writeFile(SCHEMA_CACHE_PATH, JSON.stringify(body), function(err) {
                if (err != null) {
                    throw err;
                }
                done();
            })
        })
    })
});

var schemaValidator = new JaySchema();

var errorArr = [];
var skipErrorValidation = false;

tsd.onError(function (err) {
    testCommon.printError("Error:" + err);
    errorArr.push(err);
});

function clearErrors() {
    errorArr.length = 0;
    skipValidateSchema = true;
    skipErrorValidation = false;
}

var testSinks = [];
if (testCommon.verbose) {
    testSinks.push(tsd.Sinks.createConsoleSink());
}

function createMetrics() {
    return new tsd.TsdMetrics();
}

function validateSchema(jsonObject) {
    var schemaValidation = schemaValidator.validate(jsonObject, log_schema);
    assert.lengthOf(schemaValidation, 0, "Schema Validation failed: " + JSON.stringify(schemaValidation));
}

describe('Query log sink', function () {
    var queryLogSink = tsd.Sinks.createQueryLogSink(TEST_LOGFILE_NAME, 1000, 2);

    //let's sniff the wire
    var originalInfo = queryLogSink.logger.info;

    function sinkSniffer(replacement) {
        queryLogSink.logger.info = function (serializedEvent) {
            testCommon.print(JSON.stringify(JSON.parse(serializedEvent), null, 2));
            replacement(serializedEvent);
        }
    }

    beforeEach(function () {
        clearErrors();
        tsd.init(testSinks.concat([queryLogSink]));
    });

    afterEach(function () {
        if (!skipErrorValidation) {
            if (errorArr.length > 0) {
                this.test.error(new Error("Errors reported when non is expected.\n" + errorArr.toString()));
            }
        }
    });

    it('should output counters, timers and gauges correctly according to schema', function (done) {
        var helloCounter = Math.floor(Math.random() * 50.0);
        var worldCounter = Math.floor(Math.random() * 50.0);
        var customAnnotation = "HelloWorld";
        var gg0 = Math.floor(Math.random() * 50.0);
        var gg1 = Math.floor(Math.random() * 50.0);
        var ct0 = Date.now();

        var m = createMetrics();

        m.annotate(customAnnotation, customAnnotation);
        m.startTimer("timer1");
        m.incrementCounter("hello", helloCounter);
        m.incrementCounter("hello");
        m.resetCounter("brandNew");
        m.decrementCounter("world");
        m.resetCounter("world");
        m.decrementCounter("world", worldCounter);
        m.setGauge("gg", gg0);
        m.setGauge("gg", gg1);
        m.setTimer("customTimer", ct0, tsd.Units.MILLISECOND);

        var deserializedEvent = undefined;
        //don't run assertions within the sniffer, because the sniffer runs within the try catch block of the
        //metrics objects and within the test error listener, so assertion error would interfere with these and
        //cause global catastrophe that makes knowing what happen not so easy
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });

        setTimeout(function () {
            testCommon.print("stop timer1 after ~750ms");
            m.stopTimer("timer1");
            m.close();
            validateSchema(deserializedEvent);

            assert.property(deserializedEvent.data.annotations, "initTimestamp");
            assert.property(deserializedEvent.data.annotations, "finalTimestamp");
            assert.property(deserializedEvent.data.annotations, customAnnotation);

            assert.equal(deserializedEvent.data.counters.brandNew.values[0].value, 0,
                "resetCounter didn't create counter with value 0");
            assert.equal(deserializedEvent.data.counters.hello.values[0].value, helloCounter + 1,
                "increment counter happy case failed");
            assert.equal(deserializedEvent.data.counters.world.values[0].value, -1,
                "decrement counter happy case failed");
            assert.equal(deserializedEvent.data.counters.world.values[1].value, -worldCounter,
                "decrement additional sample failed");
            assert.equal(deserializedEvent.data.gauges.gg.values[0].value, gg0, "setting gauge failed");
            assert.equal(deserializedEvent.data.gauges.gg.values[1].value, gg1, "setting additional guage failed");
            assert.timerValue(deserializedEvent.data.timers.timer1.values[0].value, 750,
                "log.timers.timer1.values[0] >= 750");
            assert.equal(deserializedEvent.data.timers.customTimer.values[0].value, ct0,
                "setTimer failed to set the correct time duration");
            assert.equal(deserializedEvent.data.timers.customTimer.values[0].unit, "millisecond",
                "setTimer failed to set the correct time unit");
            done();
        }, 750);
    });

    it("should not output counters or gauges that are empty, but output empty timers with non stopped samples", function (done) {
        skipErrorValidation = true;
        var m = createMetrics();

        m.startTimer("TIMER1");
        m.createTimer("Timer2");
        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });
        m.close(); //serialization reports two errors

        validateSchema(deserializedEvent);
        assert.property(deserializedEvent.data.annotations, "initTimestamp");
        assert.property(deserializedEvent.data.annotations, "finalTimestamp");
        assert.notProperty(deserializedEvent.data, "counters");
        assert.notProperty(deserializedEvent.data, "gauges");
        assert.property(deserializedEvent.data, "timers");
        assert.lengthOf(deserializedEvent.data.timers.TIMER1.values, 0);
        assert.lengthOf(deserializedEvent.data.timers.Timer2.values, 0);

        assert.lengthOf(errorArr, 2, "unexpected count of errors");

        assert.include(errorArr[0].toString(), "TIMER1");
        assert.include(errorArr[1].toString(), "Timer2");

        done();
    });

    //TODO: Move to sink tests
    it('should not emit timer sample for non closed samples', function (done) {
        skipErrorValidation = true;
        var m = createMetrics();

        testCommon.print("start timer1");
        m.createTimer("timer1");
        testCommon.print("start timer1 second sample and stop it");
        m.createTimer("timer1").stop();

        testCommon.print("start timer2");
        m.createTimer("timer2");
        testCommon.print("start timer2 second sample");
        m.createTimer("timer2");

        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });

        setTimeout(function () {
            testCommon.print("close (timer 3 is not auto stopped)");
            m.close();
            assert.lengthOf(deserializedEvent.data.timers.timer1.values, 1, "non stopped timer sample was emitted");
            assert.lengthOf(deserializedEvent.data.timers.timer2.values, 0, "timer 2 was auto stopped when not expected to");

            assert.lengthOf(errorArr, 3, "unexpected count of errors. Errors dump: " + errorArr.toString());
            done();
        }, 100);
    });

    it("should not output counters, metrics or gauges if they are empty", function (done) {
        var m = createMetrics();

        testCommon.print("Do nothing");
        var deserializedEvent = undefined;
        sinkSniffer(function (serializedEvent) {
            deserializedEvent = JSON.parse(serializedEvent);
        });
        m.close();

        validateSchema(deserializedEvent);
        assert.property(deserializedEvent.data.annotations, "initTimestamp");
        assert.property(deserializedEvent.data.annotations, "finalTimestamp");
        assert.notProperty(deserializedEvent.data, "counters");
        assert.notProperty(deserializedEvent.data, "gauges");
        assert.notProperty(deserializedEvent.data, "timers");
        done();
    });

    it("should test correctly for metrics list and sample", function (done) {
        function TestSink() {}

        util.inherits(TestSink, tsd.Sink);

        var emittedMetricEvent;
        TestSink.prototype.record = function (metricsEvent) {
            emittedMetricEvent = metricsEvent;
        };
        tsd.init(testSinks.concat([new TestSink()]));
        var m = createMetrics();
        m.incrementCounter("test");
        m.close();

        assert.isTrue(tsd.Sink.isMetricsList(emittedMetricEvent.counters.test));
        assert.isFalse(tsd.Sink.isMetricsList(emittedMetricEvent.counters.test[0]));

        assert.isTrue(tsd.Sink.isMetricSample(emittedMetricEvent.counters.test.getValues()[0]));
        assert.isFalse(tsd.Sink.isMetricSample(emittedMetricEvent.counters.test.getValues()));
        done();
    });
});

describe('Console log sink', function () {
    var consoleLogSink = tsd.Sinks.createConsoleSink();

    function sinkSniffer(replacement) {
        consoleLogSink._console = {};
        consoleLogSink._console.log = function (serializedEvent) {
            replacement(serializedEvent);
        }
    }

    function TestSink() {
    }

    util.inherits(TestSink, tsd.Sink);

    var emittedMetricEvent;
    TestSink.prototype.record = function (metricsEvent) {
        emittedMetricEvent = metricsEvent;
    };

    beforeEach(function () {
        clearErrors();
        tsd.init(testSinks.concat([new TestSink(), consoleLogSink]));
    });

    afterEach(function () {
        if (!skipErrorValidation) {
            if (errorArr.length > 0) {
                this.test.error(new Error("Errors reported when non is expected.\n" + errorArr.toString()));
            }
        }
    });

    it('should work', function (done) {
        var helloCounter = Math.floor(Math.random() * 50.0);
        var worldCounter = Math.floor(Math.random() * 50.0);
        var customAnnotation = "HelloWorld";
        var gg0 = Math.floor(Math.random() * 50.0);
        var gg1 = Math.floor(Math.random() * 50.0);
        var ct0 = Date.now();

        var m = createMetrics();

        m.annotate(customAnnotation, customAnnotation);
        m.startTimer("timer1");
        m.incrementCounter("hello", helloCounter);
        m.incrementCounter("hello");
        m.resetCounter("brandNew");
        m.decrementCounter("world");
        m.resetCounter("world");
        m.decrementCounter("world", worldCounter);
        m.setGauge("gg", gg0);
        m.setGauge("gg", gg1);
        m.setTimer("customTimer", ct0, tsd.Units.MILLISECOND);

        var serializedEvent = undefined;

        sinkSniffer(function (data) {
            serializedEvent = data;
        });

        setTimeout(function () {
            testCommon.print("stop timer1 after ~750ms");
            m.stopTimer("timer1");
            m.close();

            assert.equal(JSON.stringify(emittedMetricEvent, function (key, value) {
                if (tsd.Sink.isMetricSample(value)) {
                    return {
                        value: value.getValue(),
                        unit: value.getUnit()
                    };
                }
                if (tsd.Sink.isMetricsList(value)) {
                    var listValue = {};
                    listValue[key] = {
                        values: value.getValues()
                    };
                    return {
                        values: value.getValues()
                    };
                }
                if (key[0] !== "_") {
                    return value;
                }
            }, 2), serializedEvent);
            done();
        }, 750);
    });
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * @fileoverview Copies built assets from our build directory to each of their respective package's
 * dist/ folder.
 */

const path = require('path');
const fs = require('fs');
const cpFile = require('cp-file');
const toSlugCase = require('to-slug-case');
const {spawnSync} = require('child_process');
const {sync: globSync} = require('glob');

const PKG_RE = /(?:material\-components\-web)|(?:mdc\.[a-zA-Z\-]+)/;

const isValidCwd = (
  path.basename(process.cwd()) === 'material-components-web' &&
  fs.existsSync('packages') &&
  fs.existsSync('build')
);

if (!isValidCwd) {
  console.error(
    'Invalid CWD. Please ensure you are running this from the root of the repo, and that you have run `npm run dist`'
  );
  process.exit(1);
}

function cleanPkgDistDirs() {
  const statuses = globSync('packages/*/dist').map(
    (distPath) => spawnSync('rm', ['-r', distPath], {stdio: 'inherit'}).status);

  if (statuses.find((status) => status > 0)) {
    console.error('Failed to clean package dist folders prior to copying');
    process.exit(1);
  }
}

function getAssetEntry(asset) {
  const [entryName] = path.parse(asset).name.match(PKG_RE);
  const [MDC, componentName] = entryName.split('.');
  const dealingWithSubpackage = Boolean(MDC && componentName);
  if (!dealingWithSubpackage) {
    return entryName;
  }
  return [MDC, toSlugCase(componentName)].join('-');
}

function cpAsset(asset) {
  const assetPkg = path.join('packages', getAssetEntry(asset));
  if (!fs.existsSync(assetPkg)) {
    Promise.reject(new Error(`Non-existent asset package path ${assetPkg} for ${asset}`));
  }
  const destDir = path.join(assetPkg, 'dist', path.basename(asset));
  return cpFile(asset, destDir).then(() => console.log(`cp ${asset} -> ${destDir}`));
}

cleanPkgDistDirs();

Promise.all(globSync('build/*.{css,js}').map(cpAsset)).catch((err) => {
  console.error(`Error encountered copying assets: ${err}`);
  process.exit(1);
});
<commit_msg>fix(package): Add source-map files to npm releases (#4206)<commit_after>/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * @fileoverview Copies built assets from our build directory to each of their respective package's
 * dist/ folder.
 */

const path = require('path');
const fs = require('fs');
const cpFile = require('cp-file');
const toSlugCase = require('to-slug-case');
const {spawnSync} = require('child_process');
const {sync: globSync} = require('glob');

const PKG_RE = /(?:material\-components\-web)|(?:mdc\.[a-zA-Z\-]+)/;

const isValidCwd = (
  path.basename(process.cwd()) === 'material-components-web' &&
  fs.existsSync('packages') &&
  fs.existsSync('build')
);

if (!isValidCwd) {
  console.error(
    'Invalid CWD. Please ensure you are running this from the root of the repo, and that you have run `npm run dist`'
  );
  process.exit(1);
}

function cleanPkgDistDirs() {
  const statuses = globSync('packages/*/dist').map(
    (distPath) => spawnSync('rm', ['-r', distPath], {stdio: 'inherit'}).status);

  if (statuses.find((status) => status > 0)) {
    console.error('Failed to clean package dist folders prior to copying');
    process.exit(1);
  }
}

function getAssetEntry(asset) {
  const [entryName] = path.parse(asset).name.match(PKG_RE);
  const [MDC, componentName] = entryName.split('.');
  const dealingWithSubpackage = Boolean(MDC && componentName);
  if (!dealingWithSubpackage) {
    return entryName;
  }
  return [MDC, toSlugCase(componentName)].join('-');
}

function cpAsset(asset) {
  const assetPkg = path.join('packages', getAssetEntry(asset));
  if (!fs.existsSync(assetPkg)) {
    Promise.reject(new Error(`Non-existent asset package path ${assetPkg} for ${asset}`));
  }
  const destDir = path.join(assetPkg, 'dist', path.basename(asset));
  return cpFile(asset, destDir).then(() => console.log(`cp ${asset} -> ${destDir}`));
}

cleanPkgDistDirs();

Promise.all(globSync('build/*.{css,js,map}').map(cpAsset)).catch((err) => {
  console.error(`Error encountered copying assets: ${err}`);
  process.exit(1);
});
<|endoftext|>


<commit_before>require('dotenv').config();

const _ = require('lodash');
const detect = require('language-detect');
const different = require('different');
const inspector = require('schema-inspector');
const parse = require('parse-link-header');
const request = require('request-promise-native');

const API_BASE_URL = 'https://api.github.com';

// Standard options for making requests to Github API
const baseOpts = {
    headers: {
        'Accept': 'application/vnd.github.v3+json',
        'User-Agent': 'Request-Promise'
    },
    qs: {
        per_page: 100, // eslint-disable-line
    },
    json: true,
    resolveWithFullResponse: true
};

/**
 * Gets a Github user's repository programming language distribution
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {Object} token       Github personal access token
 * @return {Promise}            Resolves if API request performed successfully
 *                              Rejects if parameters are invalid, or error occurs with API request
 */
exports.getRepoLanguages = (visibility, token) => {
    // First get the User's repositories
    return getUserRepos(visibility, token).then((responses) => {
        // Parse the repos json from the response bodies
        var repos = [];
        _.each(responses, (response) => {
            repos = repos.concat(response.body);
        });

        // Map Promises for each URL to resolve to the total language byte count
        var urls = _.map(repos, 'languages_url');
        var promises = _.map(urls, _.curry(createAPIRequestPromise)(token, null));

        return Promise.all(promises);
    }).then((responses) => {
        var results = _.map(responses, 'body');

        // Count bytes per language
        var totals = {};
        _.each(results, (obj) => {
            _.each(obj, (val, key) => {
                if (!totals[key]) totals[key] = 0;
                totals[key] += obj[key];
            });
        });

        return totals;
    });
};

/**
 * Gets a Github user's commit programming language distribution
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {Object} token       Github personal access token
 * @return {Promise}            Resolves if API request performed successfully
 *                              Rejects if parameters are invalid, or error occurs with API request
 */
exports.getCommitLanguages = (visibility, token) => {
    // First get the user's repositories
    return getUserRepos(visibility, token).then((responses) => {
        // Parse the repos json from the response bodies
        var repos = [];
        _.each(responses, (response) => {
            repos = repos.concat(response.body);
        });

        var options = _.defaultsDeep({
            uri: API_BASE_URL + '/user',
            qs: {
                access_token: token, // eslint-disable-line
            },
            resolveWithFullResponse: false
        }, baseOpts);

        // Get Github username from access token
        return request(options).then((user) => {
            // Get Repo commit URLs
            var urls = _.map(repos, (repo) => {
                return repo.url + '/commits'
            });

            // Map a Promise for each repo commit URL
            var promises = _.map(urls, _.curry(getRepoCommits)(user.login, token));
            promises = promises.map((p) => p.then((v) => v, (e) => ({ error: e })));

            return Promise.all(promises).then((results) => {
                var commits = [];

                // Get commits from Promise reponses
                results.forEach((result) => {
                    if (result.error) {
                        // TODO: Promise may be rejected in certain cases should we do anything here?
                    } else {
                        _.each(result, (value) => {
                            commits = commits.concat(value.body);
                        });
                    }
                });

                // Map a promise for each individual commit URL
                var urls = _.chain(commits).filter((c) => {
                                return c && c.author && c.author.login === user.login;
                            }).map('url').value();

                var promises = _.map(urls, _.curry(createAPIRequestPromise)(token, null));

                return Promise.all(promises);
            }).then((responses) => {
                var commits = _.map(responses, 'body');
                var totals = {};

                // For each file in the commit files
                _.each(commits, (commit) => {
                    var commitLangs = []; // Store all the languages present in commit
                    _.each(commit.files, (file) => {
                        var language = detect.filename(file.filename);
                        if (language) {
                            // Create empty object to hold total values
                            if (!totals[language]) totals[language] = {
                                bytes: 0,
                                commits: 0
                            };

                            // Add one to the language commit count if we haven't already
                            if (commitLangs.indexOf(language) == -1) {
                                commitLangs.push(language);
                                totals[language].commits += 1;
                            }

                            // Parse Git diff
                            different.parseDiffFromString('diff\n' + file.patch, (diff) => {
                                // Sum number of bytes from additions and add to results
                                var byteCount = _.reduce(diff[0].additions, (sum, line) => {
                                    return line.length;
                                }, 0);

                                totals[language].bytes += byteCount;
                            });
                        }
                    });
                });

                return totals;
            });
        });
    });
};

/**
 * Gets a list of Github user's repositories from the Github API
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {String} token       Github personal access token
 * @return {Promise}            Resolves if repo URLs are obtained
 *                              Rejects if an error occurs obtaining URLs
 */
function getUserRepos(visibility, token) {
    // First validate the user token input
    var validation = {
        type: 'string'
    };
    var result = inspector.validate(validation, token);
    if (!result.valid) throw Error(result.format());

    // Form options for API request
    var url = API_BASE_URL + '/user/repos';
    var options = _.defaultsDeep({
        uri: url,
        qs: {
            access_token: token, // eslint-disable-line
            visibility: visibility
        }
    }, baseOpts);

    return request(options).then((response) => { // eslint-disable-line
        var link = parse(response.headers.link);
        var promises = []; // To store the promises to resolve the other pages of repos

        if (link) { // Get the other pages of results if necessary
            var start = Number(link.next.page), end = Number(link.last.page);
            for (var page = start; page <= end; page++) {
                promises.push(_.curry(createAPIRequestPromise)(token, {
                    page: page,
                    visibility: visibility
                }, url));
            }
        }
        promises.push(_.curry(createAPIRequestPromise)(token, {
            page: 1,
            visibility: visibility
        }, url));

        return Promise.all(promises);
    });
}

/**
 * Creates and returns a promise to resolve to all of the commits for a Github repo
 * @param  {String} username    Github username
 * @param  {String} token       Github personal access token
 * @param  {String} repoUrl     Github repository URL
 * @return {Promise}            Promise to resolve repo commits
 */
function getRepoCommits(username, token, repoUrl) {
    // Form options for API request
    var options = _.defaultsDeep({
        uri: repoUrl,
        qs: {
            access_token: token, // eslint-disable-line
        }
    }, baseOpts);
    if (username) options.qs.author = username;

    return request(options).then((response) => { // eslint-disable-line
        var promises = []; // To store the promises to resolve the other pages of commits
        var link = parse(response.headers.link);

        if (link) { // Get the other pages of results if necessary
            var start = Number(link.next.page), end = Number(link.last.page);
            for (var page = start; page <= end; page++) {
                promises.push(_.curry(createAPIRequestPromise)(token, {
                    page: page,
                    author: username
                }, repoUrl));
            }
        }
        promises.push(_.curry(createAPIRequestPromise)(token, {
            page: 1,
            author: username
        }, repoUrl));

        return Promise.all(promises);
    });
}

/**
 * Creates and returns a promise to resolve to a specific Github API request result
 * @param  {String} token   Github personal access token
 * @param  {Object} qs      Extra query string parameters
 * @param  {String} url     Github API URL
 * @return {Promise}        Promise to be resolved somewhere
 */
function createAPIRequestPromise(token, qs, url) {
    // Form options for API request
    var options = _.defaultsDeep({
        uri: url,
        qs: {
            access_token: token, // eslint-disable-line
        }
    }, baseOpts);
    if (qs) {
        for (var key in qs) {
            if (Object.prototype.hasOwnProperty.call(qs, key))
                options.qs[key] = qs[key];
        }
    }

    // Perform API request and handle result appropriately
    return request(options);
}
<commit_msg>Replaced var with const/let, replaced Array.indexOf with Array.includes<commit_after>require('dotenv').config();

const _ = require('lodash');
const detect = require('language-detect');
const different = require('different');
const inspector = require('schema-inspector');
const parse = require('parse-link-header');
const request = require('request-promise-native');

const API_BASE_URL = 'https://api.github.com';

// Standard options for making requests to Github API
const baseOpts = {
    headers: {
        'Accept': 'application/vnd.github.v3+json',
        'User-Agent': 'Request-Promise'
    },
    qs: {
        per_page: 100, // eslint-disable-line
    },
    json: true,
    resolveWithFullResponse: true
};

/**
 * Gets a Github user's repository programming language distribution
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {Object} token       Github personal access token
 * @return {Promise}            Resolves if API request performed successfully
 *                              Rejects if parameters are invalid, or error occurs with API request
 */
exports.getRepoLanguages = (visibility, token) => {
    // First get the User's repositories
    return getUserRepos(visibility, token).then((responses) => {
        // Parse the repos json from the response bodies
        let repos = [];
        _.each(responses, (response) => {
            repos = repos.concat(response.body);
        });

        // Map Promises for each URL to resolve to the total language byte count
        const urls = _.map(repos, 'languages_url');
        const promises = _.map(urls, _.curry(createAPIRequestPromise)(token, null));

        return Promise.all(promises);
    }).then((responses) => {
        const results = _.map(responses, 'body');

        // Count bytes per language
        const totals = {};
        _.each(results, (obj) => {
            _.each(obj, (val, key) => {
                if (!totals[key]) totals[key] = 0;
                totals[key] += obj[key];
            });
        });

        return totals;
    });
};

/**
 * Gets a Github user's commit programming language distribution
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {Object} token       Github personal access token
 * @return {Promise}            Resolves if API request performed successfully
 *                              Rejects if parameters are invalid, or error occurs with API request
 */
exports.getCommitLanguages = (visibility, token) => {
    // First get the user's repositories
    return getUserRepos(visibility, token).then((responses) => {
        // Parse the repos json from the response bodies
        let repos = [];
        _.each(responses, (response) => {
            repos = repos.concat(response.body);
        });

        const options = _.defaultsDeep({
            uri: API_BASE_URL + '/user',
            qs: {
                access_token: token, // eslint-disable-line
            },
            resolveWithFullResponse: false
        }, baseOpts);

        // Get Github username from access token
        return request(options).then((user) => {
            // Get Repo commit URLs
            const urls = _.map(repos, (repo) => {
                return repo.url + '/commits'
            });

            // Map a Promise for each repo commit URL
            let promises = _.map(urls, _.curry(getRepoCommits)(user.login, token));
            promises = promises.map((p) => p.then((v) => v, (e) => ({ error: e })));

            return Promise.all(promises).then((results) => {
                let commits = [];

                // Get commits from Promise reponses
                results.forEach((result) => {
                    if (result.error) {
                        // TODO: Promise may be rejected in certain cases should we do anything here?
                    } else {
                        _.each(result, (value) => {
                            commits = commits.concat(value.body);
                        });
                    }
                });

                // Map a promise for each individual commit URL
                const urls = _.chain(commits).filter((c) => {
                                return c && c.author && c.author.login === user.login;
                            }).map('url').value();

                const promises = _.map(urls, _.curry(createAPIRequestPromise)(token, null));

                return Promise.all(promises);
            }).then((responses) => {
                const commits = _.map(responses, 'body');
                const totals = {};

                // For each file in the commit files
                _.each(commits, (commit) => {
                    const commitLangs = []; // Store all the languages present in commit
                    _.each(commit.files, (file) => {
                        const language = detect.filename(file.filename);
                        if (language) {
                            // Create empty object to hold total values
                            if (!totals[language]) totals[language] = {
                                bytes: 0,
                                commits: 0
                            };

                            // Add one to the language commit count if we haven't already
                            if (!commitLangs.includes(language)) {
                                commitLangs.push(language);
                                totals[language].commits += 1;
                            }

                            // Parse Git diff
                            different.parseDiffFromString('diff\n' + file.patch, (diff) => {
                                // Sum number of bytes from additions and add to results
                                const byteCount = _.reduce(diff[0].additions, (sum, line) => {
                                    return line.length;
                                }, 0);

                                totals[language].bytes += byteCount;
                            });
                        }
                    });
                });

                return totals;
            });
        });
    });
};

/**
 * Gets a list of Github user's repositories from the Github API
 * @param  {String} visibility  Type of repositories to find (can be all, public, or private)
 * @param  {String} token       Github personal access token
 * @return {Promise}            Resolves if repo URLs are obtained
 *                              Rejects if an error occurs obtaining URLs
 */
function getUserRepos(visibility, token) {
    // First validate the user token input
    const validation = {
        type: 'string'
    };
    const result = inspector.validate(validation, token);
    if (!result.valid) throw Error(result.format());

    // Form options for API request
    const url = API_BASE_URL + '/user/repos';
    const options = _.defaultsDeep({
        uri: url,
        qs: {
            access_token: token, // eslint-disable-line
            visibility: visibility
        }
    }, baseOpts);

    return request(options).then((response) => { // eslint-disable-line
        const link = parse(response.headers.link);
        const promises = []; // To store the promises to resolve the other pages of repos

        if (link) { // Get the other pages of results if necessary
            const start = Number(link.next.page), end = Number(link.last.page);
            for (let page = start; page <= end; page++) {
                promises.push(_.curry(createAPIRequestPromise)(token, {
                    page: page,
                    visibility: visibility
                }, url));
            }
        }
        promises.push(_.curry(createAPIRequestPromise)(token, {
            page: 1,
            visibility: visibility
        }, url));

        return Promise.all(promises);
    });
}

/**
 * Creates and returns a promise to resolve to all of the commits for a Github repo
 * @param  {String} username    Github username
 * @param  {String} token       Github personal access token
 * @param  {String} repoUrl     Github repository URL
 * @return {Promise}            Promise to resolve repo commits
 */
function getRepoCommits(username, token, repoUrl) {
    // Form options for API request
    const options = _.defaultsDeep({
        uri: repoUrl,
        qs: {
            access_token: token, // eslint-disable-line
        }
    }, baseOpts);
    if (username) options.qs.author = username;

    return request(options).then((response) => { // eslint-disable-line
        const promises = []; // To store the promises to resolve the other pages of commits
        const link = parse(response.headers.link);

        if (link) { // Get the other pages of results if necessary
            const start = Number(link.next.page), end = Number(link.last.page);
            for (let page = start; page <= end; page++) {
                promises.push(_.curry(createAPIRequestPromise)(token, {
                    page: page,
                    author: username
                }, repoUrl));
            }
        }
        promises.push(_.curry(createAPIRequestPromise)(token, {
            page: 1,
            author: username
        }, repoUrl));

        return Promise.all(promises);
    });
}

/**
 * Creates and returns a promise to resolve to a specific Github API request result
 * @param  {String} token   Github personal access token
 * @param  {Object} qs      Extra query string parameters
 * @param  {String} url     Github API URL
 * @return {Promise}        Promise to be resolved somewhere
 */
function createAPIRequestPromise(token, qs, url) {
    // Form options for API request
    const options = _.defaultsDeep({
        uri: url,
        qs: {
            access_token: token, // eslint-disable-line
        }
    }, baseOpts);
    if (qs) {
        for (let key in qs) {
            if (Object.prototype.hasOwnProperty.call(qs, key))
                options.qs[key] = qs[key];
        }
    }

    // Perform API request and handle result appropriately
    return request(options);
}
<|endoftext|>


<commit_before>(function() {

var insertEvents=[];

function catchInsertEvent(event) {
	// just cache the event.  if we try to access the object, we screw things up
	insertEvents[insertEvents.length]=event;
}

function mungeMenus(event) {
	document.removeEventListener('DOMNodeInserted', catchInsertEvent, true);
	window.removeEventListener('DOMContentLoaded', mungeMenus, true);

	var toolsMenu=
		document.getElementById('menu_ToolsPopup') || // firefox
		document.getElementById('taskPopup') ;        // thunderbird

	var moreToolsMenu=document.getElementById('more-tools-menupopup');

	var mungeFlag=false;

	// for each insert event, find the element, and decide
	// if we should do something with it
	for (var i=insertEvents.length-1, event=null, el=null; event=insertEvents[i]; i--) {
		try {
			el=event.target;

			if (toolsMenu!=el.parentNode) continue;
			// if we got here, the insert was to the tools menu.  move the element!
			toolsMenu.removeChild(el);
			moreToolsMenu.appendChild(el);

			mungeFlag=true;
		} catch (e) { }
	}

	if (mungeFlag) {
		// we did munge something into this menu; remove the label and separator
		document.getElementById('more-tools-label').setAttribute('hidden', true);
		document.getElementById('more-tools-sep').setAttribute('hidden', true);
	}
}

document.addEventListener('DOMNodeInserted', catchInsertEvent, true);
window.addEventListener('DOMContentLoaded', mungeMenus, true);

})();
<commit_msg>Refs #140  * Incorporate submitted code, plus enhancements.  Still requires testing.<commit_after>(function() {
var orgDocument=document.lastChild.cloneNode(true);
orgDocument.getElementById=document.getElementById;

function mungeMenus(event) {
	window.removeEventListener('DOMContentLoaded', mungeMenus, true);

	var toolsMenu=
		document.getElementById('menu_ToolsPopup') || // firefox
		document.getElementById('taskPopup') ;        // thunderbird
	var moreToolsMenu=document.getElementById('more-tools-menupopup');

	var orgMenuPopups=orgDocument.getElementsByTagName("menupopup");
	var orgToolsMenu=null;
	for (var i=0, popup=null; popup=orgMenuPopups[i]; i++) {
		if (popup.id=="menu_ToolsPopup" || // firefox
		    popup.id=="taskPopup"          // thunderbird
		) {
			orgToolsMenu=orgMenuPopups[i];
		}
	}

	var mungeFlag=false;
	var moveTool=function(e) {
		if (e.target) e=e.target;

		toolsMenu.removeChild(e);
		moreToolsMenu.appendChild(e);

		if (!mungeFlag) {
			document.getElementById('more-tools-label').setAttribute('hidden', true);
			document.getElementById('more-tools-sep').setAttribute('hidden', true);
		}
		mungeFlag=true;
	}

	function nodeWasInOrgMenu(node) {
		for (var i=0, orgNode=false; orgNode=orgToolsMenu.childNodes[i]; i++) {
			if (node.isEqualNode(orgNode)) return true;
		}
		return false;
	}

	for (var i=0, node=false; node=toolsMenu.childNodes[i]; i++) {
		if (!nodeWasInOrgMenu(node)) {
			moveTool(node);
		}
	}

	// Free this memory.
	orgDocument=null;
	orgToolsMenu=null;
	orgMenuPopups=null;

	// Move any node added in the future.
	toolsMenu.addEventListener('DOMNodeInserted', moveTool, true)
}
window.addEventListener('DOMContentLoaded', mungeMenus, true);
})();
<|endoftext|>


<commit_before>/**
 * ngElectron service for AngularJS
 * (c)2015 C. Byerley @develephant
 * http://develephant.github.io/ngElectron
 * See also: https://develephant.gitgub.io/amy
 * Version 0.3.3
 */
'use strict';

const electron_host = 'ELECTRON_BRIDGE_HOST';
const electron_client = 'ELECTRON_BRIDGE_CLIENT';
const electron_host_id = 'electron-host';
const db_silo = 'client/data';

const ipc = require('ipc');
const diskdb = require('diskdb');

angular.module('ngElectron', [])

.factory("electron", [function() {
  var o = new Object();

  //ipc -> host (main process)
  o.send = function( data ) {
    ipc.send(electron_host, data);
  };

  //diskdb
  o.db = function( collection ) {
    if ( diskdb ) {
      var collection_arr = [];
      if (typeof collection == 'object') {
        collection_arr = collection;
      } else if (typeof collection == 'string') {
        collection_arr.push( collection );
      }

      return diskdb.connect(db_silo, collection_arr);
    }

    return 'diskdb is not installed and/or configured.'
  };

  //remote require
  o.require         = require('remote').require;

  //Electron api
  o.app             = o.require('app');
  o.browserWindow   = o.require('browser-window');
  o.clipboard       = o.require('clipboard');
  o.dialog          = o.require('dialog');
  o.dialog          = o.require('dialog');
  o.globalShortcut  = o.require('global-shortcut');
  o.menu            = o.require('menu');
  o.menuItem        = o.require('menu-item');
  o.nativeImage     = o.require('native-image');
  o.powerMonitor    = o.require('power-monitor');
  o.protocol        = o.require('protocol');
  o.screen          = o.require('screen');
  o.shell           = o.require('shell');
  o.tray            = o.require('tray');

  //Node 11 (abridged) api
  o.buffer          = o.require('buffer');
  o.childProcess    = o.require('child_process');
  o.crypto          = o.require('crypto');
  o.dns             = o.require('dns');
  o.emitter         = o.require('events').EventEmitter;
  o.fs              = o.require('fs');
  o.http            = o.require('http');
  o.https           = o.require('https');
  o.net             = o.require('net');
  o.os              = o.require('os');
  o.path            = o.require('path');
  o.querystring     = o.require('querystring');
  o.url             = o.require('url');
  o.zlib            = o.require('zlib');

  return o;
}])

// Start listening for AngularJS ipc messages
.run(['$rootScope','electron',
function($rootScope, electron) {
  console.log('ngElectron has joined the room.');
  //Start listening for host messages
  ipc.on(electron_client, function( data ) {
    //Event type: 'electron-host'
    $rootScope.$broadcast(electron_host_id, data);
  });

  /*
  Add $electron as a special root property.
  Though generally not a good practice,
  it helps protect the electron instance
  and we are in a more closed enviroment
  as it is.
  */
  $rootScope.$electron = electron;
}]);
<commit_msg>Exposing diskdb pathToFolder<commit_after>/**
 * ngElectron service for AngularJS
 * (c)2015 C. Byerley @develephant
 * http://develephant.github.io/ngElectron
 * See also: https://develephant.gitgub.io/amy
 * Version 0.3.3
 */
'use strict';

const electron_host = 'ELECTRON_BRIDGE_HOST';
const electron_client = 'ELECTRON_BRIDGE_CLIENT';
const electron_host_id = 'electron-host';
const db_silo = 'client/data';

const ipc = require('ipc');
const diskdb = require('diskdb');

angular.module('ngElectron', [])

.factory("electron", [function() {
  var o = new Object();

  //ipc -> host (main process)
  o.send = function( data ) {
    ipc.send(electron_host, data);
  };

  //diskdb
  o.db = function( pathToFolder, collection ) {
    if ( diskdb ) {
      var collection_arr = [];
      if (typeof collection == 'object') {
        collection_arr = collection;
      } else if (typeof collection == 'string') {
        collection_arr.push( collection );
      }

      return diskdb.connect(pathToFolder, collection_arr);
    }

    return 'diskdb is not installed and/or configured.'
  };

  //remote require
  o.require         = require('remote').require;

  //Electron api
  o.app             = o.require('app');
  o.browserWindow   = o.require('browser-window');
  o.clipboard       = o.require('clipboard');
  o.dialog          = o.require('dialog');
  o.dialog          = o.require('dialog');
  o.globalShortcut  = o.require('global-shortcut');
  o.menu            = o.require('menu');
  o.menuItem        = o.require('menu-item');
  o.nativeImage     = o.require('native-image');
  o.powerMonitor    = o.require('power-monitor');
  o.protocol        = o.require('protocol');
  o.screen          = o.require('screen');
  o.shell           = o.require('shell');
  o.tray            = o.require('tray');

  //Node 11 (abridged) api
  o.buffer          = o.require('buffer');
  o.childProcess    = o.require('child_process');
  o.crypto          = o.require('crypto');
  o.dns             = o.require('dns');
  o.emitter         = o.require('events').EventEmitter;
  o.fs              = o.require('fs');
  o.http            = o.require('http');
  o.https           = o.require('https');
  o.net             = o.require('net');
  o.os              = o.require('os');
  o.path            = o.require('path');
  o.querystring     = o.require('querystring');
  o.url             = o.require('url');
  o.zlib            = o.require('zlib');

  return o;
}])

// Start listening for AngularJS ipc messages
.run(['$rootScope','electron',
function($rootScope, electron) {
  console.log('ngElectron has joined the room.');
  //Start listening for host messages
  ipc.on(electron_client, function( data ) {
    //Event type: 'electron-host'
    $rootScope.$broadcast(electron_host_id, data);
  });

  /*
  Add $electron as a special root property.
  Though generally not a good practice,
  it helps protect the electron instance
  and we are in a more closed enviroment
  as it is.
  */
  $rootScope.$electron = electron;
}]);
<|endoftext|>


<commit_before>/* eslint-disable no-param-reassign, max-len */
/**
 * The main navigation with a list of all components
 */

import React from 'react';
import { IndexLink } from 'react-router';

import getComponentNameFromPath from '../../../utils/getComponentNameFromPath';
import styles from './styles.css';
import mapValues from 'lodash/mapValues';
import values from 'lodash/values';
import has from 'lodash/has';

class Navigation extends React.Component {

  state = {
    filterString: '',
  };

  componentDidMount() {
    // making sure the component is re-rendered when the component data is loaded
    document.documentElement.addEventListener(
      'styleguide-plugin-update-navigation',
      () => {
        this.forceUpdate();
      },
      false
    );
  }

  setFilter = (event) => {
    this.setState({
      filterString: event.target.value,
    });
  }

  renderSubNavigation = (componentPath) => {
    if (this.props.activeComponentPath === componentPath) {
      if (has(window.STYLEGUIDE_PLUGIN_CLIENT_API.cache, componentPath)) {
        const plugins = window.STYLEGUIDE_PLUGIN_CLIENT_API.cache[componentPath].navigation;
        return values(mapValues(plugins, (plugin, pluginKey) => (

            {
              values(mapValues(plugin, (link) => (

                  <IndexLink
                    to={`/${componentPath}?id=${link.id}`}
                    className={styles.subListItem}
                    activeClassName={styles.subListItemActive}
                  >
                    {link.title}
                  </IndexLink>

              )))
            }

        )));
      }

      return null;
    }

    return null;
  }

  renderComponents = () => (
    // Iterate through all components and generate a list
    Object.keys(window.STYLEGUIDE_PLUGIN_CLIENT_API.scripts)
      .map((componentPath) => {
        // Clean the component name
        // TODO Maybe do this earlier, not on every mount
        const componentName = getComponentNameFromPath(componentPath);
        if (componentName.indexOf(this.state.filterString) > -1) {
          return (
            // IndexLink so not all links that match a part of the route are highlighted

              <IndexLink
                to={`/${componentPath}`}
                className={styles.listItem}
                activeClassName={styles.listItemActive}
              >
                {componentName}
              </IndexLink>
              {this.renderSubNavigation(componentPath)}

          );
        }
        return null;
      }
    )
  )

  render() {
    return (

        <IndexLink
          to="/"
          className={styles.titleItem}
        >

            Home

        </IndexLink>
        <input
          className={styles.filterInput}
          placeholder="Filter"
          onChange={this.setFilter}
        />

          {this.renderComponents()}

    );
  }
}

export default Navigation;
<commit_msg>ignore case when filtering<commit_after>/* eslint-disable no-param-reassign, max-len */
/**
 * The main navigation with a list of all components
 */

import React from 'react';
import { IndexLink } from 'react-router';

import getComponentNameFromPath from '../../../utils/getComponentNameFromPath';
import styles from './styles.css';
import mapValues from 'lodash/mapValues';
import values from 'lodash/values';
import has from 'lodash/has';

class Navigation extends React.Component {

  state = {
    filterString: '',
  };

  componentDidMount() {
    // making sure the component is re-rendered when the component data is loaded
    document.documentElement.addEventListener(
      'styleguide-plugin-update-navigation',
      () => {
        this.forceUpdate();
      },
      false
    );
  }

  setFilter = (event) => {
    this.setState({
      filterString: event.target.value,
    });
  }

  renderSubNavigation = (componentPath) => {
    if (this.props.activeComponentPath === componentPath) {
      if (has(window.STYLEGUIDE_PLUGIN_CLIENT_API.cache, componentPath)) {
        const plugins = window.STYLEGUIDE_PLUGIN_CLIENT_API.cache[componentPath].navigation;
        return values(mapValues(plugins, (plugin, pluginKey) => (

            {
              values(mapValues(plugin, (link) => (

                  <IndexLink
                    to={`/${componentPath}?id=${link.id}`}
                    className={styles.subListItem}
                    activeClassName={styles.subListItemActive}
                  >
                    {link.title}
                  </IndexLink>

              )))
            }

        )));
      }

      return null;
    }

    return null;
  }

  renderComponents = () => (
    // Iterate through all components and generate a list
    Object.keys(window.STYLEGUIDE_PLUGIN_CLIENT_API.scripts)
      .map((componentPath) => {
        // Clean the component name
        // TODO Maybe do this earlier, not on every mount
        const componentName = getComponentNameFromPath(componentPath);
        if (componentName.toLowerCase().indexOf(this.state.filterString.toLowerCase()) > -1) {
          return (
            // IndexLink so not all links that match a part of the route are highlighted

              <IndexLink
                to={`/${componentPath}`}
                className={styles.listItem}
                activeClassName={styles.listItemActive}
              >
                {componentName}
              </IndexLink>
              {this.renderSubNavigation(componentPath)}

          );
        }
        return null;
      }
    )
  )

  render() {
    return (

        <IndexLink
          to="/"
          className={styles.titleItem}
        >

            Home

        </IndexLink>
        <input
          className={styles.filterInput}
          placeholder="Filter"
          onChange={this.setFilter}
        />

          {this.renderComponents()}

    );
  }
}

export default Navigation;
<|endoftext|>


<commit_before>// Load Config
var config = require('./config');

// Load Modules
var amqp   = require('amqplib/callback_api'),
    fs     = require('fs'),
    Ivona  = require('ivona-node'),
    log4js = require('log4js'),
    md5    = require('md5'),
    player = require('./player'),
    TDB    = require('./tdb');

// Init Logging
log4js.configure('', config.l4j);
var log    = log4js.getLogger();

// Connect to DB
var tdb = new TDB(config.pg, log4js);

// Global Variabales
var ivona; // Ivona Connection

tdb.reg.get('/config/ivona', function (err, result) {
  ivona = new Ivona(result);
  say("Initializing.");
  say("Connecting to queue.");

  connectAMQP();
});

function say(text) {
  voiceHash(text, config.voice.name, function (vhash) {
    // create filename
    var file = config.cache_dir + '/' + vhash + '.mp3';

    fs.stat(file, function (err, stat) {
      if (err == null) {
        player.playFile(file);
      } else if (err.code == 'ENOENT') {
        log.info('retrieving file.');
        ivonaGetFile(text, config.voice, file, function (response) {
          player.playFile(response.file);
        });
      } else {
        log.error(err);
      }
    });
  });
}

function voiceHash(text, voiceName, cb) {
  var voiceHash = md5(voiceName + '|' + text);
  log.trace({
    text: text,
    voice: voiceName,
    hash: voiceHash
  });
  cb(voiceHash);
}

function ivonaGetFile(text, voiceObj, file, cb) {
  log.debug('retrieving voice into file %s', file);
  ivona.createVoice(text, {
    body: {
      voice: voiceObj
    }
  }).pipe(
    fs.createWriteStream(file)
  ).on('finish', function (result) {
      var response = {
        text: text,
        file: file
      };
      cb(response)
    }
  );
}

function handleMessage(msg) {
  var sayReq = JSON.parse(msg);

  log.debug(sayReq);

  if (sayReq.say) {
    say(sayReq.say);
  }
}

function connectAMQP() {
  amqp.connect(config.amqp, function(err, conn) {
    say('Queue connected.');
    conn.createChannel(function(err, ch) {
      var q = 'say';

      ch.assertQueue(q, {durable: false});
      ch.consume(q, function(msg) {
        var message = msg.content.toString();
        log.debug(message);
        handleMessage(message);
      }, {noAck: true});
    });
  });
}<commit_msg>catch malformed json packets<commit_after>// Load Config
var config = require('./config');

// Load Modules
var amqp   = require('amqplib/callback_api'),
    fs     = require('fs'),
    Ivona  = require('ivona-node'),
    log4js = require('log4js'),
    md5    = require('md5'),
    player = require('./player'),
    TDB    = require('./tdb');

// Init Logging
log4js.configure('', config.l4j);
var log    = log4js.getLogger();

// Connect to DB
var tdb = new TDB(config.pg, log4js);

// Global Variabales
var ivona; // Ivona Connection

tdb.reg.get('/config/ivona', function (err, result) {
  ivona = new Ivona(result);
  say("Initializing.");
  say("Connecting to queue.");

  connectAMQP();
});

function say(text) {
  voiceHash(text, config.voice.name, function (vhash) {
    // create filename
    var file = config.cache_dir + '/' + vhash + '.mp3';

    fs.stat(file, function (err, stat) {
      if (err == null) {
        player.playFile(file);
      } else if (err.code == 'ENOENT') {
        log.info('retrieving file.');
        ivonaGetFile(text, config.voice, file, function (response) {
          player.playFile(response.file);
        });
      } else {
        log.error(err);
      }
    });
  });
}

function voiceHash(text, voiceName, cb) {
  var voiceHash = md5(voiceName + '|' + text);
  log.trace({
    text: text,
    voice: voiceName,
    hash: voiceHash
  });
  cb(voiceHash);
}

function ivonaGetFile(text, voiceObj, file, cb) {
  log.debug('retrieving voice into file %s', file);
  ivona.createVoice(text, {
    body: {
      voice: voiceObj
    }
  }).pipe(
    fs.createWriteStream(file)
  ).on('finish', function (result) {
      var response = {
        text: text,
        file: file
      };
      cb(response)
    }
  );
}

function handleMessage(msg) {
  var sayReq = {};

  try {
    sayReq = JSON.parse(msg);
  } catch (e) {
    log.error('JSON packet [%s] is malformed', msg)
  }

  if (sayReq.say) {
    say(sayReq.say);
  }
}

function connectAMQP() {
  amqp.connect(config.amqp, function(err, conn) {
    say('Queue connected.');
    conn.createChannel(function(err, ch) {
      var q = 'say';

      ch.assertQueue(q, {durable: false});
      ch.consume(q, function(msg) {
        var message = msg.content.toString();
        handleMessage(message);
      }, {noAck: true});
    });
  });
}<|endoftext|>


<commit_before>import { Cookies } from 'meteor/ostrio:cookies';
const cookies = new Cookies();
const subManager = new SubsManager();
const { calculateIndex } = Utils;
const swimlaneWhileSortingHeight = 150;

BlazeComponent.extendComponent({
  onCreated() {
    this.isBoardReady = new ReactiveVar(false);

    // The pattern we use to manually handle data loading is described here:
    // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
    // XXX The boardId should be readed from some sort the component "props",
    // unfortunatly, Blaze doesn't have this notion.
    this.autorun(() => {
      const currentBoardId = Session.get('currentBoard');
      if (!currentBoardId) return;
      const handle = subManager.subscribe('board', currentBoardId, false);
      Tracker.nonreactive(() => {
        Tracker.autorun(() => {
          this.isBoardReady.set(handle.ready());
        });
      });
    });
  },

  onlyShowCurrentCard() {
    return Utils.isMiniScreen() && Session.get('currentCard');
  },

  goHome() {
    FlowRouter.go('home');
  },
}).register('board');

BlazeComponent.extendComponent({
  onCreated() {
    this.showOverlay = new ReactiveVar(false);
    this.draggingActive = new ReactiveVar(false);
    this._isDragging = false;
    // Used to set the overlay
    this.mouseHasEnterCardDetails = false;

    // fix swimlanes sort field if there are null values
    const currentBoardData = Boards.findOne(Session.get('currentBoard'));
    const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
    if (nullSortSwimlanes.count() > 0) {
      const swimlanes = currentBoardData.swimlanes();
      let count = 0;
      swimlanes.forEach(s => {
        Swimlanes.update(s._id, {
          $set: {
            sort: count,
          },
        });
        count += 1;
      });
    }

    // fix lists sort field if there are null values
    const nullSortLists = currentBoardData.nullSortLists();
    if (nullSortLists.count() > 0) {
      const lists = currentBoardData.lists();
      let count = 0;
      lists.forEach(l => {
        Lists.update(l._id, {
          $set: {
            sort: count,
          },
        });
        count += 1;
      });
    }
  },
  onRendered() {
    const boardComponent = this;
    const $swimlanesDom = boardComponent.$('.js-swimlanes');

    $swimlanesDom.sortable({
      tolerance: 'pointer',
      appendTo: '.board-canvas',
      helper(evt, item) {
        const helper = $(`
`);
        helper.append(item.clone());
        // Also grab the list of lists of cards
        const list = item.next();
        helper.append(list.clone());
        return helper;
      },
      items: '.swimlane:not(.placeholder)',
      placeholder: 'swimlane placeholder',
      distance: 7,
      start(evt, ui) {
        const listDom = ui.placeholder.next('.js-swimlane');
        const parentOffset = ui.item.parent().offset();

        ui.placeholder.height(ui.helper.height());
        EscapeActions.executeUpTo('popup-close');
        listDom.addClass('moving-swimlane');
        boardComponent.setIsDragging(true);

        ui.placeholder.insertAfter(ui.placeholder.next());
        boardComponent.origPlaceholderIndex = ui.placeholder.index();

        // resize all swimlanes + headers to be a total of 150 px per row
        // this could be achieved by setIsDragging(true) but we want immediate
        // result
        ui.item
          .siblings('.js-swimlane')
          .css('height', `${swimlaneWhileSortingHeight - 26}px`);

        // set the new scroll height after the resize and insertion of
        // the placeholder. We want the element under the cursor to stay
        // at the same place on the screen
        ui.item.parent().get(0).scrollTop =
          ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
      },
      beforeStop(evt, ui) {
        const parentOffset = ui.item.parent().offset();
        const siblings = ui.item.siblings('.js-swimlane');
        siblings.css('height', '');

        // compute the new scroll height after the resize and removal of
        // the placeholder
        const scrollTop =
          ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;

        // then reset the original view of the swimlane
        siblings.removeClass('moving-swimlane');

        // and apply the computed scrollheight
        ui.item.parent().get(0).scrollTop = scrollTop;
      },
      stop(evt, ui) {
        // To attribute the new index number, we need to get the DOM element
        // of the previous and the following card -- if any.
        const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
        const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
        const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);

        $swimlanesDom.sortable('cancel');
        const swimlaneDomElement = ui.item.get(0);
        const swimlane = Blaze.getData(swimlaneDomElement);

        Swimlanes.update(swimlane._id, {
          $set: {
            sort: sortIndex.base,
          },
        });

        boardComponent.setIsDragging(false);
      },
      sort(evt, ui) {
        // get the mouse position in the sortable
        const parentOffset = ui.item.parent().offset();
        const cursorY =
          evt.pageY - parentOffset.top + ui.item.parent().scrollTop();

        // compute the intended index of the placeholder (we need to skip the
        // slots between the headers and the list of cards)
        const newplaceholderIndex = Math.floor(
          cursorY / swimlaneWhileSortingHeight,
        );
        let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;

        // if we are scrolling far away from the bottom of the list
        if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
          destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
        }

        // update the placeholder position in the DOM tree
        if (destPlaceholderIndex !== ui.placeholder.index()) {
          if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
            ui.placeholder.insertBefore(
              ui.placeholder
                .siblings()
                .slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1),
            );
          } else {
            ui.placeholder.insertAfter(
              ui.placeholder
                .siblings()
                .slice(destPlaceholderIndex - 1, destPlaceholderIndex),
            );
          }
        }
      },
    });

    this.autorun(() => {
      let showDesktopDragHandles = false;
      currentUser = Meteor.user();
      if (currentUser) {
        showDesktopDragHandles = (currentUser.profile || {})
          .showDesktopDragHandles;
      } else if (cookies.has('showDesktopDragHandles')) {
        showDesktopDragHandles = true;
      } else {
        showDesktopDragHandles = false;
      }
      if (Utils.isMiniScreen() || showDesktopDragHandles) {
        $swimlanesDom.sortable({
          handle: '.js-swimlane-header-handle',
        });
      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
        $swimlanesDom.sortable({
          handle: '.swimlane-header',
        });
      }

      // Disable drag-dropping if the current user is not a board member
      $swimlanesDom.sortable('option', 'disabled', !userIsMember());
    });

    function userIsMember() {
      return (
        Meteor.user() &&
        Meteor.user().isBoardMember() &&
        !Meteor.user().isCommentOnly()
      );
    }

    // If there is no data in the board (ie, no lists) we autofocus the list
    // creation form by clicking on the corresponding element.
    const currentBoard = Boards.findOne(Session.get('currentBoard'));
    if (userIsMember() && currentBoard.lists().count() === 0) {
      boardComponent.openNewListForm();
    }
  },

  isViewSwimlanes() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
    } else {
      return cookies.get('boardView') === 'board-view-swimlanes';
    }
  },

  isViewLists() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-lists';
    } else {
      return cookies.get('boardView') === 'board-view-lists';
    }
  },

  isViewCalendar() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-cal';
    } else {
      return cookies.get('boardView') === 'board-view-cal';
    }
  },

  openNewListForm() {
    if (this.isViewSwimlanes()) {
      this.childComponents('swimlane')[0]
        .childComponents('addListAndSwimlaneForm')[0]
        .open();
    } else if (this.isViewLists()) {
      this.childComponents('listsGroup')[0]
        .childComponents('addListForm')[0]
        .open();
    }
  },
  events() {
    return [
      {
        // XXX The board-overlay div should probably be moved to the parent
        // component.
        mouseup() {
          if (this._isDragging) {
            this._isDragging = false;
          }
        },
      },
    ];
  },

  // XXX Flow components allow us to avoid creating these two setter methods by
  // exposing a public API to modify the component state. We need to investigate
  // best practices here.
  setIsDragging(bool) {
    this.draggingActive.set(bool);
  },

  scrollLeft(position = 0) {
    const swimlanes = this.$('.js-swimlanes');
    swimlanes &&
      swimlanes.animate({
        scrollLeft: position,
      });
  },

  scrollTop(position = 0) {
    const swimlanes = this.$('.js-swimlanes');
    swimlanes &&
      swimlanes.animate({
        scrollTop: position,
      });
  },
}).register('boardBody');

BlazeComponent.extendComponent({
  onRendered() {
    this.autorun(function() {
      $('#calendar-view').fullCalendar('refetchEvents');
    });
  },
  calendarOptions() {
    return {
      id: 'calendar-view',
      defaultView: 'agendaDay',
      editable: true,
      timezone: 'local',
      header: {
        left: 'title   today prev,next',
        center:
          'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
        right: '',
      },
      // height: 'parent', nope, doesn't work as the parent might be small
      height: 'auto',
      /* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
      navLinks: true,
      nowIndicator: true,
      businessHours: {
        // days of week. an array of zero-based day of week integers (0=Sunday)
        dow: [1, 2, 3, 4, 5], // Monday - Friday
        start: '8:00',
        end: '18:00',
      },
      locale: TAPi18n.getLanguage(),
      events(start, end, timezone, callback) {
        const currentBoard = Boards.findOne(Session.get('currentBoard'));
        const events = [];
        const pushEvent = function(card, title, start, end, extraCls) {
          start = start || card.startAt;
          end = end || card.endAt;
          title = title || card.title;
          const className =
            (extraCls ? `${extraCls} ` : '') +
            (card.color ? `calendar-event-${card.color}` : '');
          events.push({
            id: card._id,
            title,
            start,
            end: end || card.endAt,
            allDay:
              Math.abs(end.getTime() - start.getTime()) / 1000 === 24 * 3600,
            url: FlowRouter.url('card', {
              boardId: currentBoard._id,
              slug: currentBoard.slug,
              cardId: card._id,
            }),
            className,
          });
        };
        currentBoard
          .cardsInInterval(start.toDate(), end.toDate())
          .forEach(card => {
            pushEvent(card);
          });
        currentBoard
          .cardsDueInBetween(start.toDate(), end.toDate())
          .forEach(card => {
            pushEvent(
              card,
              `${card.title} ${TAPi18n.__('card-due')}`,
              card.dueAt,
              new Date(card.dueAt.getTime() + 36e5),
            );
          });
        events.sort(function(first, second) {
          return first.id > second.id ? 1 : -1;
        });
        callback(events);
      },
      eventResize(event, delta, revertFunc) {
        let isOk = false;
        const card = Cards.findOne(event.id);

        if (card) {
          card.setEnd(event.end.toDate());
          isOk = true;
        }
        if (!isOk) {
          revertFunc();
        }
      },
      eventDrop(event, delta, revertFunc) {
        let isOk = false;
        const card = Cards.findOne(event.id);
        if (card) {
          // TODO: add a flag for allDay events
          if (!event.allDay) {
            card.setStart(event.start.toDate());
            card.setEnd(event.end.toDate());
            isOk = true;
          }
        }
        if (!isOk) {
          revertFunc();
        }
      },
    };
  },
  isViewCalendar() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-cal';
    } else {
      return cookies.get('boardView') === 'board-view-cal';
    }
  },
}).register('calendarView');
<commit_msg>Sort callback should return 0 if values are equal<commit_after>import { Cookies } from 'meteor/ostrio:cookies';
const cookies = new Cookies();
const subManager = new SubsManager();
const { calculateIndex } = Utils;
const swimlaneWhileSortingHeight = 150;

BlazeComponent.extendComponent({
  onCreated() {
    this.isBoardReady = new ReactiveVar(false);

    // The pattern we use to manually handle data loading is described here:
    // https://kadira.io/academy/meteor-routing-guide/content/subscriptions-and-data-management/using-subs-manager
    // XXX The boardId should be readed from some sort the component "props",
    // unfortunatly, Blaze doesn't have this notion.
    this.autorun(() => {
      const currentBoardId = Session.get('currentBoard');
      if (!currentBoardId) return;
      const handle = subManager.subscribe('board', currentBoardId, false);
      Tracker.nonreactive(() => {
        Tracker.autorun(() => {
          this.isBoardReady.set(handle.ready());
        });
      });
    });
  },

  onlyShowCurrentCard() {
    return Utils.isMiniScreen() && Session.get('currentCard');
  },

  goHome() {
    FlowRouter.go('home');
  },
}).register('board');

BlazeComponent.extendComponent({
  onCreated() {
    this.showOverlay = new ReactiveVar(false);
    this.draggingActive = new ReactiveVar(false);
    this._isDragging = false;
    // Used to set the overlay
    this.mouseHasEnterCardDetails = false;

    // fix swimlanes sort field if there are null values
    const currentBoardData = Boards.findOne(Session.get('currentBoard'));
    const nullSortSwimlanes = currentBoardData.nullSortSwimlanes();
    if (nullSortSwimlanes.count() > 0) {
      const swimlanes = currentBoardData.swimlanes();
      let count = 0;
      swimlanes.forEach(s => {
        Swimlanes.update(s._id, {
          $set: {
            sort: count,
          },
        });
        count += 1;
      });
    }

    // fix lists sort field if there are null values
    const nullSortLists = currentBoardData.nullSortLists();
    if (nullSortLists.count() > 0) {
      const lists = currentBoardData.lists();
      let count = 0;
      lists.forEach(l => {
        Lists.update(l._id, {
          $set: {
            sort: count,
          },
        });
        count += 1;
      });
    }
  },
  onRendered() {
    const boardComponent = this;
    const $swimlanesDom = boardComponent.$('.js-swimlanes');

    $swimlanesDom.sortable({
      tolerance: 'pointer',
      appendTo: '.board-canvas',
      helper(evt, item) {
        const helper = $(`
`);
        helper.append(item.clone());
        // Also grab the list of lists of cards
        const list = item.next();
        helper.append(list.clone());
        return helper;
      },
      items: '.swimlane:not(.placeholder)',
      placeholder: 'swimlane placeholder',
      distance: 7,
      start(evt, ui) {
        const listDom = ui.placeholder.next('.js-swimlane');
        const parentOffset = ui.item.parent().offset();

        ui.placeholder.height(ui.helper.height());
        EscapeActions.executeUpTo('popup-close');
        listDom.addClass('moving-swimlane');
        boardComponent.setIsDragging(true);

        ui.placeholder.insertAfter(ui.placeholder.next());
        boardComponent.origPlaceholderIndex = ui.placeholder.index();

        // resize all swimlanes + headers to be a total of 150 px per row
        // this could be achieved by setIsDragging(true) but we want immediate
        // result
        ui.item
          .siblings('.js-swimlane')
          .css('height', `${swimlaneWhileSortingHeight - 26}px`);

        // set the new scroll height after the resize and insertion of
        // the placeholder. We want the element under the cursor to stay
        // at the same place on the screen
        ui.item.parent().get(0).scrollTop =
          ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;
      },
      beforeStop(evt, ui) {
        const parentOffset = ui.item.parent().offset();
        const siblings = ui.item.siblings('.js-swimlane');
        siblings.css('height', '');

        // compute the new scroll height after the resize and removal of
        // the placeholder
        const scrollTop =
          ui.placeholder.get(0).offsetTop + parentOffset.top - evt.pageY;

        // then reset the original view of the swimlane
        siblings.removeClass('moving-swimlane');

        // and apply the computed scrollheight
        ui.item.parent().get(0).scrollTop = scrollTop;
      },
      stop(evt, ui) {
        // To attribute the new index number, we need to get the DOM element
        // of the previous and the following card -- if any.
        const prevSwimlaneDom = ui.item.prevAll('.js-swimlane').get(0);
        const nextSwimlaneDom = ui.item.nextAll('.js-swimlane').get(0);
        const sortIndex = calculateIndex(prevSwimlaneDom, nextSwimlaneDom, 1);

        $swimlanesDom.sortable('cancel');
        const swimlaneDomElement = ui.item.get(0);
        const swimlane = Blaze.getData(swimlaneDomElement);

        Swimlanes.update(swimlane._id, {
          $set: {
            sort: sortIndex.base,
          },
        });

        boardComponent.setIsDragging(false);
      },
      sort(evt, ui) {
        // get the mouse position in the sortable
        const parentOffset = ui.item.parent().offset();
        const cursorY =
          evt.pageY - parentOffset.top + ui.item.parent().scrollTop();

        // compute the intended index of the placeholder (we need to skip the
        // slots between the headers and the list of cards)
        const newplaceholderIndex = Math.floor(
          cursorY / swimlaneWhileSortingHeight,
        );
        let destPlaceholderIndex = (newplaceholderIndex + 1) * 2;

        // if we are scrolling far away from the bottom of the list
        if (destPlaceholderIndex >= ui.item.parent().get(0).childElementCount) {
          destPlaceholderIndex = ui.item.parent().get(0).childElementCount - 1;
        }

        // update the placeholder position in the DOM tree
        if (destPlaceholderIndex !== ui.placeholder.index()) {
          if (destPlaceholderIndex < boardComponent.origPlaceholderIndex) {
            ui.placeholder.insertBefore(
              ui.placeholder
                .siblings()
                .slice(destPlaceholderIndex - 2, destPlaceholderIndex - 1),
            );
          } else {
            ui.placeholder.insertAfter(
              ui.placeholder
                .siblings()
                .slice(destPlaceholderIndex - 1, destPlaceholderIndex),
            );
          }
        }
      },
    });

    this.autorun(() => {
      let showDesktopDragHandles = false;
      currentUser = Meteor.user();
      if (currentUser) {
        showDesktopDragHandles = (currentUser.profile || {})
          .showDesktopDragHandles;
      } else if (cookies.has('showDesktopDragHandles')) {
        showDesktopDragHandles = true;
      } else {
        showDesktopDragHandles = false;
      }
      if (Utils.isMiniScreen() || showDesktopDragHandles) {
        $swimlanesDom.sortable({
          handle: '.js-swimlane-header-handle',
        });
      } else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
        $swimlanesDom.sortable({
          handle: '.swimlane-header',
        });
      }

      // Disable drag-dropping if the current user is not a board member
      $swimlanesDom.sortable('option', 'disabled', !userIsMember());
    });

    function userIsMember() {
      return (
        Meteor.user() &&
        Meteor.user().isBoardMember() &&
        !Meteor.user().isCommentOnly()
      );
    }

    // If there is no data in the board (ie, no lists) we autofocus the list
    // creation form by clicking on the corresponding element.
    const currentBoard = Boards.findOne(Session.get('currentBoard'));
    if (userIsMember() && currentBoard.lists().count() === 0) {
      boardComponent.openNewListForm();
    }
  },

  isViewSwimlanes() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-swimlanes';
    } else {
      return cookies.get('boardView') === 'board-view-swimlanes';
    }
  },

  isViewLists() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-lists';
    } else {
      return cookies.get('boardView') === 'board-view-lists';
    }
  },

  isViewCalendar() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-cal';
    } else {
      return cookies.get('boardView') === 'board-view-cal';
    }
  },

  openNewListForm() {
    if (this.isViewSwimlanes()) {
      this.childComponents('swimlane')[0]
        .childComponents('addListAndSwimlaneForm')[0]
        .open();
    } else if (this.isViewLists()) {
      this.childComponents('listsGroup')[0]
        .childComponents('addListForm')[0]
        .open();
    }
  },
  events() {
    return [
      {
        // XXX The board-overlay div should probably be moved to the parent
        // component.
        mouseup() {
          if (this._isDragging) {
            this._isDragging = false;
          }
        },
      },
    ];
  },

  // XXX Flow components allow us to avoid creating these two setter methods by
  // exposing a public API to modify the component state. We need to investigate
  // best practices here.
  setIsDragging(bool) {
    this.draggingActive.set(bool);
  },

  scrollLeft(position = 0) {
    const swimlanes = this.$('.js-swimlanes');
    swimlanes &&
      swimlanes.animate({
        scrollLeft: position,
      });
  },

  scrollTop(position = 0) {
    const swimlanes = this.$('.js-swimlanes');
    swimlanes &&
      swimlanes.animate({
        scrollTop: position,
      });
  },
}).register('boardBody');

BlazeComponent.extendComponent({
  onRendered() {
    this.autorun(function() {
      $('#calendar-view').fullCalendar('refetchEvents');
    });
  },
  calendarOptions() {
    return {
      id: 'calendar-view',
      defaultView: 'agendaDay',
      editable: true,
      timezone: 'local',
      header: {
        left: 'title   today prev,next',
        center:
          'agendaDay,listDay,timelineDay agendaWeek,listWeek,timelineWeek month,timelineMonth timelineYear',
        right: '',
      },
      // height: 'parent', nope, doesn't work as the parent might be small
      height: 'auto',
      /* TODO: lists as resources: https://fullcalendar.io/docs/vertical-resource-view */
      navLinks: true,
      nowIndicator: true,
      businessHours: {
        // days of week. an array of zero-based day of week integers (0=Sunday)
        dow: [1, 2, 3, 4, 5], // Monday - Friday
        start: '8:00',
        end: '18:00',
      },
      locale: TAPi18n.getLanguage(),
      events(start, end, timezone, callback) {
        const currentBoard = Boards.findOne(Session.get('currentBoard'));
        const events = [];
        const pushEvent = function(card, title, start, end, extraCls) {
          start = start || card.startAt;
          end = end || card.endAt;
          title = title || card.title;
          const className =
            (extraCls ? `${extraCls} ` : '') +
            (card.color ? `calendar-event-${card.color}` : '');
          events.push({
            id: card._id,
            title,
            start,
            end: end || card.endAt,
            allDay:
              Math.abs(end.getTime() - start.getTime()) / 1000 === 24 * 3600,
            url: FlowRouter.url('card', {
              boardId: currentBoard._id,
              slug: currentBoard.slug,
              cardId: card._id,
            }),
            className,
          });
        };
        currentBoard
          .cardsInInterval(start.toDate(), end.toDate())
          .forEach(card => {
            pushEvent(card);
          });
        currentBoard
          .cardsDueInBetween(start.toDate(), end.toDate())
          .forEach(card => {
            pushEvent(
              card,
              `${card.title} ${TAPi18n.__('card-due')}`,
              card.dueAt,
              new Date(card.dueAt.getTime() + 36e5),
            );
          });
        events.sort((first, second) => {
          return first.id === second.id ? 0 : first.id > second.id ? 1 : -1;
        });
        callback(events);
      },
      eventResize(event, delta, revertFunc) {
        let isOk = false;
        const card = Cards.findOne(event.id);

        if (card) {
          card.setEnd(event.end.toDate());
          isOk = true;
        }
        if (!isOk) {
          revertFunc();
        }
      },
      eventDrop(event, delta, revertFunc) {
        let isOk = false;
        const card = Cards.findOne(event.id);
        if (card) {
          // TODO: add a flag for allDay events
          if (!event.allDay) {
            card.setStart(event.start.toDate());
            card.setEnd(event.end.toDate());
            isOk = true;
          }
        }
        if (!isOk) {
          revertFunc();
        }
      },
    };
  },
  isViewCalendar() {
    currentUser = Meteor.user();
    if (currentUser) {
      return (currentUser.profile || {}).boardView === 'board-view-cal';
    } else {
      return cookies.get('boardView') === 'board-view-cal';
    }
  },
}).register('calendarView');
<|endoftext|>