<commit_before>/*globals qq */
/**
 * Upload handler used by the upload to Azure module that depends on File API support, and, therefore, makes use of
 * `XMLHttpRequest` level 2 to upload `File`s and `Blob`s directly to Azure Blob Storage containers via the
 * associated Azure API.
 *
 * @param spec Options passed from the base handler
 * @param proxy Callbacks & methods used to query for or push out data/changes
 */
// TODO l18n for error messages returned to UI
qq.azure.XhrUploadHandler = function(spec, proxy) {
    "use strict";

    var handler = this,
        log = proxy.log,
        cors = spec.cors,
        endpointStore = spec.endpointStore,
        paramsStore = spec.paramsStore,
        signature = spec.signature,
        filenameParam = spec.filenameParam,
        minFileSizeForChunking = spec.chunking.minFileSize,
        deleteBlob = spec.deleteBlob,
        onGetBlobName = spec.onGetBlobName,
        getName = proxy.getName,
        getSize = proxy.getSize,

        getBlobMetadata = function(id) {
            var params = paramsStore.get(id);
            params[filenameParam] = getName(id);
            return params;
        },

        api = {
            putBlob: new qq.azure.PutBlob({
                getBlobMetadata: getBlobMetadata,
                log: log
            }),

            putBlock: new qq.azure.PutBlock({
                log: log
            }),

            putBlockList: new qq.azure.PutBlockList({
                getBlobMetadata: getBlobMetadata,
                log: log
            }),

            getSasForPutBlobOrBlock: new qq.azure.GetSas({
                cors: cors,
                customHeaders: signature.customHeaders,
                endpointStore: {
                    get: function() {
                        return signature.endpoint;
                    }
                },
                log: log,
                restRequestVerb: "PUT"
            })
        };

    function determineBlobUrl(id) {
        var containerUrl = endpointStore.get(id),
            promise = new qq.Promise(),
            getBlobNameSuccess = function(blobName) {
                handler._getFileState(id).key = blobName;
                promise.success(containerUrl + "/" + blobName);
            },
            getBlobNameFailure = function(reason) {
                promise.failure(reason);
            };

        onGetBlobName(id).then(getBlobNameSuccess, getBlobNameFailure);

        return promise;
    }

    function getSignedUrl(id) {
        var promise = new qq.Promise(),
            getSasSuccess = function(sasUri) {
                log("GET SAS request succeeded.");
                promise.success(sasUri);
            },
            getSasFailure = function(reason, getSasXhr) {
                log("GET SAS request failed: " + reason, "error");
                promise.failure({error: "Problem communicating with local server"}, getSasXhr);
            },
            determineBlobUrlSuccess = function(blobUrl) {
                api.getSasForPutBlobOrBlock.request(id, blobUrl).then(
                    getSasSuccess,
                    getSasFailure
                );
            },
            determineBlobUrlFailure = function(reason) {
                log(qq.format("Failed to determine blob name for ID {} - {}", id, reason), "error");
                promise.failure({error: reason});
            };

        determineBlobUrl(id).then(determineBlobUrlSuccess, determineBlobUrlFailure);

        return promise;
    }

    function handleSimpleUpload(id) {
        var promise = new qq.Promise(),
            fileOrBlob = handler.getFile(id);

        getSignedUrl(id).then(function(sasUri) {
            var xhr = handler._createXhr(id);

            handler._registerProgressHandler(id);

            api.putBlob.upload(id, xhr, sasUri, fileOrBlob).then(
                function() {
                    log("Put Blob call succeeded for " + id);
                    promise.success({}, xhr);
                },
                function() {
                    log("Put Blob call failed for " + id, "error");

                    var azureError = qq.azure.util.parseAzureError(xhr.responseText, log),
                        errorMsg = "Problem sending file to Azure";

                    promise.failure({error: errorMsg, azureError: azureError && azureError.message});
                }
            );
        }, promise.failure);

        return promise;
    }

    function combineChunks(id) {
        var promise = new qq.Promise();

        getSignedUrl(id).then(function(sasUri) {
            var mimeType = handler._getMimeType(id),
                blockIds = handler._getFileState(id).chunking.blockIds;

            api.putBlockList.send(id, sasUri, blockIds, mimeType, function(xhr) {
                handler._registerXhr(id, xhr, api.putBlockList);
            })
                .then(function(xhr) {
                    log("Success combining chunks for id " + id);
                    promise.success({}, xhr);
                }, function(xhr) {
                    log("Attempt to combine chunks failed for id " + id, "error");

                    var azureError = qq.azure.util.parseAzureError(xhr.responseText, log),
                        errorMsg = "Problem sending file to Azure";

                    promise.failure({error: errorMsg,
                        azureError: azureError && azureError.message,
                        reset: xhr.status === 403
                    });
                });

        }, promise.failure);

        return promise;
    }

    qq.extend(this, {
        uploadChunk: function(id, chunkIdx) {
            var promise = new qq.Promise(),
                totalParts = handler._getFileState(id).chunking.parts,
                chunkAlreadyUploaded = handler._getChunkDataState(id).azureLastPartSuccess === chunkIdx;

            // If we have already successfully sent this chunk, the put block list likely failed,
            // and we should just retry that.
            if (chunkIdx + 1 === totalParts && chunkAlreadyUploaded) {
                combineChunks(id).then(promise.success, promise.failure);
            }
            else {
                getSignedUrl(id).then(
                    function(sasUri) {
                        var xhr = handler._createXhr(id),
                        chunkData = handler._getChunkData(id, chunkIdx);

                        handler._registerProgressHandler(id, chunkData.size);
                        handler._registerXhr(id, xhr, api.putBlock);

                        api.putBlock.upload(id, xhr, sasUri, chunkIdx, chunkData.blob).then(
                            function(blockId) {
                                if (!handler._getChunkDataState(id).blockIds) {
                                    handler._getChunkDataState(id).blockIds = [];
                                }

                                handler._getChunkDataState(id).blockIds.push(blockId);
                                handler._getChunkDataState(id).azureLastPartSuccess = chunkIdx;
                                log("Put Block call succeeded for " + id);

                                if (chunkIdx+1 === totalParts) {
                                    combineChunks(id).then(promise.success, promise.failure);
                                }
                                else {
                                    promise.success({}, xhr);
                                }
                            },
                            function() {
                                log(qq.format("Put Block call failed for ID {} on part {}", id, chunkIdx), "error");

                                var azureError = qq.azure.util.parseAzureError(xhr.responseText, log),
                                    errorMsg = "Problem sending file to Azure";

                                promise.failure({error: errorMsg,
                                    azureError: azureError && azureError.message,
                                    reset: xhr.status === 403
                                });
                            }
                        );
                    },
                    promise.failure
                );
            }

            return promise;
        },

        uploadFile: handleSimpleUpload
    });

    qq.extend(this, new qq.XhrUploadHandler({
            options: qq.extend({namespace: "azure"}, spec),
            proxy: qq.extend({getEndpoint: spec.endpointStore.get}, proxy)
        }
    ));

    qq.override(this, function(super_) {
        return {
            expunge: function(id) {
                var relatedToCancel = handler._getFileState(id).canceled,
                    chunkingData = handler._getFileState(id).chunking,
                    blockIds = (chunkingData && chunkingData.blockIds) || [];

                if (relatedToCancel && blockIds.length > 0) {
                    deleteBlob(id);
                }

                super_.expunge(id);
            },

            _shouldChunkThisFile: function(id) {
                var maybePossible = super_._shouldChunkThisFile(id);

                if (maybePossible && getSize(id) >= minFileSizeForChunking) {
                    return true;
                }
                return false;
            }
        };
    });
};
<commit_msg>chore($handler-cleanup): Azure handler cleanup #1157<commit_after>/*globals qq */
/**
 * Upload handler used by the upload to Azure module that depends on File API support, and, therefore, makes use of
 * `XMLHttpRequest` level 2 to upload `File`s and `Blob`s directly to Azure Blob Storage containers via the
 * associated Azure API.
 *
 * @param spec Options passed from the base handler
 * @param proxy Callbacks & methods used to query for or push out data/changes
 */
