CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/builder/editor/layers/edit-feature-content-view.js

Summary

Maintainability
D
3 days
Test Coverage
var _ = require('underscore');
var $ = require('jquery');
var CoreView = require('backbone/core-view');
var template = require('./edit-feature-content.tpl');
var EditFeatureActionView = require('./edit-feature-content-views/edit-feature-action-view');
var EditFeatureControlView = require('./edit-feature-content-views/edit-feature-control-view');
var EditFeatureHeaderView = require('./edit-feature-content-views/edit-feature-header-view');
var VisTableModel = require('builder/data/visualization-table-model');
var EditFeatureInnerView = require('./edit-feature-content-views/edit-feature-inner-view');
var PanelWithOptionsView = require('builder/components/view-options/panel-with-options-view');
var ScrollView = require('builder/components/scroll/scroll-view');
var Notifier = require('builder/components/notifier/notifier');
var EditFeatureGeometryFormModel = require('./edit-feature-content-views/edit-feature-geometry-form-model');
var EditFeatureGeometryPointFormModel = require('./edit-feature-content-views/edit-feature-geometry-point-form-model');
var EditFeatureAttributesFormModel = require('./edit-feature-content-views/edit-feature-attributes-form-model');

var StyleHelper = require('builder/helpers/style');
var StyleConstants = require('builder/components/form-components/_constants/_style');
var StylesFactory = require('builder/editor/style/styles-factory');

var Router = require('builder/routes/router');

var NOTIFICATION_ID = 'editFeatureNotification';
var NOTIFICATION_ERROR_TEMPLATE = _.template("<span class='u-errorTextColor'><%- title %></span>");

var REQUIRED_OPTS = [
  'stackLayoutModel',
  'layerDefinitionModel',
  'configModel',
  'mapModeModel',
  'editorModel',
  'modals'
];

var GEOMETRY_TYPE = 'simple_geom';

