<commit_before>/*
 * Project: Ajax Hustle Bunny
 *
 * Purpose: Ajax Hustle Bunny handles the hopping around from view to view using Ajax. It gives able apps
 *          the ajax ability to handle history. Ajax Hustle Bunny also makes sure that events can be handled
 *          throughout both Ajax and non-ajax switching of views and that it can do so without causing memory
 *          issues caused by listening for events on elements that have been removed from the page flow or
 *          from stacking identical events on top of elements which can occur on some SPA's when navigation
 *          to one page reoccurs.
 * Author: Michael Rosata @ TqSoft
 *
 * License: MIT 2015 created by Michael Rosata. mrosata1984@gmail.com
 *
 * Instructions:
 *
 *      --- To make a link use Ajax, simply add the 'ahb-click' class and data-ahb-action attribute
 *
 *        ex: Hop-Hop!
 *            this will load into the main container www.example.com/rabbit/hole
 *            and it will inform any subscribers to the fact that the view has been changed so that your
 *            class/objects can react/behave appropriately.
 *
 *      --- To subscribe to ajax events made by the AjaxHustleBunny use the ajaxEvents subscribe method.
 *
 *        ex: ajaxHustleBunny.ajaxEvents.subscribe('any-handle', myObject.myHandler);
 *            now, when a new view is loaded, myObject will have the chance to take any action needed
 *            based on the "endpoint", the endpoint for /rabbit/hole = "rabbit/hole". So that would be
 *            the argument passed to myObject.myHandler after ajax events.
 *
 *      ---- To when/did
 *           //Todo: Explain the when/did subscription handler. It's basically a spring loaded event. Once the spring pops anyone can clearly see that yes it did occur, but it can't be listened to twice. It is extremely useful in handling load order throughout applications that may load many scripts from various domains. The scripts may not always load in the same order but you want to start each event as soon as possible. So this pattern will allow for your setup functions to fire when loaded and declare dependancies. If the dependancies have been met already then it will be told that. If not then it can set a callback to fire when dependancies are met. I like to think of this pattern as a dominoe effect.
 *
 *      //TODO: Might be good to have an event that fires before the ajax request is made. However since there is no need for it yet, I'm not going to create more noise.
 */

var ajaxHustleBunny = (function($, window, undefined) {

  // Sometimes certain pages need body classes. If that's the case, map the $action to the class
  var bodyClassMap = {
    'complaints/index' : 'controls'
  };

  function filterEndpoint(endpoint, action) {
    if (/^\/[^/]+\/?$/.test(endpoint)) {
      endpoint = endpoint.replace(/(^\/[^/]+)\/?$/, '$1/index');
    }
    if (/^\/[^/]+\/[^/]+\/[^/]+\/?$/.test(endpoint)) {
      endpoint = endpoint.replace(/^(\/[^/]+\/[^/]+)(\/[0-9]+\/?)$/, '$1');
    }
    if (endpoint.length && endpoint[0] == '/')
      endpoint = endpoint.substr(1);
    // Don't want id on endpoint IE: controller/action/id <--- remove /id
    var ep1 = endpoint.split('/')[0] || '';
    var ep2 = endpoint.split('/')[1] || '';
    endpoint = ep1 + '/' + ep2;
    return endpoint;
  }

  /**
   * return the action from a url endpoint
   * @param endpoint
   * @return string|integer - empty string or action if exists.
   */
  function filterAction(endpoint) {
    var action = '';
    if (/^\/?[^/]+\/[^/]+\/[^/]+\/?$/.test(endpoint)) {
      action = endpoint.replace(/^\/[^/]+\/[^/]+\/([0-9]+)\/?$/, '$1');
    }
    return parseInt(action, 10);
  }

  /**
   * AjaxHustleBunny Public API
   */
  return {
    setUp: false, // whether we've setup initial pushstate.
    container: '.mainCont',
    updateState: false,
    stateObject : {url: 'index', title: ''},
    onHopState: function(event) {
      if (event && event.state) {
        ajaxHustleBunny.stateObject.url = event.state.url;
        ajaxHustleBunny.updateUrl = false;
        ajaxHustleBunny.getPage();
      }
    },

    /**
     * Change Page With Ajax (NEW! 07-13-2015)
     *
     * This used to only be available through clicks via .ahb-click event. I moved it out here because it isn't a good
     * idea to have to try to figure out this stuff in multiple places. So now we can call our own view loading url
     * without a user click (if we need) and it will behave identical to an ahb-click event
     *
     * @param pathname - full enpoint and action /something/something/3 or /something/thing
     */
    hopHere: function(pathname) {
      ajaxHustleBunny.updateState = true;
      ajaxHustleBunny.stateObject.url = pathname;
      ajaxHustleBunny.getPage(pathname);
    },

    /**
     * Create click handler for ahb-click links
     */
    setupClicks: function () {
      $(document).on('click.abh.click', '.ahb-click', function (e) {
        // Get information needed to make the ajax transaction a success
        var endpoint = $(this).data('ahb-action');
        // Call the method to change page and take appropriate view loading actions
        ajaxHustleBunny.hopHere(endpoint);
        // So links won't leave hashtag in navigation bar
        return false;
      });
    },

    /**
     * This determines if state needs to be updated or not and then does it.
     * Makes it simple to not have to do the check from 100 other places. If in doubt,
     * just pass the endpoint into this method from anywhere in program and AjaxHustleBunny
     * will figure it out.
     *
     * @param string endpoint
     * @param bool force - whether to update state regardless.
     */
    handleStateUpdate: function(endpoint, force) {
      if (window && window.history) {
        if (ajaxHustleBunny.updateState || !!force) {
          window.history.pushState(ajaxHustleBunny.stateObject, '', endpoint);
          ajaxHustleBunny.updateState = false;
        }
      }
    },

    /**
     * Load the page/partial into the view using Ajax if the History API is available
     * @param endpoint
     */
    getPage: function(endpoint) {
      endpoint = endpoint || ajaxHustleBunny.stateObject.url;
      var action = '';

      // If there is no endpoint, then we can't do anything really.
      if (endpoint == '')
        return false;

      var loadingArea = $(ajaxHustleBunny.container);
      // Check if we have access to the pushState API
      if (!!window.history && 'pushState' in window.history) {
        $.ajax(endpoint, {
          type: 'post',
          dataType: 'html',
          data: {cp_action: 'ajax'},
          url: endpoint,
          success: function (res) {
            if (loadingArea.length) {
              loadingArea.html(res);
            }

            // Check if we need to push an updated state object into History
            ajaxHustleBunny.handleStateUpdate(endpoint);

            if (endpoint.match(/[^/]+\/[^/]+\/[^/]\/?$/)) {
              action = endpoint.replace(/[^/]+\/[^/]+\/([^/]+)\/?$/, "$1");

            }

            actionValue = filterAction(endpoint);
            endpoint = filterEndpoint(endpoint);
            // Take care of controller-action specific JavaScript
            ajaxHustleBunny.fireOffEndpointAction(endpoint, actionValue);
          }
        });
      }
      // ELSE -- We don't have access to pushState, so just load new page
      else {
        window.location = endpoint;
      }

    },

    /**
     * Store function references to cleanups that should be ran when views change
     */
    cleanUp: [],
    /**
     * If any views have actions that should be fired along with them, or cleanup that has to be fired
     * when the page navigates off of them, this is where that is done. It is called automatically so there
     * is no need to call on your own. (it calls on any page load or ajax load)
     * @param endpoint
     */
    fireOffEndpointAction: function(endpoint, actionValue) {
      var historyEndpoint = endpoint;
      actionValue = actionValue || filterAction(endpoint);;
      if (/^\/[^/]+\/?$/.test(endpoint)) {
        endpoint = endpoint.replace(/(^\/[^/]+)\/?$/, '$1/index');
        historyEndpoint = endpoint;
      }

      console.log('Ajax Endpoint Event:: %s  @ fireOffEndpointAction()', endpoint);
      // One of the first things to do is reset any actions that may or may not have happened. We have a bunny array for this!
      if (ajaxHustleBunny.cleanUp.length > 0) {
        var len = ajaxHustleBunny.cleanUp.length,
            i = 0, cleanupFn;

        for (i; i < len; i++) {
          // Try to perform whatever clean up has been asked of us by views
          try {
            cleanupFn = ajaxHustleBunny.cleanUp[i]();
          } catch(e) {}
        }
        // reset the array!
        ajaxHustleBunny.cleanUp = [];
      }

      if (!ajaxHustleBunny.setUp && window && window.history) {
        // This only fires 1 time, it sets the initial state so we can back button to it.
        if (historyEndpoint.substring(0,1) != '/')
          storedEnpoint = '/' + historyEndpoint;
        ajaxHustleBunny.stateObject.url = historyEndpoint;
        window.history.pushState(ajaxHustleBunny.stateObject, '', historyEndpoint);
      }
      ajaxHustleBunny.setUp = true;

      // Now the real work TODO: Check why I took time to remove that leading '/'
      endpoint = filterEndpoint(endpoint);

      if (bodyClassMap.hasOwnProperty(endpoint)) {
        $('body').removeClass().addClass(bodyClassMap[endpoint]);
      }

      /*-
       -- This used to check if there was as method called 'endpoint' and then fire it. Then that would play out any
       -- code that needed to be ran upon that view change or action by any Aroma js stuff running. It' getting heavy
       -- where ajaxHustleBunny is doing a lot of Aroma stuff... not what it was originally intended to do. So, now we
       -- don't check if ajaxEvents.hasOwnProperty(endpoint), just pass any endpoint to ajaxEvents.publish() and it can
       -- send it out to subscribers and let them figure out what to do. This will cut back on tons of repeated code.
       */
      ajaxHustleBunny.ajaxEvents.publish(endpoint, actionValue);

      return false;

    },

    /**
     * After certain parts of the page load, they may need some special actions to be taken. If so, that is what
     * these methods are for.
     * TODO: Find better way to fire off these - maybe the jQuery Ajax Events! (note: this way isn't bad now)
     */
    ajaxEvents: {
      /**
       * This isn't a typical PubSub, it's not a hub to allow objects to listen to each other, it only is a notifier of
       * endpoints or actions fired off from ajax. To subscribe just pass method to call as arg from outside to
       * ajaxHustleBunny.ajaxEvents.subscribe( method ).
       * @param object       - The object of which the method belongs to.
       * @param notifyMethod - function/method to call with name of any endpoint fired
       */
      subscribe: function(pubName, notifyMethod) {
        var subs;
        if (!this.subscribers) {
          this.subscribers = {};
        }
        subs = this.subscribers;
        if (subs.hasOwnProperty(pubName)) {
          throw new Error("The publish event named " + pubName + " has already been subscribed to ajaxHustleBunny.ajaxEvents with method: " + subs[pubName]);
        } else {
          if (!!notifyMethod)
            subs[pubName] = notifyMethod;
        }
      },

      /**
       * Publish out endpoints after firing any endpoint method inside house first (ajaxHustleBunny)
       * @param endpoint
       */
      publish: function(endpoint, action) {
        // set the globals (these are going to be handy for initial page load handlers) 07/15/2015
        this.currentEndpoint = endpoint;
        this.currentAction = action;

        if (this.hasOwnProperty(endpoint)) {
          // fire off anything specific to endpoint in this object
          this[endpoint]();
        }

        var subs = this.subscribers,
            pubName;
        for (pubName in subs) {
          if (subs.hasOwnProperty(pubName) && $.isFunction(subs[pubName])) {
            subs[pubName](endpoint, action);
          }
        }

      },

      // Things that have been finished.
      finished: [],
      // Methods waiting for things to finish.
      waiting: {},

      did: function(what) {
        if (this.finished.indexOf(what) == -1) {
          // Add 'what' was done to list if not there already
          this.finished.push(what);
        }
        if (this.waiting.hasOwnProperty(what)) {
          var waitingList = this.waiting[what],
              i = waitingList.length;
          while (i) {
            i--;
            if ($.isFunction(waitingList[i])) {
              // execute the function
              waitingList[i](what);
            }
            // We're iterating down so we can just POP! off.
            waitingList.pop();
          }
        }
      },

      when: function(what, tell) {
        if (this.finished.indexOf(what) > -1 && $.isFunction(tell)) {
          // If the 'what' has already happened, then tell the 'tell'
          tell(what);
        } else {
          if (!this.waiting.hasOwnProperty(what)) {
            // If we don't have waiting list for this 'what', then start one!
            this.waiting[what] = [];
          }
          this.waiting[what].push(tell);
        }
      }

    } // End ahb.ajaxEvents

  }; // End Return API

}(jQuery,window));
<commit_msg>Update ajax-hustle-bunny.js<commit_after>/*
 * Project: Ajax Hustle Bunny
 *
 * Purpose: Ajax Hustle Bunny handles the hopping around from view to view using Ajax. It gives able apps
 *          the ajax ability to handle history. Ajax Hustle Bunny also makes sure that events can be handled
 *          throughout both Ajax and non-ajax switching of views and that it can do so without causing memory
 *          issues caused by listening for events on elements that have been removed from the page flow or
 *          from stacking identical events on top of elements which can occur on some SPA's when navigation
 *          to one page reoccurs.
 * Author: Michael Rosata @ TqSoft
 *
 * License: MIT 2015 created by Michael Rosata. mrosata1984@gmail.com
 *
 */

