<commit_before>/**
 * Copyright 2014 aixigo AG
 * Released under the MIT license.
 * http://laxarjs.org/license
 */
define( [
   '../logging/log',
   '../utilities/path',
   '../utilities/assert',
   '../utilities/object',
   '../utilities/string',
   './paths',
   './features_provider',
   '../widget_adapters/adapters'
], function( log, path, assert, object, string, paths, featuresProvider, adapters ) {
   'use strict';

   var TYPE_WIDGET = 'widget';
   var TYPE_ACTIVITY = 'activity';
   var TECHNOLOGY_ANGULAR = 'angular';

   var DEFAULT_INTEGRATION = { type: TYPE_WIDGET, technology: TECHNOLOGY_ANGULAR };

   var ID_SEPARATOR = '-';
   var INVALID_ID_MATCHER = /[^A-Za-z0-9_\.-]/g;

   /**
    * @typedef {{then: Function}} Promise
    *
    * @param q
    * @param fileResourceProvider
    * @param themeManager
    * @param cssLoader
    * @param eventBus
    * @returns {{load: Function}}
    */
   function create( q, fileResourceProvider, themeManager, cssLoader, eventBus ) {

      return {
         load: load
      };

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Load a widget using an appropriate adapter
       *
       * First, get the given widget's specification to validate and instantiate the widget features.
       * Then, instantiate a widget adapter matching the widget's technology. Using the adapter, create the
       * widget controller. The adapter is returned and can be used to attach the widget to the DOM, or to
       * destroy it.
       *
       * @param {Object} widgetConfiguration
       *    a widget instance configuration (as used in page definitions) to instantiate the widget from
       * @param {Object} [optionalOptions]
       *    map of additonal options
       * @param {Function} optionalOptions.onBeforeControllerCreation
       *    a function to call just before the controller is set up. It receives environment and adapter
       *    specific injections as arguments
       *
       * @return {Promise} a promise for a widget adapter, with an already instantiated controller
       */
      function load( widgetConfiguration, optionalOptions ) {
         var resolvedWidgetPath = path.resolveAssetPath( widgetConfiguration.widget, paths.WIDGETS, 'local' );
         var widgetJsonPath = path.join( resolvedWidgetPath, 'widget.json' );

         var options = object.options( optionalOptions, {
            onBeforeControllerCreation: function() {}
         } );

         return fileResourceProvider.provide( widgetJsonPath )
            .then( function( specification ) {
               var integration = object.options( specification.integration, DEFAULT_INTEGRATION );
               var type = integration.type;
               var technology = integration.technology;
               // Handle legacy widget code:
               if( type === TECHNOLOGY_ANGULAR ) {
                  type = TYPE_WIDGET;
               }
               if( type !== TYPE_WIDGET && type !== TYPE_ACTIVITY ) {
                  throwError( widgetConfiguration, 'unknown integration type ' + type );
               }

               var throwWidgetError = throwError.bind( null, widgetConfiguration );
               var features =
                  featuresProvider.featuresForWidget( specification, widgetConfiguration, throwWidgetError );
               var anchorElement = document.createElement( 'DIV' );
               anchorElement.className = camelCaseToDashed( specification.name );
               anchorElement.id = 'ax' + ID_SEPARATOR + widgetConfiguration.id;
               var widgetEventBus = createEventBusForWidget( eventBus, specification, widgetConfiguration );

               var adapterFactory = adapters.getFor( technology );
               var adapter = adapterFactory.create( {
                  anchorElement: anchorElement,
                  context: {
                     eventBus: widgetEventBus,
                     features: features,
                     id: createIdGeneratorForWidget( widgetConfiguration.id ),
                     widget: {
                        area: widgetConfiguration.area,
                        id: widgetConfiguration.id,
                        path: widgetConfiguration.widget
                     }
                  },
                  specification: specification
               } );
               adapter.createController( options );

               return {
                  id: widgetConfiguration.id,
                  adapter: adapter,
                  destroy: function() {
                     widgetEventBus.release();
                     adapter.destroy();
                  },
                  applyViewChanges: adapterFactory.applyViewChanges || null,
                  templatePromise: loadAssets( resolvedWidgetPath, integration, specification )
               };

            }, function( err ) {
               var message = 'Could not load spec for widget [0] from [1]: [2]';
               log.error( message, widgetConfiguration.widget, widgetJsonPath, err );
            } );
      }

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Locates and loads the widget HTML template for this widget (if any) as well as any CSS stylesheets
       * used by this widget or its controls.
       *
       * @param widgetPath
       *    The path suffix used to look up the widget, as given in the instance configuration.
       * @param integration
       *    Details on the integration type and technology: Activities do not require assets.
       * @param widgetSpecification
       *    The widget specification, used to find out if any controls need to be loaded.
       *
       * @return {Promise<String>}
       *    A promise that will be resolved with the contents of any HTML template for this widget, or with
       *    `null` if there is no template (for example, if this is an activity).
       */
      function loadAssets( widgetPath, integration, widgetSpecification ) {
         return integration.type === TYPE_ACTIVITY ? q.when( null ) : resolve().then( function( urls ) {
            urls.cssFileUrls.forEach( function( url ) { cssLoader.load( url ); } );
            return urls.templateUrl ? fileResourceProvider.provide( urls.templateUrl ) : null;
         } );

         /////////////////////////////////////////////////////////////////////////////////////////////////////

         function resolve() {
            var technicalName = widgetPath.split( '/' ).pop();
            var htmlFile = technicalName + '.html';
            var cssFile = path.join( 'css/', technicalName + '.css' );

            var promises = [];
            promises.push( themeManager.urlProvider(
               path.join( widgetPath, '[theme]' ),
               path.join( paths.THEMES, '[theme]', 'widgets', widgetPath )
            ).provide( [ htmlFile, cssFile ] ) );

            promises = promises.concat( ( widgetSpecification.controls || [] )
               .map( function( controlReference ) {
                  // By appending a path now and .json afterwards, trick RequireJS into generating the
                  // correct descriptor path when loading from a 'package'.
                  var resolvedControlPath = path.resolveAssetPath( controlReference, paths.CONTROLS );
                  var descriptorUrl = path.join( resolvedControlPath, 'control.json' );
                  return fileResourceProvider.provide( descriptorUrl )
                     .then( function( descriptor ) {
                        // LaxarJS 1.x style control (name determined from descriptor):
                        var name = camelCaseToDashed( descriptor.name );
                        return themeManager.urlProvider(
                           path.join( resolvedControlPath, '[theme]' ),
                           path.join( paths.THEMES, '[theme]', 'controls', name )
                        ).provide( [ path.join( 'css/',  name + '.css' ) ] );
                     }, function() {
                        // LaxarJS 0.x style controls (no descriptor, uses AMD path as name):
                        var name = controlReference.split( '/' ).pop();
                        return themeManager.urlProvider(
                           path.join( resolvedControlPath, '[theme]' ),
                           path.join( paths.THEMES, '[theme]', controlReference )
                        ).provide( [ path.join( 'css/', name + '.css' ) ] );
                     } );
               } ) );

            return q.all( promises )
               .then( function( results ) {
                  var widgetUrls = results[ 0 ];
                  var cssUrls = results.slice( 1 )
                     .map( function( urls ) { return urls[ 0 ]; } )
                     .concat( widgetUrls.slice( 1 ) )
                     .filter( function( url ) { return !!url; } );

                  return {
                     templateUrl: widgetUrls[ 0 ] || '',
                     cssFileUrls: cssUrls
                  };
               } );
         }
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function camelCaseToDashed( str ) {
      return str.replace( /[A-Z]/g, function( character, offset ) {
         return ( offset > 0 ? '-' : '' ) + character.toLowerCase();
      } );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function throwError( widgetConfiguration, message ) {
      throw new Error( string.format(
         'Error loading widget "[widget]" (id: "[id]"): [0]', [ message ], widgetConfiguration
      ) );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function createIdGeneratorForWidget( widgetId ) {
      var charCodeOfA = 'a'.charCodeAt( 0 );
      function fixLetter( l ) {
         // We map invalid characters deterministically to valid lower case letters. Thereby a collision of
         // two ids with different invalid characters at the same positions is less likely to occur.
         return String.fromCharCode( charCodeOfA + l.charCodeAt( 0 ) % 26 );
      }

      var prefix = 'ax' + ID_SEPARATOR + widgetId.replace( INVALID_ID_MATCHER, fixLetter ) + ID_SEPARATOR;
      return function( localId ) {
         return prefix + ( '' + localId ).replace( INVALID_ID_MATCHER, fixLetter );
      };
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function createEventBusForWidget( eventBus, widgetSpecification, widgetConfiguration ) {

      var collaboratorId = 'widget.' + widgetSpecification.name + '#' + widgetConfiguration.id;

      function forward( to ) {
         return function() {
            return eventBus[ to ].apply( eventBus, arguments );
         };
      }

      function augmentOptions( optionalOptions ) {
         return object.options( optionalOptions, { sender: collaboratorId } );
      }

      var subscriptions = [];
      function unsubscribe( subscriber ) {
         eventBus.unsubscribe( subscriber );
      }

      return {
         addInspector: forward( 'addInspector' ),
         setErrorHandler: forward( 'setErrorHandler' ),
         setMediator: forward( 'setMediator' ),
         unsubscribe: unsubscribe,
         subscribe: function( eventName, subscriber, optionalOptions ) {
            subscriptions.push( subscriber );

            var options = object.options( optionalOptions, { subscriber: collaboratorId } );

            eventBus.subscribe( eventName, subscriber, options );
         },
         publish: function( eventName, optionalEvent, optionalOptions ) {
            return eventBus.publish( eventName, optionalEvent, augmentOptions( optionalOptions ) );
         },
         publishAndGatherReplies: function( eventName, optionalEvent, optionalOptions ) {
            return eventBus.publishAndGatherReplies( eventName, optionalEvent, augmentOptions( optionalOptions ) );
         },
         release: function() {
            subscriptions.forEach( unsubscribe );
         }
      };
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   return {
      create: create
   };

} );
<commit_msg>Proof of concept for fixing LaxarJS/laxar#225<commit_after>/**
 * Copyright 2014 aixigo AG
 * Released under the MIT license.
 * http://laxarjs.org/license
 */
define( [
   '../logging/log',
   '../utilities/path',
   '../utilities/assert',
   '../utilities/object',
   '../utilities/string',
   './paths',
   './features_provider',
   '../widget_adapters/adapters'
], function( log, path, assert, object, string, paths, featuresProvider, adapters ) {
   'use strict';

   var TYPE_WIDGET = 'widget';
   var TYPE_ACTIVITY = 'activity';
   var TECHNOLOGY_ANGULAR = 'angular';

   var DEFAULT_INTEGRATION = { type: TYPE_WIDGET, technology: TECHNOLOGY_ANGULAR };

   var ID_SEPARATOR = '-';
   var INVALID_ID_MATCHER = /[^A-Za-z0-9_\.-]/g;

   /**
    * @typedef {{then: Function}} Promise
    *
    * @param q
    * @param fileResourceProvider
    * @param themeManager
    * @param cssLoader
    * @param eventBus
    * @returns {{load: Function}}
    */
   function create( q, fileResourceProvider, themeManager, cssLoader, eventBus ) {

      return {
         load: load
      };

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Load a widget using an appropriate adapter
       *
       * First, get the given widget's specification to validate and instantiate the widget features.
       * Then, instantiate a widget adapter matching the widget's technology. Using the adapter, create the
       * widget controller. The adapter is returned and can be used to attach the widget to the DOM, or to
       * destroy it.
       *
       * @param {Object} widgetConfiguration
       *    a widget instance configuration (as used in page definitions) to instantiate the widget from
       * @param {Object} [optionalOptions]
       *    map of additonal options
       * @param {Function} optionalOptions.onBeforeControllerCreation
       *    a function to call just before the controller is set up. It receives environment and adapter
       *    specific injections as arguments
       *
       * @return {Promise} a promise for a widget adapter, with an already instantiated controller
       */
      function load( widgetConfiguration, optionalOptions ) {
         var resolvedWidgetPath = path.resolveAssetPath( widgetConfiguration.widget, paths.WIDGETS, 'local' );
         var widgetJsonPath = path.join( resolvedWidgetPath, 'widget.json' );

         var options = object.options( optionalOptions, {
            onBeforeControllerCreation: function() {}
         } );

         return fileResourceProvider.provide( widgetJsonPath )
            .then( function( specification ) {
               var integration = object.options( specification.integration, DEFAULT_INTEGRATION );
               var type = integration.type;
               var technology = integration.technology;
               // Handle legacy widget code:
               if( type === TECHNOLOGY_ANGULAR ) {
                  type = TYPE_WIDGET;
               }
               if( type !== TYPE_WIDGET && type !== TYPE_ACTIVITY ) {
                  throwError( widgetConfiguration, 'unknown integration type ' + type );
               }

               var throwWidgetError = throwError.bind( null, widgetConfiguration );
               var features =
                  featuresProvider.featuresForWidget( specification, widgetConfiguration, throwWidgetError );
               var anchorElement = document.createElement( 'DIV' );
               anchorElement.className = camelCaseToDashed( specification.name );
               anchorElement.id = 'ax' + ID_SEPARATOR + widgetConfiguration.id;
               var widgetEventBus = createEventBusForWidget( eventBus, specification, widgetConfiguration );

               var adapterFactory = adapters.getFor( technology );
               var adapter = adapterFactory.create( {
                  anchorElement: anchorElement,
                  context: {
                     eventBus: widgetEventBus,
                     features: features,
                     id: createIdGeneratorForWidget( widgetConfiguration.id ),
                     widget: {
                        area: widgetConfiguration.area,
                        id: widgetConfiguration.id,
                        path: widgetConfiguration.widget
                     }
                  },
                  specification: specification
               } );
               adapter.createController( options );

               return {
                  id: widgetConfiguration.id,
                  adapter: adapter,
                  destroy: function() {
                     widgetEventBus.release();
                     adapter.destroy();
                  },
                  applyViewChanges: adapterFactory.applyViewChanges || null,
                  templatePromise: loadAssets( resolvedWidgetPath, integration, specification )
               };

            }, function( err ) {
               var message = 'Could not load spec for widget [0] from [1]: [2]';
               log.error( message, widgetConfiguration.widget, widgetJsonPath, err );
            } );
      }

      ////////////////////////////////////////////////////////////////////////////////////////////////////////

      /**
       * Locates and loads the widget HTML template for this widget (if any) as well as any CSS stylesheets
       * used by this widget or its controls.
       *
       * @param widgetPath
       *    The path suffix used to look up the widget, as given in the instance configuration.
       * @param integration
       *    Details on the integration type and technology: Activities do not require assets.
       * @param widgetSpecification
       *    The widget specification, used to find out if any controls need to be loaded.
       *
       * @return {Promise<String>}
       *    A promise that will be resolved with the contents of any HTML template for this widget, or with
       *    `null` if there is no template (for example, if this is an activity).
       */
      function loadAssets( widgetPath, integration, widgetSpecification ) {
         return integration.type === TYPE_ACTIVITY ? q.when( null ) : resolve().then( function( urls ) {
            urls.cssFileUrls.forEach( function( url ) { cssLoader.load( url ); } );
            return urls.templateUrl ? fileResourceProvider.provide( urls.templateUrl ) : null;
         } );

         /////////////////////////////////////////////////////////////////////////////////////////////////////

         function resolve() {
            var specifiedName = camelCaseToDashed( widgetSpecification.name );
            var specifiedHtmlFile = specifiedName + '.html';
            var specifiedCssFile = path.join( 'css/', specifiedName + '.css' );
            var technicalName = widgetPath.split( '/' ).pop();
            var technicalHtmlFile = technicalName + '.html';
            var technicalCssFile = path.join( 'css/', technicalName + '.css' );

            var promises = [];
            promises.push( themeManager.urlProvider(
               path.join( widgetPath, '[theme]' ),
               path.join( paths.THEMES, '[theme]', 'widgets', widgetPath )
            ).provide( [
               specifiedHtmlFile,
               specifiedCssFile,
               technicalHtmlFile,
               technicalCssFile
            ] ) );

            promises = promises.concat( ( widgetSpecification.controls || [] )
               .map( function( controlReference ) {
                  // By appending a path now and .json afterwards, trick RequireJS into generating the
                  // correct descriptor path when loading from a 'package'.
                  var resolvedControlPath = path.resolveAssetPath( controlReference, paths.CONTROLS );
                  var descriptorUrl = path.join( resolvedControlPath, 'control.json' );
                  return fileResourceProvider.provide( descriptorUrl )
                     .then( function( descriptor ) {
                        // LaxarJS 1.x style control (name determined from descriptor):
                        var name = camelCaseToDashed( descriptor.name );
                        return themeManager.urlProvider(
                           path.join( resolvedControlPath, '[theme]' ),
                           path.join( paths.THEMES, '[theme]', 'controls', name )
                        ).provide( [ path.join( 'css/',  name + '.css' ) ] );
                     }, function() {
                        // LaxarJS 0.x style controls (no descriptor, uses AMD path as name):
                        var name = controlReference.split( '/' ).pop();
                        return themeManager.urlProvider(
                           path.join( resolvedControlPath, '[theme]' ),
                           path.join( paths.THEMES, '[theme]', controlReference )
                        ).provide( [ path.join( 'css/', name + '.css' ) ] );
                     } );
               } ) );

            return q.all( promises )
               .then( function( results ) {
                  var widgetUrls = results[ 0 ];
                  var cssUrls = results.slice( 1 )
                     .map( function( urls ) { return urls[ 0 ]; } )
                     .concat( ( widgetUrls[ 1 ] || widgetUrls[ 3 ] ) )
                     .filter( function( url ) { return !!url; } );

                  return {
                     templateUrl: widgetUrls[ 0 ] || widgetUrls[ 2 ] || '',
                     cssFileUrls: cssUrls
                  };
               } );
         }
      }
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function camelCaseToDashed( str ) {
      return str.replace( /[A-Z]/g, function( character, offset ) {
         return ( offset > 0 ? '-' : '' ) + character.toLowerCase();
      } );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function throwError( widgetConfiguration, message ) {
      throw new Error( string.format(
         'Error loading widget "[widget]" (id: "[id]"): [0]', [ message ], widgetConfiguration
      ) );
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function createIdGeneratorForWidget( widgetId ) {
      var charCodeOfA = 'a'.charCodeAt( 0 );
      function fixLetter( l ) {
         // We map invalid characters deterministically to valid lower case letters. Thereby a collision of
         // two ids with different invalid characters at the same positions is less likely to occur.
         return String.fromCharCode( charCodeOfA + l.charCodeAt( 0 ) % 26 );
      }

      var prefix = 'ax' + ID_SEPARATOR + widgetId.replace( INVALID_ID_MATCHER, fixLetter ) + ID_SEPARATOR;
      return function( localId ) {
         return prefix + ( '' + localId ).replace( INVALID_ID_MATCHER, fixLetter );
      };
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   function createEventBusForWidget( eventBus, widgetSpecification, widgetConfiguration ) {

      var collaboratorId = 'widget.' + widgetSpecification.name + '#' + widgetConfiguration.id;

      function forward( to ) {
         return function() {
            return eventBus[ to ].apply( eventBus, arguments );
         };
      }

      function augmentOptions( optionalOptions ) {
         return object.options( optionalOptions, { sender: collaboratorId } );
      }

      var subscriptions = [];
      function unsubscribe( subscriber ) {
         eventBus.unsubscribe( subscriber );
      }

      return {
         addInspector: forward( 'addInspector' ),
         setErrorHandler: forward( 'setErrorHandler' ),
         setMediator: forward( 'setMediator' ),
         unsubscribe: unsubscribe,
         subscribe: function( eventName, subscriber, optionalOptions ) {
            subscriptions.push( subscriber );

            var options = object.options( optionalOptions, { subscriber: collaboratorId } );

            eventBus.subscribe( eventName, subscriber, options );
         },
         publish: function( eventName, optionalEvent, optionalOptions ) {
            return eventBus.publish( eventName, optionalEvent, augmentOptions( optionalOptions ) );
         },
         publishAndGatherReplies: function( eventName, optionalEvent, optionalOptions ) {
            return eventBus.publishAndGatherReplies( eventName, optionalEvent, augmentOptions( optionalOptions ) );
         },
         release: function() {
            subscriptions.forEach( unsubscribe );
         }
      };
   }

   ///////////////////////////////////////////////////////////////////////////////////////////////////////////

   return {
      create: create
   };

} );
<|endoftext|>


<commit_before>/*!
 * Module dependencies.
 */

var chokidar = require('chokidar'),
    path = require('path'),
    useragent = require('./ext/useragent');

/**
 * AutoReload Middleware.
 *
 * Watches the file system for changes and notifies client.
 *
 * Options:
 *
 *   - `options` {Object}
 *     - `options.autoreload` {Boolean} to enable the middleware (default: true)
 */

module.exports = function(options) {
    var browserToggle = false,
        devicesConnected = [],
        lastModified = Date.now(),
        watches = [ path.join(process.cwd(), 'www/**/*') ];

    // optional options parameter
    options = options || {};
    if (typeof options.autoreload !== 'boolean')
        options.autoreload = true;

    // enable AutoReload
    if (options.autoreload) {
        var watch = chokidar.watch(watches, {
            ignored: /[\/\\]\./,
            ignoreInitial: true,
            persistent: true
        });

        // exposing watch to browser middleware
        options.watch = watch;

        watch.on('error', function(e) {
            if (options.emitter) {
                options.emitter.emit('error', e);
            }
        });

        // flag as outdated on all local file system changes
        watch.on('all', function(event, filepath) {
            lastModified = Date.now();

            options.filesToUpdate.push([Date.now(), filepath]);

            if (options.emitter) {
                options.emitter.emit('log', 'file changed', filepath);
            }
        });

        // stop watching when the server shutsdown
        options.emitter.on('close', function() {
            watch.close();
        });

        options.emitter.on('browserPrepare', function() {
            browserToggle = true;
        });
    }

    // the app constantly polls the server checking for the outdated state
    // if the app detects the outdated state to be true, it will force a reload on the page
    return function(req, res, next) {
        if (req.url.indexOf('/__api__/autoreload') === 0) {
            if (req.method === 'GET') {
                // by default, lastUpdated is undefined.
                // on the first non-autoreload request, it is timestamped.
                // when the first request is to autoreload, we timestamp
                // it to 0 because no content has ever been retrieved,
                // which means that the content on the device is out-of-date.
                if (!req.session.lastUpdated) {
                    req.session.lastUpdated = 0;
                }
            }
            else if (req.method === 'POST') {
                req.session.lastUpdated = Date.now();
            }

            if (browserToggle && useragent.parse(req.headers['user-agent']).platform === 'browser') {
                req.session.lastUpdated = 0;
                browserToggle = false;
            }

            // track new devices as they connect for analytics
            if (req.sessionID) {
                var sessionID = req.sessionID;
                if (sessionID && !devicesConnected[sessionID]) {
                    devicesConnected[sessionID] = true;
                    options.emitter.emit('deviceConnected');
                }
            }

            res.writeHead(200, { 'Content-Type': 'text/json' });

            res.end(JSON.stringify({
                content: {
                    lastModified: lastModified,
                    lastUpdated: req.session.lastUpdated,
                    outdated: (lastModified > req.session.lastUpdated)
                },
                projectChanged: (options.appID !== require('url').parse(req.url, true).query.appID)
            }));
        }
        else {
            // when lastUpdated is undefined, set it as up-to-date
            // since a legit resource request is going through
            if (!req.session.lastUpdated) {
                req.session.lastUpdated = Date.now();
            }
            next();
        }
    };
};
<commit_msg>Update autoreload.js<commit_after>/*!
 * Module dependencies.
 */

var chokidar = require('chokidar'),
    path = require('path'),
    useragent = require('./ext/useragent');

/**
 * AutoReload Middleware.
 *
 * Watches the file system for changes and notifies client.
 *
 * Options:
 *
 *   - `options` {Object}
 *     - `options.autoreload` {Boolean} to enable the middleware (default: true)
 */

module.exports = function(options) {
    var browserToggle = false,
        devicesConnected = [],
        lastModified = Date.now(),
        watches = [ path.join(process.cwd(), 'www/**/*') ];

    // optional options parameter
    options = options || {};
    if (typeof options.autoreload !== 'boolean')
        options.autoreload = true;

    // enable AutoReload
    if (options.autoreload) {
        var watch = chokidar.watch(watches, {
            ignored: /([\/\\]\.)|(node_modules)|(\/node_modules)/gm
            ignoreInitial: true,
            persistent: true
        });

        // exposing watch to browser middleware
        options.watch = watch;

        watch.on('error', function(e) {
            if (options.emitter) {
                options.emitter.emit('error', e);
            }
        });

        // flag as outdated on all local file system changes
        watch.on('all', function(event, filepath) {
            lastModified = Date.now();

            options.filesToUpdate.push([Date.now(), filepath]);

            if (options.emitter) {
                options.emitter.emit('log', 'file changed', filepath);
            }
        });

        // stop watching when the server shutsdown
        options.emitter.on('close', function() {
            watch.close();
        });

        options.emitter.on('browserPrepare', function() {
            browserToggle = true;
        });
    }

    // the app constantly polls the server checking for the outdated state
    // if the app detects the outdated state to be true, it will force a reload on the page
    return function(req, res, next) {
        if (req.url.indexOf('/__api__/autoreload') === 0) {
            if (req.method === 'GET') {
                // by default, lastUpdated is undefined.
                // on the first non-autoreload request, it is timestamped.
                // when the first request is to autoreload, we timestamp
                // it to 0 because no content has ever been retrieved,
                // which means that the content on the device is out-of-date.
                if (!req.session.lastUpdated) {
                    req.session.lastUpdated = 0;
                }
            }
            else if (req.method === 'POST') {
                req.session.lastUpdated = Date.now();
            }

            if (browserToggle && useragent.parse(req.headers['user-agent']).platform === 'browser') {
                req.session.lastUpdated = 0;
                browserToggle = false;
            }

            // track new devices as they connect for analytics
            if (req.sessionID) {
                var sessionID = req.sessionID;
                if (sessionID && !devicesConnected[sessionID]) {
                    devicesConnected[sessionID] = true;
                    options.emitter.emit('deviceConnected');
                }
            }

            res.writeHead(200, { 'Content-Type': 'text/json' });

            res.end(JSON.stringify({
                content: {
                    lastModified: lastModified,
                    lastUpdated: req.session.lastUpdated,
                    outdated: (lastModified > req.session.lastUpdated)
                },
                projectChanged: (options.appID !== require('url').parse(req.url, true).query.appID)
            }));
        }
        else {
            // when lastUpdated is undefined, set it as up-to-date
            // since a legit resource request is going through
            if (!req.session.lastUpdated) {
                req.session.lastUpdated = Date.now();
            }
            next();
        }
    };
};
<|endoftext|>


<commit_before>'use strict';

// 3rd-party
var async = require('async');

// internal
var prop  = require('../common').prop;
var Asset = require('./asset');

var BundledAsset = module.exports = function BundledAsset() {
  Asset.apply(this, arguments);
};

require('util').inherits(BundledAsset, Asset);

BundledAsset.prototype.getSource = function (callback) {
  var self = this;
  var Klass = this.environment.ContextClass;
  var processed_asset = this.environment.findAsset(this.pathname, {bundle: false});
  var context = new (Klass)(self.environment, self.logical_path, self.pathname);

  processed_asset.getSource(function (err, source) {
    async.forEachSeries(processed_asset.required_assets, function (asset, next) {
      asset.getSource(function (err, data) {
        source += data;
        next(err);
      });
    }, function (err) {
      context.evaluate(self.pathname, {
        data: source,
        processors: self.environment.getBundleProcessors(self.content_type)
      }, callback);
    });
  });
};
<commit_msg>Refactor BundledAsset<commit_after>/** internal
 *  class BundledAsset
 *
 *  `BundledAsset`s are used for files that need to be processed and
 *  concatenated with other assets. Use for `.js` and `.css` files.
 **/

'use strict';

// 3rd-party
var _     = require('underscore');
var async = require('async');

// internal
var prop    = require('../common').prop;
var getter  = require('../common').getter;
var Asset   = require('./asset');

/**
 *  new BundledAsset(environment, logical_path, pathname)
 **/
var BundledAsset = module.exports = function BundledAsset(environment, logical_path, pathname) {
  Asset.call(this, environment, logical_path, pathname);

  prop(this, '__processedAsset__', environment.findAsset(pathname, {bundle: false}));
};

require('util').inherits(BundledAsset, Asset);

/**
 *  BundledAsset#compile(callback) -> Void
 *
 *  Runs asset compilation.
 **/
BundledAsset.prototype.compile = function (callback) {
  var self = this;

  // do not compile again once asset was compiled
  if (this.__source__) {
    callback(null, this);
    return;
  }

  async.series([
    // compile processed asset first
    function (next) {
      self.__processedAsset__.compile(next);
    },

    // process ourselves
    function (next) {
      var Klass, context, processors, options, source = "";

      prop(self, '__requiredAssets__', self.__processedAsset__.__requiredAssets__);

      // gather dependency bodies
      self.toArray().forEach(function (dependency) {
        source += dependency.toString();
      });

      // prepare to build ourself
      Klass       = self.environment.ContextClass;
      context     = new Klass(self.environment, self.logical_path, self.pathname);
      processors  = self.environment.getBundleProcessors(self.contentType);
      options     = {data: source, processors: processors};

      context.evaluate(self.pathname, options, function (err, source) {
        if (err) {
          next(err);
          return;
        }

        self.__source__ = source;

        // update some props
        self.mtime  = _.max(_.pluck(self.toArray(), 'mtime'));
        self.length = source.length;
        self.digest = self.environment.digest.update(source).digest('hex');

        next(err);
      });
    }
  ], function (err) {
    if (err) {
      callback(err);
      return;
    }

    callback(null, self);
  });
};

/**
 *  BundledAsset#body -> String
 *
 *  Get asset's own processed contents. Excludes any of its required
 *  dependencies but does run any processors or engines on the
 *  original file.
 **/
getter(BundledAsset.prototype, 'body', function () {
  return this.__processedAsset__.source;
});

/**
 *  BundledAsset#source -> String
 *
 *  Get asset's processed content with all requried dependencies.
 *
 *
 *  ##### Throws Error
 *
 *  - When called before [[BundledAsset#compile]]
 **/
getter(BundledAsset.prototype, 'source', function () {
  if (!this.__source__) {
    throw new Error("Can't read body. Asset wasn't compiled yet.");
  }

  return this.__source__;
});

/**
 *  BundledAsset#dependencies -> Array
 *
 *  Return an `Array` of `Asset` files that are declared dependencies.
 **/
getter(BundledAsset.prototype, 'dependencies', function () {
  return _.reject(this.toArray(), function (asset) {
    return this.__processedAsset__ === asset;
  }, this);
});

/**
 *  BundledAsset#toArray() -> Array
 *
 *  Return array of porcessed assets this asset contains of.
 *
 *
 *  ##### Throws Error
 *
 *  - When called before [[BundledAsset#compile]]
 **/
BundledAsset.prototype.toArray = function () {
  if (!this.__requiredAssets__) {
    throw new Error("Can't get required assets. Asset wasn't compiled yet.");
  }

  return this.__requiredAssets__;
};

/**
 *  BundledAsset#isFresh() -> Boolean
 *
 *  Checks if Asset is stale by comparing the actual mtime and
 *  digest to the inmemory model.
 **/
BundledAsset.prototype.isFresh = function () {
  return this.__processedAsset__.isFresh(this.environment);
};
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */

var BinaryParser = require('./binary_parser').BinaryParser,
  inspect = require('util').inspect,
  debug = require('util').debug;

/**
 * Machine id.
 *
 * Create a random 3-byte value (i.e. unique for this
 * process). Other drivers use a md5 of the machine id here, but
 * that would mean an asyc call to gethostname, so we don't bother.
 */

var MACHINE_ID = parseInt(Math.random() * 0xFFFFFF, 10);

/**
 * Constructor.
 *
 * @param {String} id (optional)
 */

function ObjectID (id) {
  // Throw an error if it's not a valid setup
  if(id != null && 'number' != typeof id && (id.length != 12 && id.length != 24)) throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters in hex format");
  // Generate id based on the input
  if (null == id || 'number' == typeof id) {
    this.id = this.generate(id);
  } else if (/^[0-9a-fA-F]{24}$/.test(id)) {
    return ObjectID.createFromHexString(id);
  } else {
    this.id = id;
  }
};

/**
 * Updates the ObjectID index.
 *
 * @return {Number}
 */

ObjectID.prototype.get_inc = function() {
  return ObjectID.index = (ObjectID.index + 1) % 0xFFFFFF;
};

/**
 * Generates an ObjectId.
 *
 * @return {String}
 */

ObjectID.prototype.generate = function(time) {
  if ('number' == typeof time) {
    var time4Bytes = BinaryParser.encodeInt(time, 32, true, true);
    /* for time-based ObjectID the bytes following the time will be zeroed */
    var machine3Bytes = BinaryParser.encodeInt(0, 24, false);
    var pid2Bytes = BinaryParser.fromShort(0);
    var index3Bytes = BinaryParser.encodeInt(0, 24, false, true);
  } else {
  	var unixTime = parseInt(Date.now()/1000,10);
    var time4Bytes = BinaryParser.encodeInt(unixTime, 32, true, true);
    var machine3Bytes = BinaryParser.encodeInt(MACHINE_ID, 24, false);
    var pid2Bytes = BinaryParser.fromShort(process.pid);
    var index3Bytes = BinaryParser.encodeInt(this.get_inc(), 24, false, true);
  }

  return time4Bytes + machine3Bytes + pid2Bytes + index3Bytes;
};

/**
 * Converts this ObjectId to a hex string.
 *
 * @return {String}
 */

ObjectID.prototype.toHexString = function() {
  var hexString = ''
    , number
    , value;

  for (var index = 0, len = this.id.length; index < len; index++) {
    value = BinaryParser.toByte(this.id.substr(index, 1));
    number = value <= 15
      ? '0' + value.toString(16)
      : value.toString(16);
    hexString = hexString + number;
  }

  return hexString;
};

/**
 * Converts this id to a string.
 *
 * @return {String}
 */

ObjectID.prototype.toString = function() {
  return this.toHexString();
};

/**
 * Converts to a string representation of this Id.
 */

ObjectID.prototype.inspect = ObjectID.prototype.toString;

/**
 * Converts to its JSON representation.
 */

ObjectID.prototype.toJSON = ObjectID.prototype.toString;

/**
 * Compares the equality of this ObjectID with `otherID`.
 *
 * @return {Bool}
 */

ObjectID.prototype.equals = function equals (otherID) {
  var id = (otherID instanceof ObjectID || otherID.toHexString)
    ? otherID.id
    : ObjectID.createFromHexString(otherID).id;

  return this.id === id;
}

/**
 * Returns the generation time in seconds that this
 * ID was generated.
 *
 * @return {Number}
 */

ObjectID.prototype.__defineGetter__("generationTime", function() {
  return Math.floor(BinaryParser.decodeInt(this.id.substring(0,4), 32, true, true));
});

ObjectID.prototype.getTimestamp = function() {
  var timestamp = new Date();
  timestamp.setTime(Math.floor(BinaryParser.decodeInt(this.id.substring(0,4), 32, true, true)) * 1000);
  return timestamp;
}

/**
 * Statics.
 */

ObjectID.index = 0;

ObjectID.createPk = function createPk () {
  return new ObjectID();
};

/**
 * Creates an ObjectID from a hex string representation
 * of an ObjectID.
 *
 * @param {String} hexString
 * @return {ObjectID}
 */

ObjectID.createFromHexString = function createFromHexString (hexString) {
  // Throw an error if it's not a valid setup
  if(hexString != null && hexString.length != 24) throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters in hex format");

  var len = hexString.length;

  if(len > 12*2) {
    throw new Error('Id cannot be longer than 12 bytes');
  }

  var result = ''
    , string
    , number;

  for (var index = 0; index < len; index += 2) {
    string = hexString.substr(index, 2);
    number = parseInt(string, 16);
    result += BinaryParser.fromByte(number);
  }

  return new ObjectID(result);
};

/**
 * Expose.
 */

exports.ObjectID = ObjectID;

<commit_msg>~10% perf improvement for ObjectId#toHexString<commit_after>/**
 * Module dependencies.
 */

var BinaryParser = require('./binary_parser').BinaryParser,
  inspect = require('util').inspect,
  debug = require('util').debug;

/**
 * Machine id.
 *
 * Create a random 3-byte value (i.e. unique for this
 * process). Other drivers use a md5 of the machine id here, but
 * that would mean an asyc call to gethostname, so we don't bother.
 */

var MACHINE_ID = parseInt(Math.random() * 0xFFFFFF, 10);

/**
 * Constructor.
 *
 * @param {String} id (optional)
 */

function ObjectID (id) {
  // Throw an error if it's not a valid setup
  if(id != null && 'number' != typeof id && (id.length != 12 && id.length != 24)) throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters in hex format");
  // Generate id based on the input
  if (null == id || 'number' == typeof id) {
    this.id = this.generate(id);
  } else if (/^[0-9a-fA-F]{24}$/.test(id)) {
    return ObjectID.createFromHexString(id);
  } else {
    this.id = id;
  }
};

/**
 * Updates the ObjectID index.
 *
 * @return {Number}
 */

ObjectID.prototype.get_inc = function() {
  return ObjectID.index = (ObjectID.index + 1) % 0xFFFFFF;
};

/**
 * Generates an ObjectId.
 *
 * @return {String}
 */

ObjectID.prototype.generate = function(time) {
  if ('number' == typeof time) {
    var time4Bytes = BinaryParser.encodeInt(time, 32, true, true);
    /* for time-based ObjectID the bytes following the time will be zeroed */
    var machine3Bytes = BinaryParser.encodeInt(0, 24, false);
    var pid2Bytes = BinaryParser.fromShort(0);
    var index3Bytes = BinaryParser.encodeInt(0, 24, false, true);
  } else {
  	var unixTime = parseInt(Date.now()/1000,10);
    var time4Bytes = BinaryParser.encodeInt(unixTime, 32, true, true);
    var machine3Bytes = BinaryParser.encodeInt(MACHINE_ID, 24, false);
    var pid2Bytes = BinaryParser.fromShort(process.pid);
    var index3Bytes = BinaryParser.encodeInt(this.get_inc(), 24, false, true);
  }

  return time4Bytes + machine3Bytes + pid2Bytes + index3Bytes;
};

/**
 * Converts this ObjectId to a hex string.
 *
 * @return {String}
 */

ObjectID.prototype.toHexString = function() {
  var hexString = ''
    , number
    , value;

  for (var index = 0, len = this.id.length; index < len; index++) {
    value = BinaryParser.toByte(this.id[index]);
    number = value <= 15
      ? '0' + value.toString(16)
      : value.toString(16);
    hexString = hexString + number;
  }

  return hexString;
};

/**
 * Converts this id to a string.
 *
 * @return {String}
 */

ObjectID.prototype.toString = function() {
  return this.toHexString();
};

/**
 * Converts to a string representation of this Id.
 */

ObjectID.prototype.inspect = ObjectID.prototype.toString;

/**
 * Converts to its JSON representation.
 */

ObjectID.prototype.toJSON = ObjectID.prototype.toString;

/**
 * Compares the equality of this ObjectID with `otherID`.
 *
 * @return {Bool}
 */

ObjectID.prototype.equals = function equals (otherID) {
  var id = (otherID instanceof ObjectID || otherID.toHexString)
    ? otherID.id
    : ObjectID.createFromHexString(otherID).id;

  return this.id === id;
}

/**
 * Returns the generation time in seconds that this
 * ID was generated.
 *
 * @return {Number}
 */

ObjectID.prototype.__defineGetter__("generationTime", function() {
  return Math.floor(BinaryParser.decodeInt(this.id.substring(0,4), 32, true, true));
});

ObjectID.prototype.getTimestamp = function() {
  var timestamp = new Date();
  timestamp.setTime(Math.floor(BinaryParser.decodeInt(this.id.substring(0,4), 32, true, true)) * 1000);
  return timestamp;
}

/**
 * Statics.
 */

ObjectID.index = 0;

ObjectID.createPk = function createPk () {
  return new ObjectID();
};

/**
 * Creates an ObjectID from a hex string representation
 * of an ObjectID.
 *
 * @param {String} hexString
 * @return {ObjectID}
 */

ObjectID.createFromHexString = function createFromHexString (hexString) {
  // Throw an error if it's not a valid setup
  if(hexString != null && hexString.length != 24) throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters in hex format");

  var len = hexString.length;

  if(len > 12*2) {
    throw new Error('Id cannot be longer than 12 bytes');
  }

  var result = ''
    , string
    , number;

  for (var index = 0; index < len; index += 2) {
    string = hexString.substr(index, 2);
    number = parseInt(string, 16);
    result += BinaryParser.fromByte(number);
  }

  return new ObjectID(result);
};

/**
 * Expose.
 */

exports.ObjectID = ObjectID;

<|endoftext|>


<commit_before>/**
 * @file CascaderList component
 * @author wendy(wendy.wei@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import CascaderListItem from '../_CascaderListItem/CascaderListItem';
import Theme from '../Theme';
import Tip from '../Tip';

import Util from '../_vendors/Util';
import CascaderCalculation from '../_vendors/TreeCalculation';

export default class CascaderList extends Component {

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value,
            path: CascaderCalculation.calPath(props.value, {
                ...props,
                data: props.items
            })
        };

        this.changeHandler = ::this.changeHandler;

    }

    changeHandler(path) {

        const value = path[path.length - 1].value;

        this.setState({
            path,
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value, path);
        });

    }

    render() {
        const {className, style, listWidth, items, valueField, displayField} = this.props;
        const {path, value} = this.state;

        return (
            <div className={`cascader-list ${className}`}
                 style={style}>
                <CascaderListItem listData={items}
                                  value={value}
                                  path={path}
                                  listWidth={listWidth}
                                  valueField={valueField}
                                  displayField={displayField}
                                  depth={CascaderCalculation.calDepth(items, path)}
                                  onChange={this.changeHandler}/>

        );
    }
}

CascaderList.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The value of CascaderList.
     */
    value: PropTypes.any,

    /**
     * The width of CascaderList.
     */
    listWidth: PropTypes.number,

    /**
     * The item-data of CascaderList.
     */
    items: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

            /**
             * The CSS class name of the list button.
             */
            className: PropTypes.string,

            /**
             * Override the styles of the list button.
             */
            style: PropTypes.object,

            /**
             * The theme of the list button.
             */
            theme: PropTypes.oneOf(Object.keys(Theme).map(key => Theme[key])),

            /**
             * The text value of the list button.Type can be string or number.
             */
            value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The list item's display text. Type can be string, number or bool.
             */
            text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The desc value of the list button. Type can be string or number.
             */
            desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * If true,the list item will be disabled.
             */
            disabled: PropTypes.bool,

            /**
             * If true,the button will be have loading effect.
             */
            isLoading: PropTypes.bool,

            /**
             * If true,the element's ripple effect will be disabled.
             */
            disableTouchRipple: PropTypes.bool,

            /**
             * Use this property to display an icon. It will display on the left.
             */
            iconCls: PropTypes.string,

            /**
             * Use this property to display an icon. It will display on the right.
             */
            rightIconCls: PropTypes.string,

            /**
             * The message of tip.
             */
            tip: PropTypes.string,

            /**
             * The position of tip.
             */
            tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

            /**
             * If true,the item will have center displayed ripple effect.
             */
            rippleDisplayCenter: PropTypes.bool,

            children: PropTypes.array,

            /**
             * You can create a complicated renderer callback instead of value and desc prop.
             */
            itemRenderer: PropTypes.func,

            /**
             * Callback function fired when a list item touch-tapped.
             */
            onTouchTap: PropTypes.func

        }), PropTypes.string, PropTypes.number])),

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The depth of CascaderList.
     */
    depth: PropTypes.number

};

CascaderList.defaultProps = {

    className: '',
    style: null,
    listWidth: 200,

    items: [],

    valueField: 'value',
    displayField: 'text'

};<commit_msg>optimize CascaderList<commit_after>/**
 * @file CascaderList component
 * @author wendy(wendy.wei@derbysoft.com)
 */

import React, {Component} from 'react';
import PropTypes from 'prop-types';

import CascaderListItem from '../_CascaderListItem/CascaderListItem';
import Theme from '../Theme';
import Tip from '../Tip';

import Util from '../_vendors/Util';
import CascaderCalculation from '../_vendors/TreeCalculation';

export default class CascaderList extends Component {

    static Theme = Theme;

    constructor(props, ...restArgs) {

        super(props, ...restArgs);

        this.state = {
            value: props.value,
            path: CascaderCalculation.calPath(props.value, {
                ...props,
                data: props.items
            })
        };

        this.changeHandler = ::this.changeHandler;

    }

    changeHandler(path) {

        const value = path[path.length - 1].value;

        this.setState({
            path,
            value
        }, () => {
            const {onChange} = this.props;
            onChange && onChange(value, path);
        });

    }

    render() {
        const {className, style, listWidth, items, valueField, displayField} = this.props;
        const {path, value} = this.state;

        return (
            <div className={`cascader-list ${className}`}
                 style={style}>
                <CascaderListItem listData={items}
                                  value={value}
                                  path={path}
                                  listWidth={listWidth}
                                  valueField={valueField}
                                  displayField={displayField}
                                  depth={CascaderCalculation.calDepth(items, path)}
                                  onChange={this.changeHandler}/>

        );
    }
}

CascaderList.propTypes = {

    /**
     * The CSS class name of the root element.
     */
    className: PropTypes.string,

    /**
     * Override the styles of the root element.
     */
    style: PropTypes.object,

    /**
     * The value of CascaderList.
     */
    value: PropTypes.any,

    /**
     * The width of CascaderList.
     */
    listWidth: PropTypes.number,

    /**
     * The item-data of CascaderList.
     */
    items: PropTypes.oneOfType([

        // not grouped
        PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape({

            /**
             * The CSS class name of the list button.
             */
            className: PropTypes.string,

            /**
             * Override the styles of the list button.
             */
            style: PropTypes.object,

            /**
             * The theme of the list button.
             */
            theme: PropTypes.oneOf(Object.keys(Theme).map(key => Theme[key])),

            /**
             * The text value of the list button.Type can be string or number.
             */
            value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The list item's display text. Type can be string, number or bool.
             */
            text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * The desc value of the list button. Type can be string or number.
             */
            desc: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

            /**
             * If true,the list item will be disabled.
             */
            disabled: PropTypes.bool,

            /**
             * If true,the button will be have loading effect.
             */
            isLoading: PropTypes.bool,

            /**
             * If true,the element's ripple effect will be disabled.
             */
            disableTouchRipple: PropTypes.bool,

            /**
             * Use this property to display an icon. It will display on the left.
             */
            iconCls: PropTypes.string,

            /**
             * Use this property to display an icon. It will display on the right.
             */
            rightIconCls: PropTypes.string,

            /**
             * The message of tip.
             */
            tip: PropTypes.string,

            /**
             * The position of tip.
             */
            tipPosition: PropTypes.oneOf(Util.enumerateValue(Tip.Position)),

            /**
             * If true,the item will have center displayed ripple effect.
             */
            rippleDisplayCenter: PropTypes.bool,

            children: PropTypes.array,

            /**
             * You can create a complicated renderer callback instead of value and desc prop.
             */
            itemRenderer: PropTypes.func,

            /**
             * Callback function fired when a list item touch-tapped.
             */
            onTouchTap: PropTypes.func

        }), PropTypes.string, PropTypes.number])),

        // grouped
        PropTypes.array

    ]).isRequired,

    /**
     * The value field name in data. (default: "value")
     */
    valueField: PropTypes.string,

    /**
     * The display field name in data. (default: "text")
     */
    displayField: PropTypes.string,

    /**
     * The depth of CascaderList.
     */
    depth: PropTypes.number

};

CascaderList.defaultProps = {

    className: '',
    style: null,
    listWidth: 200,

    items: [],

    valueField: 'value',
    displayField: 'text'

};<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');
const rules = require('./rules');

// Rule settings can take a number of forms, e.g.
// a. "rule-name": null
// b. "rule-name": [null, ...]
// c. "rule-name": primaryOption
// d. "rule-name": [primaryOption]
// e. "rule-name": [primaryOption, secondaryOption]
// Where primaryOption can be anything: primitive, Object, or Array.

/**
 * This function normalizes all the possibilities into the
 * standard form: [primaryOption, secondaryOption]
 * Except in the cases with null, a & b, in which case
 * null is returned
 * @template T
 * @template {Object} O
 * @param {import('stylelint').StylelintConfigRuleSettings<T, O>} rawSettings
 * @param {string} ruleName
 * @param {boolean} [primaryOptionArray] If primaryOptionArray is not provided, we try to get it from the rules themselves, which will not work for plugins
 * @return {[T] | [T, O] | null}
 */
module.exports = function (
	rawSettings,
	ruleName,
	// If primaryOptionArray is not provided, we try to get it from the
	// rules themselves, which will not work for plugins
	primaryOptionArray,
) {
	if (_.isNil(rawSettings)) {
		return null;
	}

	if (!Array.isArray(rawSettings)) {
		return [rawSettings];
	}
	// Everything below is an array ...

	if (rawSettings.length > 0 && _.isNil(rawSettings[0])) {
		return null;
	}

	if (primaryOptionArray === undefined) {
		const rule = rules[ruleName];

		if (rule && 'primaryOptionArray' in rule) {
			primaryOptionArray = rule.primaryOptionArray;
		}
	}

	if (!primaryOptionArray) {
		return rawSettings;
	}
	// Everything below is a rule that CAN have an array for a primary option ...
	// (they might also have something else, e.g. rule-properties-order can
	// have the string "alphabetical")

	if (rawSettings.length === 1 && Array.isArray(rawSettings[0])) {
		return rawSettings;
	}

	if (
		rawSettings.length === 2 &&
		!_.isPlainObject(rawSettings[0]) &&
		_.isPlainObject(rawSettings[1])
	) {
		return rawSettings;
	}

	// `T` must be an array type, but TSC thinks it's probably invalid to
	// cast `[T]` to `T` so we cast through `any` first.
	return [/** @type {T} */ (/** @type {any} */ (rawSettings))];
};
<commit_msg>Remove `_.isNil` (#5330)<commit_after>'use strict';

const _ = require('lodash');
const rules = require('./rules');

// Rule settings can take a number of forms, e.g.
// a. "rule-name": null
// b. "rule-name": [null, ...]
// c. "rule-name": primaryOption
// d. "rule-name": [primaryOption]
// e. "rule-name": [primaryOption, secondaryOption]
// Where primaryOption can be anything: primitive, Object, or Array.

/**
 * This function normalizes all the possibilities into the
 * standard form: [primaryOption, secondaryOption]
 * Except in the cases with null, a & b, in which case
 * null is returned
 * @template T
 * @template {Object} O
 * @param {import('stylelint').StylelintConfigRuleSettings<T, O>} rawSettings
 * @param {string} ruleName
 * @param {boolean} [primaryOptionArray] If primaryOptionArray is not provided, we try to get it from the rules themselves, which will not work for plugins
 * @return {[T] | [T, O] | null}
 */
module.exports = function (
	rawSettings,
	ruleName,
	// If primaryOptionArray is not provided, we try to get it from the
	// rules themselves, which will not work for plugins
	primaryOptionArray,
) {
	if (rawSettings === null || rawSettings === undefined) {
		return null;
	}

	if (!Array.isArray(rawSettings)) {
		return [rawSettings];
	}
	// Everything below is an array ...

	if (rawSettings.length > 0 && (rawSettings[0] === null || rawSettings[0] === undefined)) {
		return null;
	}

	if (primaryOptionArray === undefined) {
		const rule = rules[ruleName];

		if (rule && 'primaryOptionArray' in rule) {
			primaryOptionArray = rule.primaryOptionArray;
		}
	}

	if (!primaryOptionArray) {
		return rawSettings;
	}
	// Everything below is a rule that CAN have an array for a primary option ...
	// (they might also have something else, e.g. rule-properties-order can
	// have the string "alphabetical")

	if (rawSettings.length === 1 && Array.isArray(rawSettings[0])) {
		return rawSettings;
	}

	if (
		rawSettings.length === 2 &&
		!_.isPlainObject(rawSettings[0]) &&
		_.isPlainObject(rawSettings[1])
	) {
		return rawSettings;
	}

	// `T` must be an array type, but TSC thinks it's probably invalid to
	// cast `[T]` to `T` so we cast through `any` first.
	return [/** @type {T} */ (/** @type {any} */ (rawSettings))];
};
<|endoftext|>


<commit_before>import FileSaver from 'file-saver';
import {
  getArrangerTableColumns,
  queryDataByIds,
  queryCountByValues,
  queryDataByValues,
  queryAggregations,
} from './arrangerQueryHelper';
import { hasKeyChain } from './utils';
import { fetchWithCreds } from '../actions';
import { manifestServiceApiPath } from '../localconf';

const checkArrangerGraphqlField = (arrangerConfig) => {
  const MSG_GQLFIELD_FAIL = 'Couldn\'t find key "graphqlField" in Arranger configuration.';
  if (!hasKeyChain(arrangerConfig, 'graphqlField')) {
    throw MSG_GQLFIELD_FAIL;
  }
};

/**
 * Create manifest JSON for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.manifestMapping.resourceIndexType - type name of resource index
 * @param {string} arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex - name of
 *                                reference field in resource index
 * @param {string} messageOnFail - message for describing a failure
 * @param {Function} errorCallback - callback function on error (Optional)
 *                                If undefined, simply throw error message
 */
const createManifestByFilter = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  errorCallback,
  messageOnFail,
) => {
  checkArrangerGraphqlField(arrangerConfig);
  if (!hasKeyChain(arrangerConfig, 'manifestMapping.resourceIndexType')
    || !hasKeyChain(arrangerConfig, 'manifestMapping.referenceIdFieldInResourceIndex')) {
    if (errorCallback === undefined) {
      throw messageOnFail;
    } else {
      errorCallback(500, messageOnFail);
    }
  }
  // Fetch docs by bucket for this key
  const key = arrangerConfig.manifestMapping.divideKey;
  const rowFilter = {
    name: arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
    values: selectedTableRows,
  };
  let filtersByKey = [[rowFilter]];
  if (key !== undefined) {
    const keyBuckets = await queryAggregations(
      apiFunc,
      projectId,
      arrangerConfig.manifestMapping.resourceIndexType,
      key,
    );
    filtersByKey = keyBuckets.map(
      bucket => [rowFilter, { name: key, values: [bucket.key] }]);
  }
  const manifestJSON = await Promise.all(
    filtersByKey.map(filters => queryDataByValues(
      apiFunc,
      projectId,
      arrangerConfig.manifestMapping.resourceIndexType,
      filters,
      [
        arrangerConfig.manifestMapping.resourceIdField,
        arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
      ],
    )));
  return manifestJSON;
};

/**
 * Download selected data in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {string} fileName - file name for downloading
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.graphqlField - the data type name for arranger
 */
export const downloadData = async (
  apiFunc,
  projectId,
  graphqlIdField,
  selectedTableRows,
  arrangerConfig,
  fileName,
) => {
  checkArrangerGraphqlField(arrangerConfig);
  const columns = await getArrangerTableColumns(
    apiFunc,
    projectId,
    arrangerConfig.graphqlField,
  );
  const responseDataJSON = await queryDataByIds(
    apiFunc,
    projectId,
    graphqlIdField,
    selectedTableRows,
    arrangerConfig.graphqlField,
    columns,
  );
  const blob = new Blob([JSON.stringify(responseDataJSON, null, 2)], { type: 'text/json' });
  FileSaver.saveAs(blob, fileName);
};

/**
 * Download manifest data for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} fileName - file name for downloading
 */
export const downloadManifest = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  fileName,
) => {
  const MSG_DOWNLOAD_MANIFEST_FAIL = 'Error downloading manifest file';
  const manifestJSON = await createManifestByFilter(
    apiFunc,
    projectId,
    selectedTableRows,
    arrangerConfig,
    undefined,
    MSG_DOWNLOAD_MANIFEST_FAIL);
  const blob = new Blob([JSON.stringify(manifestJSON, null, 2)], { type: 'text/json' });
  FileSaver.saveAs(blob, fileName);
};

