<commit_before>'use strict';

let L = require( 'leaflet-headless' ),
    worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] ),
    maxZoom = 13;

/* eslint-disable no-underscore-dangle */
/**
 * Validate that the bounds contain no outlier.
 *
 * An outlier is a layer whom bounds do not fit into the world,
 * i.e. `-180 <= longitude <= 180  &&  -90 <= latitude <= 90`
 *
 * There is a special case for **masks** (polygons that cover the entire
 * globe with a hole to highlight a specific area). In this case the
 * algorithm tries to validate the hole bounds.
 *
 * @param {L.Layer} layer Layer to get and validate the bounds.
 * @return {L.LatLng|boolean} Bounds if valid.
 * @private
 */
function validateBounds( layer ) {
    let bounds = ( typeof layer.getBounds === 'function' ) && layer.getBounds();

    bounds = bounds || ( typeof layer.getLatLng === 'function' ) && layer.getLatLng();

    if ( bounds && worldLatLng.contains( bounds ) ) {
        return bounds;
    } else if ( layer instanceof L.Polygon && layer._holes && layer._holes[ 0 ] ) {
        bounds = new L.LatLngBounds( layer._convertLatLngs( layer._holes[ 0 ] ) );
        if ( worldLatLng.contains( bounds ) ) {
            return bounds;
        }
    }
    return false;
}

/**
 * Gets the valid bounds of a map/layer.
 *
 * @param {L.Map|L.Layer} layer
 * @return {L.LatLngBounds} Extended bounds
 * @private
 */
function getValidBounds( layer ) {
    let layerBounds = new L.LatLngBounds();

    if ( typeof layer.eachLayer === 'function' ) {
        layer.eachLayer( function ( child ) {
            layerBounds.extend( getValidBounds( child ) );
        } );
    } else {
        layerBounds.extend( validateBounds( layer ) );
    }
    return layerBounds;
}

/**
 * Gets the most optimal center and zoom for the map so that all the features
 * are visible.
 *
 * @param {Object} params Parameters from `requestHandler`
 * @param {Object} data GeoJSON for the map
 * @return {Object}
 * @return {number[]} return.center Latitude and longitude.
 * @return {number} return.zoom Zoom
 */
module.exports = function autoPosition( params, data ) {

    let dataLayer = L.geoJSON( data ),
        center,
        zoom,
        maxBounds,
        map = L.map( document.createElement( 'div' ) );

    map.setView( [ 0, 0 ], 11 );
    map.setSize( params.w, params.h );

    maxBounds = getValidBounds( dataLayer );

    if ( maxBounds.isValid() ) {
        map.fitBounds( maxBounds );
    } else {
        map.fitWorld();
    }

    if ( params.zoom === 'a' ) {
        zoom = map.getZoom();
        if ( zoom > maxZoom ) {
            zoom = maxZoom;
            map.setZoom( zoom );
        }
    } else {
        zoom = params.zoom;
    }

    center = map.getCenter();

    return {
        latitude: params.lat === 'a' ? center.lat : params.lat,
        longitude: params.lon === 'a' ? center.lng : params.lon,
        zoom: zoom
    };
};
<commit_msg>Disable maxzoom in autopostion service<commit_after>'use strict';

let L = require( 'leaflet-headless' ),
    worldLatLng = new L.LatLngBounds( [ -90, -180 ], [ 90, 180 ] ),

/* eslint-disable no-underscore-dangle */
/**
 * Validate that the bounds contain no outlier.
 *
 * An outlier is a layer whom bounds do not fit into the world,
 * i.e. `-180 <= longitude <= 180  &&  -90 <= latitude <= 90`
 *
 * There is a special case for **masks** (polygons that cover the entire
 * globe with a hole to highlight a specific area). In this case the
 * algorithm tries to validate the hole bounds.
 *
 * @param {L.Layer} layer Layer to get and validate the bounds.
 * @return {L.LatLng|boolean} Bounds if valid.
 * @private
 */
function validateBounds( layer ) {
    let bounds = ( typeof layer.getBounds === 'function' ) && layer.getBounds();

    bounds = bounds || ( typeof layer.getLatLng === 'function' ) && layer.getLatLng();

    if ( bounds && worldLatLng.contains( bounds ) ) {
        return bounds;
    } else if ( layer instanceof L.Polygon && layer._holes && layer._holes[ 0 ] ) {
        bounds = new L.LatLngBounds( layer._convertLatLngs( layer._holes[ 0 ] ) );
        if ( worldLatLng.contains( bounds ) ) {
            return bounds;
        }
    }
    return false;
}

/**
 * Gets the valid bounds of a map/layer.
 *
 * @param {L.Map|L.Layer} layer
 * @return {L.LatLngBounds} Extended bounds
 * @private
 */
function getValidBounds( layer ) {
    let layerBounds = new L.LatLngBounds();

    if ( typeof layer.eachLayer === 'function' ) {
        layer.eachLayer( function ( child ) {
            layerBounds.extend( getValidBounds( child ) );
        } );
    } else {
        layerBounds.extend( validateBounds( layer ) );
    }
    return layerBounds;
}

/**
 * Gets the most optimal center and zoom for the map so that all the features
 * are visible.
 *
 * @param {Object} params Parameters from `requestHandler`
 * @param {Object} data GeoJSON for the map
 * @return {Object}
 * @return {number[]} return.center Latitude and longitude.
 * @return {number} return.zoom Zoom
 */
module.exports = function autoPosition( params, data ) {

    let dataLayer = L.geoJSON( data ),
        center,
        zoom,
        maxBounds,
        map = L.map( document.createElement( 'div' ) );

    map.setView( [ 0, 0 ], 11 );
    map.setSize( params.w, params.h );

    maxBounds = getValidBounds( dataLayer );

    if ( maxBounds.isValid() ) {
        map.fitBounds( maxBounds );
    } else {
        map.fitWorld();
    }

    center = map.getCenter();

    return {
        latitude: params.lat === 'a' ? center.lat : params.lat,
        longitude: params.lon === 'a' ? center.lng : params.lon,
        zoom: params.zoom === 'a' ? map.getZoom() : params.zoom,
    };
};
<|endoftext|>


<commit_before>var Adapter     = require('ember-deploy/utilities/adapter');
var Promise     = require('ember-cli/lib/ext/promise');
var SilentError = require('ember-cli/lib/errors/silent');
var RSVP        = require('rsvp');
var azure       = require('azure-storage');
var walk        = require('walk');
var fs          = require('fs');
var chalk       = require('chalk');
var path        = require('path');
var mime        = require('mime');

var green = chalk.green;
var white = chalk.white;

var AZURE_CONTAINER_NAME = 'emberdeploy';

module.exports = Adapter.extend({
  init: function() {
    if (!this.config) {
      return Promise.reject(new SilentError('You have to pass a config!'));
    }

    var config = this.config.assets;

    // be transparant to either passing a connectionString or the storageAccount + storageAccessKey
    if(config.connectionString) {
      this.client = azure.createBlobService(config.connectionString);
    } else if (config.storageAccount && config.storageAccessKey) {
      this.client = azure.createBlobService(config.storageAccount, config.storageAccessKey);
    } else {
      console.error("No connection string or storage account plus access key set for this Azure deployment.");
      return Promise.reject(new SilentError('No connection string or storage account plus access key set for this Azure deployment.'));
    }
  },

  upload: function() {
    var _this = this;
    var blobService = this.client;

    if (!this.ui) {
      var message = 'You have to pass a UI to an adapter.';
      return Promise.reject(new SilentError(message));
    }

    this.ui.pleasantProgress.start(green('Uploading assets'), green('.'));

    return new Promise(function(resolve, reject) {
      // create container
      blobService.createContainerIfNotExists(AZURE_CONTAINER_NAME, {publicAccessLevel : 'blob'}, function(error, result, response){
        if(!error){
          // set CORS

          var serviceProperties = {};

          serviceProperties.Cors = {
            CorsRule: [{
              AllowedOrigins: ['*'],
              AllowedMethods: ['GET'],
              AllowedHeaders: [],
              ExposedHeaders: [],
              MaxAgeInSeconds: 60
            }]
          };

          blobService.setServiceProperties(serviceProperties, function(error, result, response) {
            if(!error) {
              // walk the directory to be uploaded
              walker = walk.walk("tmp/assets-sync", { followLinks: false });

              walker.on("file", _this._uploadFile.bind(_this));

              walker.on("errors", function(root, nodeStatsArray, next) {
                nodeStatsArray.forEach(function (n) {
                  console.error("[ERROR] " + n.name);
                  console.error(n.error.message || (n.error.code + ": " + n.error.path));
                });
                reject();
              });

              walker.on("end", function() {
                resolve();
              });
            } else {
              reject(error);
            }
          });
        } else {
          reject(error);
        }
      });
    });
  },

  _uploadFile: function(root, fileStat, next) {
    var _this = this;
    var blobService  = this.client;

    var resolvedFile = path.resolve(root, fileStat.name);
    var targetDirectory = path.normalize(root).replace("tmp" + path.sep + "assets-sync" + path.sep, "");
    var targetFile = targetDirectory + path.sep + fileStat.name;

    var options = {
      contentType: mime.lookup(resolvedFile)
    }

    if(this.config.assets.gzip !== false) {
      options["contentEncoding"] = "gzip";
    }

    blobService.createBlockBlobFromLocalFile(AZURE_CONTAINER_NAME, targetFile, resolvedFile, options, function(error, result, response){
      if(!error){
        // file uploaded
      } else {
        console.error("Error uploading " + targetFile);
        console.log(error);
      }

      next();
    });
  }
});
<commit_msg>figure out by extension whether content had been gziped<commit_after>var Adapter     = require('ember-deploy/utilities/adapter');
var Promise     = require('ember-cli/lib/ext/promise');
var SilentError = require('ember-cli/lib/errors/silent');
var RSVP        = require('rsvp');
var azure       = require('azure-storage');
var walk        = require('walk');
var fs          = require('fs');
var chalk       = require('chalk');
var path        = require('path');
var mime        = require('mime');

