<commit_before>/* Copyright (C) 2017 Canonical Ltd. */
'use strict';

const PropTypes = require('prop-types');
const React = require('react');

const BasicTable = require('../../basic-table/basic-table');
const CreateModelButton = require('../../create-model-button/create-model-button');
const DateDisplay = require('../../date-display/date-display');
const Popup = require('../../popup/popup');
const Spinner = require('../../spinner/spinner');
const SvgIcon = require('../../svg-icon/svg-icon');

/**
  Model list React component used to display a list of the users models in
  their profile.
*/
class ProfileModelList extends React.Component {
  constructor() {
    super();
    this.state = {
      loadingModels: false,
      models: null,
      notification: null
    };
  }

  componentWillMount() {
    this._fetchModels(this.props.facadesExist);
  }

  componentWillReceiveProps(nextProps) {
    const props = this.props;
    if (props.userInfo.profile !== nextProps.userInfo.profile ||
        props.facadesExist !== nextProps.facadesExist) {
      this._fetchModels(nextProps.facadesExist);
    }
  }

  /**
    Makes a request of the controller to fetch the user's availble models.
    @param {Boolean} facadesExist Whether the controller is connected or not.
  */
  _fetchModels(facadesExist) {
    if (!facadesExist) {
      console.warn('Controller not connected, skipping fetching models.');
      return;
    }
    // Delay the call until after the state change to prevent race
    // conditions.
    this.setState({loadingModels: true}, () => {
      this.props.listModelsWithInfo(this._fetchModelsCallback.bind(this));
    });
  }

  /**
    Callback for the controller listModelsWithInfo call.
    @param {String} err The error from the request, or null.
    @param {Array} modelList The list of models.
  */
  _fetchModelsCallback(err, modelList) {
    this.setState({loadingModels: false}, () => {
      if (err) {
        const message = 'Cannot load models';
        console.error(message, err);
        this.props.addNotification({
          title: message,
          message: `${message}: ${err}`,
          level: 'error'
        });
        return;
      }
      this.setState({models: modelList});
    });
  }

  /**
    Shows the confirmation modal for destroying a model.
    @param {Object} model The model data.
  */
  _showConfirmation(model) {
    const buttons = [{
      title: 'Cancel',
      action: () => this.setState({notification: null}),
      type: 'inline-neutral'
    }, {
      title: 'Destroy',
      action: () => {
        this.setState({notification: null});
        this.props.destroyModels([model.uuid], () => {
          this._fetchModels(this.props.facadesExist);
        });
      },
      type: 'destructive'
    }];
    const message = `Are you sure you want to destroy ${model.name}?`
      + ' All the applications and units included in the model will be'
      + ' destroyed. This action cannot be undone.';
    this.setState({
      notification: (
        <Popup
          buttons={buttons}
          title="Destroy model">

{message}

        </Popup>)
    });
  }

  /**
    Calls to destroy the supplied model.
    @param {Object} model The model object.
    @param {String} bdRef The ref index of the button dropdown component.
    @param {Object} e The click event.
  */
  _destroyModel(model, bdRef, e) {
    e.preventDefault();
    e.stopPropagation();
    if (model.isController) {
      this.props.addNotification({
        title: 'Cannot destroy model',
        message: 'The controller model cannot be destroyed.',
        level: 'error'
      });
      return;
    }
    this._showConfirmation(model);
  }

  /**
    Navigate to a credential.
    @param credential {String} The credential name.
  */
  _handleCredentialClick(credential) {
    this.props.changeState({
      hash: `credentials/${credential}`
    });
  }

  /**
    Generates the list of models that are owned by the active user.
    @return {Object} The model list as JSX.
  */
  _generateModels() {
    const icons = new Map([
      ['read', 'show_16'],
      ['write', 'edit_16'],
      ['admin', 'user_16']
    ]);
    const profileUsername = this.props.userInfo.profile;
    const models = this.state.models || [];
    return models.reduce((modelList, model, index) => {
      // Keep only the models that aren't currently in the destroy cycle.
      if (!model.isAlive) {
        return modelList;
      }
      const bdRef = `mymodel-button-dropdown-${index}`;
      const owner = model.owner.replace('@external', '') || profileUsername;
      const path = `${this.props.baseURL}u/${owner}/${model.name}`;
      const profileUser = model.users.find(user => user.displayName === profileUsername);
      const userIsAdmin = profileUser.access === 'admin';
      const username = owner === profileUsername ? 'Me' : owner;
      const region = model.region ? '/' + model.region : '';
      const nameContent = (

          {model.name}
        );
      const regionContent = (

            {model.numMachines}

          {model.provider}{region}

);
      const accessContent = (
        <SvgIcon
          name={icons.get(profileUser.access)}
          size="16" />);
      const dateContent = (
        <DateDisplay
          date={model.lastConnection || '--'}
          relative={true} />);
      const destroyContent = userIsAdmin ? (

          <SvgIcon
            name="delete_16"
            size="16" />
        ) : null;
      modelList.push({
        columns: [{
          content: nameContent,
          columnSize: 3
        }, {
          content: username,
          columnSize: 2
        }, {
          content: regionContent,
          columnSize: 3
        }, {
          content: accessContent,
          columnSize: 1
        }, {
          content: dateContent,
          columnSize: 2
        }, {
          content: destroyContent,
          columnSize: 1,
          classes: ['u-text-align--right']
        }],
        expandedContent: (

              {nameContent}

              {username}

              {regionContent}

              {accessContent}

              {dateContent}

              {destroyContent}

                {model.credentialName}

),
        key: model.name
      });
      return modelList;
    }, []) || [];
  }

  /**
    Call the switchModel prop to switch a model passing it the necessary
    model data. Prevents default and propagation.
    @param {Object} model The model data necessary for the switchModel call.
    @param {Object} e The click event.
  */
  switchToModel(model, e) {
    e.preventDefault();
    e.stopPropagation(); // Required to avoid react error about root DOM node.
    this.props.switchModel(model);
  }

  _generateNotification() {
    if (!this.state.notification) {
      return;
    }
    return this.state.notification;
  }

  render() {
    if (this.state.loadingModels) {
      return (

          <Spinner />

);
    }
    const rowData = this._generateModels();
    return (

          <CreateModelButton
            title="Start a new model"
            changeState={this.props.changeState}
            switchModel={this.props.switchModel} />

            My models

              ({rowData.length})

        {!rowData.length ? null : <BasicTable
          headerClasses={['profile__entity-table-header-row']}
          headerColumnClasses={['profile__entity-table-header-column']}
          headers={[{
            content: 'Name',
            columnSize: 3
          }, {
            content: 'Owner',
            columnSize: 2
          }, {
            content: 'Machines, cloud/region',
            columnSize: 3
          }, {
            content: '',
            columnSize: 1
          }, {
            content: 'Last accessed',
            columnSize: 2
          }, {
            content: '',
            columnSize: 1
          }]}
          rowClasses={['profile__entity-table-row']}
          rowColumnClasses={['profile__entity-table-column']}
          rows={rowData} />}
        {this._generateNotification()}

);
  }

};

ProfileModelList.propTypes = {
  acl: PropTypes.object,
  addNotification: PropTypes.func.isRequired,
  baseURL: PropTypes.string.isRequired,
  changeState: PropTypes.func.isRequired,
  destroyModels: PropTypes.func.isRequired,
  facadesExist: PropTypes.bool.isRequired,
  listModelsWithInfo: PropTypes.func.isRequired,
  models: PropTypes.array,
  switchModel: PropTypes.func.isRequired,
  // userInfo must have the following attributes:
  // - external: the external user name to use for retrieving data, for
  //   instance, from the charm store. Might be null if the user is being
  //   displayed for the current user and they are not authenticated to
  //   the charm store;
  // - isCurrent: whether the profile is being displayed for the currently
  //   authenticated user;
  // - profile: the user name for whom profile details must be displayed.
  userInfo: PropTypes.object.isRequired
};

module.exports = ProfileModelList;
<commit_msg>Don't show the creds for shared models.<commit_after>/* Copyright (C) 2017 Canonical Ltd. */
'use strict';

const PropTypes = require('prop-types');
const React = require('react');

const BasicTable = require('../../basic-table/basic-table');
const CreateModelButton = require('../../create-model-button/create-model-button');
const DateDisplay = require('../../date-display/date-display');
const Popup = require('../../popup/popup');
const Spinner = require('../../spinner/spinner');
const SvgIcon = require('../../svg-icon/svg-icon');

/**
  Model list React component used to display a list of the users models in
  their profile.
*/
class ProfileModelList extends React.Component {
  constructor() {
    super();
    this.state = {
      loadingModels: false,
      models: null,
      notification: null
    };
  }

  componentWillMount() {
    this._fetchModels(this.props.facadesExist);
  }

  componentWillReceiveProps(nextProps) {
    const props = this.props;
    if (props.userInfo.profile !== nextProps.userInfo.profile ||
        props.facadesExist !== nextProps.facadesExist) {
      this._fetchModels(nextProps.facadesExist);
    }
  }

  /**
    Makes a request of the controller to fetch the user's availble models.
    @param {Boolean} facadesExist Whether the controller is connected or not.
  */
  _fetchModels(facadesExist) {
    if (!facadesExist) {
      console.warn('Controller not connected, skipping fetching models.');
      return;
    }
    // Delay the call until after the state change to prevent race
    // conditions.
    this.setState({loadingModels: true}, () => {
      this.props.listModelsWithInfo(this._fetchModelsCallback.bind(this));
    });
  }

