sanger/limber

View on GitHub
app/frontend/javascript/tubes-to-rack/components/TubesToRack.vue

Summary

Maintainability
Test Coverage
<!--
  This is partially complete work and needs a few more pieces putting into place:
  - Swap out the UI of the tube rack for a component James G was working on.
  - Parse the tubes added to the rack into the correct format and hook up the
    API call to the V2 SequenceScape endpoint for tube racks.
  - Review the validations applied here and adjust if needed.
-->

<template>
  <lb-page>
    <lb-loading-modal v-if="loading" :message="progressMessage" />
    <lb-main-content>
      <b-card bg-variant="dark" text-variant="white">
        <h3>Tube rack UI goes here</h3>
        <p>
          Scanned tubes will appear in a UI representation of the tube rack. This will be similar in function to the
          plate UI but will be capable of opening the tube information page when viewed after creation.
        </p>
      </b-card>
    </lb-main-content>
    <lb-sidebar>
      <b-card header="Scan tubes" header-tag="h3">
        <b-form-group label="Scan the tube barcodes into the relevant rack coordinates:" class="fixed-height-scroll">
          <lb-labware-scan
            v-for="i in tubeCount"
            :key="i"
            :api="devourApi"
            :label="indexToName(i - 1)"
            :fields="tubeFields"
            :includes="tubeIncludes"
            :validators="scanValidators"
            :colour-index="i"
            :labware-type="'tube'"
            :valid-message="''"
            @change="updateTube(i, $event)"
          />
        </b-form-group>
        <hr />
        <b-button :disabled="!valid" variant="success" @click="createRack()"> Create </b-button>
      </b-card>
    </lb-sidebar>
  </lb-page>
</template>

<script>
import LabwareScan from '@/javascript/shared/components/LabwareScan.vue'
import LoadingModal from '@/javascript/shared/components/LoadingModal.vue'
import { checkDuplicates, checkMatchingPurposes } from '@/javascript/shared/components/tubeScanValidators.js'
import devourApi from '@/javascript/shared/devourApi.js'
import { handleFailedRequest } from '@/javascript/shared/requestHelpers.js'
import resources from '@/javascript/shared/resources.js'
import { buildTubeObjs } from '@/javascript/shared/tubeHelpers.js'
import { indexToName } from '@/javascript/shared/wellHelpers.js'
import filterProps from './filterProps.js'

export default {
  name: 'TubesToRack',
  components: {
    'lb-labware-scan': LabwareScan,
    'lb-loading-modal': LoadingModal,
  },
  props: {
    // Sequencescape API V2 URL
    sequencescapeApi: { type: String, default: 'http://localhost:3000/api/v2' },

    // Sequencescape API V2 API key
    sequencescapeApiKey: { type: String, default: 'development' },

    // Width of tube rack
    rackWidth: { type: Number, default: 8 },

    // Height of tube rack
    rackHeight: { type: Number, default: 2 },

    // Limber target Asset URL for posting the transfers
    targetUrl: { type: String, required: true },
  },
  data() {
    return {
      // Array containing objects with scanned tubes, their states and the
      // index of the form input in which they were scanned.
      // Note: Cannot use computed functions as data is invoked before.
      tubes: buildTubeObjs(this.rackWidth * this.rackHeight),

      // Devour API object to deserialise assets from sequencescape API.
      // (See ../../shared/resources.js for details)
      devourApi: devourApi({ apiUrl: this.sequencescapeApi }, resources, this.sequencescapeApiKey),

      // Flag for toggling loading screen
      loading: false,

      // Message to be shown during loading screen
      progressMessage: '',
    }
  },
  computed: {
    scanValidators() {
      const allTubes = this.tubes.map((tubeItem) => tubeItem.labware)
      return [checkDuplicates(allTubes), checkMatchingPurposes(this.firstTubePurpose)]
    },
    tubeCount() {
      return this.rackWidth * this.rackHeight
    },
    tubeFields() {
      return filterProps.tubeFields
    },
    tubeIncludes() {
      return filterProps.tubeIncludes
    },
    firstTubePurpose() {
      return this.validTubes[0]?.labware.purpose
    },
    unsuitableTubes() {
      return this.tubes.filter((tube) => !(tube.state === 'valid' || tube.state === 'empty'))
    },
    valid() {
      return (
        this.validTubes.length > 0 && // At least one tube is validated
        this.unsuitableTubes.length === 0
      ) // None of the tubes are invalid
    },
    validTubes() {
      return this.tubes.filter((tube) => tube.state === 'valid')
    },
  },
  methods: {
    indexToName(index) {
      return indexToName(index, this.rackHeight)
    },
    updateTube(index, data) {
      this.$set(this.tubes, index - 1, { ...data, index: index - 1 })
    },
    createRack() {
      this.progressMessage = 'Creating tube rack...'
      this.loading = true
      let payload = {
        tube_rack: {
          purpose_uuid: this.firstTubePurpose.uuid,
        },
      }
      this.$axios({
        method: 'post',
        url: this.targetUrl,
        headers: { 'X-Requested-With': 'XMLHttpRequest' },
        data: payload,
      })
        .then((response) => {
          // Ajax responses automatically follow redirects, which
          // would result in us receiving the full HTML for the child
          // plate here, which we'd then need to inject into the
          // page, and update the history. Instead we don't redirect
          // application/json requests, and redirect the user ourselves.
          this.progressMessage = response.data.message
          this.locationObj.href = response.data.redirect
        })
        .catch((error) => {
          // Something has gone wrong
          handleFailedRequest(error)
          this.loading = false
        })
    },
  },
}
</script>

<style>
.fixed-height-scroll {
  height: 460px;
  overflow-y: scroll;
}
</style>