var green = chalk.green;
var white = chalk.white;

var AZURE_CONTAINER_NAME = 'emberdeploy';

module.exports = Adapter.extend({
  init: function() {
    if (!this.config) {
      return Promise.reject(new SilentError('You have to pass a config!'));
    }

    var config = this.config.assets;

    // be transparant to either passing a connectionString or the storageAccount + storageAccessKey
    if(config.connectionString) {
      this.client = azure.createBlobService(config.connectionString);
    } else if (config.storageAccount && config.storageAccessKey) {
      this.client = azure.createBlobService(config.storageAccount, config.storageAccessKey);
    } else {
      console.error("No connection string or storage account plus access key set for this Azure deployment.");
      return Promise.reject(new SilentError('No connection string or storage account plus access key set for this Azure deployment.'));
    }
  },

  upload: function() {
    var _this = this;
    var blobService = this.client;

    if (!this.ui) {
      var message = 'You have to pass a UI to an adapter.';
      return Promise.reject(new SilentError(message));
    }

    this.ui.pleasantProgress.start(green('Uploading assets'), green('.'));

    return new Promise(function(resolve, reject) {
      // create container
      blobService.createContainerIfNotExists(AZURE_CONTAINER_NAME, {publicAccessLevel : 'blob'}, function(error, result, response){
        if(!error){
          // set CORS

          var serviceProperties = {};

          serviceProperties.Cors = {
            CorsRule: [{
              AllowedOrigins: ['*'],
              AllowedMethods: ['GET'],
              AllowedHeaders: [],
              ExposedHeaders: [],
              MaxAgeInSeconds: 60
            }]
          };

          blobService.setServiceProperties(serviceProperties, function(error, result, response) {
            if(!error) {
              // walk the directory to be uploaded
              walker = walk.walk("tmp/assets-sync", { followLinks: false });

              walker.on("file", _this._uploadFile.bind(_this));

              walker.on("errors", function(root, nodeStatsArray, next) {
                nodeStatsArray.forEach(function (n) {
                  console.error("[ERROR] " + n.name);
                  console.error(n.error.message || (n.error.code + ": " + n.error.path));
                });
                reject();
              });

              walker.on("end", function() {
                resolve();
              });
            } else {
              reject(error);
            }
          });
        } else {
          reject(error);
        }
      });
    });
  },

  _uploadFile: function(root, fileStat, next) {
    var _this = this;
    var blobService  = this.client;

    var resolvedFile = path.resolve(root, fileStat.name);
    var targetDirectory = path.normalize(root).replace("tmp" + path.sep + "assets-sync" + path.sep, "");
    var targetFile = targetDirectory + path.sep + fileStat.name;

    var options = {}

    var extname = path.extname(resolvedFile);
    var hasBeenGziped = extname === '.js' || extname === '.css';

    if(this.config.assets.gzip !== false && hasBeenGziped) {
      options["contentEncoding"] = "gzip";
    }

    blobService.createBlockBlobFromLocalFile(AZURE_CONTAINER_NAME, targetFile, resolvedFile, options, function(error, result, response){
      if(!error){
        // file uploaded
      } else {
        console.error("Error uploading " + targetFile);
        console.log(error);
      }

      next();
    });
  }
});
<|endoftext|>


<commit_before>/* test/app/unit/controllers/page.js -- test Page Angular controller
 * Copyright 2014 Sergei Ianovich
 *
 * Licensed under AGPL-3.0 or later, see LICENSE
 * Process Control Service Web Interface
 */
'use strict';

describe("Page Controller", function() {
  var httpBackend;

  beforeEach(function() {
    angular.mock.module('pcs.services');
    angular.mock.module('pcs.controllers');
  });

  beforeEach(inject(function($httpBackend) {
    httpBackend = $httpBackend;
  }));

  afterEach(function(){
    httpBackend.verifyNoOutstandingExpectation();
  });

  describe("PageCtrl", function() {
    var scope, location, controller;

    beforeEach(inject(function($location, $controller) {
      location = $location;
      controller = $controller;
      scope = {};
      controller('PageCtrl', { $scope: scope });
    }));

    it("should define pager", function() {
      expect(scope.pager).to.exist();
      expect(scope.pager.first).to.equal(0);
      expect(scope.pager.last).to.equal(0);
      expect(scope.pager.count).to.equal(0);
      expect(scope.pager.page).to.equal(1);
      expect(scope.pager.show).to.not.be.true;
    });

    it("should define moment", function() {
      expect(scope.moment).to.exist();
    });

    it("should define operator", function() {
      expect(scope.operator).to.eql({});
    });

    describe("#setNewURL", function() {
      it("should update newURL", function() {
        expect(scope.newURL).to.be.undefined;
        scope.setNewURL("HELLO");
        expect(scope.newURL).to.equal("HELLO");
      });
    });
  });
});
// vim:ts=2 sts=2 sw=2 et:
<commit_msg>tests: ajax: pager: count<commit_after>/* test/app/unit/controllers/page.js -- test Page Angular controller
 * Copyright 2014 Sergei Ianovich
 *
 * Licensed under AGPL-3.0 or later, see LICENSE
 * Process Control Service Web Interface
 */
'use strict';

describe("Page Controller", function() {
  var httpBackend;

  beforeEach(function() {
    angular.mock.module('pcs.services');
    angular.mock.module('pcs.controllers');
  });

  beforeEach(inject(function($httpBackend) {
    httpBackend = $httpBackend;
  }));

  afterEach(function(){
    httpBackend.verifyNoOutstandingExpectation();
  });

  describe("PageCtrl", function() {
    var scope, location, controller;

    beforeEach(inject(function($location, $controller) {
      location = $location;
      controller = $controller;
      scope = {};
      controller('PageCtrl', { $scope: scope });
    }));

    it("should define pager", function() {
      expect(scope.pager).to.exist();
      expect(scope.pager.first).to.equal(0);
      expect(scope.pager.last).to.equal(0);
      expect(scope.pager.count).to.equal(0);
      expect(scope.pager.page).to.equal(1);
      expect(scope.pager.show).to.not.be.true;
    });

    it("should define moment", function() {
      expect(scope.moment).to.exist();
    });

    it("should define operator", function() {
      expect(scope.operator).to.eql({});
    });

    describe("#setNewURL", function() {
      it("should update newURL", function() {
        expect(scope.newURL).to.be.undefined;
        scope.setNewURL("HELLO");
        expect(scope.newURL).to.equal("HELLO");
      });
    });

    describe("#page", function() {
      it("should update pager count", function() {
        scope.page(1, 23, 26);
        expect(scope.pager.count).to.equal(26);
      });
    });
  });
});
// vim:ts=2 sts=2 sw=2 et:
<|endoftext|>


<commit_before>import React, {Component} from 'react';
import {mount} from 'enzyme';
import {Provider} from 'react-redux';
import {genNotification, mockStore} from '../fixtures';
import {types, removeNotification} from '../../src/store/notifications';
import css from '../../src/components/Notification/Notification.scss';
import ConnectNotification, {Notification} from '../../src/components/Notification/Notification';