/**
 * Download manifest data for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {Function} callback - callback function on success
 * @param {Function} errorCallback - callback function on error
 */
export const exportToWorkspace = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  callback,
  errorCallback,
) => {
  const MSG_EXPORT_MANIFEST_FAIL = 'Error exporting manifest file.';
  const MSG_EXPORT_FAIL = 'There was an error exporting your cohort.';
  const manifestJSON = await createManifestByFilter(
    apiFunc,
    projectId,
    selectedTableRows,
    arrangerConfig,
    errorCallback,
    MSG_EXPORT_MANIFEST_FAIL);
  fetchWithCreds({
    path: `${manifestServiceApiPath}`,
    body: JSON.stringify(manifestJSON.flat()),
    method: 'POST',
  })
    .then(
      ({ status, data }) => {
        switch (status) {
        case 200:
          callback(data);
          return;
        default:
          errorCallback(status, MSG_EXPORT_FAIL);
        }
      },
    );
};

/**
 * Get number of manifest entries for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.graphqlField - the data type name for arranger
 * @param {string} arrangerConfig.manifestMapping.resourceIndexType - type name of resource index
 * @param {string} arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex - name of
 *                                reference field in resource index
 * @returns {number} number of manifest entries
 */
export const getManifestEntryCount = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
) => {
  const MSG_GET_MANIFEST_COUNT_FAIL = 'Error getting manifest file count';
  checkArrangerGraphqlField(arrangerConfig);
  if (!hasKeyChain(arrangerConfig, 'manifestMapping.resourceIndexType')
    || !hasKeyChain(arrangerConfig, 'manifestMapping.referenceIdFieldInResourceIndex')) {
    throw MSG_GET_MANIFEST_COUNT_FAIL;
  }

  const manifestEntryCount = await queryCountByValues(
    apiFunc,
    projectId,
    arrangerConfig.manifestMapping.resourceIndexType,
    [{
      name: arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
      values: selectedTableRows,
    }],
  );
  return manifestEntryCount;
};
<commit_msg>fix/flat manifest (#541)<commit_after>import FileSaver from 'file-saver';
import {
  getArrangerTableColumns,
  queryDataByIds,
  queryCountByValues,
  queryDataByValues,
  queryAggregations,
} from './arrangerQueryHelper';
import { hasKeyChain } from './utils';
import { fetchWithCreds } from '../actions';
import { manifestServiceApiPath } from '../localconf';

const checkArrangerGraphqlField = (arrangerConfig) => {
  const MSG_GQLFIELD_FAIL = 'Couldn\'t find key "graphqlField" in Arranger configuration.';
  if (!hasKeyChain(arrangerConfig, 'graphqlField')) {
    throw MSG_GQLFIELD_FAIL;
  }
};

/**
 * Create manifest JSON for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.manifestMapping.resourceIndexType - type name of resource index
 * @param {string} arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex - name of
 *                                reference field in resource index
 * @param {string} messageOnFail - message for describing a failure
 * @param {Function} errorCallback - callback function on error (Optional)
 *                                If undefined, simply throw error message
 */
const createManifestByFilter = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  errorCallback,
  messageOnFail,
) => {
  checkArrangerGraphqlField(arrangerConfig);
  if (!hasKeyChain(arrangerConfig, 'manifestMapping.resourceIndexType')
    || !hasKeyChain(arrangerConfig, 'manifestMapping.referenceIdFieldInResourceIndex')) {
    if (errorCallback === undefined) {
      throw messageOnFail;
    } else {
      errorCallback(500, messageOnFail);
    }
  }
  // Fetch docs by bucket for this key
  const key = arrangerConfig.manifestMapping.divideKey;
  const rowFilter = {
    name: arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
    values: selectedTableRows,
  };
  let filtersByKey = [[rowFilter]];
  if (key !== undefined) {
    const keyBuckets = await queryAggregations(
      apiFunc,
      projectId,
      arrangerConfig.manifestMapping.resourceIndexType,
      key,
    );
    filtersByKey = keyBuckets.map(
      bucket => [rowFilter, { name: key, values: [bucket.key] }]);
  }
  const manifestJSON = await Promise.all(
    filtersByKey.map(filters => queryDataByValues(
      apiFunc,
      projectId,
      arrangerConfig.manifestMapping.resourceIndexType,
      filters,
      [
        arrangerConfig.manifestMapping.resourceIdField,
        arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
      ],
    )));
  return manifestJSON;
};

