text: // Copyright (C) 2016 The Android Open Source Project
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  // http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  (function() {
    'use strict';
  
    // TODO(davido): Add the rest of the change actions.
    var ChangeActions = {
      ABANDON: 'abandon',
      DELETE: '/',
      RESTORE: 'restore',
      REVERT: 'revert',
    };
  
    // TODO(andybons): Add the rest of the revision actions.
    var RevisionActions = {
      CHERRYPICK: 'cherrypick',
      DELETE: '/',
      PUBLISH: 'publish',
      REBASE: 'rebase',
      SUBMIT: 'submit',
    };
  
    var ActionLoadingLabels = {
      'abandon': 'Abandoning...',
      'cherrypick': 'Cherry-Picking...',
      'delete': 'Deleting...',
      'publish': 'Publishing...',
      'rebase': 'Rebasing...',
      'restore': 'Restoring...',
      'revert': 'Reverting...',
      'submit': 'Submitting...',
    };
  
    var ActionType = {
      CHANGE: 'change',
      REVISION: 'revision',
    };
  
    var ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_';
  
    Polymer({
      is: 'gr-change-actions',
  
      /**
       * Fired when the change should be reloaded.
       *
       * @event reload-change
       */
  
      properties: {
        change: Object,
        actions: {
          type: Object,
          value: function() { return {}; },
        },
        primaryActionKeys: {
          type: Array,
          value: function() {
            return [
              RevisionActions.PUBLISH,
              RevisionActions.SUBMIT,
            ];
          },
        },
        changeNum: String,
        patchNum: String,
        commitMessage: {
          type: String,
          value: '',
        },
  
        _loading: {
          type: Boolean,
          value: true,
        },
        _revisionActions: {
          type: Object,
          value: function() { return {}; },
        },
        _revisionActionValues: {
          type: Array,
          computed: '_computeRevisionActionValues(_revisionActions.*, ' +
              'primaryActionKeys.*, _additionalActions.*)',
        },
        _changeActionValues: {
          type: Array,
          computed: '_computeChangeActionValues(actions.*, ' +
              'primaryActionKeys.*, _additionalActions.*)',
        },
        _additionalActions: {
          type: Array,
          value: function() { return []; },
        },
        _hiddenChangeActions: {
          type: Array,
          value: function() { return []; },
        },
        _hiddenRevisionActions: {
          type: Array,
          value: function() { return []; },
        },
      },
  
      ActionType: ActionType,
      ChangeActions: ChangeActions,
      RevisionActions: RevisionActions,
  
      behaviors: [
        Gerrit.RESTClientBehavior,
      ],
  
      observers: [
        '_actionsChanged(actions.*, _revisionActions.*, _additionalActions.*)',
      ],
  
      ready: function() {
        this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
      },
  
      reload: function() {
        if (!this.changeNum || !this.patchNum) {
          return Promise.resolve();
        }
  
        this._loading = true;
        return this._getRevisionActions().then(function(revisionActions) {
          if (!revisionActions) { return; }
  
          this._revisionActions = revisionActions;
          this._loading = false;
        }.bind(this)).catch(function(err) {
          alert('Couldn’t load revision actions. Check the console ' +
              'and contact the PolyGerrit team for assistance.');
          this._loading = false;
          throw err;
        }.bind(this));
      },
  
      addActionButton: function(type, label) {
        if (type !== ActionType.CHANGE && type !== ActionType.REVISION) {
          throw Error('Invalid action type: ' + type);
        }
        var action = {
          enabled: true,
          label: label,
          __type: type,
          __key: ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36),
        };
        this.push('_additionalActions', action);
        return action.__key;
      },
  
      removeActionButton: function(key) {
        var idx = this._indexOfActionButtonWithKey(key);
        if (idx === -1) {
          return;
        }
        this.splice('_additionalActions', idx, 1);
      },
  
      setActionButtonProp: function(key, prop, value) {
        this.set([
          '_additionalActions',
          this._indexOfActionButtonWithKey(key),
          prop,
        ], value);
      },
  
      setActionHidden: function(type, key, hidden) {
        var path;
        if (type === ActionType.CHANGE) {
          path = '_hiddenChangeActions';
        } else if (type === ActionType.REVISION) {
          path = '_hiddenRevisionActions';
        } else {
          throw Error('Invalid action type given: ' + type);
        }
  
        var idx = this.get(path).indexOf(key);
        if (hidden && idx === -1) {
          this.push(path, key);
        } else if (!hidden && idx !== -1) {
          this.splice(path, idx, 1);
        }
      },
  
      _indexOfActionButtonWithKey: function(key) {
        for (var i = 0; i < this._additionalActions.length; i++) {
          if (this._additionalActions[i].__key === key) {
            return i;
          }
        }
        return -1;
      },
  
      _getRevisionActions: function() {
        return this.$.restAPI.getChangeRevisionActions(this.changeNum,
            this.patchNum);
      },
  
      _actionCount: function(actionsChangeRecord, additionalActionsChangeRecord) {
        var additionalActions = (additionalActionsChangeRecord &&
            additionalActionsChangeRecord.base) || [];
        return this._keyCount(actionsChangeRecord) + additionalActions.length;
      },
  
      _keyCount: function(changeRecord) {
        return Object.keys((changeRecord && changeRecord.base) || {}).length;
      },
  
      _actionsChanged: function(actionsChangeRecord, revisionActionsChangeRecord,
          additionalActionsChangeRecord) {
        var additionalActions = (additionalActionsChangeRecord &&
            additionalActionsChangeRecord.base) || [];
        this.hidden = this._keyCount(actionsChangeRecord) === 0 &&
            this._keyCount(revisionActionsChangeRecord) === 0 &&
                additionalActions.length === 0;
      },
  
      _getValuesFor: function(obj) {
        return Object.keys(obj).map(function(key) {
          return obj[key];
        });
      },
  
      _computeRevisionActionValues: function(actionsChangeRecord,
          primariesChangeRecord, additionalActionsChangeRecord) {
        return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
            additionalActionsChangeRecord, ActionType.REVISION);
      },
  
      _computeChangeActionValues: function(actionsChangeRecord,
          primariesChangeRecord, additionalActionsChangeRecord) {
        return this._getActionValues(actionsChangeRecord, primariesChangeRecord,
            additionalActionsChangeRecord, ActionType.CHANGE);
      },
  
      _getActionValues: function(actionsChangeRecord, primariesChangeRecord,
          additionalActionsChangeRecord, type) {
        if (!actionsChangeRecord || !primariesChangeRecord) { return []; }
  
        var actions = actionsChangeRecord.base || {};
        var primaryActionKeys = primariesChangeRecord.base || [];
        var result = [];
        var values = this._getValuesFor(
            type === ActionType.CHANGE ? ChangeActions : RevisionActions);
        for (var a in actions) {
          if (values.indexOf(a) === -1) { continue; }
          actions[a].__key = a;
          actions[a].__type = type;
          actions[a].__primary = primaryActionKeys.indexOf(a) !== -1;
          if (actions[a].label === 'Delete') {
            // This label is common within change and revision actions. Make it
            // more explicit to the user.
            if (type === ActionType.CHANGE) {
              actions[a].label += ' Change';
            } else if (type === ActionType.REVISION) {
              actions[a].label += ' Revision';
            }
          }
          // Triggers a re-render by ensuring object inequality.
          // TODO(andybons): Polyfill for Object.assign.
          result.push(Object.assign({}, actions[a]));
        }
  
        var additionalActions = (additionalActionsChangeRecord &&
        additionalActionsChangeRecord.base) || [];
        additionalActions = additionalActions.filter(function(a) {
          return a.__type === type;
        }).map(function(a) {
          a.__primary = primaryActionKeys.indexOf(a.__key) !== -1;
          // Triggers a re-render by ensuring object inequality.
          // TODO(andybons): Polyfill for Object.assign.
          return Object.assign({}, a);
        });
        return result.concat(additionalActions);
      },
  
      _computeLoadingLabel: function(action) {
        return ActionLoadingLabels[action] || 'Working...';
      },
  
      _canSubmitChange: function() {
        return this.$.jsAPI.canSubmitChange(this.change,
            this._getRevision(this.change, this.patchNum));
      },
  
      _computeActionHidden: function(key, hiddenActionsChangeRecord) {
        var hiddenActions =
            (hiddenActionsChangeRecord && hiddenActionsChangeRecord.base) || [];
        return hiddenActions.indexOf(key) !== -1;
      },
  
      _getRevision: function(change, patchNum) {
        var num = window.parseInt(patchNum, 10);
        for (var hash in change.revisions) {
          var rev = change.revisions[hash];
          if (rev._number === num) {
            return rev;
          }
        }
        return null;
      },
  
      _modifyRevertMsg: function() {
        return this.$.jsAPI.modifyRevertMsg(this.change,
            this.$.confirmRevertDialog.message, this.commitMessage);
      },
  
      showRevertDialog: function() {
        this.$.confirmRevertDialog.populateRevertMessage(
            this.commitMessage, this.change.current_revision);
        this.$.confirmRevertDialog.message = this._modifyRevertMsg();
        this._showActionDialog(this.$.confirmRevertDialog);
      },
  
      _handleActionTap: function(e) {
        e.preventDefault();
        var el = Polymer.dom(e).rootTarget;
        var key = el.getAttribute('data-action-key');
        if (key.indexOf(ADDITIONAL_ACTION_KEY_PREFIX) === 0) {
          this.fire(key + '-tap', {node: el});
          return;
        }
        var type = el.getAttribute('data-action-type');
        if (type === ActionType.REVISION) {
          this._handleRevisionAction(key);
        } else if (key === ChangeActions.REVERT) {
          this.showRevertDialog();
        } else if (key === ChangeActions.ABANDON) {
          this._showActionDialog(this.$.confirmAbandonDialog);
        } else {
          this._fireAction(this._prependSlash(key), this.actions[key], false);
        }
      },
  
      _handleRevisionAction: function(key) {
        switch (key) {
          case RevisionActions.REBASE:
            this._showActionDialog(this.$.confirmRebase);
            break;
          case RevisionActions.CHERRYPICK:
            this.$.confirmCherrypick.branch = '';
            this._showActionDialog(this.$.confirmCherrypick);
            break;
          case RevisionActions.SUBMIT:
            if (!this._canSubmitChange()) {
              return;
            }
            /* falls through */ // required by JSHint
          default:
            this._fireAction(this._prependSlash(key),
                this._revisionActions[key], true);
        }
      },
  
      _prependSlash: function(key) {
        return key === '/' ? key : '/' + key;
      },
  
      _handleConfirmDialogCancel: function() {
        this._hideAllDialogs();
      },
  
      _hideAllDialogs: function() {
        var dialogEls =
            Polymer.dom(this.root).querySelectorAll('.confirmDialog');
        for (var i = 0; i < dialogEls.length; i++) {
          dialogEls[i].hidden = true;
        }
        this.$.overlay.close();
      },
  
      _handleRebaseConfirm: function() {
        var payload = {};
        var el = this.$.confirmRebase;
        if (el.clearParent) {
          // There is a subtle but important difference between setting the base
          // to an empty string and omitting it entirely from the payload. An
          // empty string implies that the parent should be cleared and the
          // change should be rebased on top of the target branch. Leaving out
          // the base implies that it should be rebased on top of its current
          // parent.
          payload.base = '';
        } else if (el.base && el.base.length > 0) {
          payload.base = el.base;
        }
        this.$.overlay.close();
        el.hidden = true;
        this._fireAction('/rebase', this._revisionActions.rebase, true, payload);
      },
  
      _handleCherrypickConfirm: function() {
        var el = this.$.confirmCherrypick;
        if (!el.branch) {
          // TODO(davido): Fix error handling
          alert('The destination branch can’t be empty.');
          return;
        }
        if (!el.message) {
          alert('The commit message can’t be empty.');
          return;
        }
        this.$.overlay.close();
        el.hidden = true;
        this._fireAction(
            '/cherrypick',
            this._revisionActions.cherrypick,
            true,
            {
              destination: el.branch,
              message: el.message,
            }
        );
      },
  
      _handleRevertDialogConfirm: function() {
        var el = this.$.confirmRevertDialog;
        this.$.overlay.close();
        el.hidden = true;
        this._fireAction('/revert', this.actions.revert, false,
            {message: el.message});
      },
  
      _handleAbandonDialogConfirm: function() {
        var el = this.$.confirmAbandonDialog;
        this.$.overlay.close();
        el.hidden = true;
        this._fireAction('/abandon', this.actions.abandon, false,
            {message: el.message});
      },
  
      _setLoadingOnButtonWithKey: function(key) {
        var buttonEl = this.$$('[data-action-key="' + key + '"]');
        buttonEl.setAttribute('loading', true);
        buttonEl.disabled = true;
        return function() {
          buttonEl.removeAttribute('loading');
          buttonEl.disabled = false;
        };
      },
  
      _fireAction: function(endpoint, action, revAction, opt_payload) {
        var cleanupFn = this._setLoadingOnButtonWithKey(action.__key);
  
        this._send(action.method, opt_payload, endpoint, revAction, cleanupFn)
            .then(this._handleResponse.bind(this, action));
      },
  
      _showActionDialog: function(dialog) {
        this._hideAllDialogs();
  
        dialog.hidden = false;
        this.$.overlay.open().then(function() {
          if (dialog.resetFocus) {
            dialog.resetFocus();
          }
        });
      },
  
      // TODO(rmistry): Redo this after
      // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved.
      _setLabelValuesOnRevert: function(newChangeId) {
        var labels = this.$.jsAPI.getLabelValuesPostRevert(this.change);
        if (labels) {
          var url = '/changes/' + newChangeId + '/revisions/current/review';
          this.$.restAPI.send(this.actions.revert.method, url, {labels: labels});
        }
      },
  
      _handleResponse: function(action, response) {
        return this.$.restAPI.getResponseObject(response).then(function(obj) {
          switch (action.__key) {
            case ChangeActions.REVERT:
              this._setLabelValuesOnRevert(obj.change_id);
              // Fall through.
            case RevisionActions.CHERRYPICK:
              page.show(this.changePath(obj._number));
              break;
            case ChangeActions.DELETE:
            case RevisionActions.DELETE:
              if (action.__type === ActionType.CHANGE) {
                page.show('/');
              } else {
                page.show(this.changePath(this.changeNum));
              }
              break;
            default:
              this.fire('reload-change', null, {bubbles: false});
              break;
          }
        }.bind(this));
      },
  
      _handleResponseError: function(response) {
        if (response.ok) { return response; }
  
        return response.text().then(function(errText) {
          alert('Could not perform action: ' + errText);
          throw Error(errText);
        });
      },
  
      _send: function(method, payload, actionEndpoint, revisionAction,
          cleanupFn) {
        var url = this.$.restAPI.getChangeActionURL(this.changeNum,
            revisionAction ? this.patchNum : null, actionEndpoint);
        return this.$.restAPI.send(method, url, payload).then(function(response) {
          cleanupFn.call(this);
          return response;
        }.bind(this)).then(this._handleResponseError.bind(this));
      },
    });
  })();
  