/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * 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.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, regexp: true,
indent: 4, maxerr: 50 */
/*global define, $, brackets, PathUtils */

/* Functions for working with extension packages */

define(function (require, exports, module) {
    "use strict";

    var AppInit              = require("utils/AppInit"),
        FileUtils            = require("file/FileUtils"),
        StringUtils          = require("utils/StringUtils"),
        Strings              = require("strings"),
        ExtensionLoader      = require("utils/ExtensionLoader"),
        NodeConnection       = require("utils/NodeConnection");

    var Errors = {
        ERROR_LOADING: "ERROR_LOADING",
        MALFORMED_URL: "MALFORMED_URL",
        UNSUPPORTED_PROTOCOL: "UNSUPPORTED_PROTOCOL"
    };

    /**
     * @const
     * Amount of time to wait before automatically rejecting the connection
     * deferred. If we hit this timeout, we'll never have a node connection
     * for the installer in this run of Brackets.
     */
    var NODE_CONNECTION_TIMEOUT = 30000; // 30 seconds - TODO: share with StaticServer?

    /**
     * @private
     * @type{jQuery.Deferred.<NodeConnection>}
     * A deferred which is resolved with a NodeConnection or rejected if
     * we are unable to connect to Node.
     */
    var _nodeConnectionDeferred = $.Deferred();

    /**
     * @type {number} Used to generate unique download ids
     */
    var _uniqueId = 0;

    /**
     * TODO: can this go away now that we never call it directly?
     *
     * Validates the package at the given path. The actual validation is
     * handled by the Node server.
     *
     * The promise is resolved with an object:
     * { errors: Array.<{string}>, metadata: { name:string, version:string, ... } }
     * metadata is pulled straight from package.json and will be undefined
     * if there are errors or null if the extension did not include package.json.
     *
     * @param {string} Absolute path to the package zip file
     * @param {{requirePackageJSON: ?boolean}} validation options
     * @return {$.Promise} A promise that is resolved with information about the package
     */
    function validate(path, options) {
        var d = new $.Deferred();
        _nodeConnectionDeferred
            .done(function (nodeConnection) {
                if (nodeConnection.connected()) {
                    nodeConnection.domains.extensionManager.validate(path, options)
                        .done(function (result) {
                            d.resolve({
                                errors: result.errors,
                                metadata: result.metadata
                            });
                        })
                        .fail(function (error) {
                            d.reject(error);
                        });
                } else {
                    d.reject();
                }
            })
            .fail(function (error) {
                d.reject(error);
            });
        return d.promise();
    }

    /**
     * Validates and installs the package at the given path. Validation and
     * installation is handled by the Node process.
     *
     * The extension will be installed into the user's extensions directory.
     * If the user already has the extension installed, it will instead go
     * into their disabled extensions directory.
     *
     * The promise is resolved with an object:
     * { errors: Array.<{string}>, metadata: { name:string, version:string, ... },
     * disabledReason:string, installedTo:string, commonPrefix:string }
     * metadata is pulled straight from package.json and is likely to be undefined
     * if there are errors. It is null if there was no package.json.
     *
     * disabledReason is either null or the reason the extension was installed disabled.
     *
     * @param {string} path Absolute path to the package zip file
     * @param {?string} nameHint Hint for the extension folder's name (used in favor of
     *          path's filename if present, and if no package metadata present).
     * @return {$.Promise} A promise that is resolved with information about the package
     *          (which may include errors, in which case the extension was disabled), or
     *          rejected with an error object.
     */
    function install(path, nameHint) {
        var d = new $.Deferred();
        _nodeConnectionDeferred
            .done(function (nodeConnection) {
                if (nodeConnection.connected()) {
                    var destinationDirectory = ExtensionLoader.getUserExtensionPath();
                    var disabledDirectory = destinationDirectory.replace(/\/user$/, "/disabled");
                    nodeConnection.domains.extensionManager.install(path, destinationDirectory, {
                        disabledDirectory: disabledDirectory,
                        apiVersion: brackets.metadata.apiVersion,
                        nameHint: nameHint
                    })
                        .done(function (result) {
                            // If there were errors or the extension is disabled, we don't
                            // try to load it so we're ready to return
                            if (result.errors.length > 0 || result.disabledReason) {
                                d.resolve(result);
                            } else {
                                // This was a new extension and everything looked fine.
                                // We load it into Brackets right away.
                                ExtensionLoader.loadExtension(result.name, {
                                    baseUrl: result.installedTo
                                }, "main").then(function () {
                                    d.resolve(result);
                                }, function () {
                                    d.reject(Errors.ERROR_LOADING);
                                });
                            }
                        })
                        .fail(function (error) {
                            d.reject(error);
                        });
                } else {
                    d.reject();
                }
            })
            .fail(function (error) {
                d.reject(error);
            });
        return d.promise();
    }

    /**
     * Special case handling to make the common case of downloading from GitHub easier; modifies 'urlInfo' as
     * needed. Converts a bare GitHub repo URL to the corresponding master ZIP URL; or if given a direct
     * master ZIP URL already, sets a nicer download filename (both cases use the repo name).
     *
     * @param {{url:string, parsed:Array.<string>, filenameHint:string}} urlInfo
     */
    function githubURLFilter(urlInfo) {
        if (urlInfo.parsed.hostname === "github.com" || urlInfo.parsed.hostname === "www.github.com") {
            // Is it a URL to the root of a repo? (/user/repo)
            var match = /^\/[^\/?]+\/([^\/?]+)(\/?)$/.exec(urlInfo.parsed.pathname);
            if (match) {
                if (!match[2]) {
                    urlInfo.url += "/";
                }
                urlInfo.url += "archive/master.zip";
                urlInfo.filenameHint = match[1] + ".zip";

            } else {
                // Is it a URL directly to the repo's 'master.zip'? (/user/repo/archive/master.zip)
                match = /^\/[^\/?]+\/([^\/?]+)\/archive\/master.zip$/.exec(urlInfo.parsed.pathname);
                if (match) {
                    urlInfo.filenameHint = match[1] + ".zip";
                }
            }
        }
    }

    /**
     * Downloads from the given URL to a temporary location. On success, resolves with the path of the
     * downloaded file (typically in a temp folder) and a hint for the real filename. On failure, rejects
     * with an error object.
     *
     * @param {string} url URL of the file to be downloaded
     * @param {number} downloadId Unique number to identify this request
     * @return {$.Promise}
     */
    function download(url, downloadId) {
        var d = new $.Deferred();
        _nodeConnectionDeferred
            .done(function (connection) {
                if (connection.connected()) {
                    // Validate URL
                    // TODO: PathUtils fails to parse URLs that are missing the protocol part (e.g. starts immediately with "www...")
                    var parsed = PathUtils.parseUrl(url);
                    if (!parsed.hostname) {  // means PathUtils failed to parse at all
                        d.reject(Errors.MALFORMED_URL);
                        return d.promise();
                    }
                    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
                        d.reject(Errors.UNSUPPORTED_PROTOCOL);
                        return d.promise();
                    }

                    var urlInfo = { url: url, parsed: parsed, filenameHint: parsed.filename };
                    githubURLFilter(urlInfo);

                    // Decide download destination
                    var filename = urlInfo.filenameHint;
                    filename = filename.replace(/[^a-zA-Z0-9_\- \(\)\.]/g, "_"); // make sure it's a valid filename
                    if (!filename) {  // in case of URL ending in "/"
                        filename = "extension.zip";
                    }

                    // Download the bits (using Node since brackets-shell doesn't support binary file IO)
                    var r = connection.domains.extensionManager.downloadFile(downloadId, urlInfo.url);
                    r.done(function (result) {
                        d.resolve({ localPath: result, filenameHint: urlInfo.filenameHint });
                    }).fail(function (err) {
                        d.reject(err);
                    });

                } else {
                    d.reject();
                }
            })
            .fail(function (error) {
                d.reject(error);
            });
        return d.promise();
    }

    /**
     * Attempts to synchronously cancel the given pending download. This may not be possible, e.g.
     * if the download has already finished.
     *
     * @param {number} downloadId Identifier previously passed to download()
     */
    function cancelDownload(downloadId) {
        // TODO: if we're still waiting on the NodeConnection, how do we cancel?
        console.assert(_nodeConnectionDeferred.isResolved());
        _nodeConnectionDeferred.done(function (connection) {
            connection.domains.extensionManager.abortDownload(downloadId);
        });
    }

    /**
     * On success, resolves with an extension metadata object; at that point, the extension has already
     * started running in Brackets. On failure (including validation errors), rejects with an error object.
     *
     * An error object consists of either a string error code OR an array where the first entry is the error
     * code and the remaining entries are further info. The error code string is one of either
     * ExtensionsDomain.Errors or Package.Errors. Use formatError() to convert an error object to a friendly,
     * localized error message.
     *
     * The returned cancel() function will *attempt* to cancel installation, but it is not guaranteed to
     * succeed. If cancel() succeeds, the Promise is rejected with a CANCELED error code. If we're unable
     * to cancel, the Promise is resolved or rejected normally, as if cancel() had never been called.
     *
     * @return {{promise: $.Promise, cancel: function():boolean}}
     */
    function installFromURL(url) {
        var STATE_DOWNLOADING = 1,
            STATE_INSTALLING = 2,
            STATE_SUCCEEDED = 3,
            STATE_FAILED = 4;

        var d = new $.Deferred();
        var state = STATE_DOWNLOADING;

        var downloadId = (_uniqueId++);
        download(url, downloadId)
            .done(function (downloadResult) {
                state = STATE_INSTALLING;

                install(downloadResult.localPath, downloadResult.filenameHint)
                    .done(function (result) {
                        if (result.errors && result.errors.length > 0) {
                            // Validation errors - for now, only return the first one
                            state = STATE_FAILED;
                            d.reject(result.errors[0]);
                        } else if (result.disabledReason) {
                            // Extension valid but left disabled (wrong API version, extension name collision, etc.)
                            state = STATE_FAILED;
                            d.reject(result.disabledReason);
                        } else {
                            // Success! Extension is now running in Brackets
                            state = STATE_SUCCEEDED;
                            d.resolve(result.metadata);
                        }
                    })
                    .fail(function (err) {
                        // File IO errors, internal error in install()/validate(), or extension startup crashed
                        state = STATE_FAILED;
                        d.reject(err);  // TODO: needs to be err.message ?
                    })
                    .always(function () {
                        // Whether success or failure, we can delete the original downloaded ZIP file now
                        brackets.fs.unlink(downloadResult.localPath, function (err) {
                            // ignore errors
                        });
                    });
            })
            .fail(function (err) {
                // Download error (the Node-side download code cleans up any partial ZIP file)
                state = STATE_FAILED;
                d.reject(err);
            });

        return {
            promise: d.promise(),
            cancel: function () {
                if (state === STATE_DOWNLOADING) {
                    // This will trigger download()'s fail() handler with CANCELED as the err code
                    cancelDownload(downloadId);
                }
                // Else it's too late to cancel; we'll continue on through the done() chain and emit
                // a success result (calling done() handlers) if all else goes well.
            }
        };
    }

    /**
     * Converts an error object as returned by install() or installFromURL() into a flattened, localized string.
     * @param {string|Array.<string>} error
     * @return {string}
     */
    function formatError(error) {
        function localize(key) {
            return Strings[key] || Strings.UNKNOWN_ERROR;
        }

        if (Array.isArray(error)) {
            error[0] = localize(error[0]);
            return StringUtils.format.apply(window, error);
        } else {
            return localize(error);
        }
    }

    /**
     * Allows access to the deferred that manages the node connection. This
     * is *only* for unit tests. Messing with this not in testing will
     * potentially break everything.
     *
     * @private
     * @return {jQuery.Deferred} The deferred that manages the node connection
     */
    function _getNodeConnectionDeferred() {
        return _nodeConnectionDeferred;
    }

    // Initializes node connection
    // TODO: duplicates code from StaticServer
    // TODO: can this be done lazily?
    AppInit.appReady(function () {
        // Start up the node connection, which is held in the
        // _nodeConnectionDeferred module variable. (Use
        // _nodeConnectionDeferred.done() to access it.
        var connectionTimeout = setTimeout(function () {
            console.error("[Extensions] Timed out while trying to connect to node");
            _nodeConnectionDeferred.reject();
        }, NODE_CONNECTION_TIMEOUT);

        var _nodeConnection = new NodeConnection();
        _nodeConnection.connect(true).then(function () {
            var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/ExtensionManagerDomain";

            _nodeConnection.loadDomains(domainPath, true)
                .then(
                    function () {
                        clearTimeout(connectionTimeout);
                        _nodeConnectionDeferred.resolve(_nodeConnection);
                    },
                    function () { // Failed to connect
                        console.error("[Extensions] Failed to connect to node", arguments);
                        clearTimeout(connectionTimeout);
                        _nodeConnectionDeferred.reject();
                    }
                );
        });
    });

    // For unit tests only
    exports._getNodeConnectionDeferred = _getNodeConnectionDeferred;

    exports.installFromURL = installFromURL;
    exports.validate = validate;
    exports.install = install;
    exports.formatError = formatError;
});