  /**
    Callback for the controller listModelsWithInfo call.
    @param {String} err The error from the request, or null.
    @param {Array} modelList The list of models.
  */
  _fetchModelsCallback(err, modelList) {
    this.setState({loadingModels: false}, () => {
      if (err) {
        const message = 'Cannot load models';
        console.error(message, err);
        this.props.addNotification({
          title: message,
          message: `${message}: ${err}`,
          level: 'error'
        });
        return;
      }
      this.setState({models: modelList});
    });
  }

  /**
    Shows the confirmation modal for destroying a model.
    @param {Object} model The model data.
  */
  _showConfirmation(model) {
    const buttons = [{
      title: 'Cancel',
      action: () => this.setState({notification: null}),
      type: 'inline-neutral'
    }, {
      title: 'Destroy',
      action: () => {
        this.setState({notification: null});
        this.props.destroyModels([model.uuid], () => {
          this._fetchModels(this.props.facadesExist);
        });
      },
      type: 'destructive'
    }];
    const message = `Are you sure you want to destroy ${model.name}?`
      + ' All the applications and units included in the model will be'
      + ' destroyed. This action cannot be undone.';
    this.setState({
      notification: (
        <Popup
          buttons={buttons}
          title="Destroy model">

{message}

        </Popup>)
    });
  }

  /**
    Calls to destroy the supplied model.
    @param {Object} model The model object.
    @param {String} bdRef The ref index of the button dropdown component.
    @param {Object} e The click event.
  */
  _destroyModel(model, bdRef, e) {
    e.preventDefault();
    e.stopPropagation();
    if (model.isController) {
      this.props.addNotification({
        title: 'Cannot destroy model',
        message: 'The controller model cannot be destroyed.',
        level: 'error'
      });
      return;
    }
    this._showConfirmation(model);
  }

  /**
    Navigate to a credential.
    @param credential {String} The credential name.
  */
  _handleCredentialClick(credential) {
    this.props.changeState({
      hash: `credentials/${credential}`
    });
  }

  /**
    Generates the list of models that are owned by the active user.
    @return {Object} The model list as JSX.
  */
  _generateModels() {
    const icons = new Map([
      ['read', 'show_16'],
      ['write', 'edit_16'],
      ['admin', 'user_16']
    ]);
    const profileUsername = this.props.userInfo.profile;
    const models = this.state.models || [];
    return models.reduce((modelList, model, index) => {
      // Keep only the models that aren't currently in the destroy cycle.
      if (!model.isAlive) {
        return modelList;
      }
      const bdRef = `mymodel-button-dropdown-${index}`;
      const owner = model.owner.replace('@external', '') || profileUsername;
      const path = `${this.props.baseURL}u/${owner}/${model.name}`;
      const profileUser = model.users.find(user => user.displayName === profileUsername);
      const userIsAdmin = profileUser.access === 'admin';
      const username = owner === profileUsername ? 'Me' : owner;
      const region = model.region ? '/' + model.region : '';
      const nameContent = (

          {model.name}
        );
      const regionContent = (

            {model.numMachines}

          {model.provider}{region}

);
      const accessContent = (
        <SvgIcon
          name={icons.get(profileUser.access)}
          size="16" />);
      const dateContent = (
        <DateDisplay
          date={model.lastConnection || '--'}
          relative={true} />);
      const destroyContent = userIsAdmin ? (

          <SvgIcon
            name="delete_16"
            size="16" />
        ) : null;
      // Only show the expanded content if the user is the model owner as we
      // don't want to show other users' credential info.
      const expandedContent = owner === profileUsername ? (

            {nameContent}

            {username}

            {regionContent}

            {accessContent}

            {dateContent}

            {destroyContent}

              {model.credentialName}

) : null;
      modelList.push({
        columns: [{
          content: nameContent,
          columnSize: 3
        }, {
          content: username,
          columnSize: 2
        }, {
          content: regionContent,
          columnSize: 3
        }, {
          content: accessContent,
          columnSize: 1
        }, {
          content: dateContent,
          columnSize: 2
        }, {
          content: destroyContent,
          columnSize: 1,
          classes: ['u-text-align--right']
        }],
        expandedContent: expandedContent,
        key: model.name
      });
      return modelList;
    }, []) || [];
  }

  /**
    Call the switchModel prop to switch a model passing it the necessary
    model data. Prevents default and propagation.
    @param {Object} model The model data necessary for the switchModel call.
    @param {Object} e The click event.
  */
  switchToModel(model, e) {
    e.preventDefault();
    e.stopPropagation(); // Required to avoid react error about root DOM node.
    this.props.switchModel(model);
  }

  _generateNotification() {
    if (!this.state.notification) {
      return;
    }
    return this.state.notification;
  }

  render() {
    if (this.state.loadingModels) {
      return (

          <Spinner />

);
    }
    const rowData = this._generateModels();
    return (

          <CreateModelButton
            title="Start a new model"
            changeState={this.props.changeState}
            switchModel={this.props.switchModel} />

            My models

              ({rowData.length})

        {!rowData.length ? null : <BasicTable
          headerClasses={['profile__entity-table-header-row']}
          headerColumnClasses={['profile__entity-table-header-column']}
          headers={[{
            content: 'Name',
            columnSize: 3
          }, {
            content: 'Owner',
            columnSize: 2
          }, {
            content: 'Machines, cloud/region',
            columnSize: 3
          }, {
            content: '',
            columnSize: 1
          }, {
            content: 'Last accessed',
            columnSize: 2
          }, {
            content: '',
            columnSize: 1
          }]}
          rowClasses={['profile__entity-table-row']}
          rowColumnClasses={['profile__entity-table-column']}
          rows={rowData} />}
        {this._generateNotification()}

);
  }

};

ProfileModelList.propTypes = {
  acl: PropTypes.object,
  addNotification: PropTypes.func.isRequired,
  baseURL: PropTypes.string.isRequired,
  changeState: PropTypes.func.isRequired,
  destroyModels: PropTypes.func.isRequired,
  facadesExist: PropTypes.bool.isRequired,
  listModelsWithInfo: PropTypes.func.isRequired,
  models: PropTypes.array,
  switchModel: PropTypes.func.isRequired,
  // userInfo must have the following attributes:
  // - external: the external user name to use for retrieving data, for
  //   instance, from the charm store. Might be null if the user is being
  //   displayed for the current user and they are not authenticated to
  //   the charm store;
  // - isCurrent: whether the profile is being displayed for the currently
  //   authenticated user;
  // - profile: the user name for whom profile details must be displayed.
  userInfo: PropTypes.object.isRequired
};

module.exports = ProfileModelList;
<|endoftext|>


<commit_before>var _ = require('underscore');
var Backbone = require('backbone');
var camshaftReference = require('./camshaft-reference');
var QueryGeometryModel = require('./query-geometry-model');
var QuerySchemaModel = require('./query-schema-model');
var nodeIds = require('../value-objects/analysis-node-ids');

/**
 * Base model for an analysis definition node.
 * May point to one or multiple nodes in turn (referenced by ids).
 */