describe('Notification', () => {
  let notification = null;
  let store = null;

  // default className for Notification component
  const className = {
    main: css['notification'],
    meta: css['notification-meta'],
    title: css['notification-title'],
    message: css['notification-message'],
    icon: `fa ${css['notification-icon']}`,
    status: (status) => {
      return css[`notification--${status}`];
    },
    dismissible: css['notification--dismissible'],
    // `fa` corresponds to font-awesome's class name
    actions: (count) => {
      if (count === 0) {
        return;
      }
      else if (count === 1) {
        return css['notification--actions-1'];
      }
      else if (count === 2) {
        return css['notification--actions-2'];
      }
      return css['notification-actions'];
    },
    action: css['notification-action'],
    actionText: css['notification-action-text']
  };
  // Expected Notification component
  // used to compare HTML
  class ExpectedNotification extends Component {
    static defaultProps = {
      className: className,
      onAdd: () => {
      },
      onRemove: () => {
      },
      actions: []
    };

    /**
     * Return HTML message
     * @returns {Object}
     * @private
     */
    _messageToHTML() {
      const {message} = this.props;
      return {
        __html: message
      };
    }

    /**
     * Render action button(s)
     * @returns {*}
     */
    _renderActions() {
      const {actions, className} = this.props;
      return actions.map((action) => {
        return (
          <button key={action.name} className={className.action}
            onClick={action.onClick}>

              {(action.primary
                ? {action.name}
                : action.name)}

          </button>
        );
      });
    }

    /**
     * Render
     * @returns {XML}
     */
    render() {
      const {title, message, status, dismissAfter,
        dismissible, className, actions, allowHTML
      } = this.props;
      const isDismissible = (dismissible && actions.length === 0);
      // if there is no actions, it remove automatically
      // the notification after `dismissAfter` duration
      if (actions.length === 0 && dismissAfter > 0) {
        setTimeout(() => this._remove(), dismissAfter);
      }

      return (

            {(title
              ?
{title}

              : '')}
            {(message
              ? (allowHTML
              ?

              :
{message}
)
              : '')}

          {(actions.length
            ?

            {this._renderActions()}

            : '')}

      );
    }
  }

  beforeEach('generate a new notification and init store', () => {
    notification = genNotification();
    store = mockStore({notifications: []});
  });

  it('should mount with default props', () => {
    delete notification.onAdd;
    delete notification.onRemove;
    const wrapper = mount(
      <Notification key={notification.id} {...notification}
        removeNotification={removeNotification}/>
    );
    expect(wrapper.props().className.main).toEqual(className.main);
    expect(wrapper.props().className.meta).toEqual(className.meta);
    expect(wrapper.props().className.title).toEqual(className.title);
    expect(wrapper.props().className.message).toEqual(className.message);
    expect(wrapper.props().className.icon).toEqual(className.icon);
    expect(wrapper.props().className.status()).toEqual(className.status());
    expect(wrapper.props().className.dismissible).toEqual(className.dismissible);
    expect(wrapper.props().className.actions()).toEqual(className.actions());
    expect(wrapper.props().className.actions(0)).toEqual(className.actions(0));
    expect(wrapper.props().className.actions(1)).toEqual(className.actions(1));
    expect(wrapper.props().className.actions(2)).toEqual(className.actions(2));
    expect(wrapper.props().className.action).toEqual(className.action);
    expect(wrapper.props().className.actionText).toEqual(className.actionText);
    expect(wrapper.props().removeNotification).toEqual(removeNotification);
    expect(wrapper.props().onAdd()).toEqual((() => {
    })());
    expect(wrapper.props().onRemove()).toEqual((() => {
    })());
  });

  it('should render component (with title)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without title)', () => {
    notification.title = null;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with message)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with HTML message)', () => {
    // add HTML in message and allow HTML
    notification.message = `${notification.message} <b>HEY</b>`;
    notification.allowHTML = true;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without message)', () => {
    notification.message = null;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with 2 action buttons)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with 1 action button)', () => {
    delete notification.actions[1];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without action buttons)', () => {
    notification.actions = [];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should run onAdd() callback at componentDidMount() lifecycle', () => {
    const errorMessage = 'onAdd() callback';
    // we throw an error to capture where
    // the code has been executed before error was thrown
    notification.onAdd = () => {
      throw new Error(errorMessage);
    };
    try {
      mount(
        <Provider store={store}>
          <ConnectNotification key={notification.id} {...notification}/>
        </Provider>
      );
    }
    catch (error) {
      expect(error.stack).toMatch(/onAdd\ncomponentDidMount/);
      expect(error.message).toEqual(errorMessage);
    }
    // and we update function to check that
    // component mount without any error
    notification.onAdd = () => {
      return 0;
    };
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
  });

  it('should run onRemove() callback at componentWillUnmount() lifecycle', () => {
    const errorMessage = 'onRemove() callback';
    // we throw an error to capture where
    // the code has been executed before error was thrown
    notification.onRemove = () => {
      throw new Error(errorMessage);
    };
    let wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    try {
      wrapper.unmount();
    }
    catch (error) {
      expect(error.stack).toMatch(/onRemove\ncomponentWillUnmount/);
      expect(error.message).toEqual(errorMessage);
    }
    // and we update function without `throw` call to check that
    // component unmount without any error
    notification.onRemove = () => {
      return 0;
    };
    wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.unmount();
  });

  it('should create an action to remove the notification ' +
    'when it is clicked', () => {
    notification.dismissible = true;
    notification.actions = [];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([expectedAction]);
  });

  it('should create an action to remove the notification ' +
    'when a action button is clicked (dismissible : false)', () => {
    // we set `dismissible` to `false` to be sure
    // that dismissible is ignored in this case
    notification.dismissible = false;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(`.${css['notification-action']}`).first().simulate('click');
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([expectedAction]);
  });

  it('should create an action to remove the notification after ' +
    '`dismissAfter` duration', (done) => {
    notification.dismissAfter = 10;
    // remove actions otherwise `remove()` is not called after `dismissAfter` duration
    notification.actions = [];
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([]);
    setTimeout(() => {
      expect(store.getActions()).toEqual([expectedAction]);
      done();
    }, 15);
  });

  it('should not create an action to remove the notification ' +
    'when it is clicked (dismissible : false)', () => {
    notification.dismissible = false;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
  });

  it('should not create an action to remove the notification ' +
    'when it is clicked (actions.length > 0)', (done) => {
    // we set `dismissible` to `true` to be sure that
    // `actions.length` must equals `0` to allow removing
    // same thing for `dismissAfter`
    notification.dismissible = true;
    notification.dismissAfter = 5;
    let wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
    // we remove an action and test it again
    notification.actions = notification.actions.slice(0, 1);
    wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
    // check after `dismissAfter` duration
    setTimeout(() => {
      expect(store.getActions()).toEqual([]);
      done();
    }, 10);
  });

  it('should not create an action to remove the notification after ' +
    '`dismissAfter` duration (dismissAfter = 0)', (done) => {
    notification.dismissAfter = 0;
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    setTimeout(() => {
      expect(store.getActions()).toEqual([]);
      done();
    }, 10);
  });
});
<commit_msg>add test for Notification component (simulate mouse actions)<commit_after>import React, {Component} from 'react';
import {mount} from 'enzyme';
import {Provider} from 'react-redux';
import {genNotification, mockStore} from '../fixtures';
import {Timer} from '../../src/helpers';
import {types, removeNotification} from '../../src/store/notifications';
import css from '../../src/components/Notification/Notification.scss';
import ConnectNotification, {Notification} from '../../src/components/Notification/Notification';

describe('Notification', () => {
  let notification = null;
  let store = null;

  // default className for Notification component
  const className = {
    main: css['notification'],
    meta: css['notification-meta'],
    title: css['notification-title'],
    message: css['notification-message'],
    icon: `fa ${css['notification-icon']}`,
    status: (status) => {
      return css[`notification--${status}`];
    },
    dismissible: css['notification--dismissible'],
    // `fa` corresponds to font-awesome's class name
    actions: (count) => {
      if (count === 0) {
        return;
      }
      else if (count === 1) {
        return css['notification--actions-1'];
      }
      else if (count === 2) {
        return css['notification--actions-2'];
      }
      return css['notification-actions'];
    },
    action: css['notification-action'],
    actionText: css['notification-action-text']
  };
  // Expected Notification component
  // used to compare HTML
  class ExpectedNotification extends Component {
    static defaultProps = {
      className: className,
      onAdd: () => {
      },
      onRemove: () => {
      },
      actions: []
    };

    /**
     * Return HTML message
     * @returns {Object}
     * @private
     */
    _messageToHTML() {
      const {message} = this.props;
      return {
        __html: message
      };
    }

    /**
     * Render action button(s)
     * @returns {*}
     */
    _renderActions() {
      const {actions, className} = this.props;
      return actions.map((action) => {
        return (
          <button key={action.name} className={className.action}
            onClick={action.onClick}>

              {(action.primary
                ? {action.name}
                : action.name)}

          </button>
        );
      });
    }

    /**
     * Render
     * @returns {XML}
     */
    render() {
      const {
        title, message, status, dismissAfter,
        dismissible, className, actions, allowHTML
      } = this.props;
      const isDismissible = (dismissible && actions.length === 0);
      // if there is no actions, it remove automatically
      // the notification after `dismissAfter` duration
      if (actions.length === 0 && dismissAfter > 0) {
        setTimeout(() => this._remove(), dismissAfter);
      }

      return (

            {(title
              ?
{title}

              : '')}
            {(message
              ? (allowHTML
              ?

              :
{message}
)
              : '')}

          {(actions.length
            ?

            {this._renderActions()}

            : '')}

      );
    }
  }

  beforeEach('generate a new notification and init store', () => {
    notification = genNotification();
    store = mockStore({notifications: []});
  });

  it('should mount with default props', () => {
    delete notification.onAdd;
    delete notification.onRemove;
    const wrapper = mount(
      <Notification key={notification.id} {...notification}
        removeNotification={removeNotification}/>
    );
    expect(wrapper.props().className.main).toEqual(className.main);
    expect(wrapper.props().className.meta).toEqual(className.meta);
    expect(wrapper.props().className.title).toEqual(className.title);
    expect(wrapper.props().className.message).toEqual(className.message);
    expect(wrapper.props().className.icon).toEqual(className.icon);
    expect(wrapper.props().className.status()).toEqual(className.status());
    expect(wrapper.props().className.dismissible).toEqual(className.dismissible);
    expect(wrapper.props().className.actions()).toEqual(className.actions());
    expect(wrapper.props().className.actions(0)).toEqual(className.actions(0));
    expect(wrapper.props().className.actions(1)).toEqual(className.actions(1));
    expect(wrapper.props().className.actions(2)).toEqual(className.actions(2));
    expect(wrapper.props().className.action).toEqual(className.action);
    expect(wrapper.props().className.actionText).toEqual(className.actionText);
    expect(wrapper.props().removeNotification).toEqual(removeNotification);
    expect(wrapper.props().onAdd()).toEqual((() => {
    })());
    expect(wrapper.props().onRemove()).toEqual((() => {
    })());
  });

  it('should mount with initial state', () => {
    // without a timer
    let wrapper = mount(
      <Notification key={notification.id} {...notification}
        removeNotification={removeNotification}/>
    );
    expect(wrapper.state().timer).toEqual(null);
    // with a timer
    delete notification.actions;
    wrapper = mount(
      <Notification key={notification.id} {...notification}
        removeNotification={removeNotification}/>
    );
    expect(wrapper.state().timer).toBeA(Timer);
  });

  it('should update state when receiving new props (', () => {
    // state component will be init without timer because it have actions
    let wrapper = mount(
      <Notification key={notification.id} {...notification}
        removeNotification={removeNotification}/>
    );
    expect(wrapper.state().timer).toEqual(null);
    // we delete actions to provoke creation of a Timer
    // at `componentWillReceivedProps()` component lifecycle
    notification.actions = [];
    wrapper.setProps(notification);
    expect(wrapper.state().timer).toBeA(Timer);
  });

  it('should render component (with title)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without title)', () => {
    notification.title = null;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with message)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with HTML message)', () => {
    // add HTML in message and allow HTML
    notification.message = `${notification.message} <b>HEY</b>`;
    notification.allowHTML = true;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without message)', () => {
    notification.message = null;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with 2 action buttons)', () => {
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (with 1 action button)', () => {
    delete notification.actions[1];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should render component (without action buttons)', () => {
    notification.actions = [];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );

    const expectedComponent = mount(
      <ExpectedNotification key={notification.id} {...notification}/>
    );
    expect(wrapper.html()).toEqual(expectedComponent.html());
  });

  it('should run onAdd() callback at componentDidMount() lifecycle', () => {
    const errorMessage = 'onAdd() callback';
    // we throw an error to capture where
    // the code has been executed before error was thrown
    notification.onAdd = () => {
      throw new Error(errorMessage);
    };
    try {
      mount(
        <Provider store={store}>
          <ConnectNotification key={notification.id} {...notification}/>
        </Provider>
      );
    }
    catch (error) {
      expect(error.stack).toMatch(/onAdd\ncomponentDidMount/);
      expect(error.message).toEqual(errorMessage);
    }
    // and we update function to check that
    // component mount without any error
    notification.onAdd = () => {
      return 0;
    };
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
  });

  it('should run onRemove() callback at componentWillUnmount() lifecycle', () => {
    const errorMessage = 'onRemove() callback';
    // we throw an error to capture where
    // the code has been executed before error was thrown
    notification.onRemove = () => {
      throw new Error(errorMessage);
    };
    let wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    try {
      wrapper.unmount();
    }
    catch (error) {
      expect(error.stack).toMatch(/onRemove\ncomponentWillUnmount/);
      expect(error.message).toEqual(errorMessage);
    }
    // and we update function without `throw` call to check that
    // component unmount without any error
    notification.onRemove = () => {
      return 0;
    };
    wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.unmount();
  });

  it('should create an action to remove the notification ' +
    'when it is clicked', () => {
    notification.dismissible = true;
    notification.actions = [];
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([expectedAction]);
  });

  it('should create an action to remove the notification ' +
    'when a action button is clicked (dismissible : false)', () => {
    // we set `dismissible` to `false` to be sure
    // that dismissible is ignored in this case
    notification.dismissible = false;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(`.${css['notification-action']}`).first().simulate('click');
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([expectedAction]);
  });

  it('should create an action to remove the notification after ' +
    '`dismissAfter` duration', (done) => {
    notification.dismissAfter = 10;
    // remove actions otherwise `remove()` is not called after `dismissAfter` duration
    notification.actions = [];
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    expect(store.getActions()).toEqual([]);
    setTimeout(() => {
      expect(store.getActions()).toEqual([expectedAction]);
      done();
    }, 15);
  });

  it('should not create an action to remove the notification ' +
    'when it is clicked (dismissible : false)', () => {
    notification.dismissible = false;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
  });

  it('should not create an action to remove the notification ' +
    'when it is clicked (actions.length > 0)', (done) => {
    // we set `dismissible` to `true` to be sure that
    // `actions.length` must equals `0` to allow removing
    // same thing for `dismissAfter`
    notification.dismissible = true;
    notification.dismissAfter = 5;
    let wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
    // we remove an action and test it again
    notification.actions = notification.actions.slice(0, 1);
    wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    wrapper.find(ConnectNotification).simulate('click');
    expect(store.getActions()).toEqual([]);
    // check after `dismissAfter` duration
    setTimeout(() => {
      expect(store.getActions()).toEqual([]);
      done();
    }, 10);
  });

  it('should not create an action to remove the notification after ' +
    '`dismissAfter` duration (dismissAfter = 0)', (done) => {
    notification.dismissAfter = 0;
    mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    );
    setTimeout(() => {
      expect(store.getActions()).toEqual([]);
      done();
    }, 10);
  });

  it('should not create an action to remove the notification ' +
    'while mouse is hovering it', (done) => {
    notification.dismissAfter = 10;
    delete notification.actions;
    const wrapper = mount(
      <Provider store={store}>
        <ConnectNotification key={notification.id} {...notification}/>
      </Provider>
    ).find(ConnectNotification);
    const expectedAction = {
      type: types.REMOVE_NOTIFICATION,
      payload: notification.id
    };
    // hover notification after 5s
    setTimeout(() => {
      wrapper.simulate('mouseEnter');
    }, 5);
    // check 5s after `dismissAfter` duration that the store
    // is empty because mouse is hovering the notification
    setTimeout(() => {
      expect(store.getActions()).toEqual([]);
      // we leave notification
      wrapper.simulate('mouseLeave');
    }, 15);
    // and we check that the store contains an action
    setTimeout(() => {
      expect(store.getActions()).toEqual([expectedAction]);
      done();
    }, 21);
  });
});
<|endoftext|>