it("should only allow certain characters in the name", function () {

var validateName = packageValidator.__get__("validateName");

REPORTER = dot

test:
	@NODE_ENV=test ./node_modules/.bin/mocha --recursive --reporter $(REPORTER) --ignore-leaks --timeout 3000

test-cov:
	@NODE_ENV=test ./node_modules/.bin/mocha --require blanket --recursive --ignore-leaks --timeout 3000 -R travis-cov

test-cov-html:
	@NODE_ENV=test ./node_modules/.bin/mocha --require blanket --recursive --ignore-leaks --timeout 3000 -R html-cov > coverage.html

.PHONY: test test-cov test-cov-html


-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);

-moz-transition:    -moz-transform 0.25s cubic-bezier(0,0,0.25,1);

.leaflet-hack {
width: 1px;
height: 1px;
}

/*
 * L.GridLayer is used as base class for grid-like layers like TileLayer.
 */

L.GridLayer = L.Layer.extend({

	options: {
		pane: 'tilePane',

		tileSize: 256,
		opacity: 1,

		unloadInvisibleTiles: L.Browser.mobile,
		updateWhenIdle: L.Browser.mobile,
		updateInterval: 150

		/*
		minZoom: <Number>,
		maxZoom: <Number>,
		attribution: <String>,
		zIndex: <Number>,
		bounds: <LatLngBounds>
		*/
	},

	initialize: function (options) {
		options = L.setOptions(this, options);
	},

	onAdd: function () {
		this._initContainer();

		if (!this.options.updateWhenIdle) {
			// update tiles on move, but not more often than once per given interval
			this._update = L.Util.throttle(this._update, this.options.updateInterval, this);
		}

		this._reset();
		this._update();
	},

	beforeAdd: function (map) {
		map._addZoomLimit(this);
	},

	onRemove: function (map) {
		this._clearBgBuffer();
		L.DomUtil.remove(this._container);

		map._removeZoomLimit(this);

		this._container = null;
	},

	bringToFront: function () {
		if (this._map) {
			L.DomUtil.toFront(this._container);
			this._setAutoZIndex(Math.max);
		}
		return this;
	},

	bringToBack: function () {
		if (this._map) {
			L.DomUtil.toBack(this._container);
			this._setAutoZIndex(Math.min);
		}
		return this;
	},

	getAttribution: function () {
		return this.options.attribution;
	},

	getContainer: function () {
		return this._container;
	},

	setOpacity: function (opacity) {
		this.options.opacity = opacity;

		if (this._map) {
			this._updateOpacity();
		}
		return this;
	},

	setZIndex: function (zIndex) {
		this.options.zIndex = zIndex;
		this._updateZIndex();

		return this;
	},

	redraw: function () {
		if (this._map) {
			this._reset({hard: true});
			this._update();
		}
		return this;
	},

	getEvents: function () {
		var events = {
			viewreset: this._reset,
			moveend: this._update
		};

		if (!this.options.updateWhenIdle) {
			events.move = this._update;
		}

		if (this._zoomAnimated) {
			events.zoomanimstart = this._startZoomAnim;
			events.zoomanim = this._animateZoom;
			events.zoomend = this._endZoomAnim;
		}

		return events;
	},

	_updateZIndex: function () {
		if (this._container && this.options.zIndex !== undefined) {
			this._container.style.zIndex = this.options.zIndex;
		}
	},

	_setAutoZIndex: function (compare) {
		// go through all other layers of the same pane, set zIndex to max + 1 (front) or min - 1 (back)

		var layers = this.getPane().children,
		    edgeZIndex = -compare(-Infinity, Infinity); // -Infinity for max, Infinity for min

		for (var i = 0, len = layers.length, zIndex; i < len; i++) {

			zIndex = layers[i].style.zIndex;

			if (layers[i] !== this._container && zIndex) {
				edgeZIndex = compare(edgeZIndex, +zIndex);
			}
		}

		if (isFinite(edgeZIndex)) {
			this.options.zIndex = edgeZIndex + compare(-1, 1);
			this._updateZIndex();
		}
	},

	_updateOpacity: function () {
		var opacity = this.options.opacity;

		if (L.Browser.ielt9) {
			// IE doesn't inherit filter opacity properly, so we're forced to set it on tiles
			for (var i in this._tiles) {
				L.DomUtil.setOpacity(this._tiles[i], opacity);
			}
		} else {
			L.DomUtil.setOpacity(this._container, opacity);
		}
	},

	_initContainer: function () {
		if (this._container) { return; }

		this._container = L.DomUtil.create('div', 'leaflet-layer');
		this._updateZIndex();

		if (this._zoomAnimated) {
			var className = 'leaflet-tile-container leaflet-zoom-animated';

			this._bgBuffer = L.DomUtil.create('div', className, this._container);
			this._tileContainer = L.DomUtil.create('div', className, this._container);

		} else {
			this._tileContainer = this._container;
		}

		if (this.options.opacity < 1) {
			this._updateOpacity();
		}

		this.getPane().appendChild(this._container);
	},

	_reset: function (e) {
		for (var key in this._tiles) {
			this.fire('tileunload', {
				tile: this._tiles[key]
			});
		}

		this._tiles = {};
		this._tilesToLoad = 0;
		this._tilesTotal = 0;

		this._tileContainer.innerHTML = '';

		if (this._zoomAnimated && e && e.hard) {
			this._clearBgBuffer();
		}

		this._tileNumBounds = this._getTileNumBounds();
		this._resetWrap();
	},

	_resetWrap: function () {
		var map = this._map,
		    crs = map.options.crs;

		if (crs.infinite) { return; }

		var tileSize = this._getTileSize();

		if (crs.wrapLng) {
			this._wrapLng = [
				Math.floor(map.project([0, crs.wrapLng[0]]).x / tileSize),
				Math.ceil(map.project([0, crs.wrapLng[1]]).x / tileSize)
			];
		}

		if (crs.wrapLat) {
			this._wrapLat = [
				Math.floor(map.project([crs.wrapLat[0], 0]).y / tileSize),
				Math.ceil(map.project([crs.wrapLat[1], 0]).y / tileSize)
			];
		}
	},

	_getTileSize: function () {
		return this.options.tileSize;
	},

	_update: function () {

		if (!this._map) { return; }

		var bounds = this._map.getPixelBounds(),
		    zoom = this._map.getZoom(),
		    tileSize = this._getTileSize();

		if (zoom > this.options.maxZoom ||
		    zoom < this.options.minZoom) { return; }

		// tile coordinates range for the current view
		var tileBounds = L.bounds(
			bounds.min.divideBy(tileSize).floor(),
			bounds.max.divideBy(tileSize).floor());

		this._addTiles(tileBounds);

		if (this.options.unloadInvisibleTiles) {
			this._removeOtherTiles(tileBounds);
		}
	},

	_addTiles: function (bounds) {
		var queue = [],
		    center = bounds.getCenter(),
		    zoom = this._map.getZoom();

		var j, i, coords;

		// create a queue of coordinates to load tiles from
		for (j = bounds.min.y; j <= bounds.max.y; j++) {
			for (i = bounds.min.x; i <= bounds.max.x; i++) {

				coords = new L.Point(i, j);
				coords.z = zoom;

				// add tile to queue if it's not in cache or out of bounds
				if (!(this._tileCoordsToKey(coords) in this._tiles) && this._isValidTile(coords)) {
					queue.push(coords);
				}
			}
		}

		var tilesToLoad = queue.length;

		if (tilesToLoad === 0) { return; }

		// if its the first batch of tiles to load
		if (!this._tilesToLoad) {
			this.fire('loading');
		}

		this._tilesToLoad += tilesToLoad;
		this._tilesTotal += tilesToLoad;

		// sort tile queue to load tiles in order of their distance to center
		queue.sort(function (a, b) {
			return a.distanceTo(center) - b.distanceTo(center);
		});

		// create DOM fragment to append tiles in one batch
		var fragment = document.createDocumentFragment();

		for (i = 0; i < tilesToLoad; i++) {
			this._addTile(queue[i], fragment);
		}
		this._tileContainer.appendChild(fragment);
	},

	_isValidTile: function (coords) {
		var crs = this._map.options.crs;

		if (!crs.infinite) {
			// don't load tile if it's out of bounds and not wrapped
			var bounds = this._tileNumBounds;
			if ((!crs.wrapLng && (coords.x < bounds.min.x || coords.x > bounds.max.x)) ||
			    (!crs.wrapLat && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; }
		}

		if (!this.options.bounds) { return true; }

		// don't load tile if it doesn't intersect the bounds in options
		var tileBounds = this._tileCoordsToBounds(coords);
		return L.latLngBounds(this.options.bounds).intersects(tileBounds);
	},

	// converts tile coordinates to its geographical bounds
	_tileCoordsToBounds: function (coords) {

		var map = this._map,
		    tileSize = this.options.tileSize,

		    nwPoint = coords.multiplyBy(tileSize),
		    sePoint = nwPoint.add([tileSize, tileSize]),

		    nw = map.wrapLatLng(map.unproject(nwPoint, coords.z)),
		    se = map.wrapLatLng(map.unproject(sePoint, coords.z));

		return new L.LatLngBounds(nw, se);
	},

	// converts tile coordinates to key for the tile cache
	_tileCoordsToKey: function (coords) {
		return coords.x + ':' + coords.y;
	},

	// converts tile cache key to coordiantes
	_keyToTileCoords: function (key) {
		var kArr = key.split(':'),
		    x = parseInt(kArr[0], 10),
		    y = parseInt(kArr[1], 10);

		return new L.Point(x, y);
	},

	// remove any present tiles that are off the specified bounds
	_removeOtherTiles: function (bounds) {
		for (var key in this._tiles) {
			if (!bounds.contains(this._keyToTileCoords(key))) {
				this._removeTile(key);
			}
		}
	},

	_removeTile: function (key) {
		var tile = this._tiles[key];

		L.DomUtil.remove(tile);

		delete this._tiles[key];

		this.fire('tileunload', {tile: tile});
	},

	_initTile: function (tile) {
		var size = this._getTileSize();

		L.DomUtil.addClass(tile, 'leaflet-tile');

		tile.style.width = size + 'px';
		tile.style.height = size + 'px';

		tile.onselectstart = L.Util.falseFn;
		tile.onmousemove = L.Util.falseFn;

		// update opacity on tiles in IE7-8 because of filter inheritance problems
		if (L.Browser.ielt9 && this.options.opacity < 1) {
			L.DomUtil.setOpacity(tile, this.options.opacity);
		}

		// without this hack, tiles disappear after zoom on Chrome for Android
		// https://github.com/Leaflet/Leaflet/issues/2078
		if (L.Browser.android && !L.Browser.android23) {
			tile.style.WebkitBackfaceVisibility = 'hidden';
		}
	},

	_addTile: function (coords, container) {
		var tilePos = this._getTilePos(coords);

		// wrap tile coords if necessary (depending on CRS)
		this._wrapCoords(coords);

		var tile = this.createTile(coords, L.bind(this._tileReady, this));

		this._initTile(tile);

		// if createTile is defined with a second argument ("done" callback),
		// we know that tile is async and will be ready later; otherwise
		if (this.createTile.length < 2) {
			// mark tile as ready, but delay one frame for opacity animation to happen
			setTimeout(L.bind(this._tileReady, this, null, tile), 0);
		}

		// we prefer translate over top/left because it fixes gaps between tiles in Safari,
		// but not 3d so that we don't create a HW-accelerated layer from each tile which is slow
		L.DomUtil.setPosition(tile, tilePos, true);

		// save tile in cache
		this._tiles[this._tileCoordsToKey(coords)] = tile;

		container.appendChild(tile);
		this.fire('tileloadstart', {tile: tile});
	},

	_tileReady: function (err, tile) {
		if (err) {
			this.fire('tileerror', {
				error: err,
				tile: tile
			});
		}

		L.DomUtil.addClass(tile, 'leaflet-tile-loaded');

		this.fire('tileload', {tile: tile});

		this._tilesToLoad--;

		if (this._tilesToLoad === 0) {
			this._visibleTilesReady();
		}
	},

	_visibleTilesReady: function () {
		this.fire('load');

		if (this._zoomAnimated) {
			// clear scaled tiles after all new tiles are loaded (for performance)
			clearTimeout(this._clearBgBufferTimer);
			this._clearBgBufferTimer = setTimeout(L.bind(this._clearBgBuffer, this), 300);
		}
	},

	_getTilePos: function (coords) {
		return coords
				.multiplyBy(this._getTileSize())
				.subtract(this._map.getPixelOrigin());
	},

	_wrapCoords: function (coords) {
		coords.x = this._wrapLng ? L.Util.wrapNum(coords.x, this._wrapLng) : coords.x;
		coords.y = this._wrapLat ? L.Util.wrapNum(coords.y, this._wrapLat) : coords.y;
	},

	// get the global tile coordinates range for the current zoom
	_getTileNumBounds: function () {
		var bounds = this._map.getPixelWorldBounds(),
			size = this._getTileSize();

		return bounds ? L.bounds(
				bounds.min.divideBy(size).floor(),
				bounds.max.divideBy(size).ceil().subtract([1, 1])) : null;
	},

	_startZoomAnim: function () {
		this._prepareBgBuffer();
		this._prevTranslate = this._translate;
		this._prevScale = this._scale;
	},

	_animateZoom: function (e) {
		// avoid stacking transforms by calculating cumulating translate/scale sequence
		this._translate = this._prevTranslate.multiplyBy(e.scale).add(e.origin.multiplyBy(1 - e.scale)).add(e.delta);
		this._scale = this._prevScale * e.scale;

		L.DomUtil.setTransform(this._bgBuffer, this._translate, this._scale);
	},

	_endZoomAnim: function () {
		var front = this._tileContainer;
		front.style.visibility = '';
		L.DomUtil.toFront(front); // bring to front
	},

	_clearBgBuffer: function () {
		var map = this._map;

		if (map && !map._animatingZoom && !map.touchZoom._zooming) {
			this._bgBuffer.innerHTML = '';
			L.DomUtil.setTransform(this._bgBuffer);
		}
	},

	_prepareBgBuffer: function () {

		var front = this._tileContainer,
		    bg = this._bgBuffer;

		if (this._abortLoading) {
			this._abortLoading();
		}

		if (this._tilesToLoad / this._tilesTotal > 0.5) {
			// if foreground layer doesn't have many tiles loaded,
			// keep the existing bg layer and just zoom it some more
			front.style.visibility = 'hidden';
			return;
		}

		// prepare the buffer to become the front tile pane
		bg.style.visibility = 'hidden';
		L.DomUtil.setTransform(bg);

		// switch out the current layer to be the new bg layer (and vice-versa)
		this._tileContainer = bg;
		this._bgBuffer = front;

		// reset bg layer transform info
		this._translate = new L.Point(0, 0);
		this._scale = 1;

		// prevent bg buffer from clearing right after zoom
		clearTimeout(this._clearBgBufferTimer);
	}
});

L.gridLayer = function (options) {
	return new L.GridLayer(options);
};


/*
 * L.Control.Attribution is used for displaying attribution on the map (added by default).
 */

L.Control.Attribution = L.Control.extend({
	options: {
		position: 'bottomright',
		prefix: 'Leaflet'
	},

	initialize: function (options) {
		L.setOptions(this, options);

		this._attributions = {};
	},

	onAdd: function (map) {
		this._container = L.DomUtil.create('div', 'leaflet-control-attribution');
		L.DomEvent.disableClickPropagation(this._container);

		// TODO ugly, refactor
		for (var i in map._layers) {
			if (map._layers[i].getAttribution) {
				this.addAttribution(map._layers[i].getAttribution());
			}
		}

		this._update();

		return this._container;
	},

	setPrefix: function (prefix) {
		this.options.prefix = prefix;
		this._update();
		return this;
	},

	addAttribution: function (text) {
		if (!text) { return; }

		if (!this._attributions[text]) {
			this._attributions[text] = 0;
		}
		this._attributions[text]++;

		this._update();

		return this;
	},

	removeAttribution: function (text) {
		if (!text) { return; }

		if (this._attributions[text]) {
			this._attributions[text]--;
			this._update();
		}

		return this;
	},

	_update: function () {
		if (!this._map) { return; }

		var attribs = [];

		for (var i in this._attributions) {
			if (this._attributions[i]) {
				attribs.push(i);
			}
		}

		var prefixAndAttribs = [];

		if (this.options.prefix) {
			prefixAndAttribs.push(this.options.prefix);
		}
		if (attribs.length) {
			prefixAndAttribs.push(attribs.join(', '));
		}

		this._container.innerHTML = prefixAndAttribs.join(' | ');
	}
});

L.Map.mergeOptions({
	attributionControl: true
});

L.Map.addInitHook(function () {
	if (this.options.attributionControl) {
		this.attributionControl = (new L.Control.Attribution()).addTo(this);
	}
});

L.control.attribution = function (options) {
	return new L.Control.Attribution(options);
};


<!DOCTYPE html>

	<title>Leaflet debug page</title>

	<link rel="stylesheet" href="../../dist/leaflet.css" />

	<link rel="stylesheet" href="../css/screen.css" />


/*
 * L.Path is the base class for all Leaflet vector layers like polygons and circles.
 */

L.Path = L.Layer.extend({

	options: {
		stroke: true,
		color: '#3388ff',
		weight: 3,
		opacity: 1,
		lineCap: 'round',
		lineJoin: 'round',
		// dashArray: null

		// fill: false
		// fillColor: same as color by default
		fillOpacity: 0.2,

		// className: ''
		clickable: true
	},

	onAdd: function () {
		this._renderer = this._map.getRenderer(this);
		this._renderer._initPath(this);

		// defined in children classes
		this._project();
		this._update();

		this._renderer._addPath(this);
	},

	onRemove: function () {
		this._renderer._removePath(this);
	},

	getEvents: function () {
		return {
			viewreset: this._project,
			moveend: this._update
		};
	},

	redraw: function () {
		if (this._map) {
			this._renderer._updatePath(this);
		}
		return this;
	},

	setStyle: function (style) {
		L.setOptions(this, style);
		if (this._renderer) {
			this._renderer._updateStyle(this);
		}
		return this;
	},

	bringToFront: function () {
		this._renderer._bringToFront(this);
		return this;
	},

	bringToBack: function () {
		this._renderer._bringToBack(this);
		return this;
	},

	_fireMouseEvent: function (e, type) {
		this._map._fireMouseEvent(this, e, type, true);
	},

	_clickTolerance: function () {
		// used when doing hit detection for Canvas layers
		return (this.options.stroke ? this.options.weight / 2 : 0) + (L.Browser.touch ? 10 : 0);
	}
});


this._bgBuffer = L.DomUtil.create('div', className, this._container);

this._tileContainer = L.DomUtil.create('div', className, this._container);


/*
 * L.DomUtil contains various utility functions for working with DOM.
 */

L.DomUtil = {
	get: function (id) {
		return typeof id === 'string' ? document.getElementById(id) : id;
	},

	getStyle: function (el, style) {

		var value = el.style[style] || (el.currentStyle && el.currentStyle[style]);

		if ((!value || value === 'auto') && document.defaultView) {
			var css = document.defaultView.getComputedStyle(el, null);
			value = css ? css[style] : null;
		}

		return value === 'auto' ? null : value;
	},

	create: function (tagName, className, container) {

		var el = document.createElement(tagName);
		el.className = className;

		if (container) {
			container.appendChild(el);
		}

		return el;
	},

	remove: function (el) {
		var parent = el.parentNode;
		if (parent) {
			parent.removeChild(el);
		}
	},

	toFront: function (el) {
		el.parentNode.appendChild(el);
	},

	toBack: function (el) {
		var parent = el.parentNode;
		parent.insertBefore(el, parent.firstChild);
	},

	hasClass: function (el, name) {
		if (el.classList !== undefined) {
			return el.classList.contains(name);
		}
		var className = L.DomUtil.getClass(el);
		return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
	},

	addClass: function (el, name) {
		if (el.classList !== undefined) {
			var classes = L.Util.splitWords(name);
			for (var i = 0, len = classes.length; i < len; i++) {
				el.classList.add(classes[i]);
			}
		} else if (!L.DomUtil.hasClass(el, name)) {
			var className = L.DomUtil.getClass(el);
			L.DomUtil.setClass(el, (className ? className + ' ' : '') + name);
		}
	},

	removeClass: function (el, name) {
		if (el.classList !== undefined) {
			el.classList.remove(name);
		} else {
			L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
		}
	},

	setClass: function (el, name) {
		if (el.className.baseVal === undefined) {
			el.className = name;
		} else {
			// in case of SVG element
			el.className.baseVal = name;
		}
	},

	getClass: function (el) {
		return el.className.baseVal === undefined ? el.className : el.className.baseVal;
	},

	setOpacity: function (el, value) {

		if ('opacity' in el.style) {
			el.style.opacity = value;

		} else if ('filter' in el.style) {

			var filter = false,
			    filterName = 'DXImageTransform.Microsoft.Alpha';

			// filters collection throws an error if we try to retrieve a filter that doesn't exist
			try {
				filter = el.filters.item(filterName);
			} catch (e) {
				// don't set opacity to 1 if we haven't already set an opacity,
				// it isn't needed and breaks transparent pngs.
				if (value === 1) { return; }
			}

			value = Math.round(value * 100);

			if (filter) {
				filter.Enabled = (value !== 100);
				filter.Opacity = value;
			} else {
				el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
			}
		}
	},

	testProp: function (props) {

		var style = document.documentElement.style;

		for (var i = 0; i < props.length; i++) {
			if (props[i] in style) {
				return props[i];
			}
		}
		return false;
	},

	setTransform: function (el, offset, scale, no3d) {
		no3d = no3d || !L.Browser.any3d,
		pos = offset || new L.Point(0, 0);

		el.style[L.DomUtil.TRANSFORM] =
			'translate' + (no3d ? '(' : '3d(') + pos.x + 'px,' + pos.y + 'px' + (no3d ? ')' : ',0)') +
			(scale ? ' scale(' + scale + ')' : '');
	},

	setPosition: function (el, point, no3d) { // (HTMLElement, Point[, Boolean])

		// jshint camelcase: false
		el._leaflet_pos = point;

		if (L.Browser.any3d) {
			L.DomUtil.setTransform(el, point, null, no3d);
		} else {
			el.style.left = point.x + 'px';
			el.style.top = point.y + 'px';
		}
	},

	getPosition: function (el) {
		// this method is only used for elements previously positioned using setPosition,
		// so it's safe to cache the position for performance

		// jshint camelcase: false
		return el._leaflet_pos;
	}
};

(function () {
	// prefix style property names

	L.DomUtil.TRANSFORM = L.DomUtil.testProp(
			['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);

	// webkitTransition comes first because some browser versions that drop vendor prefix don't do
	// the same for the transitionend event, in particular the Android 4.1 stock browser

	var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp(
			['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);

	L.DomUtil.TRANSITION_END =
			transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend';

	if ('onselectstart' in document) {
		L.DomUtil.disableTextSelection = function () {
			L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault);
		};
		L.DomUtil.enableTextSelection = function () {
			L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault);
		};

	} else {
		var userSelectProperty = L.DomUtil.testProp(
			['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);

		L.DomUtil.disableTextSelection = function () {
			if (userSelectProperty) {
				var style = document.documentElement.style;
				this._userSelect = style[userSelectProperty];
				style[userSelectProperty] = 'none';
			}
		};
		L.DomUtil.enableTextSelection = function () {
			if (userSelectProperty) {
				document.documentElement.style[userSelectProperty] = this._userSelect;
				delete this._userSelect;
			}
		};
	}

	L.DomUtil.disableImageDrag = function () {
		L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault);
	};
	L.DomUtil.enableImageDrag = function () {
		L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault);
	};
})();


/*
 * L.Browser handles different browser and feature detections for internal Leaflet use.
 */

(function () {

	var ua = navigator.userAgent.toLowerCase(),
	    doc = document.documentElement,

	    ie = 'ActiveXObject' in window,

	    webkit    = ua.indexOf('webkit') !== -1,
	    phantomjs = ua.indexOf('phantom') !== -1,
	    android23 = ua.search('android [23]') !== -1,

	    mobile = typeof orientation !== 'undefined',
	    msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent,
	    pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer,

	    ie3d = ie && ('transition' in doc.style),
	    webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
	    gecko3d = 'MozPerspective' in doc.style,
	    opera3d = 'OTransition' in doc.style;

	var retina = 'devicePixelRatio' in window && window.devicePixelRatio > 1;

	if (!retina && 'matchMedia' in window) {
		var matches = window.matchMedia('(min-resolution:144dpi)');
		retina = matches && matches.matches;
	}

	var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window ||
			(window.DocumentTouch && document instanceof window.DocumentTouch));

	L.Browser = {
		ie: ie,
		ielt9: ie && !document.addEventListener,
		webkit: webkit,
		gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie,
		android: ua.indexOf('android') !== -1,
		android23: android23,
		chrome: ua.indexOf('chrome') !== -1,

		ie3d: ie3d,
		webkit3d: webkit3d,
		gecko3d: gecko3d,
		opera3d: opera3d,
		any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs,

		mobile: mobile,
		mobileWebkit: mobile && webkit,
		mobileWebkit3d: mobile && webkit3d,
		mobileOpera: mobile && window.opera,

		touch: !!touch,
		msPointer: !!msPointer,
		pointer: !!pointer,

		retina: !!retina
	};

}());


<style>
	iframe {
		position: absolute;
		top: 50px;
		left: 50px;
		width: 500px;
		height: 500px;
	}
</style>
<iframe src="map.html">
</iframe>


// a pretty funny hack - Safari bugs out with HW-acceleration when you pan to the right

// and there are no elements in the top left corner of the tile container, this fixes it

var hack = L.DomUtil.create('div', 'leaflet-hack', this._tileContainer);


<!DOCTYPE html>

    <title>Leaflet debug page</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4.5/leaflet.css" />
    <link rel="stylesheet" href="../css/screen.css" />


node_modules
.DS_Store
tmp/**/*
.idea
.idea/**/*
*.iml
*.sublime-*
_site
dist/*.js
coverage/
*.js.html
index.html
.mailmap


describe("Projection.Mercator", function () {
	var p = L.Projection.Mercator;

	describe("#project", function () {
		it("projects a center point", function () {
			//edge cases
			expect(p.project(new L.LatLng(0, 0))).near(new L.Point(0, 0));
		});

		it("projects the northeast corner of the world", function () {
			expect(p.project(new L.LatLng(85.0840591556, 180))).near(new L.Point(20037508, 20037508));
		});

		it("projects the southwest corner of the world", function () {
			expect(p.project(new L.LatLng(-85.0840591556, -180))).near(new L.Point(-20037508, -20037508));
		});

		it("projects other points", function () {
			expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6413524));

			// from https://github.com/Leaflet/Leaflet/issues/1578
			expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001)))
			        .near(new L.Point(8918060.964088084, 6755099.410887127));
		});
	});

	describe("#unproject", function () {
		function pr(point) {
			return p.project(p.unproject(point));
		}

		it("unprojects a center point", function () {
			expect(pr(new L.Point(0, 0))).near(new L.Point(0, 0));
		});

		it("unprojects pi points", function () {
			expect(pr(new L.Point(-Math.PI, Math.PI))).near(new L.Point(-Math.PI, Math.PI));
			expect(pr(new L.Point(-Math.PI, -Math.PI))).near(new L.Point(-Math.PI, -Math.PI));

			expect(pr(new L.Point(0.523598775598, 1.010683188683))).near(new L.Point(0.523598775598, 1.010683188683));
		});

		it('unprojects other points', function () {
			// from https://github.com/Leaflet/Leaflet/issues/1578
			expect(pr(new L.Point(8918060.964088084, 6755099.410887127)));
		});
	});
});
describe("Projection.SphericalMercator", function () {
	var p = L.Projection.SphericalMercator;

	describe("#project", function () {
		it("projects a center point", function () {
			//edge cases
			expect(p.project(new L.LatLng(0, 0))).near(new L.Point(0, 0));
		});

		it("projects the northeast corner of the world", function () {
			expect(p.project(new L.LatLng(85.0511287798, 180))).near(new L.Point(20037508, 20037508));
		});

		it("projects the southwest corner of the world", function () {
			expect(p.project(new L.LatLng(-85.0511287798, -180))).near(new L.Point(-20037508, -20037508));
		});

		it("projects other points", function () {
			expect(p.project(new L.LatLng(50, 30))).near(new L.Point(3339584, 6446275));

			// from https://github.com/Leaflet/Leaflet/issues/1578
			expect(p.project(new L.LatLng(51.9371170300465, 80.11230468750001)))
					.near(new L.Point(8918060.96409, 6788763.38325));
		});
	});

	describe("#unproject", function () {
		function pr(point) {
			return p.project(p.unproject(point));
		}

		it("unprojects a center point", function () {
			expect(pr(new L.Point(0, 0))).near(new L.Point(0, 0));
		});

		it("unprojects pi points", function () {
			expect(pr(new L.Point(-Math.PI, Math.PI))).near(new L.Point(-Math.PI, Math.PI));
			expect(pr(new L.Point(-Math.PI, -Math.PI))).near(new L.Point(-Math.PI, -Math.PI));

			expect(pr(new L.Point(0.523598775598, 1.010683188683))).near(new L.Point(0.523598775598, 1.010683188683));
		});

		it('unprojects other points', function () {
			// from https://github.com/Leaflet/Leaflet/issues/1578
			expect(pr(new L.Point(8918060.964088084, 6755099.410887127)));
		});
	});
});


"publish": "lerna publish from-package",

"clean": "lerna run clean --parallel --stream && lerna clean --yes && rimraf node_modules **/*.tsbuildinfo",

"test": "lerna run --stream --parallel test",

"inso-start": "npm run build:sr --prefix packages/insomnia && npm start --prefix packages/insomnia-inso",

"inso-package": "npm run build:sr --prefix packages/insomnia && npm run package --prefix packages/insomnia-inso",

"inso-package:artifacts": "npm run artifacts --prefix packages/insomnia-inso",

"watch:app": "npm run build:main.min.js --prefix packages/insomnia && npm run start:dev-server --prefix packages/insomnia",

"app-start": "npm start --prefix packages/insomnia",

keep keep keep keep replace keep keep keep keep

"inso-start": "npm start --prefix packages/insomnia-inso",

insomnia

Insomnia is a cross-platform REST client, built on top of Electron.

npm install && lerna bootstrap && lerna run --stream bootstrap

npm run build --prefix packages/insomnia && npm run package --prefix packages/insomnia

npm run bump-version --prefix packages/insomnia

npm run test:dev --prefix packages/insomnia-smoke-test

npm run test:build --prefix packages/insomnia-smoke-test

npm run test:package --prefix packages/insomnia-smoke-test

npm run test:cli --prefix packages/insomnia-smoke-test

npm run clean && npm run bootstrap && npm run app-start


Swagger Petstore 1.0.0

This is a sample server Petstore server.  You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).  For this sample, you can use the api key `special-key` to test the authorization filters.

Base environment

{{ scheme }}://{{ host }}{{ base_path }}

Finds Pets by status

Multiple status values can be provided with comma separated strings

Finds Pets by tags

Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.

Find pet by ID

Returns a single pet

Updates a pet in the store with form data

uploads an image

{{ base_url }}/pet/{{ petId }}/uploadImage

Returns pet inventories by status

Returns a map of status codes to quantities

Find purchase order by ID

For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions

{{ base_url }}/store/order/{{ orderId }}

Delete purchase order by ID

For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors

Create user

This can only be done by the logged in user.

Creates list of users with given input array


import { ipcMain } from 'electron';

import { GrpcRequestEventEnum } from '../../common/grpc-events';
import * as grpc from '../../network/grpc';
import { GrpcIpcRequestParams } from '../../network/grpc/prepare';
import { ResponseCallbacks } from '../../network/grpc/response-callbacks';

export function registergRPCHandlers() {
  ipcMain.on(GrpcRequestEventEnum.start, (event, params: GrpcIpcRequestParams) =>
    grpc.start(params, new ResponseCallbacks(event)),
  );
  ipcMain.on(GrpcRequestEventEnum.sendMessage, (event, params: GrpcIpcRequestParams) =>
    // @ts-expect-error -- TSCONVERSION
    grpc.sendMessage(params, new ResponseCallbacks(event)),
  );
  ipcMain.on(GrpcRequestEventEnum.commit, (_, requestId) => grpc.commit(requestId));
  ipcMain.on(GrpcRequestEventEnum.cancel, (_, requestId) => grpc.cancel(requestId));
  ipcMain.on(GrpcRequestEventEnum.cancelMultiple, (_, requestIdS) =>
    grpc.cancelMultiple(requestIdS),
  );
}


import React, { FC, ReactNode, useCallback } from 'react';

import { toKebabCase } from '../../../../../common/misc';
import { useActiveRequest } from '../../../../hooks/use-active-request';
import { Button } from '../../../base/button';
import { AuthRow } from './auth-row';

interface Props {
  label: string;
  property: string;
  invert?: boolean;
  help?: ReactNode;
  onTitle?: string;
  offTitle?: string;
}

const ToggleIcon: FC<{isOn: boolean}> = ({ isOn }) => isOn ? <i data-testid="toggle-is-on" className="fa fa-check-square-o" /> : <i data-testid="toggle-is-off" className="fa fa-square-o" />;

export const AuthToggleRow: FC<Props> = ({
  label,
  property,
  help,
  invert,
  onTitle = 'Disable item',
  offTitle = 'Enable item',
}) => {
  const { activeRequest: { authentication }, patchAuth } = useActiveRequest();

  const databaseValue = Boolean(authentication[property]);
  const toggle = useCallback(
    (_event: React.MouseEvent<HTMLButtonElement>, value?: boolean) =>
      patchAuth({ [property]: value }), [patchAuth, property]
  );

  const isActuallyOn = invert ? !databaseValue : databaseValue;

  const id = toKebabCase(label);
  const title = isActuallyOn ? onTitle : offTitle;

  return (
    <AuthRow labelFor={id} label={label} help={help}>
      <Button
        className="btn btn--super-duper-compact"
        id={id}
        onClick={toggle}
        value={!databaseValue}
        title={title}
      >
        <ToggleIcon isOn={isActuallyOn} />
      </Button>
    </AuthRow>
  );
};


insomnia

The Collaborative API Design Tool

esr --cache ./scripts/build.ts --noErrorTruncation

concurrently --names sr:source,sr:types "npm run build:sr:source" "npm run build:sr:types"

cross-env USE_HARD_LINKS=false electron-builder build --config electron-builder.config.js

concurrently -n dev,app --kill-others "npm run start:dev-server" "npm run start:electron"

cross-env NODE_ENV=development esr esbuild.main.ts && electron .

tsc --noEmit --project tsconfig.build.json


import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { createRef, PureComponent } from 'react';

import { AUTOBIND_CFG } from '../../../common/constants';
import { Modal } from '../base/modal';
import { ModalBody } from '../base/modal-body';
import { ModalFooter } from '../base/modal-footer';
import { ModalHeader } from '../base/modal-header';
import { showModal } from '.';

export interface SelectModalShowOptions {
  message: string | null;
  onCancel?: () => void;
  onDone?: (selectedValue: string | null) => void | Promise<void>;
  options: {
    name: string;
    value: string;
  }[];
  title: string | null;
  value: string | null;
  noEscape?: boolean;
}

const initialState: SelectModalShowOptions = {
  message: null,
  options: [],
  title: null,
  value: null,
};

@autoBindMethodsForReact(AUTOBIND_CFG)
export class SelectModal extends PureComponent<{}, SelectModalShowOptions> {
  modal = createRef<Modal>();
  doneButton = createRef<HTMLButtonElement>();
  state: SelectModalShowOptions = initialState;

  async _onDone() {
    this.modal.current?.hide();
    await this.state.onDone?.(this.state.value);
  }

  _handleSelectChange(event: React.SyntheticEvent<HTMLSelectElement>) {
    this.setState({ value: event.currentTarget.value });
  }

  show({
    message,
    onCancel,
    onDone,
    options,
    title,
    value,
    noEscape,
  }: SelectModalShowOptions = initialState) {
    this.setState({
      message,
      onCancel,
      onDone,
      options,
      title,
      value,
      noEscape,
    });
    this.modal.current?.show();
    setTimeout(() => {
      this.doneButton.current?.focus();
    }, 100);
  }

  render() {
    const { message, title, options, value, onCancel, noEscape } = this.state;

    return (
      <Modal ref={this.modal} onCancel={onCancel} noEscape={noEscape}>
        <ModalHeader>{title || 'Confirm?'}</ModalHeader>
        <ModalBody className="wide pad">

{message}

            <select onChange={this._handleSelectChange} value={value ?? undefined}>
              {options.map(({ name, value }) => (
                <option key={value} value={value}>
                  {name}
                </option>
              ))}
            </select>

        </ModalBody>
        <ModalFooter>
          <button ref={this.doneButton} className="btn" onClick={this._onDone}>
            Done
          </button>
        </ModalFooter>
      </Modal>
    );
  }
}

export const showSelectModal = (opts: SelectModalShowOptions) => showModal(SelectModal, opts);


import { createBuilder } from '@develohpanda/fluent-builder';
import { describe, expect, it } from '@jest/globals';

import { GrpcMethodTypeEnum } from '../../network/grpc/method';
import { grpcMethodDefinitionSchema } from '../../ui/context/grpc/__schemas__';
import {
  getGrpcPathSegments,
  getShortGrpcPath,
  groupGrpcMethodsByPackage,
  NO_PACKAGE_KEY,
} from '../grpc-paths';

describe('getGrpcPathSegments', () => {
  it.each([
    ['package', 'service', 'method'],
    ['nested.package', 'service', 'method'],
    ['another.nested.package', 'service', 'method'],
    ['.another.package', 'service', 'method'],
  ])(
    'should extract package, service and method from "/%s.%s/%s"',
    (packageName, serviceName, methodName) => {
      expect(getGrpcPathSegments(`/${packageName}.${serviceName}/${methodName}`)).toStrictEqual({
        packageName,
        serviceName,
        methodName,
      });
    },
  );

  it.each([['service', 'method']])(
    'should extract service and method from "/%s/%s"',
    (serviceName, methodName) => {
      expect(getGrpcPathSegments(`/${serviceName}/${methodName}`)).toStrictEqual({
        packageName: undefined,
        serviceName,
        methodName,
      });
    },
  );
});

describe('getShortGrpcPath', () => {
  it('should return shortened path', () => {
    const packageName = 'package';
    const serviceName = 'service';
    const methodName = 'method';
    const fullPath = '/package.service/method';
    const shortPath = getShortGrpcPath(
      {
        packageName,
        serviceName,
        methodName,
      },
      fullPath,
    );
    expect(shortPath).toBe('/service/method');
  });

  it('should return full path', () => {
    const packageName = undefined;
    const serviceName = 'service';
    const methodName = 'method';
    const fullPath = '/service/method';
    const shortPath = getShortGrpcPath(
      {
        packageName,
        serviceName,
        methodName,
      },
      fullPath,
    );
    expect(shortPath).toBe(fullPath);
  });
});
const methodBuilder = createBuilder(grpcMethodDefinitionSchema);

describe('groupGrpcMethodsByPackage', () => {
  it('should group methods by package', () => {
    const packageMethod1 = methodBuilder
      .path('/package1.service/u')
      .requestStream(false)
      .responseStream(false)
      .build();
    const packageMethod2 = methodBuilder
      .path('/package1.service/ss')
      .requestStream(false)
      .responseStream(true)
      .build();
    const newPackage = methodBuilder
      .path('/package2.service/cs')
      .requestStream(true)
      .responseStream(false)
      .build();
    const noPackage = methodBuilder
      .path('/service/bd')
      .requestStream(true)
      .responseStream(true)
      .build();
    const grouped = groupGrpcMethodsByPackage([
      packageMethod1,
      packageMethod2,
      newPackage,
      noPackage,
    ]);
    expect(Object.keys(grouped).length).toBe(3);
    expect(grouped[NO_PACKAGE_KEY]).toStrictEqual([
      {
        segments: {
          packageName: undefined,
          serviceName: 'service',
          methodName: 'bd',
        },
        type: GrpcMethodTypeEnum.bidi,
        fullPath: noPackage.path,
      },
    ]);
    expect(grouped.package1).toStrictEqual([
      {
        segments: {
          packageName: 'package1',
          serviceName: 'service',
          methodName: 'u',
        },
        type: GrpcMethodTypeEnum.unary,
        fullPath: packageMethod1.path,
      },
      {
        segments: {
          packageName: 'package1',
          serviceName: 'service',
          methodName: 'ss',
        },
        type: GrpcMethodTypeEnum.server,
        fullPath: packageMethod2.path,
      },
    ]);
    expect(grouped.package2).toStrictEqual([
      {
        segments: {
          packageName: 'package2',
          serviceName: 'service',
          methodName: 'cs',
        },
        type: GrpcMethodTypeEnum.client,
        fullPath: newPackage.path,
      },
    ]);
  });
});


import { createBuilder } from '@develohpanda/fluent-builder';
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { mocked } from 'jest-mock';

import { globalBeforeEach } from '../../../__jest__/before-each';
import { isLoggedIn as _isLoggedIn } from '../../../account/session';
import { database } from '../../../common/database';
import * as models from '../../../models';
import { DEFAULT_PROJECT_ID } from '../../../models/project';
import { backendProjectWithTeamSchema, teamSchema } from '../../__schemas__/type-schemas';
import MemoryDriver from '../../store/drivers/memory-driver';
import { initializeProjectFromTeam } from '../initialize-model-from';
import { migrateCollectionsIntoRemoteProject } from '../migrate-collections';
import { VCS } from '../vcs';

jest.mock('../vcs');
jest.mock('../../../account/session', () => ({
  isLoggedIn: jest.fn(),
}));

const isLoggedIn = mocked(_isLoggedIn);
const newMockedVcs = () => mocked(new VCS(new MemoryDriver()), true);

const projectWithTeamBuilder = createBuilder(backendProjectWithTeamSchema);
const teamBuilder = createBuilder(teamSchema);

describe('migrateCollectionsIntoRemoteProject', () => {
  beforeEach(async () => {
    await globalBeforeEach();
    // Always return logged in
    isLoggedIn.mockReturnValue(true);
    teamBuilder.reset();
    projectWithTeamBuilder.reset();
  });

  it('exits if not logged in', async () => {
    // Arrange
    isLoggedIn.mockReturnValue(false);
    const vcs = newMockedVcs();

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.hasBackendProjectForRootDocument).not.toHaveBeenCalled();
    expect(vcs.remoteBackendProjectsInAnyTeam).not.toHaveBeenCalled();
  });

  it('does not migrate if collection is in non-remote project but no local backend project exists', async () => {
    // Arrange
    const vcs = newMockedVcs();

    const defaultProject = await models.project.getById(DEFAULT_PROJECT_ID);
    const workspaceInBase = await models.workspace.create({ parentId: defaultProject?._id });

    const localProject = await models.project.create();
    const workspaceInLocal = await models.workspace.create({ parentId: localProject._id });

    vcs.hasBackendProjectForRootDocument.mockResolvedValue(false); // no local backend project

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.remoteBackendProjectsInAnyTeam).not.toHaveBeenCalled();
    await expect(models.workspace.getById(workspaceInBase._id)).resolves.toStrictEqual(workspaceInBase);
    await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toStrictEqual(workspaceInLocal);
  });

  it('does not migrate if all collections are in a remote project already', async () => {
    // Arrange
    const vcs = newMockedVcs();

    const remoteProject = await models.project.create({ remoteId: 'str' });
    const workspaceInRemote = await models.workspace.create({ parentId: remoteProject._id });

    vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.remoteBackendProjectsInAnyTeam).not.toHaveBeenCalled();
    await expect(models.workspace.getById(workspaceInRemote._id)).resolves.toStrictEqual(workspaceInRemote);
  });

  it('does not migrate if design documents', async () => {
    // Arrange
    const vcs = newMockedVcs();

    const localProject = await models.project.create();
    const workspaceInLocal = await models.workspace.create({ scope: 'design', parentId: localProject._id });

    vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.remoteBackendProjectsInAnyTeam).not.toHaveBeenCalled();
    await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toStrictEqual(workspaceInLocal);
  });

  it('does migrate if collection in non-remote project with local backend project - create remote project', async () => {
    // Arrange
    const vcs = newMockedVcs();
    const localProject = await models.project.create();
    const workspaceInLocal = await models.workspace.create({ parentId: localProject._id });

    const team = teamBuilder.build();
    const remoteBackendProjectWithTeam = projectWithTeamBuilder
      .rootDocumentId(workspaceInLocal._id)
      .team(team)
      .build();

    vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project
    vcs.remoteBackendProjectsInAnyTeam.mockResolvedValue([remoteBackendProjectWithTeam]); // has local backend project

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.remoteBackendProjectsInAnyTeam).toHaveBeenCalledTimes(1);
    const createdRemoteProject = await models.project.getByRemoteId(team.id);
    await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toMatchObject({
      ...workspaceInLocal,
      parentId: createdRemoteProject?._id,
    });
  });

  it('does migrate if collection in non-remote project with local backend project - use existing remote project', async () => {
    // Arrange
    const vcs = newMockedVcs();
    const localProject = await models.project.create();
    const workspaceInLocal = await models.workspace.create({ parentId: localProject._id });

    const team = teamBuilder.build();
    const existingRemoteProject = await initializeProjectFromTeam(team);
    await database.batchModifyDocs({ upsert: [existingRemoteProject] });

    const remoteProjectWithTeam = projectWithTeamBuilder
      .rootDocumentId(workspaceInLocal._id)
      .team(team)
      .build();

    vcs.hasBackendProjectForRootDocument.mockResolvedValue(true); // has local backend project
    vcs.remoteBackendProjectsInAnyTeam.mockResolvedValue([remoteProjectWithTeam]); // has local backend project

    // Act
    await migrateCollectionsIntoRemoteProject(vcs);

    // Assert
    expect(vcs.remoteBackendProjectsInAnyTeam).toHaveBeenCalledTimes(1);
    await expect(models.workspace.getById(workspaceInLocal._id)).resolves.toMatchObject({
      ...workspaceInLocal,
      parentId: existingRemoteProject?._id,
    });
  });
});


