|
| 1 | +<template> |
| 2 | + <v-dialog v-model="dialog" scrollable max-width="min(90vw, 1024px)"> |
| 3 | + <v-card :title="$t('modalTitleAddGermplasm')"> |
| 4 | + <template #text> |
| 5 | + <p>{{ $t('modalTextAddGermplasm') }}</p> |
| 6 | + |
| 7 | + <v-card :title="$t('formGroupSetupShowFields')" class="mb-3"> |
| 8 | + <template #text> |
| 9 | + <div class="d-flex flex-wrap ga-2"> |
| 10 | + <v-btn-toggle v-model="visibleFields" color="primary" variant="tonal" multiple direction="vertical"> |
| 11 | + <v-btn value="treatment" :text="$t('formCheckboxSetupShowTreatment')" :prepend-icon="mdiSprinklerFire" /> |
| 12 | + <v-btn value="friendlyName" :text="$t('formCheckboxSetupShowFriendlyName')" :prepend-icon="mdiEyeCheck" /> |
| 13 | + <v-btn value="barcode" :text="$t('formCheckboxSetupShowBarcode')" :prepend-icon="mdiBarcode" /> |
| 14 | + <v-btn value="pedigree" :text="$t('formCheckboxSetupShowPedigree')" :prepend-icon="mdiFamilyTree" /> |
| 15 | + </v-btn-toggle> |
| 16 | + |
| 17 | + <v-textarea |
| 18 | + :label="$t('formLabelAddGermplasmName')" |
| 19 | + :hint="$t('formDescriptionAddGermplasm')" |
| 20 | + persistent-hint |
| 21 | + v-model="areaInput" |
| 22 | + > |
| 23 | + <template #append-inner> |
| 24 | + <v-btn :icon="mdiPlus" v-tooltip:top="$t('buttonAdd')" @click="addFromInput" /> |
| 25 | + </template> |
| 26 | + </v-textarea> |
| 27 | + </div> |
| 28 | + </template> |
| 29 | + </v-card> |
| 30 | + |
| 31 | + <v-virtual-scroll |
| 32 | + :items="newGermplasm" |
| 33 | + max-height="75vh" |
| 34 | + > |
| 35 | + <template #default="{ index }"> |
| 36 | + <v-card class="mb-3"> |
| 37 | + <template #text> |
| 38 | + <GermplasmMetadataInputSection :visible-fields="visibleFields" v-model="newGermplasm[index]" /> |
| 39 | + </template> |
| 40 | + </v-card> |
| 41 | + </template> |
| 42 | + </v-virtual-scroll> |
| 43 | + |
| 44 | + <div class=""> |
| 45 | + <v-btn v-if="hasErrors || hasWarnings" @click="bottomSheetVisible = true" :text="$t('formFeedbackLayout', feedback.length)" :prepend-icon="hasErrors ? mdiAlertCircle : mdiAlert" :color="hasErrors ? 'error' : 'warning'" /> |
| 46 | + </div> |
| 47 | + </template> |
| 48 | + |
| 49 | + <v-card-actions> |
| 50 | + <v-spacer /> |
| 51 | + <v-btn :text="$t('buttonCancel')" @click="hide" /> |
| 52 | + <v-btn :text="$t('buttonCheck')" @click="check" :disabled="!canCheck" color="warning" variant="tonal" /> |
| 53 | + <v-btn :text="$t('buttonAdd')" @click="save" :disabled="!canContinue" color="primary" variant="tonal" /> |
| 54 | + </v-card-actions> |
| 55 | + </v-card> |
| 56 | + |
| 57 | + <v-bottom-sheet v-model="bottomSheetVisible" inset> |
| 58 | + <v-card :title="$t('modalTitleLayoutFeedback')"> |
| 59 | + <template #text> |
| 60 | + <p>{{ $t('modalTextLayoutFeedback') }}</p> |
| 61 | + |
| 62 | + <v-list> |
| 63 | + <v-list-item |
| 64 | + v-for="(item, itemIndex) in feedback" |
| 65 | + :key="`feedback-item-${itemIndex}`" |
| 66 | + :prepend-icon="item.type === 'error' ? mdiAlertCircle : mdiAlert" |
| 67 | + :base-color="item.type === 'error' ? 'error' : 'warning'" |
| 68 | + :title="item.message" |
| 69 | + /> |
| 70 | + </v-list> |
| 71 | + </template> |
| 72 | + </v-card> |
| 73 | + </v-bottom-sheet> |
| 74 | + </v-dialog> |
| 75 | +</template> |
| 76 | + |
| 77 | +<script setup lang="ts"> |
| 78 | + import { addTrialGermplasm, getTrialData } from '@/plugins/idb' |
| 79 | + import type { TrialPlus } from '@/plugins/types/client' |
| 80 | + import type { CellMetadata } from '@/plugins/types/gridscore' |
| 81 | + import { mdiAlert, mdiAlertCircle, mdiBarcode, mdiEyeCheck, mdiFamilyTree, mdiPlus, mdiSprinklerFire } from '@mdi/js' |
| 82 | + import emitter from 'tiny-emitter/instance' |
| 83 | + import type { LayoutFeedback } from '@/components/setup/GermplasmLayoutTable.vue' |
| 84 | + import { useI18n } from 'vue-i18n' |
| 85 | +
|
| 86 | + const compProps = defineProps<{ |
| 87 | + trial: TrialPlus |
| 88 | + }>() |
| 89 | +
|
| 90 | + const { t } = useI18n() |
| 91 | +
|
| 92 | + const dialog = ref(false) |
| 93 | + const newGermplasm = ref<CellMetadata[]>([]) |
| 94 | + const visibleFields = ref<('treatment' | 'friendlyName' | 'pedigree' | 'barcode')[]>([]) |
| 95 | + const areaInput = ref<string>() |
| 96 | + const feedback = ref<LayoutFeedback[]>([]) |
| 97 | + const bottomSheetVisible = ref(false) |
| 98 | +
|
| 99 | + let uniqueNames = new Set<string>() |
| 100 | + let uniqueBarcodes = new Set<string>() |
| 101 | +
|
| 102 | + const hasErrors = computed(() => feedback.value && feedback.value.some(f => f.type === 'error')) |
| 103 | + const hasWarnings = computed(() => feedback.value && feedback.value.some(f => f.type === 'warning')) |
| 104 | + const canContinue = computed(() => canCheck.value && hasErrors.value === false) |
| 105 | + const canCheck = computed(() => newGermplasm.value.length > 0) |
| 106 | +
|
| 107 | + function show () { |
| 108 | + dialog.value = true |
| 109 | + areaInput.value = undefined |
| 110 | + feedback.value = [] |
| 111 | +
|
| 112 | + getTrialData(compProps.trial.localId || '') |
| 113 | + .then(data => { |
| 114 | + uniqueNames = new Set<string>() |
| 115 | + uniqueBarcodes = new Set<string>() |
| 116 | + Object.values(data).forEach(c => { |
| 117 | + uniqueNames.add(`${c.germplasm}|${c.rep}`) |
| 118 | +
|
| 119 | + if (c.barcode) { |
| 120 | + uniqueBarcodes.add(c.barcode) |
| 121 | + } |
| 122 | + }) |
| 123 | +
|
| 124 | + newGermplasm.value = [] |
| 125 | + }) |
| 126 | + } |
| 127 | + function hide () { |
| 128 | + newGermplasm.value = [] |
| 129 | + areaInput.value = undefined |
| 130 | + feedback.value = [] |
| 131 | + dialog.value = false |
| 132 | + } |
| 133 | + function check () { |
| 134 | + feedback.value = [] |
| 135 | + const barcodeSet = new Set<string>(uniqueBarcodes) |
| 136 | + const germplasmSet = new Set<string>(uniqueNames) |
| 137 | + newGermplasm.value.forEach((cell, index) => { |
| 138 | + const barcode = cell.barcode |
| 139 | + if (barcode) { |
| 140 | + if (barcodeSet.has(barcode)) { |
| 141 | + feedback.value.push({ |
| 142 | + type: 'error', |
| 143 | + message: t('formFeedbackSetupDuplicateBarcode', { columnIndex: 1, rowIndex: index + 1, germplasm: cell.germplasm, rep: cell.rep, barcode: barcode }), |
| 144 | + }) |
| 145 | + } |
| 146 | + barcodeSet.add(barcode) |
| 147 | + } |
| 148 | +
|
| 149 | + const displayName = `${cell.germplasm}|${cell.rep}` |
| 150 | + if (germplasmSet.has(displayName)) { |
| 151 | + feedback.value.push({ |
| 152 | + type: 'warning', |
| 153 | + message: t('formFeedbackSetupDuplicateGermplasmRep', { columnIndex: 1, rowIndex: index + 1, germplasm: cell.germplasm, rep: cell.rep || 'N/A' }), |
| 154 | + }) |
| 155 | + } else { |
| 156 | + germplasmSet.add(displayName) |
| 157 | + } |
| 158 | + }) |
| 159 | + } |
| 160 | + function save () { |
| 161 | + if (!feedback.value.some(f => f.type === 'error')) { |
| 162 | + addTrialGermplasm(compProps.trial.localId || '', newGermplasm.value) |
| 163 | + .then(() => { |
| 164 | + emitter.emit('trials-updated') |
| 165 | + hide() |
| 166 | + }) |
| 167 | + .catch(e => { |
| 168 | + console.error('nay!', e) |
| 169 | + }) |
| 170 | + } |
| 171 | + } |
| 172 | + function addFromInput () { |
| 173 | + const lines = (areaInput.value || '').split(/\r?\n/).map(p => p.trim()).filter(p => p.length > 0) |
| 174 | +
|
| 175 | + lines.forEach(l => { |
| 176 | + newGermplasm.value.push({ |
| 177 | + germplasm: l, |
| 178 | + rep: '', |
| 179 | + barcode: '', |
| 180 | + treatment: '', |
| 181 | + friendlyName: '', |
| 182 | + pedigree: '', |
| 183 | + }) |
| 184 | + }) |
| 185 | +
|
| 186 | + areaInput.value = undefined |
| 187 | + } |
| 188 | +
|
| 189 | + defineExpose({ |
| 190 | + show, |
| 191 | + hide, |
| 192 | + }) |
| 193 | +</script> |
0 commit comments