Skip to content

Commit 7f10525

Browse files
- FIX: Fixed issues with germplasm autocomplete selection.
- ADD: Added QR code input to germplasm addition modal. -
1 parent 8855ae5 commit 7f10525

File tree

10 files changed

+150
-69
lines changed

10 files changed

+150
-69
lines changed

src/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
<v-img
88
class="ms-4"
9+
style="cursor: pointer" @click="$router.push('/')"
910
src="/img/gridscore-next.svg"
1011
max-height="40"
1112
max-width="40"
@@ -111,7 +112,7 @@
111112

112113
<ConfirmModal />
113114
<ChangelogModal />
114-
<v-snackbar-queue timeout="6000" location="top" v-model="snackbarQueue" />
115+
<v-snackbar-queue timeout="4000" location="top" v-model="snackbarQueue" />
115116
<v-overlay
116117
:model-value="loading"
117118
class="align-center justify-center"

src/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ declare module 'vue' {
5555
PlotCanvas: typeof import('./components/data/PlotCanvas.vue')['default']
5656
PlotDataInformation: typeof import('./components/plot/PlotDataInformation.vue')['default']
5757
PlotInformation: typeof import('./components/plot/PlotInformation.vue')['default']
58+
QRScanInput: typeof import('./components/inputs/QRScanInput.vue')['default']
5859
ReplicateHeatmap: typeof import('./components/chart/ReplicateHeatmap.vue')['default']
5960
ResponsiveButton: typeof import('./components/util/ResponsiveButton.vue')['default']
6061
RouterLink: typeof import('vue-router')['RouterLink']

src/components/inputs/GermplasmAutocomplete.vue

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
:prepend-inner-icon="mdiMagnify"
2424
>
2525
<template #prepend>
26-
<v-btn :icon="mdiQrcodeScan" v-tooltip:top="$t('tooltipScanQRCode')" @click="showCamera = !showCamera" :color="showCamera ? 'info' : undefined" />
26+
<v-btn :icon="mdiQrcodeScan" v-tooltip:top="$t('tooltipScanQRCode')" @click="toggleCamera" :color="showCamera ? 'info' : undefined" />
2727
</template>
2828

2929
<template #append v-if="supportsNfc">
@@ -40,11 +40,25 @@
4040
</template>
4141
</v-autocomplete>
4242

43-
<QrcodeStream
44-
v-if="showCamera"
45-
:formats="['qr_code', 'code_128', 'code_39', 'upc_a', 'upc_e']"
46-
@detect="onDetect"
47-
/>
43+
<v-bottom-sheet
44+
v-if="scanInBottomSheet"
45+
v-model="bottomSheetVisible"
46+
inset
47+
max-height="75vh"
48+
>
49+
<QrcodeStream
50+
v-if="showCamera"
51+
:formats="['qr_code', 'code_128', 'code_39', 'upc_a', 'upc_e']"
52+
@detect="onDetect"
53+
/>
54+
</v-bottom-sheet>
55+
<template v-else>
56+
<QrcodeStream
57+
v-if="showCamera"
58+
:formats="['qr_code', 'code_128', 'code_39', 'upc_a', 'upc_e']"
59+
@detect="onDetect"
60+
/>
61+
</template>
4862
</div>
4963
</template>
5064

@@ -64,24 +78,39 @@
6478
const supportsNfc = ref(false)
6579
const showCamera = ref(false)
6680
const abortController = shallowRef<AbortController>()
81+
const bottomSheetVisible = ref(false)
6782
6883
export interface GermplasmAutoCompleteProps {
6984
trial: TrialPlus
7085
density?: 'default' | 'comfortable' | 'compact'
7186
multiple?: boolean
7287
label?: string
7388
hint?: string
89+
scanInBottomSheet?: boolean
7490
}
7591
7692
const compProps = withDefaults(defineProps<GermplasmAutoCompleteProps>(), {
7793
density: 'default',
7894
multiple: false,
7995
label: 'formLabelSearch',
96+
scanInBottomSheet: false,
8097
})
8198
8299
const searchField = useTemplateRef('searchField')
83100
const performanceMode = computed(() => store.storePerformanceMode === true || trialGermplasm.value.length > 1000)
84101
102+
function toggleCamera () {
103+
if (compProps.scanInBottomSheet) {
104+
bottomSheetVisible.value = true
105+
106+
nextTick(() => {
107+
showCamera.value = !showCamera.value
108+
})
109+
} else {
110+
showCamera.value = !showCamera.value
111+
}
112+
}
113+
85114
function getTrialGermplasm () {
86115
const data = getTrialDataCached()
87116
@@ -183,6 +212,10 @@
183212
getTrialGermplasm()
184213
})
185214
215+
watch(bottomSheetVisible, async () => {
216+
showCamera.value = false
217+
})
218+
186219
onMounted(() => {
187220
supportsNfc.value = 'NDEFReader' in window
188221
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<template>
2+
<v-textarea v-model="model" :label="label" :hint="hint" :persistent-hint="hint !== undefined && hint !== ''" v-if="textarea">
3+
<template #append-inner>
4+
<v-btn :icon="mdiQrcodeScan" v-tooltip:top="localTooltip" @click="showCamera = !showCamera" />
5+
</template>
6+
</v-textarea>
7+
<v-text-field v-model="model" :label="label" :hint="hint" :persistent-hint="hint !== undefined && hint !== ''" v-else>
8+
<template #append>
9+
<v-btn :icon="mdiQrcodeScan" v-tooltip:top="localTooltip" @click="showCamera = !showCamera" />
10+
</template>
11+
</v-text-field>
12+
13+
<QrcodeStream
14+
v-if="showCamera"
15+
:formats="formats"
16+
@detect="onDetect"
17+
/>
18+
</template>
19+
20+
<script setup lang="ts">
21+
import { mdiQrcodeScan } from '@mdi/js'
22+
import { useI18n } from 'vue-i18n'
23+
import { QrcodeStream, type BarcodeFormat, type DetectedBarcode } from 'vue-qrcode-reader'
24+
25+
const { t } = useI18n()
26+
27+
export interface QRScanInputProps {
28+
label: string
29+
hint?: string
30+
tooltip?: string
31+
textarea?: boolean
32+
formats?: BarcodeFormat[]
33+
}
34+
35+
const model = defineModel<string>()
36+
37+
const showCamera = ref(false)
38+
const localTooltip = computed(() => compProps.tooltip || t('buttonScanQR'))
39+
40+
const compProps = withDefaults(defineProps<QRScanInputProps>(), {
41+
textarea: false,
42+
formats: () => ['qr_code'],
43+
})
44+
45+
function onDetect (detectedCodes: DetectedBarcode[]) {
46+
if (detectedCodes && detectedCodes.length > 0) {
47+
let c = detectedCodes[0]?.rawValue
48+
49+
if (c) {
50+
if (c.includes('/')) {
51+
c = c.slice(c.lastIndexOf('/') + 1)
52+
}
53+
54+
if (compProps.textarea && model.value) {
55+
model.value += `\n${c}`
56+
} else {
57+
model.value = c
58+
}
59+
showCamera.value = false
60+
61+
nextTick(() => emit('code-scanned'))
62+
}
63+
}
64+
}
65+
66+
const emit = defineEmits(['code-scanned'])
67+
</script>

src/components/modals/AddTrialGermplasmModal.vue

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@
1414
<v-btn value="pedigree" :text="$t('formCheckboxSetupShowPedigree')" :prepend-icon="mdiFamilyTree" />
1515
</v-btn-toggle>
1616

17-
<v-textarea
17+
<QRScanInput
18+
v-model="areaInput"
19+
textarea
1820
:label="$t('formLabelAddGermplasmName')"
1921
: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>
22+
:tooltip="$t('tooltipScanQRCodeAddGermplasm')"
23+
:formats="['qr_code', 'code_128', 'code_39', 'ean_8', 'ean_13']"
24+
/>
2725
</div>
26+
27+
<v-btn :prepend-icon="mdiPlus" color="primary" :disabled="areaInput === undefined || areaInput.trim().length === 0" class="mt-2" :text="$t('buttonAdd')" @click="addFromInput" />
2828
</template>
2929
</v-card>
3030

@@ -42,7 +42,7 @@
4242
</v-virtual-scroll>
4343

4444
<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'" />
45+
<v-btn v-if="hasErrors || hasWarnings" @click="bottomSheetVisible = true" :text="$t('formFeedbackLayout', feedback?.length || 0)" :prepend-icon="hasErrors ? mdiAlertCircle : mdiAlert" :color="hasErrors ? 'error' : 'warning'" />
4646
</div>
4747
</template>
4848

@@ -82,6 +82,7 @@
8282
import emitter from 'tiny-emitter/instance'
8383
import type { LayoutFeedback } from '@/components/setup/GermplasmLayoutTable.vue'
8484
import { useI18n } from 'vue-i18n'
85+
import QRScanInput from '@/components/inputs/QRScanInput.vue'
8586
8687
const compProps = defineProps<{
8788
trial: TrialPlus
@@ -93,21 +94,21 @@
9394
const newGermplasm = ref<CellMetadata[]>([])
9495
const visibleFields = ref<('treatment' | 'friendlyName' | 'pedigree' | 'barcode')[]>([])
9596
const areaInput = ref<string>()
96-
const feedback = ref<LayoutFeedback[]>([])
97+
const feedback = ref<LayoutFeedback[] | undefined>()
9798
const bottomSheetVisible = ref(false)
9899
99100
let uniqueNames = new Set<string>()
100101
let uniqueBarcodes = new Set<string>()
101102
102103
const hasErrors = computed(() => feedback.value && feedback.value.some(f => f.type === 'error'))
103104
const hasWarnings = computed(() => feedback.value && feedback.value.some(f => f.type === 'warning'))
104-
const canContinue = computed(() => canCheck.value && hasErrors.value === false)
105+
const canContinue = computed(() => canCheck.value && feedback.value && hasErrors.value === false)
105106
const canCheck = computed(() => newGermplasm.value.length > 0)
106107
107108
function show () {
108109
dialog.value = true
109110
areaInput.value = undefined
110-
feedback.value = []
111+
feedback.value = undefined
111112
112113
getTrialData(compProps.trial.localId || '')
113114
.then(data => {
@@ -127,7 +128,7 @@
127128
function hide () {
128129
newGermplasm.value = []
129130
areaInput.value = undefined
130-
feedback.value = []
131+
feedback.value = undefined
131132
dialog.value = false
132133
}
133134
function check () {
@@ -138,7 +139,7 @@
138139
const barcode = cell.barcode
139140
if (barcode) {
140141
if (barcodeSet.has(barcode)) {
141-
feedback.value.push({
142+
feedback.value?.push({
142143
type: 'error',
143144
message: t('formFeedbackSetupDuplicateBarcode', { columnIndex: 1, rowIndex: index + 1, germplasm: cell.germplasm, rep: cell.rep, barcode: barcode }),
144145
})
@@ -148,7 +149,7 @@
148149
149150
const displayName = `${cell.germplasm}|${cell.rep}`
150151
if (germplasmSet.has(displayName)) {
151-
feedback.value.push({
152+
feedback.value?.push({
152153
type: 'warning',
153154
message: t('formFeedbackSetupDuplicateGermplasmRep', { columnIndex: 1, rowIndex: index + 1, germplasm: cell.germplasm, rep: cell.rep || 'N/A' }),
154155
})
@@ -158,7 +159,7 @@
158159
})
159160
}
160161
function save () {
161-
if (!feedback.value.some(f => f.type === 'error')) {
162+
if (!feedback.value?.some(f => f.type === 'error')) {
162163
addTrialGermplasm(compProps.trial.localId || '', newGermplasm.value)
163164
.then(() => {
164165
emitter.emit('trials-updated')

src/components/trial/TrialImport.vue

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,12 @@
2828
</template>
2929
</v-card>
3030

31-
<v-text-field v-model="shareCode" :label="$t('formLabelTrialImportCode')" :hint="$t('formDescriptionTrialImportCode')" persistent-hint>
32-
<template #append>
33-
<v-btn :icon="mdiQrcodeScan" v-tooltip:top="$t('buttonScanQR')" @click="showCamera = !showCamera" />
34-
</template>
35-
</v-text-field>
36-
37-
<QrcodeStream
38-
v-if="showCamera"
39-
:formats="['qr_code']"
40-
@detect="onDetect"
31+
<QRScanInput
32+
v-model="shareCode"
33+
:label="$t('formLabelTrialImportCode')"
34+
:hint="$t('formDescriptionTrialImportCode')"
35+
:tooltip="$t('buttonScanQR')"
36+
@code-scanned="getTrial"
4137
/>
4238

4339
<p class="text-error mt-3 mb-0" v-if="serverError"><span v-html="serverError" /></p>
@@ -92,10 +88,10 @@
9288
import { coreStore } from '@/stores/app'
9389
import { UseOnline } from '@vueuse/components'
9490
import { useI18n } from 'vue-i18n'
95-
import { QrcodeStream, type DetectedBarcode } from 'vue-qrcode-reader'
9691
9792
import emitter from 'tiny-emitter/instance'
98-
import { mdiFolderTable, mdiInformation, mdiLanDisconnect, mdiMagnify, mdiNotebookCheck, mdiNotebookPlus, mdiQrcodeScan } from '@mdi/js'
93+
import { mdiFolderTable, mdiInformation, mdiLanDisconnect, mdiMagnify, mdiNotebookCheck, mdiNotebookPlus } from '@mdi/js'
94+
import QRScanInput from '@/components/inputs/QRScanInput.vue'
9995
10096
const compProps = defineProps<{
10197
code?: string
@@ -109,7 +105,6 @@
109105
const loadFromRemote = ref(false)
110106
const remoteUrl = ref<string>()
111107
const remoteToken = ref<string>()
112-
const showCamera = ref(false)
113108
const bottomSheetVisible = ref(false)
114109
const trialGroup = ref<string>()
115110
const noChangeRequired = ref(false)
@@ -123,23 +118,6 @@
123118
const serverError = ref<string>()
124119
const infoMessage = ref<string>()
125120
126-
function onDetect (detectedCodes: DetectedBarcode[]) {
127-
if (detectedCodes && detectedCodes.length > 0) {
128-
let c = detectedCodes[0]?.rawValue
129-
130-
if (c) {
131-
if (c.includes('/')) {
132-
c = c.slice(c.lastIndexOf('/') + 1)
133-
}
134-
135-
shareCode.value = c
136-
showCamera.value = false
137-
138-
getTrial()
139-
}
140-
}
141-
}
142-
143121
function getTrial () {
144122
if (!shareCode.value) {
145123
return

src/pages/collect/grid.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
<GermplasmAutocomplete
7373
:trial="trial"
7474
v-model="searchMatch"
75+
scan-in-bottom-sheet
7576
min-width="120px"
7677
max-width="min(50vw, 250px)"
7778
density="compact"

src/pages/collect/input.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
5050
const trial = ref<TrialPlus>()
5151
const geolocation = ref<Geolocation>()
52-
const searchMatch = ref<CellPlus[]>([])
52+
const searchMatch = ref<CellPlus>()
5353
5454
const dataEntryModal = useTemplateRef('dataEntryModal')
5555
const searchField = useTemplateRef('searchField')
@@ -120,9 +120,9 @@
120120
}
121121
122122
watch(searchMatch, async newValue => {
123-
if (newValue && newValue.length > 0 && newValue[0]) {
124-
selectPlot(newValue[0].row || 0, newValue[0].column || 0)
125-
searchMatch.value = []
123+
if (newValue) {
124+
selectPlot(newValue.row || 0, newValue.column || 0)
125+
searchMatch.value = undefined
126126
}
127127
})
128128

0 commit comments

Comments
 (0)