"build:app": "esr --cache ./scripts/build.ts --noErrorTruncation",

"build:main.min.js": "cross-env NODE_ENV=development esr esbuild.main.ts",

"build:sr": "concurrently --names sr:source,sr:types \"npm run build:sr:source\" \"npm run build:sr:types\"",

"build": "npm run build:sr && npm run build:app",

The Collaborative API Design Tool


import { describe, expect, it } from '@jest/globals';

import { setDefaultProtocol } from './protocol';

describe('setDefaultProtocol()', () => {
  it('no-ops on empty url', () => {
    const url = setDefaultProtocol('');
    expect(url).toBe('');
  });

  it('correctly sets protocol for empty', () => {
    const url = setDefaultProtocol('google.com');
    expect(url).toBe('http://google.com');
  });

  it('correctly sets protocol for padded domain', () => {
    const url = setDefaultProtocol('   google.com   ');
    expect(url).toBe('http://google.com');
  });

  it('does not set for valid url', () => {
    const url = setDefaultProtocol('https://google.com');
    expect(url).toBe('https://google.com');
  });

  it('does not set for valid url', () => {
    const url = setDefaultProtocol('http://google.com');
    expect(url).toBe('http://google.com');
  });

  it('does not set for invalid url', () => {
    const url = setDefaultProtocol('httbad://google.com');
    expect(url).toBe('httbad://google.com');
  });
});


