CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/cartodb/old_common/header.js

Summary

Maintainability
F
3 days
Test Coverage

/**
 *  Common header for vis view ( table | derived )
 *
 *  - It needs a visualization model, config and user data.
 *
 *    var header = new cdb.admin.Header({
 *      el:       this.$('header'),
 *      model:    visusalization_model,
 *      user:     user_model,
 *      config:   config,
 *      geocoder: geocoder
 *    });
 *
 */

cdb.admin.Header = cdb.core.View.extend({

  _TEXTS: {
    saving:         _t('Saving...'),
    saved:          _t('Saved'),
    error:          _t('Something went wrong, try again later'),
    metadata: {
      edit:         _t('Edit metadata...'),
      view:         _t('View metadata...')
    },
    visualization: {
      loader:       _t('Changing to visualization'),
      created:      _t('Visualization created')
    },
    share: {
      publish:        _t('PUBLISH'),
      visualize:    _t('VISUALIZE')
    },
    share_privacy: {
      ok_next:      _t('Share it now!')
    },
    rename: {
      readonly:     _t('It is not possible to rename<br/>the dataset in <%- mode %> mode'),
      owner:        _t('It is not possible to rename<br/>the dataset if you are not the owner')
    }
  },

  _MAX_DESCRIPTION_LENGTH: 200,

  events: {
    'click a.title':        '_changeTitle',
    'click .metadata a':    '_changeMetadata',
    'click a.options':      '_openOptionsMenu',
    'click a.share':        '_shareVisualization',
    'click a.privacy':      '_showPrivacyDialog',
    'click header nav a':   '_onTabClick'
  },

  initialize: function(options) {

    _.bindAll(this, '_changeTitle', '_setPrivacy');

    this.$body = $('body');
    this.dataLayer = null;
    this.globalError = this.options.globalError;
    this._initBinds();

    // Display all the visualization info
    this.setInfo();
  },

  // Set new dataLayer from the current layerView
  setActiveLayer: function(layerView) {
    // Clean before bindings
    if (this.dataLayer) {
      this.dataLayer.unbind('applySQLView applyFilter errorSQLView clearSQLView', this.setEditableInfo,  this);
      this.dataLayer.table.unbind('change:isSync', this.setEditableInfo, this);
      this.dataLayer.table.unbind('change:permission', this.setInfo, this);
    }

    // Set new datalayer
    this.dataLayer = layerView.model;

    // Apply bindings if model is not a visualization
    if (!this.model.isVisualization()) {
      this.dataLayer.bind('applySQLView applyFilter errorSQLView clearSQLView', this.setEditableInfo,  this);
      this.dataLayer.table.bind('change:isSync', this.setEditableInfo, this);
      this.dataLayer.table.bind('change:permission', this.setInfo, this);
      this.setEditableInfo();
    }

  },

  _initBinds: function() {
    this.model.bind('change:name',        this._setName,            this);
    this.model.bind('change:type',        this.setInfo,             this);
    this.model.bind('change:privacy',     this._setPrivacy,      this);
    this.model.bind('change:permission',  this._setSharedCount,  this);
  },

  _openOptionsMenu: function(e) {
    this.killEvent(e);

    var self = this;
    var $target = $(e.target);

    // Options menu
    this.options_menu = new cdb.admin.HeaderOptionsMenu({
      target: $(e.target),
      model: this.model, // master_vis
      dataLayer: this.dataLayer,
      user: this.options.user,
      private_tables: this.options.user.get("actions").private_tables,
      geocoder: this.options.geocoder,
      backgroundPollingModel: this.options.backgroundPollingModel,
      globalError: this.options.globalError,
      template_base: 'table/header/views/options_menu'
    }).bind("onDropdownShown",function(ev) {
      cdb.god.unbind("closeDialogs", self.options_menu.hide, self.options_menu);
      cdb.god.trigger("closeDialogs");
      cdb.god.bind("closeDialogs", self.options_menu.hide, self.options_menu);
    }).bind('onDropdownHidden', function() {
      this.clean();
      $target.unbind('click');
      cdb.god.unbind(null, null, self.options_menu);
    });

    this.$body.append(this.options_menu.render().el);
    this.options_menu.open(e);
  },

  /**
   *  Share visualization function, it could show
   *  the name dialog to create a new visualization
   *  or directly the share dialog :).
   */
  _shareVisualization: function(e) {
    this.killEvent(e);

    var view;
    if (this.model.isVisualization()) {
      view = new cdb.editor.PublishView({
        clean_on_hide: true,
        enter_to_confirm: true,
        user: this.options.user,
        model: this.model // vis
      });
    } else {
      view = new cdb.editor.CreateVisFirstView({
        clean_on_hide: true,
        enter_to_confirm: true,
        model: this.model,
        router: window.table_router,
        title: 'A map is required to publish',
        explanation: 'A map is a shareable mix of layers, styles and queries. You can view all your maps in your dashboard.'
      });
    }
    view.appendToBody();
  },

  _showPrivacyDialog: function(e) {
    if (e) this.killEvent(e);

    if (this.model.isOwnedByUser(this.options.user)) {
      var dialog = new cdb.editor.ChangePrivacyView({
        vis: this.model, //vis
        user: this.options.user,
        enter_to_confirm: true,
        clean_on_hide: true
      });
      dialog.appendToBody();
    }
  },

  /**
   *  Set visualization info
   */
  setInfo: function() {
    this._setName();
    this._setSyncInfo();
    this._setVisualization();
    this._setMetadata();
  },

  /**
   *  Set editable visualization info
   */
  setEditableInfo: function() {
    this._setName();
    this._setSyncInfo();
    this._setMetadata();
  },

  _setPrivacy: function() {

    var $share  = this.$('a.privacy');

    // Update shared count if it is neccessary
    this._setSharedCount();

    var privacy = this.model.get("privacy").toLowerCase();

    if (privacy == "public") {

      $share
      .removeClass("private")
      .removeClass("link_protected")
      .removeClass("password_protected")
      .removeClass("organization")
      .addClass("public");

    } else if (privacy == "link"){

      $share
      .removeClass("public")
      .removeClass("private")
      .removeClass("password_protected")
      .removeClass("organization")
      .addClass("link_protected");

    } else if (privacy == "private"){

      $share
      .removeClass("public")
      .removeClass("link_protected")
      .removeClass("password_protected")
      .removeClass("organization")
      .addClass("private");

    } else if (privacy == "password"){

      $share
      .removeClass("private")
      .removeClass("link_protected")
      .removeClass("public")
      .removeClass("organization")
      .addClass("password_protected");

    } else if (privacy == "organization"){

      $share
      .removeClass("private")
      .removeClass("link_protected")
      .removeClass("public")
      .removeClass("password_protected")
      .addClass("organization");

    }

    // User is owner of this visualization (table or derived)?
    var isOwner = this.model.permission.isOwner(this.options.user);
    $share.find('i')[ isOwner ? 'removeClass' : 'addClass' ]('disabled');

  },

  _setSharedCount: function() {
    var isOwner = this.model.permission.isOwner(this.options.user);
    var $share  = this.$('a.privacy i');

    $share.empty();

    if (isOwner) {
      var $count = $('<span>').addClass('shared_users');

      if (this.model.permission.acl.size() > 0) {
        // Get total shared users or if the whole organization has access
        var shared_users = 0;
        var users_perm = this.model.permission.getUsersWithAnyPermission();

        if (this.model.permission.isSharedWithOrganization()) {
          shared_users = 'ORG';
        } else {
          shared_users = users_perm.length;
        }

        $count.text( (shared_users !== 0) ? shared_users : '' );

        $share.append($count);
      }
    }
  },

  /**
   *  Change metadata link text
   */
  _setMetadata: function() {
    var isOwner = this.model.permission.isOwner(this.options.user);
    var $metadata = this.$('.metadata a');

    var text = this._TEXTS.metadata.edit;
    var href = "#/edit-metadata";

    if (!isOwner) {
      text = this._TEXTS.metadata.view;
      href = "#/view-metadata";
    }

    $metadata
      .attr('href', href)
      .text(text);
  },

  /**
   *  Set layer sync info if it is needed
   */
  _setSyncInfo: function() {
    this.sync_info && this.sync_info.clean();

    if (!this.model.isVisualization() && this.isSyncTable()) {
      this.$el.addClass('synced');

      this.sync_info = new cdb.admin.SyncInfo({
        dataLayer: this.dataLayer,
        user: this.options.user
      });

      this.$('.sync_status').append(this.sync_info.render().el);
      this.addView(this.sync_info);

    } else {
      this.$el.removeClass('synced');
    }
  },

  /**
   *  Set name of the visualization
   */
  _setName: function() {
    var $title = this.$('h1 a.title');

    $title
      [(this.isVisEditable() && !this.isSyncTable()) ? 'removeClass' : 'addClass' ]('disabled')
      .text(this.model.get('name'))

    document.title = this.model.get('name') + " | CARTO";
  },


  /**
   *  Set visualization type and change share button
   */
  _setVisualization: function() {
    // Change visualization type
    var $back            = this.$('a.back');
    var $share           = this.$('a.share');
    var is_visualization = this.model.isVisualization();

    if (is_visualization) {
      $share.find("span").text(this._TEXTS.share.publish);
      this._setPrivacy();
      var route = cdb.config.prefixUrl() + "/dashboard/maps";
      $back.attr("href", route );
    } else {
      $share.find("span").text(this._TEXTS.share.visualize);
      this._setPrivacy();
      var route = cdb.config.prefixUrl() + "/dashboard/datasets";
      $back.attr("href", route );
    }
  },

  /**
   *  Change visualization metadata
   */
  _changeMetadata: function(ev) {
    ev.preventDefault();

    var dlg = new cdb.editor.EditVisMetadataView({
      maxLength: this._MAX_DESCRIPTION_LENGTH,
      vis: this.model,
      dataLayer: this.dataLayer && this.dataLayer.table,
      user: this.options.user,
      clean_on_hide: true,
      enter_to_confirm: false,
      onShowPrivacy: this._showPrivacyDialog.bind(this),
      onDone: this._onChangeMetadata.bind(this)
    });

    dlg.appendToBody();
  },

  _onChangeMetadata: function(nameChanged) {
    // Check if attr saved is name to change url when
    // visualization is table type
    if (nameChanged && !this.model.isVisualization()) {
      window.table_router.navigate(this._generateTableUrl(), {trigger: false});
      window.table_router.addToHistory();
    }
  },

  /**
   *  Change visualization title
   */
  _changeTitle: function(e) {
    this.killEvent(e);

    var self = this;
    var isOwner = this.model.permission.isOwner(this.options.user);

    if (this.isVisEditable()) {
      this.title_dialog && this.title_dialog.clean();
      cdb.god.trigger("closeDialogs");

      var title_dialog = this.title_dialog = new cdb.admin.EditTextDialog({
        initial_value: this.model.get('name'),
        template_name: 'table/views/edit_name',
        clean_on_hide: true,
        modal_class: 'edit_name_dialog',
        onResponse: setTitle
      });

      cdb.god.bind("closeDialogs", title_dialog.hide, title_dialog);

      // Set position and show
      var pos = $(e.target).offset();
      pos.left -= $(window).scrollLeft()
      pos.top -= $(window).scrollTop()
      var w = Math.max($(e.target).width() + 100, 280);
      title_dialog.showAt(pos.left - 20, pos.top - 10, w);
    } else {
      var $el = $(e.target);
      $el
        .bind('mouseleave', destroyTipsy)
        .tipsy({
          fade:     true,
          trigger:  'manual',
          html:     true,
          title:    function() {
            var mode = self.isSyncTable() ? 'sync' : 'read-only';
            return _.template(self._TEXTS.rename[ !isOwner ? 'owner' : 'readonly' ])({ mode: mode })
          }
        })
        .tipsy('show')
    }

    function destroyTipsy() {
      var $el = $(this);
      var tipsy = $el.data('tipsy');
      if (tipsy) {
        $el
          .tipsy('hide')
          .unbind('mouseleave', destroyTipsy);
      }
    }

    function setTitle(val) {
      if (val !== self.model.get('name') && val != '') {
        // Sanitize description (html and events)
        var title = cdb.Utils.stripHTML(val,'');

        if (self.model.isVisualization()) {
          self._onSetAttributes({ name: title });
        } else {
          // close any prev modal if existing
          if (self.change_confirmation) {
            self.change_confirmation.clean();
          }
          self.change_confirmation = cdb.editor.ViewFactory.createDialogByTemplate('common/dialogs/confirm_rename_dataset');

          // If user confirms, app set the new name
          self.change_confirmation.ok = function() {
            self._onSetAttributes({ name: title });
            if (_.isFunction(this.close)) {
              this.close();
            }
          };

          self.change_confirmation
            .appendToBody()
            .open();
        }
      }
    }
  },



  /**
   *  Wait function before set new visualization attributes
   */
  _onSetAttributes: function(d) {

    var old_data = this.model.toJSON();
    var new_data = d;

    this.model.set(d, { silent: true });

    // Check if there is any difference
    if (this.model.hasChanged()) {
      var self = this;

      this.globalError.showError(this._TEXTS.saving, 'load', -1);

      this.model.save({},{
        wait: true,
        success: function(m) {
          // Check if attr saved is name to change url
          if (new_data.name !== old_data.name && !self.model.isVisualization()) {
            window.table_router.navigate(self._generateTableUrl(), {trigger: false});
            window.table_router.addToHistory();
          }

          self.globalError.showError(self._TEXTS.saved, 'info', 3000);
        },
        error: function(msg, resp) {
          var err =  resp && JSON.parse(resp.responseText).errors[0];
          self.globalError.showError(err, 'error', 3000);
          self.model.set(old_data, { silent: true });
          self.setInfo();
        }
      });
    }


  },

  /**
   *  Check if visualization/table is editable
   *  (Checking if it is visualization and/or data layer is in sql view)
   */
  isVisEditable: function() {
    if (this.model.isVisualization()) {
      return true;
    } else {
      var table = this.dataLayer && this.dataLayer.table;

      if (!table) {
        return false;
      } else if (table && (table.isReadOnly() || !table.permission.isOwner(this.options.user))) {
        return false;
      } else {
        return true;
      }
    }
  },


  isSyncTable: function() {
    if (this.dataLayer && this.dataLayer.table) {
      return this.dataLayer.table.isSync();
    }
    return false;
  },


  _generateTableUrl: function(e) {
    // Let's create the url ourselves //
    var url = '';

    // Check visualization type and get table or viz id
    if (this.model.isVisualization()) {
      url += '/viz/' + this.model.get('id');
    } else {
      var isOwner = this.model.permission.isOwner(this.options.user);
      var table = new cdb.admin.CartoDBTableMetadata(this.model.get('table'));

      // Qualify table urls if user is not the owner
      if (!isOwner) {
        var owner_username = this.model.permission.owner.get('username');
        url += '/tables/' + owner_username + '.' + table.getUnqualifiedName();
      } else {
        url += '/tables/' + table.getUnqualifiedName();
      }
    }

    // Get scenario parameter from event or current url (table or map)
    var current = e ? $(e.target).attr('href') : window.location.pathname;
    if (current.search('/map') != -1) {
      url += '/map'
    } else {
      url += '/table'
    }

    return url;
  },


  _onTabClick: function(e) {
    e.preventDefault();
    window.table_router.navigate(this._generateTableUrl(e), {trigger: true});
  }
});