// TODO l18n for error messages returned to UI
qq.azure.XhrUploadHandler = function(spec, proxy) {
    "use strict";

    var handler = this,
        log = proxy.log,
        cors = spec.cors,
        endpointStore = spec.endpointStore,
        paramsStore = spec.paramsStore,
        signature = spec.signature,
        filenameParam = spec.filenameParam,
        minFileSizeForChunking = spec.chunking.minFileSize,
        deleteBlob = spec.deleteBlob,
        onGetBlobName = spec.onGetBlobName,
        getName = proxy.getName,
        getSize = proxy.getSize,

        getBlobMetadata = function(id) {
            var params = paramsStore.get(id);
            params[filenameParam] = getName(id);
            return params;
        },

        api = {
            putBlob: new qq.azure.PutBlob({
                getBlobMetadata: getBlobMetadata,
                log: log
            }),

            putBlock: new qq.azure.PutBlock({
                log: log
            }),

            putBlockList: new qq.azure.PutBlockList({
                getBlobMetadata: getBlobMetadata,
                log: log
            }),

            getSasForPutBlobOrBlock: new qq.azure.GetSas({
                cors: cors,
                customHeaders: signature.customHeaders,
                endpointStore: {
                    get: function() {
                        return signature.endpoint;
                    }
                },
                log: log,
                restRequestVerb: "PUT"
            })
        };

    function combineChunks(id) {
        var promise = new qq.Promise();

        getSignedUrl(id).then(function(sasUri) {
            var mimeType = handler._getMimeType(id),
                blockIds = handler._getFileState(id).chunking.blockIds;

            api.putBlockList.send(id, sasUri, blockIds, mimeType, function(xhr) {
                handler._registerXhr(id, xhr, api.putBlockList);
            })
                .then(function(xhr) {
                    log("Success combining chunks for id " + id);
                    promise.success({}, xhr);
                }, function(xhr) {
                    log("Attempt to combine chunks failed for id " + id, "error");
                    handleFailure(xhr, promise);
                });

        }, promise.failure);

        return promise;
    }

    function determineBlobUrl(id) {
        var containerUrl = endpointStore.get(id),
            promise = new qq.Promise(),
            getBlobNameSuccess = function(blobName) {
                handler._getFileState(id).key = blobName;
                promise.success(containerUrl + "/" + blobName);
            },
            getBlobNameFailure = function(reason) {
                promise.failure(reason);
            };

        onGetBlobName(id).then(getBlobNameSuccess, getBlobNameFailure);

        return promise;
    }

    function getSignedUrl(id) {
        var promise = new qq.Promise(),
            getSasSuccess = function(sasUri) {
                log("GET SAS request succeeded.");
                promise.success(sasUri);
            },
            getSasFailure = function(reason, getSasXhr) {
                log("GET SAS request failed: " + reason, "error");
                promise.failure({error: "Problem communicating with local server"}, getSasXhr);
            },
            determineBlobUrlSuccess = function(blobUrl) {
                api.getSasForPutBlobOrBlock.request(id, blobUrl).then(
                    getSasSuccess,
                    getSasFailure
                );
            },
            determineBlobUrlFailure = function(reason) {
                log(qq.format("Failed to determine blob name for ID {} - {}", id, reason), "error");
                promise.failure({error: reason});
            };

        determineBlobUrl(id).then(determineBlobUrlSuccess, determineBlobUrlFailure);

        return promise;
    }

    function handleFailure(xhr, promise) {
        var azureError = qq.azure.util.parseAzureError(xhr.responseText, log),
            errorMsg = "Problem sending file to Azure";

        promise.failure({error: errorMsg,
            azureError: azureError && azureError.message,
            reset: xhr.status === 403
        });
    }

    qq.extend(this, {
        uploadChunk: function(id, chunkIdx) {
            var promise = new qq.Promise(),
                totalParts = handler._getFileState(id).chunking.parts,
                chunkAlreadyUploaded = handler._getChunkDataState(id).azureLastPartSuccess === chunkIdx;

            // If we have already successfully sent this chunk, the put block list likely failed,
            // and we should just retry that.
            if (chunkIdx + 1 === totalParts && chunkAlreadyUploaded) {
                combineChunks(id).then(promise.success, promise.failure);
            }
            else {
                getSignedUrl(id).then(
                    function(sasUri) {
                        var xhr = handler._createXhr(id),
                        chunkData = handler._getChunkData(id, chunkIdx);

                        handler._registerProgressHandler(id, chunkData.size);
                        handler._registerXhr(id, xhr, api.putBlock);

                        api.putBlock.upload(id, xhr, sasUri, chunkIdx, chunkData.blob).then(
                            function(blockId) {
                                if (!handler._getChunkDataState(id).blockIds) {
                                    handler._getChunkDataState(id).blockIds = [];
                                }

                                handler._getChunkDataState(id).blockIds.push(blockId);
                                handler._getChunkDataState(id).azureLastPartSuccess = chunkIdx;
                                log("Put Block call succeeded for " + id);

                                if (chunkIdx+1 === totalParts) {
                                    combineChunks(id).then(promise.success, promise.failure);
                                }
                                else {
                                    promise.success({}, xhr);
                                }
                            },
                            function() {
                                log(qq.format("Put Block call failed for ID {} on part {}", id, chunkIdx), "error");
                                handleFailure(xhr, promise);
                            }
                        );
                    },
                    promise.failure
                );
            }

            return promise;
        },

        uploadFile: function(id) {
            var promise = new qq.Promise(),
                fileOrBlob = handler.getFile(id);

            getSignedUrl(id).then(function(sasUri) {
                var xhr = handler._createXhr(id);

                handler._registerProgressHandler(id);

                api.putBlob.upload(id, xhr, sasUri, fileOrBlob).then(
                    function() {
                        log("Put Blob call succeeded for " + id);
                        promise.success({}, xhr);
                    },
                    function() {
                        log("Put Blob call failed for " + id, "error");
                        handleFailure(xhr, promise);
                    }
                );
            }, promise.failure);

            return promise;
        }
    });

    qq.extend(this, new qq.XhrUploadHandler({
            options: qq.extend({namespace: "azure"}, spec),
            proxy: qq.extend({getEndpoint: spec.endpointStore.get}, proxy)
        }
    ));

    qq.override(this, function(super_) {
        return {
            expunge: function(id) {
                var relatedToCancel = handler._getFileState(id).canceled,
                    chunkingData = handler._getFileState(id).chunking,
                    blockIds = (chunkingData && chunkingData.blockIds) || [];

                if (relatedToCancel && blockIds.length > 0) {
                    deleteBlob(id);
                }

                super_.expunge(id);
            },

            _shouldChunkThisFile: function(id) {
                var maybePossible = super_._shouldChunkThisFile(id);

                if (maybePossible && getSize(id) >= minFileSizeForChunking) {
                    return true;
                }
                return false;
            }
        };
    });
};
<|endoftext|>


<commit_before>import { Field, Formik } from 'formik'
import React, { Component } from 'react'
import {
  Button,
  ButtonToolbar,
  ControlLabel,
  FormControl,
  FormGroup,
  HelpBlock,
  Label,
  ToggleButton,
  ToggleButtonGroup
} from 'react-bootstrap'
import { formatPhoneNumber, isPossiblePhoneNumber } from 'react-phone-number-input'
import Input from 'react-phone-number-input/input'
import styled, { css } from 'styled-components'
import * as yup from 'yup'

import { isBlank } from '../../util/ui'

const allowedNotificationChannels = [
  {
    type: 'email',
    text: 'Email'
  },
  {
    type: 'sms',
    text: 'SMS'
  },
  {
    type: 'none',
    text: 'Don\'t notify me'
  }
]

// Styles
// HACK: Preverve container height.
const Details = styled.div`
  min-height: 150px;
  margin-bottom: 15px;
`
const ControlStrip = styled.div`
  > * {
    margin-right: 4px;
  }
`
const phoneFieldCss = css`
  display: inline-block;
  vertical-align: middle;
  width: 14em;
`
const InlineTextInput = styled(FormControl)`
  ${phoneFieldCss}
`
const InlineStatic = styled(FormControl.Static)`
  ${phoneFieldCss}
`
const InlinePhoneInput = styled(Input)`
  ${phoneFieldCss}
`

const FlushLink = styled(Button)`
  padding-left: 0;
  padding-right: 0;
`

const INVALID_PHONE_MSG = 'Please enter a valid phone number.'

/**
 * @param {*} props The props from which to extract the Formik state to test.
 * @param {*} field THe field name to test.
 * @returns One of the Bootstrao validationState values.
 */
function getErrorState (props, field) {
  const { errors, touched, values } = props
  const value = values[field]
  const isInvalid = isBlank(value) || !!errors[field]

  // one of the Bootstrap validationState values.
  return isInvalid && touched[field]
    ? 'error'
    : null
}

const codeValidationSchema = yup.object({
  validationCode: yup.string().required('Please enter a validation code.')
})

/**
 * User notification preferences pane.
 */
class NotificationPrefsPane extends Component {
  _handleOnPhoneVerified = ({ isPhoneNumberVerified, phoneNumber }) => {
    const { setFieldValue } = this.props

    // Update the parent's Formik state (so when saving, it sends the updated numbers)
    setFieldValue('isPhoneNumberVerified', isPhoneNumberVerified)
    setFieldValue('phoneNumber', phoneNumber)
  }

  render () {
    // All props below are Formik props (https://formik.org/docs/api/formik#props-1)
    // except * below which indicates the prop is provided by UserAccountScreen.
    const {
      handleBlur,
      handleChange,
      onRequestPhoneVerificationCode, // *
      onSendPhoneVerificationCode, // *
      phoneFormatOptions, // *
      values: userData
    } = this.props

    const {
      email,
      isPhoneNumberVerified,
      notificationChannel,
      phoneNumber
    } = userData

    const initialFormikValues = {
      validationCode: ''
    }

    return (

          You can receive notifications about trips you frequently take.

        <FormGroup>
          <ControlLabel>How would you like to receive notifications?</ControlLabel>
          <ButtonToolbar>
            <ToggleButtonGroup
              name='notificationChannel'
              type='radio'
              defaultValue={notificationChannel}
            >
              {allowedNotificationChannels.map(({ type, text }, index) => (
                // FIXME: If removing the Save/Cancel buttons on the account screen,
                // persist changes immediately when onChange is triggered.
                <ToggleButton
                  bsStyle={notificationChannel === type ? 'primary' : 'default'}
                  key={index}
                  // onBlur and onChange have to be set on individual controls instead of the control group
                  // in order for Formik to correcly process the changes.
                  onBlur={handleBlur}
                  onChange={handleChange}
                  value={type}
                >
                  {text}
                </ToggleButton>
              ))}
            </ToggleButtonGroup>
          </ButtonToolbar>
        </FormGroup>

          {notificationChannel === 'email' && (
            <FormGroup>
              <ControlLabel>Notification emails will be sent to:</ControlLabel>
              <FormControl.Static>{email}</FormControl.Static>
            </FormGroup>
          )}
          {notificationChannel === 'sms' && (
            <Formik
              // Phone number needs to be validated on change.
              validateOnChange
              validationSchema={codeValidationSchema}
              initialValues={initialFormikValues}
            >
              {
                // Pass Formik props to the component rendered so Formik can manage its validation.
                // (The validation for this component is independent of the validation set in UserAccountScreen.)
                innerProps => {
                  return (
                    <PhoneNumberEditor
                      {...innerProps}
                      initialPhoneNumber={phoneNumber}
                      initialPhoneNumberVerified={isPhoneNumberVerified}
                      onRequestCode={onRequestPhoneVerificationCode}
                      onSubmitCode={onSendPhoneVerificationCode}
                      onVerified={this._handleOnPhoneVerified}
                      phoneFormatOptions={phoneFormatOptions}
                    />
                  )
                }
              }
            </Formik>
          )}

    )
  }
}

export default NotificationPrefsPane

/**
 * Sub-component that handles phone number and validation code editing and validation intricacies.
 */
class PhoneNumberEditor extends Component {
  constructor (props) {
    super(props)

    const { initialPhoneNumber, initialPhoneNumberVerified } = props
    this.state = {
      // Holds the last known verified phone number.
      currentNumber: initialPhoneNumber,

      // The verified status of the above number.
      currentNumberVerified: initialPhoneNumberVerified,

      // If true, phone number is being edited.
      // For new users, render component in editing state.
      isEditing: isBlank(initialPhoneNumber),

      // If true, a phone verification request has been sent and the UI is awaiting the user sending the code.
      isPending: false,

      // Holds the new phone number (+15555550123 format) entered by the user
      // (outside of Formik because (Phone)Input does not have a standard onChange event or simple valitity test).
      newPhoneNumber: ''
    }
  }

  _handleEditNumber = () => {
    this.setState({
      isEditing: true,
      isPending: false,
      newPhoneNumber: ''
    })
  }

  _handleNewPhoneNumberChange = newPhoneNumber => {
    this.setState({
      newPhoneNumber
    })
  }

  _handleCancelEditNumber = () => {
    this.setState({
      isEditing: false,
      isPending: false
    })
  }

  _handlePhoneNumberKeyDown = e => {
    if (e.keyCode === 13) {
      // On the user pressing enter (keyCode 13) on the phone number field,
      // prevent form submission and request the code.
      e.preventDefault()
      this._handleRequestCode()
    }
  }

  _handleValidationCodeKeyDown = e => {
    if (e.keyCode === 13) {
      // On the user pressing enter (keyCode 13) on the validation code field,
      // prevent form submission and send the validation code.
      e.preventDefault()
      this._handleSubmitCode()
    }
  }

  _handleRequestCode = async () => {
    // Send phone verification request with the entered values.
    const { onRequestCode, setFieldValue, setFieldTouched } = this.props
    const { newPhoneNumber } = this.state

    // Empty the input box (and disable the Send text button).
    this.setState({
      newPhoneNumber: ''
    })

    const result = await onRequestCode(newPhoneNumber)

    if (result && result.phoneNumber === newPhoneNumber && !result.isPhoneNumberVerified) {
      // On success (phoneNumber is updated and verified status is false), update the phone number in component state.
      this.setState({
        newPhoneNumber: result.phoneNumber
      })

      // Set empty code field in Formik's state and prompt for code.
      setFieldValue('validationCode', '')
      setFieldTouched('validationCode', false)
      this.setState({
        isEditing: false,
        isPending: true
      })
    }
  }

