<commit_before>import _ from 'lodash';
import BigNumber from 'bignumber.js';
import {Widget, Inject, Intent} from 'interstellar-core';
import {Alert, AlertGroup} from 'interstellar-ui-messages';
import {Account, Asset, Keypair, Memo, Operation, TransactionBuilder, xdr} from 'stellar-base';
import {FederationServer} from 'stellar-sdk';
import BasicClientError from '../errors';
import StellarLedger from 'stellar-ledger-api';

@Widget('send', 'SendWidgetController', 'interstellar-basic-client/send-widget')
@Inject("$scope", "$rootScope", '$sce', "interstellar-sessions.Sessions", "interstellar-network.Server", "interstellar-ui-messages.Alerts")
export default class SendWidgetController {
  constructor($scope, $rootScope, $sce, Sessions, Server, Alerts) {
    if (!Sessions.hasDefault()) {
      console.error('No session');
      return;
    }

    this.view = 'sendSetup';
    this.$scope = $scope;
    this.$rootScope = $rootScope;
    this.Server = Server;
    this.Sessions = Sessions;
    this.session = Sessions.default;
    this.rocketImage = require('../images/sending.gif');
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
    this.memoBlocked = false;
    this.stellarAddress = null;
    // Resolved destination (accountId/address)
    this.destination = null;

    this.$scope.$watch('widget.memoType', type => {
      switch (type) {
        case 'MEMO_ID':
          this.memoPlaceholder = 'Enter memo ID number';
          break;
        case 'MEMO_TEXT':
          this.memoPlaceholder = 'Up to 28 characters';
          break;
        case 'MEMO_HASH':
        case 'MEMO_RETURN':
          this.memoPlaceholder = 'Enter 64 character encoded string';
          break;
      }
    });

    this.addressAlertGroup = new AlertGroup();
    this.addressAlertGroup.registerUpdateListener(alerts => {
      this.addressAlerts = alerts;
    });
    Alerts.registerGroup(this.addressAlertGroup);

    this.amountAlertGroup = new AlertGroup();
    this.amountAlertGroup.registerUpdateListener(alerts => {
      // Some amount alert messages contain HTML
      for (let alert of alerts) {
        alert._text = $sce.trustAsHtml(alert._text)
      }
      this.amountAlerts = alerts;
    });
    Alerts.registerGroup(this.amountAlertGroup);

    this.memoAlertGroup = new AlertGroup();
    this.memoAlertGroup.registerUpdateListener(alerts => {
      this.memoAlerts = alerts;
    });
    Alerts.registerGroup(this.memoAlertGroup);

    this.useLedger = this.session.data && this.session.data['useLedger'];
    this.bip32Path = this.session.data['bip32Path'];
  }

  loadDestination($event) {
    this.loadingDestination = true;
    this.addressAlertGroup.clear();

    let resetState = () => {
      this.destination = null;
      this.stellarAddress = null;
      this.loadingDestination = false;
      this.memoBlocked = false;

    };

    if (!this.destinationAddress) {
      resetState();
      return;
    }

    FederationServer.resolve(this.destinationAddress)
      .then(value => {
        this.destination = value.account_id;

        if (this.destinationAddress == this.destination) {
          this.stellarAddress = null;
        } else {
          this.stellarAddress = this.destinationAddress;
        }

        switch (value.memo_type) {
          case 'id':
            value.memo_type = 'MEMO_ID';
            break;
          case 'text':
            value.memo_type = 'MEMO_TEXT';
            break;
          case 'hash':
            value.memo_type = 'MEMO_HASH';
            break;
          default:
            delete value.memo;
            delete value.memo_type;
        }

        if (value.memo_type && value.memo) {
          this.memo = true;
          this.memoType = value.memo_type;
          this.memoValue = value.memo;
          this.memoBlocked = true;
        } else {
          this.memoBlocked = false;

        }
        this.loadingDestination = false;
        this.$scope.$apply();
      })
      .catch(error => {
        let alert;
        if (this.destinationAddress.indexOf('*') < 0) {
          alert = new Alert({
            title: 'Invalid public key.',
            text: 'Public keys are uppercase and begin with the letter "G."',
            type: Alert.TYPES.ERROR
          });
        } else {
          alert = new Alert({
            title: 'Stellar address cannot be found or is invalid.',
            text: '',
            type: Alert.TYPES.ERROR
          });
        }
        this.addressAlertGroup.show(alert);
        resetState();
        this.$scope.$apply();
      });
  }

  showMemo($event) {
    $event.preventDefault();
    this.memo = true;
    this.memoType = 'MEMO_TEXT';
    this.memoValue = null;
  }

  hideMemo($event) {
    if ($event) {
      $event.preventDefault();
    }
    this.memoAlertGroup.clear();
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
  }

  showView($event, v) {
    if ($event) {
      $event.preventDefault();
    }
    this.view = v;
  }

  send() {
    if (this.loadingDestination) {
      return false;
    }

    this.sending = true;
    this.addressAlertGroup.clear();
    this.amountAlertGroup.clear();
    this.memoAlertGroup.clear();

    if (!Account.isValidAccountId(this.destination)) {
      let alert = new Alert({
        title: 'Stellar address or public key is invalid.',
        text: 'Public keys are uppercase and begin with the letter "G."',
        type: Alert.TYPES.ERROR
      });
      this.addressAlertGroup.show(alert);
    }

    // Check if amount is valid
    if (!Operation.isValidAmount(this.amount)) {
      let alert = new Alert({
        title: '',
        text: 'This amount is invalid.',
        type: Alert.TYPES.ERROR
      });
      this.amountAlertGroup.show(alert);
    }

    if (this.memo) {
      let memo, memoError;
      try {
        switch (this.memoType) {
          case 'MEMO_TEXT':
            memoError = 'MEMO_TEXT must contain a maximum of 28 characters';
            memo = Memo.text(this.memoValue);
            break;
          case 'MEMO_ID':
            memoError = 'MEMO_ID must be a valid 64 bit unsigned integer';
            memo = Memo.id(this.memoValue);
            break;
          case 'MEMO_HASH':
            memoError = 'MEMO_HASH must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.hash(this.memoValue);
            break;
          case 'MEMO_RETURN':
            memoError = 'MEMO_RETURN must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.returnHash(this.memoValue);
            break;
        }
      } catch (error) {
        let alert = new Alert({
          title: '',
          text: memoError,
          type: Alert.TYPES.ERROR
        });
        this.memoAlertGroup.show(alert);
      }
    }

    if (this.addressAlerts.length || this.amountAlerts.length || this.memoAlerts.length) {
      this.sending = false;
      return;
    }

    return this.Server.accounts()
      .accountId(this.session.address)
      .call()
      .then(account => {
        // Check if sending this transaction would make balance go below minimum balance
        let minimumBalance = 20 + (account.subentry_count) * 10;
        let nativeBalance = _(account.balances).find(balance => balance.asset_type === 'native').balance;
        let maxSend = new BigNumber(nativeBalance).minus(minimumBalance);
        if (maxSend.lt(this.amount)) {
          throw new BasicClientError('InsufficientBalanceError', {maxSend});
        }
      })
      .then(() => {
        // Check if destination account exists. If no, at least 20 XLM must be sent.
        if (new BigNumber(this.amount).gte(20)) {
          return;
        }

        return this.Server.accounts()
          .accountId(this.destination)
          .call()
          .catch(err => {
            if (err.name === 'NotFoundError') {
              throw new BasicClientError('DestinationAccountNotExistError');
            }
          });
      })
      .then(() => {
        this.displayedAmount = new BigNumber(this.amount).toFormat();
        this.showView(null, 'sendConfirm');
      })
      .catch(err => {
        let alert;
        switch (err.name) {
          case 'NotFoundError':
            alert = new Alert({
              title: '',
              text: 'Your account isn\'t funded.',
              type: Alert.TYPES.ERROR
            });
            break;
          case 'InsufficientBalanceError':
            alert = new Alert({
              title: 'Insufficient balance.',
              text:
                `To maintain your <a href="https://www.stellar.org/developers/learn/concepts/fees.html#minimum-balance" target="_blank">minimum balance</a>, the most you can currently send is ${err.data.maxSend}.`,
              type: Alert.TYPES.ERROR
            });
            break;
          case 'DestinationAccountNotExistError':
            alert = new Alert({
              title: 'Destination account doesn\'t exist.',
              text: 'You account must send at least 20 lumens to create an account.',
              type: Alert.TYPES.ERROR
            });
            break;
          default:
            alert = new Alert({
              title: 'Unknown error: '+err.name,
              text: '',
              type: Alert.TYPES.ERROR
            });
            break;
        }
        this.amountAlertGroup.show(alert);
      })
      .finally(() => {
        this.sending = false;
        this.$scope.$apply();
      });
  }

  confirm() {
    this.showView(null, 'sendWaiting');
    return this.Server.accounts()
      .accountId(this.destination)
      .call()
      .then(() => {
        // Account exist. Send payment operation.
        let operation = Operation.payment({
          destination: this.destination,
          asset: Asset.native(),
          amount: this.amount
        });
        return this._submitTransaction(operation);
      })
      .catch(err => {
        if (err.name === 'NotFoundError') {
          // Account does not exist. Send create_account operation.
          let operation = Operation.createAccount({
            destination: this.destination,
            startingBalance: this.amount
          });
          return this._submitTransaction(operation);
        } else {
          throw err;
        }
      });
  }

  _submitTransaction(operation) {

    return this.Sessions.loadDefaultAccount()
      .then(() => {
        let memo = Memo.none();

        if (this.memo) {
          switch (this.memoType) {
            case 'MEMO_TEXT':
              memo = Memo.text(this.memoValue);
              break;
            case 'MEMO_ID':
              memo = Memo.id(this.memoValue);
              break;
            case 'MEMO_HASH':
              memo = Memo.hash(this.memoValue);
              break;
            case 'MEMO_RETURN':
              memo = Memo.returnHash(this.memoValue);
              break;
          }
        }

        let transaction = new TransactionBuilder(this.session.getAccount())
          .addOperation(operation)
          .addMemo(memo)
          .build();

        if (this.useLedger) {
          let ledgerApi = new StellarLedger.Api(new StellarLedger.comm(120));
          return ledgerApi.signTx_async(this.bip32Path, transaction).then(result => {
            let signature = result['signature'];
            let keyPair = Keypair.fromAccountId(this.session.address);
            let hint = keyPair.signatureHint();
            let decorated = new xdr.DecoratedSignature({hint, signature});
            transaction.signatures.push(decorated);
            return this.Server.submitTransaction(transaction);
          }).catch(e => {
            this.ledgerError = e;
            throw e;
          });
        } else {
          transaction.sign(Keypair.fromSeed(this.session.getSecret()));
          return this.Server.submitTransaction(transaction);
        }
      })
      .then(() => {
        this.success = true;
        this.destinationAddress = null;
        this.destination = null;
        this.amount = null;
        this.memo = false;
        this.$rootScope.$broadcast('account-viewer.transaction-success');
      })
      .catch(e => {
        this.success = false;
        if (this.ledgerError !== null) {
          this.outcomeMessage = this.ledgerError;
        } else {
          this.outcomeMessage = JSON.stringify(e, null, '  ');
        }
      })
      .finally(() => {
        this.showView(null, 'sendOutcome');
        this.$scope.$apply()
      });
  }
}
<commit_msg>Fix SendController for logins using secret key<commit_after>import _ from 'lodash';
import BigNumber from 'bignumber.js';
import {Widget, Inject, Intent} from 'interstellar-core';
import {Alert, AlertGroup} from 'interstellar-ui-messages';
import {Account, Asset, Keypair, Memo, Operation, TransactionBuilder, xdr} from 'stellar-base';
import {FederationServer} from 'stellar-sdk';
import BasicClientError from '../errors';
import StellarLedger from 'stellar-ledger-api';

