CartoDB/cartodb20

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

Summary

Maintainability
F
4 days
Test Coverage
var _ = require('underscore');
var $ = require('jquery');
var Backbone = require('backbone');
var CoreView = require('backbone/core-view');
var createTextLabelsTabPaneRouted = require('builder/components/tab-pane/create-text-labels-tab-pane-routed');
var TabPaneTemplate = require('./layer-tab-pane.tpl');
var LayerHeaderView = require('./layer-header-view');
var TablesCollection = require('builder/data/tables-collection');
var AnalysisSourceOptionsModel = require('./layer-content-views/analyses/analysis-source-options-model');
var AnalysisFormsCollection = require('./layer-content-views/analyses/analysis-forms-collection');
var TableManager = require('builder/components/table/table-manager');
var changeViewButtons = require('./change-view-buttons.tpl');
var editOverlay = require('./edit-overlay.tpl');
var DataView = require('./layer-content-views/data/data-view');
var LayerContentAnalysesView = require('./layer-content-views/analyses/analyses-view');
var StyleView = require('builder/editor/style/style-view');
var InfowindowsView = require('./layer-content-views/infowindow/infowindows-view');
var LegendsView = require('./layer-content-views/legend/legends-view');
var FeatureDefinitionModel = require('builder/data/feature-definition-model');
var AnalysesService = require('./layer-content-views/analyses/analyses-service');
var checkAndBuildOpts = require('builder/helpers/required-opts');
var TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
var Toggler = require('builder/components/toggler/toggler-view');
var Router = require('builder/routes/router');
var fetchAllQueryObjects = require('builder/helpers/fetch-all-query-objects');
var LayerContentModel = require('builder/data/layer-content-model');

var TABS = {
  data: 'data',
  analyses: 'analyses',
  style: 'style',
  popups: 'popups',
  legends: 'legends'
};

var DISABLED_TABS = [
  TABS.style,
  TABS.popups,
  TABS.legends
];

var ROUTER_ADD_GEOMETRY = {
  point: 'addPoint',
  line: 'addLine',
  polygon: 'addPolygon'
};

var CONTEXTS = LayerContentModel.CONTEXTS;

var REQUIRED_OPTS = [
  'userActions',
  'analysisDefinitionNodesCollection',
  'layerDefinitionsCollection',
  'layerDefinitionModel',
  'widgetDefinitionsCollection',
  'legendDefinitionsCollection',
  'stackLayoutModel',
  'modals',
  'onboardings',
  'configModel',
  'editorModel',
  'userModel',
  'mapDefinitionModel',
  'mapModeModel',
  'visDefinitionModel',
  'stateDefinitionModel',
  'onboardingNotification'
];

