Skip to content

Commit f309fb5

Browse files
- CHG: Improved performance of germplasm autocomplete for large trials.
- FIX: Fixed trait data inputs being enabled for viewers. - ADD: Added events modal. - ADD: Added data import functionality. - ADD: Added metadata import functionality. - CHG: Improved layout of horizontal trial cards. - ADD: Added home page help section. - ADD: Added initial version of trait data export page. -
1 parent bb2860f commit f309fb5

38 files changed

+2074
-117
lines changed

src/App.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<template #activator="{ props }">
2323
<v-btn :icon="mdiNotebook" v-bind="props" />
2424
</template>
25-
<TrialCard max-width="400px" :trial="selectedTrial" :show-actions="false" :interactive="false" />
25+
<TrialCard max-width="400px" :trial="selectedTrial" :show-actions="false" :interactive="false" ref="selectedTrialCard" />
2626
</v-menu>
2727

2828
<v-menu>
@@ -205,7 +205,9 @@
205205
}
206206
207207
function loadTrialInfo () {
208-
selectedTrial.value = getTrialCached()
208+
nextTick(() => {
209+
selectedTrial.value = getTrialCached()
210+
})
209211
}
210212
211213
function enablePlausible () {
@@ -387,12 +389,16 @@
387389
emitter.on('show-snackbar', showSnackbar)
388390
emitter.on('show-loading', showLoading)
389391
emitter.on('trial-data-loaded', loadTrialInfo)
392+
emitter.on('trial-properties-changed', loadTrialInfo)
393+
emitter.on('trial-information-updated', loadTrialInfo)
390394
emitter.on('plausible-event', plausibleEvent)
391395
})
392396
onBeforeUnmount(() => {
393397
emitter.off('show-snackbar', showSnackbar)
394398
emitter.off('show-loading', showLoading)
395399
emitter.off('trial-data-loaded', loadTrialInfo)
400+
emitter.off('trial-properties-changed', loadTrialInfo)
401+
emitter.off('trial-information-updated', loadTrialInfo)
396402
emitter.off('plausible-event', plausibleEvent)
397403
})
398404