var ajaxHustleBunny = (function($, window, undefined) {

  // Sometimes certain pages need body classes. If that's the case, map the $action to the class
  var bodyClassMap = {
    'complaints/index' : 'controls'
  };

  function filterEndpoint(endpoint) {
    if (/^\/[^/]+\/?$/.test(endpoint)) {
      endpoint = endpoint.replace(/(^\/[^/]+)\/?$/, '$1/index');
    }
    if (/^\/[^/]+\/[^/]+\/[^/]+\/?$/.test(endpoint)) {
      endpoint = endpoint.replace(/^(\/[^/]+\/[^/]+)(\/[0-9]+\/?)$/, '$1');
    }
    if (endpoint.length && endpoint[0] == '/')
      endpoint = endpoint.substr(1);
    // Don't want id on endpoint IE: controller/action/id <--- remove /id
    var ep1 = endpoint.split('/')[0] || '';
    var ep2 = endpoint.split('/')[1] || '';
    endpoint = ep1 + '/' + ep2;
    return endpoint;
  }

  /**
   * return the action from a url endpoint
   * @param endpoint
   * @return string|integer - empty string or action if exists.
   */
  function filterAction(endpoint) {
    var action = '';
    if (/^\/?[^/]+\/[^/]+\/[^/]+\/?$/.test(endpoint)) {
      action = endpoint.replace(/^\/[^/]+\/[^/]+\/([0-9]+)\/?$/, '$1');
    }
    return parseInt(action, 10);
  }

  /**
   * AjaxHustleBunny Public API
   */
  return {
    setUp: false, // whether we've setup initial pushstate.
    container: '.mainCont',
    updateState: false,
    stateObject : {url: 'index', title: ''},
    onHopState: function(event) {
      if (event && event.state) {
        ajaxHustleBunny.stateObject.url = event.state.url;
        ajaxHustleBunny.updateUrl = false;
        ajaxHustleBunny.getPage();
      }
    },

    /**
     * Change Page With Ajax (NEW! 07-13-2015)
     *
     * This used to only be available through clicks via .ahb-click event. I moved it out here because it isn't a good
     * idea to have to try to figure out this stuff in multiple places. So now we can call our own view loading url
     * without a user click (if we need) and it will behave identical to an ahb-click event
     *
     * @param pathname - full enpoint and action /something/something/3 or /something/thing
     */
    hopHere: function(pathname) {
      ajaxHustleBunny.updateState = true;
      ajaxHustleBunny.stateObject.url = pathname;
      ajaxHustleBunny.getPage(pathname);
    },

    /**
     * Create click handler for ahb-click links
     */
    setupClicks: function () {
      $(document).on('click.abh.click', '.ahb-click', function (e) {
        // Get information needed to make the ajax transaction a success
        var endpoint = $(this).data('ahb-action');
        // Call the method to change page and take appropriate view loading actions
        ajaxHustleBunny.hopHere(endpoint);
        // So links won't leave hashtag in navigation bar
        return false;
      });
    },

    /**
     * This determines if state needs to be updated or not and then does it.
     * Makes it simple to not have to do the check from 100 other places. If in doubt,
     * just pass the endpoint into this method from anywhere in program and AjaxHustleBunny
     * will figure it out.
     *
     * @param endpoint string - the '/controller/module' from url
     * @param force bool - whether to update state regardless.
     */
    handleStateUpdate: function(endpoint, force) {
      if (window && window.history) {
        if (ajaxHustleBunny.updateState || !!force) {
          window.history.pushState(ajaxHustleBunny.stateObject, '', endpoint);
          ajaxHustleBunny.updateState = false;
        }
      }
    },

    /**
     * Load the page/partial into the view using Ajax if the History API is available
     * @param endpoint
     */
    getPage: function(endpoint) {
      endpoint = endpoint || ajaxHustleBunny.stateObject.url;
      var action = '';

      // If there is no endpoint, then we can't do anything really.
      if (endpoint == '')
        return false;

      var loadingArea = $(ajaxHustleBunny.container);
      // Check if we have access to the pushState API
      if (!!window.history && 'pushState' in window.history) {
        $.ajax(endpoint, {
          type: 'post',
          dataType: 'html',
          data: {cp_action: 'ajax'},
          url: endpoint,
          success: function (res) {
            if (loadingArea.length) {
              loadingArea.html(res);
            }

            // Check if we need to push an updated state object into History
            ajaxHustleBunny.handleStateUpdate(endpoint);

            if (endpoint.match(/[^/]+\/[^/]+\/[^/]\/?$/)) {
              action = endpoint.replace(/[^/]+\/[^/]+\/([^/]+)\/?$/, "$1");

            }

            actionValue = filterAction(endpoint);
            endpoint = filterEndpoint(endpoint);
            // Take care of controller-action specific JavaScript
            ajaxHustleBunny.fireOffEndpointAction(endpoint, actionValue);
          }
        });
      }
      // ELSE -- We don't have access to pushState, so just load new page
      else {
        window.location = endpoint;
      }

    },

    /**
     * Store function references to cleanups that should be ran when views change
     */
    cleanUp: [],
    /**
     * If any views have actions that should be fired along with them, or cleanup that has to be fired
     * when the page navigates off of them, this is where that is done. It is called automatically so there
     * is no need to call on your own. (it calls on any page load or ajax load)
     *
     * @param endpoint
     * @param actionValue
     */
    fireOffEndpointAction: function(endpoint, actionValue) {
      var historyEndpoint = endpoint;
      actionValue = actionValue || filterAction(endpoint);
      if (/^\/[^/]+\/?$/.test(endpoint)) {
        endpoint = endpoint.replace(/(^\/[^/]+)\/?$/, '$1/index');
        historyEndpoint = endpoint;
      }

      console.log('Ajax Endpoint Event:: %s  @ fireOffEndpointAction()', endpoint);
      // One of the first things to do is reset any actions that may or may not have happened. We have a bunny array for this!
      if (ajaxHustleBunny.cleanUp.length > 0) {
        var len = ajaxHustleBunny.cleanUp.length,
            i = 0, cleanupFn;

        for (i; i < len; i++) {
          // Try to perform whatever clean up has been asked of us by views
          try {
            cleanupFn = ajaxHustleBunny.cleanUp[i]();
          } catch(e) {}
        }
        // reset the array!
        ajaxHustleBunny.cleanUp = [];
      }

      if (!ajaxHustleBunny.setUp && window && window.history) {
        // This only fires 1 time, it sets the initial state so we can back button to it.
        if (historyEndpoint.substring(0,1) != '/')
          storedEnpoint = '/' + historyEndpoint;
        ajaxHustleBunny.stateObject.url = historyEndpoint;
        window.history.pushState(ajaxHustleBunny.stateObject, '', historyEndpoint);
      }
      ajaxHustleBunny.setUp = true;

      // Now the real work TODO: Check why I took time to remove that leading '/'
      endpoint = filterEndpoint(endpoint);

      if (bodyClassMap.hasOwnProperty(endpoint)) {
        $('body').removeClass().addClass(bodyClassMap[endpoint]);
      }

      /*-
       -- This used to check if there was as method called 'endpoint' and then fire it. Then that would play out any
       -- code that needed to be ran upon that view change or action by any Aroma js stuff running. It' getting heavy
       -- where ajaxHustleBunny is doing a lot of Aroma stuff... not what it was originally intended to do. So, now we
       -- don't check if ajaxEvents.hasOwnProperty(endpoint), just pass any endpoint to ajaxEvents.publish() and it can
       -- send it out to subscribers and let them figure out what to do. This will cut back on tons of repeated code.
       */
      ajaxHustleBunny.ajaxEvents.publish(endpoint, actionValue);

      return false;

    },

    /**
     * After certain parts of the page load, they may need some special actions to be taken. If so, that is what
     * these methods are for.
     * TODO: Find better way to fire off these - maybe the jQuery Ajax Events! (note: this way isn't bad now)
     */
    ajaxEvents: {
      /**
       * This isn't a typical PubSub, it's not a hub to allow objects to listen to each other, it only is a notifier of
       * endpoints or actions fired off from ajax. To subscribe just pass method to call as arg from outside to
       * ajaxHustleBunny.ajaxEvents.subscribe( method ).
       * @param pubName       - The object of which the method belongs to.
       * @param notifyMethod  - function/method to call with name of any endpoint fired
       * @param initialNotice - Immediate Initial Notification of the current page. This is good because the purpose of this is to keep all the objects
       *                        that manage views synced up and informed. While each publish gets to each object in sequence, their first notifications
       *                        are split in time due to the fact that they may not have registered to get notices yet. So set this 3rd option to true
       *                        to get an instant notification of the current page as if it was just loaded by ajax.
       */
      subscribe: function(pubName, notifyMethod, initialNotice) {
        var subs;
        if (!this.subscribers) {
          this.subscribers = {};
        }
        subs = this.subscribers;
        if (subs.hasOwnProperty(pubName)) {
          throw new Error("The publish event named " + pubName + " has already been subscribed to ajaxHustleBunny.ajaxEvents with method: " + subs[pubName]);
        } else {
          if (!!notifyMethod)
            subs[pubName] = notifyMethod;
          if (initialNotice) {
            // If asked for by new subscriber, we should publish immediately to them
            notifyMethod(this.currentEndpoint, this.currentAction);
          }
        }
      },

      /**
       * Publish out endpoints after firing any endpoint method inside house first (ajaxHustleBunny)
       * @param endpoint - url endpoint 'controller/model'
       * @param action  - url action
       */
      publish: function(endpoint, action) {
        // set the globals (these are going to be handy for initial page load handlers) 07/15/2015
        this.currentEndpoint = endpoint;
        this.currentAction = action;

        if (this.hasOwnProperty(endpoint)) {
          // fire off anything specific to endpoint in this object
          this[endpoint]();
        }

        var subs = this.subscribers,
            pubName;
        for (pubName in subs) {
          if (subs.hasOwnProperty(pubName) && $.isFunction(subs[pubName])) {
            subs[pubName](endpoint, action);
          }
        }

      },

      // Things that have been finished.
      finished: [],
      // Methods waiting for things to finish.
      waiting: {},

      /**
       * We will reset this on any ajax view change.
       */
      clearDidWhenNow: function() {
        //this.finished = [];
        this.waiting = {};
      },

      did: function(what) {
        if (this.finished.indexOf(what) == -1) {
          // Add 'what' was done to list if not there already
          this.finished.push(what);
        }
        if (this.waiting.hasOwnProperty(what)) {
          var waitingList = this.waiting[what],
              i = waitingList.length;
          while (i) {
            i--;
            if ($.isFunction(waitingList[i])) {
              // execute the function
              waitingList[i](what);
            }
            // We're iterating down so we can just POP! off.
            waitingList.pop();
          }
        }
      },

      /**
       * Set a 'tell' to fire on the next 'what'.
       *
       * This will never fire instantly. It has to wait for something to call did(what)
       * @param what
       * @param tell
       */
      when: function(what, tell) {
        if (!this.waiting.hasOwnProperty(what)) {
          // If we don't have waiting list for this 'what', then start one!
          this.waiting[what] = [];
        }
        this.waiting[what].push(tell);
      },

      /**
       * Same as when() except this will fire immediately if the did(what) has already been called
       * @param what
       * @param tell
       */
      after: function(what, tell) {
        if (this.finished.indexOf(what) > -1 && $.isFunction(tell)) {
          // If the 'what' has already happened, then tell the 'tell'
          tell(what);
        } else {
          // Now put this in the when list, because what it is waiting for has not happened yet
          this.when(what, tell);
        }
      },

      /**
       * LEGACY CODE
       *   TODO: SEE IF THESE PRE-AJAX-SUB-PUB METHODS CAN BE REMOVED.
       */
      'complaints/controls': function() {}

    } // End ahb.ajaxEvents

  }; // End Return API

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


<commit_before>/*
 * angular-ui-tab-scroll
 * https://github.com/VersifitTechnologies/angular-ui-tab-scroll
 *
 * Version: 2.1.3
 * License: MIT
 */

angular.module('ui.tab.scroll', [])
    .provider('scrollableTabsetConfig', function(){

          //the default options
          var defaultConfig = {
            showDropDown: true,
            showTooltips: true,
            tooltipLeftPlacement: 'right',
            tooltipRightPlacement: 'left',
            scrollBy: '50',
            autoRecalculate: false
          };

          var config = angular.extend({}, defaultConfig);

          return {
            setShowDropDown : function(value){
              config.showDropDown = (value == true);
            },
            setShowTooltips : function(value){
              config.showTooltips = (value == true);
            },
            setTooltipLeftPlacement : function(value){
              config.tooltipLeftPlacement = value;
            },
            setTooltipRightPlacement : function(value){
              config.tooltipRightPlacement = value;
            },
            setScrollBy : function(value){
              config.scrollBy = value;
            },
            setAutoRecalculate : function(value){
              config.autoRecalculate = (value == true);
            },
            $get: function(){
              return {
                showDropDown: config.showDropDown,
                showTooltips: config.showTooltips,
                tooltipLeftPlacement: config.tooltipLeftPlacement,
                tooltipRightPlacement: config.tooltipRightPlacement,
                scrollBy: config.scrollBy,
                autoRecalculate: config.autoRecalculate
              };
            }
          };
        }
    )
    .directive('scrollableTabset', [
      'scrollableTabsetConfig', '$window', '$interval', '$timeout','$sce',
      function(scrollableTabsetConfig, $window, $interval, $timeout, $sce) {

        return {
          restrict: 'AE',
          transclude: true,

          scope: {
            showDropDown: '@',
            showTooltips: '@',
            tooltipLeftPlacement: '@',
            tooltipRightPlacement: '@',
            scrollBy: '@',
            autoRecalculate: '@',
            api: '=?'
          },

          template: [
            '
',
              '<button type="button" ng-mousedown="scrollButtonDown(\'left\', $event)" ng-mouseup="scrollButtonUp()" ng-hide="hideButtons"' +
              ' ng-disabled="disableLeft" class="btn nav-button left-nav-button"' +
              ' tooltip-placement="{{tooltipLeftDirection}}" tooltip-html="tooltipLeftHtml"></button>',
              '

',
              '<button type="button" ng-mousedown="scrollButtonDown(\'right\', $event)" ng-mouseup="scrollButtonUp()" ng-hide="hideButtons"' +
              ' ng-disabled="disableRight" class="btn nav-button right-nav-button"' +
              ' tooltip-placement="{{tooltipRightDirection}}" tooltip-html="tooltipRightHtml"></button>',
              '
',
                '<button type="button" class="btn" dropdown-toggle></button>',
                '
',
                  '
',
                    '{{tab.tabScrollTitle}}',
                  '
',
                '
',
              '
',
            '
'
          ].join(''),

          link: function($scope, $el) {

            $scope.dropdownTabs = [];
            $scope.isDropDownOpen = false;
            $scope.hideButtons = true;
            $scope.hideDropDown = true;
            $scope.tooltipRightHtml = '';
            $scope.tooltipLeftHtml = '';
            $scope.disableLeft = true;
            $scope.disableRight = true;
            $scope.tooltipLeftDirection = $scope.tooltipLeftPlacement ? $scope.tooltipLeftPlacement : scrollableTabsetConfig.tooltipLeftPlacement;
            $scope.tooltipRightDirection =  $scope.tooltipRightPlacement ? $scope.tooltipRightPlacement : scrollableTabsetConfig.tooltipRightPlacement;

            $scope.api = {
              doRecalculate: function(){
                $timeout(function(){$scope.reCalcAll()});
              },

              scrollTabIntoView: function(arg){
                $timeout(function(){$scope.scrollTabIntoView(arg)});
              }
            };

            var mouseDownInterval = null;
            var isHolding = false;
            var winResizeTimeout

            var showDropDown = $scope.showDropDown ? $scope.showDropDown === 'true' : scrollableTabsetConfig.showDropDown;
            var showTooltips = $scope.showTooltips ? $scope.showTooltips === 'true' : scrollableTabsetConfig.showTooltips == true;
            var scrollByPixels = parseInt($scope.scrollBy ? $scope.scrollBy : scrollableTabsetConfig.scrollBy);

            $scope.onWindowResize = function() {
              // delay for a bit to avoid running lots of times.
              clearTimeout(winResizeTimeout);
              winResizeTimeout = setTimeout(function(){
                $scope.reCalcAll();
                $scope.$apply();
              }, 250);
            };

            var cancelMouseDownInterval = function() {
              isHolding = false;

              if(mouseDownInterval) {
                $interval.cancel(mouseDownInterval);
                mouseDownInterval = null;
              }
            };

            $scope.scrollButtonDown = function(direction, event) {
              event.stopPropagation();
              isHolding = true;

              var realScroll = direction === 'left' ? 0 - scrollByPixels : scrollByPixels;
              $scope.tabContainer.scrollLeft += realScroll;
              $scope.reCalcSides();

              mouseDownInterval = $interval(function() {

                if(isHolding) {
                  $scope.tabContainer.scrollLeft += realScroll;
                  $scope.reCalcSides();

                  if(event.target.disabled) {
                    cancelMouseDownInterval();
                  }
                }
              }, 100);
            }

            $scope.scrollButtonUp = function() {
              cancelMouseDownInterval();
            }

            $scope.activateTab = function(tab) {
              if(tab.disabled)return;
              tab.active = true;
              $timeout(function () {
                $scope.scrollTabIntoView();
              });
            }

            $scope.reCalcSides = function() {
              if(!$scope.tabContainer || $scope.hideButtons)return;
              $scope.disableRight = $scope.tabContainer.scrollLeft >= $scope.tabContainer.scrollWidth - $scope.tabContainer.offsetWidth;
              $scope.disableLeft = $scope.tabContainer.scrollLeft <= 0;

              if(showTooltips){
                $scope.reCalcTooltips();
              }
            };

            $scope.reCalcTooltips = function(){
              if(!$scope.tabContainer || $scope.hideButtons)return;
              var rightTooltips = [];
              var leftTooltips = [];

              var allTabs = $scope.tabContainer.querySelectorAll('li');
              angular.forEach(allTabs, function(tab) {

                var rightPosition = parseInt(tab.getBoundingClientRect().left + tab.getBoundingClientRect().width - $scope.tabContainer.getBoundingClientRect().left);
                var leftPosition = tab.getBoundingClientRect().left - $scope.tabContainer.getBoundingClientRect().left;
                var heading = tab.getAttribute("data-tabScrollHeading");
                var ignore = tab.getAttribute("data-tabScrollIgnore");

                if(rightPosition > $scope.tabContainer.offsetWidth && !ignore ) {
                  if(heading) {
                    rightTooltips.push(heading)
                  } else if (tab.textContent)rightTooltips.push(tab.textContent);
                }

                if (leftPosition < 0 && !ignore ) {
                  if(heading) {
                    leftTooltips.push(heading)
                  } else if (tab.textContent)leftTooltips.push(tab.textContent);
                }

              });

              var rightTooltipsHtml = rightTooltips.join('
');
              $scope.tooltipRightHtml = $sce.trustAsHtml(rightTooltipsHtml);

              var leftTooltipsHtml = leftTooltips.join('
');
              $scope.tooltipLeftHtml = $sce.trustAsHtml(leftTooltipsHtml);
            };

            $scope.scrollTabIntoView = function(arg){
              if(!$scope.tabContainer || $scope.hideButtons)return;

              var argInt = parseInt(arg);

              if(argInt) { // scroll tab index into view
                var allTabs = $scope.tabContainer.querySelectorAll('li');
                if(allTabs.length > argInt) { // only if its really exist
                  allTabs[argInt].scrollIntoView();
                }
              }

              else { // scroll selected tab into view
                var activeTab = $scope.tabContainer.querySelector('li.active');
                if(activeTab) {
                  activeTab.scrollIntoView();
                }
              }

              $scope.reCalcSides();
            };

            // init is called only once!
            $scope.init = function() {
              $scope.tabContainer = $el[0].querySelector('.spacer ul.nav-tabs');
              if(!$scope.tabContainer)return;

              var autoRecalc = $scope.autoRecalculate ? $scope.autoRecalculate === 'true' : scrollableTabsetConfig.autoRecalculate;
              if(autoRecalc) {
                var tabsetElement = angular.element($el[0].querySelector('.spacer div'));
                $scope.$watchCollection(
                    function () {
                      return tabsetElement.isolateScope() ? tabsetElement.isolateScope().tabs : false;
                    },
                    function () {
                      $timeout(function () {
                        $scope.reCalcAll()
                      });
                    }
                );
              }

              $scope.reCalcAll();

              // attaching event to window resize.
              angular.element($window).on('resize', $scope.onWindowResize);
            };

            // re-calculate if the scroll buttons are needed, than call re-calculate for both buttons.
            $scope.reCalcAll = function() {
              if(!$scope.tabContainer)return;

              $scope.hideButtons = $scope.tabContainer.scrollWidth <= $scope.tabContainer.offsetWidth;
              $scope.hideDropDown = showDropDown ? $scope.hideButtons : true;

              if(!$scope.hideButtons) {

                if(!$scope.hideDropDown) {
                  var allTabs = $scope.tabContainer.querySelectorAll('li');
                  $scope.dropdownTabs = [];
                  angular.forEach(allTabs, function (tab) {
                    var ignore = tab.getAttribute("data-tabScrollIgnore");
                    if(!ignore){
                      var heading = tab.getAttribute("data-tabScrollHeading");
                      var tabScope = angular.element(tab).isolateScope();
                      //push new field to use as title in the drop down.
                      tabScope.tabScrollTitle = heading ? heading : tabScope.headingElement.textContent;
                      $scope.dropdownTabs.push(tabScope);
                    }
                  });
                }

                $scope.reCalcSides();
              }
            };

            // this is how we init for the first time.
            $timeout(function(){
              $scope.init();
            });

            // when scope destroyed
            $scope.$on('$destroy', function () {
              angular.element($window).off('resize', $scope.onWindowResize);
            });

          }
        };
      }]);

<commit_msg>Removed "#" from dropdown items href<commit_after>/*
 * angular-ui-tab-scroll
 * https://github.com/VersifitTechnologies/angular-ui-tab-scroll
 *
 * Version: 2.1.3
 * License: MIT
 */

angular.module('ui.tab.scroll', [])
    .provider('scrollableTabsetConfig', function(){

          //the default options
          var defaultConfig = {
            showDropDown: true,
            showTooltips: true,
            tooltipLeftPlacement: 'right',
            tooltipRightPlacement: 'left',
            scrollBy: '50',
            autoRecalculate: false
          };

          var config = angular.extend({}, defaultConfig);

          return {
            setShowDropDown : function(value){
              config.showDropDown = (value == true);
            },
            setShowTooltips : function(value){
              config.showTooltips = (value == true);
            },
            setTooltipLeftPlacement : function(value){
              config.tooltipLeftPlacement = value;
            },
            setTooltipRightPlacement : function(value){
              config.tooltipRightPlacement = value;
            },
            setScrollBy : function(value){
              config.scrollBy = value;
            },
            setAutoRecalculate : function(value){
              config.autoRecalculate = (value == true);
            },
            $get: function(){
              return {
                showDropDown: config.showDropDown,
                showTooltips: config.showTooltips,
                tooltipLeftPlacement: config.tooltipLeftPlacement,
                tooltipRightPlacement: config.tooltipRightPlacement,
                scrollBy: config.scrollBy,
                autoRecalculate: config.autoRecalculate
              };
            }
          };
        }
    )
    .directive('scrollableTabset', [
      'scrollableTabsetConfig', '$window', '$interval', '$timeout','$sce',
      function(scrollableTabsetConfig, $window, $interval, $timeout, $sce) {

        return {
          restrict: 'AE',
          transclude: true,

          scope: {
            showDropDown: '@',
            showTooltips: '@',
            tooltipLeftPlacement: '@',
            tooltipRightPlacement: '@',
            scrollBy: '@',
            autoRecalculate: '@',
            api: '=?'
          },

          template: [
            '
',
              '<button type="button" ng-mousedown="scrollButtonDown(\'left\', $event)" ng-mouseup="scrollButtonUp()" ng-hide="hideButtons"' +
              ' ng-disabled="disableLeft" class="btn nav-button left-nav-button"' +
              ' tooltip-placement="{{tooltipLeftDirection}}" tooltip-html="tooltipLeftHtml"></button>',
              '

',
              '<button type="button" ng-mousedown="scrollButtonDown(\'right\', $event)" ng-mouseup="scrollButtonUp()" ng-hide="hideButtons"' +
              ' ng-disabled="disableRight" class="btn nav-button right-nav-button"' +
              ' tooltip-placement="{{tooltipRightDirection}}" tooltip-html="tooltipRightHtml"></button>',
              '
',
                '<button type="button" class="btn" dropdown-toggle></button>',
                '
',
                  '
',
                    '{{tab.tabScrollTitle}}',
                  '
',
                '
',
              '
',
            '
'
          ].join(''),

          link: function($scope, $el) {

            $scope.dropdownTabs = [];
            $scope.isDropDownOpen = false;
            $scope.hideButtons = true;
            $scope.hideDropDown = true;
            $scope.tooltipRightHtml = '';
            $scope.tooltipLeftHtml = '';
            $scope.disableLeft = true;
            $scope.disableRight = true;
            $scope.tooltipLeftDirection = $scope.tooltipLeftPlacement ? $scope.tooltipLeftPlacement : scrollableTabsetConfig.tooltipLeftPlacement;
            $scope.tooltipRightDirection =  $scope.tooltipRightPlacement ? $scope.tooltipRightPlacement : scrollableTabsetConfig.tooltipRightPlacement;

            $scope.api = {
              doRecalculate: function(){
                $timeout(function(){$scope.reCalcAll()});
              },

              scrollTabIntoView: function(arg){
                $timeout(function(){$scope.scrollTabIntoView(arg)});
              }
            };

            var mouseDownInterval = null;
            var isHolding = false;
            var winResizeTimeout

            var showDropDown = $scope.showDropDown ? $scope.showDropDown === 'true' : scrollableTabsetConfig.showDropDown;
            var showTooltips = $scope.showTooltips ? $scope.showTooltips === 'true' : scrollableTabsetConfig.showTooltips == true;
            var scrollByPixels = parseInt($scope.scrollBy ? $scope.scrollBy : scrollableTabsetConfig.scrollBy);

            $scope.onWindowResize = function() {
              // delay for a bit to avoid running lots of times.
              clearTimeout(winResizeTimeout);
              winResizeTimeout = setTimeout(function(){
                $scope.reCalcAll();
                $scope.$apply();
              }, 250);
            };

            var cancelMouseDownInterval = function() {
              isHolding = false;

              if(mouseDownInterval) {
                $interval.cancel(mouseDownInterval);
                mouseDownInterval = null;
              }
            };

            $scope.scrollButtonDown = function(direction, event) {
              event.stopPropagation();
              isHolding = true;

              var realScroll = direction === 'left' ? 0 - scrollByPixels : scrollByPixels;
              $scope.tabContainer.scrollLeft += realScroll;
              $scope.reCalcSides();

              mouseDownInterval = $interval(function() {

                if(isHolding) {
                  $scope.tabContainer.scrollLeft += realScroll;
                  $scope.reCalcSides();

                  if(event.target.disabled) {
                    cancelMouseDownInterval();
                  }
                }
              }, 100);
            }

            $scope.scrollButtonUp = function() {
              cancelMouseDownInterval();
            }

            $scope.activateTab = function(tab) {
              if(tab.disabled)return;
              tab.active = true;
              $timeout(function () {
                $scope.scrollTabIntoView();
              });
            }

            $scope.reCalcSides = function() {
              if(!$scope.tabContainer || $scope.hideButtons)return;
              $scope.disableRight = $scope.tabContainer.scrollLeft >= $scope.tabContainer.scrollWidth - $scope.tabContainer.offsetWidth;
              $scope.disableLeft = $scope.tabContainer.scrollLeft <= 0;

              if(showTooltips){
                $scope.reCalcTooltips();
              }
            };

            $scope.reCalcTooltips = function(){
              if(!$scope.tabContainer || $scope.hideButtons)return;
              var rightTooltips = [];
              var leftTooltips = [];

              var allTabs = $scope.tabContainer.querySelectorAll('li');
              angular.forEach(allTabs, function(tab) {

                var rightPosition = parseInt(tab.getBoundingClientRect().left + tab.getBoundingClientRect().width - $scope.tabContainer.getBoundingClientRect().left);
                var leftPosition = tab.getBoundingClientRect().left - $scope.tabContainer.getBoundingClientRect().left;
                var heading = tab.getAttribute("data-tabScrollHeading");
                var ignore = tab.getAttribute("data-tabScrollIgnore");

                if(rightPosition > $scope.tabContainer.offsetWidth && !ignore ) {
                  if(heading) {
                    rightTooltips.push(heading)
                  } else if (tab.textContent)rightTooltips.push(tab.textContent);
                }

                if (leftPosition < 0 && !ignore ) {
                  if(heading) {
                    leftTooltips.push(heading)
                  } else if (tab.textContent)leftTooltips.push(tab.textContent);
                }

              });

              var rightTooltipsHtml = rightTooltips.join('
');
              $scope.tooltipRightHtml = $sce.trustAsHtml(rightTooltipsHtml);

              var leftTooltipsHtml = leftTooltips.join('
');
              $scope.tooltipLeftHtml = $sce.trustAsHtml(leftTooltipsHtml);
            };

            $scope.scrollTabIntoView = function(arg){
              if(!$scope.tabContainer || $scope.hideButtons)return;

              var argInt = parseInt(arg);

              if(argInt) { // scroll tab index into view
                var allTabs = $scope.tabContainer.querySelectorAll('li');
                if(allTabs.length > argInt) { // only if its really exist
                  allTabs[argInt].scrollIntoView();
                }
              }

              else { // scroll selected tab into view
                var activeTab = $scope.tabContainer.querySelector('li.active');
                if(activeTab) {
                  activeTab.scrollIntoView();
                }
              }

              $scope.reCalcSides();
            };

            // init is called only once!
            $scope.init = function() {
              $scope.tabContainer = $el[0].querySelector('.spacer ul.nav-tabs');
              if(!$scope.tabContainer)return;

              var autoRecalc = $scope.autoRecalculate ? $scope.autoRecalculate === 'true' : scrollableTabsetConfig.autoRecalculate;
              if(autoRecalc) {
                var tabsetElement = angular.element($el[0].querySelector('.spacer div'));
                $scope.$watchCollection(
                    function () {
                      return tabsetElement.isolateScope() ? tabsetElement.isolateScope().tabs : false;
                    },
                    function () {
                      $timeout(function () {
                        $scope.reCalcAll()
                      });
                    }
                );
              }

              $scope.reCalcAll();

              // attaching event to window resize.
              angular.element($window).on('resize', $scope.onWindowResize);
            };

            // re-calculate if the scroll buttons are needed, than call re-calculate for both buttons.
            $scope.reCalcAll = function() {
              if(!$scope.tabContainer)return;

              $scope.hideButtons = $scope.tabContainer.scrollWidth <= $scope.tabContainer.offsetWidth;
              $scope.hideDropDown = showDropDown ? $scope.hideButtons : true;

              if(!$scope.hideButtons) {

                if(!$scope.hideDropDown) {
                  var allTabs = $scope.tabContainer.querySelectorAll('li');
                  $scope.dropdownTabs = [];
                  angular.forEach(allTabs, function (tab) {
                    var ignore = tab.getAttribute("data-tabScrollIgnore");
                    if(!ignore){
                      var heading = tab.getAttribute("data-tabScrollHeading");
                      var tabScope = angular.element(tab).isolateScope();
                      //push new field to use as title in the drop down.
                      tabScope.tabScrollTitle = heading ? heading : tabScope.headingElement.textContent;
                      $scope.dropdownTabs.push(tabScope);
                    }
                  });
                }

                $scope.reCalcSides();
              }
            };

            // this is how we init for the first time.
            $timeout(function(){
              $scope.init();
            });

            // when scope destroyed
            $scope.$on('$destroy', function () {
              angular.element($window).off('resize', $scope.onWindowResize);
            });

          }
        };
      }]);

<|endoftext|>


<commit_before>export const PROVIDER_TYPES_DICTIONARY = {
  cartodb: {
    label: 'CartoDB',
    value: 'cartodb',
    connectorType: 'rest',
    connectorUrlHint: 'Example: https://wri-01.carto.com/api/v2/sql?q=SELECT%20*%20FROM%20combined01_prepared%20where%20impactparameter=%27Food Demand%27'
  },
  gee: {
    label: 'GEE (Google Earth Engine)',
    value: 'gee',
    connectorType: 'rest',
    connectorUrlHint: 'Example: projects/wri-datalab/HansenComposite_14-15'
  },
  nexgddp: {
    label: 'NexGDDP (Nasa yearly downscaled climate projections)',
    value: 'nexgddp',
    connectorType: 'rest',
    connectorUrlHint: 'Example: scenario/model'
  },
  featureservice: {
    value: 'featureservice',
    label: 'Feature Service (Arcgis)',
    connectorType: 'rest',
    connectorUrlHint: 'Example: http://gis-gfw.wri.org/arcgis/rest/services/prep/nex_gddp_indicators/MapServer/6?f=pjson'
  },
  csv: {
    label: 'CSV',
    value: 'csv',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://en.wikipedia.org/wiki/Comma-separated_values'
  },
  json: {
    label: 'JSON',
    value: 'json',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: http://www.json.org/'
  },
  tsv: {
    label: 'TSV',
    value: 'tsv',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://en.wikipedia.org/wiki/Tab-separated_values'
  },
  xml: {
    label: 'XML',
    value: 'xml',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://www.w3.org/TR/REC-xml/'
  },
  wms: {
    label: 'WMS',
    value: 'wms',
    connectorType: 'wms',
    connectorUrlHint: ''
  }
};

export const STATE_DEFAULT = {
  step: 1,
  stepLength: 1,
  submitting: false,
  loading: false,
  dataset: {},
  form: {
    authorization: '',
    // STEP 1
    name: '',
    subtitle: '',
    application: [],
    type: 'tabular',
    provider: null,
    connectorType: '',
    connectorUrlHint: '',
    published: false,
    geoInfo: false,
    verified: false,
    protected: false,
    env: 'production',
    tableName: '',
    widgetRelevantProps: [],
    layerRelevantProps: [],
    connectorUrl: '',
    dataPath: '',
    legend: {
      lat: undefined,
      long: undefined,
      date: [],
      country: []
    },
    subscribable: [],
    mainDateField: null
  }
};

export const DATASET_TYPES = [
  {
    label: 'Tabular',
    value: 'tabular'
  },
  {
    label: 'Raster',
    value: 'raster'
  }
];

export const FORM_ELEMENTS = {
  elements: {},
  validate() {
    const elements = this.elements;
    Object.keys(elements).forEach((k) => {
      elements[k].validate();
    });
  },
  isValid() {
    const elements = this.elements;
    const valid = Object.keys(elements)
      .map(k => elements[k].isValid())
      .filter(v => v !== null)
      .every(element => element);

    return valid;
  }
};
<commit_msg>Datasets: be sure that the mainDateField is a string<commit_after>export const PROVIDER_TYPES_DICTIONARY = {
  cartodb: {
    label: 'CartoDB',
    value: 'cartodb',
    connectorType: 'rest',
    connectorUrlHint: 'Example: https://wri-01.carto.com/api/v2/sql?q=SELECT%20*%20FROM%20combined01_prepared%20where%20impactparameter=%27Food Demand%27'
  },
  gee: {
    label: 'GEE (Google Earth Engine)',
    value: 'gee',
    connectorType: 'rest',
    connectorUrlHint: 'Example: projects/wri-datalab/HansenComposite_14-15'
  },
  nexgddp: {
    label: 'NexGDDP (Nasa yearly downscaled climate projections)',
    value: 'nexgddp',
    connectorType: 'rest',
    connectorUrlHint: 'Example: scenario/model'
  },
  featureservice: {
    value: 'featureservice',
    label: 'Feature Service (Arcgis)',
    connectorType: 'rest',
    connectorUrlHint: 'Example: http://gis-gfw.wri.org/arcgis/rest/services/prep/nex_gddp_indicators/MapServer/6?f=pjson'
  },
  csv: {
    label: 'CSV',
    value: 'csv',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://en.wikipedia.org/wiki/Comma-separated_values'
  },
  json: {
    label: 'JSON',
    value: 'json',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: http://www.json.org/'
  },
  tsv: {
    label: 'TSV',
    value: 'tsv',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://en.wikipedia.org/wiki/Tab-separated_values'
  },
  xml: {
    label: 'XML',
    value: 'xml',
    basic: true,
    connectorType: 'document',
    connectorUrlHint: 'Format specification: https://www.w3.org/TR/REC-xml/'
  },
  wms: {
    label: 'WMS',
    value: 'wms',
    connectorType: 'wms',
    connectorUrlHint: ''
  }
};

export const STATE_DEFAULT = {
  step: 1,
  stepLength: 1,
  submitting: false,
  loading: false,
  dataset: {},
  form: {
    authorization: '',
    // STEP 1
    name: '',
    subtitle: '',
    application: [],
    type: 'tabular',
    provider: null,
    connectorType: '',
    connectorUrlHint: '',
    published: false,
    geoInfo: false,
    verified: false,
    protected: false,
    env: 'production',
    tableName: '',
    widgetRelevantProps: [],
    layerRelevantProps: [],
    connectorUrl: '',
    dataPath: '',
    legend: {
      lat: undefined,
      long: undefined,
      date: [],
      country: []
    },
    subscribable: [],
    mainDateField: ''
  }
};

export const DATASET_TYPES = [
  {
    label: 'Tabular',
    value: 'tabular'
  },
  {
    label: 'Raster',
    value: 'raster'
  }
];

export const FORM_ELEMENTS = {
  elements: {},
  validate() {
    const elements = this.elements;
    Object.keys(elements).forEach((k) => {
      elements[k].validate();
    });
  },
  isValid() {
    const elements = this.elements;
    const valid = Object.keys(elements)
      .map(k => elements[k].isValid())
      .filter(v => v !== null)
      .every(element => element);

    return valid;
  }
};
<|endoftext|>


<commit_before>'use strict';

module.exports.api = api = {
    "^\/devices$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("DEVICES GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/token$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("TOKEN GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/this$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("THIS GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/proximagicnode$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("proximagicnode GET!");
            } else if (q.method === "POST"){
                console.log("proximagicnode POST!");
                api.apiResponse(r, {
                    status: 'ok'
                });
            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    unsupportedMethod: (q, r) => {
        apiResponse(r, {
            status: 'error',
            response: 'This API request does not support method '+q.method
        });
    },
    apiResponse: (r, msg) => {
        if ( msg.status !== 'ok' ) {
            r.writeHead( 418, {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            } );
        } else {
            r.writeHead( 200, {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            } );
        }

        r.write( JSON.stringify( msg ) );
        r.end();
    }
}
<commit_msg>Refactoring api<commit_after>'use strict';

let api = {
    "^\/devices$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("DEVICES GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/token$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("TOKEN GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/this$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("THIS GET!");

            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    "^\/proximagicnode$": {
        func: (q, r) => {
            if(q.method === "GET") {
                console.log("proximagicnode GET!");
            } else if (q.method === "POST"){
                console.log("proximagicnode POST!");
                api.apiResponse(r, {
                    status: 'ok'
                });
            } else {
                unsupportedMethod(q, r);
            }
        }
    },
    unsupportedMethod: (q, r) => {
        apiResponse(r, {
            status: 'error',
            response: 'This API request does not support method '+q.method
        });
    },
    apiResponse: (r, msg) => {
        if ( msg.status !== 'ok' ) {
            r.writeHead( 418, {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            } );
        } else {
            r.writeHead( 200, {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            } );
        }

        r.write( JSON.stringify( msg ) );
        r.end();
    }
}

module.exports.api = api;
<|endoftext|>


<commit_before>import React from "react";
import tabify from "../../../utils/tabify";
import AbstractGraph from "../AbstractGraph";

export default class Table extends AbstractGraph {
    render() {
        const { response, configuration } = this.props;
        const rows = tabify(response.results);
        const properties = configuration.data;
        const columns = properties.columns;

        const {
          border,
          fontColor,
          header,
          padding,
          width,
        } = this.getConfiguredProperties();

        if (!rows)
            return (

No Rows

            );

        if (!columns)
            return (

No columns

            );

        return (

                        { columns.map(({column, label}, i) =>(
                            { label || column }
                        )) }

                    { rows.map((row, j) =>(

                            { columns.map(({column}, i) =>(
                                { row[column] }
                            )) }

                    )) }

        );
    }
}

Table.propTypes = {
  configuration: React.PropTypes.object,
  response: React.PropTypes.object
};
<commit_msg>Adjust indentation for readability in table<commit_after>import React from "react";
import tabify from "../../../utils/tabify";
import AbstractGraph from "../AbstractGraph";

export default class Table extends AbstractGraph {
    render() {
        const { response, configuration } = this.props;
        const rows = tabify(response.results);
        const properties = configuration.data;
        const columns = properties.columns;

        const {
            border,
            fontColor,
            header,
            padding,
            width
        } = this.getConfiguredProperties();

        if (!rows)
            return (

No Rows

            );

        if (!columns)
            return (

No columns

            );

        return (

                        { columns.map(({column, label}, i) =>(
                            { label || column }
                        )) }

                    { rows.map((row, j) =>(

                            { columns.map(({column}, i) =>(
                                { row[column] }
                            )) }

                    )) }

        );
    }
}

Table.propTypes = {
  configuration: React.PropTypes.object,
  response: React.PropTypes.object
};
<|endoftext|>


<commit_before>import React, { Component } from 'react';

export default class AddProject extends Component {
  constructor() {
    super();

    this.state = {
      projectId: '',
    };

    this.submit = this.submit.bind(this);
  }

  submit(e) {
    e.preventDefault();
    this.props.onAdd(this.state.projectId);
  }

  render() {
    return (
      <form onSubmit={this.submit}>

Project ID

        <input
          id="projectId"
          onChange={e => this.setState({ projectId: e.target.value })}
          type="number"
          value={this.state.projectId}
        />
        <button type="submit">Add</button>
      </form>
    );
  }
}
<commit_msg>Move manual project ID dropdown to Bootstrap styles<commit_after>import React, { Component } from 'react';

export default class AddProject extends Component {
  constructor() {
    super();

    this.state = {
      projectId: '',
    };

    this.submit = this.submit.bind(this);
  }

  submit(e) {
    e.preventDefault();
    this.props.onAdd(this.state.projectId);
  }

  render() {
    return (
      <form onSubmit={this.submit}>

Project ID

          <input
            id="projectId"
            onChange={e => this.setState({ projectId: e.target.value })}
            type="number"
            value={this.state.projectId}
            className="form-control"
          />

            <button type="submit" className="btn btn-primary">Add</button>

      </form>
    );
  }
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import {
    Text,
    View,
    ActivityIndicator,
    AsyncStorage
} from 'react-native';
import { connect } from 'react-redux';
import { Actions } from 'react-native-router-flux';
//import moment from 'moment';
import { fetchWeek } from '../actions';
import DayView from './DayViewComponent';
import DaySwitcher from './DaySwitcherComponent';

class Timetable extends Component {
    componentWillMount(){
        this.props.fetchWeek(this.props.user, this.props.program, '16');  // Using the 16th week of the year to get results from the API
        AsyncStorage.getItem('masterdata')
            .then((item) => JSON.parse(item))
            .then((itemJson) => {
                this.setState({
                    masterdata: JSON.stringify(itemJson)
                });
            });
    }

    renderTimetable(){
        if (this.props.loading) {
            return (
                <View style={styles.spinner}>
                    <ActivityIndicator size={'large'} />
                </View>
            );
        } else {
            if (this.props.week && this.state.masterdata) {
                let events = [];
                let eventsList = JSON.parse(this.props.week)["events"];

                for (let event in eventsList) {
                    if (eventsList[event]["day"] === "tue") {  // Just for now to test the condition
                        events.push(JSON.stringify(eventsList[event]));
                    }
                }

                return (
                    <View style={styles.dayView}>
                        <DayView day="Dienstag" week={this.props.week} masterdata={this.state.masterdata} events={events} />
                        <DaySwitcher day={JSON.parse(this.props.week)["today"]} />
                    </View>
                );
            }
        }
    }

    render() {
        return (
            <View style={{ flex: 1 }}>
                {this.renderTimetable()}
            </View>
        );
    }
}

const styles = {
    dayView: {
        flex: 1,
        flexDirection: "column"
    },
    headerDay: {
        flexDirection: "row",
        justifyContent: "space-between",
        backgroundColor: "#CCCCCC",
        padding: 10
    },
    spinner: {
        flex: 1,
        justifyContent: "center",
        alignItems: "center"
    }
};

const mapStateToProps = state => {
    return {
        user: state.login.user,
        program: state.login.program,
        week: state.timetable.fetchedWeek,
        selectedDay: state.timetable.selectedDay,
        loading: state.timetable.loadingFetch
    }
};

export default connect(mapStateToProps, { fetchWeek })(Timetable);
<commit_msg>Refactored the 'Timetable' component to actually use the selected day. Also switched to the 17th week of the year to better test everything.<commit_after>import React, { Component } from 'react';
import {
    Text,
    View,
    ActivityIndicator,
    AsyncStorage
} from 'react-native';
import { connect } from 'react-redux';
import { Actions } from 'react-native-router-flux';
import moment from 'moment';
import { fetchWeek, selectDay } from '../actions';
import i18n from 'react-native-i18n';
import bundledTranslations from '../translations';
import DayView from './DayViewComponent';
import DaySwitcher from './DaySwitcherComponent';

class Timetable extends Component {
    componentWillMount(){
        this.props.fetchWeek(this.props.user, this.props.program, '17');  // Using the 17th week of the year to get results from the API
        AsyncStorage.getItem('masterdata')
            .then((item) => JSON.parse(item))
            .then((itemJson) => {
                this.setState({
                    masterdata: JSON.stringify(itemJson)
                });
            });
        this.props.selectDay('mon');  // Start every first render with Monday
    }

    renderTimetable(){
        if (this.props.loading) {
            return (
                <View style={styles.spinner}>
                    <ActivityIndicator size={'large'} />
                </View>
            );
        } else {
            if (this.props.week && this.state.masterdata) {
                let events = [];
                let eventsList = JSON.parse(this.props.week)["events"];

                for (let event in eventsList) {
                    if (eventsList[event]["day"] === this.props.selectedDay) {
                        events.push(JSON.stringify(eventsList[event]));
                    }
                }

                let day = moment().day(this.props.selectedDay).week(this.props.currentWeek).format('ddd');
                let date = moment().day(this.props.selectedDay).week(this.props.currentWeek).format('DD.MM.YYYY');

                return (
                    <View style={styles.dayView}>
                        <DayView day={i18n.t(day)} week={this.props.week} masterdata={this.state.masterdata} events={events} />
                        <DaySwitcher day={i18n.t(day) + ', ' + date} />
                    </View>
                );
            }
        }
    }

    render() {
        return (
            <View style={{ flex: 1 }}>
                {this.renderTimetable()}
            </View>
        );
    }
}

i18n.locale = 'de';
i18n.fallbacks = true;
i18n.translations = bundledTranslations;

const styles = {
    dayView: {
        flex: 1,
        flexDirection: "column"
    },
    headerDay: {
        flexDirection: "row",
        justifyContent: "space-between",
        backgroundColor: "#CCCCCC",
        padding: 10
    },
    spinner: {
        flex: 1,
        justifyContent: "center",
        alignItems: "center"
    }
};

const mapStateToProps = state => {
    return {
        user: state.login.user,
        program: state.login.program,
        week: state.timetable.fetchedWeek,
        currentWeek: state.timetable.currentWeek,
        selectedDay: state.timetable.selectedDay,
        loading: state.timetable.loadingFetch
    }
};

export default connect(mapStateToProps, { fetchWeek, selectDay })(Timetable);
<|endoftext|>


<commit_before>import update from 'immutability-helper';
import { includes, indexOf } from 'lodash';
import { createAction } from 'redux-actions';

export const ADD_ACTION = 'addActionAction';
export const EDIT_ACTION = 'editActionAction';
export const EDIT_ACTION_ATTRIBUTE = 'editActionAttributeAction';
export const REMOVE_ACTION = 'removeActionAction';

export function addAction (typeSpecifier, phaseIndex) {
  const actionId = Math.random().toString(36).replace(/[^a-z]+/g, '');
  const newAction = {
    id: actionId,
    phase: phaseIndex,
    records: [],
    attributes: { TypeSpecifier: typeSpecifier }
  };
  return createAction(ADD_ACTION)(newAction);
}

export function editAction (editedAction) {
  return createAction(EDIT_ACTION)(editedAction);
}

export function editActionAttribute (editedActionAttribute) {
  return createAction(EDIT_ACTION_ATTRIBUTE)(editedActionAttribute);
}

export function removeAction (actionToRemove, phaseId) {
  return createAction(REMOVE_ACTION)({ actionToRemove, phaseId });
}

export const addActionAction = (state, { payload }) => {
  return update(state, {
    phases: {
      [payload.phase]: {
        actions: {
          $push: [payload.id]
        }
      }
    },
    actions: {
      [payload.id]: {
        $set: payload
      }
    }
  });
};

export const editActionAction = (state, { payload }) => {
  return update(state, {
    actions: {
      [payload.id]: {
        attributes: {
          $set: payload.attributes
        }
      }
    }
  });
};

export const editActionAttributeAction = (state, { payload }) => {
  if (payload.typeSpecifier) {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            TypeSpecifier: {
              $set: payload.typeSpecifier
            }
          }
        }
      }
    });
  } else if (payload.type) {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            ActionType: {
              $set: payload.type
            }
          }
        }
      }
    });
  } else {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            [payload.attributeIndex]: {
              $set: payload.attribute
            }
          }
        }
      }
    });
  }
};

