18F/e-QIP-prototype

View on GitHub
src/components/Section/Package/Attachments.jsx

Summary

Maintainability
A
1 hr
Test Coverage
import React from 'react'
import FileSaver from 'file-saver'
import { i18n, env } from '../../../config'
import { api } from '../../../services'
import { Show, Field, Text, RadioGroup, Radio, Svg } from '../../Form'

export default class Attachments extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      attachments: [],
      errorMessage: this.props.errorMessage
    }

    this.supportsBlobs = false
    try {
      this.supportsBlobs = !!new window.Blob()
    } catch (e) {
      this.supportsBlobs = false
    }

    this.update = this.update.bind(this)
    this.updateAttachmentType = this.updateAttachmentType.bind(this)
    this.updateUploadDescription = this.updateUploadDescription.bind(this)
    this.updateOtherMethod = this.updateOtherMethod.bind(this)
    this.uploadFile = this.uploadFile.bind(this)
    this.delete = this.delete.bind(this)
    this.getStoredAttachments = this.getStoredAttachments.bind(this)
    this.displayAttachments = this.displayAttachments.bind(this)
  }

  componentDidMount() {
    this.getStoredAttachments()
  }

  update(queue) {
    this.props.onUpdate({
      AttachmentType: this.props.AttachmentType,
      UploadDescription: this.props.UploadDescription,
      OtherMethod: this.props.OtherMethod,
      ...queue
    })
  }

  updateAttachmentType(values) {
    this.update({ AttachmentType: values })
  }

  updateUploadDescription(values) {
    this.update({ UploadDescription: values })
  }

  updateOtherMethod(values) {
    this.update({ OtherMethod: values })
  }

  uploadFile(event) {
    const set = attachment => {
      // reset the file input
      this.refs.form.reset()

      // add the attachment
      const attachments = this.state.attachments
      attachments.push(attachment)
      this.setState({ attachments: attachments, errorMessage: '' }, () => {
        // clear out description value
        this.updateUploadDescription({ value: '' })
      })
    }

    const description = (this.props.UploadDescription || {}).value
    const file = this.refs.file.files[0]
    let formData = new window.FormData()
    formData.append('file', file)

    api
      .saveAttachment(formData)
      .then(response => {
        const attachmentID = response.data || 0
        if (attachmentID === 0) {
          return
        }
        if (!description) {
          set({
            id: attachmentID,
            filename: file.name,
            size: file.size,
            description: description
          })
          return
        }
        api
          .updateAttachment(attachmentID, description)
          .then(response => {
            set({
              id: attachmentID,
              filename: file.name,
              size: file.size,
              description: description
            })
          })
          .catch(() => {
            this.setState({
              errorMessage: i18n.t(
                'application.attachments.upload.error.update'
              )
            })
          })
      })
      .catch(() => {
        this.setState({
          errorMessage: i18n.t('application.attachments.upload.error.save')
        })
      })
    event.preventDefault()
  }

  download(id) {
    if (!this.supportsBlobs) {
      return
    }

    const attachment = this.state.attachments.find(x => x.id === id)
    api
      .getAttachment(id)
      .then(response => {
        const blob = blobFromBase64(response.data, 'application/octet-stream')
        FileSaver.saveAs(blob, attachment.filename)
      })
      .catch(() => {
        this.setState({
          errorMessage: i18n.t('application.attachments.upload.error.download')
        })
      })
  }

  delete(id) {
    api
      .deleteAttachment(id)
      .then(response => {
        let attachments = this.state.attachments
        attachments = attachments.filter(x => x.id !== id)
        this.setState({ attachments: attachments, errorMessage: '' })
      })
      .catch(() => {
        this.setState({
          errorMessage: i18n.t('application.attachments.upload.error.delete')
        })
      })
  }

  getStoredAttachments() {
    api.listAttachments().then(response => {
      this.setState({ attachments: response.data || [], errorMessage: '' })
    })
  }

  displayAttachments(items) {
    return items.map((x, i) => {
      return (
        <tr key={`attachment-${x.id}`}>
          <td>
            <Show when={this.supportsBlobs}>
              <a
                href="javascript:;;"
                aria-label={`Download ${x.filename}`}
                onClick={this.download.bind(this, x.id)}>
                <strong>
                  {`${i + 1}. `}
                  {x.description ? `${x.description} - ` : ''}
                </strong>
                {x.filename}
              </a>
            </Show>
            <Show when={!this.supportsBlobs}>
              <strong>
                {`${i + 1}. `}
                {x.description ? `${x.description} - ` : ''}
              </strong>
              {x.filename}
            </Show>
          </td>
          <td>
            <button onClick={this.delete.bind(this, x.id)}>
              <i className="fa fa-trash" aria-hidden="true" />
              <span>
                {i18n.t('application.attachments.upload.files.remove')}
              </span>
            </button>
          </td>
        </tr>
      )
    })
  }

  limits() {
    const fileMax = env.FileMaximumSize()

    const fileTypes = env.FileTypes()
    fileTypes[fileTypes.length - 1] = `or ${fileTypes[fileTypes.length - 1]}`

    const message = i18n
      .t('application.attachments.upload.limits')
      .replace('{types}', fileTypes.join(', '))
      .replace('{max_size}', humanReadableFileSize(fileMax))

    return (
      <p>
        <strong>{message}</strong>
      </p>
    )
  }

  render() {
    return (
      <div className="attachments">
        <Field
          title={i18n.t('application.attachments.method.title')}
          help="application.attachments.help"
          className="attachment-type"
          optional={true}>
          {i18n.m('application.attachments.method.para')}
          <RadioGroup
            className="attachment-type option-list eapp-extend-labels physical-radio"
            onError={this.props.onError}
            selectedValue={(this.props.AttachmentType || {}).value}>
            <Radio
              name="attachment-type-upload"
              label={i18n.m('application.attachments.type.upload')}
              value="Upload"
              onUpdate={this.updateAttachmentType}
              onError={this.props.onError}>
              <div className="attachment-icon upload">
                <Svg src="/img/attach-upload.svg" />
              </div>
            </Radio>
            <Radio
              name="attachment-type-fax"
              label={i18n.m('application.attachments.type.fax')}
              value="Fax"
              onUpdate={this.updateAttachmentType}
              onError={this.props.onError}>
              <div className="attachment-icon fax">
                <Svg src="/img/attach-fax.svg" />
              </div>
            </Radio>
            <Radio
              name="attachment-type-other"
              label={i18n.m('application.attachments.type.other')}
              value="Other"
              onUpdate={this.updateAttachmentType}
              onError={this.props.onError}>
              <div className="attachment-icon mail">
                <Svg src="/img/attach-mail.svg" />
              </div>
            </Radio>
          </RadioGroup>
        </Field>

        <Show when={(this.props.AttachmentType || {}).value === 'Upload'}>
          <Field
            title={i18n.t('application.attachments.upload.title')}
            optional={true}
            className={
              this.state.errorMessage
                ? 'no-margin-bottom upload-area'
                : 'upload-area'
            }>
            {i18n.m('application.attachments.upload.para')}
            {this.limits()}
            <form ref="form">
              <input id="file-upload" ref="file" type="file" />
            </form>
            <Text
              {...this.props.UploadDescription}
              name="UploadDescription"
              label={i18n.t('application.attachments.upload.description')}
              onUpdate={this.updateUploadDescription}
              onError={this.props.onError}
            />
            <button onClick={this.uploadFile}>
              {i18n.t('application.attachments.upload.send')}
            </button>
          </Field>

          <Show when={this.state.errorMessage}>
            <div className="field upload-error">
              <div className="table expand">
                <div className="usa-alert usa-alert-error" role="alert">
                  <div className="usa-alert-body">
                    <h5 className="usa-alert-heading">{i18n.t('application.attachments.upload.error.title')}</h5>
                    <p>{this.state.errorMessage}</p>
                  </div>
                </div>
              </div>
            </div>
          </Show>

          <Show when={this.state.attachments.length > 0}>
            <Field
              title={i18n.t('application.attachments.upload.files.title')}
              optional={true}>
              {i18n.m('application.attachments.upload.files.para')}
              <table>
                <tbody>{this.displayAttachments(this.state.attachments)}</tbody>
              </table>
            </Field>
          </Show>
        </Show>

        <Show when={(this.props.AttachmentType || {}).value === 'Fax'}>
          <Field
            title={i18n.t('application.attachments.fax.title')}
            optional={true}
            className="fax-area">
            {i18n.m('application.attachments.fax.para')}
            <button>{i18n.t('application.attachments.fax.print')}</button>
          </Field>
        </Show>

        <Show when={(this.props.AttachmentType || {}).value === 'Other'}>
          <Field
            title={i18n.t('application.attachments.other.title')}
            optional={true}
            className="other-area">
            {i18n.m('application.attachments.other.para')}
            <Text
              {...this.props.OtherMethod}
              name="OtherMethod"
              label={i18n.t('application.attachments.other.method')}
              onUpdate={this.updateOtherMethod}
              onError={this.props.onError}
            />
          </Field>
        </Show>
      </div>
    )
  }
}

Attachments.defaultProps = {
  errorMessage: '',
  AttachmentType: {},
  UploadDescription: {},
  OtherMethod: {},
  section: 'releases',
  subsection: 'attachments',
  onUpdate: queue => {},
  onError: (value, arr) => {
    return arr
  }
}

const humanReadableFileSize = (bytes, si = true) => {
  const threshold = si ? 1000 : 1024
  if (Math.abs(bytes) < threshold) {
    return `${bytes} B`
  }

  const units = si
    ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
    : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
  let u = -1
  do {
    bytes /= threshold
    u++
  } while (Math.abs(bytes) >= threshold && u < units.length - 1)

  return `${bytes.toFixed(1)} ${units[u]}`
}

const blobFromBase64 = (base64, contentType = '', size = 512) => {
  const binary = window.atob(base64)
  const buffer = []
  for (let offset = 0; offset < binary.length; offset += size) {
    let slice = binary.slice(offset, offset + size)
    let numbers = new Array(slice.length)
    for (let i = 0; i < slice.length; i++) {
      numbers[i] = slice.charCodeAt(i)
    }
    buffer.push(new Uint8Array(numbers))
  }
  return new window.Blob(buffer, { type: contentType })
}