module.exports = CoreView.extend({

  className: 'Editor-content',

  events: {
    'click .js-back': 'cleanAndGoBack'
  },

  initialize: function (opts) {
    _.each(REQUIRED_OPTS, function (item) {
      if (opts[item] === undefined) throw new Error(item + ' is required');
      this['_' + item] = opts[item];
    }, this);

    this._featureModel = this._mapModeModel.getFeatureDefinition();
    this._tableName = '';
    this._url = '';

    this._editorModel.set({
      edition: false
    });

    this._getTable();
    this._initBinds();
  },

  render: function () {
    this.clearSubViews();
    this.$el.html(template());

    this._initViews();

    return this;
  },

  _initBinds: function () {
    if (this._querySchemaModel.get('status') !== 'fetched') {
      // status can be: fetched, unavailable, fetching
      this._querySchemaModel.bind('change:status', this.render, this);
      this._querySchemaModel.fetch();
    }

    this._queryGeometryModel.bind('change:' + GEOMETRY_TYPE, this._checkNewLayerGeometryTypeDefinition, this);

    this.listenTo(this._editorModel, 'cancelPreviousEditions', this.clean);

    this.listenTo(this._featureModel, 'updateFeature', this._updateForms);
    this.listenTo(this._featureModel, 'destroyFeature', this._onDestroyFeature);
    this.listenTo(this._featureModel, 'destroyFeatureSuccess', this._onDestroyFeatureSuccess);
    this.listenTo(this._featureModel, 'destroyFeatureFailed', this._onDestroyFeatureFailed);
    this.listenTo(this._featureModel, 'addFeature', this._onAddFeature);
    this.listenTo(this._featureModel, 'saveFeature', this._onSaveFeature);
    this.listenTo(this._featureModel, 'saveFeatureSuccess', this._onSaveFeatureSuccess);
    this.listenTo(this._featureModel, 'saveFeatureFailed', this._onSaveFeatureFailed);
  },

  _initViews: function () {
    if (this._featureModel.isNew()) {
      this._addRow();
    } else {
      this._renderInfo();
    }
  },

  _updateForms: function () {
    this._updateGeometry();
    this._attributesFormModel.trigger('updateFeature');
  },

  _updateGeometry: function () {
    var geojson = null;

    try {
      geojson = JSON.parse(this._featureModel.get('the_geom'));
    } catch (err) {
      // if the geom is not a valid json value
    }

    var attrs;

    if (this._featureModel.isPoint()) {
      attrs = {
        lng: geojson && geojson.coordinates[0],
        lat: geojson && geojson.coordinates[1]
      };
    } else {
      attrs = {
        the_geom: this._featureModel.get('the_geom')
      };
    }

    this._geometryFormModel.set(attrs);
    this._geometryFormModel.trigger('updateFeature', attrs);
  },

  _addRow: function () {
    this._renderInfo();
  },

  _renderInfo: function () {
    this._renderHeader();
    this._renderContent();
  },

  _renderHeader: function () {
    if (this._headerView) {
      this.removeView(this._headerView);
      this._headerView.clean();
    }

    this._headerView = new EditFeatureHeaderView({
      url: this._url,
      tableName: this._tableName,
      layerDefinitionModel: this._layerDefinitionModel,
      model: this._featureModel,
      modals: this._modals,
      isNew: this._featureModel.isNew(),
      backAction: this.cleanAndGoBack.bind(this)
    });
    this.addView(this._headerView);
    this.$('.js-editFeatureHeader').html(this._headerView.render().el);
  },

  _renderContent: function () {
    if (this._contentView) {
      this.removeView(this._contentView);
      this._contentView.clean();
    }

    var geojson = null;

    try {
      geojson = JSON.parse(this._featureModel.get('the_geom'));
    } catch (err) {
      // if the geom is not a valid json value
    }

    if (this._featureModel.isPoint()) {
      this._geometryFormModel = new EditFeatureGeometryPointFormModel({
        lng: geojson && geojson.coordinates[0],
        lat: geojson && geojson.coordinates[1]
      }, {
        featureModel: this._featureModel
      });
    } else {
      this._geometryFormModel = new EditFeatureGeometryFormModel({
        the_geom: this._featureModel.get('the_geom')
      }, {
        featureModel: this._featureModel
      });
    }

    this._attributesFormModel = new EditFeatureAttributesFormModel(this._featureModel.toJSON(), {
      featureModel: this._featureModel,
      columnsCollection: this._sourceNode.querySchemaModel.columnsCollection,
      configModel: this._configModel,
      nodeDefModel: this._layerDefinitionModel.getAnalysisDefinitionNodeModel()
    });

    this._contentView = new PanelWithOptionsView({
      className: 'Editor-content',
      editorModel: this._editorModel,
      createContentView: function () {
        return new ScrollView({
          createContentView: function () {
            return new EditFeatureInnerView({
              featureModel: this._featureModel,
              geometryFormModel: this._geometryFormModel,
              attributesFormModel: this._attributesFormModel
            });
          }.bind(this)
        });
      }.bind(this),
      createControlView: function () {
        return new EditFeatureControlView();
      },
      createActionView: function () {
        return new EditFeatureActionView({
          model: this.model,
          featureModel: this._featureModel,
          geometryFormModel: this._geometryFormModel,
          attributesFormModel: this._attributesFormModel
        });
      }.bind(this)
    });
    this.addView(this._contentView);
    this.$('.js-editFeatureContent').html(this._contentView.render().el);
  },

  _onDestroyFeature: function () {
    if (Notifier.getNotification(NOTIFICATION_ID)) {
      Notifier.removeNotification(NOTIFICATION_ID);
    }

    this.notification = Notifier.addNotification({
      id: NOTIFICATION_ID,
      status: 'loading',
      info: _t('notifications.edit-feature.destroy.loading'),
      closable: false
    });
  },

  _onDestroyFeatureSuccess: function () {
    this._onUpdateFeature('destroy');

    this.notification.set({
      status: 'success',
      info: _t('notifications.edit-feature.destroy.success'),
      closable: true
    });

    var layerId = this._layerDefinitionModel.get('id');
    Router.goToStyleTab(layerId);
  },

  _onDestroyFeatureFailed: function (mdl, error) {
    this.notification.set({
      status: 'error',
      info: NOTIFICATION_ERROR_TEMPLATE({
        title: _t('notifications.edit-feature.destroy.error')
      }),
      closable: true
    });
  },

  _onAddFeature: function () {
    if (Notifier.getNotification(NOTIFICATION_ID)) {
      Notifier.removeNotification(NOTIFICATION_ID);
    }

    this.notification = Notifier.addNotification({
      id: NOTIFICATION_ID,
      status: 'loading',
      info: _t('notifications.edit-feature.adding'),
      closable: false
    });
  },

  _onSaveFeature: function () {
    if (Notifier.getNotification(NOTIFICATION_ID)) {
      Notifier.removeNotification(NOTIFICATION_ID);
    }

    this.notification = Notifier.addNotification({
      id: NOTIFICATION_ID,
      status: 'loading',
      info: _t('notifications.edit-feature.save.loading'),
      closable: false
    });
  },

  _onSaveFeatureSuccess: function (operation, model) {
    this._onUpdateFeature(operation);
    this._needReloadVis() && this._mapModeModel.trigger('reloadVis');

    this.notification.set({
      status: 'success',
      info: _t('notifications.edit-feature.save.success'),
      closable: true
    });

    Router.editFeature(model);
  },

  _checkNewLayerGeometryTypeDefinition: function (queryGeometryModel, currentGeometryType) {
    var layerGeometryTypeHasBeenDefined = currentGeometryType && !(queryGeometryModel.previous(GEOMETRY_TYPE));

    if (layerGeometryTypeHasBeenDefined) {
      var defaultStyle = _.extend({ type: StyleConstants.Type.SIMPLE },
        StylesFactory.getDefaultStyleAttrsByType(StyleConstants.Type.SIMPLE, currentGeometryType)
      );
      this._layerDefinitionModel.styleModel.set(defaultStyle);
    }
  },

  _onSaveFeatureFailed: function (mdl, error) {
    this.notification.set({
      status: 'error',
      info: NOTIFICATION_ERROR_TEMPLATE({
        title: _t('notifications.edit-feature.save.error')
      }),
      closable: true
    });
  },

  _onUpdateFeature: function (operation) {
    var self = this;
    this._queryGeometryModel.hasValueAsync()
      .then(function (hasGeom) {
        if (operation !== 'save' || !hasGeom) {
          self._queryGeometryModel.resetFetch();
        }
      });

    if (operation === 'add' || operation === 'destroy') {
      this._rowsCollection.resetFetch();
    }
  },

  _needReloadVis: function () {
    var style = this._layerDefinitionModel.styleModel;
    var color = StyleHelper.getColorAttribute(style);
    var size = StyleHelper.getSizeAttribute(style);
    var changed = _.keys(this._featureModel.changedAttributes());
    return (color && _.contains(changed, color) || size && _.contains(changed, size));
  },

  _getTable: function () {
    this._sourceNode = this._getSourceNode();

    if (this._sourceNode) {
      var tableName = this._sourceNode.get('table_name');
      this._visTableModel = new VisTableModel({
        id: tableName,
        table: {
          name: tableName
        }
      }, {
        configModel: this._configModel
      });

      this._querySchemaModel = this._sourceNode.querySchemaModel;
      this._queryGeometryModel = this._sourceNode.queryGeometryModel;
      this._rowsCollection = this._sourceNode.queryRowsCollection;
    }

    if (this._visTableModel) {
      var tableModel = this._visTableModel.getTableModel();
      this._tableName = tableModel.getUnquotedName();
      this._url = this._visTableModel && this._visTableModel.datasetURL();
    }
  },

  _getSourceNode: function () {
    var node = this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
    var source;
    if (node.get('type') === 'source') {
      source = node;
    } else {
      var primarySource = node.getPrimarySource();
      if (primarySource && primarySource.get('type') === 'source') {
        source = primarySource;
      }
    }

    return source;
  },

  _destroyContextOverlay: function () {
    $('.js-editOverlay').fadeOut(200, function () {
      $('.js-editOverlay').remove();
    });
  },

  cleanAndGoBack: function () {
    this.clean();
    this._mapModeModel.enterViewingMode();
    Router.goToPreviousRoute({
      options: { replace: true }
    });
  },

  clean: function () {
    this._destroyContextOverlay();
    CoreView.prototype.clean.apply(this);
  }
});