@Widget('send', 'SendWidgetController', 'interstellar-basic-client/send-widget')
@Inject("$scope", "$rootScope", '$sce', "interstellar-sessions.Sessions", "interstellar-network.Server", "interstellar-ui-messages.Alerts")
export default class SendWidgetController {
  constructor($scope, $rootScope, $sce, Sessions, Server, Alerts) {
    if (!Sessions.hasDefault()) {
      console.error('No session');
      return;
    }

    this.view = 'sendSetup';
    this.$scope = $scope;
    this.$rootScope = $rootScope;
    this.Server = Server;
    this.Sessions = Sessions;
    this.session = Sessions.default;
    this.rocketImage = require('../images/sending.gif');
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
    this.memoBlocked = false;
    this.stellarAddress = null;
    // Resolved destination (accountId/address)
    this.destination = null;

    this.$scope.$watch('widget.memoType', type => {
      switch (type) {
        case 'MEMO_ID':
          this.memoPlaceholder = 'Enter memo ID number';
          break;
        case 'MEMO_TEXT':
          this.memoPlaceholder = 'Up to 28 characters';
          break;
        case 'MEMO_HASH':
        case 'MEMO_RETURN':
          this.memoPlaceholder = 'Enter 64 character encoded string';
          break;
      }
    });

    this.addressAlertGroup = new AlertGroup();
    this.addressAlertGroup.registerUpdateListener(alerts => {
      this.addressAlerts = alerts;
    });
    Alerts.registerGroup(this.addressAlertGroup);

    this.amountAlertGroup = new AlertGroup();
    this.amountAlertGroup.registerUpdateListener(alerts => {
      // Some amount alert messages contain HTML
      for (let alert of alerts) {
        alert._text = $sce.trustAsHtml(alert._text)
      }
      this.amountAlerts = alerts;
    });
    Alerts.registerGroup(this.amountAlertGroup);

    this.memoAlertGroup = new AlertGroup();
    this.memoAlertGroup.registerUpdateListener(alerts => {
      this.memoAlerts = alerts;
    });
    Alerts.registerGroup(this.memoAlertGroup);

    this.useLedger = this.session.data && this.session.data['useLedger'];
    this.bip32Path = this.session.data && this.session.data['bip32Path'];
  }

  loadDestination($event) {
    this.loadingDestination = true;
    this.addressAlertGroup.clear();

    let resetState = () => {
      this.destination = null;
      this.stellarAddress = null;
      this.loadingDestination = false;
      this.memoBlocked = false;

    };

    if (!this.destinationAddress) {
      resetState();
      return;
    }

    FederationServer.resolve(this.destinationAddress)
      .then(value => {
        this.destination = value.account_id;

        if (this.destinationAddress == this.destination) {
          this.stellarAddress = null;
        } else {
          this.stellarAddress = this.destinationAddress;
        }

        switch (value.memo_type) {
          case 'id':
            value.memo_type = 'MEMO_ID';
            break;
          case 'text':
            value.memo_type = 'MEMO_TEXT';
            break;
          case 'hash':
            value.memo_type = 'MEMO_HASH';
            break;
          default:
            delete value.memo;
            delete value.memo_type;
        }

        if (value.memo_type && value.memo) {
          this.memo = true;
          this.memoType = value.memo_type;
          this.memoValue = value.memo;
          this.memoBlocked = true;
        } else {
          this.memoBlocked = false;

        }
        this.loadingDestination = false;
        this.$scope.$apply();
      })
      .catch(error => {
        let alert;
        if (this.destinationAddress.indexOf('*') < 0) {
          alert = new Alert({
            title: 'Invalid public key.',
            text: 'Public keys are uppercase and begin with the letter "G."',
            type: Alert.TYPES.ERROR
          });
        } else {
          alert = new Alert({
            title: 'Stellar address cannot be found or is invalid.',
            text: '',
            type: Alert.TYPES.ERROR
          });
        }
        this.addressAlertGroup.show(alert);
        resetState();
        this.$scope.$apply();
      });
  }

  showMemo($event) {
    $event.preventDefault();
    this.memo = true;
    this.memoType = 'MEMO_TEXT';
    this.memoValue = null;
  }

  hideMemo($event) {
    if ($event) {
      $event.preventDefault();
    }
    this.memoAlertGroup.clear();
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
  }

  showView($event, v) {
    if ($event) {
      $event.preventDefault();
    }
    this.view = v;
  }

  send() {
    if (this.loadingDestination) {
      return false;
    }

    this.sending = true;
    this.addressAlertGroup.clear();
    this.amountAlertGroup.clear();
    this.memoAlertGroup.clear();

    if (!Account.isValidAccountId(this.destination)) {
      let alert = new Alert({
        title: 'Stellar address or public key is invalid.',
        text: 'Public keys are uppercase and begin with the letter "G."',
        type: Alert.TYPES.ERROR
      });
      this.addressAlertGroup.show(alert);
    }

    // Check if amount is valid
    if (!Operation.isValidAmount(this.amount)) {
      let alert = new Alert({
        title: '',
        text: 'This amount is invalid.',
        type: Alert.TYPES.ERROR
      });
      this.amountAlertGroup.show(alert);
    }

    if (this.memo) {
      let memo, memoError;
      try {
        switch (this.memoType) {
          case 'MEMO_TEXT':
            memoError = 'MEMO_TEXT must contain a maximum of 28 characters';
            memo = Memo.text(this.memoValue);
            break;
          case 'MEMO_ID':
            memoError = 'MEMO_ID must be a valid 64 bit unsigned integer';
            memo = Memo.id(this.memoValue);
            break;
          case 'MEMO_HASH':
            memoError = 'MEMO_HASH must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.hash(this.memoValue);
            break;
          case 'MEMO_RETURN':
            memoError = 'MEMO_RETURN must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.returnHash(this.memoValue);
            break;
        }
      } catch (error) {
        let alert = new Alert({
          title: '',
          text: memoError,
          type: Alert.TYPES.ERROR
        });
        this.memoAlertGroup.show(alert);
      }
    }

    if (this.addressAlerts.length || this.amountAlerts.length || this.memoAlerts.length) {
      this.sending = false;
      return;
    }

    return this.Server.accounts()
      .accountId(this.session.address)
      .call()
      .then(account => {
        // Check if sending this transaction would make balance go below minimum balance
        let minimumBalance = 20 + (account.subentry_count) * 10;
        let nativeBalance = _(account.balances).find(balance => balance.asset_type === 'native').balance;
        let maxSend = new BigNumber(nativeBalance).minus(minimumBalance);
        if (maxSend.lt(this.amount)) {
          throw new BasicClientError('InsufficientBalanceError', {maxSend});
        }
      })
      .then(() => {
        // Check if destination account exists. If no, at least 20 XLM must be sent.
        if (new BigNumber(this.amount).gte(20)) {
          return;
        }

        return this.Server.accounts()
          .accountId(this.destination)
          .call()
          .catch(err => {
            if (err.name === 'NotFoundError') {
              throw new BasicClientError('DestinationAccountNotExistError');
            }
          });
      })
      .then(() => {
        this.displayedAmount = new BigNumber(this.amount).toFormat();
        this.showView(null, 'sendConfirm');
      })
      .catch(err => {
        let alert;
        switch (err.name) {
          case 'NotFoundError':
            alert = new Alert({
              title: '',
              text: 'Your account isn\'t funded.',
              type: Alert.TYPES.ERROR
            });
            break;
          case 'InsufficientBalanceError':
            alert = new Alert({
              title: 'Insufficient balance.',
              text:
                `To maintain your <a href="https://www.stellar.org/developers/learn/concepts/fees.html#minimum-balance" target="_blank">minimum balance</a>, the most you can currently send is ${err.data.maxSend}.`,
              type: Alert.TYPES.ERROR
            });
            break;
          case 'DestinationAccountNotExistError':
            alert = new Alert({
              title: 'Destination account doesn\'t exist.',
              text: 'You account must send at least 20 lumens to create an account.',
              type: Alert.TYPES.ERROR
            });
            break;
          default:
            alert = new Alert({
              title: 'Unknown error: '+err.name,
              text: '',
              type: Alert.TYPES.ERROR
            });
            break;
        }
        this.amountAlertGroup.show(alert);
      })
      .finally(() => {
        this.sending = false;
        this.$scope.$apply();
      });
  }

  confirm() {
    this.showView(null, 'sendWaiting');
    return this.Server.accounts()
      .accountId(this.destination)
      .call()
      .then(() => {
        // Account exist. Send payment operation.
        let operation = Operation.payment({
          destination: this.destination,
          asset: Asset.native(),
          amount: this.amount
        });
        return this._submitTransaction(operation);
      })
      .catch(err => {
        if (err.name === 'NotFoundError') {
          // Account does not exist. Send create_account operation.
          let operation = Operation.createAccount({
            destination: this.destination,
            startingBalance: this.amount
          });
          return this._submitTransaction(operation);
        } else {
          throw err;
        }
      });
  }