module.exports = CoreView.extend({
  module: 'editor:layers:layer-content-view',

  events: {
    'click .js-back': '_onClickBack',
    'click .js-fix-sql': '_onClickFixSQL'
  },

  options: {
    selectedTabItem: null,
    analysisPayload: null // id or a new analysis node attrs (may not be complete)
  },

  initialize: function (opts) {
    checkAndBuildOpts(opts, REQUIRED_OPTS, this);

    this.model = new LayerContentModel({}, {
      querySchemaModel: this._getQuerySchemaModel(),
      queryGeometryModel: this._getQueryGeometryModel(),
      queryRowsCollection: this._getQueryRowsCollection()
    });

    this._initViewState();
    this._onClickNewGeometry = this._onClickNewGeometry.bind(this);

    this._togglerModel = new Backbone.Model({
      labels: [_t('editor.context-switcher.map'), _t('editor.context-switcher.data')],
      active: this._isContextTable()
    });

    AnalysesService.setLayerId(this._layerDefinitionModel.get('id'));

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

  render: function () {
    this._unbindEvents();
    this.clearSubViews();
    this._destroyTable();
    this.$el.empty();

    this._renderHeaderView();
    this._renderLayerTabPaneView();

    if (this._isContextTable()) {
      this._initTable();
    }

    this._renderContextButtons();

    return this;
  },

  _initViewState: function () {
    this._viewState = new Backbone.Model({
      isLayerEmpty: false,
      canBeGeoreferenced: false,
      isLayerDone: false
    });
    this._setViewState();
  },

  _initBinds: function () {
    var querySchemaModel = this._getQuerySchemaModel();
    var queryRowsCollection = this._getQueryRowsCollection();
    var queryGeometryModel = this._getQueryGeometryModel();
    var layerDefinitionModel = this._layerDefinitionModel;

    this.listenTo(this.model, 'change:context', this._renderContextButtons);
    this.listenTo(this.model, 'change:state', this._updateViewState);
    this.listenTo(this._getAnalysisDefinitionNodeModel(), 'change:error', this.render);

    this.listenTo(layerDefinitionModel, 'remove', this._onClickBack);
    this.listenTo(layerDefinitionModel, 'change:source', this._updateViewState);
    this.listenTo(layerDefinitionModel, 'change:visible', this._renderContextButtons);
    this.listenTo(this._viewState, 'change', this.render);

    this.listenTo(this._editorModel, 'change:edition', this._changeStyle);

    this.listenTo(this._togglerModel, 'change:active', this._onTogglerChanged);

    this.listenTo(this._widgetDefinitionsCollection, 'add remove', this._renderContextButtons);

    this.listenTo(this._onboardings, 'style', function () {
      this._layerTabPaneView.collection.select('name', 'style');
    });

    this.listenTo(querySchemaModel, 'change:query', function () {
      this._renderContextButtons();

      if (this._editorModel.isEditing()) {
        this.listenToOnce(querySchemaModel, 'sync', function (e) {
          queryRowsCollection.fetch({
            success: function () {
              Router.goToDataTab(layerDefinitionModel.get('id'));
            }
          });
        });
      }
    });

    this.listenToOnce(queryGeometryModel, 'change:simple_geom', function (model, simpleGeom) {
      this._renderContextButtons();
    }, this);

    this.listenTo(Router.getRouteModel(), 'change:currentRoute', this._handleRoute);
  },

  _renderLayerTabPaneView: function () {
    var self = this;
    var analysisPayload = this.options.analysisPayload;
    var layerId = this._layerDefinitionModel.get('id');
    var canBeGeoreferenced = this._viewState.get('canBeGeoreferenced');
    var isDone = this._viewState.get('isLayerDone');
    var isEmpty = this._viewState.get('isLayerEmpty') && isDone;
    var selectedTabItem = this.options.selectedTabItem;
    var analysisFailed = this._getAnalysisDefinitionNodeModel().hasFailed();
    var isDisabled = canBeGeoreferenced || isEmpty || analysisFailed;

    if (isDisabled && (!selectedTabItem || _.contains(DISABLED_TABS, selectedTabItem))) {
      if ((!isEmpty && canBeGeoreferenced) || analysisFailed) {
        selectedTabItem = TABS.analyses;
      } else {
        selectedTabItem = TABS.data;
      }
    }

    var tabPaneTabs = [{
      label: _t('editor.layers.menu-tab-pane-labels.data'),
      name: TABS.data,
      selected: selectedTabItem === TABS.data,
      onClick: function () { Router.goToDataTab(layerId); },
      disabled: false,
      createContentView: function () {
        return new DataView({
          className: 'Editor-content',
          widgetDefinitionsCollection: self._widgetDefinitionsCollection,
          layerDefinitionModel: self._layerDefinitionModel,
          stackLayoutModel: self._stackLayoutModel,
          userActions: self._userActions,
          configModel: self._configModel,
          editorModel: self._editorModel,
          userModel: self._userModel,
          onboardings: self._onboardings,
          onboardingNotification: self._onboardingNotification,
          layerContentModel: self.model
        });
      }
    }, {
      label: _t('editor.layers.menu-tab-pane-labels.analyses'),
      name: TABS.analyses,
      selected: selectedTabItem === TABS.analyses,
      disabled: isEmpty && !analysisFailed,
      onClick: function () { Router.goToAnalysisTab(layerId); },
      createContentView: function () {
        var layerDefinitionModel = self._layerDefinitionModel;
        var analysisSourceOptionsModel = new AnalysisSourceOptionsModel(null, {
          analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
          layerDefinitionsCollection: self._layerDefinitionsCollection,
          tablesCollection: new TablesCollection(null, {
            configModel: self._configModel
          })
        });
        analysisSourceOptionsModel.fetch();

        var analysisFormsCollection = new AnalysisFormsCollection(null, {
          userActions: self._userActions,
          configModel: self._configModel,
          layerDefinitionModel: self._layerDefinitionModel,
          analysisSourceOptionsModel: analysisSourceOptionsModel
        });

        analysisFormsCollection.resetByLayerDefinition();

        if (!self._layerDefinitionModel.hasAnalyses() && canBeGeoreferenced && !analysisPayload) {
          analysisPayload = AnalysesService.generateGeoreferenceAnalysis();
        }

        // e.g. when selected from layers view
        var selectedNodeId;
        if (_.isString(analysisPayload) && analysisFormsCollection.get(analysisPayload)) {
          selectedNodeId = analysisPayload;
        } else if (_.isObject(analysisPayload)) {
          // payload passed after continue when an option was selected in add-analysis-view
          selectedNodeId = analysisPayload.id;
          // Detect when an analysis finishes so we can redirect and / or re-enable the tabs
          self.listenTo(self._analysisDefinitionNodesCollection, 'add', function (model) {
            var nodeSource = model.getPrimarySource();
            // This logic only applies to a geocoding analysis that's the first, on a layer that lacks geocoding
            if (nodeSource && nodeSource.get('type') === 'source' &&
                model.get('type').indexOf('georeference') === 0 &&
                canBeGeoreferenced) {
              var callback = function (model) {
                if (model.isDone() || model.hasFailed()) {
                  this.stopListening(model, 'change:status', callback);
                }

                if (model.isDone()) {
                  Router.goToStyleTab(layerId);
                }

                if (model.hasFailed()) {
                  self.render();
                }
              };

              self.listenTo(model, 'change:status', callback);
            }
          });

          analysisFormsCollection.addHead(analysisPayload);
        } else {
          var lastAnalysisNode = analysisFormsCollection.at(0);

          if (lastAnalysisNode) {
            selectedNodeId = lastAnalysisNode.get('id');
            Router.goToAnalysisNode(layerDefinitionModel.get('id'), selectedNodeId, { trigger: false, replace: true });
          } else {
            Router.goToAnalysisTab(layerDefinitionModel.get('id'), { trigger: false, replace: true });
          }
        }

        // remove payload once we have used it, to not have it being invoked again when switching tabs
        selectedTabItem = null;
        analysisPayload = null;

        return new LayerContentAnalysesView({
          className: 'Editor-content',
          userActions: self._userActions,
          analysisDefinitionNodesCollection: self._analysisDefinitionNodesCollection,
          editorModel: self._editorModel,
          userModel: self._userModel,
          analysisFormsCollection: analysisFormsCollection,
          configModel: self._configModel,
          layerDefinitionModel: self._layerDefinitionModel,
          stackLayoutModel: self._stackLayoutModel,
          selectedNodeId: selectedNodeId,
          onboardings: self._onboardings,
          onboardingNotification: self._onboardingNotification,
          layerContentModel: self.model
        });
      }
    }, {
      label: _t('editor.layers.menu-tab-pane-labels.style'),
      name: TABS.style,
      selected: selectedTabItem === TABS.style,
      layerId: layerId,
      onClick: function () { Router.goToStyleTab(layerId); },
      disabled: isDisabled,
      createContentView: function () {
        // remove payload once we have used it, to not have it being invoked again when switching tabs
        selectedTabItem = null;

        var styleView = new StyleView({
          className: 'Editor-content',
          configModel: self._configModel,
          userModel: self._userModel,
          userActions: self._userActions,
          analysisDefinitionsCollection: self.analysisDefinitionsCollection,
          queryGeometryModel: self._getQueryGeometryModel(),
          querySchemaModel: self._getQuerySchemaModel(),
          queryRowsCollection: self._getQueryRowsCollection(),
          layerDefinitionsCollection: self._layerDefinitionsCollection,
          layerDefinitionModel: self._layerDefinitionModel,
          editorModel: self._editorModel,
          modals: self._modals,
          onboardings: self._onboardings,
          onboardingNotification: self._onboardingNotification,
          layerContentModel: self.model
        });

        return styleView;
      }
    }, {
      label: _t('editor.layers.menu-tab-pane-labels.popups'),
      name: TABS.popups,
      selected: selectedTabItem === TABS.popups,
      onClick: function () { Router.goToPopupsTab(layerId); },
      disabled: isDisabled,
      createContentView: function () {
        var nodeDefModel = self._layerDefinitionModel.getAnalysisDefinitionNodeModel();
        return new InfowindowsView({
          className: 'Editor-content',
          userActions: self._userActions,
          layerDefinitionModel: self._layerDefinitionModel,
          querySchemaModel: nodeDefModel.querySchemaModel,
          editorModel: self._editorModel,
          layerContentModel: self.model
        });
      }
    }, {
      label: _t('editor.layers.menu-tab-pane-labels.legends'),
      name: TABS.legends,
      selected: selectedTabItem === TABS.legends,
      onClick: function () { Router.goToLegendsTab(layerId); },
      disabled: isDisabled,
      createContentView: function () {
        var nodeDefModel = self._layerDefinitionModel.getAnalysisDefinitionNodeModel();
        return new LegendsView({
          className: 'Editor-content',
          mapDefinitionModel: self._mapDefinitionModel,
          userActions: self._userActions,
          layerDefinitionModel: self._layerDefinitionModel,
          querySchemaModel: nodeDefModel.querySchemaModel,
          queryRowsCollection: nodeDefModel.queryRowsCollection,
          legendDefinitionsCollection: self._legendDefinitionsCollection,
          editorModel: self._editorModel,
          userModel: self._userModel,
          configModel: self._configModel,
          modals: self._modals,
          layerContentModel: self.model
        });
      }
    }];

    var tabPaneOptions = {
      tabPaneOptions: {
        className: 'Tab-pane js-editorPanelContent',
        template: TabPaneTemplate,
        tabPaneItemOptions: {
          tagName: 'li',
          klassName: 'CDB-NavMenu-item'
        }
      },
      tabPaneItemLabelOptions: {
        tagName: 'button',
        className: 'CDB-NavMenu-link u-upperCase'
      }
    };

    this._layerTabPaneView = createTextLabelsTabPaneRouted(tabPaneTabs, tabPaneOptions);
    this.$el.append(this._layerTabPaneView.render().el);
    this.addView(this._layerTabPaneView);

    this.listenTo(this._layerTabPaneView.collection, 'change:selected', this._quitEditing, this);
    this._changeStyle();
  },

  _renderHeaderView: function () {
    var analysisDefinitionNodeModel = this._getAnalysisDefinitionNodeModel();
    var tableNodeModel = analysisDefinitionNodeModel.isSourceType() && analysisDefinitionNodeModel.getTableModel();
    var layerHeaderView = new LayerHeaderView({
      layerDefinitionsCollection: this._layerDefinitionsCollection,
      layerDefinitionModel: this._layerDefinitionModel,
      userActions: this._userActions,
      configModel: this._configModel,
      modals: this._modals,
      tableNodeModel: tableNodeModel,
      editorModel: this._editorModel,
      stateDefinitionModel: this._stateDefinitionModel,
      visDefinitionModel: this._visDefinitionModel,
      userModel: this._userModel,
      widgetDefinitionsCollection: this._widgetDefinitionsCollection
    });

    this.addView(layerHeaderView);
    this.$el.append(layerHeaderView.render().$el);
  },

  _updateViewState: function () {
    var setViewStateFn = this._setViewState.bind(this);

    if (this.model.isDone()) {
      setViewStateFn();
      return false;
    }

    this._fetchAllQueryObjects({
      queryRowsCollection: this._getQueryRowsCollection(),
      queryGeometryModel: this._getQueryGeometryModel(),
      querySchemaModel: this._getQuerySchemaModel()
    }).then(function () {
      setViewStateFn();
    });
  },

  _fetchAllQueryObjects: fetchAllQueryObjects.bind(this),

  _destroyTable: function () {
    if (this._tableView) {
      TableManager.destroy(this._tableView);
      delete this._tableView;
    }
  },

  _initTable: function () {
    if (this._tableView) {
      this._destroyTable();
    }

    this._tableView = TableManager.create({
      analysisDefinitionNodeModel: this._getAnalysisDefinitionNodeModel(),
      configModel: this._configModel,
      modals: this._modals,
      userModel: this._userModel
    });

    $('.js-editor .CDB-Dashboard-canvas').append(this._tableView.render().el);
  },

  _isContextTable: function () {
    return this.model.get('context') === CONTEXTS.table;
  },

  _renderContextButtons: function () {
    this._destroyContextButtons();

    var analysisDefinitionNodeModel = this._getAnalysisDefinitionNodeModel();
    var isReadOnly = analysisDefinitionNodeModel.isReadOnly();
    var isVisible = this._layerDefinitionModel.get('visible');
    var queryGeometryModel = this._getQueryGeometryModel();

    $('.CDB-Dashboard-canvas .CDB-Map-canvas').append(
      changeViewButtons({
        context: this.model.get('context'),
        isThereOtherWidgets: this._widgetDefinitionsCollection.isThereOtherWidgets(),
        isThereTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries(),
        isThereAnimatedTimeSeries: this._widgetDefinitionsCollection.isThereTimeSeries({ animated: true }),
        queryGeometryModel: queryGeometryModel.get('simple_geom'),
        isSourceType: analysisDefinitionNodeModel.isSourceType(),
        isVisible: isVisible,
        isReadOnly: isReadOnly
      })
    );

    var togglerView = new Toggler({
      model: this._togglerModel
    });

    $('.js-mapTableView').append(togglerView.render().el);
    $('.js-newGeometry').on('click', this._onClickNewGeometry);
    this._addContextButtonsTooltips(isReadOnly, isVisible);
  },

  _addContextButtonsTooltips: function (isReadOnly, isVisible) {
    if (isReadOnly || !isVisible) {
      var disabledButtonTooltip = new TipsyTooltipView({
        el: $('.js-newGeometryView'),
        title: function () {
          return _t('editor.edit-feature.geometry-disabled');
        }
      });

      this.addView(disabledButtonTooltip);
      return;
    }

    var tooltips = $('.js-newGeometry').map(function () {
      return new TipsyTooltipView({
        el: $(this),
        title: function () {
          return $(this).data('tooltip');
        }
      });
    });

    _.forEach(tooltips, this.addView.bind(this));
  },

  _destroyContextButtons: function () {
    $('.js-newGeometry').off('click', this._onClickNewGeometry);

    $('.js-switchers').remove();
  },

  _onTogglerChanged: function () {
    var context = this._togglerModel.get('active') ? CONTEXTS.table : CONTEXTS.map;

    context === CONTEXTS.table
      ? this._initTable()
      : this._destroyTable();

    this.model.set('context', context);
  },

  _unbindEvents: function () {
    if (this._layerTabPaneView && this._layerTabPaneView.collection) {
      this._layerTabPaneView.collection.off('change:selected', this._quitEditing, this);
    }
  },

  _onClickBack: function () {
    this._editorModel.set({ edition: false });
    Router.goToLayerList();
  },

  _onClickNewGeometry: function (ev) {
    var $target = $(ev.target).closest('.js-newGeometry');
    var featureType = $target.data('feature-type');
    var addGeometry = ROUTER_ADD_GEOMETRY[featureType];

    if ($target.closest('.js-newGeometryItem').hasClass('is-disabled')) return false;

    if (Router[addGeometry]) {
      Router[addGeometry](this._layerDefinitionModel.get('id'));
    }
  },

  _handleRoute: function (routeModel) {
    var currentRoute = routeModel.get('currentRoute');
    var routeName = currentRoute[0];
    var OVERLAY_FADE_MS = 200;

    if (routeName.indexOf('add_feature_') === 0) {
      var featureType = routeName.split('add_feature_')[1];
      var editOverlayText = _t('editor.edit-feature.overlay-text', { featureType: _t('editor.edit-feature.features.' + featureType) });

      $('.CDB-Dashboard-canvas .CDB-Map-canvas').append(editOverlay({
        text: editOverlayText
      }));

      $('.js-editOverlay').fadeIn(OVERLAY_FADE_MS, function () {
        $('.js-editOverlay').removeClass('is-hidden');
      });

      var feature = this._newFeatureDefinitionModel({ featureType: featureType });
      this._mapModeModel.enterDrawingFeatureMode(feature);
      this._stackLayoutModel.goToStep(2, feature.getLayerDefinition(), !feature.isNew());
    }
  },

  _newFeatureDefinitionModel: function (opts) {
    return new FeatureDefinitionModel({}, {
      configModel: this._configModel,
      layerDefinitionModel: this._layerDefinitionModel,
      userModel: this._userModel,
      featureType: opts.featureType
    });
  },

  _onClickFixSQL: function () {
    this._editorModel.set({ edition: true });
    this._layerTabPaneView.getTabPane('data').set({ selected: true });
  },

  _changeStyle: function () {
    this._layerTabPaneView && this._layerTabPaneView.changeStyleMenu(this._editorModel);
  },

  _quitEditing: function () {
    if (this._layerTabPaneView.getSelectedTabPaneName() !== TABS.style &&
        this._layerTabPaneView.getSelectedTabPaneName() !== TABS.popups &&
        this._layerTabPaneView.getSelectedTabPaneName() !== TABS.legends) {
      this._editorModel.set({ edition: false });
    }
  },

  _getAnalysisDefinitionNodeModel: function () {
    return this._layerDefinitionModel.getAnalysisDefinitionNodeModel();
  },

  _getQueryGeometryModel: function () {
    return this._getAnalysisDefinitionNodeModel().queryGeometryModel;
  },

  _getQuerySchemaModel: function () {
    return this._getAnalysisDefinitionNodeModel().querySchemaModel;
  },

  _getQueryRowsCollection: function () {
    return this._getAnalysisDefinitionNodeModel().queryRowsCollection;
  },

  _setViewState: function () {
    var self = this;
    var emptyPromise = this._layerDefinitionModel.isEmptyAsync();
    var geoReferencePromise = this._layerDefinitionModel.canBeGeoreferenced();
    this._viewState.set('isLayerDone', self._layerDefinitionModel.isDone());

    Promise.all([emptyPromise, geoReferencePromise])
      .then(function (values) {
        self._viewState.set({
          isLayerEmpty: values[0],
          canBeGeoreferenced: values[1],
          isLayerDone: self._layerDefinitionModel.isDone()
        });
      });
  },

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