  _handleSubmitCode = async () => {
    const { onSubmitCode, onVerified, setFieldValue, values } = this.props
    const { validationCode } = values

    // Clear the code field and disable the verify button.
    // (The code to send is already captured in const above.)
    setFieldValue('validationCode', '')

    const result = await onSubmitCode(validationCode)

    if (result && result.isPhoneNumberVerified) {
      // On success (status is changed to verified), update the phone number in component state and hide the code field.
      this.setState({
        currentNumber: result.phoneNumber,
        currentNumberVerified: true,
        isPending: false,
        newPhoneNumber: ''
      })

      onVerified(result)
    }
  }

  render () {
    // All props below are Formik props (https://formik.org/docs/api/formik#props-1)
    // except where indicated.
    const {
      errors,
      phoneFormatOptions, // Provided by parent.
      touched
    } = this.props
    const { currentNumber, currentNumberVerified, isEditing, isPending, newPhoneNumber } = this.state

    // Here are the states we are dealing with:
    // - First time entering a phone number/validation code (blank value, not modified)
    //   => no color, no feedback indication.
    // - Typing backspace all the way to erase a number/code (blank value, modified)
    //   => red error.
    // - Typing a phone number that doesn't match the configured phoneNumberRegEx
    //   => red error.
    const isPhoneInvalid = !isBlank(newPhoneNumber) && !isPossiblePhoneNumber(newPhoneNumber)
    const phoneErrorState = isPhoneInvalid ? 'error' : null
    const codeErrorState = getErrorState(this.props, 'validationCode')

    return (
      <>
        {isEditing
          ? (
            // FIXME: If removing the Save/Cancel buttons on the account screen,
            // make this <FormGroup> a <form> and remove onKeyDown handler.
            <FormGroup validationState={phoneErrorState}>
              <ControlLabel>Enter your phone number for SMS notifications:</ControlLabel>
              <ControlStrip>
                <InlinePhoneInput
                  className='form-control'
                  country={phoneFormatOptions.countryCode}
                  onChange={this._handleNewPhoneNumberChange}
                  onKeyDown={this._handlePhoneNumberKeyDown}
                  placeholder='Enter phone number'
                  readOnly={!isEditing}
                  type='tel'
                  value={newPhoneNumber}
                />
                <Button
                  bsStyle='primary'
                  disabled={isPhoneInvalid}
                  onClick={this._handleRequestCode}
                >
                  Send verification text
                </Button>
                { // Show cancel button only if a phone number is already recorded.
                  currentNumber && <Button onClick={this._handleCancelEditNumber}>Cancel</Button>}
                {isPhoneInvalid && <HelpBlock>{INVALID_PHONE_MSG}</HelpBlock>}
              </ControlStrip>
            </FormGroup>
          ) : (
            <FormGroup>
              <ControlLabel>SMS notifications will be sent to:</ControlLabel>
              <ControlStrip>
                <InlineStatic>
                  {isPending
                    // eslint-disable-next-line jsx-a11y/label-has-for
                    ? <>{formatPhoneNumber(newPhoneNumber)} Pending</>
                    // eslint-disable-next-line jsx-a11y/label-has-for
                    : <>{formatPhoneNumber(currentNumber)} {currentNumberVerified && Verified}</>
                  }
                </InlineStatic>
                <Button onClick={this._handleEditNumber}>Change number</Button>
              </ControlStrip>
            </FormGroup>
          )}

        {isPending && (
          // FIXME: If removing the Save/Cancel buttons on the account screen,
          // make this <FormGroup> a <form> and remove onKeyDown handler.
          <FormGroup validationState={codeErrorState}>

              Please check the SMS messaging app on your mobile phone
              for a text message with a verification code, and enter the code below.

            <ControlLabel>Verification code:</ControlLabel>
            <ControlStrip>
              <Field as={InlineTextInput}
                name='validationCode'
                onKeyDown={this._handleValidationCodeKeyDown}
                placeholder='123456'
                // onBlur, onChange, and value are passed automatically by Formik
              />
              <Button
                bsStyle='primary'
                disabled={!!errors.validationCode}
                onClick={this._handleSubmitCode}
              >
                Verify
              </Button>
              {touched.validationCode && errors.validationCode && (
                <HelpBlock>{errors.validationCode}</HelpBlock>
              )}
            </ControlStrip>
            <FlushLink bsStyle='link' onClick={this._handleRequestCode}>Request a new code</FlushLink>
          </FormGroup>
        )}
      </>
    )
  }
}
<commit_msg>refactor(PhoneNumberEditor): Exit edit mode if user enters same number as current.<commit_after>import { Field, Formik } from 'formik'
import React, { Component } from 'react'
import {
  Button,
  ButtonToolbar,
  ControlLabel,
  FormControl,
  FormGroup,
  HelpBlock,
  Label,
  ToggleButton,
  ToggleButtonGroup
} from 'react-bootstrap'
import { formatPhoneNumber, isPossiblePhoneNumber } from 'react-phone-number-input'
import Input from 'react-phone-number-input/input'
import styled, { css } from 'styled-components'
import * as yup from 'yup'

import { isBlank } from '../../util/ui'

const allowedNotificationChannels = [
  {
    type: 'email',
    text: 'Email'
  },
  {
    type: 'sms',
    text: 'SMS'
  },
  {
    type: 'none',
    text: 'Don\'t notify me'
  }
]

// Styles
// HACK: Preverve container height.
const Details = styled.div`
  min-height: 150px;
  margin-bottom: 15px;
`
const ControlStrip = styled.div`
  > * {
    margin-right: 4px;
  }
`
const phoneFieldCss = css`
  display: inline-block;
  vertical-align: middle;
  width: 14em;
`
const InlineTextInput = styled(FormControl)`
  ${phoneFieldCss}
`
const InlineStatic = styled(FormControl.Static)`
  ${phoneFieldCss}
`
const InlinePhoneInput = styled(Input)`
  ${phoneFieldCss}
`

const FlushLink = styled(Button)`
  padding-left: 0;
  padding-right: 0;
`

const INVALID_PHONE_MSG = 'Please enter a valid phone number.'

/**
 * @param {*} props The props from which to extract the Formik state to test.
 * @param {*} field THe field name to test.
 * @returns One of the Bootstrao validationState values.
 */
function getErrorState (props, field) {
  const { errors, touched, values } = props
  const value = values[field]
  const isInvalid = isBlank(value) || !!errors[field]

  // one of the Bootstrap validationState values.
  return isInvalid && touched[field]
    ? 'error'
    : null
}

const codeValidationSchema = yup.object({
  validationCode: yup.string().required('Please enter a validation code.')
})

/**
 * User notification preferences pane.
 */
class NotificationPrefsPane extends Component {
  _handleOnPhoneVerified = ({ isPhoneNumberVerified, phoneNumber }) => {
    const { setFieldValue } = this.props

    // Update the parent's Formik state (so when saving, it sends the updated numbers)
    setFieldValue('isPhoneNumberVerified', isPhoneNumberVerified)
    setFieldValue('phoneNumber', phoneNumber)
  }

  render () {
    // All props below are Formik props (https://formik.org/docs/api/formik#props-1)
    // except * below which indicates the prop is provided by UserAccountScreen.
    const {
      handleBlur,
      handleChange,
      onRequestPhoneVerificationCode, // *
      onSendPhoneVerificationCode, // *
      phoneFormatOptions, // *
      values: userData
    } = this.props

    const {
      email,
      isPhoneNumberVerified,
      notificationChannel,
      phoneNumber
    } = userData

    const initialFormikValues = {
      validationCode: ''
    }

    return (

          You can receive notifications about trips you frequently take.

        <FormGroup>
          <ControlLabel>How would you like to receive notifications?</ControlLabel>
          <ButtonToolbar>
            <ToggleButtonGroup
              name='notificationChannel'
              type='radio'
              defaultValue={notificationChannel}
            >
              {allowedNotificationChannels.map(({ type, text }, index) => (
                // FIXME: If removing the Save/Cancel buttons on the account screen,
                // persist changes immediately when onChange is triggered.
                <ToggleButton
                  bsStyle={notificationChannel === type ? 'primary' : 'default'}
                  key={index}
                  // onBlur and onChange have to be set on individual controls instead of the control group
                  // in order for Formik to correcly process the changes.
                  onBlur={handleBlur}
                  onChange={handleChange}
                  value={type}
                >
                  {text}
                </ToggleButton>
              ))}
            </ToggleButtonGroup>
          </ButtonToolbar>
        </FormGroup>

          {notificationChannel === 'email' && (
            <FormGroup>
              <ControlLabel>Notification emails will be sent to:</ControlLabel>
              <FormControl.Static>{email}</FormControl.Static>
            </FormGroup>
          )}
          {notificationChannel === 'sms' && (
            <Formik
              // Phone number needs to be validated on change.
              validateOnChange
              validationSchema={codeValidationSchema}
              initialValues={initialFormikValues}
            >
              {
                // Pass Formik props to the component rendered so Formik can manage its validation.
                // (The validation for this component is independent of the validation set in UserAccountScreen.)
                innerProps => {
                  return (
                    <PhoneNumberEditor
                      {...innerProps}
                      initialPhoneNumber={phoneNumber}
                      initialPhoneNumberVerified={isPhoneNumberVerified}
                      onRequestCode={onRequestPhoneVerificationCode}
                      onSubmitCode={onSendPhoneVerificationCode}
                      onVerified={this._handleOnPhoneVerified}
                      phoneFormatOptions={phoneFormatOptions}
                    />
                  )
                }
              }
            </Formik>
          )}

    )
  }
}

export default NotificationPrefsPane

/**
 * Sub-component that handles phone number and validation code editing and validation intricacies.
 */
class PhoneNumberEditor extends Component {
  constructor (props) {
    super(props)

    const { initialPhoneNumber, initialPhoneNumberVerified } = props
    this.state = {
      // Holds the last known verified phone number.
      currentNumber: initialPhoneNumber,

      // The verified status of the above number.
      currentNumberVerified: initialPhoneNumberVerified,

      // If true, phone number is being edited.
      // For new users, render component in editing state.
      isEditing: isBlank(initialPhoneNumber),

      // If true, a phone verification request has been sent and the UI is awaiting the user sending the code.
      isPending: false,

      // Holds the new phone number (+15555550123 format) entered by the user
      // (outside of Formik because (Phone)Input does not have a standard onChange event or simple valitity test).
      newPhoneNumber: ''
    }
  }

  _handleEditNumber = () => {
    this.setState({
      isEditing: true,
      isPending: false,
      newPhoneNumber: ''
    })
  }

  _handleNewPhoneNumberChange = newPhoneNumber => {
    this.setState({
      newPhoneNumber
    })
  }

  _handleCancelEditNumber = () => {
    this.setState({
      isEditing: false,
      isPending: false
    })
  }

