Skip to content

Commit 756c9b4

Browse files
- ADD: Added brapi export germplasm and trait lookup.
- FIX: Fixed brapi config changes not persisting into trial. -
1 parent 7eb653a commit 756c9b4

File tree

11 files changed

+538
-65
lines changed

11 files changed

+538
-65
lines changed

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@
184184
185185
const changelogVersionNumber = ref<string>()
186186
187-
const trialInfoPages = ref<string[]>(['/collect/grid', '/visualization/heatmap', '/visualization/timeline', '/visualization/statistics', '/visualization/map', '/collect/walk', '/collect/input', '/export'])
187+
const trialInfoPages = ref<string[]>(['/collect/grid', '/visualization/heatmap', '/visualization/timeline', '/visualization/statistics', '/visualization/map', '/collect/walk', '/collect/input'])
188188
189189
let plausible: any
190190
let wakeLock: WakeLockSentinel | undefined
Lines changed: 344 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,353 @@
11
<template>
2-
<BrapiConfig @brapi-config-updated="update" />
2+
<div>
3+
<BrapiConfig :trial="trial" @brapi-config-updated="triggerTrialReload" />
4+
5+
<v-row class="mt-5">
6+
<v-col cols="12" lg="6" v-if="germplasmWithBrapiDbIds">
7+
<v-card :title="$t('pageBrapiExportBrapiGermplasmIdTitle')" :loading="germplasmLoading">
8+
<template #loader="{ isActive }">
9+
<v-progress-linear :active="isActive" color="primary" height="3" indeterminate />
10+
</template>
11+
<template #text>
12+
<!-- @vue-ignore -->
13+
<p :class="allGermplasmValidDbId ? 'text-success' : 'text-error'">{{ $t('pageBrapiExportBrapiGermplasmIdText', germplasmWithBrapiDbIds) }}</p>
14+
</template>
15+
<template #actions>
16+
<v-btn variant="tonal" :color="allGermplasmValidDbId ? undefined : 'primary'" :disabled="germplasmLoading || allGermplasmValidDbId" @click="searchBrapiGermplasmMatches" :prepend-icon="mdiMagnify" :text="$t('buttonUpdate')" />
17+
</template>
18+
</v-card>
19+
</v-col>
20+
<v-col cols="12" lg="6" v-if="traitsWithBrapiDbIds">
21+
<v-card :title="$t('pageBrapiExportBrapiTraitIdTitle')" :loading="traitsLoading">
22+
<template #loader="{ isActive }">
23+
<v-progress-linear :active="isActive" color="primary" height="3" indeterminate />
24+
</template>
25+
<template #text>
26+
<!-- @vue-ignore -->
27+
<p :class="allTraitsValidDbId ? 'text-success' : 'text-error'">{{ $t('pageBrapiExportBrapiTraitIdText', traitsWithBrapiDbIds) }}</p>
28+
</template>
29+
<template #actions>
30+
<v-btn variant="tonal" :color="allTraitsValidDbId ? undefined : 'primary'" :disabled="traitsLoading || allTraitsValidDbId" @click="searchBrapiTraitMatches" :prepend-icon="mdiMagnify" :text="$t('buttonUpdate')" />
31+
<template v-if="traitLookupRanAtLeastOnce && !allTraitsValidDbId">
32+
<v-spacer />
33+
<v-btn variant="tonal" color="primary" @click="writeTraitsWithoutBrapiId" :prepend-icon="mdiCloudPlus" :text="$t('buttonUpload')" />
34+
</template>
35+
</template>
36+
</v-card>
37+
</v-col>
38+
</v-row>
39+
</div>
340
</template>
441