export const removeActionAction = (state, { payload }) => {
  const stateCopy = Object.assign({}, state);
  const actionIndex = indexOf(
    stateCopy.phases[payload.phaseId].actions,
    payload.actionToRemove
  );

  const recordsUnderAction = stateCopy.actions[payload.actionToRemove].records;
  for (const record in stateCopy.records) {
    if (includes(recordsUnderAction, record)) {
      delete stateCopy.records[record];
    }
  }

  delete stateCopy.actions[payload.actionToRemove];
  return update(state, {
    actions: {
      $set: stateCopy.actions
    },
    phases: {
      [payload.phaseId]: {
        actions: {
          $splice: [[actionIndex, 1]]
        }
      }
    },
    records: {
      $set: stateCopy.records
    }
  });
};
<commit_msg>Create reducer functions for editing Action with EditorForm<commit_after>import update from 'immutability-helper';
import { includes, indexOf } from 'lodash';
import { createAction } from 'redux-actions';

export const ADD_ACTION = 'addActionAction';
export const EDIT_ACTION = 'editActionAction';
export const EDIT_ACTION_ATTRIBUTE = 'editActionAttributeAction';
export const REMOVE_ACTION = 'removeActionAction';

export function addAction (typeSpecifier, phaseIndex) {
  const actionId = Math.random().toString(36).replace(/[^a-z]+/g, '');
  const newAction = {
    id: actionId,
    phase: phaseIndex,
    records: [],
    attributes: { TypeSpecifier: typeSpecifier }
  };
  return createAction(ADD_ACTION)(newAction);
}

