<commit_before>/*
 * Collapsible widget extension: menu behavior
 * https://github.com/filamentgroup/collapsible
 * Copyright (c) 2014 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

;(function ($, window, document, undefined) {

	$( document ).bind( "init", function( e ){
		var pluginName = "collapsible";
		if( $( e.target ).is( "." + pluginName ) ){
			var $collapsible = $( e.target );
			var self = $collapsible.data( pluginName );
			var $trigger = $collapsible.prev().filter( "." + pluginName + "-trigger" );
			var triggerExpand = pluginName + "-trigger-expand";

			var isMenu = function(){
				return $collapsible.find( "." + pluginName + "-content" ).css( "position" ) === "absolute";
			};

			// tapout/clickout behavior
			$( document ).bind( "touchstart." + pluginName + " click." + pluginName, function( a ){
				// if the event target is not in the collapsible, and the collapsible is expanded, and it's a menu presentation... collapse it!
				if( !$( a.target ).closest( e.target ).length &&
					!$( a.target ).closest( $trigger ).length &&
					!$collapsible.data( pluginName ).collapsed &&
					isMenu() ){

					setTimeout(function(){
						$collapsible.data( pluginName ).collapse();
					});
					a.preventDefault();
				}
			} );

			// hover behavior for collapsibles and triggers relies on the presence of data-collapsible-hover attr
			if( $collapsible.is( "[data-collapsible-hover]" ) ){

				$collapsible
					.add( $trigger )
					.bind( "mouseenter." + pluginName, function(){
						if( isMenu() ){
							$collapsible.data( pluginName ).expand();
						}
					} )
					.bind( "mouseleave." + pluginName, function(){
						if( isMenu() ){
							$collapsible.data( pluginName ).collapse();
						}
					} );

				$collapsible
					.bind( "expand", function(){
						$trigger.addClass( triggerExpand );
					} )
					.bind( "collapse", function(){
						$trigger.removeClass( triggerExpand );
					} );
			}

			// add keyboard/arrow handling
			// arrow method handles the arrow key navigation, which largely maps to the tab key within the component
			self.arrow = function( target, back ){

				// find all focusables in this collapsible content area
				var $focusables = this.content.find( this.focusable );
				// nextTab will be the next element receiving focus from this arrow keydown
				var nextTab;

				// if the keydown target is the header and it's down or right, focus on the first focusable
				if( $( target ).is( this.header ) && !back ){
					nextTab = $focusables[ 0 ];
				}
				else {
					// if it's a backward arrow, reverse the array
					if( back ){
						// this if can go away once https://github.com/filamentgroup/shoestring/issues/80 is fixed
						// check if already an array (shoestring with bug above)
						if( $focusables.reverse ){
							$focusables = $focusables.reverse();
						}
						// otherwisejquery will need a get()
						else {
							$focusables = $( $focusables.get().reverse() );
						}
					}
					// afterTarget becomes true once the target has been passed in the each loop
					var afterTarget = false;
					// loop focusables
					$focusables.each(function(){
						// if nextTab isn't defined yet, we're after the target in the loop, and the target appears to be displayed
						// NOTE: the offset checks replaced the following, which tied keyboard behavior to aria state:
							// !$( this ).closest( ".collapsible-collapsed .collapsible-content" ).length
							// unfortunately, we sometimes display visually elements that are still aria-hidden.
							// The current check caters to sighted keyboard users over non-sighted keyboard users. TODO: figure this out.
						if( !nextTab && afterTarget && this.offsetHeight > 0 && this.offsetLeft > -1 ){
							nextTab = this;
						}
						// try to set afterTarget if not already set
						if( !afterTarget ) {
							afterTarget = $( this ).is( target );
						}
					});
				}

				// if we have a next element to send focus to at this point, do that. Otherwise, focus back on header
				if( nextTab ){
					nextTab.focus();
				}
				else {
					// no nextTab? focus back to header
					this.header[ 0 ].focus();
				}
			};

			// bind keydown handlers.
			// arrow key handling applies to the entire collapsible
			self.element
				.bind( "keydown." + pluginName, function( e ){
					// arrow key behavior: collapsible must be expanded to accept arrow navigation
					if( !self.collapsed ){
						if( e.which === 39 || e.which === 40 ){
							self.arrow( e.target );
							e.preventDefault();
						}
						else if( e.which === 37 || e.which === 38 ){
							self.arrow( e.target, true );
							e.preventDefault();
						}
					}

				});

		}
	} );

})(jQuery, window, document);
<commit_msg>improve tap-out handling to ignore gestures and scrolling<commit_after>/*
 * Collapsible widget extension: menu behavior
 * https://github.com/filamentgroup/collapsible
 * Copyright (c) 2014 Filament Group, Inc.
 * Licensed under the MIT, GPL licenses.
 */

;(function ($, window, document, undefined) {

	$( document ).bind( "init", function( e ){
		var pluginName = "collapsible";
		if( $( e.target ).is( "." + pluginName ) ){
			var $collapsible = $( e.target );
			var self = $collapsible.data( pluginName );
			var $trigger = $collapsible.prev().filter( "." + pluginName + "-trigger" );
			var triggerExpand = pluginName + "-trigger-expand";

			var isMenu = function(){
				return $collapsible.find( "." + pluginName + "-content" ).css( "position" ) === "absolute";
			};

			// tapout/clickout behavior
			var targetTop;
			var touchCancel = false;
			$( "body" )
				.bind( "gesturestart." + pluginName, function( a ){
					touchCancel = true;
				})
				.bind( "touchstart." + pluginName, function( a ){
					targetTop = a.target.getBoundingClientRect().top;
				})
				.bind( "touchend." + pluginName + " click." + pluginName, function( a ){
					if( a.type === "touchend" ){
						if( targetTop && Math.abs( targetTop - a.target.getBoundingClientRect().top ) > 5 ){
							touchCancel = true;
						}
					}

					// if the event target is not in the collapsible, and the collapsible is expanded, and it's a menu presentation... collapse it!
					if( !$( a.target ).closest( e.target ).length &&
						!$( a.target ).closest( $trigger ).length &&
						!$collapsible.data( pluginName ).collapsed &&
						isMenu()  &&
						touchCancel === false ){

						setTimeout(function(){
							$collapsible.data( pluginName ).collapse();
						});
						a.preventDefault();
					}
					setTimeout( function(){
						targetTop = null;
						touchCancel = false;
					}, 200 );
				} );

			// hover behavior for collapsibles and triggers relies on the presence of data-collapsible-hover attr
			if( $collapsible.is( "[data-collapsible-hover]" ) ){

				$collapsible
					.add( $trigger )
					.bind( "mouseenter." + pluginName, function(){
						if( isMenu() ){
							$collapsible.data( pluginName ).expand();
						}
					} )
					.bind( "mouseleave." + pluginName, function(){
						if( isMenu() ){
							$collapsible.data( pluginName ).collapse();
						}
					} );

				$collapsible
					.bind( "expand", function(){
						$trigger.addClass( triggerExpand );
					} )
					.bind( "collapse", function(){
						$trigger.removeClass( triggerExpand );
					} );
			}

			// add keyboard/arrow handling
			// arrow method handles the arrow key navigation, which largely maps to the tab key within the component
			self.arrow = function( target, back ){

				// find all focusables in this collapsible content area
				var $focusables = this.content.find( this.focusable );
				// nextTab will be the next element receiving focus from this arrow keydown
				var nextTab;

				// if the keydown target is the header and it's down or right, focus on the first focusable
				if( $( target ).is( this.header ) && !back ){
					nextTab = $focusables[ 0 ];
				}
				else {
					// if it's a backward arrow, reverse the array
					if( back ){
						// this if can go away once https://github.com/filamentgroup/shoestring/issues/80 is fixed
						// check if already an array (shoestring with bug above)
						if( $focusables.reverse ){
							$focusables = $focusables.reverse();
						}
						// otherwisejquery will need a get()
						else {
							$focusables = $( $focusables.get().reverse() );
						}
					}
					// afterTarget becomes true once the target has been passed in the each loop
					var afterTarget = false;
					// loop focusables
					$focusables.each(function(){
						// if nextTab isn't defined yet, we're after the target in the loop, and the target appears to be displayed
						// NOTE: the offset checks replaced the following, which tied keyboard behavior to aria state:
							// !$( this ).closest( ".collapsible-collapsed .collapsible-content" ).length
							// unfortunately, we sometimes display visually elements that are still aria-hidden.
							// The current check caters to sighted keyboard users over non-sighted keyboard users. TODO: figure this out.
						if( !nextTab && afterTarget && this.offsetHeight > 0 && this.offsetLeft > -1 ){
							nextTab = this;
						}
						// try to set afterTarget if not already set
						if( !afterTarget ) {
							afterTarget = $( this ).is( target );
						}
					});
				}

				// if we have a next element to send focus to at this point, do that. Otherwise, focus back on header
				if( nextTab ){
					nextTab.focus();
				}
				else {
					// no nextTab? focus back to header
					this.header[ 0 ].focus();
				}
			};

			// bind keydown handlers.
			// arrow key handling applies to the entire collapsible
			self.element
				.bind( "keydown." + pluginName, function( e ){
					// arrow key behavior: collapsible must be expanded to accept arrow navigation
					if( !self.collapsed ){
						if( e.which === 39 || e.which === 40 ){
							self.arrow( e.target );
							e.preventDefault();
						}
						else if( e.which === 37 || e.which === 38 ){
							self.arrow( e.target, true );
							e.preventDefault();
						}
					}

				});

		}
	} );

})(jQuery, window, document);
<|endoftext|>


<commit_before>/**
 * CTA component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * Internal dependencies
 */
import Link from '../Link';
import Button from '../Button';