542
<script setup lang="ts">
643
import BrapiConfig from '@/components/util/BrapiConfig.vue'
44+
import { brapiDefaultCatchHandler, brapiPostGermplasmSearch, brapiPostObservationVariables, brapiPostObservationVariableSearch } from '@/plugins/brapi'
45+
import { getTrialDataCached } from '@/plugins/datastore'
46+
import { updateGermplasmBrapiIds, updateTraitBrapiIds } from '@/plugins/idb'
47+
import type { ObservationVariable, Scale } from '@/plugins/types/brapi'
48+
import type { CellPlus, TrialPlus } from '@/plugins/types/client'
49+
import { TraitDataType } from '@/plugins/types/gridscore'
50+
import { mdiCloudPlus, mdiMagnify } from '@mdi/js'
51+
52+
import emitter from 'tiny-emitter/instance'
53+
54+
const compProps = defineProps<{
55+
trial: TrialPlus
56+
}>()
57+
58+
interface Counts {
59+
count: number
60+
total: number
61+
itemsWithoutId: string[]
62+
}
63+
64+
const emit = defineEmits(['trigger-reload-trial'])
65+
66+
const germplasmWithBrapiDbIds = ref<Counts>()
67+
const germplasmLoading = ref(false)
68+
const traitsWithBrapiDbIds = ref<Counts>()
69+
const traitsLoading = ref(false)
70+
const traitLookupRanAtLeastOnce = ref(false)
71+
72+
let trialData: { [index: string]: CellPlus } | undefined
73+
74+
const allGermplasmValidDbId = computed(() => germplasmWithBrapiDbIds.value !== undefined && germplasmWithBrapiDbIds.value.count === germplasmWithBrapiDbIds.value.total)
75+
const allTraitsValidDbId = computed(() => traitsWithBrapiDbIds.value !== undefined && traitsWithBrapiDbIds.value.count === traitsWithBrapiDbIds.value.total)
76+
77+
function updateBrapiGermplasmDbIdCounts () {
78+
const trialData = getTrialDataCached()
79+
if (!trialData) {
80+
germplasmWithBrapiDbIds.value = {
81+
count: 0,
82+
total: 0,
83+
itemsWithoutId: [],
84+
}
85+
} else {
86+
let count = 0
87+
let total = 0
88+
const itemsWithoutId = []
89+
// For each field row
90+
for (let y = 0; y < compProps.trial.layout.rows; y++) {
91+
// And each field column
92+
for (let x = 0; x < compProps.trial.layout.columns; x++) {
93+
// Get the data cell
94+
const cell = trialData[`${y}|${x}`]
95+
// If there is data
96+
if (cell) {
97+
total++
98+
99+
if (cell.brapiId) {
100+
count++
101+
} else {
102+
itemsWithoutId.push(cell.germplasm)
103+
}
104+
}
105+
}
106+
}
107+
108+
germplasmWithBrapiDbIds.value = {
109+
count,
110+
total,
111+
itemsWithoutId,
112+
}
113+
}
114+
}
115+
116+
function updateBrapiTraitDbIdCounts () {
117+
let count = 0
118+
const total = compProps.trial.traits.length
119+
const itemsWithoutId: string[] = []
120+
121+
compProps.trial.traits.forEach(t => {
122+
if (t.brapiId !== undefined && t.brapiId !== null) {
123+
count++
124+
} else {
125+
itemsWithoutId.push(t.name)
126+
}
127+
})
128+
129+
traitsWithBrapiDbIds.value = {
130+
count,
131+
total,
132+
itemsWithoutId,
133+
}
134+
}
135+
136+
function searchBrapiTraitMatches () {
137+
traitsLoading.value = true
138+
139+
brapiPostObservationVariableSearch({
140+
observationVariableNames: traitsWithBrapiDbIds.value?.itemsWithoutId,
141+
}).then(result => {
142+
traitLookupRanAtLeastOnce.value = true
143+
const map = new Map<string, ObservationVariable[]>()
144+
if (result) {
145+
result.forEach(g => {
146+
const lower = g.observationVariableName?.toLowerCase() || ''
147+
let match = map.get(lower)
148+
if (!match) {
149+
match = []
150+
}
151+
152+
match.push(g)
153+
154+
map.set(lower, match)
155+
})
156+
157+
updateBrapiTraitDbIdsInDatabase(map)
158+
}
159+
}).catch(brapiDefaultCatchHandler).finally(() => {
160+
traitsLoading.value = false
161+
})
162+
}
163+
164+
function updateBrapiTraitDbIdsInDatabase (map: Map<string, ObservationVariable[]>) {
165+
traitsLoading.value = true
166+
167+
const mapping: { [index: string]: string } = {}
168+
169+
compProps.trial.traits.forEach(t => {
170+
const brapiMatches = map.get(t.name.toLowerCase())
171+
172+
if (brapiMatches && brapiMatches.length > 0) {
173+
brapiMatches.forEach(brapiMatch => {
174+
if (brapiMatch && brapiMatch.scale) {
175+
// Check data type
176+
let matches = false
177+
switch (brapiMatch.scale.dataType) {
178+
case 'Date':
179+
matches = t.dataType === 'date'
180+
break
181+
case 'Text':
182+
matches = t.dataType === 'text'
183+
break
184+
case 'Numeric':
185+
case 'Numerical':
186+
matches = t.dataType === 'float' || t.dataType === 'int' || t.dataType === 'range'
187+
break
188+
case 'Duration':
189+
matches = t.dataType === 'int'
190+
break
191+
case 'Nominal':
192+
case 'Ordinal':
193+
matches = t.dataType === 'categorical'
194+
break
195+
}
196+
197+
if (matches) {
198+
t.brapiId = brapiMatch.observationVariableDbId
199+
mapping[t.id || ''] = brapiMatch.observationVariableDbId || ''
200+
}
201+
}
202+
})
203+
}
204+
})
205+
206+
if (Object.keys(mapping).length > 0) {
207+
updateTraitBrapiIds(compProps.trial.localId || '', mapping)
208+
.then(() => emitter.emit('trial-selected'))
209+
}
210+
211+
traitsLoading.value = false
212+
}
213+
214+
function writeTraitsWithoutBrapiId () {
215+
traitsLoading.value = true
216+
const twbdi = traitsWithBrapiDbIds.value
217+
218+
if (twbdi && twbdi.itemsWithoutId && twbdi.itemsWithoutId.length > 0) {
219+
const newTraits = compProps.trial.traits.concat().filter(t => twbdi.itemsWithoutId.includes(t.name)).map(t => {
220+
const newObsv: ObservationVariable = {
221+
observationVariableName: t.name,
222+
trait: {
223+
traitName: t.name,
224+
traitClass: t.group ? t.group.name : undefined,
225+
},
226+
}
227+
228+
const scale: Scale = {
229+
dataType: undefined,
230+
validValues: undefined,
231+
}
232+
233+
switch (t.dataType) {
234+
case TraitDataType.date:
235+
scale.dataType = 'Date'
236+
break
237+
case TraitDataType.float:
238+
case TraitDataType.int:
239+
case TraitDataType.range:
240+
scale.dataType = 'Numerical'
241+
break
242+
case TraitDataType.categorical:
243+
scale.dataType = 'Ordinal'
244+
break
245+
default:
246+
scale.dataType = 'Text'
247+
}
248+
249+
if (t.restrictions) {
250+
scale.validValues = {}
251+
if (t.restrictions.min !== undefined && t.restrictions.min !== null) {
252+
scale.validValues.minimumValue = `${t.restrictions.min}`
253+
}
254+
if (t.restrictions.max !== undefined && t.restrictions.max !== null) {
255+
scale.validValues.maximumValue = `${t.restrictions.max}`
256+
}
257+
if (t.restrictions.categories && t.restrictions.categories.length > 0) {
258+
scale.validValues.categories = t.restrictions.categories.map(c => {
259+
return {
260+
label: c,
261+
value: c,
262+
}
263+
})
264+
}
265+
}
266+
267+
newObsv.scale = scale
268+
269+
return newObsv
270+
})
271+
272+
brapiPostObservationVariables(newTraits)
273+
.then(() => searchBrapiTraitMatches())
274+
.catch(brapiDefaultCatchHandler)
275+
.finally(() => {
276+
traitsLoading.value = false
277+
})
278+
}
279+
}
280+
281+
function searchBrapiGermplasmMatches () {
282+
germplasmLoading.value = true
283+
brapiPostGermplasmSearch({
284+
germplasmNames: germplasmWithBrapiDbIds.value?.itemsWithoutId,
285+
}).then(result => {
286+
const map = new Map<string, string>()
287+
if (result) {
288+
result.forEach(g => {
289+
map.set(g.germplasmName.toLowerCase(), g.germplasmDbId)
290+
})
291+
292+
updateBrapiGermplasmDbIdsInDatabase(map)
293+
}
294+
}).catch(brapiDefaultCatchHandler).finally(() => {
295+
germplasmLoading.value = false
296+
})
297+
}
298+
299+
function updateBrapiGermplasmDbIdsInDatabase (map: Map<string, string>) {
300+
if (trialData) {
301+
germplasmLoading.value = true
302+
303+
const mapping: { [index: string]: string } = {}
304+
// For each field row
305+
for (let y = 0; y < compProps.trial.layout.rows; y++) {
306+
// And each field column
307+
for (let x = 0; x < compProps.trial.layout.columns; x++) {
308+
// Get the data cell
309+
const cell = trialData[`${y}|${x}`]
310+
// If there is data
311+
if (cell) {
312+
const id = map.get(cell.germplasm.toLowerCase())
313+
314+
if (id !== undefined && id !== null) {
315+
mapping[`${y}|${x}`] = id
316+
}
317+
}
318+
}
319+
}
320+
321+
// Store the data in the database
322+
if (Object.keys(mapping).length > 0) {
323+
updateGermplasmBrapiIds(compProps.trial.localId || '', mapping)
324+
.then(() => emitter.emit('trial-selected'))
325+
}
326+
327+
germplasmLoading.value = false
328+
}
329+
}
330+
331+
function triggerTrialReload () {
332+
emit('trigger-reload-trial')
333+
}
7334
8335
function update () {
9-
// TODO
336+
updateBrapiGermplasmDbIdCounts()
337+
updateBrapiTraitDbIdCounts()
10338
}
339+
340+
watch(() => compProps.trial, async () => update())
341+
342+
onMounted(() => {
343+
trialData = getTrialDataCached()
344+
345+
update()
346+
347+
emitter.on('trial-data-loaded', update)
348+
})
349+
350+
onBeforeUnmount(() => {
351+
emitter.off('trial-data-loaded', update)
352+
})
11353
</script>