  _submitTransaction(operation) {

    return this.Sessions.loadDefaultAccount()
      .then(() => {
        let memo = Memo.none();

        if (this.memo) {
          switch (this.memoType) {
            case 'MEMO_TEXT':
              memo = Memo.text(this.memoValue);
              break;
            case 'MEMO_ID':
              memo = Memo.id(this.memoValue);
              break;
            case 'MEMO_HASH':
              memo = Memo.hash(this.memoValue);
              break;
            case 'MEMO_RETURN':
              memo = Memo.returnHash(this.memoValue);
              break;
          }
        }

        let transaction = new TransactionBuilder(this.session.getAccount())
          .addOperation(operation)
          .addMemo(memo)
          .build();

        if (this.useLedger) {
          let ledgerApi = new StellarLedger.Api(new StellarLedger.comm(120));
          return ledgerApi.signTx_async(this.bip32Path, transaction).then(result => {
            let signature = result['signature'];
            let keyPair = Keypair.fromAccountId(this.session.address);
            let hint = keyPair.signatureHint();
            let decorated = new xdr.DecoratedSignature({hint, signature});
            transaction.signatures.push(decorated);
            return this.Server.submitTransaction(transaction);
          }).catch(e => {
            this.ledgerError = e;
            throw e;
          });
        } else {
          transaction.sign(Keypair.fromSeed(this.session.getSecret()));
          return this.Server.submitTransaction(transaction);
        }
      })
      .then(() => {
        this.success = true;
        this.destinationAddress = null;
        this.destination = null;
        this.amount = null;
        this.memo = false;
        this.$rootScope.$broadcast('account-viewer.transaction-success');
      })
      .catch(e => {
        this.success = false;
        if (this.ledgerError !== null) {
          this.outcomeMessage = this.ledgerError;
        } else {
          this.outcomeMessage = JSON.stringify(e, null, '  ');
        }
      })
      .finally(() => {
        this.showView(null, 'sendOutcome');
        this.$scope.$apply()
      });
  }
}
<|endoftext|>


<commit_before>import _ from 'lodash';
import BigNumber from 'bignumber.js';
import {Widget, Inject, Intent} from 'interstellar-core';
import {Alert, AlertGroup} from 'interstellar-ui-messages';
import {Account, Asset, Keypair, Memo, Operation, Transaction, TransactionBuilder, xdr} from 'stellar-sdk';
import {FederationServer} from 'stellar-sdk';
import BasicClientError from '../errors';
import knownAccounts from '../known_accounts';
import StellarLedger from 'stellar-ledger-api';

@Widget('send', 'SendWidgetController', 'interstellar-basic-client/send-widget')
@Inject("$scope", "$rootScope", '$sce', "interstellar-sessions.Sessions", "interstellar-network.Server", "interstellar-ui-messages.Alerts")
export default class SendWidgetController {
  constructor($scope, $rootScope, $sce, Sessions, Server, Alerts) {
    if (!Sessions.hasDefault()) {
      console.error('No session');
      return;
    }

    this.view = 'sendSetup';
    this.$scope = $scope;
    this.$rootScope = $rootScope;
    this.Server = Server;
    this.Sessions = Sessions;
    this.session = Sessions.default;
    this.rocketImage = require('../images/sending.gif');
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
    this.memoBlocked = false;
    this.stellarAddress = null;
    // Resolved destination (accountId/address)
    this.destination = null;
    // XDR of the last transaction submitted to horizon. Will be helpful resubmit is needed.
    this.lastTransactionXDR = null;

    this.$scope.$watch('widget.memoType', type => {
      switch (type) {
        case 'MEMO_ID':
          this.memoPlaceholder = 'Enter memo ID number';
          break;
        case 'MEMO_TEXT':
          this.memoPlaceholder = 'Up to 28 characters';
          break;
        case 'MEMO_HASH':
        case 'MEMO_RETURN':
          this.memoPlaceholder = 'Enter 64 character encoded string';
          break;
      }
    });

    this.addressAlertGroup = new AlertGroup();
    this.addressAlertGroup.registerUpdateListener(alerts => {
      this.addressAlerts = alerts;
    });
    Alerts.registerGroup(this.addressAlertGroup);

    this.amountAlertGroup = new AlertGroup();
    this.amountAlertGroup.registerUpdateListener(alerts => {
      // Some amount alert messages contain HTML
      for (let alert of alerts) {
        alert._text = $sce.trustAsHtml(alert._text)
      }
      this.amountAlerts = alerts;
    });
    Alerts.registerGroup(this.amountAlertGroup);

    this.memoAlertGroup = new AlertGroup();
    this.memoAlertGroup.registerUpdateListener(alerts => {
      this.memoAlerts = alerts;
    });
    Alerts.registerGroup(this.memoAlertGroup);

    this.useLedger = this.session.data && this.session.data['useLedger'];
    this.bip32Path = this.session.data && this.session.data['bip32Path'];
  }

  loadDestination($event) {
    this.loadingDestination = true;
    this.addressAlertGroup.clear();

    let resetState = () => {
      this.destination = null;
      this.stellarAddress = null;
      this.loadingDestination = false;
      this.memoBlocked = false;

    };

    if (!this.destinationAddress) {
      resetState();
      return;
    }

    FederationServer.resolve(this.destinationAddress)
      .then(value => {
        this.destination = value.account_id;

        if (this.destinationAddress == this.destination) {
          this.stellarAddress = null;
        } else {
          this.stellarAddress = this.destinationAddress;
        }

        switch (value.memo_type) {
          case 'id':
            value.memo_type = 'MEMO_ID';
            break;
          case 'text':
            value.memo_type = 'MEMO_TEXT';
            break;
          case 'hash':
            value.memo_type = 'MEMO_HASH';
            break;
          default:
            delete value.memo;
            delete value.memo_type;
        }

        if (value.memo_type && value.memo) {
          this.memo = true;
          this.memoType = value.memo_type;
          this.memoValue = value.memo;
          this.memoBlocked = true;
        } else {
          this.memoBlocked = false;
        }

        if (this.destination in knownAccounts && knownAccounts[this.destination].memo_required) {
          this.memo = true;
        }

        this.loadingDestination = false;
        this.$scope.$apply();
      })
      .catch(error => {
        console.log(error);
        let alert;
        if (this.destinationAddress.indexOf('*') < 0) {
          alert = new Alert({
            title: 'Invalid public key.',
            text: 'Public keys are uppercase and begin with the letter "G."',
            type: Alert.TYPES.ERROR
          });
        } else {
          alert = new Alert({
            title: 'Stellar address cannot be found or is invalid.',
            text: '',
            type: Alert.TYPES.ERROR
          });
        }
        this.addressAlertGroup.show(alert);
        resetState();
        this.$scope.$apply();
      });
  }

  showMemo($event) {
    $event.preventDefault();
    this.memo = true;
    this.memoType = 'MEMO_TEXT';
    this.memoValue = null;
  }

  hideMemo($event) {
    if ($event) {
      $event.preventDefault();
    }
    this.memoAlertGroup.clear();
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
  }

  showView($event, v) {
    if ($event) {
      $event.preventDefault();
    }
    this.view = v;
  }

  send() {
    if (this.loadingDestination) {
      return false;
    }

    this.sending = true;
    this.ledgerError = false;

    this.addressAlertGroup.clear();
    this.amountAlertGroup.clear();
    this.memoAlertGroup.clear();

    if (!Account.isValidAccountId(this.destination)) {
      let alert = new Alert({
        title: 'Stellar address or public key is invalid.',
        text: 'Public keys are uppercase and begin with the letter "G."',
        type: Alert.TYPES.ERROR
      });
      this.addressAlertGroup.show(alert);
    }

    // Check if amount is valid
    if (!Operation.isValidAmount(this.amount)) {
      let alert = new Alert({
        title: '',
        text: 'This amount is invalid.',
        type: Alert.TYPES.ERROR
      });
      this.amountAlertGroup.show(alert);
    }

    // check if the destination requires a memo
    if (this.destination in knownAccounts && knownAccounts[this.destination].memo_required && !this.memoValue) {
      let alert = new Alert({
        title: '',
        text: 'The payment destination (' + knownAccounts[this.destination].name +') requires you to specify a memo to identify your account.',
        type: Alert.TYPES.ERROR
      });
      this.memoAlertGroup.show(alert);
      this.sending = false;
      return;
    }

    if (this.memo) {
      let memo, memoError;
      try {
        switch (this.memoType) {
          case 'MEMO_TEXT':
            memoError = 'MEMO_TEXT must contain a maximum of 28 characters';
            memo = Memo.text(this.memoValue);
            break;
          case 'MEMO_ID':
            memoError = 'MEMO_ID must be a valid 64 bit unsigned integer';
            memo = Memo.id(this.memoValue);
            break;
          case 'MEMO_HASH':
            memoError = 'MEMO_HASH must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.hash(this.memoValue);
            break;
          case 'MEMO_RETURN':
            memoError = 'MEMO_RETURN must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.returnHash(this.memoValue);
            break;
        }
      } catch (error) {
        let alert = new Alert({
          title: '',
          text: memoError,
          type: Alert.TYPES.ERROR
        });
        this.memoAlertGroup.show(alert);
      }
    }

    if (this.addressAlerts.length || this.amountAlerts.length || this.memoAlerts.length) {
      this.sending = false;
      return;
    }

    return this.Server.accounts()
      .accountId(this.session.address)
      .call()
      .then(account => {
        // Check if sending this transaction would make balance go below minimum balance
        let minimumBalance = 20 + (account.subentry_count) * 10;
        let nativeBalance = _(account.balances).find(balance => balance.asset_type === 'native').balance;
        let maxSend = new BigNumber(nativeBalance).minus(minimumBalance);
        if (maxSend.lt(this.amount)) {
          throw new BasicClientError('InsufficientBalanceError', {maxSend});
        }
      })
      .then(() => {
        // Check if destination account exists. If no, at least 20 XLM must be sent.
        if (new BigNumber(this.amount).gte(20)) {
          return;
        }

        return this.Server.accounts()
          .accountId(this.destination)
          .call()
          .catch(err => {
            if (err.name === 'NotFoundError') {
              throw new BasicClientError('DestinationAccountNotExistError');
            }
          });
      })
      .then(() => {
        this.displayedAmount = new BigNumber(this.amount).toFormat();
        this.showView(null, 'sendConfirm');
      })
      .catch(err => {
        let alert;
        switch (err.name) {
          case 'NotFoundError':
            alert = new Alert({
              title: '',
              text: 'Your account isn\'t funded.',
              type: Alert.TYPES.ERROR
            });
            break;
          case 'InsufficientBalanceError':
            alert = new Alert({
              title: 'Insufficient balance.',
              text:
                `To maintain your <a href="https://www.stellar.org/developers/learn/concepts/fees.html#minimum-balance" target="_blank">minimum balance</a>, the most you can currently send is ${err.data.maxSend}.`,
              type: Alert.TYPES.ERROR
            });
            break;
          case 'DestinationAccountNotExistError':
            alert = new Alert({
              title: 'Destination account doesn\'t exist.',
              text: 'You account must send at least 20 lumens to create an account.',
              type: Alert.TYPES.ERROR
            });
            break;
          default:
            alert = new Alert({
              title: 'Unknown error: '+err.name,
              text: '',
              type: Alert.TYPES.ERROR
            });
            break;
        }
        this.amountAlertGroup.show(alert);
      })
      .finally(() => {
        this.sending = false;
        this.$scope.$apply();
      });
  }