const CTA = ( {
	title,
	description,
	ctaLink,
	ctaLabel,
	ctaLinkExternal,
	ctaType,
	error,
	onClick,
	'aria-label': ariaLabel,
} ) => (

		{ title &&
{ title }
 }
		{ description && typeof description === 'string' && (

{ description }

		) }
		{ description && typeof description !== 'string' && (

				{ description }

		) }
		{ ctaLabel && ctaType === 'button' && (
			<Button
				aria-label={ ariaLabel }
				href={ ctaLink }
				onClick={ onClick }
			>
				{ ctaLabel }
			</Button>
		) }
		{ ctaLabel && ctaType === 'link' && (
			<Link
				href={ ctaLink }
				onClick={ onClick }
				aria-label={ ariaLabel }
				external={ ctaLinkExternal }
				hideExternalIndicator={ ctaLinkExternal }
				arrow
			>
				{ ctaLabel }
			</Link>
		) }

);

CTA.propTypes = {
	title: PropTypes.string.isRequired,
	description: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
	ctaLink: PropTypes.string,
	ctaLinkExternal: PropTypes.bool,
	ctaLabel: PropTypes.string,
	ctaType: PropTypes.string,
	'aria-label': PropTypes.string,
	error: PropTypes.bool,
	onClick: PropTypes.func,
};

CTA.defaultProps = {
	title: '',
	description: '',
	ctaLink: '',
	ctaLabel: '',
	ctaType: 'link',
	error: false,
	onClick: () => {},
};

export default CTA;
<commit_msg>Upgrade CTA component with children prop.<commit_after>/**
 * CTA component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * Internal dependencies
 */
import Link from '../Link';
import Button from '../Button';

const CTA = ( {
	title,
	description,
	ctaLink,
	ctaLabel,
	ctaLinkExternal,
	ctaType,
	error,
	onClick,
	'aria-label': ariaLabel,
	children,
} ) => (

		{ title &&
{ title }
 }
		{ description && typeof description === 'string' && (

{ description }

		) }
		{ description && typeof description !== 'string' && (

				{ description }

		) }
		{ ctaLabel && ctaType === 'button' && (
			<Button
				aria-label={ ariaLabel }
				href={ ctaLink }
				onClick={ onClick }
			>
				{ ctaLabel }
			</Button>
		) }
		{ ctaLabel && ctaType === 'link' && (
			<Link
				href={ ctaLink }
				onClick={ onClick }
				aria-label={ ariaLabel }
				external={ ctaLinkExternal }
				hideExternalIndicator={ ctaLinkExternal }
				arrow
			>
				{ ctaLabel }
			</Link>
		) }
		{ children }

);

CTA.propTypes = {
	title: PropTypes.string.isRequired,
	description: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
	ctaLink: PropTypes.string,
	ctaLinkExternal: PropTypes.bool,
	ctaLabel: PropTypes.string,
	ctaType: PropTypes.string,
	'aria-label': PropTypes.string,
	error: PropTypes.bool,
	onClick: PropTypes.func,
	children: PropTypes.node,
};

CTA.defaultProps = {
	title: '',
	description: '',
	ctaLink: '',
	ctaLabel: '',
	ctaType: 'link',
	error: false,
	onClick: () => {},
};

export default CTA;
<|endoftext|>


<commit_before>/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