export function editAction (attributes, actionId) {
  let editedAttributes = {};

  for (const key in attributes) {
    if (attributes.hasOwnProperty(key)) {
      if (attributes[key].checked === true) {
        editedAttributes[key] = attributes[key].value;
      }
    }
  }

  const editedAction = Object.assign({}, {
    attributes: editedAttributes
  });

  return createAction(EDIT_ACTION)({ editedAction, actionId });
}

export function editActionAttribute (editedActionAttribute) {
  return createAction(EDIT_ACTION_ATTRIBUTE)(editedActionAttribute);
}

export function removeAction (actionToRemove, phaseId) {
  return createAction(REMOVE_ACTION)({ actionToRemove, phaseId });
}

export const addActionAction = (state, { payload }) => {
  return update(state, {
    phases: {
      [payload.phase]: {
        actions: {
          $push: [payload.id]
        }
      }
    },
    actions: {
      [payload.id]: {
        $set: payload
      }
    }
  });
};

export const editActionAction = (state, { payload }) => {
  return update(state, {
    actions: {
      [payload.id]: {
        attributes: {
          $set: payload.editedAction.attributes
        }
      }
    }
  });
};

export const editActionAttributeAction = (state, { payload }) => {
  if (payload.typeSpecifier) {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            TypeSpecifier: {
              $set: payload.typeSpecifier
            }
          }
        }
      }
    });
  } else if (payload.type) {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            ActionType: {
              $set: payload.type
            }
          }
        }
      }
    });
  } else {
    return update(state, {
      actions: {
        [payload.actionId]: {
          attributes: {
            [payload.attributeIndex]: {
              $set: payload.attribute
            }
          }
        }
      }
    });
  }
};