<commit_before>/*
 * Waltz - Enterprise Architecture
 * Copyright (C) 2016, 2017 Waltz open source project
 * See README.md for more information
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import _ from "lodash";
import {initialiseData} from "../../../common";
import {buildHierarchies, switchToParentIds} from "../../../common/hierarchy-utils";
import {CORE_API} from "../../../common/services/core-api-utils";
import {distinctRatingCodes, indexRatingSchemes} from "../../../ratings/rating-utils";
import template from "./measurable-ratings-browser.html";

/**
 * @name waltz-measurable-ratings-browser
 *
 * @description
 * This component ...
 */

const bindings = {
    measurables: '<',
    categories: '<',
    ratingTallies: '<',
    onSelect: '<',
    scrollHeight: '<'
};

const initialState = {
    containerClass: [],
    measurables: [],
    ratingTallies: [],
    treeOptions: {
        nodeChildren: "children",
        dirSelectable: true,
        equality: function(node1, node2) {
            if (node1 && node2) {
                return node1.id === node2.id;
            } else {
                return false;
            }
        }
    },
    onSelect: (d) => console.log('wmrb: default on-select', d),
    visibility: {
        tab: null
    }
};

function combineRatingTallies(r1, r2) {
    return _.mergeWith(
        {}, r1, r2,
        (v1, v2) => (v1 || 0) + (v2 || 0));
}

function toRatingsSummaryObj(ratings = []) {
    const counts = _.countBy(ratings, 'rating');
    const total = _.sum(_.values(counts));
    return Object.assign({}, counts, { total });
}

function prepareTreeData(data = []) {
    return switchToParentIds(buildHierarchies(data));
}

function prepareTabs(categories = [], measurables = [], ratingSchemesById = {}) {
    const measurablesByCategory = _.groupBy(measurables, 'categoryId');

    const tabs = _.map(categories, category => {
        const measurablesForCategory = measurablesByCategory[category.id];
        const treeData = prepareTreeData(measurablesForCategory);
        const maxSize = _.chain(treeData)
            .map('totalRatings.total')
            .max()
            .value();

        return {
            category,
            ratingScheme: ratingSchemesById[category.ratingSchemeId],
            treeData,
            maxSize,
            expandedNodes: []
        };
    });

    return _.sortBy(tabs, 'category.name');
}

function findFirstNonEmptyTab(tabs = []) {
    const firstNonEmptyTab = _.find(tabs, t => t.treeData.length > 0);
    return _.get(firstNonEmptyTab || tabs[0], 'category.id');
}

function initialiseRatingTalliesMap(ratingTallies = [], measurables = []) {
    const talliesById = _.groupBy(ratingTallies, 'id');

    const reducer = (acc, m) => {
        const talliesForMeasurable = talliesById[m.id];
        const summaryObj = talliesForMeasurable
            ? toRatingsSummaryObj(talliesForMeasurable)
            : {};

        acc[m.id] = {
            direct: _.clone(summaryObj),
            compound: _.clone(summaryObj),
        };
        return acc;
    };
    const talliesMap = _.reduce(measurables, reducer, {});
    return talliesMap;
}

function mkRatingTalliesMap(ratingTallies = [], measurables = []) {
    const measurablesById = _.keyBy(measurables, 'id');
    const talliesMap = initialiseRatingTalliesMap(ratingTallies, measurables);
    _.each(measurables, m => {
        const rs = talliesMap[m.id];

        while (m.parentId) {
            const parent = measurablesById[m.parentId];
            if (! parent) break;
            const parentRating = talliesMap[m.parentId];
            parentRating.compound = combineRatingTallies(parentRating.compound, rs.direct);
            m = parent;
        }
    });

    return talliesMap;
}

function controller(serviceBroker) {
    const vm = initialiseData(this, initialState);

    const prepareData = () => {
        if (_.isEmpty(vm.measurables) ||
            _.isEmpty(vm.ratingTallies) ||
            _.isEmpty(vm.categories) ||
            _.isEmpty(vm.ratingSchemesById)) {
            return;
        } else {
            vm.tabs = prepareTabs(vm.categories, vm.measurables, vm.ratingSchemesById);
            vm.ratingsMap = mkRatingTalliesMap(vm.ratingTallies, vm.measurables);
            vm.visibility.tab = findFirstNonEmptyTab(vm.tabs);
            vm.maxTotal = _.max(
                _.flatMap(
                    _.values(vm.ratingsMap),
                    r => _.get(r, 'compound.total', [0])));
        }
    };

    vm.$onInit = () => {
        return serviceBroker
            .loadAppData(CORE_API.RatingSchemeStore.findAll)
            .then(r => {
                vm.ratingSchemesById = indexRatingSchemes(r.data);
                vm.distinctRatingCodes = distinctRatingCodes(r.data);
            })
            .then(() => prepareData());
    };

    vm.$onChanges = (c) => {
        if (c.scrollHeight) {
            vm.containerClass = [
                `waltz-scroll-region-${vm.scrollHeight}`
            ];
        }
        prepareData();
    };
}

controller.$inject = [
    'ServiceBroker'
];

const component = {
    template,
    bindings,
    controller
};