  confirm() {
    this.showView(null, 'sendWaiting');
    return this.Server.accounts()
      .accountId(this.destination)
      .call()
      .then(() => {
        // Account exist. Send payment operation.
        let operation = Operation.payment({
          destination: this.destination,
          asset: Asset.native(),
          amount: this.amount
        });
        return this._submitTransaction(operation);
      })
      .catch(err => {
        if (err.name === 'NotFoundError') {
          // Account does not exist. Send create_account operation.
          let operation = Operation.createAccount({
            destination: this.destination,
            startingBalance: this.amount
          });
          return this._submitTransaction(operation);
        } else {
          throw err;
        }
      });
  }

  resubmitTransaction() {
    if (this.lastTransactionXDR === null) {
      return;
    }

    this.showView(null, 'sendWaiting');

    var transaction = new Transaction(this.lastTransactionXDR);

    return this.Server.submitTransaction(transaction)
      .then(this._submitOnSuccess.bind(this))
      .catch(this._submitOnFailure.bind(this))
      .finally(this._submitFinally.bind(this));
  }

  _submitTransaction(operation) {
    return this.Sessions.loadDefaultAccount()
      .then(() => {
        let memo = Memo.none();

        if (this.memo) {
          switch (this.memoType) {
            case 'MEMO_TEXT':
              memo = Memo.text(this.memoValue);
              break;
            case 'MEMO_ID':
              memo = Memo.id(this.memoValue);
              break;
            case 'MEMO_HASH':
              memo = Memo.hash(this.memoValue);
              break;
            case 'MEMO_RETURN':
              memo = Memo.returnHash(this.memoValue);
              break;
          }
        }

        let transaction = new TransactionBuilder(this.session.getAccount())
          .addOperation(operation)
          .addMemo(memo)
          .build();

        if (this.useLedger) {
          let ledgerApi = new StellarLedger.Api(new StellarLedger.comm(120));
          return ledgerApi.signTx_async(this.bip32Path, transaction).then(result => {
            let signature = result['signature'];
            let keyPair = Keypair.fromAccountId(this.session.address);
            let hint = keyPair.signatureHint();
            let decorated = new xdr.DecoratedSignature({hint, signature});
            transaction.signatures.push(decorated);
            return transaction;
          }).catch(e => {
            this.ledgerError = true;
            throw e;
          });
        } else {
          transaction.sign(Keypair.fromSeed(this.session.getSecret()));
          return transaction;
        }
      })
      .then(transaction => {
        this.lastTransactionXDR = transaction.toEnvelope().toXDR().toString("base64");
        return this.Server.submitTransaction(transaction);
      })
      .then(this._submitOnSuccess.bind(this))
      .catch(this._submitOnFailure.bind(this))
      .finally(this._submitFinally.bind(this));
  }

  _submitOnSuccess() {
    this.success = true;
    this.destinationAddress = null;
    this.destination = null;
    this.amount = null;
    this.memo = false;
    this.lastTransactionXDR = null;
    this.$rootScope.$broadcast('account-viewer.transaction-success');
  }

  _submitOnFailure(e) {
    this.success = false;
    this.needsResubmit = false;

    if (this.ledgerError) {
      if (e.errorCode == 2) {
        this.outcomeMessage = `Couldn't connect to Ledger device. Connection can only be established using a secure connection.`;
      } else if (e.errorCode == 5) {
        this.outcomeMessage = `Ledger connection timeout.`;
      } else {
        this.outcomeMessage = 'Unknown error: '+JSON.stringify(e);
      }
    } else {
      if (e.title != "Transaction Failed" && e.title != "Transaction Malformed") {
        this.needsResubmit = true;
      }
      this.outcomeMessage = JSON.stringify(e, null, '  ');
    }
  }

  _submitFinally() {
    this.showView(null, 'sendOutcome');
    this.$scope.$apply()
  }
}
<commit_msg>#48 remove debug logging<commit_after>import _ from 'lodash';
import BigNumber from 'bignumber.js';
import {Widget, Inject, Intent} from 'interstellar-core';
import {Alert, AlertGroup} from 'interstellar-ui-messages';
import {Account, Asset, Keypair, Memo, Operation, Transaction, TransactionBuilder, xdr} from 'stellar-sdk';
import {FederationServer} from 'stellar-sdk';
import BasicClientError from '../errors';
import knownAccounts from '../known_accounts';
import StellarLedger from 'stellar-ledger-api';

@Widget('send', 'SendWidgetController', 'interstellar-basic-client/send-widget')
@Inject("$scope", "$rootScope", '$sce', "interstellar-sessions.Sessions", "interstellar-network.Server", "interstellar-ui-messages.Alerts")
export default class SendWidgetController {
  constructor($scope, $rootScope, $sce, Sessions, Server, Alerts) {
    if (!Sessions.hasDefault()) {
      console.error('No session');
      return;
    }

    this.view = 'sendSetup';
    this.$scope = $scope;
    this.$rootScope = $rootScope;
    this.Server = Server;
    this.Sessions = Sessions;
    this.session = Sessions.default;
    this.rocketImage = require('../images/sending.gif');
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
    this.memoBlocked = false;
    this.stellarAddress = null;
    // Resolved destination (accountId/address)
    this.destination = null;
    // XDR of the last transaction submitted to horizon. Will be helpful resubmit is needed.
    this.lastTransactionXDR = null;

    this.$scope.$watch('widget.memoType', type => {
      switch (type) {
        case 'MEMO_ID':
          this.memoPlaceholder = 'Enter memo ID number';
          break;
        case 'MEMO_TEXT':
          this.memoPlaceholder = 'Up to 28 characters';
          break;
        case 'MEMO_HASH':
        case 'MEMO_RETURN':
          this.memoPlaceholder = 'Enter 64 character encoded string';
          break;
      }
    });

    this.addressAlertGroup = new AlertGroup();
    this.addressAlertGroup.registerUpdateListener(alerts => {
      this.addressAlerts = alerts;
    });
    Alerts.registerGroup(this.addressAlertGroup);

    this.amountAlertGroup = new AlertGroup();
    this.amountAlertGroup.registerUpdateListener(alerts => {
      // Some amount alert messages contain HTML
      for (let alert of alerts) {
        alert._text = $sce.trustAsHtml(alert._text)
      }
      this.amountAlerts = alerts;
    });
    Alerts.registerGroup(this.amountAlertGroup);

    this.memoAlertGroup = new AlertGroup();
    this.memoAlertGroup.registerUpdateListener(alerts => {
      this.memoAlerts = alerts;
    });
    Alerts.registerGroup(this.memoAlertGroup);

    this.useLedger = this.session.data && this.session.data['useLedger'];
    this.bip32Path = this.session.data && this.session.data['bip32Path'];
  }

  loadDestination($event) {
    this.loadingDestination = true;
    this.addressAlertGroup.clear();

    let resetState = () => {
      this.destination = null;
      this.stellarAddress = null;
      this.loadingDestination = false;
      this.memoBlocked = false;

    };

    if (!this.destinationAddress) {
      resetState();
      return;
    }

    FederationServer.resolve(this.destinationAddress)
      .then(value => {
        this.destination = value.account_id;

        if (this.destinationAddress == this.destination) {
          this.stellarAddress = null;
        } else {
          this.stellarAddress = this.destinationAddress;
        }

        switch (value.memo_type) {
          case 'id':
            value.memo_type = 'MEMO_ID';
            break;
          case 'text':
            value.memo_type = 'MEMO_TEXT';
            break;
          case 'hash':
            value.memo_type = 'MEMO_HASH';
            break;
          default:
            delete value.memo;
            delete value.memo_type;
        }

        if (value.memo_type && value.memo) {
          this.memo = true;
          this.memoType = value.memo_type;
          this.memoValue = value.memo;
          this.memoBlocked = true;
        } else {
          this.memoBlocked = false;
        }

        if (this.destination in knownAccounts && knownAccounts[this.destination].memo_required) {
          this.memo = true;
        }

        this.loadingDestination = false;
        this.$scope.$apply();
      })
      .catch(error => {
        let alert;
        if (this.destinationAddress.indexOf('*') < 0) {
          alert = new Alert({
            title: 'Invalid public key.',
            text: 'Public keys are uppercase and begin with the letter "G."',
            type: Alert.TYPES.ERROR
          });
        } else {
          alert = new Alert({
            title: 'Stellar address cannot be found or is invalid.',
            text: '',
            type: Alert.TYPES.ERROR
          });
        }
        this.addressAlertGroup.show(alert);
        resetState();
        this.$scope.$apply();
      });
  }

  showMemo($event) {
    $event.preventDefault();
    this.memo = true;
    this.memoType = 'MEMO_TEXT';
    this.memoValue = null;
  }

  hideMemo($event) {
    if ($event) {
      $event.preventDefault();
    }
    this.memoAlertGroup.clear();
    this.memo = false;
    this.memoType = null;
    this.memoValue = null;
  }

  showView($event, v) {
    if ($event) {
      $event.preventDefault();
    }
    this.view = v;
  }