module.exports = Backbone.Model.extend({

  initialize: function (attrs, opts) {
    if (!this.id) throw new Error('id is required');

    var modelOpts = {configModel: opts.configModel};
    this.querySchemaModel = new QuerySchemaModel(undefined, modelOpts);
    this.queryGeometryModel = new QueryGeometryModel({simple_geom: this.get('simple_geom')}, modelOpts);

    this.listenTo(this.queryGeometryModel, 'change:val', this._onGeometryValChanged);
  },

  /**
   * @override {Backbone.prototype.parse}  flatten the provided analysis data and create source nodes if there are any.
   */
  parse: function (r, opts) {
    var sourceNames = camshaftReference.getSourceNamesForAnalysisType(r.type);

    var parsedParams = _.reduce(r.params, function (memo, val, name) {
      var sourceName = sourceNames[sourceNames.indexOf(name)];

      if (sourceName) {
        this.collection.add(val, opts);
        memo[name] = val.id;
      } else {
        memo[name] = val;
      }

      return memo;
    }, {}, this);

    var parsedOptions = _.reduce(r.options, function (memo, val, name) {
      memo[name] = val;
      return memo;
    }, {}, this);

    return _.defaults(
      _.omit(r, 'params', 'options'),
      parsedOptions,
      parsedParams
    );
  },

  /**
   * @override {Backbone.prototype.toJSON} unflatten the internal structure to the expected nested JSON data structure.
   * @param {Object} options
   * @param {Boolean} options.skipOptions - Add or not analysis options to the definition
   */
  toJSON: function (options) {
    options = options || {};

    var analysisParams = [];
    var paramsforJSON = {};
    var paramsForType = camshaftReference.paramsForType(this.get('type'));

    // Undo the parsing of the params previously done in .parse() (when model was created)
    for (var name in paramsForType) {
      var param = paramsForType[name];
      var val = this.get(name);

      if (param.type === 'node') {
        var node = this.collection.get(val);
        if (!node) {
          throw new Error('no node found for param "' + name + '" with id "' + val + '" in node ' + JSON.stringify(this.attributes));
        }
        val = node.toJSON(options);
      }

      paramsforJSON[name] = val;

      analysisParams.push(name);
    }

    var json = {
      id: this.get('id'),
      type: this.get('type'),
      params: paramsforJSON
    };

    var optionsAttrs = _.omit(this.attributes, analysisParams.concat('id', 'type'));
    if (!(options.skipOptions || _.isEmpty(optionsAttrs))) {
      json['options'] = optionsAttrs;
    }

    return json;
  },

  /**
   * @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
   */
  isNew: function () {
    return true;
  },

  canBeDeletedByUser: function () {
    return this.hasPrimarySource();
  },

  destroy: function () {
    if (this.querySchemaModel) {
      this.querySchemaModel.destroy();
      this.querySchemaModel = null;
    }

    return Backbone.Model.prototype.destroy.apply(this, arguments);
  },

  /**
   * Creates a new node that have same params as the current node, with only the id differing.
   * @param {String} newId - e.g. 'b1'
   * @return {Object} instance of analysis-definition-node-model
   */
  clone: function (newId) {
    if (!newId) throw new Error('newId is required');
    if (!_.isString(newId) || newId.length < 2) throw new Error('newId is required as a non-empty string');
    if (newId === this.id) throw new Error('newId must be different from current id, ' + this.id);

    var attrs = this.toJSON();
    attrs.id = newId;
    return this.collection.add(attrs);
  },

  linkedListBySameLetter: function () {
    var list = [this];
    var prev = this;
    var letter = this.letter();

    while (true) {
      var current = prev.getPrimarySource();
      if (current && current.letter() === letter) {
        list.push(current);
        prev = current;
      } else {
        break;
      }
    }

    return list;
  },

  /**
   * @return {Boolean, null} null if the geoemtry type of this node is not known,
   *   this is to indicate that the query model is not ready yet, up to caller to make the call or not
   */
  isValidAsInputForType: function (analysisType) {
    if (analysisType === 'source') return false;

    var geometry = this.queryGeometryModel.get('simple_geom');
    return geometry
      ? camshaftReference.isValidInputGeometryForType(geometry, analysisType)
      : null;
  },

  containsNode: function (other) {
    if (!other) return false;
    return this.id === other.id || this._sourcesContains(other);
  },

  _sourcesContains: function (other) {
    var primarySource = this.getPrimarySource();
    var secondarySource = this.getSecondarySource();
    return !!(
      (primarySource && primarySource.containsNode(other)) ||
      (secondarySource && secondarySource.containsNode(other))
    );
  },

  hasPrimarySource: function () {
    return !!this.getPrimarySource();
  },

  hasSecondarySource: function () {
    return !!this.getSecondarySource();
  },

  getPrimarySource: function () {
    return this.collection.get(this._getPrimarySourceId());
  },

  getSecondarySource: function () {
    // Assumption: there are only 1-2 sources, so secondary is the one that's not the primary
    var primarySourceId = this._getPrimarySourceId();
    var secondarySourceId = _.find(this.sourceIds(), function (sourceId) {
      return sourceId !== primarySourceId;
    });
    return this.collection.get(secondarySourceId);
  },

  /**
   * @return {Array} e.g. ['c3', 'b2']
   */
  sourceIds: function () {
    return _.map(this._sourceNames(), function (sourceName) {
      return this.get(sourceName);
    }, this);
  },

  changeSourceIds: function (currentId, newId) {
    this._sourceNames().forEach(function (sourceName) {
      if (this.get(sourceName) === currentId) {
        this.set(sourceName, newId);
      }
    }, this);
  },

  letter: function () {
    return nodeIds.letter(this.id);
  },

  _getPrimarySourceId: function () {
    var primarySourceName = this.get('primary_source_name') || this._sourceNames()[0];
    return this.get(primarySourceName);
  },

  /**
   * @return {Array} e.g. ['polygons_source', 'points_source']
   */
  _sourceNames: function () {
    return camshaftReference.getSourceNamesForAnalysisType(this.get('type'));
  },

  _onGeometryValChanged: function (m, val) {
    this.set('simple_geom', val, {silent: true});
  }

});
<commit_msg>Set geometry model from the beginning + "storing" simple_geom in the proper node model<commit_after>var _ = require('underscore');
var Backbone = require('backbone');
var camshaftReference = require('./camshaft-reference');
var QueryGeometryModel = require('./query-geometry-model');
var QuerySchemaModel = require('./query-schema-model');
var nodeIds = require('../value-objects/analysis-node-ids');

/**
 * Base model for an analysis definition node.
 * May point to one or multiple nodes in turn (referenced by ids).
 */
module.exports = Backbone.Model.extend({

  initialize: function (attrs, opts) {
    if (!this.id) throw new Error('id is required');

    var modelOpts = {configModel: opts.configModel};
    var simpleGeom = this.get('simple_geom');
    this.querySchemaModel = new QuerySchemaModel(undefined, modelOpts);
    this.queryGeometryModel = new QueryGeometryModel({
      simple_geom: simpleGeom,
      status: simpleGeom ? 'fetched' : 'unfetched'
    }, modelOpts);

    this.listenTo(this.queryGeometryModel, 'change:simple_geom', this._onGeometryTypeChanged);
  },

  /**
   * @override {Backbone.prototype.parse}  flatten the provided analysis data and create source nodes if there are any.
   */
  parse: function (r, opts) {
    var sourceNames = camshaftReference.getSourceNamesForAnalysisType(r.type);

    var parsedParams = _.reduce(r.params, function (memo, val, name) {
      var sourceName = sourceNames[sourceNames.indexOf(name)];

      if (sourceName) {
        this.collection.add(val, opts);
        memo[name] = val.id;
      } else {
        memo[name] = val;
      }

      return memo;
    }, {}, this);

    var parsedOptions = _.reduce(r.options, function (memo, val, name) {
      memo[name] = val;
      return memo;
    }, {}, this);

    return _.defaults(
      _.omit(r, 'params', 'options'),
      parsedOptions,
      parsedParams
    );
  },

  /**
   * @override {Backbone.prototype.toJSON} unflatten the internal structure to the expected nested JSON data structure.
   * @param {Object} options
   * @param {Boolean} options.skipOptions - Add or not analysis options to the definition
   */
  toJSON: function (options) {
    options = options || {};

    var analysisParams = [];
    var paramsforJSON = {};
    var paramsForType = camshaftReference.paramsForType(this.get('type'));

    // Undo the parsing of the params previously done in .parse() (when model was created)
    for (var name in paramsForType) {
      var param = paramsForType[name];
      var val = this.get(name);

      if (param.type === 'node') {
        var node = this.collection.get(val);
        if (!node) {
          throw new Error('no node found for param "' + name + '" with id "' + val + '" in node ' + JSON.stringify(this.attributes));
        }
        val = node.toJSON(options);
      }

      paramsforJSON[name] = val;

      analysisParams.push(name);
    }

    var json = {
      id: this.get('id'),
      type: this.get('type'),
      params: paramsforJSON
    };

    var optionsAttrs = _.omit(this.attributes, analysisParams.concat('id', 'type'));
    if (!(options.skipOptions || _.isEmpty(optionsAttrs))) {
      json['options'] = optionsAttrs;
    }

    return json;
  },

  /**
   * @override {Backbone.prototype.isNew} for this.destroy() to work (not try to send DELETE request)
   */
  isNew: function () {
    return true;
  },

  canBeDeletedByUser: function () {
    return this.hasPrimarySource();
  },

  destroy: function () {
    if (this.querySchemaModel) {
      this.querySchemaModel.destroy();
      this.querySchemaModel = null;
    }

    return Backbone.Model.prototype.destroy.apply(this, arguments);
  },

  /**
   * Creates a new node that have same params as the current node, with only the id differing.
   * @param {String} newId - e.g. 'b1'
   * @return {Object} instance of analysis-definition-node-model
   */
  clone: function (newId) {
    if (!newId) throw new Error('newId is required');
    if (!_.isString(newId) || newId.length < 2) throw new Error('newId is required as a non-empty string');
    if (newId === this.id) throw new Error('newId must be different from current id, ' + this.id);

    var attrs = this.toJSON();
    attrs.id = newId;
    return this.collection.add(attrs);
  },

  linkedListBySameLetter: function () {
    var list = [this];
    var prev = this;
    var letter = this.letter();

    while (true) {
      var current = prev.getPrimarySource();
      if (current && current.letter() === letter) {
        list.push(current);
        prev = current;
      } else {
        break;
      }
    }

    return list;
  },

  /**
   * @return {Boolean, null} null if the geoemtry type of this node is not known,
   *   this is to indicate that the query model is not ready yet, up to caller to make the call or not
   */
  isValidAsInputForType: function (analysisType) {
    if (analysisType === 'source') return false;

    var geometry = this.queryGeometryModel.get('simple_geom');
    return geometry
      ? camshaftReference.isValidInputGeometryForType(geometry, analysisType)
      : null;
  },

  containsNode: function (other) {
    if (!other) return false;
    return this.id === other.id || this._sourcesContains(other);
  },

  _sourcesContains: function (other) {
    var primarySource = this.getPrimarySource();
    var secondarySource = this.getSecondarySource();
    return !!(
      (primarySource && primarySource.containsNode(other)) ||
      (secondarySource && secondarySource.containsNode(other))
    );
  },

  hasPrimarySource: function () {
    return !!this.getPrimarySource();
  },

  hasSecondarySource: function () {
    return !!this.getSecondarySource();
  },

  getPrimarySource: function () {
    return this.collection.get(this._getPrimarySourceId());
  },

  getSecondarySource: function () {
    // Assumption: there are only 1-2 sources, so secondary is the one that's not the primary
    var primarySourceId = this._getPrimarySourceId();
    var secondarySourceId = _.find(this.sourceIds(), function (sourceId) {
      return sourceId !== primarySourceId;
    });
    return this.collection.get(secondarySourceId);
  },

  /**
   * @return {Array} e.g. ['c3', 'b2']
   */
  sourceIds: function () {
    return _.map(this._sourceNames(), function (sourceName) {
      return this.get(sourceName);
    }, this);
  },

  changeSourceIds: function (currentId, newId) {
    this._sourceNames().forEach(function (sourceName) {
      if (this.get(sourceName) === currentId) {
        this.set(sourceName, newId);
      }
    }, this);
  },

  letter: function () {
    return nodeIds.letter(this.id);
  },

  _getPrimarySourceId: function () {
    var primarySourceName = this.get('primary_source_name') || this._sourceNames()[0];
    return this.get(primarySourceName);
  },

  /**
   * @return {Array} e.g. ['polygons_source', 'points_source']
   */
  _sourceNames: function () {
    return camshaftReference.getSourceNamesForAnalysisType(this.get('type'));
  },

  _onGeometryTypeChanged: function (m, value) {
    this.set('simple_geom', value);
  }

});
<|endoftext|>