export const removeActionAction = (state, { payload }) => {
  const stateCopy = Object.assign({}, state);
  const actionIndex = indexOf(
    stateCopy.phases[payload.phaseId].actions,
    payload.actionToRemove
  );

  const recordsUnderAction = stateCopy.actions[payload.actionToRemove].records;
  for (const record in stateCopy.records) {
    if (includes(recordsUnderAction, record)) {
      delete stateCopy.records[record];
    }
  }

  delete stateCopy.actions[payload.actionToRemove];
  return update(state, {
    actions: {
      $set: stateCopy.actions
    },
    phases: {
      [payload.phaseId]: {
        actions: {
          $splice: [[actionIndex, 1]]
        }
      }
    },
    records: {
      $set: stateCopy.records
    }
  });
};
<|endoftext|>


<commit_before>const axios = require('axios');

const hidePMFields = {
  'Affordable Housing Investments': true,
  'Velodrome Resurfacing': true,
  'New Leicester Hwy Sidewalks': true,
  'Swannanoa River Greenway Phase 1': true,
  'Coxe Ave - Feasibility & Design': true,
  'Eagle Market Place': true,
  'I - 26 Connector': true,
  'Lee Walker Heights Redevelopment': true,
};

let xyCache = null;
let cacheDate = null;

function prepareProjects(rows) {
  if (rows.length === 0) return [];
  return rows.map(itm => {
    let latitude = [];
    let longitude = [];
    if (xyCache.hasOwnProperty(itm.project)) {
      latitude = xyCache[itm.project].latitude;
      longitude = xyCache[itm.project].longitude;
    }
    console.log(JSON.stringify(itm));
    return {
      gis_id: itm.gis_id,
      munis_project_number: itm.munis_project_number,
      project: itm.project,
      display_name: itm.display_name,
      location_details: itm.location_details,
      zip_code: itm.zip_code,
      type: itm.type,
      category: itm.category,
      coa_contact: itm.coa_contact,
      phone_number: itm.phone_number,
      email_address: itm.email_address,
      owner_department: itm.owner_department,
      administering_department: itm.administering_department,
      project_description: itm.project_description,
      status: itm.status,
      total_project_funding_budget_document: itm.total_project_funding_budget_d,
      total_spent: itm.ltd_actuals ? itm.ltd_actuals : 0,
      encumbered: itm.encumbrances ? itm.encumbrances : 0,
      target_construction_start: itm.target_construction_start,
      target_construction_end: itm.target_construction_end,
      actual_construction_end: itm.actual_construction_end,
      amount_behind_schedule: itm.amount_behind_schedule,
      estimated_construction_duration: itm.estimated_construction_duration,
      project_folder: itm.project_folder,
      project_webpage_more_information: itm.project_webpage__more_information_,
      communication_plan: itm.communication_plan,
      photo_url: itm.photo_url,
      project_updates: itm.project_updates,
      show_pm_fields: true,
      latitude,
      longitude,
    };
  });
}