/**
 * Download selected data in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {string} fileName - file name for downloading
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.graphqlField - the data type name for arranger
 */
export const downloadData = async (
  apiFunc,
  projectId,
  graphqlIdField,
  selectedTableRows,
  arrangerConfig,
  fileName,
) => {
  checkArrangerGraphqlField(arrangerConfig);
  const columns = await getArrangerTableColumns(
    apiFunc,
    projectId,
    arrangerConfig.graphqlField,
  );
  const responseDataJSON = await queryDataByIds(
    apiFunc,
    projectId,
    graphqlIdField,
    selectedTableRows,
    arrangerConfig.graphqlField,
    columns,
  );
  const blob = new Blob([JSON.stringify(responseDataJSON, null, 2)], { type: 'text/json' });
  FileSaver.saveAs(blob, fileName);
};

/**
 * Download manifest data for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} fileName - file name for downloading
 */
export const downloadManifest = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  fileName,
) => {
  const MSG_DOWNLOAD_MANIFEST_FAIL = 'Error downloading manifest file';
  const manifestJSON = await createManifestByFilter(
    apiFunc,
    projectId,
    selectedTableRows,
    arrangerConfig,
    undefined,
    MSG_DOWNLOAD_MANIFEST_FAIL);
  const blob = new Blob([JSON.stringify(manifestJSON.flat(), null, 2)], { type: 'text/json' });
  FileSaver.saveAs(blob, fileName);
};