<commit_before>'use strict';

// MODULES //

var isnan = require( '@stdlib/math/base/assert/is-nan' );
var isInteger = require( '@stdlib/math/base/assert/is-integer' );
var ln = require( '@stdlib/math/base/special/ln' );
var round = require( '@stdlib/math/base/special/round' );
var PHI = require( '@stdlib/math/constants/float64-phi' );
var PINF = require( '@stdlib/math/constants/float64-pinf' );

// VARIABLES //

var SQRT_5 = 2.23606797749979;
var LN_PHI = ln( PHI );

// MAIN //

/**
* Computes the Fibonacci number index.
*
* ## Notes
*
* * We use `round` instead of `floor` due to errors introduced by floating-point precision.
*
*
* @param {NonNegativeInteger} F - Fibonacci number
* @returns {number} Fibonacci number index
*
* @example
* var n = fibonacciIndex( 0 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 1 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 2 );
* // returns 3
*
* @example
* var n = fibonacciIndex( 3 );
* // returns 4
*
* @example
* var n = fibonacciIndex( 5 );
* // returns 5
*
* @example
* var n = fibonacciIndex( 8 );
* // returns 6
*
* @example
* var n = fibonacciIndex( NaN );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 3.14 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( -1 );
* // returns NaN
*/
function fibonacciIndex( F ) {
	var x;
	if (
		isnan( F ) ||
		isInteger( F ) === false ||
		F <= 1 ||
		F === PINF
	) {
		return NaN;
	}
	x = ( F*SQRT_5 ) + 0.5;
	return round( ln( x ) / LN_PHI );
} // end FUNCTION fibonacciIndex()

// EXPORTS //

module.exports = fibonacciIndex;
<commit_msg>Update return type<commit_after>'use strict';

// MODULES //

var isnan = require( '@stdlib/math/base/assert/is-nan' );
var isInteger = require( '@stdlib/math/base/assert/is-integer' );
var ln = require( '@stdlib/math/base/special/ln' );
var round = require( '@stdlib/math/base/special/round' );
var PHI = require( '@stdlib/math/constants/float64-phi' );
var PINF = require( '@stdlib/math/constants/float64-pinf' );

// VARIABLES //

var SQRT_5 = 2.23606797749979;
var LN_PHI = ln( PHI );

// MAIN //

/**
* Computes the Fibonacci number index.
*
* ## Notes
*
* * We use `round` instead of `floor` due to errors introduced by floating-point precision.
*
*
* @param {NonNegativeInteger} F - Fibonacci number
* @returns {NonNegativeInteger} Fibonacci number index
*
* @example
* var n = fibonacciIndex( 0 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 1 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 2 );
* // returns 3
*
* @example
* var n = fibonacciIndex( 3 );
* // returns 4
*
* @example
* var n = fibonacciIndex( 5 );
* // returns 5
*
* @example
* var n = fibonacciIndex( 8 );
* // returns 6
*
* @example
* var n = fibonacciIndex( NaN );
* // returns NaN
*
* @example
* var n = fibonacciIndex( 3.14 );
* // returns NaN
*
* @example
* var n = fibonacciIndex( -1 );
* // returns NaN
*/
function fibonacciIndex( F ) {
	var x;
	if (
		isnan( F ) ||
		isInteger( F ) === false ||
		F <= 1 ||
		F === PINF
	) {
		return NaN;
	}
	x = ( F*SQRT_5 ) + 0.5;
	return round( ln( x ) / LN_PHI );
} // end FUNCTION fibonacciIndex()

// EXPORTS //

module.exports = fibonacciIndex;
<|endoftext|>


<commit_before>'use strict';

// MODULES //

var tape = require( 'tape' );
var Float64Array = require( '@stdlib/types/array/float64' ); // eslint-disable-line no-redeclare
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
var hasProp = require( '@stdlib/assert/has-property' );
var isFunction = require( '@stdlib/assert/is-function' );
var ctor = require( './../lib' );

// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.strictEqual( typeof ctor, 'function', 'main export is a function' );
	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ 2, 1 ];
	offset = 0;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 4.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ 2, -1 ];
	offset = 1;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 3.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ -2, 1 ];
	offset = 2;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 2.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ -2, -1 ];
	offset = 3;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 1.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ 1, 2 ];
	offset = 0;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 4.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ -1, 2 ];
	offset = 1;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 3.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ 1, -2 ];
	offset = 2;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 2.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ -1, -2 ];
	offset = 3;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 1.0, 'returns expected value' );

	t.end();
});
<commit_msg>Add input argument validation tests<commit_after>'use strict';

// MODULES //

var tape = require( 'tape' );
var Float64Array = require( '@stdlib/types/array/float64' ); // eslint-disable-line no-redeclare
var hasOwnProp = require( '@stdlib/assert/has-own-property' );
var hasProp = require( '@stdlib/assert/has-property' );
var isFunction = require( '@stdlib/assert/is-function' );
var ctor = require( './../lib' );

// TESTS //

tape( 'main export is a function', function test( t ) {
	t.ok( true, __filename );
	t.strictEqual( typeof ctor, 'function', 'main export is a function' );
	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method which throws an error if not provided an integer value', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var values;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;
	var i;

	values = [
		'5',
		3.14,
		NaN,
		true,
		false,
		null,
		void 0,
		[],
		{},
		function noop() {}
	];

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ 2, 1 ];
	offset = 0;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	for ( i = 0; i < values.length; i++ ) {
		t.throws( badValue( values[ i ] ), TypeError, 'throws an error when provided ' + values[ i ] );
	}
	t.end();

	function badValue( value ) {
		return function badValue() {
			arr.iget( value );
		};
	}
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ 2, 1 ];
	offset = 0;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 4.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ 2, -1 ];
	offset = 1;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 3.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ -2, 1 ];
	offset = 2;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 2.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (row-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'row-major';
	strides = [ -2, -1 ];
	offset = 3;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 1.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ 1, 2 ];
	offset = 0;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 4.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ -1, 2 ];
	offset = 1;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 3.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ 1, -2 ];
	offset = 2;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 1.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 2.0, 'returns expected value' );

	t.end();
});

tape( 'an ndarray constructor returns an instance which has an `iget` method for retrieving an array element using a linear index (column-major)', function test( t ) {
	var strides;
	var buffer;
	var offset;
	var dtype;
	var order;
	var shape;
	var arr;
	var f;

	dtype = 'float64';
	buffer = new Float64Array( [ 1.0, 2.0, 3.0, 4.0 ] );
	shape = [ 2, 2 ];
	order = 'column-major';
	strides = [ -1, -2 ];
	offset = 3;

	f = ctor( dtype, shape.length );
	arr = f( buffer, shape, strides, offset, order );

	t.strictEqual( hasOwnProp( arr, 'iget' ), false, 'does not have own property' );
	t.strictEqual( hasProp( arr, 'iget' ), true, 'has property' );
	t.strictEqual( isFunction( arr.iget ), true, 'has method' );

	t.strictEqual( arr.iget( 0 ), 4.0, 'returns expected value' );
	t.strictEqual( arr.iget( 1 ), 3.0, 'returns expected value' );
	t.strictEqual( arr.iget( 2 ), 2.0, 'returns expected value' );
	t.strictEqual( arr.iget( 3 ), 1.0, 'returns expected value' );

	t.end();
});
<|endoftext|>


<commit_before>import {
  withKnobs,
  boolean
} from '@storybook/addon-knobs';

import AposAreaMenu from './AposAreaMenu.vue';
const data = getData();

export default {
  title: 'Area Menu',
  decorators: [ withKnobs ]
};

export const areaMenu = () => {
  const menuFormat = boolean('Menu Items Grouped?', true);
  return {
    data() {
      return {
        index: 0,
        widgetOptions: {
          '@apostrophecms/rich-text': {}
        }
      };
    },
    methods: {
      handler(action) {
        console.log(`heard ${action}`);
      }
    },
    computed: {
      contextMenuOptions() {
        if (menuFormat) {
          return { menu: data.menu };
        } else {
          return { menu: data.menu[1].items };
        }
      }
    },
    components: {
      AposAreaMenu
    },
    template: `
      <AposAreaMenu
        v-on:click="handler"
        :contextMenuOptions="contextMenuOptions"
        :widgetOptions="widgetOptions"
        style="margin-left: 100px;"
      />
    `
  };
};