  _handlePhoneNumberKeyDown = e => {
    if (e.keyCode === 13) {
      // On the user pressing enter (keyCode 13) on the phone number field,
      // prevent form submission and request the code.
      e.preventDefault()
      this._handleRequestCode()
    }
  }

  _handleValidationCodeKeyDown = e => {
    if (e.keyCode === 13) {
      // On the user pressing enter (keyCode 13) on the validation code field,
      // prevent form submission and send the validation code.
      e.preventDefault()
      this._handleSubmitCode()
    }
  }

  _handleRequestCode = async () => {
    // Send phone verification request with the entered values.
    const { onRequestCode, setFieldValue, setFieldTouched } = this.props
    const { currentNumber, currentNumberVerified, newPhoneNumber } = this.state

    // If user enters the same phone number as their current verified number,
    // just cancel editing without sending a verification SMS.
    if (newPhoneNumber === currentNumber && currentNumberVerified) {
      this.setState({
        isEditing: false,
        isPending: false,
        newPhoneNumber: ''
      })
    } else {
      // Empty the input box (and disable the Send text button).
      this.setState({
        newPhoneNumber: ''
      })

      const result = await onRequestCode(newPhoneNumber)

      if (result && result.phoneNumber === newPhoneNumber && !result.isPhoneNumberVerified) {
        // On success (phoneNumber is updated and verified status is false), update the phone number in component state.
        this.setState({
          newPhoneNumber: result.phoneNumber
        })

        // Set empty code field in Formik's state and prompt for code.
        setFieldValue('validationCode', '')
        setFieldTouched('validationCode', false)
        this.setState({
          isEditing: false,
          isPending: true
        })
      }
    }
  }

  _handleSubmitCode = async () => {
    const { onSubmitCode, onVerified, setFieldValue, values } = this.props
    const { validationCode } = values

    // Clear the code field and disable the verify button.
    // (The code to send is already captured in const above.)
    setFieldValue('validationCode', '')

    const result = await onSubmitCode(validationCode)

    if (result && result.isPhoneNumberVerified) {
      // On success (status is changed to verified), update the phone number in component state and hide the code field.
      this.setState({
        currentNumber: result.phoneNumber,
        currentNumberVerified: true,
        isPending: false,
        newPhoneNumber: ''
      })

      onVerified(result)
    }
  }

  render () {
    // All props below are Formik props (https://formik.org/docs/api/formik#props-1)
    // except where indicated.
    const {
      errors,
      phoneFormatOptions, // Provided by parent.
      touched
    } = this.props
    const { currentNumber, currentNumberVerified, isEditing, isPending, newPhoneNumber } = this.state

    // Here are the states we are dealing with:
    // - First time entering a phone number/validation code (blank value, not modified)
    //   => no color, no feedback indication.
    // - Typing backspace all the way to erase a number/code (blank value, modified)
    //   => red error.
    // - Typing a phone number that doesn't match the configured phoneNumberRegEx
    //   => red error.
    const isPhoneInvalid = !isPossiblePhoneNumber(newPhoneNumber)
    const showPhoneError = isPhoneInvalid && !isBlank(newPhoneNumber)
    const phoneErrorState = showPhoneError ? 'error' : null
    const codeErrorState = getErrorState(this.props, 'validationCode')

    return (
      <>
        {isEditing
          ? (
            // FIXME: If removing the Save/Cancel buttons on the account screen,
            // make this <FormGroup> a <form> and remove onKeyDown handler.
            <FormGroup validationState={phoneErrorState}>
              <ControlLabel>Enter your phone number for SMS notifications:</ControlLabel>
              <ControlStrip>
                <InlinePhoneInput
                  className='form-control'
                  country={phoneFormatOptions.countryCode}
                  onChange={this._handleNewPhoneNumberChange}
                  onKeyDown={this._handlePhoneNumberKeyDown}
                  placeholder='Enter phone number'
                  readOnly={!isEditing}
                  type='tel'
                  value={newPhoneNumber}
                />
                <Button
                  bsStyle='primary'
                  disabled={isPhoneInvalid}
                  onClick={this._handleRequestCode}
                >
                  Send verification text
                </Button>
                { // Show cancel button only if a phone number is already recorded.
                  currentNumber && <Button onClick={this._handleCancelEditNumber}>Cancel</Button>}
                {showPhoneError && <HelpBlock>{INVALID_PHONE_MSG}</HelpBlock>}
              </ControlStrip>
            </FormGroup>
          ) : (
            <FormGroup>
              <ControlLabel>SMS notifications will be sent to:</ControlLabel>
              <ControlStrip>
                <InlineStatic>
                  {isPending
                    // eslint-disable-next-line jsx-a11y/label-has-for
                    ? <>{formatPhoneNumber(newPhoneNumber)} Pending</>
                    // eslint-disable-next-line jsx-a11y/label-has-for
                    : <>{formatPhoneNumber(currentNumber)} {currentNumberVerified && Verified}</>
                  }
                </InlineStatic>
                <Button onClick={this._handleEditNumber}>Change number</Button>
              </ControlStrip>
            </FormGroup>
          )}

        {isPending && (
          // FIXME: If removing the Save/Cancel buttons on the account screen,
          // make this <FormGroup> a <form> and remove onKeyDown handler.
          <FormGroup validationState={codeErrorState}>

              Please check the SMS messaging app on your mobile phone
              for a text message with a verification code, and enter the code below.

            <ControlLabel>Verification code:</ControlLabel>
            <ControlStrip>
              <Field as={InlineTextInput}
                name='validationCode'
                onKeyDown={this._handleValidationCodeKeyDown}
                placeholder='123456'
                // onBlur, onChange, and value are passed automatically by Formik
              />
              <Button
                bsStyle='primary'
                disabled={!!errors.validationCode}
                onClick={this._handleSubmitCode}
              >
                Verify
              </Button>
              {touched.validationCode && errors.validationCode && (
                <HelpBlock>{errors.validationCode}</HelpBlock>
              )}
            </ControlStrip>
            <FlushLink bsStyle='link' onClick={this._handleRequestCode}>Request a new code</FlushLink>
          </FormGroup>
        )}
      </>
    )
  }
}
<|endoftext|>


<commit_before>//Plugin for Babel's parser (Babylon)

//TEMP
const log = console.log;

import { types as tt } from "babylon/lib/tokenizer/types";
import Parser from "babylon/lib/parser";

export default function (instance) {
	instance.extend("parseExpressionStatement", function (inner) {
		return function (node, expr) {
		  if (expr.type === "Identifier") {
			if (expr.name === "context") {
			  return this.dci_parseContext(node);
			}
			else if (expr.name === "role") {
			  return this.dci_parseRole(node);
			}
		  }

		  return inner.call(this, node, expr);
		};
	});

	instance.extend("parseExport", function (inner) {
		return function(node) {
			//Copied and pasted from babylon/src/parser/statement.js
			//with small modifications to allow DCI contexts declared with the `context` keyword
			//to be exported (modifications indicated with 'DCI' comments).

			this.next();
			// export * from '...'
			if (this.match(tt.star)) {
			  let specifier = this.startNode();
			  this.next();
			  if (this.hasPlugin("exportExtensions") && this.eatContextual("as")) {
				specifier.exported = this.parseIdentifier();
				node.specifiers = [this.finishNode(specifier, "ExportNamespaceSpecifier")];
				this.parseExportSpecifiersMaybe(node);
				this.parseExportFrom(node, true);
			  } else {
				this.parseExportFrom(node, true);
				return this.finishNode(node, "ExportAllDeclaration");
			  }
			} else if (this.hasPlugin("exportExtensions") && this.isExportDefaultSpecifier()) {
			  let specifier = this.startNode();
			  specifier.exported = this.parseIdentifier(true);
			  node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")];
			  if (this.match(tt.comma) && this.lookahead().type === tt.star) {
				this.expect(tt.comma);
				let specifier = this.startNode();
				this.expect(tt.star);
				this.expectContextual("as");
				specifier.exported = this.parseIdentifier();
				node.specifiers.push(this.finishNode(specifier, "ExportNamespaceSpecifier"));
			  } else {
				this.parseExportSpecifiersMaybe(node);
			  }
			  this.parseExportFrom(node, true);
			} else if (this.eat(tt._default)) { // export default ...
			  let expr = this.startNode();
			  let needsSemi = false;
			  if (this.eat(tt._function)) {
				expr = this.parseFunction(expr, true, false, false, true);
			  } else if (this.match(tt._class)) {
				expr = this.parseClass(expr, true, true);

			  //DCI
			  //ADDED FOR DCI
			  } else if (this.state.value === 'context') {
				this.next();
				expr = this.dci_parseContext(expr);

			  } else {
				needsSemi = true;
				expr = this.parseMaybeAssign();
			  }
			  node.declaration = expr;
			  if (needsSemi) this.semicolon();
			  this.checkExport(node);
			  return this.finishNode(node, "ExportDefaultDeclaration");

			//DCI (modified)
			} else if (this.state.type.keyword || this.state.value === 'context' || this.shouldParseExportDeclaration()) {
			//} else if (this.state.type.keyword || this.shouldParseExportDeclaration()) {

			  node.specifiers = [];
			  node.source = null;
			  node.declaration = this.parseExportDeclaration(node);
			} else { // export { x, y as z } [from '...']
			  node.declaration = null;
			  node.specifiers = this.parseExportSpecifiers();
			  this.parseExportFrom(node);
			}
			this.checkExport(node);
			return this.finishNode(node, "ExportNamedDeclaration");
		};
	});
}

let pp = Parser.prototype;

// Roles

pp.dci_parseRole = function (node) {
	node.id = this.parseIdentifier();
	this.dci_parseRoleBody(node);
	return this.finishNode(node, "RoleDeclaration");
};

//Parse role body
//Adapted from parseClassBody(),
//https://github.com/babel/babel/blob/master/packages/babylon/src/parser/statement.js
pp.dci_parseRoleBody = function (node) {
	let roleBody = this.startNode();
	roleBody.body = [];

	this.expect(tt.braceL);

	while (!this.eat(tt.braceR)) {
		if (this.eat(tt.semi)) {
		  continue;
		}

		let method = this.startNode();

		let isGenerator = this.eat(tt.star);
		let isGetSet = false;
		let isAsync = false;

	    this.parsePropertyName(method);

		let isAsyncMethod = this.hasPlugin("asyncFunctions") && !this.match(tt.parenL) && !method.computed && method.key.type === "Identifier" && method.key.name === "async";
		if (isAsyncMethod) {
		  if (this.hasPlugin("asyncGenerators") && this.eat(tt.star)) isGenerator = true;
		  isAsync = true;
		  this.parsePropertyName(method);
		}

		method.kind = "method";

		if (!method.computed) {
		  let { key } = method;

		  // handle get/set methods
		  // eg. class Foo { get bar() {} set bar() {} }
		  if (!isAsync && !isGenerator && key.type === "Identifier" && !this.match(tt.parenL) && (key.name === "get" || key.name === "set")) {
			isGetSet = true;
			method.kind = key.name;
			key = this.parsePropertyName(method);
		  }
		}

		this.dci_parseRoleMethod(roleBody, method, isGenerator, isAsync);

		// get methods aren't allowed to have any parameters
		// set methods must have exactly 1 parameter
		if (isGetSet) {
		  let paramCount = method.kind === "get" ? 0 : 1;
		  if (method.params.length !== paramCount) {
			let start = method.start;
			if (method.kind === "get") {
			  this.raise(start, "getter should have no params");
			} else {
			  this.raise(start, "setter should have exactly one param");
			}
		  }
		}
	}

	node.body = this.finishNode(roleBody, "RoleDeclarationBody");
};