  send() {
    if (this.loadingDestination) {
      return false;
    }

    this.sending = true;
    this.ledgerError = false;

    this.addressAlertGroup.clear();
    this.amountAlertGroup.clear();
    this.memoAlertGroup.clear();

    if (!Account.isValidAccountId(this.destination)) {
      let alert = new Alert({
        title: 'Stellar address or public key is invalid.',
        text: 'Public keys are uppercase and begin with the letter "G."',
        type: Alert.TYPES.ERROR
      });
      this.addressAlertGroup.show(alert);
    }

    // Check if amount is valid
    if (!Operation.isValidAmount(this.amount)) {
      let alert = new Alert({
        title: '',
        text: 'This amount is invalid.',
        type: Alert.TYPES.ERROR
      });
      this.amountAlertGroup.show(alert);
    }

    // check if the destination requires a memo
    if (this.destination in knownAccounts && knownAccounts[this.destination].memo_required && !this.memoValue) {
      let alert = new Alert({
        title: '',
        text: 'The payment destination (' + knownAccounts[this.destination].name +') requires you to specify a memo to identify your account.',
        type: Alert.TYPES.ERROR
      });
      this.memoAlertGroup.show(alert);
      this.sending = false;
      return;
    }

    if (this.memo) {
      let memo, memoError;
      try {
        switch (this.memoType) {
          case 'MEMO_TEXT':
            memoError = 'MEMO_TEXT must contain a maximum of 28 characters';
            memo = Memo.text(this.memoValue);
            break;
          case 'MEMO_ID':
            memoError = 'MEMO_ID must be a valid 64 bit unsigned integer';
            memo = Memo.id(this.memoValue);
            break;
          case 'MEMO_HASH':
            memoError = 'MEMO_HASH must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.hash(this.memoValue);
            break;
          case 'MEMO_RETURN':
            memoError = 'MEMO_RETURN must be a 32 byte hash represented in hexadecimal (A-Z0-9)';
            memo = Memo.returnHash(this.memoValue);
            break;
        }
      } catch (error) {
        let alert = new Alert({
          title: '',
          text: memoError,
          type: Alert.TYPES.ERROR
        });
        this.memoAlertGroup.show(alert);
      }
    }

    if (this.addressAlerts.length || this.amountAlerts.length || this.memoAlerts.length) {
      this.sending = false;
      return;
    }

    return this.Server.accounts()
      .accountId(this.session.address)
      .call()
      .then(account => {
        // Check if sending this transaction would make balance go below minimum balance
        let minimumBalance = 20 + (account.subentry_count) * 10;
        let nativeBalance = _(account.balances).find(balance => balance.asset_type === 'native').balance;
        let maxSend = new BigNumber(nativeBalance).minus(minimumBalance);
        if (maxSend.lt(this.amount)) {
          throw new BasicClientError('InsufficientBalanceError', {maxSend});
        }
      })
      .then(() => {
        // Check if destination account exists. If no, at least 20 XLM must be sent.
        if (new BigNumber(this.amount).gte(20)) {
          return;
        }

        return this.Server.accounts()
          .accountId(this.destination)
          .call()
          .catch(err => {
            if (err.name === 'NotFoundError') {
              throw new BasicClientError('DestinationAccountNotExistError');
            }
          });
      })
      .then(() => {
        this.displayedAmount = new BigNumber(this.amount).toFormat();
        this.showView(null, 'sendConfirm');
      })
      .catch(err => {
        let alert;
        switch (err.name) {
          case 'NotFoundError':
            alert = new Alert({
              title: '',
              text: 'Your account isn\'t funded.',
              type: Alert.TYPES.ERROR
            });
            break;
          case 'InsufficientBalanceError':
            alert = new Alert({
              title: 'Insufficient balance.',
              text:
                `To maintain your <a href="https://www.stellar.org/developers/learn/concepts/fees.html#minimum-balance" target="_blank">minimum balance</a>, the most you can currently send is ${err.data.maxSend}.`,
              type: Alert.TYPES.ERROR
            });
            break;
          case 'DestinationAccountNotExistError':
            alert = new Alert({
              title: 'Destination account doesn\'t exist.',
              text: 'You account must send at least 20 lumens to create an account.',
              type: Alert.TYPES.ERROR
            });
            break;
          default:
            alert = new Alert({
              title: 'Unknown error: '+err.name,
              text: '',
              type: Alert.TYPES.ERROR
            });
            break;
        }
        this.amountAlertGroup.show(alert);
      })
      .finally(() => {
        this.sending = false;
        this.$scope.$apply();
      });
  }

  confirm() {
    this.showView(null, 'sendWaiting');
    return this.Server.accounts()
      .accountId(this.destination)
      .call()
      .then(() => {
        // Account exist. Send payment operation.
        let operation = Operation.payment({
          destination: this.destination,
          asset: Asset.native(),
          amount: this.amount
        });
        return this._submitTransaction(operation);
      })
      .catch(err => {
        if (err.name === 'NotFoundError') {
          // Account does not exist. Send create_account operation.
          let operation = Operation.createAccount({
            destination: this.destination,
            startingBalance: this.amount
          });
          return this._submitTransaction(operation);
        } else {
          throw err;
        }
      });
  }

  resubmitTransaction() {
    if (this.lastTransactionXDR === null) {
      return;
    }

    this.showView(null, 'sendWaiting');

    var transaction = new Transaction(this.lastTransactionXDR);

    return this.Server.submitTransaction(transaction)
      .then(this._submitOnSuccess.bind(this))
      .catch(this._submitOnFailure.bind(this))
      .finally(this._submitFinally.bind(this));
  }

  _submitTransaction(operation) {
    return this.Sessions.loadDefaultAccount()
      .then(() => {
        let memo = Memo.none();

        if (this.memo) {
          switch (this.memoType) {
            case 'MEMO_TEXT':
              memo = Memo.text(this.memoValue);
              break;
            case 'MEMO_ID':
              memo = Memo.id(this.memoValue);
              break;
            case 'MEMO_HASH':
              memo = Memo.hash(this.memoValue);
              break;
            case 'MEMO_RETURN':
              memo = Memo.returnHash(this.memoValue);
              break;
          }
        }

        let transaction = new TransactionBuilder(this.session.getAccount())
          .addOperation(operation)
          .addMemo(memo)
          .build();

        if (this.useLedger) {
          let ledgerApi = new StellarLedger.Api(new StellarLedger.comm(120));
          return ledgerApi.signTx_async(this.bip32Path, transaction).then(result => {
            let signature = result['signature'];
            let keyPair = Keypair.fromAccountId(this.session.address);
            let hint = keyPair.signatureHint();
            let decorated = new xdr.DecoratedSignature({hint, signature});
            transaction.signatures.push(decorated);
            return transaction;
          }).catch(e => {
            this.ledgerError = true;
            throw e;
          });
        } else {
          transaction.sign(Keypair.fromSeed(this.session.getSecret()));
          return transaction;
        }
      })
      .then(transaction => {
        this.lastTransactionXDR = transaction.toEnvelope().toXDR().toString("base64");
        return this.Server.submitTransaction(transaction);
      })
      .then(this._submitOnSuccess.bind(this))
      .catch(this._submitOnFailure.bind(this))
      .finally(this._submitFinally.bind(this));
  }

  _submitOnSuccess() {
    this.success = true;
    this.destinationAddress = null;
    this.destination = null;
    this.amount = null;
    this.memo = false;
    this.lastTransactionXDR = null;
    this.$rootScope.$broadcast('account-viewer.transaction-success');
  }

  _submitOnFailure(e) {
    this.success = false;
    this.needsResubmit = false;

    if (this.ledgerError) {
      if (e.errorCode == 2) {
        this.outcomeMessage = `Couldn't connect to Ledger device. Connection can only be established using a secure connection.`;
      } else if (e.errorCode == 5) {
        this.outcomeMessage = `Ledger connection timeout.`;
      } else {
        this.outcomeMessage = 'Unknown error: '+JSON.stringify(e);
      }
    } else {
      if (e.title != "Transaction Failed" && e.title != "Transaction Malformed") {
        this.needsResubmit = true;
      }
      this.outcomeMessage = JSON.stringify(e, null, '  ');
    }
  }

  _submitFinally() {
    this.showView(null, 'sendOutcome');
    this.$scope.$apply()
  }
}
<|endoftext|>


<commit_before>const path = require('path')
const os = require('os')
const chalk = require('chalk')
const fs = require('fs-extra')

const isLocalPath = v => /^[./]|(^[a-zA-Z]:)/.test(v)

const normalizeEntry = v => {
  if (v.startsWith('module:')) {
    return v.replace(/^module:/, '')
  }
  if (isLocalPath(v)) {
    return v
  }
  return `./${v}`
}