/**
 * Download manifest data for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {Function} callback - callback function on success
 * @param {Function} errorCallback - callback function on error
 */
export const exportToWorkspace = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
  callback,
  errorCallback,
) => {
  const MSG_EXPORT_MANIFEST_FAIL = 'Error exporting manifest file.';
  const MSG_EXPORT_FAIL = 'There was an error exporting your cohort.';
  const manifestJSON = await createManifestByFilter(
    apiFunc,
    projectId,
    selectedTableRows,
    arrangerConfig,
    errorCallback,
    MSG_EXPORT_MANIFEST_FAIL);
  fetchWithCreds({
    path: `${manifestServiceApiPath}`,
    body: JSON.stringify(manifestJSON.flat()),
    method: 'POST',
  })
    .then(
      ({ status, data }) => {
        switch (status) {
        case 200:
          callback(data);
          return;
        default:
          errorCallback(status, MSG_EXPORT_FAIL);
        }
      },
    );
};

/**
 * Get number of manifest entries for selected rows in arranger table.
 * @param {function} apiFunc - function created by arranger for fetching data
 * @param {string} projectId - arranger project ID
 * @param {string[]} selectedTableRows - list of ids of selected rows
 * @param {Object} arrangerConfig - arranger configuration object
 * @param {string} arrangerConfig.graphqlField - the data type name for arranger
 * @param {string} arrangerConfig.manifestMapping.resourceIndexType - type name of resource index
 * @param {string} arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex - name of
 *                                reference field in resource index
 * @returns {number} number of manifest entries
 */
export const getManifestEntryCount = async (
  apiFunc,
  projectId,
  selectedTableRows,
  arrangerConfig,
) => {
  const MSG_GET_MANIFEST_COUNT_FAIL = 'Error getting manifest file count';
  checkArrangerGraphqlField(arrangerConfig);
  if (!hasKeyChain(arrangerConfig, 'manifestMapping.resourceIndexType')
    || !hasKeyChain(arrangerConfig, 'manifestMapping.referenceIdFieldInResourceIndex')) {
    throw MSG_GET_MANIFEST_COUNT_FAIL;
  }

  const manifestEntryCount = await queryCountByValues(
    apiFunc,
    projectId,
    arrangerConfig.manifestMapping.resourceIndexType,
    [{
      name: arrangerConfig.manifestMapping.referenceIdFieldInResourceIndex,
      values: selectedTableRows,
    }],
  );
  return manifestEntryCount;
};
<|endoftext|>