export default component;<commit_msg>Filter unused categories from ratings browser<commit_after>/*
 * Waltz - Enterprise Architecture
 * Copyright (C) 2016, 2017 Waltz open source project
 * See README.md for more information
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

import _ from "lodash";
import {initialiseData} from "../../../common";
import {buildHierarchies, switchToParentIds} from "../../../common/hierarchy-utils";
import {CORE_API} from "../../../common/services/core-api-utils";
import {distinctRatingCodes, indexRatingSchemes} from "../../../ratings/rating-utils";
import template from "./measurable-ratings-browser.html";

/**
 * @name waltz-measurable-ratings-browser
 *
 * @description
 * This component ...
 */

const bindings = {
    measurables: '<',
    categories: '<',
    ratingTallies: '<',
    onSelect: '<',
    scrollHeight: '<'
};

const initialState = {
    containerClass: [],
    measurables: [],
    ratingTallies: [],
    treeOptions: {
        nodeChildren: "children",
        dirSelectable: true,
        equality: function(node1, node2) {
            if (node1 && node2) {
                return node1.id === node2.id;
            } else {
                return false;
            }
        }
    },
    onSelect: (d) => console.log('wmrb: default on-select', d),
    visibility: {
        tab: null
    }
};

function combineRatingTallies(r1, r2) {
    return _.mergeWith(
        {}, r1, r2,
        (v1, v2) => (v1 || 0) + (v2 || 0));
}

function toRatingsSummaryObj(ratings = []) {
    const counts = _.countBy(ratings, 'rating');
    const total = _.sum(_.values(counts));
    return Object.assign({}, counts, { total });
}

function prepareTreeData(data = []) {
    return switchToParentIds(buildHierarchies(data));
}

function prepareTabs(categories = [], measurables = [], ratingSchemesById = {}) {
    const measurablesByCategory = _.groupBy(measurables, 'categoryId');
    return _
        .chain(categories)
        .filter(category => _.get(measurablesByCategory, category.id, []).length > 0)
        .map(category => {
            const measurablesForCategory = measurablesByCategory[category.id];
            const treeData = prepareTreeData(measurablesForCategory);
            const maxSize = _.chain(treeData)
                .map('totalRatings.total')
                .max()
                .value();

            return {
                category,
                ratingScheme: ratingSchemesById[category.ratingSchemeId],
                treeData,
                maxSize,
                expandedNodes: []
            };
        })
        .sortBy('category.name')
        .value();
}

function findFirstNonEmptyTab(tabs = []) {
    const firstNonEmptyTab = _.find(tabs, t => t.treeData.length > 0);
    return _.get(firstNonEmptyTab || tabs[0], 'category.id');
}

function initialiseRatingTalliesMap(ratingTallies = [], measurables = []) {
    const talliesById = _.groupBy(ratingTallies, 'id');

    const reducer = (acc, m) => {
        const talliesForMeasurable = talliesById[m.id];
        const summaryObj = talliesForMeasurable
            ? toRatingsSummaryObj(talliesForMeasurable)
            : {};

        acc[m.id] = {
            direct: _.clone(summaryObj),
            compound: _.clone(summaryObj),
        };
        return acc;
    };
    return _.reduce(measurables, reducer, {});
}

function mkRatingTalliesMap(ratingTallies = [], measurables = []) {
    const measurablesById = _.keyBy(measurables, 'id');
    const talliesMap = initialiseRatingTalliesMap(ratingTallies, measurables);
    _.each(measurables, m => {
        const rs = talliesMap[m.id];

        while (m.parentId) {
            const parent = measurablesById[m.parentId];
            if (! parent) break;
            const parentRating = talliesMap[m.parentId];
            parentRating.compound = combineRatingTallies(parentRating.compound, rs.direct);
            m = parent;
        }
    });

    return talliesMap;
}

function controller(serviceBroker) {
    const vm = initialiseData(this, initialState);

    const prepareData = () => {
        if (_.isEmpty(vm.measurables) ||
            _.isEmpty(vm.ratingTallies) ||
            _.isEmpty(vm.categories) ||
            _.isEmpty(vm.ratingSchemesById)) {
        } else {
            vm.tabs = prepareTabs(vm.categories, vm.measurables, vm.ratingSchemesById);
            vm.ratingsMap = mkRatingTalliesMap(vm.ratingTallies, vm.measurables);
            vm.visibility.tab = findFirstNonEmptyTab(vm.tabs);
            vm.maxTotal = _.max(
                _.flatMap(
                    _.values(vm.ratingsMap),
                    r => _.get(r, 'compound.total', [0])));
        }
    };

    vm.$onInit = () => {
        return serviceBroker
            .loadAppData(CORE_API.RatingSchemeStore.findAll)
            .then(r => {
                vm.ratingSchemesById = indexRatingSchemes(r.data);
                vm.distinctRatingCodes = distinctRatingCodes(r.data);
            })
            .then(() => prepareData());
    };

    vm.$onChanges = (c) => {
        if (c.scrollHeight) {
            vm.containerClass = [
                `waltz-scroll-region-${vm.scrollHeight}`
            ];
        }
        prepareData();
    };
}

controller.$inject = [
    'ServiceBroker'
];

const component = {
    template,
    bindings,
    controller
};

export default component;<|endoftext|>


<commit_before>import { getWrapper, timeout } from '../shared';
import NavigationBar from '../../src/components/NavigationBar';
import ComposeTextPanel from '../../src/components/ComposeTextPanel';
import DropdownSelect from '../../src/components/DropdownSelect';
import ConversationPanel from '../../src/components/ConversationPanel';
import { Message } from '../../src/components/ConversationMessageList';

let wrapper = null;
let panel = null;
let submitButton = null;
let textArea = null;
let toNumber = null;
beforeEach(async () => {
  jasmine.DEFAULT_TIMEOUT_INTERVAL = 32000;
  wrapper = await getWrapper();
  const navigationBar = wrapper.find(NavigationBar).first();
  await navigationBar.props().goTo('/composeText');
  panel = wrapper.find(ComposeTextPanel).first();
  submitButton = panel.find('.submitButton').first();
  textArea = panel.find('.textField').first().find('textarea').first();
  toNumber = panel.find('.numberInput').first();
});

describe('compose text panel', () => {
  test('initial state', () => {
    expect(panel).toBeDefined();
    expect(panel.props()).toBeDefined();
    expect(submitButton.props()).toBeDefined();
    expect(textArea.props()).toBeDefined();
    expect(toNumber.props()).toBeDefined();
  });

  test('send button status', async () => {
    expect(submitButton.props().disabled).toBe(true);

    await textArea.props().onChange({ currentTarget: { value: 'Hello world' } });
    expect(textArea.props().value).toEqual('Hello world');

    await toNumber.props().onChange({ currentTarget: { value: 'Hello world' } });
    expect(toNumber.props().value).toEqual('Hello world');

    expect(submitButton.props().disabled).toBe(false);
  });

  test('from dropdown', async () => {
    const dropdownSelect = panel.find(DropdownSelect).first();
    const dropdown = dropdownSelect.find('.dropdown').first();
    const dropdownItems = dropdown.find('.dropdownItem');
    expect(dropdownItems.length > 1).toEqual(true);

    const firstNumber = dropdownItems.at(0).text();
    const secondNumber = dropdownItems.at(1).text();
    expect(firstNumber).not.toEqual(secondNumber);

    const selected = dropdownSelect.find('button.button').first().find('span.selectedValue').first();
    await dropdownItems.at(1).simulate('click');
    expect(selected.text()).toEqual(secondNumber);
    await dropdownItems.at(0).simulate('click');
    expect(selected.text()).toEqual(firstNumber);
  });

  test('send an SMS', async () => {
    await toNumber.props().onChange({ currentTarget: { value: process.env.receiver } });
    await textArea.props().onChange({ currentTarget: { value: 'Hello world 111' } });
    expect(submitButton.props().disabled).toBe(false);
    await submitButton.closest('form').simulate('submit');

    await timeout(10000);

    const conversationPanel = wrapper.find(ConversationPanel);
    expect(conversationPanel.length > 0).toBe(true);
    const messages = conversationPanel.first().find(Message);
    const lastMessage = messages.at(messages.length - 1);
    expect(lastMessage.props()).toBeDefined();
    expect(lastMessage.props().subject).toMatch(/Hello world 111/);
  });
});
<commit_msg>Add random int in sent SMS content<commit_after>import { getWrapper, timeout } from '../shared';
import NavigationBar from '../../src/components/NavigationBar';
import ComposeTextPanel from '../../src/components/ComposeTextPanel';
import DropdownSelect from '../../src/components/DropdownSelect';
import ConversationPanel from '../../src/components/ConversationPanel';
import { Message } from '../../src/components/ConversationMessageList';

let wrapper = null;
let panel = null;
let submitButton = null;
let textArea = null;
let toNumber = null;
beforeEach(async () => {
  jasmine.DEFAULT_TIMEOUT_INTERVAL = 32000;
  wrapper = await getWrapper();
  const navigationBar = wrapper.find(NavigationBar).first();
  await navigationBar.props().goTo('/composeText');
  panel = wrapper.find(ComposeTextPanel).first();
  submitButton = panel.find('.submitButton').first();
  textArea = panel.find('.textField').first().find('textarea').first();
  toNumber = panel.find('.numberInput').first();
});