module.exports = (config, api) => {
  /** Set entry */

  const webpackEntry = {}
  const { entry, pages } = api.config
  if (pages) {
    for (const entryName of Object.keys(pages)) {
      const value = pages[entryName]
      webpackEntry[entryName] = [].concat(
        typeof value === 'string' ? value : value.entry
      )
    }
    api.logger.debug('Using `pages` option thus `entry` is ignored')
  } else if (typeof entry === 'string') {
    webpackEntry.index = [entry]
  } else if (Array.isArray(entry)) {
    webpackEntry.index = entry
  } else if (typeof entry === 'object') {
    Object.assign(webpackEntry, entry)
  }

  for (const name of Object.keys(webpackEntry)) {
    webpackEntry[name] = Array.isArray(webpackEntry[name])
      ? webpackEntry[name].map(v => normalizeEntry(v))
      : normalizeEntry(webpackEntry[name])
  }

  config.merge({ entry: webpackEntry })

  /** Set extensions */
  config.resolve.extensions.merge(['.js', '.json', '.jsx', '.ts', '.tsx'])

  /** Support react-native-web by default, cuz why not? */
  // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
  config.resolve.alias
    .set('react-native', 'react-native-web')
    .set('#webpack-hot-client$', require.resolve('@poi/dev-utils/hotDevClient'))

  // output.sourceMap defaults to false in production mode
  config.devtool(
    api.config.output.sourceMap === false
      ? false
      : api.mode === 'production'
      ? 'source-map'
      : api.mode === 'test'
      ? 'cheap-module-eval-source-map'
      : 'cheap-module-source-map'
  )

  /** Alias @ to `src` folder since many apps store app code here */
  config.resolve.alias.set('@', api.resolveCwd('src'))

  /** Set mode */
  config.mode(api.mode === 'production' ? 'production' : 'development')

  config.merge({
    // Disable webpack's default minimizer
    // Minimization will be handled by mode:production plugin
    optimization: {
      minimize: false
    },
    // Disable default performance hints
    // TODO: maybe add our custom one
    performance: {
      hints: false
    }
  })

  /** Set output */
  config.output.path(api.resolveOutDir())
  config.output.filename(api.config.output.fileNames.js)
  config.output.chunkFilename(
    api.config.output.fileNames.js.replace(/\.js$/, '.chunk.js')
  )
  config.output.publicPath(api.config.output.publicUrl)

  /** Set format */
  const { format, moduleName } = api.config.output
  if (format === 'cjs') {
    config.output.libraryTarget('commonjs2')
  } else if (format === 'umd') {
    if (!moduleName) {
      api.logger.error(
        `"moduleName" is missing for ${chalk.bold('umd')} format`
      )
      api.logger.tip(
        `To add it, simply use flag ${chalk.cyan('--module-name <name>')}`
      )
      api.logger.tip(
        `Like ${chalk.cyan(
          '--module-name React'
        )} if you're building the React.js source code`
      )
      throw new api.PoiError({
        message: 'missing moduleName for umd format',
        dismiss: true
      })
    }
    config.output.libraryTarget('umd')
    config.output.library(moduleName)
  }

  // Set output target
  const { target } = api.config.output
  config.target(target === 'electron' ? 'electron-renderer' : target)

  const inYarnWorkspaces = __dirname.includes('/poi/core/poi')
  const poiDependenciesDir = inYarnWorkspaces
    ? path.join(__dirname, '../../../../node_modules')
    : path.join(__dirname, '../../../')

  /** Resolve loaders */
  config.resolveLoader.modules.add('node_modules').add(poiDependenciesDir)

  /** Resolve modules */
  config.resolve.modules.add('node_modules').add(poiDependenciesDir)

  // Add progress bar
  if (
    api.cli.options.progress !== false &&
    process.stdout.isTTY &&
    !process.env.CI
  ) {
    const homeRe = new RegExp(os.homedir(), 'g')
    config.plugin('progress').use(require('webpack').ProgressPlugin, [
      /**
       * @param {Number} per
       * @param {string} message
       * @param {string[]} args
       */
      (per, message, ...args) => {
        const spinner = require('../utils/spinner')

        const msg = `${(per * 100).toFixed(2)}% ${message} ${args
          .map(arg => {
            const message = arg.replace(homeRe, '~')
            return message.length > 40
              ? `...${message.substr(message.length - 39)}`
              : message
          })
          .join(' ')}`

        if (per === 0) {
          spinner.start(msg)
        } else if (per === 1) {
          spinner.stop()
        } else {
          spinner.text = msg
        }
      }
    ])
  }

  /** Add a default status reporter */
  config.plugin('print-status').use(require('./PrintStatusPlugin'), [
    {
      printFileStats: api.mode !== 'test',
      printSucessMessage: api.mode !== 'test',
      clearConsole: api.cli.options.clearConsole
    }
  ])

  /** Add constants plugin */
  config
    .plugin('constants')
    .use(require('webpack').DefinePlugin, [api.webpackUtils.constants])

  /** Inject envs */
  const { envs } = api.webpackUtils
  config.plugin('envs').use(require('webpack').DefinePlugin, [
    Object.keys(envs).reduce((res, name) => {
      res[`process.env.${name}`] = JSON.stringify(envs[name])
      return res
    }, {})
  ])

  /** Miniize JS files */
  if (api.config.output.minimize) {
    config.plugin('minimize').use(require('terser-webpack-plugin'), [
      {
        cache: true,
        parallel: true,
        sourceMap: api.config.output.sourceMap,
        terserOptions: {
          parse: {
            // we want terser to parse ecma 8 code. However, we don't want it
            // to apply any minfication steps that turns valid ecma 5 code
            // into invalid ecma 5 code. This is why the 'compress' and 'output'
            // sections only apply transformations that are ecma 5 safe
            // https://github.com/facebook/create-react-app/pull/4234
            ecma: 8
          },
          compress: {
            ecma: 5,
            warnings: false,
            // Disabled because of an issue with Uglify breaking seemingly valid code:
            // https://github.com/facebook/create-react-app/issues/2376
            // Pending further investigation:
            // https://github.com/mishoo/UglifyJS2/issues/2011
            comparisons: false,
            // Disabled because of an issue with Terser breaking valid code:
            // https://github.com/facebook/create-react-app/issues/5250
            // Pending futher investigation:
            // https://github.com/terser-js/terser/issues/120
            inline: 2
          },
          mangle: {
            safari10: true
          },
          output: {
            ecma: 5,
            comments: false,
            // Turned on because emoji and regex is not minified properly using default
            // https://github.com/facebook/create-react-app/issues/2488
            ascii_only: true
          }
        }
      }
    ])
  }

  if (!api.isProd) {
    config
      .plugin('case-sensitive-paths')
      .use(require('case-sensitive-paths-webpack-plugin'))

    const nodeModulesDir =
      api.configLoader.resolve('node_modules') || api.resolveCwd('node_modules')
    config
      .plugin('watch-missing-node-modules')
      .use(require('@poi/dev-utils/WatchMissingNodeModulesPlugin'), [
        nodeModulesDir
      ])
  }

  config.plugin('copy-public-folder').use(require('copy-webpack-plugin'), [
    [
      {
        from: {
          glob: '**/*',
          dot: true
        },
        context: api.resolveCwd(api.config.publicFolder),
        to: '.',
        ignore: ['.DS_Store']
      }
    ]
  ])

  if (api.config.output.clean !== false) {
    config.plugin('clean-out-dir').use(
      class CleanOutDir {
        apply(compiler) {
          compiler.hooks.beforeRun.tapPromise('clean-out-dir', async () => {
            if (api.resolveOutDir() === process.cwd()) {
              api.logger.error(`Refused to clean current working directory`)
              return
            }
            await fs.remove(api.resolveOutDir())
          })
        }
      }
    )
  }
}
<commit_msg>fix: include .wasm .mjs in resolve.extensions, closes #536<commit_after>const path = require('path')
const os = require('os')
const chalk = require('chalk')
const fs = require('fs-extra')

const isLocalPath = v => /^[./]|(^[a-zA-Z]:)/.test(v)

const normalizeEntry = v => {
  if (v.startsWith('module:')) {
    return v.replace(/^module:/, '')
  }
  if (isLocalPath(v)) {
    return v
  }
  return `./${v}`
}

module.exports = (config, api) => {
  /** Set entry */

  const webpackEntry = {}
  const { entry, pages } = api.config
  if (pages) {
    for (const entryName of Object.keys(pages)) {
      const value = pages[entryName]
      webpackEntry[entryName] = [].concat(
        typeof value === 'string' ? value : value.entry
      )
    }
    api.logger.debug('Using `pages` option thus `entry` is ignored')
  } else if (typeof entry === 'string') {
    webpackEntry.index = [entry]
  } else if (Array.isArray(entry)) {
    webpackEntry.index = entry
  } else if (typeof entry === 'object') {
    Object.assign(webpackEntry, entry)
  }

  for (const name of Object.keys(webpackEntry)) {
    webpackEntry[name] = Array.isArray(webpackEntry[name])
      ? webpackEntry[name].map(v => normalizeEntry(v))
      : normalizeEntry(webpackEntry[name])
  }

  config.merge({ entry: webpackEntry })

  /** Set extensions */
  config.resolve.extensions.merge([
    '.wasm',
    '.mjs',
    '.js',
    '.jsx',
    '.ts',
    '.tsx',
    '.json'
  ])

  /** Support react-native-web by default, cuz why not? */
  // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
  config.resolve.alias
    .set('react-native', 'react-native-web')
    .set('#webpack-hot-client$', require.resolve('@poi/dev-utils/hotDevClient'))

  // output.sourceMap defaults to false in production mode
  config.devtool(
    api.config.output.sourceMap === false
      ? false
      : api.mode === 'production'
      ? 'source-map'
      : api.mode === 'test'
      ? 'cheap-module-eval-source-map'
      : 'cheap-module-source-map'
  )

  /** Alias @ to `src` folder since many apps store app code here */
  config.resolve.alias.set('@', api.resolveCwd('src'))

  /** Set mode */
  config.mode(api.mode === 'production' ? 'production' : 'development')

  config.merge({
    // Disable webpack's default minimizer
    // Minimization will be handled by mode:production plugin
    optimization: {
      minimize: false
    },
    // Disable default performance hints
    // TODO: maybe add our custom one
    performance: {
      hints: false
    }
  })

  /** Set output */
  config.output.path(api.resolveOutDir())
  config.output.filename(api.config.output.fileNames.js)
  config.output.chunkFilename(
    api.config.output.fileNames.js.replace(/\.js$/, '.chunk.js')
  )
  config.output.publicPath(api.config.output.publicUrl)

  /** Set format */
  const { format, moduleName } = api.config.output
  if (format === 'cjs') {
    config.output.libraryTarget('commonjs2')
  } else if (format === 'umd') {
    if (!moduleName) {
      api.logger.error(
        `"moduleName" is missing for ${chalk.bold('umd')} format`
      )
      api.logger.tip(
        `To add it, simply use flag ${chalk.cyan('--module-name <name>')}`
      )
      api.logger.tip(
        `Like ${chalk.cyan(
          '--module-name React'
        )} if you're building the React.js source code`
      )
      throw new api.PoiError({
        message: 'missing moduleName for umd format',
        dismiss: true
      })
    }
    config.output.libraryTarget('umd')
    config.output.library(moduleName)
  }

  // Set output target
  const { target } = api.config.output
  config.target(target === 'electron' ? 'electron-renderer' : target)

  const inYarnWorkspaces = __dirname.includes('/poi/core/poi')
  const poiDependenciesDir = inYarnWorkspaces
    ? path.join(__dirname, '../../../../node_modules')
    : path.join(__dirname, '../../../')

  /** Resolve loaders */
  config.resolveLoader.modules.add('node_modules').add(poiDependenciesDir)

  /** Resolve modules */
  config.resolve.modules.add('node_modules').add(poiDependenciesDir)

  // Add progress bar
  if (
    api.cli.options.progress !== false &&
    process.stdout.isTTY &&
    !process.env.CI
  ) {
    const homeRe = new RegExp(os.homedir(), 'g')
    config.plugin('progress').use(require('webpack').ProgressPlugin, [
      /**
       * @param {Number} per
       * @param {string} message
       * @param {string[]} args
       */
      (per, message, ...args) => {
        const spinner = require('../utils/spinner')

        const msg = `${(per * 100).toFixed(2)}% ${message} ${args
          .map(arg => {
            const message = arg.replace(homeRe, '~')
            return message.length > 40
              ? `...${message.substr(message.length - 39)}`
              : message
          })
          .join(' ')}`

        if (per === 0) {
          spinner.start(msg)
        } else if (per === 1) {
          spinner.stop()
        } else {
          spinner.text = msg
        }
      }
    ])
  }

  /** Add a default status reporter */
  config.plugin('print-status').use(require('./PrintStatusPlugin'), [
    {
      printFileStats: api.mode !== 'test',
      printSucessMessage: api.mode !== 'test',
      clearConsole: api.cli.options.clearConsole
    }
  ])

  /** Add constants plugin */
  config
    .plugin('constants')
    .use(require('webpack').DefinePlugin, [api.webpackUtils.constants])

  /** Inject envs */
  const { envs } = api.webpackUtils
  config.plugin('envs').use(require('webpack').DefinePlugin, [
    Object.keys(envs).reduce((res, name) => {
      res[`process.env.${name}`] = JSON.stringify(envs[name])
      return res
    }, {})
  ])

  /** Miniize JS files */
  if (api.config.output.minimize) {
    config.plugin('minimize').use(require('terser-webpack-plugin'), [
      {
        cache: true,
        parallel: true,
        sourceMap: api.config.output.sourceMap,
        terserOptions: {
          parse: {
            // we want terser to parse ecma 8 code. However, we don't want it
            // to apply any minfication steps that turns valid ecma 5 code
            // into invalid ecma 5 code. This is why the 'compress' and 'output'
            // sections only apply transformations that are ecma 5 safe
            // https://github.com/facebook/create-react-app/pull/4234
            ecma: 8
          },
          compress: {
            ecma: 5,
            warnings: false,
            // Disabled because of an issue with Uglify breaking seemingly valid code:
            // https://github.com/facebook/create-react-app/issues/2376
            // Pending further investigation:
            // https://github.com/mishoo/UglifyJS2/issues/2011
            comparisons: false,
            // Disabled because of an issue with Terser breaking valid code:
            // https://github.com/facebook/create-react-app/issues/5250
            // Pending futher investigation:
            // https://github.com/terser-js/terser/issues/120
            inline: 2
          },
          mangle: {
            safari10: true
          },
          output: {
            ecma: 5,
            comments: false,
            // Turned on because emoji and regex is not minified properly using default
            // https://github.com/facebook/create-react-app/issues/2488
            ascii_only: true
          }
        }
      }
    ])
  }

  if (!api.isProd) {
    config
      .plugin('case-sensitive-paths')
      .use(require('case-sensitive-paths-webpack-plugin'))

    const nodeModulesDir =
      api.configLoader.resolve('node_modules') || api.resolveCwd('node_modules')
    config
      .plugin('watch-missing-node-modules')
      .use(require('@poi/dev-utils/WatchMissingNodeModulesPlugin'), [
        nodeModulesDir
      ])
  }

  config.plugin('copy-public-folder').use(require('copy-webpack-plugin'), [
    [
      {
        from: {
          glob: '**/*',
          dot: true
        },
        context: api.resolveCwd(api.config.publicFolder),
        to: '.',
        ignore: ['.DS_Store']
      }
    ]
  ])

  if (api.config.output.clean !== false) {
    config.plugin('clean-out-dir').use(
      class CleanOutDir {
        apply(compiler) {
          compiler.hooks.beforeRun.tapPromise('clean-out-dir', async () => {
            if (api.resolveOutDir() === process.cwd()) {
              api.logger.error(`Refused to clean current working directory`)
              return
            }
            await fs.remove(api.resolveOutDir())
          })
        }
      }
    )
  }
}
<|endoftext|>