pp.dci_parseRoleMethod = function (roleBody, method, isGenerator, isAsync) {
  this.parseMethod(method, isGenerator, isAsync);
  roleBody.body.push(this.finishNode(method, "RoleMethod"));
};

// Context declarations (context MyContext {...})

pp.dci_parseContext = function (node) {
  node.id = this.parseIdentifier();
  this.dci_parseContextBody(node);
  return this.finishNode(node, "ContextDeclaration");
};

//Parse context body
//Adapted from parseClassBody(),
//https://github.com/babel/babel/blob/master/packages/babylon/src/parser/statement.js
pp.dci_parseContextBody = function (node) {
	// context bodies are implicitly strict
	let oldStrict = this.state.strict;
	this.state.strict = true;

	let hadConstructorCall = false;
	let hadConstructor = false;
	let decorators = [];
	let ctxBody = this.startNode();
	ctxBody.body = [];

	this.expect(tt.braceL);

	while (!this.eat(tt.braceR)) {
		if (this.eat(tt.semi)) {
		  continue;
		}

		//TODO?
		//annotations
		/*
		if (this.match(tt.at)) {
		}
		*/

    	let member = this.startNode();
		this.parsePropertyName(member);

		if (member.key.type === 'Identifier' && member.key.name === 'role') {
			ctxBody.body.push(this.dci_parseRole(member));
			continue;
		}

		let method = member;

		//let isConstructorCall = false;
		let isGenerator = this.eat(tt.star);
		let isGetSet = false;
		let isAsync = false;

		if (!isGenerator && method.key.type === "Identifier" && !method.computed) {
		  if (this.dci_isContextProperty()) {
			ctxBody.body.push(this.dci_parseContextProperty(method));
			continue;
		  }

		  /*
		  if (this.hasPlugin("classConstructorCall") && method.key.name === "call" && this.match(tt.name) && this.state.value === "constructor") {
			isConstructorCall = true;
			this.parsePropertyName(method);
		  }
		  */
		}

		let isAsyncMethod = this.hasPlugin("asyncFunctions") && !this.match(tt.parenL) && !method.computed && method.key.type === "Identifier" && method.key.name === "async";
		if (isAsyncMethod) {
		  if (this.hasPlugin("asyncGenerators") && this.eat(tt.star)) isGenerator = true;
		  isAsync = true;
		  this.parsePropertyName(method);
		}

		method.kind = "method";

		if (!method.computed) {
			let { key } = method;

			// handle get/set methods
			// eg. class Foo { get bar() {} set bar() {} }
			if (!isAsync && !isGenerator && key.type === "Identifier" && !this.match(tt.parenL) && (key.name === "get" || key.name === "set")) {
			  isGetSet = true;
			  method.kind = key.name;
			  key = this.parsePropertyName(method);
			}

			// disallow invalid constructors
			let isConstructor = (key.type === "Identifier" && key.name === "constructor")
				|| (key.type === "StringLiteral" && key.value === "constructor");
			/*
			let isConstructor = !isConstructorCall && (
			  (key.type === "Identifier" && key.name === "constructor") ||
			  (key.type === "StringLiteral" && key.value === "constructor")
			);
			*/
			if (isConstructor) {
			  if (hadConstructor) this.raise(key.start, "Duplicate constructor in the same DCI context");
			  if (isGetSet) this.raise(key.start, "Constructor can't have get/set modifier");
			  if (isGenerator) this.raise(key.start, "Constructor can't be a generator");
			  if (isAsync) this.raise(key.start, "Constructor can't be an async function");
			  method.kind = "constructor";
			  hadConstructor = true;
			}
		}

		/*
		// convert constructor to a constructor call
		if (isConstructorCall) {
		  if (hadConstructorCall) this.raise(method.start, "Duplicate constructor call in the same DCI context");
		  method.kind = "constructorCall";
		  hadConstructorCall = true;
		}
		*/

		// disallow decorators on class constructors
		if ((method.kind === "constructor" || method.kind === "constructorCall") && method.decorators) {
		  this.raise(method.start, "You can't attach decorators to a DCI context constructor");
		}

		this.dci_parseContextMethod(ctxBody, method, isGenerator, isAsync);

		// get methods aren't allowed to have any parameters
		// set methods must have exactly 1 parameter
		if (isGetSet) {
		  let paramCount = method.kind === "get" ? 0 : 1;
		  if (method.params.length !== paramCount) {
			let start = method.start;
			if (method.kind === "get") {
			  this.raise(start, "getter should have no params");
			} else {
			  this.raise(start, "setter should have exactly one param");
			}
		  }
		}
	}

	if (decorators.length) {
	  this.raise(this.state.start, "You have trailing decorators with no method");
	}

	node.body = this.finishNode(ctxBody, "ContextBody");

	this.state.strict = oldStrict;
};

pp.dci_parseContextMethod = function (ctxBody, method, isGenerator, isAsync) {
  this.parseMethod(method, isGenerator, isAsync);
  ctxBody.body.push(this.finishNode(method, "ContextMethod"));
};

pp.dci_isContextProperty = function () {
  return this.match(tt.eq) || this.isLineTerminator();
};

pp.dci_parseContextProperty = function (node) {
  if (this.match(tt.eq)) {
    this.next();
    node.value = this.parseMaybeAssign();
  } else {
    node.value = null;
  }
  this.semicolon();
  return this.finishNode(node, "ContextProperty");
};<commit_msg>added parsing of role-player contracts<commit_after>//Plugin for Babel's parser (Babylon)

//TEMP
const log = console.log;

import { types as tt } from "babylon/lib/tokenizer/types";
import Parser from "babylon/lib/parser";

export default function (instance) {
	instance.extend("parseExpressionStatement", function (inner) {
		return function (node, expr) {
		  if (expr.type === "Identifier") {
			if (expr.name === "context") {
			  return this.dci_parseContext(node);
			}
			else if (expr.name === "role") {
			  return this.dci_parseRole(node);
			}
		  }

		  return inner.call(this, node, expr);
		};
	});

	instance.extend("parseExport", function (inner) {
		return function(node) {
			//Copied and pasted from babylon/src/parser/statement.js
			//with small modifications to allow DCI contexts declared with the `context` keyword
			//to be exported (modifications indicated with 'DCI' comments).

			this.next();
			// export * from '...'
			if (this.match(tt.star)) {
			  let specifier = this.startNode();
			  this.next();
			  if (this.hasPlugin("exportExtensions") && this.eatContextual("as")) {
				specifier.exported = this.parseIdentifier();
				node.specifiers = [this.finishNode(specifier, "ExportNamespaceSpecifier")];
				this.parseExportSpecifiersMaybe(node);
				this.parseExportFrom(node, true);
			  } else {
				this.parseExportFrom(node, true);
				return this.finishNode(node, "ExportAllDeclaration");
			  }
			} else if (this.hasPlugin("exportExtensions") && this.isExportDefaultSpecifier()) {
			  let specifier = this.startNode();
			  specifier.exported = this.parseIdentifier(true);
			  node.specifiers = [this.finishNode(specifier, "ExportDefaultSpecifier")];
			  if (this.match(tt.comma) && this.lookahead().type === tt.star) {
				this.expect(tt.comma);
				let specifier = this.startNode();
				this.expect(tt.star);
				this.expectContextual("as");
				specifier.exported = this.parseIdentifier();
				node.specifiers.push(this.finishNode(specifier, "ExportNamespaceSpecifier"));
			  } else {
				this.parseExportSpecifiersMaybe(node);
			  }
			  this.parseExportFrom(node, true);
			} else if (this.eat(tt._default)) { // export default ...
			  let expr = this.startNode();
			  let needsSemi = false;
			  if (this.eat(tt._function)) {
				expr = this.parseFunction(expr, true, false, false, true);
			  } else if (this.match(tt._class)) {
				expr = this.parseClass(expr, true, true);

			  //DCI
			  //ADDED FOR DCI
			  } else if (this.state.value === 'context') {
				this.next();
				expr = this.dci_parseContext(expr);

			  } else {
				needsSemi = true;
				expr = this.parseMaybeAssign();
			  }
			  node.declaration = expr;
			  if (needsSemi) this.semicolon();
			  this.checkExport(node);
			  return this.finishNode(node, "ExportDefaultDeclaration");

			//DCI modified
			} else if (this.state.type.keyword || this.state.value === 'context' || this.shouldParseExportDeclaration()) {
			//} else if (this.state.type.keyword || this.shouldParseExportDeclaration()) {

			  node.specifiers = [];
			  node.source = null;
			  node.declaration = this.parseExportDeclaration(node);
			} else { // export { x, y as z } [from '...']
			  node.declaration = null;
			  node.specifiers = this.parseExportSpecifiers();
			  this.parseExportFrom(node);
			}
			this.checkExport(node);
			return this.finishNode(node, "ExportNamedDeclaration");
		};
	});

	//allow 'void' return types in role-player contracts
	instance.extend('flowParsePrimaryType', function(inner) {
		return function(node) {
			if (this.state.type === tt._void) {
				//convert to null return type
				let node = this.startNode();
				node.value = this.match(tt._void);
				this.next();
				return this.finishNode(node, "NullLiteralTypeAnnotation");
			}
			else inner.call(this);
		};
	});
}

let pp = Parser.prototype;

// Roles

pp.dci_parseRole = function (node) {
	node.id = this.parseIdentifier();
	this.dci_parseRoleBody(node);
	return this.finishNode(node, "RoleDeclaration");
};