const resolvers = {
  Query: {
    // Note: query can EITHER specify names OR it can specify
    // one or both of categories & zipcodes
    cip_projects(obj, args, context) {
      const logger = context.logger;
      const pool = context.pool;
      let query = 'select * from amd.capital_projects_master as A '
      + 'left join amd.cip_ltd_view as B '
      + 'on A.munis_project_number = B.project_id ';
      // let query = 'SELECT * FROM amd.coa_cip_project_information ';
      const names = args.names;
      if (names && names.length > 0) {
        if (names.length <= 0) return [];
        const namesList = names.map(p => {
          return `'${p}'`;
        }).join(',');
        query += `WHERE project in (${namesList})`;
      } else {
        const categories = args.categories;
        const zipcodes = args.zipcodes;
        let haveWhere = false;
        if (categories && categories.length > 0) {
          const cList = categories.map(p => {
            return `'${p}'`;
          }).join(',');
          query += `WHERE category in (${cList})`;
          haveWhere = true;
        }
        if (zipcodes && zipcodes.length > 0) {
          const zList = zipcodes.map(p => {
            return `'${p}'`;
          }).join(',');
          if (haveWhere) {
            query += 'AND ';
          } else {
            query += 'WHERE ';
          }
          query += `zip_code in (${zList})`;
        }
      }
      query += ' ORDER BY A.display_name';

      return pool.query(query)
      .then(result => {
        const timeout = 1000 * 3600;
        if (cacheDate === null || (new Date()).getTime() - cacheDate.getTime() > timeout) {
          console.log('Updating CIP project XY cache');
          const fsUrl = 'http://services.arcgis.com/aJ16ENn1AaqdFlqx/ArcGIS/rest/services/CIP_Storymap/FeatureServer/0/query?where=1%3D1&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&returnGeodetic=false&outFields=*&returnGeometry=true&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnDistinctValues=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pjson&token=';
          return axios.get(fsUrl, { timeout: 5000 })
          .then(xyResult => {
            xyCache = {};
            xyResult.data.features.forEach(feature => {
              const f = feature.attributes;
              const fname = f.NAME.replace(/^\s+|\s+$/g, '');
              if (!xyCache.hasOwnProperty(fname)) {
                xyCache[fname] = {
                  name: fname,
                  tab_name: f.TAB_NAME,
                  short_desc: f.SHORT_DESC,
                  website: f.WEBSITE,
                  pic_url: f.PIC_URL,
                  thumb_url: f.THUMB_URL,
                  longitude: [],
                  latitude: [],
                };
              }
              xyCache[fname].longitude.push(f.LONG);
              xyCache[fname].latitude.push(f.LAT);
            });
            cacheDate = new Date();
            return result;
          });
        }
        return Promise.resolve(result);
      })
      .then(result => {
        return prepareProjects(result.rows);
      })
      .catch(error => {
        logger.error(`Error in cip_projects endpoint: ${error}`);
        throw new Error(error);
      });
    },
  },
};