<commit_before>var modifying = false;
var enabled = false;

var hideElements=[
	'_5gl-',//photobar
	'_32jt',//addfriend
	'share_root',//share
	'comment_link',//commentlink
	'UFIAddComment',//commentbox
	'UFILikeLink',//likelink
	'FriendRequestAdd',//friendrequest button
	'addButton',//friendrequest button
]

self.port.on('disabled',function(){
	creepModeOff();
});

self.port.on('enabled',function(){
	creepModeOn();
});

document.body.addEventListener("DOMSubtreeModified",function(){
	if(!modifying){
		hideStuff();
	}
},false);

function creepModeOff(){
	enabled = false;
	showStuff();
}

function creepModeOn(){
	enabled = true;
	hideStuff();
}

function hideStuff(){
	if(enabled){
		modifying=true;
		for(var i=0;i<hideElements.length;++i){
			hideClassElements(hideElements[i]);
		}
		modifying=false;
	}
}

function showStuff(){
	modifying=true;
	for(var i=0;i<hideElements.length;++i){
		showClassElements(hideElements[i]);
	}
	modifying=false;
}

function hideClassElements(className){
	var elements = document.getElementsByClassName(className);

	for(var i=0;i<elements.length;++i){
		elements[i].style.display="none";
	}
}

function showClassElements(className){
	var elements = document.getElementsByClassName(className);

	for(var i=0;i<elements.length;++i){
		elements[i].style.display="inline";
	}
}
<commit_msg>added rule for like buttons on pages<commit_after>var modifying = false;
var enabled = false;

var hideElements=[
	'_5gl-',//photobar
	'_32jt',//addfriend
	'share_root',//share
	'comment_link',//commentlink
	'UFIAddComment',//commentbox
	'UFILikeLink',//likelink
	'FriendRequestAdd',//friendrequest button
	'addButton',//friendrequest button
	'pageLikeButton', //like button on pages
]

self.port.on('disabled',function(){
	creepModeOff();
});

self.port.on('enabled',function(){
	creepModeOn();
});

document.body.addEventListener("DOMSubtreeModified",function(){
	if(!modifying){
		hideStuff();
	}
},false);

function creepModeOff(){
	enabled = false;
	showStuff();
}

function creepModeOn(){
	enabled = true;
	hideStuff();
}

function hideStuff(){
	if(enabled){
		modifying=true;
		for(var i=0;i<hideElements.length;++i){
			hideClassElements(hideElements[i]);
		}
		modifying=false;
	}
}

function showStuff(){
	modifying=true;
	for(var i=0;i<hideElements.length;++i){
		showClassElements(hideElements[i]);
	}
	modifying=false;
}

function hideClassElements(className){
	var elements = document.getElementsByClassName(className);

	for(var i=0;i<elements.length;++i){
		elements[i].style.display="none";
	}
}

function showClassElements(className){
	var elements = document.getElementsByClassName(className);

	for(var i=0;i<elements.length;++i){
		elements[i].style.display="inline";
	}
}
<|endoftext|>


<commit_before>$(function () {
function updateTestCasesFromTest() {
    var test_id=$(this).find("option:selected").html();
    var selects = $(this).closest('tr').find('.test-case-formset select');
    selects.each(
        function () {
            $(this).empty();
        });
    if (test_id != '<any>') {
        $.ajax(
            {
                url: test_case_url + test_id,
                dataType: 'json',
                success: function (data) {
                    selects.each(
                        function () {
                            var select = $(this);
                            $(data).each(
                                function (index, val) {
                                    var test_case_id = val.test_case_id;
                                    if (test_case_id.length > 50) {
                                        test_case_id = test_case_id.substring(0, 50) + "...";
                                    }
                                    select.append(Option(test_case_id, val.id));
                                });
                            select.removeAttr("disabled");
                        });
                }
            });
    } else {
        selects.each(
            function () {
                $(this).attr('disabled', 'disabled');
            });
    }
};

$("#id_tests_empty_form .test-case-formset-empty select").attr('disabled', 'disabled');
$(".test-cell select").change(updateTestCasesFromTest);

var nameAutocompleteConfig = {
        source: attr_name_completion_url
    };

var valueAutocompleteConfig = {
        source: function (request, response) {
            var attrName = this.element.closest('tr').find('.name input').val();
            $.getJSON(
                attr_value_completion_url,
                {
                    'name': attrName,
                    'term': request.term
                },
                function (data) {
                    response(data);
                }
            );
        }
    };

$("tbody .name input").autocomplete(nameAutocompleteConfig);
$("tbody .value input").autocomplete(valueAutocompleteConfig);

$("#attributes-table tbody tr").formset(
    {
        formTemplate: '#id_attributes_empty_form',
        prefix: "attributes",
        addText: "Add a required attribute",
        added: function(row) {
            row.find(".name input").unbind();
            row.find(".name input").autocomplete(nameAutocompleteConfig);
            row.find(".value input").unbind();
            row.find(".value input").autocomplete(valueAutocompleteConfig);
        }
    });

var testSetSettings = {
    formTemplate: '#id_tests_empty_form',
    prefix: "tests",
    addText: "Add a test",
    added: formsetTestCase
};

function formsetTestCase(test_row) {
    var addText;
    if (test_row.find(".test-case-formset select").size() < 2) {
        addText = 'Specify test cases';
    } else {
        addText = 'Add another test case';
    }

    var index = test_row.index();

    test_row.find(".test-case-formset > tbody > tr").formset(
        {
            formTemplate: test_row.find(".test-case-formset-empty"),
            formCssClass: "nested-dynamic-" + index,
            addText: addText,
            prefix: "tests-" + index,
            added: function (row2) {
                test_row.find('.add-row').text('Add another test case');
            },
            removed: function (row2) {
                if (test_row.find(".test-case-formset select").size() < 2) {
                    test_row.find('.add-row').text("Specify test cases");
                }
            }
        }
    );
}

$("#tests-table > tbody > tr").formset(testSetSettings);

$("#tests-table > tbody > tr").each(
    function () {
        formsetTestCase($(this));
    }
);

});<commit_msg>formCssClass has to be unique -- and that means for the test and attributes formsets too!<commit_after>$(function () {
function updateTestCasesFromTest() {
    var test_id=$(this).find("option:selected").html();
    var selects = $(this).closest('tr').find('.test-case-formset select');
    selects.each(
        function () {
            $(this).empty();
        });
    if (test_id != '<any>') {
        $.ajax(
            {
                url: test_case_url + test_id,
                dataType: 'json',
                success: function (data) {
                    selects.each(
                        function () {
                            var select = $(this);
                            $(data).each(
                                function (index, val) {
                                    var test_case_id = val.test_case_id;
                                    if (test_case_id.length > 50) {
                                        test_case_id = test_case_id.substring(0, 50) + "...";
                                    }
                                    select.append(Option(test_case_id, val.id));
                                });
                            select.removeAttr("disabled");
                        });
                }
            });
    } else {
        selects.each(
            function () {
                $(this).attr('disabled', 'disabled');
            });
    }
};

$("#id_tests_empty_form .test-case-formset-empty select").attr('disabled', 'disabled');
$(".test-cell select").change(updateTestCasesFromTest);

var nameAutocompleteConfig = {
        source: attr_name_completion_url
    };

var valueAutocompleteConfig = {
        source: function (request, response) {
            var attrName = this.element.closest('tr').find('.name input').val();
            $.getJSON(
                attr_value_completion_url,
                {
                    'name': attrName,
                    'term': request.term
                },
                function (data) {
                    response(data);
                }
            );
        }
    };

$("tbody .name input").autocomplete(nameAutocompleteConfig);
$("tbody .value input").autocomplete(valueAutocompleteConfig);

$("#attributes-table tbody tr").formset(
    {
        formTemplate: '#id_attributes_empty_form',
        prefix: "attributes",
        formCssClass: "attributes-dynamic-form",
        addText: "Add a required attribute",
        added: function(row) {
            row.find(".name input").unbind();
            row.find(".name input").autocomplete(nameAutocompleteConfig);
            row.find(".value input").unbind();
            row.find(".value input").autocomplete(valueAutocompleteConfig);
        }
    });

function formsetTestCase(test_row) {
    var addText;
    if (test_row.find(".test-case-formset select").size() < 2) {
        addText = 'Specify test cases';
    } else {
        addText = 'Add another test case';
    }

    var index = test_row.index();

    test_row.find(".test-case-formset > tbody > tr").formset(
        {
            formTemplate: test_row.find(".test-case-formset-empty"),
            formCssClass: "test-cases-dynamic-form-" + index,
            addText: addText,
            prefix: "tests-" + index,
            added: function (row2) {
                test_row.find('.add-row').text('Add another test case');
            },
            removed: function (row2) {
                if (test_row.find(".test-case-formset select").size() < 2) {
                    test_row.find('.add-row').text("Specify test cases");
                }
            }
        }
    );
}

$("#tests-table > tbody > tr").formset({
    formTemplate: '#id_tests_empty_form',
    prefix: "tests",
    formCssClass: "test-dynamic-form",
    addText: "Add a test",
    added: formsetTestCase
});

$("#tests-table > tbody > tr").each(
    function () {
        formsetTestCase($(this));
    }
);

});<|endoftext|>