function getData() {
  return {
    menu: [
      {
        label: 'Layouts',
        items: [
          {
            label: 'Two Column',
            action: 'add-two-column',
            icon: 'view-column-icon'
          },
          {
            label: 'Three Column',
            action: 'add-three-column',
            icon: 'view-column-icon'
          },
          {
            label: 'Two Thirds',
            action: 'add-two-thirds',
            icon: 'view-column-icon'
          },
          {
            label: 'Quarter',
            action: 'add-quarters',
            icon: 'view-column-icon'
          }
        ]
      },
      {
        label: 'Content Widgets',
        items: [
          {
            label: 'Marquee',
            action: 'add-marquee',
            icon: 'image-size-select-actual-icon'
          },
          {
            label: 'Image',
            action: 'add-image',
            icon: 'image-icon'
          },
          {
            label: 'Video',
            action: 'add-video',
            icon: 'video-icon'
          },
          {
            label: 'Instagram Feed',
            action: 'add-insta-feed',
            icon: 'instagram-icon'
          },
          {
            label: 'Cool API Thing',
            action: 'add-api-thing',
            icon: 'web-icon'
          }
        ]
      },
      {
        label: 'Two Column',
        action: 'add-two-column',
        icon: 'view-column-icon'
      },
      {
        label: 'Three Column',
        action: 'add-three-column',
        icon: 'view-column-icon'
      },
      {
        label: 'Marquee',
        action: 'add-marquee',
        icon: 'image-size-select-actual-icon'
      },
      {
        label: 'Image',
        action: 'add-image',
        icon: 'image-icon'
      },
      {
        label: 'Video',
        action: 'add-video',
        icon: 'video-icon'
      },
      {
        label: 'Instagram Feed',
        action: 'add-insta-feed',
        icon: 'instagram-icon'

      },
      {
        label: 'Cool API Thing',
        action: 'add-api-thing',
        icon: 'web-icon'
      }
    ]
  };
}
<commit_msg>shorthand<commit_after>import {
  withKnobs,
  boolean
} from '@storybook/addon-knobs';

import AposAreaMenu from './AposAreaMenu.vue';
const data = getData();

export default {
  title: 'Area Menu',
  decorators: [ withKnobs ]
};

export const areaMenu = () => {
  const menuFormat = boolean('Menu Items Grouped?', true);
  return {
    data() {
      return {
        index: 0,
        widgetOptions: {
          '@apostrophecms/rich-text': {}
        }
      };
    },
    methods: {
      handler(action) {
        console.log(`heard ${action}`);
      }
    },
    computed: {
      contextMenuOptions() {
        if (menuFormat) {
          return { menu: data.menu };
        } else {
          return { menu: data.menu[1].items };
        }
      }
    },
    components: {
      AposAreaMenu
    },
    template: `
      <AposAreaMenu
        @click="handler"
        :contextMenuOptions="contextMenuOptions"
        :widgetOptions="widgetOptions"
        style="margin-left: 100px;"
      />
    `
  };
};

function getData() {
  return {
    menu: [
      {
        label: 'Layouts',
        items: [
          {
            label: 'Two Column',
            action: 'add-two-column',
            icon: 'view-column-icon'
          },
          {
            label: 'Three Column',
            action: 'add-three-column',
            icon: 'view-column-icon'
          },
          {
            label: 'Two Thirds',
            action: 'add-two-thirds',
            icon: 'view-column-icon'
          },
          {
            label: 'Quarter',
            action: 'add-quarters',
            icon: 'view-column-icon'
          }
        ]
      },
      {
        label: 'Content Widgets',
        items: [
          {
            label: 'Marquee',
            action: 'add-marquee',
            icon: 'image-size-select-actual-icon'
          },
          {
            label: 'Image',
            action: 'add-image',
            icon: 'image-icon'
          },
          {
            label: 'Video',
            action: 'add-video',
            icon: 'video-icon'
          },
          {
            label: 'Instagram Feed',
            action: 'add-insta-feed',
            icon: 'instagram-icon'
          },
          {
            label: 'Cool API Thing',
            action: 'add-api-thing',
            icon: 'web-icon'
          }
        ]
      },
      {
        label: 'Two Column',
        action: 'add-two-column',
        icon: 'view-column-icon'
      },
      {
        label: 'Three Column',
        action: 'add-three-column',
        icon: 'view-column-icon'
      },
      {
        label: 'Marquee',
        action: 'add-marquee',
        icon: 'image-size-select-actual-icon'
      },
      {
        label: 'Image',
        action: 'add-image',
        icon: 'image-icon'
      },
      {
        label: 'Video',
        action: 'add-video',
        icon: 'video-icon'
      },
      {
        label: 'Instagram Feed',
        action: 'add-insta-feed',
        icon: 'instagram-icon'

      },
      {
        label: 'Cool API Thing',
        action: 'add-api-thing',
        icon: 'web-icon'
      }
    ]
  };
}
<|endoftext|>


<commit_before>"use strict";

define(['logManager',
    'clientUtil',
    'commonUtil',
    'nodeAttributeNames',
    'nodeRegistryNames',
    './ComponentBase.js',
    './SimpleModelDecorator.js'
    ], function (logManager,
             util,
             commonUtil,
             nodeAttributeNames,
             nodeRegistryNames,
             ComponentBase,
             SimpleModelDecorator) {

    var ModelEditorModelComponent;

    ModelEditorModelComponent = function (id, proj) {
        $.extend(this, new ComponentBase(id, proj));

        this.logger = logManager.create("ModelEditorModelComponent_" + id);
        this.logger.debug("Created");

        this.zIndex = 10;
        this.connectionRectWidth = 5;

        /*
         * OVERRIDE COMPONENTBASE MEMBERS
         */
        this.addedToParent = function () {
            this._addedToParent();
        };

        this.onDestroy = function () {
            //remove extra connection rectangle from DOM
            this.skinParts.connectionRect.remove();

            this.project.updateTerritory(this.territoryId, []);

            this.logger.debug("onDestroy");
        };

        this.isSelectable = function () {
            return true;
        };

        this.isMultiSelectable = function () {
            return true;
        };
        /*
         * END OVERRIDE COMPONENTBASE MEMBERS
         */

        this.decoratorInstance = null;
        this.DecoratorClass = SimpleModelDecorator;

        this._initializeDecorator();
    };

    ModelEditorModelComponent.prototype._initializeDecorator = function () {
        var node = this.project.getNode(this.getId()),
            decoratorName = "",
            customDecorator = node.getRegistry(nodeRegistryNames.decorator);

        //TODO: delete
        customDecorator = null;
        //TODO: enddelete

        if (_.isString(customDecorator)) {
            //TODO: delete
            customDecorator = "ModelWithPorts";
            //TODO: enddelete
            decoratorName = './js/ModelEditor/HTML/' + customDecorator + '.js';

            require([ decoratorName ],
                function (customDecoratorClass) {
                    this.DecoratorClass = customDecoratorClass;
                    this._initialize();
                    //TODO: fixme
                    //if the extra decorator code has to be downloaded,
                    //this.addedToParent called earlier
                    //than the whole class'es "initialize' has been called and
                    //"decoratorInstamce" would have a valid value
                    this._repositionConnectionRect();
                });

        } else {
            this._initialize();
        }
    };

    ModelEditorModelComponent.prototype._initialize = function () {
        var node = this.project.getNode(this.getId()),
            nodePosition = node.getRegistry(nodeRegistryNames.position);

        //generate skin controls
        this.el.addClass("model");

        this.el.css("z-index", this.zIndex);
        this.el.css("position", "absolute");

        //position self on parent canvas
        this._setPosition(nodePosition.x, nodePosition.y, false);

        /* TODO: THIS SHOULD BE DONE BY THE DECORATOR!!!!! */
        this.decoratorInstance = new this.DecoratorClass(this);
        this.decoratorInstance.render();
        /*this.el.css("width", "100px");
        this.el.css("height", "100px");
        this.el.css("border", "1px solid #000000");

        //apply content to controls
        this.el.html(node.getAttribute(nodeAttributeNames.name));*/
        /* TODO: END OF - THIS SHOULD BE DONE BY THE DECORATOR!!!!! */

    };

    ModelEditorModelComponent.prototype._addedToParent = function () {
        var self = this,
            selfId = this.getId();

        //create a thin edge around it that can be used to initiate connection drawing
        this.skinParts.connectionRect = $('
', {
            "id": "connectionRect_" + this.getId()
        });
        this.skinParts.connectionRect.css({"z-index": this.zIndex - 1,
            "cursor": "crosshair" });

        this.skinParts.connectionRect.insertBefore(this.el);

        this.skinParts.connectionRect.draggable({
            helper: function () {
                return $("

").data("id", selfId);
            },
            scroll: true,
            cursor: 'pointer',
            cursorAt: {
                left: 0,
                top: 0
            },
            start: function (event, ui) {
                self.skinParts.connectionRect.addClass("connection-source");
                self.parentComponent.startDrawConnection(selfId);
                event.stopPropagation();
            },
            stop: function (event, ui) {
                self.skinParts.connectionRect.removeClass("connection-source");
                self.parentComponent.endDrawConnection();
                event.stopPropagation();
            },
            drag: function (event, ui) {
                self.parentComponent.onDrawConnection(event);
            }
        });

        self.skinParts.connectionRect.bind("mousedown", function (event) {
            event.stopPropagation();
        });

        //make the whole parent container to be able to accept connection end drop
        this.el.droppable({
            accept: ".connection-source",
            hoverClass: "connection-end-state-hover",
            drop: function (event, ui) {
                var srcId = ui.helper.data("id");

                self.parentComponent.createConnection(srcId, selfId);
                event.stopPropagation();
            }
        });

        this._repositionConnectionRect();

        this._notifyParentAboutBBoxChanged();
    };

    ModelEditorModelComponent.prototype._repositionConnectionRect = function () {
        var bBox = this.getBoundingBox();

        if (this.skinParts.connectionRect) {
            this.skinParts.connectionRect.css({
                "position": "absolute",
                "left": bBox.x - this.connectionRectWidth,
                "top": bBox.y - this.connectionRectWidth,
                "width": bBox.width + this.connectionRectWidth * 2,
                "height": bBox.height + this.connectionRectWidth * 2
            });
        }
    };

    ModelEditorModelComponent.prototype.onSelect = function () {
        this.el.addClass("selected");
    };

    ModelEditorModelComponent.prototype.onDeselect = function () {
        this.el.removeClass("selected");
    };

    ModelEditorModelComponent.prototype._setPosition = function (pX, pY, updateDB) {
        var node = this.project.getNode(this.getId());

        //if position is different than the given one
        this.el.css({   "left": pX,
                        "top": pY });

        if (updateDB === true) {
            this.project.setRegistry(this.getId(), nodeRegistryNames.position, { "x": pX, "y": pY });
        }

        this.logger.debug("Object position changed to [" + pX + ", " + pY + "]" + (updateDB === true ? ", new position is saved back to database" : ""));

        /*if (decoratorInstance) {
            if ($.isFunction(decoratorInstance.afterSetPosition)) {
                decoratorInstance.afterSetPosition();
            }
        }*/

        //fix the connection rect around it
        this._repositionConnectionRect();
    };

    ModelEditorModelComponent.prototype.setPosition = function (pX, pY) {
        this._setPosition(pX, pY, true);
    };

    ModelEditorModelComponent.prototype.update = function () {
        var node = this.project.getNode(this.getId()),
            nodePosition = node.getRegistry(nodeRegistryNames.position);

        this._setPosition(nodePosition.x, nodePosition.y, false);

        this.decoratorInstance.update.call(this.decoratorInstance);

        this._notifyParentAboutBBoxChanged();
    };

    ModelEditorModelComponent.prototype._notifyParentAboutBBoxChanged = function () {
        if (this.parentComponent) {
            this.parentComponent.childBBoxChanged.call(this.parentComponent, this.getId());
        }
    };

    ModelEditorModelComponent.prototype.getConnectionPoints = function () {
        var bBox = this.getBoundingBox(),
            result = [];

        /*if (decoratorInstance) {
            if ($.isFunction(decoratorInstance.getConnectionPoints)) {
                return decoratorInstance.getConnectionPoints();
            }
        }*/

        /*result.push({ "dir": "S", x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height});
        result.push({ "dir": "N", x:  bBox.x + bBox.width / 2, y: bBox.y});
        result.push({ "dir": "E", x: bBox.x + bBox.width, y: bBox.y + bBox.height / 2});
        result.push({ "dir": "W", x: bBox.x, y: bBox.y + bBox.height / 2});*/

        result.push({ "dir": "X", x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2});

        return result;
    };

    ModelEditorModelComponent.prototype.decoratorUpdated = function () {
        this._notifyParentAboutBBoxChanged();
    };

    return ModelEditorModelComponent;
});<commit_msg>inroducing ModelWithPorts decorator<commit_after>"use strict";