angular.module('eappApp').factory('appService',function($http, $mdDialog, $location)
{

    const APPLICATION_DEFAULT_ADDRESS = "Rue Ste-Catherine Ouest, Québec, Montréal";
    const APPLICATION_DEFAULT_LONGITUDE = -73.5815;
    const APPLICATION_DEFAULT_LATITUDE = 45.4921;
    const APPLICATION_DEFAULT_POSTCODE = "H3B 1K1";

    var service = this;

    service.host = $location.host();

    if(service.host.toString() === "localhost")
    {
        service.host = service.host.concat("/eapp/");
    }

    service.getSiteUrl = function()
    {
        var siteName = window.location.hostname.toString();

        if(siteName == "localhost")
        {
            siteName = siteName.concat("/eapp/");
        }

        return location.protocol.concat("//", siteName, "/index.php/");
    };

    service.getBaseUrl = function()
    {
        var siteName = window.location.hostname.toString();

        if(siteName == "localhost")
        {
            siteName = siteName.concat("/eapp/");
        }

        return location.protocol.concat("//", siteName, "/");
    };

    service.baseUrl = this.getBaseUrl();

    service.siteUrl = this.getBaseUrl();

    service.currentAddress = APPLICATION_DEFAULT_ADDRESS;
    service.longitude = APPLICATION_DEFAULT_LONGITUDE;
    service.latitude = APPLICATION_DEFAULT_LATITUDE;
    service.postcode = APPLICATION_DEFAULT_POSTCODE;
    service.method = "";
    service.controller = "";

    service.path = $location.absUrl().toString().substring(($location.protocol() + "://" + service.host).length);

    var pathArray = service.path.split("/");

    var i = 0;

    if(pathArray.length > i)
    {
        while(pathArray[i] == "" && pathArray.length > i + 1)
        {
            i++;
        }

        if(pathArray[i].toString() === "index.php")
        {
            i++;

            while(pathArray[i] == "" && pathArray.length > i + 1)
            {
                i++;
            }

            service.controller = pathArray[i];

            i++;

            if(pathArray.length > i)
            {
                while(pathArray[i] == "" && pathArray.length > i + 1)
                {
                    i++;
                }

                if(pathArray.length > i)
                {
                    service.method = pathArray[i];
                }
            }
        }
        else
        {
            while(pathArray[i] == "" && pathArray.length > i + 1)
            {
                i++;
            }

            service.controller = pathArray[i];

            i++;

            if(pathArray.length > i)
            {
                while(pathArray[i] == "" && pathArray.length > i + 1)
                {
                    i++;
                }

                if(pathArray.length > i)
                {
                    service.method = pathArray[i];
                }
            }
        }
    }

    service.ready = $http.post(this.siteUrl.concat("eapp/get_application_data"), null, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

    service.ready.then(function(response)
    {
        service.cart = response.data.cart;
        service.baseUrl = response.data.base_url.concat("/");
        service.siteUrl = response.data.site_url.concat("/");
        service.redirectToLogin = response.data.redirect_to_login;
        service.loggedUser = response.data.user;
        service.isUserLogged = service.loggedUser !== null;
        service.isRegularUser = service.isUserLogged && parseInt(service.loggedUser.subscription) <= 2;

        if(!service.isUserLogged)
        {
            if(window.localStorage.getItem("latitude"))
            {
                service.latitude = window.localStorage.getItem("latitude");
            }

            if(window.localStorage.getItem("longitude"))
            {
                service.longitude = window.localStorage.getItem("longitude");
            }

            if(window.localStorage.getItem("currentAddress"))
            {
                service.currentAddress = window.localStorage.getItem("currentAddress");
            }

            if(window.localStorage.getItem("postcode"))
            {
                service.postcode = window.localStorage.getItem("postcode");
            }
        }

        service.changeLocationUrl = service.siteUrl.concat("/home/change_location");

    });

    service.clearSessionItems = function()
    {
        window.sessionStorage.removeItem("filterSettings");
        window.sessionStorage.removeItem("store_id");
        window.sessionStorage.removeItem("category_id");
    };

    service.gotoShop = function()
    {
        service.clearSessionItems();
        window.location =  service.siteUrl.concat("/shop");
    };

    service.getUserCoordinates = function(callback)
    {
        // Get the current geo location only if it's not yet the case
        if ("geolocation" in navigator)
        {
            navigator.geolocation.getCurrentPosition(function(position)
            {
                service.longitude = position.coords.longitude;
                service.latitude = position.coords.latitude;
                var geocoder = new google.maps.Geocoder;
                service.geocodeLatLng(geocoder, service.latitude, service.longitude, callback);
            });
        }
    };

    service.getUserCoordinatesFromPostcode = function(postcode, callback)
    {
        var geocoder = new google.maps.Geocoder;

        geocoder.geocode( { 'address': postcode}, function(results, status)
        {
            if (status == google.maps.GeocoderStatus.OK)
            {
                callback(results[0]);
            }
        });
    };

    service.geocodeLatLng = function(geocoder, latitude, longitude, callback)
    {
        var latlng = {lat: latitude, lng: longitude};

        geocoder.geocode({'location': latlng}, function(results, status)
        {
            if (status === 'OK')
            {
                if (results[0])
                {
                    callback(results[0]);
                }
                else
                {

                    window.alert('No results found');
                }
            }
            else
            {
                window.alert('Geocoder failed due to: ' + status);
            }
        });
    };

    service.promptForZipCode = function(ev)
    {
        if(!window.localStorage.getItem("longitude") && !window.localStorage.getItem("latitude") && !service.isUserLogged && false)
        {
            // Appending dialog to document.body to cover sidenav in docs app
            var confirm = $mdDialog.prompt()
              .title('Veillez entrer votre code postal. ')
              .textContent('Ceci vas aider a optimiser les resultats.')
              .placeholder('Votre Code Postale E.g. H1H 1H1')
              .ariaLabel('Code Postale')
              .initialValue('')
              .targetEvent(ev)
              .ok('Valider!')
              .cancel('Annuler');

            $mdDialog.show(confirm).then(function(result)
            {
                var address = result;
                var geocoder = new google.maps.Geocoder();
                geocoder.geocode( { 'address': address}, function(results, status)
                {
                    service.latitude = results[0].geometry.location.lat();
                    service.longitude = results[0].geometry.location.lng();
                    window.localStorage.setItem("longitude", service.longitude);
                    window.localStorage.setItem("latitude", service.latitude);

                    if (status !== google.maps.GeocoderStatus.OK)
                    {
                        service.getUserCoordinates();
                    }

                });

            }, function()
            {
                service.getUserCoordinates();
            });
        }
    };

    service.inMyList = function(product_id)
    {
        if(service.isUserLogged)
        {
            for(var key in service.loggedUser.grocery_list)
            {
                if(parseInt(service.loggedUser.grocery_list[key].id) === parseInt(product_id))
                {
                    return true;
                }
            }
        }

        return false;
    };

    service.selectCategory = function(category)
    {
        service.clearSessionItems();
        var category_id = parseInt(category.id);
        service.recordHit("eapp_product_category ",category_id);
        window.sessionStorage.setItem("category_id", category_id);
        window.sessionStorage.setItem("category_name", category.name);
        window.location =  service.siteUrl.concat("/shop");
    };

    // THis is called for a non logged user to prompt for his zip code
    // If that's not already the case.
    if(typeof service.promptForZipCode !== "undefined")
    {
        service.promptForZipCode();
    }

    service.getActiveUserCoordinates = function()
    {
        var longitude = service.isUserLogged ? service.loggedUser.profile.longitude : service.longitude;
        var latitude = service.isUserLogged ? service.loggedUser.profile.latitude : service.latitude;

        return {
            longitude : longitude,
            latitude : latitude
        };
    };

    service.recordRetailerHit = function(id, distance)
    {
        var formData = new FormData();
        formData.append("id", id);

        if(service.isUserLogged)
        {
            formData.append("distance", distance);
            formData.append("postcode", service.loggedUser.profile.postcode);
            return $http.post(service.siteUrl.concat("eapp/record_retailer_hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

        }
        else
        {
            var geocoder = new google.maps.Geocoder;
            var latlng = {lat: parseFloat(service.latitude), lng: parseFloat(service.longitude)};
            geocoder.geocode({'location': latlng}, function(results, status)
            {

                var postcode = '';

                if (status === 'OK')
                {
                    if(results[0])
                    {
                        if(results[0].address_components[8])
                        {
                            postcode = results[0].address_components[8].long_name;
                        }
                    }
                }

                formData.append("postcode", postcode);
                formData.append("distance", distance);
                return $http.post(service.siteUrl.concat("eapp/record_retailer_hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

            });
        }

    };

    service.recordProductStat = function(storeProductId, type, distance, is_product = false)
    {
        var formData = new FormData();
        formData.append("id", storeProductId);
        formData.append("type", type);
        formData.append("is_product", JSON.stringify(is_product));

        if(service.isUserLogged)
        {
            formData.append("distance", distance);
            formData.append("postcode", service.loggedUser.profile.postcode);
            return $http.post(service.siteUrl.concat("/eapp/record_stat"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

        }
        else
        {
            var geocoder = new google.maps.Geocoder;
            var latlng = {lat: parseFloat(service.latitude), lng: parseFloat(service.longitude)};
            geocoder.geocode({'location': latlng}, function(results, status)
            {

                var postcode = '';

                if (status === 'OK')
                {
                    if(results[0])
                    {
                        if(results[0].address_components[8])
                        {
                            postcode = results[0].address_components[8].long_name;
                        }
                    }
                }

                formData.append("postcode", postcode);
                formData.append("distance", distance);
                return $http.post(service.siteUrl.concat("/eapp/record_stat"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});
            });
        }

    };

    service.recordHit = function(tableName, id)
    {
        var formData = new FormData();
        formData.append("table_name", tableName);
        formData.append("id", id);
        return $http.post(service.siteUrl.concat("admin/hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});
    };

    return service;
});

<commit_msg>Fix problem where the user's longitude/latitude was not properly updated in the appService<commit_after>/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

angular.module('eappApp').factory('appService',function($http, $mdDialog, $location)
{

    const APPLICATION_DEFAULT_ADDRESS = "Rue Ste-Catherine Ouest, Québec, Montréal";
    const APPLICATION_DEFAULT_LONGITUDE = -73.5815;
    const APPLICATION_DEFAULT_LATITUDE = 45.4921;
    const APPLICATION_DEFAULT_POSTCODE = "H3B 1K1";

    var service = this;

    service.host = $location.host();

    if(service.host.toString() === "localhost")
    {
        service.host = service.host.concat("/eapp/");
    }

    service.getSiteUrl = function()
    {
        var siteName = window.location.hostname.toString();

        if(siteName == "localhost")
        {
            siteName = siteName.concat("/eapp/");
        }

        return location.protocol.concat("//", siteName, "/index.php/");
    };

    service.getBaseUrl = function()
    {
        var siteName = window.location.hostname.toString();

        if(siteName == "localhost")
        {
            siteName = siteName.concat("/eapp/");
        }

        return location.protocol.concat("//", siteName, "/");
    };

    service.baseUrl = this.getBaseUrl();

    service.siteUrl = this.getBaseUrl();

    service.currentAddress = APPLICATION_DEFAULT_ADDRESS;
    service.longitude = APPLICATION_DEFAULT_LONGITUDE;
    service.latitude = APPLICATION_DEFAULT_LATITUDE;
    service.postcode = APPLICATION_DEFAULT_POSTCODE;
    service.method = "";
    service.controller = "";

    service.path = $location.absUrl().toString().substring(($location.protocol() + "://" + service.host).length);

    var pathArray = service.path.split("/");

    var i = 0;

    if(pathArray.length > i)
    {
        while(pathArray[i] == "" && pathArray.length > i + 1)
        {
            i++;
        }

        if(pathArray[i].toString() === "index.php")
        {
            i++;

            while(pathArray[i] == "" && pathArray.length > i + 1)
            {
                i++;
            }

            service.controller = pathArray[i];

            i++;

            if(pathArray.length > i)
            {
                while(pathArray[i] == "" && pathArray.length > i + 1)
                {
                    i++;
                }

                if(pathArray.length > i)
                {
                    service.method = pathArray[i];
                }
            }
        }
        else
        {
            while(pathArray[i] == "" && pathArray.length > i + 1)
            {
                i++;
            }

            service.controller = pathArray[i];

            i++;

            if(pathArray.length > i)
            {
                while(pathArray[i] == "" && pathArray.length > i + 1)
                {
                    i++;
                }

                if(pathArray.length > i)
                {
                    service.method = pathArray[i];
                }
            }
        }
    }

    service.ready = $http.post(this.siteUrl.concat("eapp/get_application_data"), null, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

    service.ready.then(function(response)
    {
        service.cart = response.data.cart;
        service.baseUrl = response.data.base_url.concat("/");
        service.siteUrl = response.data.site_url.concat("/");
        service.redirectToLogin = response.data.redirect_to_login;
        service.loggedUser = response.data.user;
        service.isUserLogged = service.loggedUser !== null;
        service.isRegularUser = service.isUserLogged && parseInt(service.loggedUser.subscription) <= 2;

        if(!service.isUserLogged)
        {
            if(window.localStorage.getItem("latitude"))
            {
                service.latitude = window.localStorage.getItem("latitude");
            }

            if(window.localStorage.getItem("longitude"))
            {
                service.longitude = window.localStorage.getItem("longitude");
            }

            if(window.localStorage.getItem("currentAddress"))
            {
                service.currentAddress = window.localStorage.getItem("currentAddress");
            }

            if(window.localStorage.getItem("postcode"))
            {
                service.postcode = window.localStorage.getItem("postcode");
            }
        }
        else
        {
            service.latitude = service.loggedUser.profile.latitude;
            service.longitude = service.loggedUser.profile.longitude;
            service.postcode = service.loggedUser.profile.postcode;
        }

        service.changeLocationUrl = service.siteUrl.concat("/home/change_location");

    });

    service.clearSessionItems = function()
    {
        window.sessionStorage.removeItem("filterSettings");
        window.sessionStorage.removeItem("store_id");
        window.sessionStorage.removeItem("category_id");
    };

    service.gotoShop = function()
    {
        service.clearSessionItems();
        window.location =  service.siteUrl.concat("/shop");
    };

    service.getUserCoordinates = function(callback)
    {
        // Get the current geo location only if it's not yet the case
        if ("geolocation" in navigator)
        {
            navigator.geolocation.getCurrentPosition(function(position)
            {
                service.longitude = position.coords.longitude;
                service.latitude = position.coords.latitude;
                var geocoder = new google.maps.Geocoder;
                service.geocodeLatLng(geocoder, service.latitude, service.longitude, callback);
            });
        }
    };

    service.getUserCoordinatesFromPostcode = function(postcode, callback)
    {
        var geocoder = new google.maps.Geocoder;

        geocoder.geocode( { 'address': postcode}, function(results, status)
        {
            if (status == google.maps.GeocoderStatus.OK)
            {
                callback(results[0]);
            }
        });
    };

    service.geocodeLatLng = function(geocoder, latitude, longitude, callback)
    {
        var latlng = {lat: latitude, lng: longitude};

        geocoder.geocode({'location': latlng}, function(results, status)
        {
            if (status === 'OK')
            {
                if (results[0])
                {
                    callback(results[0]);
                }
                else
                {

                    window.alert('No results found');
                }
            }
            else
            {
                window.alert('Geocoder failed due to: ' + status);
            }
        });
    };

    service.promptForZipCode = function(ev)
    {
        if(!window.localStorage.getItem("longitude") && !window.localStorage.getItem("latitude") && !service.isUserLogged && false)
        {
            // Appending dialog to document.body to cover sidenav in docs app
            var confirm = $mdDialog.prompt()
              .title('Veillez entrer votre code postal. ')
              .textContent('Ceci vas aider a optimiser les resultats.')
              .placeholder('Votre Code Postale E.g. H1H 1H1')
              .ariaLabel('Code Postale')
              .initialValue('')
              .targetEvent(ev)
              .ok('Valider!')
              .cancel('Annuler');

            $mdDialog.show(confirm).then(function(result)
            {
                var address = result;
                var geocoder = new google.maps.Geocoder();
                geocoder.geocode( { 'address': address}, function(results, status)
                {
                    service.latitude = results[0].geometry.location.lat();
                    service.longitude = results[0].geometry.location.lng();
                    window.localStorage.setItem("longitude", service.longitude);
                    window.localStorage.setItem("latitude", service.latitude);

                    if (status !== google.maps.GeocoderStatus.OK)
                    {
                        service.getUserCoordinates();
                    }

                });

            }, function()
            {
                service.getUserCoordinates();
            });
        }
    };

    service.inMyList = function(product_id)
    {
        if(service.isUserLogged)
        {
            for(var key in service.loggedUser.grocery_list)
            {
                if(parseInt(service.loggedUser.grocery_list[key].id) === parseInt(product_id))
                {
                    return true;
                }
            }
        }

        return false;
    };

    service.selectCategory = function(category)
    {
        service.clearSessionItems();
        var category_id = parseInt(category.id);
        service.recordHit("eapp_product_category ",category_id);
        window.sessionStorage.setItem("category_id", category_id);
        window.sessionStorage.setItem("category_name", category.name);
        window.location =  service.siteUrl.concat("/shop");
    };

    // THis is called for a non logged user to prompt for his zip code
    // If that's not already the case.
    if(typeof service.promptForZipCode !== "undefined")
    {
        service.promptForZipCode();
    }

    service.getActiveUserCoordinates = function()
    {
        var longitude = service.isUserLogged ? service.loggedUser.profile.longitude : service.longitude;
        var latitude = service.isUserLogged ? service.loggedUser.profile.latitude : service.latitude;

        return {
            longitude : longitude,
            latitude : latitude
        };
    };

    service.recordRetailerHit = function(id, distance)
    {
        var formData = new FormData();
        formData.append("id", id);

        if(service.isUserLogged)
        {
            formData.append("distance", distance);
            formData.append("postcode", service.loggedUser.profile.postcode);
            return $http.post(service.siteUrl.concat("eapp/record_retailer_hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

        }
        else
        {
            var geocoder = new google.maps.Geocoder;
            var latlng = {lat: parseFloat(service.latitude), lng: parseFloat(service.longitude)};
            geocoder.geocode({'location': latlng}, function(results, status)
            {

                var postcode = '';

                if (status === 'OK')
                {
                    if(results[0])
                    {
                        if(results[0].address_components[8])
                        {
                            postcode = results[0].address_components[8].long_name;
                        }
                    }
                }

                formData.append("postcode", postcode);
                formData.append("distance", distance);
                return $http.post(service.siteUrl.concat("eapp/record_retailer_hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

            });
        }

    };

    service.recordProductStat = function(storeProductId, type, distance, is_product = false)
    {
        var formData = new FormData();
        formData.append("id", storeProductId);
        formData.append("type", type);
        formData.append("is_product", JSON.stringify(is_product));

        if(service.isUserLogged)
        {
            formData.append("distance", distance);
            formData.append("postcode", service.loggedUser.profile.postcode);
            return $http.post(service.siteUrl.concat("/eapp/record_stat"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});

        }
        else
        {
            var geocoder = new google.maps.Geocoder;
            var latlng = {lat: parseFloat(service.latitude), lng: parseFloat(service.longitude)};
            geocoder.geocode({'location': latlng}, function(results, status)
            {

                var postcode = '';

                if (status === 'OK')
                {
                    if(results[0])
                    {
                        if(results[0].address_components[8])
                        {
                            postcode = results[0].address_components[8].long_name;
                        }
                    }
                }

                formData.append("postcode", postcode);
                formData.append("distance", distance);
                return $http.post(service.siteUrl.concat("/eapp/record_stat"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});
            });
        }

    };

    service.recordHit = function(tableName, id)
    {
        var formData = new FormData();
        formData.append("table_name", tableName);
        formData.append("id", id);
        return $http.post(service.siteUrl.concat("admin/hit"), formData, { transformRequest: angular.identity, headers: {'Content-Type': undefined}});
    };

    return service;
});

<|endoftext|>


<commit_before>/*
 * Apresenta o maior pico de usuários simultâneos
 * registrado no canal
 */

var moment = require('moment');

var TIMEZONE = require(__dirname + '/../../config.json').timezone;

var recorde = function(bot, data, nick, args, end) {
  var record = data.getPath('core.record');

  if (typeof record === 'undefined') {
    record = {
      value: 1,
      when: moment().format()
    }
    data.setPath('core.record', record);
  }

  bot.message('Recorde atual (usuários simultâneos): ' + record.value);
  bot.message('Registrado em ' + moment(record.when).format('DD/MM/YYYY HH:mm:ss'));
  end();
};

exports.run = recorde;<commit_msg>removido TIMEZONE (se tornou desnecessário)<commit_after>/*
 * Apresenta o maior pico de usuários simultâneos
 * registrado no canal
 */

var moment = require('moment');

var recorde = function(bot, data, nick, args, end) {
  var record = data.getPath('core.record');

  if (typeof record === 'undefined') {
    record = {
      value: 1,
      when: moment().format()
    }
    data.setPath('core.record', record);
  }

  bot.message('Recorde atual (usuários simultâneos): ' + record.value);
  bot.message('Registrado em ' + moment(record.when).format('DD/MM/YYYY HH:mm:ss'));
  end();
};

exports.run = recorde;
<|endoftext|>


<commit_before>// @flow
import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';
import * as Immutable from 'immutable';
import generate from 'babel-generator';

import type {ASTNode, ASTPath, Direction} from '../types';
import {ArrayExpression, ObjectExpression} from './collections';
import Keymap from './keymap';
import {BooleanLiteral, NumericLiteral, NullLiteral, StringLiteral} from './literals';
import {
  isBooleanLiteral,
  isNullLiteral,
  isNumericLiteral,
  isArrayExpression,
  isObjectExpression,
  isEditable
} from '../utils/checks';
import navigate from '../navigate/index';
import parse from '../utils/parse';
import styles from '../utils/styles';
import TypeElement, {injectTypeElements} from './type-element';

const {List, Map} = Immutable;

const MAX_HISTORY_LENGTH = 100;

function between(number, lower, upper) {
  return number >= lower && number <= upper;
}

injectTypeElements({
  BooleanLiteral,
  NumericLiteral,
  NullLiteral,
  StringLiteral,
  ArrayExpression,
  ObjectExpression
});

const BooleanNode = new Map({type: 'BooleanLiteral', value: true});
const NumericNode = new Map({type: 'NumericLiteral', value: ''});
const NullNode = new Map({type: 'NullLiteral'});
const StringNode = new Map({type: 'StringLiteral', value: ''});
const ArrayNode = Immutable.fromJS({type: 'ArrayExpression', elements: []});
const ObjectNode = Immutable.fromJS({type: 'ObjectExpression', properties: []});

const Container = styled.div`
  position: relative;
  display: flex;
  flex-direction: row;
  white-space: pre;
  outline: none;
  ${styles.text}
`;

const Button = styled.button`
  position: absolute;
  right: 0;
`;

const Form = styled.form`
  width: 100%;
  padding: 1px;
  overflow-x: auto;
`;

declare type Props = {
  initiallyShowKeymap: boolean,
  defaultValue: {}
};

declare type EditorState = Map<any, any>;

export default class Editor extends PureComponent {

  static defaultProps = {
    initiallShowKeymap: true,
    defaultValue: {}
  };

  state: {
    future: List<EditorState>,
    history: List<EditorState>,
    showKeymap: boolean
  };

  lastDirection: any;

  root: any;

  constructor(props: Props) {
    super(props);
    this.state = {
      future: new List(),
      history: new List([
        new Map({
          root: parse(props.defaultValue),
          selected: new List()
        })
      ]),
      showKeymap: props.initiallyShowKeymap
    };
  }

  componentDidMount() {
    document.addEventListener('copy', this.handleCopy);
    document.addEventListener('cut', this.handleCut);
    document.addEventListener('paste', this.handlePaste);
  }

  componentWillUnmount() {
    document.removeEventListener('copy', this.handleCopy);
    document.removeEventListener('cut', this.handleCut);
    document.removeEventListener('paste', this.handlePaste);
  }

  toggleShowKeymap = () => this.setState(({showKeymap}) => ({
    showKeymap: !showKeymap
  }));

  retainFocus = (el: any) => {
    if (el && !isEditable(this.getSelectedNode())) {
      const div = ReactDOM.findDOMNode(el);
      if (div instanceof HTMLElement) {
        div.focus();
      }
    }
  };

  bindRoot = (el: any) => this.root = el;

  getSelectedNode() {
    const editorState = this.state.history.first();
    return editorState.get('root').getIn(editorState.get('selected'));
  }

  getClosestCollectionPath(root: ASTNode, selected: ASTPath) {
    const selectedNode = root.getIn(selected);

    if (selectedNode) {
      if (isObjectExpression(selectedNode)) {
        return selected.push('properties');
      } else if (isArrayExpression(selectedNode)) {
        return selected.push('elements');
      }
    }

    const index = selected.findLastIndex((key) => ['elements', 'properties'].includes(key));
    return selected.slice(0, index + 1);
  }

  getDirection(key: string) {
    switch (key) {

      case 'ArrowUp':
        return 'UP';

      case 'ArrowDown':
        return 'DOWN';

      case 'ArrowLeft':
        return 'LEFT';

      case 'ArrowRight':
        return 'RIGHT';

      default:
        return;

    }
  }

  addToHistory(updateFn: (root: ASTNode, selected: ASTPath) => any) {
    this.setState(({history}) => {
      const editorState = history.first();
      const root = editorState.get('root');
      let selected = editorState.get('selected');
      if (selected.last() !== 'end' && !root.getIn(selected)) {
        selected = new List();
      }
      const newState = {root, selected, ...updateFn(root, selected)};
      return Immutable.is(selected, newState.selected) && Immutable.is(root, newState.root)
        ? undefined
        : {
          future: new List(),
          history: history.unshift(new Map(newState)).slice(0, MAX_HISTORY_LENGTH)
        };
    });
  }

  updateValue(updateFn: (any) => any) {
    this.addToHistory((root, selected) => ({
      root: root.updateIn(selected.push('value'), updateFn)
    }));
  }

  insert = (node: ASTNode) => this.addToHistory((root, selected) => {
    const collectionPath = this.getClosestCollectionPath(root, selected);
    const itemIndex = selected.last() === 'end'
      ? collectionPath.size
      : selected.get(collectionPath.size) + 1 || 0;

    const isArray = isArrayExpression(root.getIn(collectionPath.slice(0, -1)));

    const newRoot = root.updateIn(collectionPath, (list) => list.insert(
      itemIndex,
      isArray
        ? node
        : new Map({type: 'ObjectProperty', key: StringNode, value: node})
    ));
    let newSelected = collectionPath.push(itemIndex);
    if (!isArray) newSelected = newSelected.push('key');
    return {
      root: newRoot,
      selected: newSelected
    };
  });

  replace(node: Map<string, any>) {
    this.addToHistory((root, selected) => ({
      root: root.updateIn(selected, () => node)
    }));
  }

  changeSelected = (changeFn: (root: ASTNode, selected: ASTPath) => {direction?: Direction, selected: ASTPath}) => {
    return this.addToHistory((root, selected) => {
      const {direction, selected: newSelected} = changeFn(root, selected);
      this.lastDirection = direction;
      return {
        root: root.getIn(selected.push('type')) === 'NumericLiteral'
          ? root.updateIn(selected.push('value'), (value) => parseFloat(value))
          : root,
        selected: newSelected
      };
    });
  };

  deleteSelected() {
    return this.addToHistory((root, selected) => {
      const newRoot = root.deleteIn(
        selected.slice(0, 1 + selected.findLastIndex((value) => typeof value === 'number'))
      );
      const isRootDelete = selected.isEmpty() || (selected.size === 2 && selected.last() === 'end');
      return {
        root: isRootDelete ? NullNode : newRoot,
        selected: isRootDelete || selected.last() === 'end'
          ? new List()
          : navigate('DOWN', newRoot, navigate('UP', root, selected))
      };
    });
  }

  moveSelected(direction: Direction) {
    if (!['UP', 'DOWN'].includes(direction)) return;

    this.addToHistory((root, selected) => {
      const collectionPath = this.getClosestCollectionPath(root, selected);
      const itemIndex = selected.last() === 'end'
        ? collectionPath.size
        : selected.get(collectionPath.size) || 0;
      const itemPath = collectionPath.push(itemIndex);
      const newItemIndex = parseInt(itemIndex + (direction === 'UP' ? -1 : 1), 10);
      const newItemPath = collectionPath.push(newItemIndex);
      const targetItem = root.getIn(newItemPath);

      if (newItemIndex < 0 || !targetItem) {
        return
      }
      return {
        root: root
          .updateIn(itemPath, () => targetItem)
          .updateIn(newItemPath, () => root.getIn(itemPath)),
        selected: selected.update(collectionPath.size, () => newItemIndex)
      }
    });
  }

  undo() {
    this.setState(({future, history}) => ({
      future: future.unshift(history.first()),
      history: history.size > 1 ? history.shift() : history
    }));
  }

  redo() {
    this.setState(({future, history}) => ({
      future: future.shift(),
      history: future.isEmpty() ? history : history.unshift(future.first())
    }));
  }

  handleCopy = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    const editorState = this.state.history.first();
    let selected = editorState.get('selected');
    if (selected.last() === 'end') {
      selected = selected.slice(0, -2);
    }
    event.clipboardData.setData('text/plain',
      generate(editorState.get('root').getIn(selected).toJS()).code
    );
    event.preventDefault();
  };

  handleCut = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    this.handleCopy(event);
    this.deleteSelected();
  };

  handlePaste = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    const clipboardStr = event.clipboardData.getData('text/plain');
    let data;
    try {
      data = JSON.parse(clipboardStr);
    } catch (e) {
      console.error(e);
      return;
    }
    event.preventDefault();
    this.insert(parse(data));
  };

  handleKeyDown = (event: any) => {
    const {altKey, ctrlKey, key} = event;

    const direction = this.getDirection(key);
    const selectedInput: any = this.root.getSelectedInput();
    if (!altKey && direction && (
        ['UP', 'DOWN'].includes(direction) || !selectedInput
        || !between(selectedInput.selectionStart + (direction === 'LEFT' ? -1 : 1), 0, selectedInput.value.length)
    )) {
      event.preventDefault();
      return this.changeSelected((root, selected) => ({
        direction,
        selected: navigate(direction, root, selected)
      }));
    }

    if (isNumericLiteral(this.getSelectedNode()) && ['+', '-'].includes(key)) {
      event.preventDefault();
      return this.updateValue((value) => value + (key === '+' ? 1 : -1));
    }

    if (key === 'd' && ctrlKey) {
      event.preventDefault();
      return this.deleteSelected();
    }

    const selectedIsNull = isNullLiteral(this.getSelectedNode());
    if (selectedIsNull && !isNaN(parseInt(key, 10))) {
      return this.replace(NumericNode);
    }
    if (
      this.state.history.first().get('selected').last() !== 'key' && (
        selectedIsNull || altKey || isBooleanLiteral(this.getSelectedNode())
      )
    ) {
      switch (key) {

        case 's':
        case '\'':
          event.preventDefault();
          return this.replace(StringNode);

        case 'n':
          event.preventDefault();
          return this.replace(NumericNode);

        case 't':
        case 'f':
          event.preventDefault();
          return this.replace(BooleanNode.set('value', Boolean(key === 't')));

        case 'a':
        case '[':
          event.preventDefault();
          return this.replace(ArrayNode);

        case 'o':
        case '{':
          event.preventDefault();
          return this.replace(ObjectNode);

        case '.':
          event.preventDefault();
          return this.replace(NullNode);

        default:

      }
    }

    if (direction && altKey) {
      this.moveSelected(direction);
    }

    if (key === 'Enter') {
      event.preventDefault();
      return this.insert(NullNode);
    }

    if (ctrlKey && key.toLowerCase() === 'z') {
      event.preventDefault();
      return key === 'z' ? this.undo() : this.redo();
    }

  };

  handleChange = ({target: {value}}: any) => {
    this.addToHistory((root, selected) => ({
      root: root.setIn(selected.push('value'), value)
    }));
  };

  handleSelect = (selected: ASTPath) => this.changeSelected(() => ({selected}));

  render() {
    const {history, showKeymap} = this.state;
    const editorState = history.first();
    const selected = editorState.get('selected');
    const isInArray = (selected.last() === 'end' ? selected.slice(0, -2) : selected)
        .findLast((key) => ['elements', 'properties'].includes(key)) === 'elements';
    return (
      <Container tabIndex="0" ref={(el) => this.retainFocus(el)} onKeyDown={this.handleKeyDown}>
        <Button type="button" onClick={this.toggleShowKeymap}>{showKeymap ? 'x' : '?'}</Button>
        <Form onChange={this.handleChange} style={{marginRight: 10}}>
          <TypeElement
            lastDirection={this.lastDirection}
            node={editorState.get('root')}
            selected={selected}
            onSelect={this.handleSelect}
            ref={this.bindRoot}
          />
        </Form>
        {showKeymap && (
          <Keymap {...{isInArray}} selected={selected} selectedNode={this.getSelectedNode()}/>
        )}
      </Container>
    );
  }

}<commit_msg>cast to number when changing item type to numeric<commit_after>// @flow
import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';
import * as Immutable from 'immutable';
import generate from 'babel-generator';

import type {ASTNode, ASTPath, Direction} from '../types';
import {ArrayExpression, ObjectExpression} from './collections';
import Keymap from './keymap';
import {BooleanLiteral, NumericLiteral, NullLiteral, StringLiteral} from './literals';
import {
  isBooleanLiteral,
  isNullLiteral,
  isNumericLiteral,
  isArrayExpression,
  isObjectExpression,
  isEditable
} from '../utils/checks';
import navigate from '../navigate/index';
import parse from '../utils/parse';
import styles from '../utils/styles';
import TypeElement, {injectTypeElements} from './type-element';

const {List, Map} = Immutable;

const MAX_HISTORY_LENGTH = 100;

function between(number, lower, upper) {
  return number >= lower && number <= upper;
}

injectTypeElements({
  BooleanLiteral,
  NumericLiteral,
  NullLiteral,
  StringLiteral,
  ArrayExpression,
  ObjectExpression
});

const BooleanNode = new Map({type: 'BooleanLiteral', value: true});
const NumericNode = new Map({type: 'NumericLiteral', value: ''});
const NullNode = new Map({type: 'NullLiteral'});
const StringNode = new Map({type: 'StringLiteral', value: ''});
const ArrayNode = Immutable.fromJS({type: 'ArrayExpression', elements: []});
const ObjectNode = Immutable.fromJS({type: 'ObjectExpression', properties: []});

const Container = styled.div`
  position: relative;
  display: flex;
  flex-direction: row;
  white-space: pre;
  outline: none;
  ${styles.text}
`;

const Button = styled.button`
  position: absolute;
  right: 0;
`;

const Form = styled.form`
  width: 100%;
  padding: 1px;
  overflow-x: auto;
`;

declare type Props = {
  initiallyShowKeymap: boolean,
  defaultValue: {}
};

declare type EditorState = Map<any, any>;

export default class Editor extends PureComponent {

  static defaultProps = {
    initiallShowKeymap: true,
    defaultValue: {}
  };

  state: {
    future: List<EditorState>,
    history: List<EditorState>,
    showKeymap: boolean
  };

  lastDirection: any;

  root: any;

  constructor(props: Props) {
    super(props);
    this.state = {
      future: new List(),
      history: new List([
        new Map({
          root: parse(props.defaultValue),
          selected: new List()
        })
      ]),
      showKeymap: props.initiallyShowKeymap
    };
  }

  componentDidMount() {
    document.addEventListener('copy', this.handleCopy);
    document.addEventListener('cut', this.handleCut);
    document.addEventListener('paste', this.handlePaste);
  }

  componentWillUnmount() {
    document.removeEventListener('copy', this.handleCopy);
    document.removeEventListener('cut', this.handleCut);
    document.removeEventListener('paste', this.handlePaste);
  }

  toggleShowKeymap = () => this.setState(({showKeymap}) => ({
    showKeymap: !showKeymap
  }));

  retainFocus = (el: any) => {
    if (el && !isEditable(this.getSelectedNode())) {
      const div = ReactDOM.findDOMNode(el);
      if (div instanceof HTMLElement) {
        div.focus();
      }
    }
  };

  bindRoot = (el: any) => this.root = el;

  getSelectedNode() {
    const editorState = this.state.history.first();
    return editorState.get('root').getIn(editorState.get('selected'));
  }

  getClosestCollectionPath(root: ASTNode, selected: ASTPath) {
    const selectedNode = root.getIn(selected);

    if (selectedNode) {
      if (isObjectExpression(selectedNode)) {
        return selected.push('properties');
      } else if (isArrayExpression(selectedNode)) {
        return selected.push('elements');
      }
    }

    const index = selected.findLastIndex((key) => ['elements', 'properties'].includes(key));
    return selected.slice(0, index + 1);
  }

  getDirection(key: string) {
    switch (key) {

      case 'ArrowUp':
        return 'UP';

      case 'ArrowDown':
        return 'DOWN';

      case 'ArrowLeft':
        return 'LEFT';

      case 'ArrowRight':
        return 'RIGHT';

      default:
        return;

    }
  }

  addToHistory(updateFn: (root: ASTNode, selected: ASTPath) => any) {
    this.setState(({history}) => {
      const editorState = history.first();
      const root = editorState.get('root');
      let selected = editorState.get('selected');
      if (selected.last() !== 'end' && !root.getIn(selected)) {
        selected = new List();
      }
      const newState = {root, selected, ...updateFn(root, selected)};
      return Immutable.is(selected, newState.selected) && Immutable.is(root, newState.root)
        ? undefined
        : {
          future: new List(),
          history: history.unshift(new Map(newState)).slice(0, MAX_HISTORY_LENGTH)
        };
    });
  }

  updateValue(updateFn: (any) => any) {
    this.addToHistory((root, selected) => ({
      root: root.updateIn(selected.push('value'), updateFn)
    }));
  }

  insert = (node: ASTNode) => this.addToHistory((root, selected) => {
    const collectionPath = this.getClosestCollectionPath(root, selected);
    const itemIndex = selected.last() === 'end'
      ? collectionPath.size
      : selected.get(collectionPath.size) + 1 || 0;

    const isArray = isArrayExpression(root.getIn(collectionPath.slice(0, -1)));

    const newRoot = root.updateIn(collectionPath, (list) => list.insert(
      itemIndex,
      isArray
        ? node
        : new Map({type: 'ObjectProperty', key: StringNode, value: node})
    ));
    let newSelected = collectionPath.push(itemIndex);
    if (!isArray) newSelected = newSelected.push('key');
    return {
      root: newRoot,
      selected: newSelected
    };
  });

  replace(node: Map<string, any>) {
    this.addToHistory((root, selected) => ({
      root: root.updateIn(selected, () => node)
    }));
  }

  changeSelected = (changeFn: (root: ASTNode, selected: ASTPath) => {direction?: Direction, selected: ASTPath}) => {
    return this.addToHistory((root, selected) => {
      const {direction, selected: newSelected} = changeFn(root, selected);
      this.lastDirection = direction;
      return {
        root: root.getIn(selected.push('type')) === 'NumericLiteral'
          ? root.updateIn(selected.push('value'), (value) => parseFloat(value))
          : root,
        selected: newSelected
      };
    });
  };

  deleteSelected() {
    return this.addToHistory((root, selected) => {
      const newRoot = root.deleteIn(
        selected.slice(0, 1 + selected.findLastIndex((value) => typeof value === 'number'))
      );
      const isRootDelete = selected.isEmpty() || (selected.size === 2 && selected.last() === 'end');
      return {
        root: isRootDelete ? NullNode : newRoot,
        selected: isRootDelete || selected.last() === 'end'
          ? new List()
          : navigate('DOWN', newRoot, navigate('UP', root, selected))
      };
    });
  }

  moveSelected(direction: Direction) {
    if (!['UP', 'DOWN'].includes(direction)) return;

    this.addToHistory((root, selected) => {
      const collectionPath = this.getClosestCollectionPath(root, selected);
      const itemIndex = selected.last() === 'end'
        ? collectionPath.size
        : selected.get(collectionPath.size) || 0;
      const itemPath = collectionPath.push(itemIndex);
      const newItemIndex = parseInt(itemIndex + (direction === 'UP' ? -1 : 1), 10);
      const newItemPath = collectionPath.push(newItemIndex);
      const targetItem = root.getIn(newItemPath);

      if (newItemIndex < 0 || !targetItem) {
        return
      }
      return {
        root: root
          .updateIn(itemPath, () => targetItem)
          .updateIn(newItemPath, () => root.getIn(itemPath)),
        selected: selected.update(collectionPath.size, () => newItemIndex)
      }
    });
  }

  undo() {
    this.setState(({future, history}) => ({
      future: future.unshift(history.first()),
      history: history.size > 1 ? history.shift() : history
    }));
  }

  redo() {
    this.setState(({future, history}) => ({
      future: future.shift(),
      history: future.isEmpty() ? history : history.unshift(future.first())
    }));
  }

  handleCopy = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    const editorState = this.state.history.first();
    let selected = editorState.get('selected');
    if (selected.last() === 'end') {
      selected = selected.slice(0, -2);
    }
    event.clipboardData.setData('text/plain',
      generate(editorState.get('root').getIn(selected).toJS()).code
    );
    event.preventDefault();
  };

  handleCut = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    this.handleCopy(event);
    this.deleteSelected();
  };

  handlePaste = (event: any) => {
    if (isEditable(this.getSelectedNode())) {
      return;
    }

    const clipboardStr = event.clipboardData.getData('text/plain');
    let data;
    try {
      data = JSON.parse(clipboardStr);
    } catch (e) {
      console.error(e);
      return;
    }
    event.preventDefault();
    this.insert(parse(data));
  };

  handleKeyDown = (event: any) => {
    const {altKey, ctrlKey, key} = event;

    const direction = this.getDirection(key);
    const selectedInput: any = this.root.getSelectedInput();
    if (!altKey && direction && (
        ['UP', 'DOWN'].includes(direction) || !selectedInput
        || !between(selectedInput.selectionStart + (direction === 'LEFT' ? -1 : 1), 0, selectedInput.value.length)
    )) {
      event.preventDefault();
      return this.changeSelected((root, selected) => ({
        direction,
        selected: navigate(direction, root, selected)
      }));
    }

    if (isNumericLiteral(this.getSelectedNode()) && ['+', '-'].includes(key)) {
      event.preventDefault();
      return this.updateValue((value) => value + (key === '+' ? 1 : -1));
    }

    if (key === 'd' && ctrlKey) {
      event.preventDefault();
      return this.deleteSelected();
    }

    const selectedIsNull = isNullLiteral(this.getSelectedNode());
    if (selectedIsNull && !isNaN(parseInt(key, 10))) {
      return this.replace(NumericNode);
    }
    if (
      this.state.history.first().get('selected').last() !== 'key' && (
        selectedIsNull || altKey || isBooleanLiteral(this.getSelectedNode())
      )
    ) {
      switch (key) {

        case 's':
        case '\'':
          event.preventDefault();
          return this.replace(StringNode);

        case 'n':
          event.preventDefault();
          return this.replace(
            NumericNode.set('value', Number(this.getSelectedNode().get('value')) || '')
          );

        case 't':
        case 'f':
          event.preventDefault();
          return this.replace(BooleanNode.set('value', Boolean(key === 't')));

        case 'a':
        case '[':
          event.preventDefault();
          return this.replace(ArrayNode);

        case 'o':
        case '{':
          event.preventDefault();
          return this.replace(ObjectNode);

        case '.':
          event.preventDefault();
          return this.replace(NullNode);

        default:

      }
    }

    if (direction && altKey) {
      this.moveSelected(direction);
    }

    if (key === 'Enter') {
      event.preventDefault();
      return this.insert(NullNode);
    }

    if (ctrlKey && key.toLowerCase() === 'z') {
      event.preventDefault();
      return key === 'z' ? this.undo() : this.redo();
    }

  };

  handleChange = ({target: {value}}: any) => {
    this.addToHistory((root, selected) => ({
      root: root.setIn(selected.push('value'), value)
    }));
  };

  handleSelect = (selected: ASTPath) => this.changeSelected(() => ({selected}));

  render() {
    const {history, showKeymap} = this.state;
    const editorState = history.first();
    const selected = editorState.get('selected');
    const isInArray = (selected.last() === 'end' ? selected.slice(0, -2) : selected)
        .findLast((key) => ['elements', 'properties'].includes(key)) === 'elements';
    return (
      <Container tabIndex="0" ref={(el) => this.retainFocus(el)} onKeyDown={this.handleKeyDown}>
        <Button type="button" onClick={this.toggleShowKeymap}>{showKeymap ? 'x' : '?'}</Button>
        <Form onChange={this.handleChange} style={{marginRight: 10}}>
          <TypeElement
            lastDirection={this.lastDirection}
            node={editorState.get('root')}
            selected={selected}
            onSelect={this.handleSelect}
            ref={this.bindRoot}
          />
        </Form>
        {showKeymap && (
          <Keymap {...{isInArray}} selected={selected} selectedNode={this.getSelectedNode()}/>
        )}
      </Container>
    );
  }

}<|endoftext|>


<commit_before>/**
 * CTA component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * Internal dependencies
 */
import Link from '../Link';
import Button from '../Button';

const CTA = ( {
	title,
	description,
	ctaLink,
	ctaLabel,
	ctaLinkExternal,
	ctaType,
	error,
	onClick,
	'aria-label': ariaLabel,
} ) => (

		{ title &&
{ title }
 }
		{ description && typeof description === 'string' && (

{ description }

		) }
		{ description && typeof description !== 'string' && (

				{ description }

		) }
		{ ctaLabel && ctaType === 'button' && (
			<Button onClick={ onClick }>{ ctaLabel }</Button>
		) }
		{ ctaLabel && ctaType === 'link' && (
			<Link
				href={ ctaLink }
				onClick={ onClick }
				inverse={ ! error }
				caps
				arrow
				aria-label={ ariaLabel }
				external={ ctaLinkExternal }
				hideExternalIndicator={ ctaLinkExternal }
			>
				{ ctaLabel }
			</Link>
		) }

);

CTA.propTypes = {
	title: PropTypes.string.isRequired,
	description: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
	ctaLink: PropTypes.string,
	ctaLinkExternal: PropTypes.bool,
	ctaLabel: PropTypes.string,
	ctaType: PropTypes.string,
	'aria-label': PropTypes.string,
	error: PropTypes.bool,
	onClick: PropTypes.func,
};

CTA.defaultProps = {
	title: '',
	description: '',
	ctaLink: '',
	ctaLabel: '',
	ctaType: 'link',
	error: false,
	onClick: () => {},
};

export default CTA;
<commit_msg>Add aria-label and href props for the Button component.<commit_after>/**
 * CTA component.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * Internal dependencies
 */
import Link from '../Link';
import Button from '../Button';

const CTA = ( {
	title,
	description,
	ctaLink,
	ctaLabel,
	ctaLinkExternal,
	ctaType,
	error,
	onClick,
	'aria-label': ariaLabel,
} ) => (

		{ title &&
{ title }
 }
		{ description && typeof description === 'string' && (

{ description }

		) }
		{ description && typeof description !== 'string' && (

				{ description }

		) }
		{ ctaLabel && ctaType === 'button' && (
			<Button
				aria-label={ ariaLabel }
				href={ ctaLink }
				onClick={ onClick }
			>
				{ ctaLabel }
			</Button>
		) }
		{ ctaLabel && ctaType === 'link' && (
			<Link
				href={ ctaLink }
				onClick={ onClick }
				inverse={ ! error }
				caps
				arrow
				aria-label={ ariaLabel }
				external={ ctaLinkExternal }
				hideExternalIndicator={ ctaLinkExternal }
			>
				{ ctaLabel }
			</Link>
		) }

);

CTA.propTypes = {
	title: PropTypes.string.isRequired,
	description: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
	ctaLink: PropTypes.string,
	ctaLinkExternal: PropTypes.bool,
	ctaLabel: PropTypes.string,
	ctaType: PropTypes.string,
	'aria-label': PropTypes.string,
	error: PropTypes.bool,
	onClick: PropTypes.func,
};

CTA.defaultProps = {
	title: '',
	description: '',
	ctaLink: '',
	ctaLabel: '',
	ctaType: 'link',
	error: false,
	onClick: () => {},
};

export default CTA;
<|endoftext|>


<commit_before>import { assign } from '@ember/polyfills';
import { copy } from '@ember/object/internals';
import Mixin from '@ember/object/mixin';
import { isNone, isEmpty } from '@ember/utils';
import { isArray } from '@ember/array';
import {
  queryParamPropertyName,
  ajaxOptionsPropertyName
} from '../property-names';

const evaluateFunctions = function (object, record) {
  if (isArray(object)) {
    object.forEach(function (element) {
      if (typeof element === 'object') {
        evaluateFunctions(element, record);
      }
    });
  } else if (!isNone(object)) {
    Object.keys(object).forEach(function (key) {
      if (!object.hasOwnProperty(key)) {
        return;
      }
      const value = object[key];
      if (typeof value === 'function') {
        object[key] = value.apply(record);
      } else if (typeof value === 'object') {
        evaluateFunctions(value, record);
      }
    });
  }
};

/**
 * Mixin for DS.RESTAdapter.
 */
export default Mixin.create({
  findHasMany: function (store, snapshot, url, relationship) {
    const id = snapshot.id;
    const type = snapshot.modelName;

    url = this.urlPrefix(url, this.buildURL(type, id, null, 'findHasMany'));
    const query = this.buildRelationshipQuery(snapshot, relationship);

    const options = {data: query};
    snapshot.record.set(ajaxOptionsPropertyName(relationship.key), options);
    return this.ajax(url, 'GET', options);
  },
  findBelongsTo: function (store, snapshot, url, relationship) {
    const id = snapshot.id;
    const type = snapshot.modelName;

    url = this.urlPrefix(url, this.buildURL(type, id, null, 'findBelongsTo'));
    const query = this.buildRelationshipQuery(snapshot, relationship);

    const options = {data: query};
    snapshot.record.set(ajaxOptionsPropertyName(relationship.key), options);
    return this.ajax(url, 'GET', options);
  },
  buildRelationshipQuery: function (snapshot, relationship) {
    const data = {};

    //add query parameters from the model mixin's query function
    const queryParams = snapshot.record.get(queryParamPropertyName(relationship.key));
    if (!isEmpty(queryParams)) {
      data = copy(queryParams, true);
    }

    //add query parameters defined in the model itself by the 'parameters' option
    const relationshipParams = relationship.options.parameters;
    if (!isEmpty(relationshipParams)) {
      assign(data, relationshipParams);
    }

    //replace any functions in the data with their return value
    evaluateFunctions(data, snapshot.record);
    return data;
  },
  ajaxOptions: function () {
    const ajaxOptions = this._super(...arguments);
    const defaultBeforeSend = ajaxOptions.beforeSend || function () {
    };
    ajaxOptions.beforeSend = function (jqXHR) {
      defaultBeforeSend(...arguments);
      //store the jqXHR in the options object, which in turn is stored in the model itself, so the model mixin can abort it
      ajaxOptions.jqXHR = jqXHR;
    };
    return ajaxOptions;
  }
});
<commit_msg>Fix mistakingly defining const when let is needed<commit_after>import { assign } from '@ember/polyfills';
import { copy } from '@ember/object/internals';
import Mixin from '@ember/object/mixin';
import { isNone, isEmpty } from '@ember/utils';
import { isArray } from '@ember/array';
import {
  queryParamPropertyName,
  ajaxOptionsPropertyName
} from '../property-names';

const evaluateFunctions = function (object, record) {
  if (isArray(object)) {
    object.forEach(function (element) {
      if (typeof element === 'object') {
        evaluateFunctions(element, record);
      }
    });
  } else if (!isNone(object)) {
    Object.keys(object).forEach(function (key) {
      if (!object.hasOwnProperty(key)) {
        return;
      }
      const value = object[key];
      if (typeof value === 'function') {
        object[key] = value.apply(record);
      } else if (typeof value === 'object') {
        evaluateFunctions(value, record);
      }
    });
  }
};

/**
 * Mixin for DS.RESTAdapter.
 */
export default Mixin.create({
  findHasMany: function (store, snapshot, url, relationship) {
    const id = snapshot.id;
    const type = snapshot.modelName;

    url = this.urlPrefix(url, this.buildURL(type, id, null, 'findHasMany'));
    const query = this.buildRelationshipQuery(snapshot, relationship);

    const options = {data: query};
    snapshot.record.set(ajaxOptionsPropertyName(relationship.key), options);
    return this.ajax(url, 'GET', options);
  },
  findBelongsTo: function (store, snapshot, url, relationship) {
    const id = snapshot.id;
    const type = snapshot.modelName;

    url = this.urlPrefix(url, this.buildURL(type, id, null, 'findBelongsTo'));
    const query = this.buildRelationshipQuery(snapshot, relationship);

    const options = {data: query};
    snapshot.record.set(ajaxOptionsPropertyName(relationship.key), options);
    return this.ajax(url, 'GET', options);
  },
  buildRelationshipQuery: function (snapshot, relationship) {
    let data = {};

    //add query parameters from the model mixin's query function
    const queryParams = snapshot.record.get(queryParamPropertyName(relationship.key));
    if (!isEmpty(queryParams)) {
      data = copy(queryParams, true);
    }

    //add query parameters defined in the model itself by the 'parameters' option
    const relationshipParams = relationship.options.parameters;
    if (!isEmpty(relationshipParams)) {
      assign(data, relationshipParams);
    }

    //replace any functions in the data with their return value
    evaluateFunctions(data, snapshot.record);
    return data;
  },
  ajaxOptions: function () {
    const ajaxOptions = this._super(...arguments);
    const defaultBeforeSend = ajaxOptions.beforeSend || function () {
    };
    ajaxOptions.beforeSend = function (jqXHR) {
      defaultBeforeSend(...arguments);
      //store the jqXHR in the options object, which in turn is stored in the model itself, so the model mixin can abort it
      ajaxOptions.jqXHR = jqXHR;
    };
    return ajaxOptions;
  }
});
<|endoftext|>


<commit_before>/**
 * `withData` tests.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * WordPress dependencies
 */
import { addAction, removeAllActions, removeAllFilters } from '@wordpress/hooks';

/**
 * Internal dependencies
 */
import withData from './withdata';
import { render, act } from '../../../../tests/js/test-utils';
import dataAPI, { TYPE_MODULES } from '../data';
import { getCacheKey } from '../data/cache';

const collectModuleData = dataAPI.collectModuleData.bind( dataAPI );

describe( 'withData', () => {
	let TestComponent;
	const context = 'TestContext';
	// A dummy dateRange is used to allow for getting a stable cache key for the dataset.
	const dateRange = 'last-99-days';
	const loadingNode =
loading
;
	const testModule = {
		slug: 'test',
		name: 'Test Module',
		active: true,
		setupComplete: true,
	};
	const testModuleAlt = {
		slug: 'test-alt',
		name: 'Alternate Test Module',
		active: true,
		setupComplete: true,
	};

	const createDataset = ( type, identifier, datapoint, data, _context = context ) => ( { type, identifier, datapoint, data, context: _context } );
	const getCacheKeyForDataset = ( { type, identifier, datapoint, data } ) => getCacheKey( type, identifier, datapoint, data );

	beforeEach( () => {
		TestComponent = jest.fn(
			() =>
Test

		);
		removeAllActions( 'googlesitekit.dataLoaded' );
		removeAllFilters( `googlesitekit.module${ context }DataRequest` );
	} );

	afterEach( () => {
		delete global._googlesitekitLegacyData.modules[ testModule.slug ];
		delete global._googlesitekitLegacyData.modules[ testModuleAlt.slug ];
	} );

	it( 'renders the loading when there is no data yet', () => {
		const WrappedComponent = withData( TestComponent, [], loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.firstChild ).toBe( queryByTestID( 'loading-component' ) );
	} );

	it( 'renders the data dependent component when there is data', async () => {
		const [ type, identifier, datapoint, data ] = [ 'test', 'test-identifier', 'test-datapoint', { dateRange } ];
		const dataset = { type, identifier, datapoint, data, context };
		const WrappedComponent = withData( TestComponent, [ dataset ] );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const responseData = { foo: 'bar' };
		fetchMock.postOnce(
			/^\/google-site-kit\/v1\/data/,
			{ body: { [ getCacheKeyForDataset( dataset ) ]: responseData } }
		);
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( container.firstChild ).toBe( queryByTestID( 'test-component' ) );
		expect( queryByTestID( 'loading-component' ) ).not.toBeInTheDocument();
		expect( TestComponent.mock.calls[ 0 ][ 0 ].data ).toEqual( responseData );
		expect( TestComponent.mock.calls[ 0 ][ 0 ].datapoint ).toEqual( datapoint );
	} );

	it( 'renders the setup incomplete component when requesting data for a module with incomplete setup', () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = { ...testModule, setupComplete: false };
		const dataset = createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } );
		const WrappedComponent = withData( TestComponent, [ dataset ] );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		collectModuleData( context );

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module activation' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( 'Test Module module needs to be configured' );
		expect( fetchMock ).not.toHaveFetched();
	} );

	it( 'renders the setup incomplete component when requesting data from any module with incomplete setup', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = { ...testModule, setupComplete: false };
		global._googlesitekitLegacyData.modules[ testModuleAlt.slug ] = testModuleAlt;

		const requests = [
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } ),
			createDataset( TYPE_MODULES, testModuleAlt.slug, 'test-datapoint', { dateRange } ),
		];
		const WrappedComponent = withData( TestComponent, requests, loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		// testModuleAlt's request will not be filtered out because its setup is complete so a response is needed.
		const body = {
			[ getCacheKeyForDataset( requests[ 1 ] ) ]: { foo: 'bar' },
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module activation' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( 'Test Module module needs to be configured' );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
	} );

	it( 'renders the error component when an error is returned for any request', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = testModule;

		const requests = [
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint-a', { dateRange } ),
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint-b', { dateRange } ),
		];
		const WrappedComponent = withData( TestComponent, requests, loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const error = { code: 'test_error', message: 'Error!', data: { status: 500 } };
		const body = {
			[ getCacheKeyForDataset( requests[ 0 ] ) ]: error,
			[ getCacheKeyForDataset( requests[ 1 ] ) ]: { foo: 'some data' },
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Data error in Test Module' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( error.message );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
		expect( console ).toHaveWarned();
	} );

	it( 'renders the no data component when isDataZero returns `true` for the returned data', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = testModule;
		const dataset = createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } );
		const isDataZero = jest.fn( ( data ) => data.hasAnything === 'no' );
		const WrappedComponent = withData( TestComponent, [ dataset ], loadingNode, {}, isDataZero );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const responseData = { hasAnything: 'no', foo: 'bar', something: 'else' };
		const body = {
			[ getCacheKeyForDataset( dataset ) ]: responseData,
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( isDataZero ).toHaveBeenCalledWith( responseData, 'test-datapoint', dataset );
		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module Gathering Data' );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
	} );
} );
<commit_msg>Add test for multiple singular or multiple contexts.<commit_after>/**
 * `withData` tests.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * WordPress dependencies
 */
import { addAction, hasFilter, removeAllActions, removeAllFilters } from '@wordpress/hooks';

/**
 * Internal dependencies
 */
import withData from './withdata';
import { render, act } from '../../../../tests/js/test-utils';
import dataAPI, { TYPE_MODULES } from '../data';
import { getCacheKey } from '../data/cache';

const collectModuleData = dataAPI.collectModuleData.bind( dataAPI );

describe( 'withData', () => {
	let TestComponent;
	const context = 'TestContext';
	// A dummy dateRange is used to allow for getting a stable cache key for the dataset.
	const dateRange = 'last-99-days';
	const loadingNode =
loading
;
	const testModule = {
		slug: 'test',
		name: 'Test Module',
		active: true,
		setupComplete: true,
	};
	const testModuleAlt = {
		slug: 'test-alt',
		name: 'Alternate Test Module',
		active: true,
		setupComplete: true,
	};

	const createDataset = ( type, identifier, datapoint, data, _context = context ) => ( { type, identifier, datapoint, data, context: _context } );
	const getCacheKeyForDataset = ( { type, identifier, datapoint, data } ) => getCacheKey( type, identifier, datapoint, data );

	beforeEach( () => {
		TestComponent = jest.fn(
			() =>
Test

		);
		removeAllActions( 'googlesitekit.dataLoaded' );
		removeAllFilters( `googlesitekit.module${ context }DataRequest` );
	} );

	afterEach( () => {
		delete global._googlesitekitLegacyData.modules[ testModule.slug ];
		delete global._googlesitekitLegacyData.modules[ testModuleAlt.slug ];
	} );

	it( 'supports datasets with single or multiple contexts', () => {
		const datasetCommon = [ 'test', 'test-identifier', 'test-datapoint', {} ];
		const singleContextDataset = createDataset( ...datasetCommon, 'context_a' );
		const multipleContextDataset = createDataset( ...datasetCommon, [ 'context_y', 'context_z' ] );
		const hasFilterForContext = ( _context ) => hasFilter( `googlesitekit.module${ _context }DataRequest` );

		const WrappedComponent = withData( TestComponent, [ singleContextDataset, multipleContextDataset ] );

		expect( hasFilterForContext( 'context_a' ) ).toBe( false );
		expect( hasFilterForContext( 'context_y' ) ).toBe( false );
		expect( hasFilterForContext( 'context_z' ) ).toBe( false );

		render( <WrappedComponent /> );

		expect( hasFilterForContext( 'context_a' ) ).toBe( true );
		expect( hasFilterForContext( 'context_y' ) ).toBe( true );
		expect( hasFilterForContext( 'context_z' ) ).toBe( true );
	} );

	it( 'renders the loading when there is no data yet', () => {
		const WrappedComponent = withData( TestComponent, [], loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.firstChild ).toBe( queryByTestID( 'loading-component' ) );
	} );

	it( 'renders the data dependent component when there is data', async () => {
		const [ type, identifier, datapoint, data ] = [ 'test', 'test-identifier', 'test-datapoint', { dateRange } ];
		const dataset = { type, identifier, datapoint, data, context };
		const WrappedComponent = withData( TestComponent, [ dataset ] );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const responseData = { foo: 'bar' };
		fetchMock.postOnce(
			/^\/google-site-kit\/v1\/data/,
			{ body: { [ getCacheKeyForDataset( dataset ) ]: responseData } }
		);
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( container.firstChild ).toBe( queryByTestID( 'test-component' ) );
		expect( queryByTestID( 'loading-component' ) ).not.toBeInTheDocument();
		expect( TestComponent.mock.calls[ 0 ][ 0 ].data ).toEqual( responseData );
		expect( TestComponent.mock.calls[ 0 ][ 0 ].datapoint ).toEqual( datapoint );
	} );

	it( 'renders the setup incomplete component when requesting data for a module with incomplete setup', () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = { ...testModule, setupComplete: false };
		const dataset = createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } );
		const WrappedComponent = withData( TestComponent, [ dataset ] );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		collectModuleData( context );

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module activation' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( 'Test Module module needs to be configured' );
		expect( fetchMock ).not.toHaveFetched();
	} );

	it( 'renders the setup incomplete component when requesting data from any module with incomplete setup', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = { ...testModule, setupComplete: false };
		global._googlesitekitLegacyData.modules[ testModuleAlt.slug ] = testModuleAlt;

		const requests = [
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } ),
			createDataset( TYPE_MODULES, testModuleAlt.slug, 'test-datapoint', { dateRange } ),
		];
		const WrappedComponent = withData( TestComponent, requests, loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		// testModuleAlt's request will not be filtered out because its setup is complete so a response is needed.
		const body = {
			[ getCacheKeyForDataset( requests[ 1 ] ) ]: { foo: 'bar' },
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module activation' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( 'Test Module module needs to be configured' );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
	} );

	it( 'renders the error component when an error is returned for any request', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = testModule;

		const requests = [
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint-a', { dateRange } ),
			createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint-b', { dateRange } ),
		];
		const WrappedComponent = withData( TestComponent, requests, loadingNode );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const error = { code: 'test_error', message: 'Error!', data: { status: 500 } };
		const body = {
			[ getCacheKeyForDataset( requests[ 0 ] ) ]: error,
			[ getCacheKeyForDataset( requests[ 1 ] ) ]: { foo: 'some data' },
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Data error in Test Module' );
		expect( container.querySelector( '.googlesitekit-cta__description' ) ).toHaveTextContent( error.message );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
		expect( console ).toHaveWarned();
	} );

	it( 'renders the no data component when isDataZero returns `true` for the returned data', async () => {
		global._googlesitekitLegacyData.modules[ testModule.slug ] = testModule;
		const dataset = createDataset( TYPE_MODULES, testModule.slug, 'test-datapoint', { dateRange } );
		const isDataZero = jest.fn( ( data ) => data.hasAnything === 'no' );
		const WrappedComponent = withData( TestComponent, [ dataset ], loadingNode, {}, isDataZero );

		const { container, queryByTestID } = render( <WrappedComponent /> );

		const responseData = { hasAnything: 'no', foo: 'bar', something: 'else' };
		const body = {
			[ getCacheKeyForDataset( dataset ) ]: responseData,
		};
		fetchMock.postOnce( /^\/google-site-kit\/v1\/data/, { body } );
		await act(
			() => new Promise( ( resolve ) => {
				addAction( 'googlesitekit.dataLoaded', 'test.resolve', resolve );
				collectModuleData( context );
			} )
		);

		expect( isDataZero ).toHaveBeenCalledWith( responseData, 'test-datapoint', dataset );
		expect( queryByTestID( 'test-component' ) ).not.toBeInTheDocument();
		expect( container.querySelector( '.googlesitekit-cta__title' ) ).toHaveTextContent( 'Test Module Gathering Data' );
		expect( fetchMock ).toHaveFetched( /^\/google-site-kit\/v1\/data/ );
	} );
} );
<|endoftext|>