<commit_before>/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @flow
 */

'use strict';

import type {DefaultConfig} from 'types/Config';

const constants = require('./constants');
const os = require('os');
const path = require('path');
const {replacePathSepForRegex} = require('jest-regex-util');

const NODE_MODULES_REGEXP = replacePathSepForRegex(constants.NODE_MODULES);

const cacheDirectory = (() => {
  if (process.getuid == null) {
    return path.join(os.tmpdir(), 'jest');
  }
  // On some platforms tmpdir() is `/tmp`, causing conflicts between different
  // users and permission issues. Adding an additional subdivision by UID can
  // help.
  return path.join(
    os.tmpdir(),
    'jest',
    (process.getuid && process.getuid().toString(36)) || '',
  );
})();

module.exports = ({
  automock: false,
  bail: false,
  browser: false,
  cacheDirectory,
  clearMocks: false,
  coveragePathIgnorePatterns: [NODE_MODULES_REGEXP],
  coverageReporters: ['json', 'text', 'lcov', 'clover'],
  expand: false,
  globals: {},
  haste: {
    providesModuleNodeModules: [],
  },
  mapCoverage: false,
  moduleDirectories: ['node_modules'],
  moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
  moduleNameMapper: {},
  modulePathIgnorePatterns: [],
  noStackTrace: false,
  notify: false,
  preset: null,
  resetMocks: false,
  resetModules: false,
  roots: ['<rootDir>'],
  snapshotSerializers: [],
  testEnvironment: 'jest-environment-jsdom',
  testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)(spec|test).js?(x)'],
  testPathIgnorePatterns: [NODE_MODULES_REGEXP],
  testRegex: '',
  testResultsProcessor: null,
  testURL: 'about:blank',
  timers: 'real',
  transformIgnorePatterns: [NODE_MODULES_REGEXP],
  useStderr: false,
  verbose: null,
  watch: false,
}: DefaultConfig);
<commit_msg>jest-config: UID needs to be part of top-level folder (#3387)<commit_after>/**
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @flow
 */

'use strict';

import type {DefaultConfig} from 'types/Config';

const constants = require('./constants');
const os = require('os');
const path = require('path');
const {replacePathSepForRegex} = require('jest-regex-util');

const NODE_MODULES_REGEXP = replacePathSepForRegex(constants.NODE_MODULES);

const cacheDirectory = (() => {
  const {getuid} = process;
  if (getuid == null) {
    return path.join(os.tmpdir(), 'jest');
  }
  // On some platforms tmpdir() is `/tmp`, causing conflicts between different
  // users and permission issues. Adding an additional subdivision by UID can
  // help.
  return path.join(
    os.tmpdir(),
    'jest_' + getuid.call(process).toString(36),
  );
})();

module.exports = ({
  automock: false,
  bail: false,
  browser: false,
  cacheDirectory,
  clearMocks: false,
  coveragePathIgnorePatterns: [NODE_MODULES_REGEXP],
  coverageReporters: ['json', 'text', 'lcov', 'clover'],
  expand: false,
  globals: {},
  haste: {
    providesModuleNodeModules: [],
  },
  mapCoverage: false,
  moduleDirectories: ['node_modules'],
  moduleFileExtensions: ['js', 'json', 'jsx', 'node'],
  moduleNameMapper: {},
  modulePathIgnorePatterns: [],
  noStackTrace: false,
  notify: false,
  preset: null,
  resetMocks: false,
  resetModules: false,
  roots: ['<rootDir>'],
  snapshotSerializers: [],
  testEnvironment: 'jest-environment-jsdom',
  testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)(spec|test).js?(x)'],
  testPathIgnorePatterns: [NODE_MODULES_REGEXP],
  testRegex: '',
  testResultsProcessor: null,
  testURL: 'about:blank',
  timers: 'real',
  transformIgnorePatterns: [NODE_MODULES_REGEXP],
  useStderr: false,
  verbose: null,
  watch: false,
}: DefaultConfig);
<|endoftext|>


<commit_before>import React from 'react';
import {
  createComponent,
  Container,
  Grid,
  border,
  SchemaLoader,
} from 'olymp-fela';
import { graphql, gql, Link } from 'olymp';
import { Image } from 'olymp-cloudinary';
import moment from 'moment';
import { H2, Panel } from '../components';
import { range } from 'lodash';

const loaderSchema = [
  {
    height: 0,
  },
  {
    width: 'container',
    grid: [
      {
        medium: 7,
        children: range(9).map(() => ({
          height: 180,
          width: '100%',
        })),
      },
      {
        medium: 5,
        height: 500,
      },
    ],
  },
];

const Img = createComponent(
  ({ theme }) => ({
    float: 'left',
    marginRight: theme.space3,
  }),
  p => <Image {...p} />,
  p => Object.keys(p)
);

const H5 = createComponent(
  ({ theme }) => ({
    marginTop: theme.space3,
  }),
  ({ className, children }) =>
    (

      {children}

),
  p => Object.keys(p)
);

const Li = createComponent(
  ({ theme }) => ({
    paddingTop: theme.space2,
    borderBottom: border(theme),
    ':last-of-type': {
      marginBottom: theme.space3,
    },
  }),
  'li',
  p => Object.keys(p)
);

const component = graphql(
  gql`
    query terminList {
      items: terminList {
        id
        date
        art
        name
        extrakt
        slug
        bild {
          width
          height
          url
          crop
        }
      }
    }
  `,
  {
    props: ({ ownProps, data }) => ({
      ...ownProps,
      data,
      isLoading: data.loading,
      items: data.items || [],
    }),
  }
)(({ attributes, items, isLoading }) =>
  (<SchemaLoader isLoading={isLoading} schema={loaderSchema}>
    <Container {...attributes}>
      <Grid>
        <Grid.Item medium={8} paddingMedium="0 0 0 0.5rem">
          {items.map(item =>
            (<Panel id={item.id} title={item.name} key={item.id} paddingLeft={0}>

                  {item.extrakt}

                <Link to={item.slug || '/'}>Mehr erfahren...</Link>

                  {item.art} vom {moment(item.date).format('DD.MM.YYYY')}

            </Panel>)
          )}
        </Grid.Item>
        <Grid.Item
          medium={4}
          paddingMini="1rem 1rem 0 1rem"
          paddingMedium="0 1rem"
        >

Vorträge & Veranstaltungen

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

Publikationen

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

Presse

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

        </Grid.Item>
      </Grid>
    </Container>
  </SchemaLoader>)
);

export default {
  key: 'GZK.Collections.NewsBlock',
  label: 'Neuigkeiten',
  category: 'Collections',
  editable: false,
  component,
};
<commit_msg>gz: space in news + right aligned<commit_after>import React from 'react';
import {
  createComponent,
  Container,
  Grid,
  border,
  SchemaLoader,
} from 'olymp-fela';
import { graphql, gql, Link } from 'olymp';
import { Image } from 'olymp-cloudinary';
import moment from 'moment';
import { H2, Panel } from '../components';
import { range } from 'lodash';

const loaderSchema = [
  {
    height: 0,
  },
  {
    width: 'container',
    grid: [
      {
        medium: 7,
        children: range(9).map(() => ({
          height: 180,
          width: '100%',
        })),
      },
      {
        medium: 5,
        height: 500,
      },
    ],
  },
];

const Column = createComponent(
  ({ theme }) => ({
    textAlign: 'right',
  }),
  p => <Grid.Item {...p} />,
  p => Object.keys(p)
);

const Img = createComponent(
  ({ theme }) => ({
    float: 'left',
    marginRight: theme.space3,
  }),
  p => <Image {...p} />,
  p => Object.keys(p)
);

const H5 = createComponent(
  ({ theme }) => ({
    marginTop: theme.space3,
  }),
  ({ className, children }) =>
    (

      {children}

),
  p => Object.keys(p)
);

const Li = createComponent(
  ({ theme }) => ({
    paddingTop: theme.space2,
    borderBottom: border(theme),
    ':last-of-type': {
      marginBottom: theme.space3,
    },
  }),
  'li',
  p => Object.keys(p)
);

const component = graphql(
  gql`
    query terminList {
      items: terminList {
        id
        date
        art
        name
        extrakt
        slug
        bild {
          width
          height
          url
          crop
        }
      }
    }
  `,
  {
    props: ({ ownProps, data }) => ({
      ...ownProps,
      data,
      isLoading: data.loading,
      items: data.items || [],
    }),
  }
)(({ attributes, items, isLoading }) =>
  (<SchemaLoader isLoading={isLoading} schema={loaderSchema}>
    <Container {...attributes}>
      <Grid>
        <Grid.Item medium={7} paddingMedium="0 0 0 0.5rem">
          {items.map(item =>
            (<Panel id={item.id} title={item.name} key={item.id} paddingLeft={0}>

                  {item.extrakt}

                <Link to={item.slug || '/'}>Mehr erfahren...</Link>

                  {item.art} vom {moment(item.date).format('DD.MM.YYYY')}

            </Panel>)
          )}
        </Grid.Item>
        <Column
          medium={4}
          offsetMedium={1}
          offsetLarge={1}
          offsetHuge={1}
          paddingMini="1rem 1rem 0 1rem"
          paddingMedium="0 1rem"
        >

Vorträge & Veranstaltungen

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

Publikationen

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

Presse

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

                Ursachen und aktuelle Behandlungsmöglichkeiten von
                Schulterschmerzen

              21. März 2017, 18:00 Uhr

Gelenkersatz am Knie

              25. April 2017, 18:00 Uhr

MAXIMAL 5 ITEMS!

        </Column>
      </Grid>
    </Container>
  </SchemaLoader>)
);

export default {
  key: 'GZK.Collections.NewsBlock',
  label: 'Neuigkeiten',
  category: 'Collections',
  editable: false,
  component,
};
<|endoftext|>