define(['logManager',
    'clientUtil',
    'commonUtil',
    'nodeAttributeNames',
    'nodeRegistryNames',
    './ComponentBase.js',
    './SimpleModelDecorator.js'
    ], function (logManager,
             util,
             commonUtil,
             nodeAttributeNames,
             nodeRegistryNames,
             ComponentBase,
             SimpleModelDecorator) {

    var ModelEditorModelComponent;

    ModelEditorModelComponent = function (id, proj) {
        $.extend(this, new ComponentBase(id, proj));

        this.logger = logManager.create("ModelEditorModelComponent_" + id);
        this.logger.debug("Created");

        this.zIndex = 10;
        this.connectionRectWidth = 5;

        /*
         * OVERRIDE COMPONENTBASE MEMBERS
         */
        this.addedToParent = function () {
            this._addedToParent();
        };

        this.onDestroy = function () {
            //remove extra connection rectangle from DOM
            this.skinParts.connectionRect.remove();

            this.project.updateTerritory(this.territoryId, []);

            this.logger.debug("onDestroy");
        };

        this.isSelectable = function () {
            return true;
        };

        this.isMultiSelectable = function () {
            return true;
        };
        /*
         * END OVERRIDE COMPONENTBASE MEMBERS
         */

        this.decoratorInstance = null;
        this.DecoratorClass = SimpleModelDecorator;

        this._initializeDecorator();
    };

    ModelEditorModelComponent.prototype._initializeDecorator = function () {
        var node = this.project.getNode(this.getId()),
            decoratorName = "",
            customDecorator = node.getRegistry(nodeRegistryNames.decorator),
            self = this;

        if (_.isString(customDecorator)) {
            //TODO: delete
            customDecorator = "ModelWithPortsDecorator";
            //TODO: enddelete
            decoratorName = './js/ModelEditor/HTML/' + customDecorator + '.js';

            require([ decoratorName ],
                function (customDecoratorClass) {
                    self.DecoratorClass = customDecoratorClass;
                    //self._initialize();
                    //TODO: fixme
                    //if the extra decorator code has to be downloaded,
                    //this.addedToParent called earlier
                    //than the whole class'es "initialize' has been called and
                    //"decoratorInstamce" would have a valid value
                    //self._repositionConnectionRect();
                });

        } else {
            this._initialize();
        }
    };

    ModelEditorModelComponent.prototype._initialize = function () {
        var node = this.project.getNode(this.getId()),
            nodePosition = node.getRegistry(nodeRegistryNames.position);

        //generate skin controls
        this.el.addClass("model");

        this.el.css("z-index", this.zIndex);
        this.el.css("position", "absolute");

        //position self on parent canvas
        this._setPosition(nodePosition.x, nodePosition.y, false);

        //instantiate the decorator class and let it render the content
        this.decoratorInstance = new this.DecoratorClass(this);
        this.decoratorInstance.render();
    };

    ModelEditorModelComponent.prototype._addedToParent = function () {
        var self = this,
            selfId = this.getId();

        //create a thin edge around it that can be used to initiate connection drawing
        this.skinParts.connectionRect = $('
', {
            "id": "connectionRect_" + this.getId()
        });
        this.skinParts.connectionRect.css({"z-index": this.zIndex - 1,
            "cursor": "crosshair" });

        this.skinParts.connectionRect.insertBefore(this.el);

        this.skinParts.connectionRect.draggable({
            helper: function () {
                return $("

").data("id", selfId);
            },
            scroll: true,
            cursor: 'pointer',
            cursorAt: {
                left: 0,
                top: 0
            },
            start: function (event, ui) {
                self.skinParts.connectionRect.addClass("connection-source");
                self.parentComponent.startDrawConnection(selfId);
                event.stopPropagation();
            },
            stop: function (event, ui) {
                self.skinParts.connectionRect.removeClass("connection-source");
                self.parentComponent.endDrawConnection();
                event.stopPropagation();
            },
            drag: function (event, ui) {
                self.parentComponent.onDrawConnection(event);
            }
        });

        self.skinParts.connectionRect.bind("mousedown", function (event) {
            event.stopPropagation();
        });

        //make the whole parent container to be able to accept connection end drop
        this.el.droppable({
            accept: ".connection-source",
            hoverClass: "connection-end-state-hover",
            drop: function (event, ui) {
                var srcId = ui.helper.data("id");

                self.parentComponent.createConnection(srcId, selfId);
                event.stopPropagation();
            }
        });

        this._repositionConnectionRect();

        this._notifyParentAboutBBoxChanged();
    };

    ModelEditorModelComponent.prototype._repositionConnectionRect = function () {
        var bBox = this.getBoundingBox();

        if (this.skinParts.connectionRect) {
            this.skinParts.connectionRect.css({
                "position": "absolute",
                "left": bBox.x - this.connectionRectWidth,
                "top": bBox.y - this.connectionRectWidth,
                "width": bBox.width + this.connectionRectWidth * 2,
                "height": bBox.height + this.connectionRectWidth * 2
            });
        }
    };

    ModelEditorModelComponent.prototype.onSelect = function () {
        this.el.addClass("selected");
    };

    ModelEditorModelComponent.prototype.onDeselect = function () {
        this.el.removeClass("selected");
    };

    ModelEditorModelComponent.prototype._setPosition = function (pX, pY, updateDB) {
        var node = this.project.getNode(this.getId());

        //if position is different than the given one
        this.el.css({   "left": pX,
                        "top": pY });

        if (updateDB === true) {
            this.project.setRegistry(this.getId(), nodeRegistryNames.position, { "x": pX, "y": pY });
        }

        this.logger.debug("Object position changed to [" + pX + ", " + pY + "]" + (updateDB === true ? ", new position is saved back to database" : ""));

        //fix the connection rect around it
        this._repositionConnectionRect();
    };

    ModelEditorModelComponent.prototype.setPosition = function (pX, pY) {
        this._setPosition(pX, pY, true);
    };

    ModelEditorModelComponent.prototype.update = function () {
        var node = this.project.getNode(this.getId()),
            nodePosition = node.getRegistry(nodeRegistryNames.position);

        this._setPosition(nodePosition.x, nodePosition.y, false);

        this.decoratorInstance.update.call(this.decoratorInstance);

        this._notifyParentAboutBBoxChanged();
    };

    ModelEditorModelComponent.prototype._notifyParentAboutBBoxChanged = function () {
        if (this.parentComponent) {
            this.parentComponent.childBBoxChanged.call(this.parentComponent, this.getId());
        }
    };

    ModelEditorModelComponent.prototype.getConnectionPoints = function () {
        var bBox = this.getBoundingBox(),
            result = [];

        /*if (decoratorInstance) {
            if ($.isFunction(decoratorInstance.getConnectionPoints)) {
                return decoratorInstance.getConnectionPoints();
            }
        }*/

        /*result.push({ "dir": "S", x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height});
        result.push({ "dir": "N", x:  bBox.x + bBox.width / 2, y: bBox.y});
        result.push({ "dir": "E", x: bBox.x + bBox.width, y: bBox.y + bBox.height / 2});
        result.push({ "dir": "W", x: bBox.x, y: bBox.y + bBox.height / 2});*/

        result.push({ "dir": "X", x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2});

        return result;
    };

    ModelEditorModelComponent.prototype.decoratorUpdated = function () {
        this._notifyParentAboutBBoxChanged();
    };

    return ModelEditorModelComponent;
});<|endoftext|>