module.exports = resolvers;
<commit_msg>Fixed name of total budget column again and again<commit_after>const axios = require('axios');

const hidePMFields = {
  'Affordable Housing Investments': true,
  'Velodrome Resurfacing': true,
  'New Leicester Hwy Sidewalks': true,
  'Swannanoa River Greenway Phase 1': true,
  'Coxe Ave - Feasibility & Design': true,
  'Eagle Market Place': true,
  'I - 26 Connector': true,
  'Lee Walker Heights Redevelopment': true,
};

let xyCache = null;
let cacheDate = null;

function prepareProjects(rows) {
  if (rows.length === 0) return [];
  return rows.map(itm => {
    let latitude = [];
    let longitude = [];
    if (xyCache.hasOwnProperty(itm.project)) {
      latitude = xyCache[itm.project].latitude;
      longitude = xyCache[itm.project].longitude;
    }
    return {
      gis_id: itm.gis_id,
      munis_project_number: itm.munis_project_number,
      project: itm.project,
      display_name: itm.display_name,
      location_details: itm.location_details,
      zip_code: itm.zip_code,
      type: itm.type,
      category: itm.category,
      coa_contact: itm.coa_contact,
      phone_number: itm.phone_number,
      email_address: itm.email_address,
      owner_department: itm.owner_department,
      administering_department: itm.administering_department,
      project_description: itm.project_description,
      status: itm.status,
      total_project_funding_budget_document: itm.total_project_funding_budget_d,
      total_spent: itm.ltd_actuals ? itm.ltd_actuals : 0,
      encumbered: itm.encumbrances ? itm.encumbrances : 0,
      target_construction_start: itm.target_construction_start,
      target_construction_end: itm.target_construction_end,
      actual_construction_end: itm.actual_construction_end,
      amount_behind_schedule: itm.amount_behind_schedule,
      estimated_construction_duration: itm.estimated_construction_duration,
      project_folder: itm.project_folder,
      project_webpage_more_information: itm.project_webpage__more_information_,
      communication_plan: itm.communication_plan,
      photo_url: itm.photo_url,
      project_updates: itm.project_updates,
      show_pm_fields: true,
      latitude,
      longitude,
    };
  });
}