//Parse role body
//Adapted from parseClassBody(),
//https://github.com/babel/babel/blob/master/packages/babylon/src/parser/statement.js
pp.dci_parseRoleBody = function (node) {
	let roleBody = this.startNode();
	roleBody.body = [];

	this.expect(tt.braceL);

	while (!this.eat(tt.braceR)) {
		if (this.eat(tt.semi)) {
		  continue;
		}

		let method = this.startNode();

		let isGenerator = this.eat(tt.star);
		let isGetSet = false;
		let isAsync = false;

	    this.parsePropertyName(method);

		let isAsyncMethod = this.hasPlugin("asyncFunctions") && !this.match(tt.parenL) && !method.computed && method.key.type === "Identifier" && method.key.name === "async";
		if (isAsyncMethod) {
		  if (this.hasPlugin("asyncGenerators") && this.eat(tt.star)) isGenerator = true;
		  isAsync = true;
		  this.parsePropertyName(method);
		}

		method.kind = "method";

		if (!method.computed) {
		  let { key } = method;

		  // handle get/set methods
		  // eg. class Foo { get bar() {} set bar() {} }
		  if (!isAsync && !isGenerator && key.type === "Identifier" && !this.match(tt.parenL) && (key.name === "get" || key.name === "set")) {
			isGetSet = true;
			method.kind = key.name;
			key = this.parsePropertyName(method);
		  }
		}

		this.dci_parseRoleMethod(roleBody, method, isGenerator, isAsync);

		// get methods aren't allowed to have any parameters
		// set methods must have exactly 1 parameter
		if (isGetSet) {
		  let paramCount = method.kind === "get" ? 0 : 1;
		  if (method.params.length !== paramCount) {
			let start = method.start;
			if (method.kind === "get") {
			  this.raise(start, "getter should have no params");
			} else {
			  this.raise(start, "setter should have exactly one param");
			}
		  }
		}
	}

	//Role-Player contracts
	if (this.match(tt.name)) {
		let id = this.parseIdentifier();
		if (id.name === 'contract') {
			//node.contract = this.dci_parseRolePlayerContract();

			//use flow syntax plugin to parse the contract
			let contract = this.flowParseObjectType();
			contract.type = 'RolePlayerContract';
			node.contract = contract;

			/*
			let contract = this.startNode();
			this.expect(tt.braceL);
			while (!this.eat(tt.braceR)) {

			}
			this.finishNode(contract, "RolePlayerContract");
			*/
		}
	}

	node.body = this.finishNode(roleBody, "RoleDeclarationBody");
};

pp.dci_parseRoleMethod = function (roleBody, method, isGenerator, isAsync) {
  this.parseMethod(method, isGenerator, isAsync);
  roleBody.body.push(this.finishNode(method, "RoleMethod"));
};

// Context declarations (context MyContext {...})

pp.dci_parseContext = function (node) {
  node.id = this.parseIdentifier();
  this.dci_parseContextBody(node);
  return this.finishNode(node, "ContextDeclaration");
};

//Parse context body
//Adapted from parseClassBody(),
//https://github.com/babel/babel/blob/master/packages/babylon/src/parser/statement.js
pp.dci_parseContextBody = function (node) {
	// context bodies are implicitly strict
	let oldStrict = this.state.strict;
	this.state.strict = true;

	let hadConstructorCall = false;
	let hadConstructor = false;
	let decorators = [];
	let ctxBody = this.startNode();
	ctxBody.body = [];

	this.expect(tt.braceL);

	while (!this.eat(tt.braceR)) {
		if (this.eat(tt.semi)) {
		  continue;
		}

		//TODO?
		//annotations
		/*
		if (this.match(tt.at)) {
		}
		*/

    	let member = this.startNode();
		this.parsePropertyName(member);

		if (member.key.type === 'Identifier' && member.key.name === 'role') {
			ctxBody.body.push(this.dci_parseRole(member));
			continue;
		}

		let method = member;

		//let isConstructorCall = false;
		let isGenerator = this.eat(tt.star);
		let isGetSet = false;
		let isAsync = false;

		if (!isGenerator && method.key.type === "Identifier" && !method.computed) {
		  if (this.dci_isContextProperty()) {
			ctxBody.body.push(this.dci_parseContextProperty(method));
			continue;
		  }

		  /*
		  if (this.hasPlugin("classConstructorCall") && method.key.name === "call" && this.match(tt.name) && this.state.value === "constructor") {
			isConstructorCall = true;
			this.parsePropertyName(method);
		  }
		  */
		}

		let isAsyncMethod = this.hasPlugin("asyncFunctions") && !this.match(tt.parenL) && !method.computed && method.key.type === "Identifier" && method.key.name === "async";
		if (isAsyncMethod) {
		  if (this.hasPlugin("asyncGenerators") && this.eat(tt.star)) isGenerator = true;
		  isAsync = true;
		  this.parsePropertyName(method);
		}

		method.kind = "method";

		if (!method.computed) {
			let { key } = method;

			// handle get/set methods
			// eg. class Foo { get bar() {} set bar() {} }
			if (!isAsync && !isGenerator && key.type === "Identifier" && !this.match(tt.parenL) && (key.name === "get" || key.name === "set")) {
			  isGetSet = true;
			  method.kind = key.name;
			  key = this.parsePropertyName(method);
			}

			// disallow invalid constructors
			let isConstructor = (key.type === "Identifier" && key.name === "constructor")
				|| (key.type === "StringLiteral" && key.value === "constructor");
			/*
			let isConstructor = !isConstructorCall && (
			  (key.type === "Identifier" && key.name === "constructor") ||
			  (key.type === "StringLiteral" && key.value === "constructor")
			);
			*/
			if (isConstructor) {
			  if (hadConstructor) this.raise(key.start, "Duplicate constructor in the same DCI context");
			  if (isGetSet) this.raise(key.start, "Constructor can't have get/set modifier");
			  if (isGenerator) this.raise(key.start, "Constructor can't be a generator");
			  if (isAsync) this.raise(key.start, "Constructor can't be an async function");
			  method.kind = "constructor";
			  hadConstructor = true;
			}
		}

		/*
		// convert constructor to a constructor call
		if (isConstructorCall) {
		  if (hadConstructorCall) this.raise(method.start, "Duplicate constructor call in the same DCI context");
		  method.kind = "constructorCall";
		  hadConstructorCall = true;
		}
		*/

		// disallow decorators on class constructors
		if ((method.kind === "constructor" || method.kind === "constructorCall") && method.decorators) {
		  this.raise(method.start, "You can't attach decorators to a DCI context constructor");
		}

		this.dci_parseContextMethod(ctxBody, method, isGenerator, isAsync);

		// get methods aren't allowed to have any parameters
		// set methods must have exactly 1 parameter
		if (isGetSet) {
		  let paramCount = method.kind === "get" ? 0 : 1;
		  if (method.params.length !== paramCount) {
			let start = method.start;
			if (method.kind === "get") {
			  this.raise(start, "getter should have no params");
			} else {
			  this.raise(start, "setter should have exactly one param");
			}
		  }
		}
	}

	if (decorators.length) {
	  this.raise(this.state.start, "You have trailing decorators with no method");
	}

	node.body = this.finishNode(ctxBody, "ContextBody");

	this.state.strict = oldStrict;
};

pp.dci_parseContextMethod = function (ctxBody, method, isGenerator, isAsync) {
  this.parseMethod(method, isGenerator, isAsync);
  ctxBody.body.push(this.finishNode(method, "ContextMethod"));
};

pp.dci_isContextProperty = function () {
  return this.match(tt.eq) || this.isLineTerminator();
};

pp.dci_parseContextProperty = function (node) {
  if (this.match(tt.eq)) {
    this.next();
    node.value = this.parseMaybeAssign();
  } else {
    node.value = null;
  }
  this.semicolon();
  return this.finishNode(node, "ContextProperty");
};<|endoftext|>


<commit_before>// To run as a webpage
// The comment out the import lines below.
// To use IntelliSense uncomment the import lines

//import * as dl from 'deeplearn';

// div as a string, Tensor as a tensor, NumberRows as an integer, Title as a string
async function myTensorTable(myDiv, myOutTensor, myCols, myTitle){

 document.getElementById(myDiv).innerHTML += myTitle + '
'

 const myOutput = await myOutTensor.data()
 myTemp = '

'
   for (myCount = 0;    myCount <= myOutTensor.size - 1;   myCount++){
     myTemp += ''+ myOutput[myCount] + '	'
     if (myCount % myCols == myCols-1){
         myTemp += '

'
     }
   }
   myTemp += '

'
   document.getElementById(myDiv).innerHTML += myTemp + '
'
}