import React, { FunctionComponent } from 'react';
import styled, { css } from 'styled-components';

import type { ButtonProps } from './button';
import { Button } from './button';

export interface CircleButtonProps extends ButtonProps {
  height?: string;
  width?: string;
}

const StyledCircleButton = styled(Button)<CircleButtonProps>`
  && {
    padding: unset;
    font-size: var(--font-size-xl);
    border-radius: 50%;

    ${({ height }) => css`
      height: ${height || css`var(--line-height-xs)`};
    `};

    ${({ width }) => css`
      width: ${width || css`var(--line-height-xs)`};
    `};

    svg {
      padding: var(--padding-xs);
    }
  }
`;

export const CircleButton: FunctionComponent<CircleButtonProps> = props => <StyledCircleButton {...props} />;


import { BaseModel, request, requestGroup, workspace } from '../../models';

export const data: Record<string, Partial<BaseModel>[]> = {
  [workspace.type]: [
    {
      _id: 'wrk_1',
      name: 'Wrk 1',
    },
  ],
  [requestGroup.type]: [
    {
      _id: 'fld_1',
      parentId: 'wrk_1',
      name: 'Fld 1',
    },
    {
      _id: 'fld_2',
      parentId: 'wrk_1',
      name: 'Fld 2',
    },
    {
      _id: 'fld_3',
      parentId: 'fld_1',
      name: 'Fld 3',
    },
  ],
  [request.type]: [
    {
      _id: 'req_1',
      parentId: 'fld_1',
      name: 'Req 1',
    },
    {
      _id: 'req_2',
      parentId: 'fld_1',
      name: 'Req 2',
    },
    {
      _id: 'req_3',
      parentId: 'wrk_1',
      name: 'Req 3',
    },
    {
      _id: 'req_4',
      parentId: 'fld_3',
      name: 'Req 4',
    },
    {
      _id: 'req_5',
      parentId: 'wrk_1',
      name: 'Req 5',
    },
  ],
};