Skip to content

Commit 8855ae5

Browse files
- ADD: Added initial version of "add germplasm" modal.
-
1 parent eab0f14 commit 8855ae5

File tree

12 files changed

+359
-19
lines changed

12 files changed

+359
-19
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "gridscore-vuetify",
33
"private": true,
44
"type": "module",
5-
"version": "0.0.0",
5+
"version": "4.0.0",
66
"scripts": {
77
"dev": "vite",
88
"build": "run-p type-check \"build-only {@}\" --",

src/components.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ declare module 'vue' {
1010
export interface GlobalComponents {
1111
AddPersonModal: typeof import('./components/modals/AddPersonModal.vue')['default']
1212
AddTraitModal: typeof import('./components/modals/AddTraitModal.vue')['default']
13+
AddTrialGermplasmModal: typeof import('./components/modals/AddTrialGermplasmModal.vue')['default']
1314
AppFooter: typeof import('./components/AppFooter.vue')['default']
1415
ArrowDirectionGrid: typeof import('./components/util/ArrowDirectionGrid.vue')['default']
1516
BarChart: typeof import('./components/chart/BarChart.vue')['default']
@@ -22,6 +23,7 @@ declare module 'vue' {
2223
ColumnHeader: typeof import('./components/data/ColumnHeader.vue')['default']
2324
CommentModal: typeof import('./components/modals/CommentModal.vue')['default']
2425
ConfirmModal: typeof import('./components/modals/ConfirmModal.vue')['default']
26+
copy: typeof import('./components/modals/AddPersonModal copy.vue')['default']
2527
CornerPointsMap: typeof import('./components/setup/CornerPointsMap.vue')['default']
2628
DataCanvas: typeof import('./components/data/DataCanvas.vue')['default']
2729
DataEntryActions: typeof import('./components/modals/DataEntryActions.vue')['default']
@@ -34,6 +36,7 @@ declare module 'vue' {
3436
GenericAddEditFormModal: typeof import('./components/modals/GenericAddEditFormModal.vue')['default']
3537
GermplasmAutocomplete: typeof import('./components/inputs/GermplasmAutocomplete.vue')['default']
3638
GermplasmLayoutTable: typeof import('./components/setup/GermplasmLayoutTable.vue')['default']
39+
GermplasmMetadataInputSection: typeof import('./components/inputs/GermplasmMetadataInputSection.vue')['default']
3740
GpsInput: typeof import('./components/inputs/GpsInput.vue')['default']
3841
GpsTraitMap: typeof import('./components/trait/GpsTraitMap.vue')['default']
3942
GuideOrderSelector: typeof import('./components/trial/GuideOrderSelector.vue')['default']
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<div v-if="cell">
3+
<v-text-field
4+
v-model="cell.germplasm"
5+
hide-details
6+
density="compact"
7+
required
8+
:label="$t('formLabelSetupGermplasm')"
9+
/>
10+
<v-text-field
11+
v-model="cell.rep"
12+
hide-details
13+
density="compact"
14+
:label="$t('formLabelSetupRep')"
15+
/>
16+
<v-text-field
17+
v-model="cell.treatment"
18+
hide-details
19+
density="compact"
20+
v-show="visibleFields.includes('treatment')"
21+
:label="$t('formLabelSetupTreatment')"
22+
/>
23+
<v-text-field
24+
v-model="cell.friendlyName"
25+
hide-details
26+
density="compact"
27+
v-show="visibleFields.includes('friendlyName')"
28+
:label="$t('formLabelSetupFriendlyName')"
29+
/>
30+
<v-text-field
31+
v-model="cell.barcode"
32+
hide-details
33+
density="compact"
34+
v-show="visibleFields.includes('barcode')"
35+
:label="$t('formLabelSetupBarcode')"
36+
/>
37+
<v-text-field
38+
v-model="cell.pedigree"
39+
hide-details
40+
density="compact"
41+
v-show="visibleFields.includes('pedigree')"
42+
:label="$t('formLabelSetupPedigree')"
43+
/>
44+
</div>
45+
</template>
46+
47+
<script setup lang="ts">
48+
import type { CellMetadata } from '@/plugins/types/gridscore'
49+
50+
const compProps = defineProps<{
51+
visibleFields: ('treatment' | 'friendlyName' | 'pedigree' | 'barcode')[]
52+
}>()
53+
54+
const cell = defineModel<CellMetadata>()
55+
56+
const valid = computed(() => cell.value && cell.value.germplasm && cell.value.germplasm.trim().length > 0)
57+
58+
defineExpose({
59+
valid,
60+
})
61+
</script>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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>

src/components/setup/GermplasmLayoutTable.vue

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,11 @@
466466
} else {
467467
g.value = ''
468468
}
469-
g.placeholder = 'Name/Id'
469+
g.placeholder = t('formLabelSetupGermplasm')
470470
// Rep input
471471
const rep = createElement(cell, 'input') as HTMLInputElement
472472
rep.id = `rep-${row}-${column}`
473-
rep.placeholder = 'Rep'
473+
rep.placeholder = t('formLabelSetupRep')
474474
if (dataCell) {
475475
rep.value = dataCell.rep || ''
476476
} else {
@@ -483,7 +483,7 @@
483483
if (!visibleFields.value.includes('treatment')) {
484484
treatment.classList.add('d-none')
485485
}
486-
treatment.placeholder = 'Treatment'
486+
treatment.placeholder = t('formLabelSetupTreatment')
487487
if (dataCell) {
488488
treatment.value = dataCell.treatment || ''
489489
} else {
@@ -496,7 +496,7 @@
496496
if (!visibleFields.value.includes('friendlyName')) {
497497
friendlyName.classList.add('d-none')
498498
}
499-
friendlyName.placeholder = 'Friendly name'
499+
friendlyName.placeholder = t('formLabelSetupFriendlyName')
500500
if (dataCell) {
501501
friendlyName.value = dataCell.friendlyName || ''
502502
} else {
@@ -509,7 +509,7 @@
509509
if (!visibleFields.value.includes('barcode')) {
510510
barcode.classList.add('d-none')
511511
}
512-
barcode.placeholder = 'Barcode'
512+
barcode.placeholder = t('formLabelSetupBarcode')
513513
if (dataCell) {
514514
barcode.value = dataCell.barcode || ''
515515
} else {
@@ -522,7 +522,7 @@
522522
if (!visibleFields.value.includes('pedigree')) {
523523
pedigree.classList.add('d-none')
524524
}
525-
pedigree.placeholder = 'Pedigree'
525+
pedigree.placeholder = t('formLabelSetupPedigree')
526526
if (dataCell) {
527527
pedigree.value = dataCell.pedigree || ''
528528
} else {

src/components/trial/TrialSelector.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
@add-trait="addTrait(trial.raw)"
131131
@add-person="addPerson(trial.raw)"
132132
@add-metadata="addMetadata(trial.raw)"
133+
@add-germplasm="addGermplasm(trial.raw)"
133134
@add-data="addData(trial.raw)"
134135
@duplicate="router.push(`/setup/${trial.raw.localId}/clone`)"
135136
@edit="router.push(`/setup/${trial.raw.localId}/edit`)"
@@ -146,8 +147,9 @@
146147
</v-card-text>
147148

148149
<TrialShareModal :trial="selectedTrial" ref="trialShareModal" v-if="selectedTrial" />
149-
<AddTraitModal ref="addTraitModal" v-if="selectedTrial || selectedTrials.length > 0" @traits-added="addTraitsToSelectedTrials" />
150-
<AddPersonModal ref="addPersonModal" v-if="selectedTrial || selectedTrials.length > 0" @person-added="addPersonToSelectedTrials" />
150+
<AddTraitModal ref="addTraitModal" v-if="selectedTrialsEditable" @traits-added="addTraitsToSelectedTrials" />
151+
<AddPersonModal ref="addPersonModal" v-if="selectedTrialsEditable" @person-added="addPersonToSelectedTrials" />
152+
<AddTrialGermplasmModal :trial="selectedTrial" ref="addGermplasmModal" v-if="selectedTrial && selectedTrialsEditable" />
151153
<UpdateTrialMetadataModal :trial="selectedTrial" ref="updateTrialMetadataModal" v-if="selectedTrial" />
152154
<UpdateTrialDataModal :trial="selectedTrial" ref="updateTrialDataModal" v-if="selectedTrial" />
153155
</v-card>
@@ -172,6 +174,7 @@
172174
import AddPersonModal from '@/components/modals/AddPersonModal.vue'
173175
import UpdateTrialMetadataModal from '@/components/modals/UpdateTrialMetadataModal.vue'
174176
import UpdateTrialDataModal from '@/components/modals/UpdateTrialDataModal.vue'
177+
import AddTrialGermplasmModal from '@/components/modals/AddTrialGermplasmModal.vue'
175178
176179
interface TrialGroup {
177180
id: string
@@ -204,13 +207,16 @@
204207
const trialShareModal = useTemplateRef('trialShareModal')
205208
const addTraitModal = useTemplateRef('addTraitModal')
206209
const addPersonModal = useTemplateRef('addPersonModal')
210+
const addGermplasmModal = useTemplateRef('addGermplasmModal')
207211
const updateTrialMetadataModal = useTemplateRef('updateTrialMetadataModal')
208212
const updateTrialDataModal = useTemplateRef('updateTrialDataModal')
209213
210214
const filterForWarning = ref<'local' | 'remote' | 'expiry'>()
211215
212216
const editableSelectedTrials = computed(() => selectedTrials.value.filter(t => t.editable === true))
213217
218+
const selectedTrialsEditable = computed(() => (selectedTrial.value && selectedTrial.value.editable === true) || (selectedTrials.value && selectedTrials.value.length > 0 && selectedTrials.value.every(t => t.editable === true)))
219+
214220
watch(selectionEnabled, async () => {
215221
selectedTrials.value = []
216222
})
@@ -321,6 +327,12 @@
321327
}
322328
}
323329
330+
function addGermplasm (trial: TrialPlus) {
331+
selectedTrial.value = trial
332+
333+
nextTick(() => addGermplasmModal.value?.show())
334+
}
335+
324336
function addMetadata (trial: TrialPlus) {
325337
selectedTrial.value = trial
326338

0 commit comments

Comments
 (0)