describe('compose text panel', () => {
  test('initial state', () => {
    expect(panel).toBeDefined();
    expect(panel.props()).toBeDefined();
    expect(submitButton.props()).toBeDefined();
    expect(textArea.props()).toBeDefined();
    expect(toNumber.props()).toBeDefined();
  });

  test('send button status', async () => {
    expect(submitButton.props().disabled).toBe(true);

    await textArea.props().onChange({ currentTarget: { value: 'Hello world' } });
    expect(textArea.props().value).toEqual('Hello world');

    await toNumber.props().onChange({ currentTarget: { value: 'Hello world' } });
    expect(toNumber.props().value).toEqual('Hello world');

    expect(submitButton.props().disabled).toBe(false);
  });

  test('from dropdown', async () => {
    const dropdownSelect = panel.find(DropdownSelect).first();
    const dropdown = dropdownSelect.find('.dropdown').first();
    const dropdownItems = dropdown.find('.dropdownItem');
    expect(dropdownItems.length > 1).toEqual(true);

    const firstNumber = dropdownItems.at(0).text();
    const secondNumber = dropdownItems.at(1).text();
    expect(firstNumber).not.toEqual(secondNumber);

    const selected = dropdownSelect.find('button.button').first().find('span.selectedValue').first();
    await dropdownItems.at(1).simulate('click');
    expect(selected.text()).toEqual(secondNumber);
    await dropdownItems.at(0).simulate('click');
    expect(selected.text()).toEqual(firstNumber);
  });

  test('send an SMS', async () => {
    const messageContent = `Hello world ${Date.now()}`;
    await toNumber.props().onChange({ currentTarget: { value: process.env.receiver } });
    await textArea.props().onChange({ currentTarget: { value: messageContent } });
    expect(submitButton.props().disabled).toBe(false);
    await submitButton.closest('form').simulate('submit');

    await timeout(10000);

    const conversationPanel = wrapper.find(ConversationPanel);
    expect(conversationPanel.length > 0).toBe(true);
    const messages = conversationPanel.first().find(Message);
    const lastMessage = messages.at(messages.length - 1);
    expect(lastMessage.props()).toBeDefined();
    expect(lastMessage.props().subject).toMatch(new RegExp(messageContent));
  });
});
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
 * This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
 * The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
 * The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
 * Code distributed by Google as part of the polymer project is also
 * subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
 */
'use strict';
var bowerConfig = function() {
  return {
    name: 'temp',
    version: '0.0.0',
    homepage: 'https://github.com/PolymerLabs/polymerize',
    authors: [
      'none <nobody@example.com>'
    ],
    private: true
  };
}

module.exports = bowerConfig;<commit_msg>cleanup bower-config<commit_after><|endoftext|>


<commit_before>const fs = require('fs');
const path = require('path');
const util = require('util');
const Module = require('module');
const v8 = require('v8');

var slice = [].slice;

// We modified the original process.argv to let node.js load the atom.js,
// we need to restore it here.
process.argv.splice(1, 1);

// Clear search paths.
require('../common/reset-search-paths');

// Import common settings.
require('../common/init');

var globalPaths = Module.globalPaths;

if (!process.env.ELECTRON_HIDE_INTERNAL_MODULES) {
  globalPaths.push(path.join(__dirname, 'api'));
}

// Expose public APIs.
globalPaths.push(path.join(__dirname, 'api', 'exports'));

if (process.platform === 'win32') {
  // Redirect node's console to use our own implementations, since node can not
  // handle console output when running as GUI program.
  var consoleLog = function() {
    var args;
    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
    return process.log(util.format.apply(util, args) + "\n");
  };
  var streamWrite = function(chunk, encoding, callback) {
    if (Buffer.isBuffer(chunk)) {
      chunk = chunk.toString(encoding);
    }
    process.log(chunk);
    if (callback) {
      callback();
    }
    return true;
  };
  console.log = console.error = console.warn = consoleLog;
  process.stdout.write = process.stderr.write = streamWrite;

  // Always returns EOF for stdin stream.
  var Readable = require('stream').Readable;
  var stdin = new Readable;
  stdin.push(null);
  process.__defineGetter__('stdin', function() {
    return stdin;
  });
}

// Don't quit on fatal error.
process.on('uncaughtException', function(error) {

  // Do nothing if the user has a custom uncaught exception handler.
  var dialog, message, ref, stack;
  if (process.listeners('uncaughtException').length > 1) {
    return;
  }

  // Show error in GUI.
  dialog = require('electron').dialog;
  stack = (ref = error.stack) != null ? ref : error.name + ": " + error.message;
  message = "Uncaught Exception:\n" + stack;
  return dialog.showErrorBox('A JavaScript error occurred in the main process', message);
});

// Emit 'exit' event on quit.
var app = require('electron').app;

app.on('quit', function(event, exitCode) {
  return process.emit('exit', exitCode);
});

// Map process.exit to app.exit, which quits gracefully.
process.exit = app.exit;

// Load the RPC server.
require('./rpc-server');

// Load the guest view manager.
require('./guest-view-manager');

require('./guest-window-manager');

// Now we try to load app's package.json.
var packageJson = null;
var searchPaths = ['app', 'app.asar', 'default_app'];
var i, len, packagePath;
for (i = 0, len = searchPaths.length; i < len; i++) {
  packagePath = searchPaths[i];
  try {
    packagePath = path.join(process.resourcesPath, packagePath);
    packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json')));
    break;
  } catch (error) {
    continue;
  }
}

if (packageJson == null) {
  process.nextTick(function() {
    return process.exit(1);
  });
  throw new Error("Unable to find a valid app");
}

// Set application's version.
if (packageJson.version != null) {
  app.setVersion(packageJson.version);
}

// Set application's name.
if (packageJson.productName != null) {
  app.setName(packageJson.productName);
} else if (packageJson.name != null) {
  app.setName(packageJson.name);
}

// Set application's desktop name.
if (packageJson.desktopName != null) {
  app.setDesktopName(packageJson.desktopName);
} else {
  app.setDesktopName((app.getName()) + ".desktop");
}

// Set v8 flags
if (packageJson.v8Flags != null) {
  v8.setFlagsFromString(packageJson.v8Flags);
}

// Chrome 42 disables NPAPI plugins by default, reenable them here
app.commandLine.appendSwitch('enable-npapi');

// Set the user path according to application's name.
app.setPath('userData', path.join(app.getPath('appData'), app.getName()));

app.setPath('userCache', path.join(app.getPath('cache'), app.getName()));

app.setAppPath(packagePath);

// Load the chrome extension support.
require('./chrome-extension');

// Load internal desktop-capturer module.
require('./desktop-capturer');

// Set main startup script of the app.
var mainStartupScript = packageJson.main || 'index.js';

// Finally load app's main.js and transfer control to C++.
Module._load(path.join(packagePath, mainStartupScript), Module, true);
<commit_msg>Automatically set app user model ID<commit_after>const fs = require('fs');
const path = require('path');
const util = require('util');
const Module = require('module');
const v8 = require('v8');

var slice = [].slice;

// We modified the original process.argv to let node.js load the atom.js,
// we need to restore it here.
process.argv.splice(1, 1);

// Clear search paths.
require('../common/reset-search-paths');

// Import common settings.
require('../common/init');

var globalPaths = Module.globalPaths;

if (!process.env.ELECTRON_HIDE_INTERNAL_MODULES) {
  globalPaths.push(path.join(__dirname, 'api'));
}

// Expose public APIs.
globalPaths.push(path.join(__dirname, 'api', 'exports'));

if (process.platform === 'win32') {
  // Redirect node's console to use our own implementations, since node can not
  // handle console output when running as GUI program.
  var consoleLog = function() {
    var args;
    args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
    return process.log(util.format.apply(util, args) + "\n");
  };
  var streamWrite = function(chunk, encoding, callback) {
    if (Buffer.isBuffer(chunk)) {
      chunk = chunk.toString(encoding);
    }
    process.log(chunk);
    if (callback) {
      callback();
    }
    return true;
  };
  console.log = console.error = console.warn = consoleLog;
  process.stdout.write = process.stderr.write = streamWrite;

  // Always returns EOF for stdin stream.
  var Readable = require('stream').Readable;
  var stdin = new Readable;
  stdin.push(null);
  process.__defineGetter__('stdin', function() {
    return stdin;
  });
}

// Don't quit on fatal error.
process.on('uncaughtException', function(error) {

  // Do nothing if the user has a custom uncaught exception handler.
  var dialog, message, ref, stack;
  if (process.listeners('uncaughtException').length > 1) {
    return;
  }

  // Show error in GUI.
  dialog = require('electron').dialog;
  stack = (ref = error.stack) != null ? ref : error.name + ": " + error.message;
  message = "Uncaught Exception:\n" + stack;
  return dialog.showErrorBox('A JavaScript error occurred in the main process', message);
});

// Emit 'exit' event on quit.
var app = require('electron').app;

app.on('quit', function(event, exitCode) {
  return process.emit('exit', exitCode);
});

if (process.platform === 'win32') {
  // If we are a Squirrel.Windows-installed app, set app user model ID
  // so that users don't have to do this.
  //
  // Squirrel packages are always of the form:
  //
  // PACKAGE-NAME
  // - Update.exe
  // - app-VERSION
  //   - OUREXE.exe
  //
  // Squirrel itself will always set the shortcut's App User Model ID to the
  // form `com.squirrel.PACKAGE-NAME.OUREXE`. We need to call
  // app.setAppUserModelId with a matching identifier so that renderer processes
  // will inherit this value.
  var updateDotExe = path.join(
    path.dirname(process.execPath),
    '..',
    'update.exe');

  if (fs.statSyncNoException(updateDotExe)) {
    var packageDir = path.dirname(path.resolve(updateDotExe));
    var packageName = path.basename(packageDir);
    var exeName = path.basename(process.execPath).replace(/\.exe$/i, '');

    app.setAppUserModelId(`com.squirrel.${packageName}.${exeName}`);
  }
}

// Map process.exit to app.exit, which quits gracefully.
process.exit = app.exit;

// Load the RPC server.
require('./rpc-server');

// Load the guest view manager.
require('./guest-view-manager');

require('./guest-window-manager');