const resolvers = {
  Query: {
    // Note: query can EITHER specify names OR it can specify
    // one or both of categories & zipcodes
    cip_projects(obj, args, context) {
      const logger = context.logger;
      const pool = context.pool;
      let query = 'select * from amd.capital_projects_master as A '
      + 'left join amd.cip_ltd_view as B '
      + 'on A.munis_project_number = B.project_id ';
      // let query = 'SELECT * FROM amd.coa_cip_project_information ';
      const names = args.names;
      if (names && names.length > 0) {
        if (names.length <= 0) return [];
        const namesList = names.map(p => {
          return `'${p}'`;
        }).join(',');
        query += `WHERE project in (${namesList})`;
      } else {
        const categories = args.categories;
        const zipcodes = args.zipcodes;
        let haveWhere = false;
        if (categories && categories.length > 0) {
          const cList = categories.map(p => {
            return `'${p}'`;
          }).join(',');
          query += `WHERE category in (${cList})`;
          haveWhere = true;
        }
        if (zipcodes && zipcodes.length > 0) {
          const zList = zipcodes.map(p => {
            return `'${p}'`;
          }).join(',');
          if (haveWhere) {
            query += 'AND ';
          } else {
            query += 'WHERE ';
          }
          query += `zip_code in (${zList})`;
        }
      }
      query += ' ORDER BY A.display_name';

      return pool.query(query)
      .then(result => {
        const timeout = 1000 * 3600;
        if (cacheDate === null || (new Date()).getTime() - cacheDate.getTime() > timeout) {
          console.log('Updating CIP project XY cache');
          const fsUrl = 'http://services.arcgis.com/aJ16ENn1AaqdFlqx/ArcGIS/rest/services/CIP_Storymap/FeatureServer/0/query?where=1%3D1&objectIds=&time=&geometry=&geometryType=esriGeometryEnvelope&inSR=&spatialRel=esriSpatialRelIntersects&resultType=none&distance=0.0&units=esriSRUnit_Meter&returnGeodetic=false&outFields=*&returnGeometry=true&multipatchOption=xyFootprint&maxAllowableOffset=&geometryPrecision=&outSR=&datumTransformation=&applyVCSProjection=false&returnIdsOnly=false&returnCountOnly=false&returnExtentOnly=false&returnDistinctValues=false&orderByFields=&groupByFieldsForStatistics=&outStatistics=&having=&resultOffset=&resultRecordCount=&returnZ=false&returnM=false&returnExceededLimitFeatures=true&quantizationParameters=&sqlFormat=none&f=pjson&token=';
          return axios.get(fsUrl, { timeout: 5000 })
          .then(xyResult => {
            xyCache = {};
            xyResult.data.features.forEach(feature => {
              const f = feature.attributes;
              const fname = f.NAME.replace(/^\s+|\s+$/g, '');
              if (!xyCache.hasOwnProperty(fname)) {
                xyCache[fname] = {
                  name: fname,
                  tab_name: f.TAB_NAME,
                  short_desc: f.SHORT_DESC,
                  website: f.WEBSITE,
                  pic_url: f.PIC_URL,
                  thumb_url: f.THUMB_URL,
                  longitude: [],
                  latitude: [],
                };
              }
              xyCache[fname].longitude.push(f.LONG);
              xyCache[fname].latitude.push(f.LAT);
            });
            cacheDate = new Date();
            return result;
          });
        }
        return Promise.resolve(result);
      })
      .then(result => {
        return prepareProjects(result.rows);
      })
      .catch(error => {
        logger.error(`Error in cip_projects endpoint: ${error}`);
        throw new Error(error);
      });
    },
  },
};

module.exports = resolvers;
<|endoftext|>