mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
try modularizing validated form input(s)
This commit is contained in:
parent
851db7e8c5
commit
6bbb97048e
@ -9,12 +9,11 @@
|
||||
id="contribution-date"
|
||||
:model-value="formValues.date"
|
||||
name="date"
|
||||
:state="dataFieldMeta.valid"
|
||||
:label="$t('contribution.selectDate')"
|
||||
:no-flip="true"
|
||||
class="mb-4 bg-248"
|
||||
type="date"
|
||||
:schema-description="schemaDescription.fields.date"
|
||||
:rules="validationSchema"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
|
||||
@ -23,33 +22,33 @@
|
||||
<div v-else>
|
||||
<input-textarea
|
||||
id="contribution-memo"
|
||||
:model-value="formValues.memo"
|
||||
name="memo"
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
:rules="validationSchema"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<input-hour
|
||||
<ValidatedInput
|
||||
name="hours"
|
||||
:model-value="formValues.hours"
|
||||
:label="$t('form.hours')"
|
||||
placeholder="0.01"
|
||||
:rules="{
|
||||
required: true,
|
||||
// decimal: 2,
|
||||
min: 0.01,
|
||||
max: validMaxTime,
|
||||
}"
|
||||
:valid-max-time="validMaxTime"
|
||||
step="0.01"
|
||||
type="number"
|
||||
:rules="validationSchema"
|
||||
@update:model-value="updateField"
|
||||
/>
|
||||
<input-amount
|
||||
<LabeledInput
|
||||
id="contribution-amount"
|
||||
class="mt-3"
|
||||
name="amount"
|
||||
:label="$t('form.amount')"
|
||||
placeholder="20"
|
||||
:rules="{ required: true, gddSendAmount: { min: 20, max: validMaxGDD } }"
|
||||
typ="ContributionForm"
|
||||
readonly
|
||||
type="text"
|
||||
trim
|
||||
/>
|
||||
|
||||
<BRow class="mt-5">
|
||||
<BCol>
|
||||
<BButton
|
||||
@ -80,14 +79,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import InputHour from '@/components/Inputs/InputHour'
|
||||
import InputAmount from '@/components/Inputs/InputAmount'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea'
|
||||
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
|
||||
import { createContributionFormValidation } from '@/validationSchemas'
|
||||
import { useForm, useField } from 'vee-validate'
|
||||
import ValidatedInput from '@/components/Inputs/ValidatedInput'
|
||||
import LabeledInput from '@/components/Inputs/LabeledInput'
|
||||
import { memo } from '@/validationSchemas'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { object, date, number } from 'yup'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
@ -103,10 +102,34 @@ const { t } = useI18n()
|
||||
|
||||
const form = ref({ ...props.modelValue })
|
||||
|
||||
const validationSchema = createContributionFormValidation(t)
|
||||
const schemaDescription = validationSchema.describe()
|
||||
// console.log(schemaDescription)
|
||||
const maxHours = computed(() => Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth / 20))
|
||||
|
||||
const validationSchema = computed(() => {
|
||||
// const maxGDD = Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth)
|
||||
// const maxHours = Number(maxGDD / 20)
|
||||
console.log('compute validationSchema', { maxHours })
|
||||
return object({
|
||||
// The date field is required and needs to be a valid date
|
||||
// contribution date
|
||||
date: date()
|
||||
.required('contribution.noDateSelected')
|
||||
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
|
||||
.max(new Date().toISOString().slice(0, 10))
|
||||
.default(''), // date cannot be in the future
|
||||
memo,
|
||||
hours: number().required('contribution.noHours')
|
||||
.min(0.01, ({min}) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
|
||||
.max(maxHours.value, ({max}) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
|
||||
.test(
|
||||
'decimal-places',
|
||||
'form.validation.gddCreationTime.decimal-places',
|
||||
(value) => {
|
||||
if (value === undefined || value === null) return true
|
||||
return /^\d+(\.\d{0,2})?$/.test(value.toString())
|
||||
}
|
||||
),
|
||||
})
|
||||
})
|
||||
const {
|
||||
values: formValues,
|
||||
meta: formMeta,
|
||||
@ -119,15 +142,16 @@ const {
|
||||
hours: props.modelValue.hours,
|
||||
amount: props.modelValue.amount,
|
||||
},
|
||||
validationSchema,
|
||||
validationSchema: validationSchema.value,
|
||||
})
|
||||
|
||||
const { meta: dataFieldMeta } = useField('date')
|
||||
|
||||
const updateField = (newValue, name) => {
|
||||
if (typeof name === 'string' && name.length) {
|
||||
setFieldValue(name, newValue)
|
||||
}
|
||||
if (name === 'hours') {
|
||||
setFieldValue('amount', (newValue * 20).toFixed(2).toString())
|
||||
}
|
||||
}
|
||||
|
||||
const showMessage = computed(() => {
|
||||
@ -148,14 +172,6 @@ const disabled = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const validMaxGDD = computed(() => {
|
||||
return Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth)
|
||||
})
|
||||
|
||||
const validMaxTime = computed(() => {
|
||||
return Number(validMaxGDD.value / 20)
|
||||
})
|
||||
|
||||
const noOpenCreation = computed(() => {
|
||||
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) {
|
||||
return t('contribution.noOpenCreation.allMonth')
|
||||
@ -169,17 +185,6 @@ const noOpenCreation = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
watch(
|
||||
() => formValues.hours,
|
||||
() => {
|
||||
updateAmount(formValues.hours)
|
||||
},
|
||||
)
|
||||
|
||||
function updateAmount(hours) {
|
||||
setFieldValue('amount', (hours * 20).toFixed(2).toString())
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const dataToSave = { ...formValues }
|
||||
let emitOption = 'set-contribution'
|
||||
@ -194,12 +199,11 @@ function submit() {
|
||||
function fullFormReset() {
|
||||
resetForm({
|
||||
values: {
|
||||
id: null,
|
||||
date: '',
|
||||
memo: '',
|
||||
hours: 0,
|
||||
hours: 0.0,
|
||||
amount: '',
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
@update:modelValue="currentValue = $event"
|
||||
/>
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ errorMessage }}
|
||||
{{ translatedErrorString }}
|
||||
</BFormInvalidFeedback>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
@ -25,6 +25,8 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useField } from 'vee-validate'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isLanguageKey } from '@/validationSchemas'
|
||||
|
||||
const props = defineProps({
|
||||
rules: {
|
||||
@ -50,7 +52,17 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
|
||||
|
||||
const { t } = useI18n()
|
||||
const translatedErrorString = computed(() => {
|
||||
console.log(errorMessage)
|
||||
if (typeof errorMessage.value === 'object') {
|
||||
return t(errorMessage.value.key, errorMessage.value.values)
|
||||
} else if (isLanguageKey(errorMessage.value)) {
|
||||
return t(errorMessage.value)
|
||||
} else {
|
||||
return errorMessage
|
||||
}
|
||||
})
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
|
||||
|
||||
32
frontend/src/components/Inputs/LabeledInput.vue
Normal file
32
frontend/src/components/Inputs/LabeledInput.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div :class="wrapperClassName">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormInput
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<slot></slot>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineOptions } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const wrapperClassName = computed(() => `input-${props.name}`)
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
</script>
|
||||
@ -1,58 +1,73 @@
|
||||
<template>
|
||||
<div :class="wrapperClassName">
|
||||
<BFormGroup :label="label" :label-for="labelFor">
|
||||
<BFormInput
|
||||
v-bind="$attrs"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:model-value="modelValue"
|
||||
:reset-value="props.schemaDescription.default"
|
||||
:locale="$i18n.locale"
|
||||
:required="!props.schemaDescription.optional"
|
||||
@input="updateValue($event.target.value)"
|
||||
/>
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ props.errorMessage }}
|
||||
</BFormInvalidFeedback>
|
||||
</BFormGroup>
|
||||
</div>
|
||||
<LabeledInput
|
||||
v-bind="$attrs"
|
||||
:min="minValue"
|
||||
:max="maxValue"
|
||||
:model-value="currentValue"
|
||||
:reset-value="resetValue"
|
||||
:locale="$i18n.locale"
|
||||
:required="!isOptional"
|
||||
:label="props.label"
|
||||
:name="props.name"
|
||||
:state="meta.valid"
|
||||
@input="updateValue($event.target.value)">
|
||||
<BFormInvalidFeedback v-if="errorMessage">
|
||||
{{ $t(translatedErrorString) }}
|
||||
</BFormInvalidFeedback>
|
||||
</LabeledInput>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineOptions } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
import { computed } from 'vue'
|
||||
import LabeledInput from './LabeledInput'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isLanguageKey } from '@/validationSchemas'
|
||||
import { useField } from 'vee-validate'
|
||||
|
||||
const props = defineProps({
|
||||
errorMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
errorMessage: [String, Object],
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
modelValue: [String, Number],
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: [String, Number],
|
||||
schemaDescription: {
|
||||
rules: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits('update:modelValue')
|
||||
const { t } = useI18n()
|
||||
const translatedErrorString = computed(() => {
|
||||
const error = errorMessage.value
|
||||
const type = typeof error
|
||||
console.log(error)
|
||||
if (type === 'object') {
|
||||
return t(error.key, error.values)
|
||||
} else if (type === 'string' && error.length > 0 && isLanguageKey(error)) {
|
||||
return t(error)
|
||||
} else {
|
||||
return error
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const updateValue = (newValue) => emit('update:modelValue', newValue, props.name)
|
||||
|
||||
// extract additional parameter like min and max from schema
|
||||
const getDateOnly = (rules, name) => rules.find((test) => test.name === name)?.params[name]
|
||||
const getDateOnly = (schemaDescription, name) => schemaDescription.fields[props.name].tests.find((test) => test.name === name)?.params[name]
|
||||
|
||||
const minValue = computed(() => getDateOnly(props.schemaDescription.tests, 'min'))
|
||||
const maxValue = computed(() => getDateOnly(props.schemaDescription.tests, 'max'))
|
||||
const wrapperClassName = computed(() => `input-${props.name}`)
|
||||
const labelFor = computed(() => `${props.name}-input-field`)
|
||||
const schemaDescription = computed(() => props.rules.describe())
|
||||
console.log(schemaDescription.value)
|
||||
const minValue = computed(() => getDateOnly(schemaDescription.value, 'min'))
|
||||
const maxValue = computed(() => getDateOnly(schemaDescription.value, 'max'))
|
||||
const resetValue = computed(() => schemaDescription.value.fields[props.name].default)
|
||||
const isOptional = computed(() => schemaDescription.value.fields[props.name].optional)
|
||||
|
||||
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
|
||||
console.log('max value: ', maxValue)
|
||||
</script>
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
"myContributions": "Du hast noch keine Beiträge eingereicht."
|
||||
},
|
||||
"noDateSelected": "Wähle irgendein Datum im Monat",
|
||||
"noHours": "Bitte trage deine Stunden ein",
|
||||
"noOpenCreation": {
|
||||
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
|
||||
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
|
||||
@ -185,9 +186,17 @@
|
||||
"username": "Benutzername",
|
||||
"username-placeholder": "Wähle deinen Benutzernamen",
|
||||
"validation": {
|
||||
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
|
||||
"gddCreationTime": {
|
||||
"min": "Die Stunden sollten mindestens {min} groß sein",
|
||||
"max": "Die Stunden sollten höchsten {max} groß sein",
|
||||
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten"
|
||||
},
|
||||
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
|
||||
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
|
||||
"memo": {
|
||||
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein",
|
||||
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein"
|
||||
},
|
||||
"requiredField": "{fieldName} ist ein Pflichtfeld",
|
||||
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
|
||||
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
"myContributions": "You have not submitted any entries yet."
|
||||
},
|
||||
"noDateSelected": "Choose any date in the month",
|
||||
"noHours": "Please enter your hours",
|
||||
"noOpenCreation": {
|
||||
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
|
||||
"lastMonth": "The creation limit is reached for the selected month.",
|
||||
@ -188,6 +189,10 @@
|
||||
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
|
||||
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
|
||||
"is-not": "You cannot send Gradidos to yourself",
|
||||
"memo": {
|
||||
"min": "The job description should be at least {min} characters long",
|
||||
"max": "The job description should not be longer than {max} characters"
|
||||
},
|
||||
"requiredField": "The {fieldName} field is required",
|
||||
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
|
||||
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { object, string, date } from 'yup'
|
||||
import { string } from 'yup'
|
||||
|
||||
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
|
||||
export const isLanguageKey = (str) => str.match(/^(?!\.)[a-z][a-zA-Z0-9]*([.][a-z][a-zA-Z0-9]*)*(?<!\.)$/)
|
||||
|
||||
export const memo = string()
|
||||
.required('contribution.yourActivity')
|
||||
.min(5, ({min}) => ({ key: 'form.validation.memo.min', values: { min } }))
|
||||
.max(255, ({max}) => ({ key: 'form.validation.memo.max', values: { max } }))
|
||||
|
||||
export const createContributionFormValidation = (t) => {
|
||||
return object({
|
||||
// The date field is required and needs to be a valid date
|
||||
// contribution date
|
||||
date: date()
|
||||
.required(t('contribution.noDateSelected'))
|
||||
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
|
||||
.max(new Date().toISOString().slice(0, 10))
|
||||
.default(''), // date cannot be in the future
|
||||
memo: string().required(t('')).min(5).max(255),
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user