APIs

Show:
/**
 * @module ember-flexberry
 */

import Mixin from '@ember/object/mixin';
import { get } from '@ember/object'
import { merge } from '@ember/polyfills';
import { assert } from '@ember/debug';
import { inject as service } from '@ember/service';
import { isNone } from '@ember/utils';
import { A, isArray } from '@ember/array';

import Builder from 'ember-flexberry-data/query/builder';
import Condition from 'ember-flexberry-data/query/condition';
import FilterOperator from 'ember-flexberry-data/query/filter-operator';
import { BasePredicate, SimplePredicate, StringPredicate, ComplexPredicate,
  DatePredicate, stringToPredicate } from 'ember-flexberry-data/query/predicate';

/**
 * Mixin for {{#crossLink "DS.Controller"}}Controller{{/crossLink}} to support data reload.
 *
 * @class ReloadListMixin
 * @extends Mixin
 * @public
 */
export default Mixin.create({
  /**
   Service that triggers objectlistview events.

   @property objectlistviewEvents
   @type {Class}
   @default inject()
   */
  objectlistviewEvents: service(),

  /**
   * It reloads data by parameters.

     This method is called to get data for flexberry-objectlistview
     (both on list-form and lookup modal window).

   * @method reloadList
   * @public
   *
   * @param {Object} options Method options.
   * @param {String} [options.modelName] Type of records to load.
   * @param {String} [options.projectionName] Projection name to load data by.
   * @param {String} [options.perPage] Page size.
   * @param {String} [options.page] Current page.
   * @param {String} [options.sorting] Current sorting.
   * @param {String} [options.filter] Current filter.
   * @param {String} [options.filterProjectionName] Name of model projection which should be used for filtering throught search-element on toolbar. Filtering is processed only by properties defined in this projection.
   * @param {String} [options.predicate] Predicate to limit records.
   * @return {Promise}  A promise, which is resolved with a set of loaded records once the server returns.
   */
  reloadList: function(options) {
    if (options.filters instanceof ComplexPredicate) {
      var newFilter = A();
      options.filters._predicates.forEach((predicate) => {
        newFilter.push(this._normalizeNeqPredicate(predicate));
      }, this);

      options.filters._predicates = newFilter;
    } else {
      options.filters = this._normalizeNeqPredicate(options.filters);
    }

    let store = this.store;
    assert('Store for data loading is not defined.', store);

    let reloadOptions = options;

    let modelName = reloadOptions.modelName;
    assert('Model name for data loading is not defined.', modelName);

    let projectionName = reloadOptions.projectionName;
    assert('Projection name for data loading is not defined.', projectionName);

    let modelConstructor = store.modelFor(modelName);
    let projection = get(modelConstructor, 'projections')[projectionName];
    if (!projection) {
      throw new Error(`No projection with '${projectionName}' name defined in '${modelName}' model.`);
    }

    let filterProjectionName = reloadOptions.filterProjectionName;
    let filterProjection = undefined;
    if (filterProjectionName) {
      filterProjection = get(modelConstructor, 'projections')[filterProjectionName];
      if (!filterProjection) {
        throw new Error(`No projection with '${filterProjection}' name defined in '${modelName}' model.`);
      }
    }

    let allPredicates = Ember.A();

    if (reloadOptions.predicate && !(reloadOptions.predicate instanceof BasePredicate)) {
      throw new Error('Limit predicate is not correct. It has to be instance of BasePredicate.');
    }

    allPredicates.addObject(reloadOptions.predicate);

    if (reloadOptions.filters && !(reloadOptions.filters instanceof BasePredicate)) {
      throw new Error('Incorrect filters. It has to be instance of BasePredicate.');
    }

    allPredicates.addObject(reloadOptions.filters);

    if (reloadOptions.advLimit) {
      const advPredicate = stringToPredicate(reloadOptions.advLimit);
      if (!(advPredicate instanceof BasePredicate)) {
        throw new Error('Incorrect advLimit. It has to be instance of BasePredicate.');
      }

      allPredicates.addObject(advPredicate);
    }

    let perPage = reloadOptions.perPage;
    let page = reloadOptions.page;
    let pageNumber = parseInt(page, 10);
    let perPageNumber = parseInt(perPage, 10);
    assert('page must be greater than zero.', pageNumber > 0);
    assert('perPage must be greater than zero.', perPageNumber > 0);

    let builder = new Builder(store)
      .from(modelName)
      .selectByProjection(projectionName)
      .count();

    if (reloadOptions.hierarchicalAttribute) {
      if (reloadOptions.hierarchyPaging) {
        builder.top(perPageNumber).skip((pageNumber - 1) * perPageNumber);
        builder.orderBy('id asc');
      }

      let hierarchicalPredicate = new SimplePredicate(reloadOptions.hierarchicalAttribute, 'eq', null);
      allPredicates.addObject(hierarchicalPredicate);
    } else {
      builder.top(perPageNumber).skip((pageNumber - 1) * perPageNumber);
    }

    if (isArray(reloadOptions.sorting)) {
      let sorting = reloadOptions.sorting.filter(i => i.direction !== 'none').map(i => `${i.propName} ${i.direction}`).join(',');
      if (sorting) {
        builder.orderBy(sorting);
      }
    }

    const filter = reloadOptions.filter;
    const filterCondition = reloadOptions.filterCondition;
    const filterPredicate = filter
            ? this._getFilterPredicate(
                      filterProjection ? filterProjection : projection,
                      { filter, filterCondition })
            : undefined;
    allPredicates.addObject(filterPredicate);
    allPredicates = allPredicates.compact();
    const resultPredicate = allPredicates.length > 1 ?
      new ComplexPredicate(Condition.And, ...allPredicates) :
      allPredicates[0];

    this.get('objectlistviewEvents').setLimitFunction(resultPredicate, reloadOptions.componentName);

    if (resultPredicate) {
      builder.where(resultPredicate);
    }

    return store.query(modelName, builder.build());
  },

  /**
    Create predicate for attribute. Can you overload this function for extended logic.

    Default supported attribute types:
    - `string`
    - `number`

    @example
      ```javascript
      // app/routes/example.js
      ...
      // Add support for logical attribute
      predicateForAttribute(attribute, filter) {
        switch (attribute.type) {
          case 'boolean':
            let yes = ['TRUE', 'True', 'true', 'YES', 'Yes', 'yes', 'ДА', 'Да', 'да', '1', '+'];
            let no = ['False', 'False', 'false', 'NO', 'No', 'no', 'НЕТ', 'Нет', 'нет', '0', '-'];

            if (yes.indexOf(filter) > 0) {
              return new SimplePredicate(attribute.name, 'eq', 'true');
            }

            if (no.indexOf(filter) > 0) {
              return new SimplePredicate(attribute.name, 'eq', 'false');
            }

            return null;

          default:
            return this._super(...arguments);
        }
      },
      ...
      ```

    @method predicateForAttribute
    @param {Object} attribute Object contains attribute info.
    @param {String} attribute.name Name of attribute, example `name` or `type.name` if it attribute of relationship.
    @param {Object} attribute.options Object with attribute options.
    @param {Boolean} [attribute.options.hidden] Flag, indicate that this hidden attribute.
    @param {Boolean} [attribute.options.displayMemberPath] Flag, indicate that this attribute uses for display relationship.
    @param {String} attribute.type Type of attribute, example `string` or `number`.
    @param {String} filter Pattern for search.
    @param {String} filterCondition Condition for predicate, can be `or` or `and`.
    @return {BasePredicate|null} Object class of `BasePredicate` or `null`, if not need filter.
    @for ListFormRoute
  */
  predicateForAttribute(attribute, filter, filterCondition) {
    if (attribute.options.hidden && !attribute.options.displayMemberPath) {
      return null;
    }

    switch (attribute.type) {
      case 'string': {
        let words = filter.trim().replace(/\s+/g, ' ').split(' ');
        if (filterCondition && words.length > 1) {
          let predicates = words.map(word => new StringPredicate(attribute.name).contains(word));
          return new ComplexPredicate(filterCondition, ...predicates);
        }

        return new StringPredicate(attribute.name).contains(filter);
      }

      case 'number': {
        if (isFinite(filter)) {
          return new SimplePredicate(attribute.name, 'eq', +filter);
        }

        return null;
      }

      case 'decimal': {
        filter = filter.replace(',', '.');
        if (isFinite(filter)) {
          return new SimplePredicate(attribute.name, 'eq', +filter);
        }

        return null;
      }

      case 'boolean': {
        let yes = ['TRUE', 'True', 'true', 'YES', 'Yes', 'yes', 'ДА', 'Да', 'да', '1', '+'];
        let no = ['False', 'False', 'false', 'NO', 'No', 'no', 'НЕТ', 'Нет', 'нет', '0', '-'];

        if (yes.indexOf(filter) > 0) {
          return new SimplePredicate(attribute.name, 'eq', 'true');
        }

        if (no.indexOf(filter) > 0) {
          return new SimplePredicate(attribute.name, 'eq', 'false');
        }

        return null;
      }

      default: {
        return null;
      }
    }
  },

  /**
    It forms the filter predicate for data loading.

    @method _getFilterPredicate
    @param {Object} modelProjection A projection used for data retrieving.
    @param {Object} params Current parameters to form predicate.
    @return {BasePredicate|null} Filter predicate for data loading.
    @private
  */
  _getFilterPredicate: function(modelProjection, params) {
    assert('Projection is not defined', modelProjection);

    let predicates = [];
    if (params.filter) {
      let attributes = this._attributesForFilter(modelProjection, this.store);
      attributes.forEach((attribute) => {
        let predicate = this.predicateForAttribute(attribute, params.filter, params.filterCondition);
        if (predicate) {
          predicates.push(predicate);
        }
      });
    }

    return predicates.length ? predicates.length > 1 ? new ComplexPredicate(Condition.Or, ...predicates) : predicates[0] : null;
  },

  /**
    Generates array attributes for filter.

    @method _attributesForFilter
    @param {Object} projection
    @param {DS.Store} store
    @return {Array} Array objects format: `{ name, type }`, where `name` - attribute name, `type` - attribute type.
    @private
  */
  _attributesForFilter(projection, store) {
    let attributes = [];

    for (let name in projection.attributes) {
      if (projection.attributes.hasOwnProperty(name)) {
        let attribute = projection.attributes[name];
        switch (attribute.kind) {
          case 'attr': {
            let options = merge({}, attribute.options);
            options.displayMemberPath = projection.options && projection.options.displayMemberPath === name;
            attributes.push({
              name: name,
              options: options,
              type: get(store.modelFor(projection.modelName), 'attributes').get(name).type,
            });
            break;
          }

          case 'belongsTo': {
            let belongsToAttributes = this._attributesForFilter(attribute, store);
            for (let i = 0; i < belongsToAttributes.length; i++) {
              belongsToAttributes[i].name = `${name}.${belongsToAttributes[i].name}`;
              attributes.push(belongsToAttributes[i]);
            }

            break;
          }

          case 'hasMany':
            break;

          default: {
            throw new Error(`Not supported kind: ${attribute.kind}`);
          }
        }
      }
    }

    return attributes;
  },

  /**
    Generates new predicate for neq value predicate.

    @method _normalizeNeqPredicate
    @param {Object} predicate
    @return {Object} Normalized predicate.
    @private
  */
  _normalizeNeqPredicate(predicate) {
    let result = predicate;
    if (!isNone(predicate) && predicate._operator === 'neq' && predicate._value !== null) {
      let sp1 = predicate;
      let sp2;
      if (predicate instanceof SimplePredicate || predicate instanceof DatePredicate) {
        sp2 = new SimplePredicate(sp1._attributePath, FilterOperator.Eq, null);
      }

      result = sp2 ? new ComplexPredicate(Condition.Or, sp1, sp2) : sp1;
    }

    return result;
  }
});