src/components/trial/TrialCard.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
</div>
2222
</v-card-text>
2323

24-
<v-list variant="tonal" v-if="horizontal">
24+
<v-list variant="tonal" v-if="horizontal" :disabled="!interactive">
2525
<v-row no-gutters>
2626
<v-col cols="12" sm="6" md="4" lg="3">
2727
<v-list-item :prepend-icon="mdiFolderTable" :title="trial.group?.name || $t('widgetTrialSelectorGroupUnassigned')" />
@@ -50,7 +50,7 @@
5050
</v-row>
5151
</v-list>
5252

53-
<v-list variant="tonal" slim v-else>
53+
<v-list variant="tonal" slim :disabled="!interactive" v-else>
5454
<v-list-item :prepend-icon="mdiFolderTable" :title="trial.group?.name || $t('widgetTrialSelectorGroupUnassigned')" />
5555
<v-list-item :prepend-icon="mdiLandRowsHorizontal" :title="$t('widgetTrialSelectorRows')"><template #append><v-badge :content="getNumberWithSuffix(trial.layout.rows, 1)" inline /></template></v-list-item>
5656
<v-list-item :prepend-icon="mdiLandRowsVertical" :title="$t('widgetTrialSelectorColumns')"><template #append><v-badge :content="getNumberWithSuffix(trial.layout.columns, 1)" inline /></template></v-list-item>

0 commit comments

Comments
 (0)