async function myTest01(){
  dl = deeplearn
  const matrixShape = [2, 3];  // 2 rows, 3 columns.
  const myWeights  = dl.Array2D.new([3, 4], [0.0001, 0.001, 0.01, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]);
  const myInputs = dl.Array2D.new([3, 4], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
  const myBias    = dl.Scalar.new(2.3);

 //const myOutput = await my2D.data()
 document.getElementById('myDiv01').innerHTML = '
Printing myWeights [3, 4] [rows, columns]
'
 await myTensorTable('myDiv01', myWeights, 4, 'Weights')

 document.getElementById('myDiv01').innerHTML += 'Printing myInputs [3, 4] [rows, columns]
'
 await myTensorTable('myDiv01', myInputs, 4, 'Inputs')

 const myBiasOutput = await myBias.data();
 document.getElementById('myDiv01').innerHTML += 'Printing myBias Scalar = ' + myBiasOutput + '

'

 //Generic math ability set
 var myAlgorithm = new dl.NDArrayMathGPU()

 const myMatrix1  = dl.Array2D.new([2, 2], [3, 2, 5, 4]);
 const myMatrix2 =  dl.Array2D.new([2, 2], [2, 3, 4, 5]);

 await myTensorTable('myDiv01', myMatrix1, 2, 'Table A')
 await myTensorTable('myDiv01', myMatrix2, 2, 'Table B')

 myAlgorithm.scope(function() {
  var  myProduct = myAlgorithm.matMul(myMatrix1, myMatrix2)
  document.getElementById('myDiv01').innerHTML += ' A * B Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix1, myMatrix2, 'TRANSPOSED', 'REGULAR')
  document.getElementById('myDiv01').innerHTML += 'A^T * B Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix2, myMatrix1)
  document.getElementById('myDiv01').innerHTML += 'B * A Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix2, myMatrix1, 'REGULAR', 'TRANSPOSED')
  document.getElementById('myDiv01').innerHTML += ' B * A^T  Matmul 2D = ' + myProduct.getValues()  +'

'

})

 const w = new dl.Array1D.new([0.7, -0.3])

 const i = new dl.Array1D.new([0.1, 0.8])
 const b = dl.Scalar.new(2.1)

 await myTensorTable('myDiv01', w, 2, 'w')
 await myTensorTable('myDiv01', i, 2, 'i')

 document.getElementById('myDiv01').innerHTML += 'Bias= '+ b.getValues()  +'
'

 myAlgorithm.scope(function() {
   var myDot = myAlgorithm.dotProduct(w, i)
   var mySum = myAlgorithm.add(myDot, b)
   var mySig = myAlgorithm.sigmoid(mySum)
   document.getElementById('myDiv01').innerHTML += 'easy sigmoid = '+mySig.getValues()  +'
'
 })

 myAlgorithm.scope(function() {
    var myDot = myAlgorithm.dotProduct(w, i)
    var mySum = myAlgorithm.add(myDot, b)
    var mySig = myAlgorithm.sinh(mySum)
    document.getElementById('myDiv01').innerHTML += 'easy sinH = '+mySig.getValues()  +'
'
 })

 myAlgorithm.scope(function() {
    var myDot = myAlgorithm.dotProduct(w, i)
    var mySum = myAlgorithm.add(myDot, b)
    var mySig = myAlgorithm.relu(mySum)
    document.getElementById('myDiv01').innerHTML += 'easy relu = '+mySig.getValues()  +'
...
'   // Float32Array([3, 4, 5])
 })

}
<commit_msg>Update tensors.js<commit_after>// To run as a webpage
// The comment out the import lines below.
// To use IntelliSense uncomment the import lines

//import * as dl from 'deeplearn';

// div as a string, Tensor as a tensor, NumberRows as an integer, Title as a string
async function myTensorTable(myDiv, myOutTensor, myCols, myTitle){

 document.getElementById(myDiv).innerHTML += myTitle + '
'

 const myOutput = await myOutTensor.data()
 myTemp = '

'
   for (myCount = 0;    myCount <= myOutTensor.size - 1;   myCount++){
     myTemp += ''+ myOutput[myCount] + '	'
     if (myCount % myCols == myCols-1){
         myTemp += '

'
     }
   }
   myTemp += '

'
   document.getElementById(myDiv).innerHTML += myTemp + '
'
}

async function myTest01(){
  dl = deeplearn
  const matrixShape = [2, 3];  // 2 rows, 3 columns.
  const myWeights  = dl.Array2D.new([3, 3], [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]);
  const myInputs = dl.Array2D.new([3, 3], [1, 2, 3, 4, 5, 6, 7, 8, 9]);
  const myBias    = dl.Scalar.new(2.3);

 //const myOutput = await my2D.data()
 await myTensorTable('myDiv01', myWeights, 3, 'Weights [3x3]')

 await myTensorTable('myDiv01', myInputs, 3, 'Inputs [3x3]')

 const myBiasOutput = await myBias.data();
 document.getElementById('myDiv01').innerHTML += 'Bias Scalar = ' + myBiasOutput + '

'

 //Generic math ability set
 var myAlgorithm = new dl.NDArrayMathGPU()

  var  myProduct = myAlgorithm.matMul(myWeights, myInputs)
  await myTensorTable('myDiv01', myProduct, 3, 'Product of Weights x Inputs')

 const myMatrix1  = dl.Array2D.new([2, 2], [3, 2, 5, 4]);
 const myMatrix2 =  dl.Array2D.new([2, 2], [2, 3, 4, 5]);

 await myTensorTable('myDiv01', myMatrix1, 2, 'Table A')
 await myTensorTable('myDiv01', myMatrix2, 2, 'Table B')

  var  myProduct = myAlgorithm.matMul(myMatrix1, myMatrix2)
  document.getElementById('myDiv01').innerHTML += ' A * B Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix1, myMatrix2, 'TRANSPOSED', 'REGULAR')
  document.getElementById('myDiv01').innerHTML += 'A^T * B Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix2, myMatrix1)
  document.getElementById('myDiv01').innerHTML += 'B * A Matmul 2D = ' + myProduct.getValues()  +'
'

  var  myProduct = myAlgorithm.matMul(myMatrix2, myMatrix1, 'REGULAR', 'TRANSPOSED')
  document.getElementById('myDiv01').innerHTML += ' B * A^T  Matmul 2D = ' + myProduct.getValues()  +'

'

 const w = new dl.Array1D.new([0.7, -0.3])
 const i = new dl.Array1D.new([0.1, 0.8])
 const b = dl.Scalar.new(2.1)

 await myTensorTable('myDiv01', w, 2, 'w as weights')
 await myTensorTable('myDiv01', i, 2, 'i as inputs ')

 document.getElementById('myDiv01').innerHTML += 'b as Bias= '+ b.getValues()  +'

'

 myAlgorithm.scope(function() {
   var myDot = myAlgorithm.dotProduct(w, i)
   var mySum = myAlgorithm.add(myDot, b)
   var mySig = myAlgorithm.sigmoid(mySum)
   document.getElementById('myDiv01').innerHTML += 'sigmoid(add( dot(w, i)+ b) ) = '+mySig.getValues()  +'
'
 })

 myAlgorithm.scope(function() {
    var myDot = myAlgorithm.dotProduct(w, i)
    var mySum = myAlgorithm.add(myDot, b)
    var mySig = myAlgorithm.sinh(mySum)
    document.getElementById('myDiv01').innerHTML += 'sinH(add( dot(w, i)+ b) ) = '+mySig.getValues()  +'
'
 })

 myAlgorithm.scope(function() {
    var myDot = myAlgorithm.dotProduct(w, i)
    var mySum = myAlgorithm.add(myDot, b)
    var mySig = myAlgorithm.relu(mySum)
    document.getElementById('myDiv01').innerHTML += 'relu(add( dot(w, i)+ b) ) = '+mySig.getValues()  +'

'
 })

}
<|endoftext|>


<commit_before>define(['marionette',
    'collections/proposals',
    'collections/labcontacts',
    'collections/proteins',
    'modules/shipment/collections/dewarregistry',

    'views/table',
    'tpl!templates/shipment/migrate.html', 'jquery-ui'],
    function(Marionette,
        Proposals, LabContacts, Proteins, DewarRegistry,
        TableView,
        template) {

    return Marionette.LayoutView.extend({
        template: template,
        className: 'content',

        regions: {
            rprots: '.rproteins',
            rconts: '.rcontacts',
        },

        events: {
            'change @ui.dews': 'displayDewar',
            'change @ui.lc': 'displayLabContact',
            'click button[name=dewar]': 'migrateDewar',
            'click button[name=lc]': 'migrateContact',
            'click button[name=protein]': 'migrateProtein',
        },

        ui: {
            prop: 'input[name=PROPOSAL]',
            dews: 'select[name=DEWAR]',
            lc: 'select[name=LABCONTACT]',
            olc: 'select[name=OLABCONTACT]',

            prot: 'select[name=PROTEIN]',

            ti: '.TITLE',
            st: '.START',

            cn: '.CARDNAME',
            ln: '.LABNAME',
            ad: '.ADDRESS',

            ncn: '.NCARDNAME',
            nln: '.NLABNAME',
            nad: '.NADDRESS',
        },

        migrateDewar: function(e) {
            e.preventDefault()

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }

            if (!this.ui.dews.val()) {
                app.alert({ message: 'You must select a dewar to migrate' })
                return
            }

            var d = this.dewars.findWhere({ FACILITYCODE: this.ui.dews.val() })
            var p = this.proposals.findWhere({ PROPOSAL: this.ui.prop.val() })

            if (p && d && this.ui.lc.val()) {
                d.set({
                    PROPOSALID: p.get('PROPOSALID'),
                    LABCONTACTID: this.ui.lc.val()
                })

                var self = this
                d.save(d.changedAttributes(), {
                    patch: true,
                    success: function() {
                        app.alert({ message: 'Dewar successfully migrated to: '+p.get('PROPOSAL') })
                        self.reset()
                        self.dewars.fetch()
                    },

                    failure: function() {
                        app.alert({ message: 'Something went wrong when trying to migrate that Dewar' })
                    },
                })
            }
        },

        migrateContact: function() {
            var contacts = this.contacts.where({ isGridSelected: true })

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }
            if (!contacts.length) {
                app.alert({ message: 'You must select at least one contact to copy' })
                return
            }

            var ready = []
            _.each(contacts, function(c) {
                var nc = c.clone()
                nc.set({
                    LABCONTACTID: null,
                    prop: this.ui.prop.val(),
                })

                ready.push(nc.save())
            }, this)

            var self = this
            $.when.apply($, ready).done(function() {
                app.alert({ message: 'Lab Contact(s) copied to: '+self.ui.prop.val() })
            })
        },

        migrateProtein: function() {
            var ready = []
            var proteins = this.proteins.where({ isGridSelected: true })

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }
            if (!proteins.length) {
                app.alert({ message: 'You must select at least one protein to copy' })
                return
            }

            _.each(proteins, function(p) {
                var np = p.clone()
                np.set({
                    PROTEINID: null,
                    prop: this.ui.prop.val(),
                })

                ready.push(np.save())

            }, this)

            var self = this
            $.when.apply($, ready).done(function() {
                app.alert({ message: 'Protein(s) copied to: '+self.ui.prop.val() })
            })
        },

        reset: function() {
            this.ui.prop.val('')
            this.ui.lc.html('').val('')
            this.displayLabContact()
            this.displayProposal()
        },

        initialize: function(options) {
            this.proposals = new Proposals()
            this.ready = this.proposals.fetch()

            // Increasing list of dewars available to drop down
            // As of 2019-06 most recent proposals have less than 28 registered dewars
            this.dewars = new DewarRegistry([], {state: {pageSize: 9999}})
            this.dewars.fetch()

            this.labcontacts = new LabContacts()

            this.contacts = new LabContacts()
            this.listenTo(this.contacts, 'backgrid:selected', this.selectModel, this)
            this.contacts.fetch()

            this.proteins = new Proteins()
            this.listenTo(this.proteins, 'backgrid:selected', this.selectModel, this)
            this.proteins.queryParams.seq = 1
            this.proteins.fetch()
        },

        selectModel: function(m, checked) {
            m.set({ isGridSelected: checked })
        },

        updateLabContacts: function() {
            this.ui.lc.html(this.labcontacts.opts())
            this.displayLabContact()
        },

        updateDewars: function() {
            this.ui.dews.html(this.dewars.opts())
            this.displayDewar()
        },

        displayDewar: function() {
            var d = this.dewars.findWhere({ FACILITYCODE: this.ui.dews.val() })
            if (d) {
                this.ui.cn.text(d.get('CARDNAME'))
                this.ui.ln.text(d.get('LABNAME'))
                this.ui.ad.text(d.get('ADDRESS'))
            } else {
                this.ui.cn.text('')
                this.ui.ln.text('')
                this.ui.ad.text('')
            }
        },

        displayLabContact: function() {
            var lc = this.labcontacts.findWhere({ LABCONTACTID: this.ui.lc.val() })
            if (lc) {
                this.ui.ncn.text(lc.get('CARDNAME'))
                this.ui.nln.text(lc.get('LABNAME'))
                this.ui.nad.text(lc.get('ADDRESS'))
            } else {
                this.ui.ncn.text('')
                this.ui.nln.text('')
                this.ui.nad.text('')
            }
        },

        displayProposal: function() {
            var p = this.proposals.findWhere({ PROPOSAL: this.ui.prop.val() })
            if (p) {
                this.ui.ti.text(p.get('TITLE'))
                this.ui.st.text(p.get('ST'))
            } else {
                this.ui.ti.text('')
                this.ui.st.text('')
            }
        },

        getProposals: function(req, resp) {
            this.proposals.queryParams.s = req.term

            var self = this
            this.proposals.fetch().done(function() {
                resp(self.proposals.map(function(p) {
                    return {
                        label: p.get('PROPOSAL')+': '+p.get('TITLE'),
                        value: p.get('PROPOSAL'),
                    }
                }))
            })
        },

        selectProposal: function(e, ui) {
            this.labcontacts.queryParams.prop = ui.item.value
            this.labcontacts.fetch()
            this.ui.prop.val(ui.item.value)
            this.displayProposal()
        },

        onRender: function() {
            this.listenTo(this.dewars, 'sync', this.updateDewars)
            this.listenTo(this.labcontacts, 'sync', this.updateLabContacts)
            this.updateDewars()
            this.updateLabContacts()

            this.ui.prop.autocomplete({
                source: this.getProposals.bind(this),
                select: this.selectProposal.bind(this)
            })

            var columns = [
                { label: '', cell: 'select-row', headerCell: 'select-all', editable: false },
                { name: 'NAME', label: 'Name', cell: 'string', editable: false },
                { name: 'ACRONYM', label: 'Acronym', cell: 'string', editable: false },
                { name: 'MOLECULARMASS', label: 'Mass', cell: 'string', editable: false },
                { name: 'HASSEQ', label: 'Sequence', cell: 'string', editable: false },
                { name: 'PDBS', label: 'PDBs', cell: 'string', editable: false },
                { name: 'SCOUNT', label: 'Samples', cell: 'string', editable: false },
                { name: 'DCOUNT', label: 'Data Collections', cell: 'string', editable: false },
            ]

            this.rprots.show(new TableView({
                collection: this.proteins,
                columns: columns,
                tableClass: 'proteins',
                filter: 's',
                loading: true,
                noSearchUrl: false,
                backgrid: { emptyText: 'No proteins found' }
            }))

            var columns = [
                { label: '', cell: 'select-row', headerCell: 'select-all', editable: false },
                { name: 'CARDNAME', label: 'Card Name', cell: 'string', editable: false },
                { name: 'GIVENNAME', label: 'First Name', cell: 'string', editable: false },
                { name: 'FAMILYNAME', label: 'Surname', cell: 'string', editable: false },
                { name: 'ADDRESS', label: 'Address', cell: 'string', editable: false },
                { name: 'CITY', label: 'City', cell: 'string', editable: false },
                { name: 'POSTCODE', label: 'Postcode', cell: 'string', editable: false },
                { name: 'COUNTRY', label: 'Country', cell: 'string', editable: false },
                { name: 'PHONENUMBER', label: 'Phone No.', cell: 'string', editable: false },
                { name: 'LABNAME', label: 'Laboratory', cell: 'string', editable: false },
            ]

            this.rconts.show(new TableView({
                collection: this.contacts,
                columns: columns,
                tableClass: 'labcontacts',
                filter: 's',
                loading: true,
                noSearchUrl: false,
                backgrid: { emptyText: 'No lab contacts found' }
            }))

        },
    })

})
<commit_msg>Fixed labcontact drop down for migrate tool (more than 15 entries)<commit_after>define(['marionette',
    'collections/proposals',
    'collections/labcontacts',
    'collections/proteins',
    'modules/shipment/collections/dewarregistry',

    'views/table',
    'tpl!templates/shipment/migrate.html', 'jquery-ui'],
    function(Marionette,
        Proposals, LabContacts, Proteins, DewarRegistry,
        TableView,
        template) {

    return Marionette.LayoutView.extend({
        template: template,
        className: 'content',

        regions: {
            rprots: '.rproteins',
            rconts: '.rcontacts',
        },

        events: {
            'change @ui.dews': 'displayDewar',
            'change @ui.lc': 'displayLabContact',
            'click button[name=dewar]': 'migrateDewar',
            'click button[name=lc]': 'migrateContact',
            'click button[name=protein]': 'migrateProtein',
        },

        ui: {
            prop: 'input[name=PROPOSAL]',
            dews: 'select[name=DEWAR]',
            lc: 'select[name=LABCONTACT]',
            olc: 'select[name=OLABCONTACT]',

            prot: 'select[name=PROTEIN]',

            ti: '.TITLE',
            st: '.START',

            cn: '.CARDNAME',
            ln: '.LABNAME',
            ad: '.ADDRESS',

            ncn: '.NCARDNAME',
            nln: '.NLABNAME',
            nad: '.NADDRESS',
        },

        migrateDewar: function(e) {
            e.preventDefault()

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }

            if (!this.ui.dews.val()) {
                app.alert({ message: 'You must select a dewar to migrate' })
                return
            }

            var d = this.dewars.findWhere({ FACILITYCODE: this.ui.dews.val() })
            var p = this.proposals.findWhere({ PROPOSAL: this.ui.prop.val() })

            if (p && d && this.ui.lc.val()) {
                d.set({
                    PROPOSALID: p.get('PROPOSALID'),
                    LABCONTACTID: this.ui.lc.val()
                })

                var self = this
                d.save(d.changedAttributes(), {
                    patch: true,
                    success: function() {
                        app.alert({ message: 'Dewar successfully migrated to: '+p.get('PROPOSAL') })
                        self.reset()
                        self.dewars.fetch()
                    },

                    failure: function() {
                        app.alert({ message: 'Something went wrong when trying to migrate that Dewar' })
                    },
                })
            }
        },

        migrateContact: function() {
            var contacts = this.contacts.where({ isGridSelected: true })

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }
            if (!contacts.length) {
                app.alert({ message: 'You must select at least one contact to copy' })
                return
            }

            var ready = []
            _.each(contacts, function(c) {
                var nc = c.clone()
                nc.set({
                    LABCONTACTID: null,
                    prop: this.ui.prop.val(),
                })

                ready.push(nc.save())
            }, this)

            var self = this
            $.when.apply($, ready).done(function() {
                app.alert({ message: 'Lab Contact(s) copied to: '+self.ui.prop.val() })
            })
        },

        migrateProtein: function() {
            var ready = []
            var proteins = this.proteins.where({ isGridSelected: true })

            if (!this.ui.prop.val()) {
                app.alert({ message: 'You must select a proposal to migrate to' })
                return
            }
            if (!proteins.length) {
                app.alert({ message: 'You must select at least one protein to copy' })
                return
            }

            _.each(proteins, function(p) {
                var np = p.clone()
                np.set({
                    PROTEINID: null,
                    prop: this.ui.prop.val(),
                })

                ready.push(np.save())

            }, this)

            var self = this
            $.when.apply($, ready).done(function() {
                app.alert({ message: 'Protein(s) copied to: '+self.ui.prop.val() })
            })
        },

        reset: function() {
            this.ui.prop.val('')
            this.ui.lc.html('').val('')
            this.displayLabContact()
            this.displayProposal()
        },

        initialize: function(options) {
            this.proposals = new Proposals()
            this.ready = this.proposals.fetch()

            // Increasing list of dewars available to drop down
            // As of 2019-06 most recent proposals have less than 28 registered dewars
            this.dewars = new DewarRegistry([], {state: {pageSize: 9999}})
            this.dewars.fetch()

            // The list of lab contacts used in the drop down
            this.labcontacts = new LabContacts([], {state: {pageSize: 9999}})

            this.contacts = new LabContacts()
            this.listenTo(this.contacts, 'backgrid:selected', this.selectModel, this)
            this.contacts.fetch()

            this.proteins = new Proteins()
            this.listenTo(this.proteins, 'backgrid:selected', this.selectModel, this)
            this.proteins.queryParams.seq = 1
            this.proteins.fetch()
        },

        selectModel: function(m, checked) {
            m.set({ isGridSelected: checked })
        },

        updateLabContacts: function() {
            this.ui.lc.html(this.labcontacts.opts())
            this.displayLabContact()
        },

        updateDewars: function() {
            this.ui.dews.html(this.dewars.opts())
            this.displayDewar()
        },

        displayDewar: function() {
            var d = this.dewars.findWhere({ FACILITYCODE: this.ui.dews.val() })
            if (d) {
                this.ui.cn.text(d.get('CARDNAME'))
                this.ui.ln.text(d.get('LABNAME'))
                this.ui.ad.text(d.get('ADDRESS'))
            } else {
                this.ui.cn.text('')
                this.ui.ln.text('')
                this.ui.ad.text('')
            }
        },

        displayLabContact: function() {
            var lc = this.labcontacts.findWhere({ LABCONTACTID: this.ui.lc.val() })
            if (lc) {
                this.ui.ncn.text(lc.get('CARDNAME'))
                this.ui.nln.text(lc.get('LABNAME'))
                this.ui.nad.text(lc.get('ADDRESS'))
            } else {
                this.ui.ncn.text('')
                this.ui.nln.text('')
                this.ui.nad.text('')
            }
        },

        displayProposal: function() {
            var p = this.proposals.findWhere({ PROPOSAL: this.ui.prop.val() })
            if (p) {
                this.ui.ti.text(p.get('TITLE'))
                this.ui.st.text(p.get('ST'))
            } else {
                this.ui.ti.text('')
                this.ui.st.text('')
            }
        },

        getProposals: function(req, resp) {
            this.proposals.queryParams.s = req.term

            var self = this
            this.proposals.fetch().done(function() {
                resp(self.proposals.map(function(p) {
                    return {
                        label: p.get('PROPOSAL')+': '+p.get('TITLE'),
                        value: p.get('PROPOSAL'),
                    }
                }))
            })
        },

        selectProposal: function(e, ui) {
            this.labcontacts.queryParams.prop = ui.item.value
            this.labcontacts.fetch()
            this.ui.prop.val(ui.item.value)
            this.displayProposal()
        },

        onRender: function() {
            this.listenTo(this.dewars, 'sync', this.updateDewars)
            this.listenTo(this.labcontacts, 'sync', this.updateLabContacts)
            this.updateDewars()
            this.updateLabContacts()

            this.ui.prop.autocomplete({
                source: this.getProposals.bind(this),
                select: this.selectProposal.bind(this)
            })

            var columns = [
                { label: '', cell: 'select-row', headerCell: 'select-all', editable: false },
                { name: 'NAME', label: 'Name', cell: 'string', editable: false },
                { name: 'ACRONYM', label: 'Acronym', cell: 'string', editable: false },
                { name: 'MOLECULARMASS', label: 'Mass', cell: 'string', editable: false },
                { name: 'HASSEQ', label: 'Sequence', cell: 'string', editable: false },
                { name: 'PDBS', label: 'PDBs', cell: 'string', editable: false },
                { name: 'SCOUNT', label: 'Samples', cell: 'string', editable: false },
                { name: 'DCOUNT', label: 'Data Collections', cell: 'string', editable: false },
            ]

            this.rprots.show(new TableView({
                collection: this.proteins,
                columns: columns,
                tableClass: 'proteins',
                filter: 's',
                loading: true,
                noSearchUrl: false,
                backgrid: { emptyText: 'No proteins found' }
            }))

            var columns = [
                { label: '', cell: 'select-row', headerCell: 'select-all', editable: false },
                { name: 'CARDNAME', label: 'Card Name', cell: 'string', editable: false },
                { name: 'GIVENNAME', label: 'First Name', cell: 'string', editable: false },
                { name: 'FAMILYNAME', label: 'Surname', cell: 'string', editable: false },
                { name: 'ADDRESS', label: 'Address', cell: 'string', editable: false },
                { name: 'CITY', label: 'City', cell: 'string', editable: false },
                { name: 'POSTCODE', label: 'Postcode', cell: 'string', editable: false },
                { name: 'COUNTRY', label: 'Country', cell: 'string', editable: false },
                { name: 'PHONENUMBER', label: 'Phone No.', cell: 'string', editable: false },
                { name: 'LABNAME', label: 'Laboratory', cell: 'string', editable: false },
            ]

            this.rconts.show(new TableView({
                collection: this.contacts,
                columns: columns,
                tableClass: 'labcontacts',
                filter: 's',
                loading: true,
                noSearchUrl: false,
                backgrid: { emptyText: 'No lab contacts found' }
            }))

        },
    })

})
<|endoftext|>