<commit_before>'use strict';

const {
  parseFilesData,
  getProjectSettings,
  deleteOldFiles,
  updateProjectSettings,
} = require('../project-settings');

jest.mock('fs', () => ({
  createReadStream: () => null,
}));

const storeSet = jest.fn();
const providerDelete = jest.fn();
global.strapi = {
  dirs: {
    public: 'publicDir',
  },
  config: {
    get: () => ({ provider: 'local' }),
  },
  plugins: {
    upload: {
      provider: {
        async uploadStream(file) {
          file.url = `/uploads/${file.hash}`;
        },
        delete: providerDelete,
      },
      services: {
        upload: {
          formatFileInfo: () => ({
            name: 'filename.png',
            alternativeText: null,
            caption: null,
            hash: 'filename_123',
            ext: '.png',
            mime: 'image/png',
            size: 123,
          }),
        },
        'image-manipulation': {
          getDimensions: () => ({ width: 100, height: 100 }),
        },
      },
    },
  },
  store: () => ({
    get: () => ({
      menuLogo: {
        name: 'name',
        url: 'file/url',
        width: 100,
        height: 100,
        ext: 'png',
        size: 123,
        provider: 'local',
      },
    }),
    set: storeSet,
  }),
};

describe('Project setting', () => {
  beforeEach(jest.resetAllMocks);

  describe('parseFilesData', () => {
    it('Should parse valid files object', async () => {
      const files = {
        menuLogo: {
          size: 123,
          path: '/tmp/filename_123',
          name: 'file.png',
          type: 'image/png',
        },
      };

      const expectedOutput = {
        menuLogo: {
          name: 'filename.png',
          alternativeText: null,
          caption: null,
          hash: 'filename_123',
          ext: '.png',
          mime: 'image/png',
          provider: 'local',
          size: 123,
          stream: null,
          width: 100,
          height: 100,
          tmpPath: '/tmp/filename_123',
        },
      };

      const parsedFiles = await parseFilesData(files);

      expect(parsedFiles).toEqual(expectedOutput);
    });

    it('Should skip empty files object with no error', async () => {
      const files = {};
      const expectedOutput = {};

      const parsedFiles = await parseFilesData(files);
      expect(parsedFiles).toEqual(expectedOutput);
    });
  });

  describe('getProjectSettings', () => {
    it('Should return project settings from store (only the right subset of fields)', async () => {
      const projectSettings = await getProjectSettings();

      const expectedOutput = {
        menuLogo: {
          name: 'name',
          url: 'file/url',
          width: 100,
          height: 100,
          ext: 'png',
          size: 123,
        },
      };

      expect(projectSettings).toStrictEqual(expectedOutput);
    });
  });

  describe('deleteOldFiles', () => {
    it('Does not delete when there was no previous file', async () => {
      const previousSettings = {
        menuLogo: null,
      };

      const newSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          url: 'file/url',
        },
      };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).not.toBeCalled();
    });

    it('Does not delete when there is no new file uploaded', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = previousSettings;

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).not.toBeCalled();
    });

    it('Deletes when inputs are explicitely set to null', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = { menuLogo: null };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).toBeCalledTimes(1);
    });

    it('Deletes when new files are uploaded', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = {
        menuLogo: {
          ...previousSettings.menuLogo,
          url: 'file/new',
        },
      };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).toBeCalledTimes(1);
    });
  });

  describe('updateProjectSettings', () => {
    it('Updates the project settings', async () => {
      const body = {};
      const files = {
        menuLogo: {
          name: 'filename.png',
          alternativeText: null,
          caption: null,
          hash: 'filename_123',
          ext: '.png',
          mime: 'image/png',
          size: 123,
          stream: null,
          width: 100,
          height: 100,
          tmpPath: '/tmp/filename_123',
          url: '/uploads/filename_123.png',
        },
      };

      const expectedOutput = {
        menuLogo: {
          name: 'filename.png',
          hash: 'filename_123',
          url: '/uploads/filename_123',
          width: 100,
          height: 100,
          ext: '.png',
          size: 123,
        },
      };

      await updateProjectSettings({ body, files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });

    it('Updates the project settings (delete)', async () => {
      const body = { menuLogo: '' };
      const files = {};

      const expectedOutput = {
        menuLogo: null,
      };

      await updateProjectSettings({ body, files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });

    it('Keeps the previous project settings', async () => {
      const body = {};
      const files = {};

      const expectedOutput = {
        menuLogo: {
          name: 'name',
          url: 'file/url',
          width: 100,
          height: 100,
          ext: 'png',
          size: 123,
          provider: 'local',
        },
      };

      await updateProjectSettings({ body, files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });
  });
});
<commit_msg>test(logo-customization): updated tests<commit_after>'use strict';

const {
  parseFilesData,
  getProjectSettings,
  deleteOldFiles,
  updateProjectSettings,
} = require('../project-settings');

jest.mock('fs', () => ({
  ...jest.requireActual('fs'),
  createReadStream: () => null,
}));

const storeSet = jest.fn();
const providerDelete = jest.fn();
global.strapi = {
  dirs: {
    public: 'publicDir',
  },
  config: {
    get: () => ({ provider: 'local' }),
  },
  plugins: {
    upload: {
      provider: {
        async uploadStream(file) {
          file.url = `/uploads/${file.hash}`;
        },
        delete: providerDelete,
      },
      services: {
        upload: {
          formatFileInfo: () => ({
            name: 'filename.png',
            alternativeText: null,
            caption: null,
            hash: 'filename_123',
            ext: '.png',
            mime: 'image/png',
            size: 123,
          }),
        },
        'image-manipulation': {
          getDimensions: () => ({ width: 100, height: 100 }),
        },
      },
    },
  },
  store: () => ({
    get: () => ({
      menuLogo: {
        name: 'name',
        url: 'file/url',
        width: 100,
        height: 100,
        ext: 'png',
        size: 123,
        provider: 'local',
      },
    }),
    set: storeSet,
  }),
};

describe('Project setting', () => {
  beforeEach(jest.resetAllMocks);

  describe('parseFilesData', () => {
    it('Should parse valid files object', async () => {
      const files = {
        menuLogo: {
          size: 123,
          path: '/tmp/filename_123',
          name: 'file.png',
          type: 'image/png',
        },
      };

      const expectedOutput = {
        menuLogo: {
          name: 'filename.png',
          alternativeText: null,
          caption: null,
          hash: 'filename_123',
          ext: '.png',
          mime: 'image/png',
          provider: 'local',
          size: 123,
          stream: null,
          width: 100,
          height: 100,
          tmpPath: '/tmp/filename_123',
        },
      };

      const parsedFiles = await parseFilesData(files);

      expect(parsedFiles).toEqual(expectedOutput);
    });

    it('Should skip empty files object with no error', async () => {
      const files = {};
      const expectedOutput = {};

      const parsedFiles = await parseFilesData(files);
      expect(parsedFiles).toEqual(expectedOutput);
    });
  });

  describe('getProjectSettings', () => {
    it('Should return project settings from store (only the right subset of fields)', async () => {
      const projectSettings = await getProjectSettings();

      const expectedOutput = {
        menuLogo: {
          name: 'name',
          url: 'file/url',
          width: 100,
          height: 100,
          ext: 'png',
          size: 123,
        },
      };

      expect(projectSettings).toStrictEqual(expectedOutput);
    });
  });

  describe('deleteOldFiles', () => {
    it('Does not delete when there was no previous file', async () => {
      const previousSettings = {
        menuLogo: null,
      };

      const newSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          url: 'file/url',
        },
      };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).not.toBeCalled();
    });

    it('Does not delete when there is no new file uploaded', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = previousSettings;

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).not.toBeCalled();
    });

    it('Deletes when inputs are explicitely set to null', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = { menuLogo: null };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).toBeCalledTimes(1);
    });

    it('Deletes when new files are uploaded', async () => {
      const previousSettings = {
        menuLogo: {
          size: 24085,
          name: 'file.png',
          type: 'image/png',
          provider: 'local',
          url: 'file/url',
        },
      };

      const newSettings = {
        menuLogo: {
          ...previousSettings.menuLogo,
          url: 'file/new',
        },
      };

      await deleteOldFiles({ previousSettings, newSettings });

      expect(providerDelete).toBeCalledTimes(1);
    });
  });

  describe('updateProjectSettings', () => {
    it('Updates the project settings', async () => {
      const body = {};
      const files = {
        menuLogo: {
          name: 'filename.png',
          alternativeText: null,
          caption: null,
          hash: 'filename_123',
          ext: '.png',
          mime: 'image/png',
          size: 123,
          stream: null,
          width: 100,
          height: 100,
          tmpPath: '/tmp/filename_123',
          url: '/uploads/filename_123.png',
        },
      };

      const expectedOutput = {
        menuLogo: {
          name: 'filename.png',
          hash: 'filename_123',
          url: '/uploads/filename_123.png',
          width: 100,
          height: 100,
          ext: '.png',
          size: 123,
        },
      };

      await updateProjectSettings({ ...body, ...files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });

    it('Updates the project settings (delete)', async () => {
      const body = { menuLogo: '' };
      const files = {};

      const expectedOutput = {
        menuLogo: null,
      };

      await updateProjectSettings({ ...body, ...files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });

    it('Keeps the previous project settings', async () => {
      const body = {};
      const files = {};

      const expectedOutput = {
        menuLogo: {
          name: 'name',
          url: 'file/url',
          width: 100,
          height: 100,
          ext: 'png',
          size: 123,
          provider: 'local',
        },
      };

      await updateProjectSettings({ ...body, ...files });

      expect(storeSet).toBeCalledTimes(1);
      expect(storeSet).toBeCalledWith({
        key: 'project-settings',
        value: expectedOutput,
      });
    });
  });
});
<|endoftext|>


<commit_before>import React, {useRef, useEffect, useState} from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import {Icon, Ball} from 'tocco-ui'
import {withTheme} from 'styled-components'
import {injectIntl, intlShape} from 'react-intl'

import {StyledSplit, AdminSearchGrid, Box, StyledGutter, StyledHeader, StyledSplitWrapper} from './StyedComponents'
import BasicSearchFormContainer from '../../containers/BasicSearchFormContainer'
import SearchFilterList from '../SearchFilterList'

const SEARCH_FILTER_BUTTON_HEIGHT = 28
const SEARCH_FILTER_PADDING = 15
const MAX_HEIGHT_THRESHOLD = 30
const MAX_SIZE_SEARCH_FILTER = 25

const getGutter = () => () => {
  const gutterEl = document.createElement('div')
  ReactDOM.render(
    <StyledGutter>
      <Icon icon="horizontal-rule"/>
    </StyledGutter>, gutterEl
  )
  return gutterEl
}

const AdminSearchForm = ({resetSearch, theme, intl, searchFilters}) => {
  const splitWrapperEl = useRef(null)
  const [size, setSize] = useState([MAX_SIZE_SEARCH_FILTER, 100 - MAX_SIZE_SEARCH_FILTER])
  const [searchFilterExpanded, setSearchFilterExpanded] = useState(false)
  const [showExpandSearchFilter, setShowExpandSearchFilter] = useState(false)

  const msg = id => intl.formatMessage({id})

  useEffect(() => {
    const searchFilterHeight = searchFilters
      ? searchFilters.length * SEARCH_FILTER_BUTTON_HEIGHT + SEARCH_FILTER_PADDING
      : SEARCH_FILTER_BUTTON_HEIGHT
    const splitWrapperHeight = splitWrapperEl.current.clientHeight
    const searchFilterHeightPercentage = Math.ceil(100 / (splitWrapperHeight / searchFilterHeight))

    if (!searchFilterExpanded && searchFilterHeightPercentage > MAX_HEIGHT_THRESHOLD) {
      setShowExpandSearchFilter(true)
      setSize([MAX_SIZE_SEARCH_FILTER, 100 - MAX_SIZE_SEARCH_FILTER])
    } else {
      setSize([searchFilterHeightPercentage, 100 - searchFilterHeightPercentage])
    }
  }, [searchFilters, searchFilterExpanded])

  return <AdminSearchGrid>
    <StyledHeader>
      {showExpandSearchFilter && <Ball
        icon={searchFilterExpanded ? 'chevron-up' : 'chevron-down'}
        onClick={() => setSearchFilterExpanded(!searchFilterExpanded)}
      />}
      <Ball
        data-cy="reset-button"
        icon="times"
        onClick={resetSearch}
        title={msg('client.entity-list.reset')}
      />
    </StyledHeader>
    <StyledSplitWrapper ref={splitWrapperEl}>
      <StyledSplit
        direction="vertical"
        gutterSize={20}
        sizes={size}
        minSize={[35, 0]}
        gutter={getGutter()}
      >
        <Box>
          <SearchFilterList />
        </Box>
        <Box>
          <BasicSearchFormContainer disableSimpleSearch={true}/>
        </Box>
      </StyledSplit>
    </StyledSplitWrapper>
  </AdminSearchGrid>
}

AdminSearchForm.propTypes = {
  intl: intlShape.isRequired,
  searchFilters: PropTypes.arrayOf(PropTypes.object),
  resetSearch: PropTypes.func.isRequired,
  theme: PropTypes.object
}

export default injectIntl(withTheme(AdminSearchForm))
<commit_msg>refactor(entity-list): adjust search filter height<commit_after>import React, {useRef, useEffect, useState} from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import {Icon, Ball} from 'tocco-ui'
import {withTheme} from 'styled-components'
import {injectIntl, intlShape} from 'react-intl'

import {StyledSplit, AdminSearchGrid, Box, StyledGutter, StyledHeader, StyledSplitWrapper} from './StyedComponents'
import BasicSearchFormContainer from '../../containers/BasicSearchFormContainer'
import SearchFilterList from '../SearchFilterList'

const SEARCH_FILTER_BUTTON_HEIGHT = 28
const SEARCH_FILTER_PADDING = 5
const MAX_HEIGHT_THRESHOLD = 30
const MAX_SIZE_SEARCH_FILTER = 25

const getGutter = () => () => {
  const gutterEl = document.createElement('div')
  ReactDOM.render(
    <StyledGutter>
      <Icon icon="horizontal-rule"/>
    </StyledGutter>, gutterEl
  )
  return gutterEl
}

const AdminSearchForm = ({resetSearch, theme, intl, searchFilters}) => {
  const splitWrapperEl = useRef(null)
  const [size, setSize] = useState([MAX_SIZE_SEARCH_FILTER, 100 - MAX_SIZE_SEARCH_FILTER])
  const [searchFilterExpanded, setSearchFilterExpanded] = useState(false)
  const [showExpandSearchFilter, setShowExpandSearchFilter] = useState(false)

  const msg = id => intl.formatMessage({id})

  useEffect(() => {
    const searchFilterHeight = searchFilters
      ? searchFilters.length * SEARCH_FILTER_BUTTON_HEIGHT + SEARCH_FILTER_PADDING
      : SEARCH_FILTER_BUTTON_HEIGHT
    const splitWrapperHeight = splitWrapperEl.current.clientHeight
    const searchFilterHeightPercentage = Math.floor(100 / (splitWrapperHeight / searchFilterHeight))

    if (!searchFilterExpanded && searchFilterHeightPercentage > MAX_HEIGHT_THRESHOLD) {
      setShowExpandSearchFilter(true)
      setSize([MAX_SIZE_SEARCH_FILTER, 100 - MAX_SIZE_SEARCH_FILTER])
    } else {
      setSize([searchFilterHeightPercentage, 100 - searchFilterHeightPercentage])
    }
  }, [searchFilters, searchFilterExpanded])

  return <AdminSearchGrid>
    <StyledHeader>
      {showExpandSearchFilter && <Ball
        icon={searchFilterExpanded ? 'chevron-up' : 'chevron-down'}
        onClick={() => setSearchFilterExpanded(!searchFilterExpanded)}
      />}
      <Ball
        data-cy="reset-button"
        icon="times"
        onClick={resetSearch}
        title={msg('client.entity-list.reset')}
      />
    </StyledHeader>
    <StyledSplitWrapper ref={splitWrapperEl}>
      <StyledSplit
        direction="vertical"
        gutterSize={20}
        sizes={size}
        minSize={[35, 0]}
        gutter={getGutter()}
      >
        <Box>
          <SearchFilterList />
        </Box>
        <Box>
          <BasicSearchFormContainer disableSimpleSearch={true}/>
        </Box>
      </StyledSplit>
    </StyledSplitWrapper>
  </AdminSearchGrid>
}

AdminSearchForm.propTypes = {
  intl: intlShape.isRequired,
  searchFilters: PropTypes.arrayOf(PropTypes.object),
  resetSearch: PropTypes.func.isRequired,
  theme: PropTypes.object
}

export default injectIntl(withTheme(AdminSearchForm))
<|endoftext|>