CartoDB/cartodb20

View on GitHub
lib/assets/javascripts/dashboard/views/api-keys/api-keys-form-view.js

Summary

Maintainability
C
7 hrs
Test Coverage
const _ = require('underscore');
const Backbone = require('backbone');
const CoreView = require('backbone/core-view');
require('dashboard/components/form-components/index');
const checkAndBuildOpts = require('builder/helpers/required-opts');
const ApiKeyModel = require('dashboard/data/api-key-model');
const TableGrantsView = require('dashboard/components/table-grants/table-grants-view');
const TipsyTooltipView = require('builder/components/tipsy-tooltip-view');
const template = require('./api-keys-form.tpl');
const ApiKeysCollection = require('dashboard/data/api-keys-collection');

const REQUIRED_OPTS = [
  'stackLayoutModel',
  'userTablesModel',
  'userModel'
];

const API_TYPES = {
  maps: 'maps',
  sql: 'sql'
};

const DATASET_SCOPE_TYPES = {
  create: 'create',
  listing: 'listing'
};

module.exports = CoreView.extend({
  className: 'ApiKeysForm',

  events: {
    'click .js-back': '_onClickBack',
    'click .js-submit': '_onFormSubmit',
    'change input#create': '_onChangeCreateCheckbox',
    'change input#sql': '_onChangeSqlCheckbox'

  },

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

    this._apiKeysCollection = new ApiKeysCollection(null, { userModel: this._userModel });
    this._apiKeyModel = options.apiKeyModel || new ApiKeyModel(null, { userModel: this._userModel });
    this._formView = this._generateForm();

    this.listenTo(this._formView, 'change', this._onFormChanged);
    this.listenTo(this._apiKeyModel, 'change:tables', this._onFormChanged);
  },

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

    this._initViews();

    this._handleCheckboxState();

    return this;
  },

  _initViews: function () {
    this.$el.append(template({ modelIsNew: this._isNew() }));

    this.$('.js-api-keys-form').append(this._formView.render().el);

    this._tableGrantsView = new TableGrantsView({
      apiKeyModel: this._apiKeyModel,
      userTablesModel: this._userTablesModel
    });

    this.addView(this._tableGrantsView);

    this.$('.js-api-keys-tables').append(this._tableGrantsView.render().el);

    if (this._isNew()) {
      this._renderTooltip();
    }
  },

  _generateForm: function () {
    const isDisabled = !this._isNew();

    const schema = {
      name: {
        type: 'Text',
        title: 'Name',
        validators: ['required'],
        hideValidationErrors: true,
        editorAttrs: {
          disabled: isDisabled,
          placeholder: 'Your API key name',
          id: 'js-api-key-name'
        }
      },
      token: {
        type: 'Text',
        title: 'API Key',
        hasCopyButton: isDisabled,
        editorAttrs: {
          disabled: true
        }
      },
      apis: {
        type: 'MultiCheckbox',
        title: 'APIs',
        validators: ['required'],
        hideValidationErrors: true,
        fieldClass: 'u-iBlock',
        optional: true,
        inputs: [
          { name: API_TYPES.sql, label: 'SQL' },
          { name: API_TYPES.maps, label: 'MAPS' }
        ],
        editorAttrs: {
          disabled: isDisabled
        }
      },
      datasets: {
        type: 'MultiCheckbox',
        title: 'Datasets',
        hideValidationErrors: true,
        optional: true,
        inputs: [
          { name: DATASET_SCOPE_TYPES.create, label: 'CREATE' },
          { name: DATASET_SCOPE_TYPES.listing, label: 'LISTING' }
        ],
        editorAttrs: {
          disabled: isDisabled
        }
      }
    };

    this._formView = new Backbone.Form({ model: this._apiKeyModel, schema });

    return this._formView;
  },

  _isNew: function () {
    return !this._apiKeyModel.get('id');
  },

  _onClickBack: function () {
    this._stackLayoutModel.goToStep(0);
  },

  _renderTooltip: function () {
    this._validationTooltip = new TipsyTooltipView({
      el: this.$('.js-submit'),
      gravity: 's',
      title: () => 'Name and Datasets fields are required (Choose SQL or MAPS for specific permissions in selected datasets)'
    });
    this.addView(this._validationTooltip);
  },

  _hasErrors: function () {
    const formErrors = this._formView.validate();
    const selectedDatasetsPermissions = this._apiKeyModel.hasPermissionsSelected();
    const selectedApis = _.some(this._formView.getValue().apis);
    const selectedCreate = this._formView.getValue().datasets.create;
    const selectedListing = this._formView.getValue().datasets.listing;
    const selectedPermissions =
      (selectedDatasetsPermissions && selectedApis) ||
      selectedCreate ||
      (selectedListing && !selectedApis);

    return !!formErrors || !selectedPermissions;
  },

  _addApiKeyNameError: function (message) {
    this._errorTooltip = new TipsyTooltipView({
      el: this.$('#js-api-key-name'),
      gravity: 'w',
      title: () => message
    });
    this.addView(this._errorTooltip);
    this._errorTooltip.showTipsy();

    this.$('#js-api-key-name').addClass('has-error');
  },

  _onFormChanged: function () {
    this._handleFormErrors();
    this._handleCheckboxState();
    this.$('.js-error').hide();
  },

  _onChangeCreateCheckbox: function () {
    const createCheckbox = this._formView.getValue().datasets.create;

    if (createCheckbox) {
      const apisValues = _.clone(this._formView.getValue().apis);
      apisValues.sql = createCheckbox;
      this._formView.setValue('apis', apisValues);
      this.$('input#sql').prop('checked', createCheckbox);
    }
    this._onFormChanged();
  },

  _onChangeSqlCheckbox: function () {
    const SqlCheckbox = this._formView.getValue().apis.sql;

    if (!SqlCheckbox) {
      const datasetsValues = _.clone(this._formView.getValue().datasets);
      datasetsValues.create = SqlCheckbox;
      this._formView.setValue('datasets', datasetsValues);
      this.$('input#create').prop('checked', SqlCheckbox);
    }
    this._onFormChanged();
  },

  _handleCheckboxState: function () {
    const apis = this._isNew()
      ? this._formView.getValue().apis
      : this._apiKeyModel.get('apis');
    const activeApis = _.keys(apis).filter(name => apis[name]);
    const disableCheckboxes = activeApis.length === 1 && _.contains(activeApis, API_TYPES.maps);

    this.$('.ApiKeysForm-grantsTable').toggleClass('showOnlySelect', disableCheckboxes);
  },

  _handleFormErrors: function () {
    const hasErrors = this._hasErrors();

    this.$('.js-submit').toggleClass('is-disabled', hasErrors);

    if (hasErrors) {
      this._validationTooltip || this._renderTooltip();
    } else {
      this._validationTooltip && this._validationTooltip.clean();
      this._validationTooltip = null;
    }

    this.$('#js-api-key-name').removeClass('has-error');
    this._errorTooltip && this._errorTooltip.clean();
  },

  _onFormSubmit: function (event) {
    const saveButton = this.$('.js-submit');
    const isButtonLoading = saveButton.hasClass('is-loading');

    if (this._hasErrors() || isButtonLoading) return;

    saveButton.addClass('is-loading');

    const errors = this._formView.commit({ validate: true });

    if (errors) return;

    this._apiKeysCollection.create(this._apiKeyModel.attributes, {
      success: (model) => {
        this._apiKeyModel = model;
        this._generateForm();
        this.render();
      },
      error: (model, request) => {
        this._handleServerErrors(request.responseText);
        saveButton.removeClass('is-loading');
      },
      userModel: this._userModel
    });
  },

  _handleServerErrors: function (error) {
    let message;

    if (error.indexOf('Name has already been taken') !== -1) {
      message = 'Name already exists';
    } else if (error.indexOf('Name can\'t be blank') !== -1) {
      message = 'Name can\'t be blank';
    }

    message && this._addApiKeyNameError(message);

    if (!message) {
      this._displayUnhandledError(error);
    }
  },

  _displayUnhandledError: function (errorJSON) {
    const parsed = JSON.parse(errorJSON);
    this.$('.js-error').text(parsed.errors).show();
  },

  clean: function () {
    this._userTablesModel.clearParams();
    CoreView.prototype.clean.apply(this, arguments);
  }
});