src/components.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ declare module 'vue' {
1919
ColumnHeader: typeof import('./components/data/ColumnHeader.vue')['default']
2020
CommentModal: typeof import('./components/modals/CommentModal.vue')['default']
2121
ConfirmModal: typeof import('./components/modals/ConfirmModal.vue')['default']
22+
copy: typeof import('./components/modals/UpdateTrialMetadataModal copy.vue')['default']
2223
CornerPointsMap: typeof import('./components/setup/CornerPointsMap.vue')['default']
2324
DataCanvas: typeof import('./components/data/DataCanvas.vue')['default']
2425
DataEntryActions: typeof import('./components/modals/DataEntryActions.vue')['default']
2526
DataEntryModal: typeof import('./components/modals/DataEntryModal.vue')['default']
2627
DataGrid: typeof import('./components/data/DataGrid.vue')['default']
2728
DataInputCloseModal: typeof import('./components/modals/DataInputCloseModal.vue')['default']
29+
EventModal: typeof import('./components/modals/EventModal.vue')['default']
2830
FieldHubInputModal: typeof import('./components/modals/FieldHubInputModal.vue')['default']
2931
FieldLayoutHeatmap: typeof import('./components/chart/FieldLayoutHeatmap.vue')['default']
3032
GenericAddEditFormModal: typeof import('./components/modals/GenericAddEditFormModal.vue')['default']
@@ -33,10 +35,12 @@ declare module 'vue' {
3335
GpsInput: typeof import('./components/inputs/GpsInput.vue')['default']
3436
GpsTraitMap: typeof import('./components/trait/GpsTraitMap.vue')['default']
3537
GuideOrderSelector: typeof import('./components/trial/GuideOrderSelector.vue')['default']
38+
HelpCard: typeof import('./components/util/HelpCard.vue')['default']
3639
HScroll: typeof import('./components/data/HScroll.vue')['default']
3740
JumpToDropdown: typeof import('./components/util/JumpToDropdown.vue')['default']
3841
LayoutDimensions: typeof import('./components/setup/LayoutDimensions.vue')['default']
3942
LayoutMarkers: typeof import('./components/setup/LayoutMarkers.vue')['default']
43+
LineNumberTextarea: typeof import('./components/inputs/LineNumberTextarea.vue')['default']
4044
MapComponent: typeof import('./components/util/MapComponent.vue')['default']
4145
MediaModal: typeof import('./components/modals/MediaModal.vue')['default']
4246
NumberInputWithFallback: typeof import('./components/inputs/NumberInputWithFallback.vue')['default']
@@ -77,6 +81,8 @@ declare module 'vue' {
7781
TrialShareModal: typeof import('./components/modals/TrialShareModal.vue')['default']
7882
TrialSynchonizationModal: typeof import('./components/modals/TrialSynchonizationModal.vue')['default']
7983
TrialTraits: typeof import('./components/setup/TrialTraits.vue')['default']
84+
UpdateTrialDataModal: typeof import('./components/modals/UpdateTrialDataModal.vue')['default']
85+
UpdateTrialMetadataModal: typeof import('./components/modals/UpdateTrialMetadataModal.vue')['default']
8086
VScroll: typeof import('./components/data/VScroll.vue')['default']
8187
}
8288
}

src/components/inputs/GermplasmAutocomplete.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
<template #item="{ props, item }">
2424
<v-list-item
2525
v-bind="props"
26+
:title="`${item.raw.displayName} (${$t('formLabelFieldLayoutRowColumn', { row: item.raw.displayRow || 1, column: item.raw.displayColumn || 1 })})`"
2627
>
27-
<template #title><PlotInformation :cell="item.raw" /></template>
28+
<template #title v-if="performanceMode === false"><PlotInformation :cell="item.raw" /></template>
2829
</v-list-item>
2930
</template>
3031
</v-autocomplete>
@@ -36,6 +37,7 @@
3637
import { filterGermplasm } from '@/plugins/util'
3738
import { coreStore } from '@/stores/app'
3839
import { mdiMagnify } from '@mdi/js'
40+
import PlotInformation from '@/components/plot/PlotInformation.vue'
3941
4042
const store = coreStore()
4143
@@ -57,6 +59,7 @@
5759
})
5860
5961
const searchField = useTemplateRef('searchField')
62+
const performanceMode = computed(() => store.storePerformanceMode === true || trialGermplasm.value.length > 1000)
6063
6164
function getTrialGermplasm () {
6265
const data = getTrialDataCached()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<template>
2+
<div :id="id" class="d-flex flex-row">
3+
<div ref="lineNumbers" class="container__lines border-e bg-muted" />
4+
<v-textarea
5+
ref="textarea"
6+
wrap="off"
7+
:label="label"
8+
:hint="hint"
9+
persistent-hint
10+
persistent-placeholder
11+
class="pa-0 container__textarea"
12+
:placeholder="placeholder"
13+
v-model="model"
14+
:rows="15"
15+
@keydown.tab.prevent="tabber($event)"
16+
/>
17+
</div>
18+
</template>
19+
20+
<script setup lang="ts">
21+
import { getId } from '@/plugins/id'
22+
23+
const model = defineModel<string>()
24+
25+
const compProps = defineProps<{
26+
placeholder: string
27+
label: string
28+
hint: string
29+
}>()
30+
31+
const id = ref(`line-number-input-${getId()}`)
32+
33+
const styleProps = [
34+
'fontFamily',
35+
'fontSize',
36+
'fontWeight',
37+
'letterSpacing',
38+
'lineHeight',
39+
'padding',
40+
]
41+
42+
function tabber (event: Event) {
43+
const text = model.value || ''
44+
// @ts-ignore
45+
const originalSelectionStart = event.target.selectionStart
46+
const textStart = text.slice(0, originalSelectionStart)
47+
const textEnd = text.slice(originalSelectionStart)
48+
49+
model.value = `${textStart}\t${textEnd}`
50+
// @ts-ignore
51+
event.target.value = model.value // required to make the cursor stay in place.
52+
53+
nextTick(() => {
54+
// @ts-ignore
55+
event.target.selectionEnd = event.target.selectionStart = originalSelectionStart + 1
56+
})
57+
}
58+
59+
onMounted(() => {
60+
const ta = document.querySelector(`#${id.value} .container__textarea textarea`) as HTMLTextAreaElement
61+
const lineNumbersEle = document.querySelector(`#${id.value} .container__lines`) as HTMLDivElement
62+
63+
if (!ta || !lineNumbersEle) {
64+
return
65+
}
66+
67+
const textareaStyles = window.getComputedStyle(ta)
68+
styleProps.forEach((property: string) => {
69+
lineNumbersEle.style.setProperty(property, textareaStyles.getPropertyValue(property))
70+
})
71+
72+
const font = `${textareaStyles.fontSize} ${textareaStyles.fontFamily}`
73+
74+
const canvas = document.createElement('canvas')
75+
const context = canvas.getContext('2d')
76+
// @ts-ignore
77+
context.font = font
78+
79+
const calculateLineNumbers = () => {
80+
const lines = (ta.value || '').split(/\r?\n/)
81+
const numLines = lines.concat()
82+
83+
const lineNumbers = []
84+
let i = 1
85+
while (numLines.length > 0) {
86+
const numLinesOfSentence = +(numLines.shift() || '0')
87+
lineNumbers.push(i)
88+
if (numLinesOfSentence > 1) {
89+
new Array(numLinesOfSentence - 1).fill('').forEach(() => lineNumbers.push(''))
90+
}
91+
i++
92+
}
93+
94+
return lineNumbers
95+
}
96+
97+
const displayLineNumbers = () => {
98+
const lineNumbers = calculateLineNumbers()
99+
lineNumbersEle.innerHTML = Array.from({ length: lineNumbers.length }, (_, i) => `<div>${lineNumbers[i] || '&nbsp;'}</div>`).join('')
100+
}
101+
102+
ta.addEventListener('input', () => displayLineNumbers())
103+
104+
displayLineNumbers()
105+
106+
const ro = new ResizeObserver(() => {
107+
const rect = ta.getBoundingClientRect()
108+
lineNumbersEle.style.height = `${rect.height}px`
109+
displayLineNumbers()
110+
})
111+
ro.observe(ta)
112+
113+
ta.addEventListener('scroll', () => {
114+
lineNumbersEle.scrollTop = ta.scrollTop
115+
})
116+
})
117+
</script>
118+
119+
<style scoped>
120+
.container__textarea {
121+
border: none;
122+
outline: none;
123+
padding: 0.5rem;
124+
width: 100%;
125+
}
126+
.container__lines {
127+
padding: 0.5rem;
128+
text-align: right;
129+
overflow: hidden;
130+
white-space: nowrap;
131+
-webkit-user-select: none; /* Safari */
132+
-ms-user-select: none; /* IE 10 and IE 11 */
133+
user-select: none; /* Standard syntax */
134+
}
135+
</style>

src/components/inputs/SpeechRecognitionTextarea.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<v-textarea
3-
:label="$t('formLabelCommentContent')"
4-
:hint="$t('formDescriptionCommentContent')"
3+
:label="label"
4+
:hint="hint"
55
persistent-hint
66
v-model="textContent"
77
>
@@ -31,6 +31,11 @@
3131
interimResults: true,
3232
})
3333
34+
const compProps = defineProps<{
35+
label: string
36+
hint: string
37+
}>()
38+
3439
const textContent = defineModel<string>()
3540
3641
if (isSupported.value) {

src/components/inputs/TraitInput.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -292,13 +292,13 @@
292292
function handleDateInputChar (event: KeyboardEvent) {
293293
// If this could be part of a number, append to existing string
294294
// @ts-ignore
295-
if (compProps.trait.editable && (event.key === '-' || event.key === '+' || !isNaN(event.key))) {
295+
if (isEditable.value && (event.key === '-' || event.key === '+' || !isNaN(event.key))) {
296296
dateInput.value += event.key
297297
}
298298
}
299299
300300
function setDateDelta (delta: number) {
301-
if (!compProps.trait.editable) {
301+
if (!isEditable.value) {
302302
return
303303
}
304304
@@ -325,7 +325,7 @@
325325
}
326326
327327
function setDate () {
328-
if (!compProps.trait.editable) {
328+
if (!isEditable.value) {
329329
return
330330
}
331331

src/components/modals/CommentModal.vue

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
:page="page"
1212
>
1313
<template #header="{ pageCount }">
14-
<v-pagination v-model="page" :length="pageCount" />
14+
<v-pagination v-model="page" :length="pageCount" v-if="pageCount > 1" />
1515
</template>
1616

1717
<template #default="{ items }">
@@ -30,12 +30,16 @@
3030

3131
<template #footer="{ pageCount }">
3232
<template v-if="editable">
33-
<SpeechRecognitionTextarea v-model="newComment" />
33+
<SpeechRecognitionTextarea
34+
:label="$t('formLabelCommentContent')"
35+
:hint="$t('formDescriptionCommentContent')"
36+
v-model="newComment"
37+
/>
3438

35-
<v-btn color="primary" :prepend-icon="mdiCommentPlus" :text="$t('buttonCreateComment')" :disabled="!newComment || newComment.trim().length === 0" @click="addComment" />
39+
<v-btn class="mt-3" color="primary" :prepend-icon="mdiCommentPlus" :text="$t('buttonCreateComment')" :disabled="!newComment || newComment.trim().length === 0" @click="addComment" />
3640
</template>
3741

38-
<v-pagination v-model="page" :length="pageCount" />
42+
<v-pagination v-model="page" :length="pageCount" v-if="pageCount > 1" />
3943
</template>
4044
</v-data-iterator>
4145
</template>
@@ -53,6 +57,10 @@
5357
import SpeechRecognitionTextarea from '@/components/inputs/SpeechRecognitionTextarea.vue'
5458
import { mdiCalendar, mdiCommentPlus, mdiDelete } from '@mdi/js'
5559
60+
import emitter from 'tiny-emitter/instance'
61+
import { useI18n } from 'vue-i18n'
62+
63+
const { t } = useI18n()
5664
const dialog = ref(false)
5765
const perPage = ref(5)
5866
const page = ref(1)
@@ -71,11 +79,22 @@
7179
}
7280
7381
function deleteComment (comment: Comment) {
74-
if (isProxy(comment)) {
75-
comment = toRaw(comment)
76-
}
77-
78-
emit('comment-deleted', comment)
82+
emitter.emit('show-confirm', {
83+
title: t('modalTitleDeleteItem'),
84+
message: t('modalTextDeleteItem'),
85+
okTitle: t('genericYes'),
86+
cancelTitle: t('genericNo'),
87+
okVariant: 'error',
88+
callback: (result: boolean) => {
89+
if (result === true) {
90+
if (isProxy(comment)) {
91+
comment = toRaw(comment)
92+
}
93+
94+
emit('comment-deleted', comment)
95+
}
96+
},
97+
})
7998
}
8099
81100
function show () {

src/components/modals/DataEntryModal.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@
120120
v-model="cellData[trait.id || '']"
121121
v-if="cellData[trait.id || '']"
122122
:trait="trait"
123+
:editable="trial.editable || false"
123124
:measurements="cell.measurements[trait.id || '']"
124125
:people="trial.people"
125126
@traverse="(setIndex: number) => traverse(trait, traitIndex, group.traits, setIndex)"

0 commit comments

Comments
 (0)