// Now we try to load app's package.json.
var packageJson = null;
var searchPaths = ['app', 'app.asar', 'default_app'];
var i, len, packagePath;
for (i = 0, len = searchPaths.length; i < len; i++) {
  packagePath = searchPaths[i];
  try {
    packagePath = path.join(process.resourcesPath, packagePath);
    packageJson = JSON.parse(fs.readFileSync(path.join(packagePath, 'package.json')));
    break;
  } catch (error) {
    continue;
  }
}

if (packageJson == null) {
  process.nextTick(function() {
    return process.exit(1);
  });
  throw new Error("Unable to find a valid app");
}

// Set application's version.
if (packageJson.version != null) {
  app.setVersion(packageJson.version);
}

// Set application's name.
if (packageJson.productName != null) {
  app.setName(packageJson.productName);
} else if (packageJson.name != null) {
  app.setName(packageJson.name);
}

// Set application's desktop name.
if (packageJson.desktopName != null) {
  app.setDesktopName(packageJson.desktopName);
} else {
  app.setDesktopName((app.getName()) + ".desktop");
}

// Set v8 flags
if (packageJson.v8Flags != null) {
  v8.setFlagsFromString(packageJson.v8Flags);
}

// Chrome 42 disables NPAPI plugins by default, reenable them here
app.commandLine.appendSwitch('enable-npapi');

// Set the user path according to application's name.
app.setPath('userData', path.join(app.getPath('appData'), app.getName()));

app.setPath('userCache', path.join(app.getPath('cache'), app.getName()));

app.setAppPath(packagePath);

// Load the chrome extension support.
require('./chrome-extension');

// Load internal desktop-capturer module.
require('./desktop-capturer');

// Set main startup script of the app.
var mainStartupScript = packageJson.main || 'index.js';

// Finally load app's main.js and transfer control to C++.
Module._load(path.join(packagePath, mainStartupScript), Module, true);
<|endoftext|>


<commit_before>sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/m/MessageToast",
	"sap/m/MessageBox",
	"sap/ui/model/Sorter",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/model/FilterType",
	"sap/ui/model/json/JSONModel"
], function (Controller, MessageToast, MessageBox, Sorter, Filter, FilterOperator, FilterType,
	JSONModel) {
	"use strict";

	return Controller.extend("sap.ui.core.tutorial.odatav4.controller.App", {

		/**
		 *  Hook for initializing the controller
		 */
		onInit : function () {
			var oMessageManager = sap.ui.getCore().getMessageManager(),
				oMessageModel = oMessageManager.getMessageModel(),
				oMessageModelBinding = oMessageModel.bindList("/", undefined, [],
					new Filter("technical", FilterOperator.EQ, true)),
				oViewModel = new JSONModel({
					busy : false,
					hasUIChanges : false,
					usernameEmpty : true,
					order : 0
				});

			this.getView().setModel(oViewModel, "appView");
			this.getView().setModel(oMessageModel, "message");

			oMessageModelBinding.attachChange(this.onMessageBindingChange, this);
			this._bTechnicalErrors = false;
		},

		/* =========================================================== */
		/*           begin: event handlers                             */
		/* =========================================================== */

		/**
		 * Create a new entry.
		 */
		onCreate : function () {
			var oList = this.byId("peopleList"),
				oBinding = oList.getBinding("items"),
				// Create a new entry through the table's list binding
				oContext = oBinding.create({
					"UserName" : "",
					"FirstName" : "",
					"LastName" : "",
					"Age" : "18"
				});

			this._setUIChanges(true);
			this.getView().getModel("appView").setProperty("/usernameEmpty", true);

			// Select and focus the table row that contains the newly created entry
			oList.getItems().some(function (oItem) {
				if (oItem.getBindingContext() === oContext) {
					oItem.focus();
					oItem.setSelected(true);
					return true;
				}
			});
		},

		/**
		 * Delete an entry.
		 */
		onDelete : function () {
			var oSelected = this.byId("peopleList").getSelectedItem();

			if (oSelected) {
				oSelected.getBindingContext().delete("$auto").then(function () {
					MessageToast.show(this._getText("deletionSuccessMessage"));
				}.bind(this), function (oError) {
					MessageBox.error(oError.message);
				});
			}
		},

		/**
		 * Lock UI when changing data in the input controls
		 * @param {sap.ui.base.Event} oEvent - Event data
		 */
		onInputChange : function (oEvent) {
			if (oEvent.getParameter("escPressed")) {
				this._setUIChanges();
			} else {
				this._setUIChanges(true);
				/**
				 * Check if the username in the changed table row is empty and set the appView
				 * property accordingly
				 */
				if (oEvent.getSource().getParent().getBindingContext().getProperty("UserName")) {
					this.getView().getModel("appView").setProperty("/usernameEmpty", false);
				}
			}
		},

		/**
		 * Refresh the data.
		 */
		onRefresh : function () {
			var oBinding = this.byId("peopleList").getBinding("items");

			if (oBinding.hasPendingChanges()) {
				MessageBox.error(this._getText("refreshNotPossibleMessage"));
				return;
			}
			oBinding.refresh();
			MessageToast.show(this._getText("refreshSuccessMessage"));
		},

		/**
		 * Reset any unsaved changes.
		 */
		onResetChanges : function () {
			this.byId("peopleList").getBinding("items").resetChanges();
			// If there were technical errors, cancelling changes resets them.
			this._bTechnicalErrors = false;
			this._setUIChanges(false);
		},

		/**
		 * Reset the data source.
		 */
		onResetDataSource : function () {
			var oModel = this.getView().getModel(),
				oOperation = oModel.bindContext("/ResetDataSource(...)");

			oOperation.execute().then(function () {
					oModel.refresh();
					MessageToast.show(this._getText("sourceResetSuccessMessage"));
				}.bind(this), function (oError) {
					MessageBox.error(oError.message);
				}
			);
		},

		/**
		 * Save changes to the source.
		 */
		onSave : function () {
			var fnSuccess = function () {
					this._setBusy(false);
					MessageToast.show(this._getText("changesSentMessage"));
					this._setUIChanges(false);
				}.bind(this),
				fnError = function (oError) {
					this._setBusy(false);
					this._setUIChanges(false);
					MessageBox.error(oError.message);
				}.bind(this);

			this._setBusy(true); // Lock UI until submitBatch is resolved.
			this.getView().getModel().submitBatch("peopleGroup").then(fnSuccess, fnError);
			// If there were technical errors, a new save resets them.
			this._bTechnicalErrors = false;
		},

		/**
		 * Search for the term in the search field.
		 */
		onSearch : function () {
			var oView = this.getView(),
				sValue = oView.byId("searchField").getValue(),
				oFilter = new Filter("LastName", FilterOperator.Contains, sValue);

			oView.byId("peopleList").getBinding("items").filter(oFilter, FilterType.Application);
		},

		/**
		 * Sort the table according to the last name.
		 * Cycles between the three sorting states "none", "ascending" and "descending"
		 */
		onSort : function () {
			var oView = this.getView(),
				aStates = [undefined, "asc", "desc"],
				aStateTextIds = ["sortNone", "sortAscending", "sortDescending"],
				iOrder = oView.getModel("appView").getProperty("/order"),
				sOrder;

			// Cycle between the states
			iOrder = (iOrder + 1) % aStates.length;
			sOrder = aStates[iOrder];

			oView.getModel("appView").setProperty("/order", iOrder);
			oView.byId("peopleList").getBinding("items").sort(sOrder && new Sorter("LastName",
				sOrder === "desc"));

			MessageToast.show(this._getText("sortMessage", [this._getText(aStateTextIds[iOrder])]));
		},

		onMessageBindingChange : function (oEvent) {
			var aContexts = oEvent.getSource().getContexts(),
				aMessages,
				bMessageOpen = false;

			if (bMessageOpen || !aContexts.length) {
				return;
			}

			// Extract and remove the technical messages
			aMessages = aContexts.map(function (oContext) {
				return oContext.getObject();
			});
			sap.ui.getCore().getMessageManager().removeMessages(aMessages);

			this._setUIChanges(true);
			this._bTechnicalErrors = true;
			MessageBox.error(aMessages[0].message, {
				id : "serviceErrorMessageBox",
				onClose : function () {
					bMessageOpen = false;
				}
			});

			bMessageOpen = true;
		},

		onSelectionChange : function (oEvent) {
			var oDetailArea = this.byId("detailArea"),
				oLayout = this.byId("defaultLayout"),
				oOldContext = oDetailArea.getBindingContext(),
				oSearchField = this.byId("searchField"),
				oUserContext = oEvent.getParameters().listItem.getBindingContext();

			if (oOldContext) {
				oOldContext.setKeepAlive(false);
			}
			// set binding
			oDetailArea.setBindingContext(oUserContext);
			// set keepAlive for new context
			oUserContext.setKeepAlive(true, function () {
				oLayout.setSize("100%");
				oLayout.setResizable(false);
				oDetailArea.setVisible(false);
				oSearchField.setWidth("20%");
			});
			// resize view
			oDetailArea.setVisible(true);
			oLayout.setSize("60%");
			oLayout.setResizable(true);
			oSearchField.setWidth("40%");
		},

		/* =========================================================== */
		/*           end: event handlers                               */
		/* =========================================================== */

		/**
		 * Convenience method for retrieving a translatable text.
		 * @param {string} sTextId - the ID of the text to be retrieved.
		 * @param {Array} [aArgs] - optional array of texts for placeholders.
		 * @returns {string} the text belonging to the given ID.
		 */
		_getText : function (sTextId, aArgs) {
			return this.getOwnerComponent().getModel("i18n").getResourceBundle().getText(sTextId,
				aArgs);
		},

		/**
		 * Set hasUIChanges flag in View Model
		 * @param {boolean} [bHasUIChanges] - set or clear hasUIChanges
		 * if bHasUIChanges is not set, the hasPendingChanges-function of the OdataV4 model
		 * determines the result
		 */
		_setUIChanges : function (bHasUIChanges) {
			if (this._bTechnicalErrors) {
				// If there is currently a technical error, then force 'true'.
				bHasUIChanges = true;
			} else if (bHasUIChanges === undefined) {
				bHasUIChanges = this.getView().getModel().hasPendingChanges();
			}
			var oModel = this.getView().getModel("appView");
			oModel.setProperty("/hasUIChanges", bHasUIChanges);
		},

		/**
		 * Set busy flag in View Model
		 * @param {boolean} bIsBusy - set or clear busy
		 */
		_setBusy : function (bIsBusy) {
			var oModel = this.getView().getModel("appView");

			oModel.setProperty("/busy", bIsBusy);
		}
	});
});<commit_msg>[INTERNAL] sap.ui.core.demokit.tutorial.odatav4.10.webapp.controller.App.controller.js#onSelectionChange<commit_after>sap.ui.define([
	"sap/ui/core/mvc/Controller",
	"sap/m/MessageToast",
	"sap/m/MessageBox",
	"sap/ui/model/Sorter",
	"sap/ui/model/Filter",
	"sap/ui/model/FilterOperator",
	"sap/ui/model/FilterType",
	"sap/ui/model/json/JSONModel"
], function (Controller, MessageToast, MessageBox, Sorter, Filter, FilterOperator, FilterType,
	JSONModel) {
	"use strict";

	return Controller.extend("sap.ui.core.tutorial.odatav4.controller.App", {

		/**
		 *  Hook for initializing the controller
		 */
		onInit : function () {
			var oMessageManager = sap.ui.getCore().getMessageManager(),
				oMessageModel = oMessageManager.getMessageModel(),
				oMessageModelBinding = oMessageModel.bindList("/", undefined, [],
					new Filter("technical", FilterOperator.EQ, true)),
				oViewModel = new JSONModel({
					busy : false,
					hasUIChanges : false,
					usernameEmpty : true,
					order : 0
				});

			this.getView().setModel(oViewModel, "appView");
			this.getView().setModel(oMessageModel, "message");

			oMessageModelBinding.attachChange(this.onMessageBindingChange, this);
			this._bTechnicalErrors = false;
		},

		/* =========================================================== */
		/*           begin: event handlers                             */
		/* =========================================================== */

		/**
		 * Create a new entry.
		 */
		onCreate : function () {
			var oList = this.byId("peopleList"),
				oBinding = oList.getBinding("items"),
				// Create a new entry through the table's list binding
				oContext = oBinding.create({
					"UserName" : "",
					"FirstName" : "",
					"LastName" : "",
					"Age" : "18"
				});

			this._setUIChanges(true);
			this.getView().getModel("appView").setProperty("/usernameEmpty", true);

			// Select and focus the table row that contains the newly created entry
			oList.getItems().some(function (oItem) {
				if (oItem.getBindingContext() === oContext) {
					oItem.focus();
					oItem.setSelected(true);
					return true;
				}
			});
		},

		/**
		 * Delete an entry.
		 */
		onDelete : function () {
			var oSelected = this.byId("peopleList").getSelectedItem();

			if (oSelected) {
				oSelected.getBindingContext().delete("$auto").then(function () {
					MessageToast.show(this._getText("deletionSuccessMessage"));
				}.bind(this), function (oError) {
					MessageBox.error(oError.message);
				});
			}
		},

		/**
		 * Lock UI when changing data in the input controls
		 * @param {sap.ui.base.Event} oEvent - Event data
		 */
		onInputChange : function (oEvent) {
			if (oEvent.getParameter("escPressed")) {
				this._setUIChanges();
			} else {
				this._setUIChanges(true);
				/**
				 * Check if the username in the changed table row is empty and set the appView
				 * property accordingly
				 */
				if (oEvent.getSource().getParent().getBindingContext().getProperty("UserName")) {
					this.getView().getModel("appView").setProperty("/usernameEmpty", false);
				}
			}
		},

		/**
		 * Refresh the data.
		 */
		onRefresh : function () {
			var oBinding = this.byId("peopleList").getBinding("items");

			if (oBinding.hasPendingChanges()) {
				MessageBox.error(this._getText("refreshNotPossibleMessage"));
				return;
			}
			oBinding.refresh();
			MessageToast.show(this._getText("refreshSuccessMessage"));
		},

		/**
		 * Reset any unsaved changes.
		 */
		onResetChanges : function () {
			this.byId("peopleList").getBinding("items").resetChanges();
			// If there were technical errors, cancelling changes resets them.
			this._bTechnicalErrors = false;
			this._setUIChanges(false);
		},

		/**
		 * Reset the data source.
		 */
		onResetDataSource : function () {
			var oModel = this.getView().getModel(),
				oOperation = oModel.bindContext("/ResetDataSource(...)");

			oOperation.execute().then(function () {
					oModel.refresh();
					MessageToast.show(this._getText("sourceResetSuccessMessage"));
				}.bind(this), function (oError) {
					MessageBox.error(oError.message);
				}
			);
		},

		/**
		 * Save changes to the source.
		 */
		onSave : function () {
			var fnSuccess = function () {
					this._setBusy(false);
					MessageToast.show(this._getText("changesSentMessage"));
					this._setUIChanges(false);
				}.bind(this),
				fnError = function (oError) {
					this._setBusy(false);
					this._setUIChanges(false);
					MessageBox.error(oError.message);
				}.bind(this);

			this._setBusy(true); // Lock UI until submitBatch is resolved.
			this.getView().getModel().submitBatch("peopleGroup").then(fnSuccess, fnError);
			// If there were technical errors, a new save resets them.
			this._bTechnicalErrors = false;
		},

		/**
		 * Search for the term in the search field.
		 */
		onSearch : function () {
			var oView = this.getView(),
				sValue = oView.byId("searchField").getValue(),
				oFilter = new Filter("LastName", FilterOperator.Contains, sValue);

			oView.byId("peopleList").getBinding("items").filter(oFilter, FilterType.Application);
		},

		/**
		 * Sort the table according to the last name.
		 * Cycles between the three sorting states "none", "ascending" and "descending"
		 */
		onSort : function () {
			var oView = this.getView(),
				aStates = [undefined, "asc", "desc"],
				aStateTextIds = ["sortNone", "sortAscending", "sortDescending"],
				iOrder = oView.getModel("appView").getProperty("/order"),
				sOrder;

			// Cycle between the states
			iOrder = (iOrder + 1) % aStates.length;
			sOrder = aStates[iOrder];

			oView.getModel("appView").setProperty("/order", iOrder);
			oView.byId("peopleList").getBinding("items").sort(sOrder && new Sorter("LastName",
				sOrder === "desc"));

			MessageToast.show(this._getText("sortMessage", [this._getText(aStateTextIds[iOrder])]));
		},

		onMessageBindingChange : function (oEvent) {
			var aContexts = oEvent.getSource().getContexts(),
				aMessages,
				bMessageOpen = false;

			if (bMessageOpen || !aContexts.length) {
				return;
			}

			// Extract and remove the technical messages
			aMessages = aContexts.map(function (oContext) {
				return oContext.getObject();
			});
			sap.ui.getCore().getMessageManager().removeMessages(aMessages);

			this._setUIChanges(true);
			this._bTechnicalErrors = true;
			MessageBox.error(aMessages[0].message, {
				id : "serviceErrorMessageBox",
				onClose : function () {
					bMessageOpen = false;
				}
			});

			bMessageOpen = true;
		},

		onSelectionChange : function (oEvent) {
			var oDetailArea = this.byId("detailArea"),
				oLayout = this.byId("defaultLayout"),
				oOldContext = oDetailArea.getBindingContext(),
				oSearchField = this.byId("searchField"),
				oUserContext = oEvent.getParameters().listItem.getBindingContext();

			if (oOldContext) {
				oOldContext.setKeepAlive(false);
			}
			// set binding
			oDetailArea.setBindingContext(oUserContext);
			// set keepAlive for new context
			oUserContext.setKeepAlive(true, function () {
				// hides detail area when context is destroyed
				oLayout.setSize("100%");
				oLayout.setResizable(false);
				oDetailArea.setVisible(false);
				oSearchField.setWidth("20%");
			});
			// resize view
			oDetailArea.setVisible(true);
			oLayout.setSize("60%");
			oLayout.setResizable(true);
			oSearchField.setWidth("40%");
		},

		/* =========================================================== */
		/*           end: event handlers                               */
		/* =========================================================== */

		/**
		 * Convenience method for retrieving a translatable text.
		 * @param {string} sTextId - the ID of the text to be retrieved.
		 * @param {Array} [aArgs] - optional array of texts for placeholders.
		 * @returns {string} the text belonging to the given ID.
		 */
		_getText : function (sTextId, aArgs) {
			return this.getOwnerComponent().getModel("i18n").getResourceBundle().getText(sTextId,
				aArgs);
		},

		/**
		 * Set hasUIChanges flag in View Model
		 * @param {boolean} [bHasUIChanges] - set or clear hasUIChanges
		 * if bHasUIChanges is not set, the hasPendingChanges-function of the OdataV4 model
		 * determines the result
		 */
		_setUIChanges : function (bHasUIChanges) {
			if (this._bTechnicalErrors) {
				// If there is currently a technical error, then force 'true'.
				bHasUIChanges = true;
			} else if (bHasUIChanges === undefined) {
				bHasUIChanges = this.getView().getModel().hasPendingChanges();
			}
			var oModel = this.getView().getModel("appView");
			oModel.setProperty("/hasUIChanges", bHasUIChanges);
		},

		/**
		 * Set busy flag in View Model
		 * @param {boolean} bIsBusy - set or clear busy
		 */
		_setBusy : function (bIsBusy) {
			var oModel = this.getView().getModel("appView");

			oModel.setProperty("/busy", bIsBusy);
		}
	});
});<|endoftext|>