refactor TransactionForm, remove not longer needed components

This commit is contained in:
einhornimmond 2025-07-24 15:59:09 +02:00
parent 90d3f00266
commit 3b218a8da3
12 changed files with 130 additions and 561 deletions

View File

@ -22,7 +22,7 @@
</template>
<script setup>
import { ref, computed, watch, onMounted, onUpdated } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { useRoute } from 'vue-router'
import { selectCommunities } from '@/graphql/queries'
@ -50,6 +50,9 @@ onResult(({ data }) => {
if (data) {
communities.value = data.communities
setDefaultCommunity()
if (data.communities.length === 1) {
validCommunityIdentifier.value = true
}
}
})

View File

@ -2,21 +2,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue'
// Mock external components and dependencies
vi.mock('@/components/Inputs/InputAmount', () => ({
default: {
name: 'InputAmount',
template: '<input data-testid="input-amount" />',
},
}))
vi.mock('@/components/Inputs/InputTextarea', () => ({
default: {
name: 'InputTextarea',
template: '<textarea data-testid="input-textarea"></textarea>',
},
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,

View File

@ -3,8 +3,16 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import TransactionForm from './TransactionForm'
import { nextTick, ref } from 'vue'
import { SEND_TYPES } from '@/utils/sendTypes'
import { BCard, BForm, BFormRadioGroup, BRow, BCol, BFormRadio, BButton } from 'bootstrap-vue-next'
import { useForm } from 'vee-validate'
import {
BCard,
BForm,
BFormRadioGroup,
BRow,
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
} from 'bootstrap-vue-next'
import { useRoute } from 'vue-router'
vi.mock('vue-router', () => ({
@ -35,23 +43,6 @@ vi.mock('@/composables/useToast', () => ({
})),
}))
vi.mock('vee-validate', () => {
const actualUseForm = vi.fn().mockReturnValue({
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '100,00',
memo: 'Test memo',
})
}),
resetForm: vi.fn(),
defineField: vi.fn(() => [vi.fn(), {}]),
})
return { useForm: actualUseForm }
})
describe('TransactionForm', () => {
let wrapper
@ -64,6 +55,9 @@ describe('TransactionForm', () => {
mocks: {
$t: mockT,
$n: mockN,
$i18n: {
locale: 'en',
},
},
components: {
BCard,
@ -73,12 +67,11 @@ describe('TransactionForm', () => {
BCol,
BFormRadio,
BButton,
BFormInvalidFeedback,
},
stubs: {
'community-switch': true,
'input-identifier': true,
'input-amount': true,
'input-textarea': true,
'validated-input': true,
},
},
props: {
@ -102,15 +95,15 @@ describe('TransactionForm', () => {
describe('with balance <= 0.00 GDD the form is disabled', () => {
it('has a disabled input field of type text', () => {
expect(wrapper.find('input-identifier-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#identifier').attributes('disabled')).toBe('true')
})
it('has a disabled input field for amount', () => {
expect(wrapper.find('input-amount-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#amount').attributes('disabled')).toBe('true')
})
it('has a disabled textarea field', () => {
expect(wrapper.find('input-textarea-stub').attributes('disabled')).toBe('true')
expect(wrapper.find('#memo').attributes('disabled')).toBe('true')
})
it('has a message indicating that there are no GDDs to send', () => {
@ -143,41 +136,39 @@ describe('TransactionForm', () => {
describe('identifier field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-identifier-stub').exists()).toBe(true)
expect(wrapper.find('#identifier').exists()).toBe(true)
})
it('has a label form.recipient', () => {
expect(wrapper.find('input-identifier-stub').attributes('label')).toBe('form.recipient')
expect(wrapper.find('#identifier').attributes('label')).toBe('form.recipient')
})
it('has a placeholder for identifier', () => {
expect(wrapper.find('input-identifier-stub').attributes('placeholder')).toBe(
'form.identifier',
)
expect(wrapper.find('#identifier').attributes('placeholder')).toBe('form.identifier')
})
})
describe('amount field', () => {
it('has an input field of type text', () => {
expect(wrapper.find('input-amount-stub').exists()).toBe(true)
expect(wrapper.find('#amount').exists()).toBe(true)
})
it('has a label form.amount', () => {
expect(wrapper.find('input-amount-stub').attributes('label')).toBe('form.amount')
expect(wrapper.find('#amount').attributes('label')).toBe('form.amount')
})
it('has a placeholder "0.01"', () => {
expect(wrapper.find('input-amount-stub').attributes('placeholder')).toBe('0.01')
expect(wrapper.find('#amount').attributes('placeholder')).toBe('0.01')
})
})
describe('message text box', () => {
it('has a textarea field', () => {
expect(wrapper.find('input-textarea-stub').exists()).toBe(true)
expect(wrapper.find('#memo').exists()).toBe(true)
})
it('has a label form.message', () => {
expect(wrapper.find('input-textarea-stub').attributes('label')).toBe('form.message')
expect(wrapper.find('#memo').attributes('label')).toBe('form.message')
})
})
@ -233,8 +224,10 @@ describe('TransactionForm', () => {
})
it('emits set-transaction event with correct data when form is submitted', async () => {
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.amount = '100,00'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()
expect(wrapper.emitted('set-transaction')[0][0]).toEqual(
expect.objectContaining({
@ -247,20 +240,10 @@ describe('TransactionForm', () => {
})
it('handles form submission with empty amount', async () => {
vi.mocked(useForm).mockReturnValueOnce({
...vi.mocked(useForm)(),
handleSubmit: vi.fn((callback) => {
return () =>
callback({
identifier: 'test@example.com',
amount: '',
memo: 'Test memo',
})
}),
})
wrapper = createWrapper({ balance: 100.0 })
await nextTick()
wrapper.vm.form.identifier = 'test@example.com'
wrapper.vm.form.memo = 'Test memo'
await wrapper.findComponent(BForm).trigger('submit.prevent')
expect(wrapper.emitted('set-transaction')).toBeTruthy()

View File

@ -46,20 +46,24 @@
<BRow>
<BCol class="fw-bold">
<community-switch
:disabled="isBalanceDisabled"
:disabled="isBalanceEmpty"
:model-value="targetCommunity"
@update:model-value="targetCommunity = $event"
@update:model-value="updateField($event, 'targetCommunity')"
/>
</BCol>
</BRow>
</BCol>
<BCol v-if="radioSelected === SEND_TYPES.send" cols="12">
<div v-if="!userIdentifier">
<input-identifier
<ValidatedInput
id="identifier"
:model-value="form.identifier"
name="identifier"
:label="$t('form.recipient')"
:placeholder="$t('form.identifier')"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.identifier"
:disabled="isBalanceEmpty"
@update:model-value="updateField"
/>
</div>
<div v-else class="mb-4">
@ -72,13 +76,16 @@
</div>
</BCol>
<BCol cols="12" lg="6">
<input-amount
<ValidatedInput
id="amount"
:model-value="form.amount"
name="amount"
:label="$t('form.amount')"
:placeholder="'0.01'"
:rules="{ required: true, gddSendAmount: { min: 0.01, max: balance } }"
:disabled="isBalanceDisabled"
></input-amount>
:rules="validationSchema.fields.amount"
:disabled="isBalanceEmpty"
@update:model-value="updateField"
/>
</BCol>
</BRow>
</BCol>
@ -86,16 +93,20 @@
<BRow>
<BCol>
<input-textarea
<ValidatedInput
id="memo"
:model-value="form.memo"
name="memo"
:label="$t('form.message')"
:placeholder="$t('form.message')"
:rules="{ required: true, min: 5, max: 255 }"
:disabled="isBalanceDisabled"
:rules="validationSchema.fields.memo"
textarea="true"
:disabled="isBalanceEmpty"
@update:model-value="updateField"
/>
</BCol>
</BRow>
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
<div v-if="!!isBalanceEmpty" class="text-danger mt-5">
{{ $t('form.no_gdd_available') }}
</div>
<BRow v-else class="test-buttons mt-3">
@ -111,7 +122,7 @@
</BButton>
</BCol>
<BCol cols="12" md="6" lg="6" class="text-lg-end">
<BButton block type="submit" variant="gradido">
<BButton block type="submit" variant="gradido" :disabled="formIsInvalid">
{{ $t('form.check_now') }}
</BButton>
</BCol>
@ -124,15 +135,14 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useQuery } from '@vue/apollo-composable'
import { useForm } from 'vee-validate'
import { SEND_TYPES } from '@/utils/sendTypes'
import InputIdentifier from '@/components/Inputs/InputIdentifier'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import CommunitySwitch from '@/components/CommunitySwitch.vue'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import { memo as memoSchema, identifier as identifierSchema } from '@/validationSchemas'
import { object, number } from 'yup'
import { user } from '@/graphql/queries'
import CONFIG from '@/config'
import { useAppToast } from '@/composables/useToast'
@ -149,6 +159,9 @@ const props = defineProps({
},
})
const entityDataToForm = computed(() => ({ ...props }))
const form = reactive({ ...entityDataToForm.value })
const emit = defineEmits(['set-transaction'])
const route = useRoute()
@ -157,18 +170,35 @@ const { toastError } = useAppToast()
const radioSelected = ref(props.selected)
const userName = ref('')
const recipientCommunity = ref({ uuid: '', name: '' })
const { handleSubmit, resetForm, defineField, values } = useForm({
initialValues: {
identifier: props.identifier,
amount: props.amount ? String(props.amount) : '',
memo: props.memo,
targetCommunity: props.targetCommunity,
},
const validationSchema = computed(() => {
return object({
memo: memoSchema,
identifier: !userIdentifier.value ? identifierSchema.required() : identifierSchema,
amount: number()
.required()
.transform((value, originalValue) => {
if (typeof originalValue === 'string' && originalValue !== '') {
return Number(originalValue.replace(',', '.'))
}
return value
})
.min(0.01, ({ min }) => ({ key: 'form.validation.amount.min', values: { min } }))
.max(props.balance, ({ max }) => ({ key: 'form.validation.amount.max', values: { max } }))
.test('decimal-places', 'form.validation.amount.decimal-places', (value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
}),
})
})
const [targetCommunity, targetCommunityProps] = defineField('targetCommunity')
const formIsInvalid = computed(() => !validationSchema.value.isValidSync(form))
const updateField = (newValue, name) => {
if (typeof name === 'string' && name.length) {
form[name] = newValue
}
}
const userIdentifier = computed(() => {
if (route.params.userIdentifier && route.params.communityIdentifier) {
@ -180,7 +210,7 @@ const userIdentifier = computed(() => {
return null
})
const isBalanceDisabled = computed(() => props.balance <= 0)
const isBalanceEmpty = computed(() => props.balance <= 0)
const { result: userResult, error: userError } = useQuery(
user,
@ -193,6 +223,7 @@ watch(
(user) => {
if (user) {
userName.value = `${user.firstName} ${user.lastName}`
form.identifier = userIdentifier.value.identifier
}
},
{ immediate: true },
@ -204,19 +235,21 @@ watch(userError, (error) => {
}
})
const onSubmit = handleSubmit((formValues) => {
if (userIdentifier.value) formValues.identifier = userIdentifier.value.identifier
function onSubmit() {
const transformedForm = validationSchema.value.cast(form)
emit('set-transaction', {
selected: radioSelected.value,
...formValues,
amount: Number(formValues.amount.replace(',', '.')),
...transformedForm,
userName: userName.value,
})
})
}
function onReset(event) {
event.preventDefault()
resetForm()
form.amount = props.amount
form.memo = props.memo
form.identifier = props.identifier
form.targetCommunity = props.targetCommunity
radioSelected.value = SEND_TYPES.send
router.replace('/send')
}

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputAmount from './InputAmount'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ref } from 'vue'
import { BFormInput } from 'bootstrap-vue-next'
vi.mock('vue-router', () => ({
useRoute: vi.fn(() => ({
params: {},
path: '/some-path',
})),
}))
vi.mock('vue-i18n', () => ({
useI18n: vi.fn(() => ({
t: (key) => key,
n: (num) => num,
})),
}))
vi.mock('vee-validate', () => ({
useField: vi.fn(() => ({
value: ref(''),
meta: { valid: true },
errorMessage: ref(''),
})),
}))
// Mock toast
const mockToastError = vi.fn()
vi.mock('@/composables/useToast', () => ({
useAppToast: vi.fn(() => ({
toastError: mockToastError,
})),
}))
describe('InputAmount', () => {
let wrapper
const createWrapper = (propsData = {}) => {
return mount(InputAmount, {
props: {
name: 'amount',
label: 'Amount',
placeholder: 'Enter amount',
typ: 'TransactionForm',
modelValue: '12,34',
...propsData,
},
global: {
mocks: {
$route: useRoute(),
...useI18n(),
},
components: {
BFormInput,
},
directives: {
focus: {},
},
stubs: {
BFormGroup: true,
BFormInvalidFeedback: true,
BInputGroup: true,
},
},
})
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('mount in a TransactionForm', () => {
beforeEach(() => {
wrapper = createWrapper()
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12,34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
describe('mount in a ContributionForm', () => {
beforeEach(() => {
wrapper = createWrapper({
typ: 'ContributionForm',
modelValue: '12.34',
})
})
it('renders the component input-amount', () => {
expect(wrapper.find('div.input-amount').exists()).toBe(true)
})
it('normalizes the amount correctly', async () => {
await wrapper.vm.normalizeAmount('12.34')
expect(wrapper.vm.value).toBe('12.34')
})
it('does not normalize invalid input', async () => {
await wrapper.vm.normalizeAmount('12m34')
expect(wrapper.vm.value).toBe('12m34')
})
})
it('emits update:modelValue when value changes', async () => {
wrapper = createWrapper()
await wrapper.vm.normalizeAmount('15.67')
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['15.67'])
})
})

View File

@ -1,88 +0,0 @@
<template>
<div class="input-amount">
<template v-if="typ === 'TransactionForm'">
<BFormGroup :label="label" :label-for="labelFor" data-test="input-amount">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:class="$route.path === '/send' ? 'bg-248' : ''"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
:disabled="disabled"
autocomplete="off"
@update:model-value="normalizeAmount($event)"
@focus="amountFocused = true"
@blur="normalizeAmount($event)"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<BInputGroup v-else append="GDD" :label="label" :label-for="labelFor">
<BFormInput
:id="labelFor"
v-focus="amountFocused"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
readonly
trim
/>
</BInputGroup>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useField } from 'vee-validate'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
typ: { type: String, default: 'TransactionForm' },
name: { type: String, required: true, default: 'Amount' },
label: { type: String, required: true, default: 'Amount' },
placeholder: { type: String, required: true, default: 'Amount' },
balance: { type: Number, default: 0.0 },
disabled: { required: false, type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const { n } = useI18n()
const { value, meta, errorMessage } = useField(props.name, props.rules)
const amountFocused = ref(false)
const amountValue = ref(0.0)
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) value.value = newValue
},
)
const normalizeAmount = (inputValue) => {
amountFocused.value = false
if (typeof inputValue === 'string' && inputValue.length > 1) {
value.value = inputValue.replace(',', '.')
}
}
</script>

View File

@ -1,61 +0,0 @@
<template>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-identifier">
<BFormInput
:id="labelFor"
:model-value="value"
:name="name"
:placeholder="placeholder"
type="text"
:state="meta.valid"
trim
class="bg-248"
:disabled="disabled"
autocomplete="off"
@update:model-value="value = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useField } from 'vee-validate'
const props = defineProps({
rules: {
type: Object,
default: () => ({
required: true,
validIdentifier: true,
}),
},
name: { type: String, required: true },
label: { type: String, required: true },
placeholder: { type: String, required: true },
modelValue: { type: String },
disabled: { type: Boolean, required: false, default: false },
})
const emit = defineEmits(['update:modelValue', 'onValidation'])
const { value, meta, errorMessage } = useField(props.name, props.rules, {
initialValue: props.modelValue,
})
const labelFor = computed(() => props.name + '-input-field')
watch(value, (newValue) => {
emit('update:modelValue', newValue)
})
watch(
() => props.modelValue,
(newValue) => {
if (newValue !== value.value) {
value.value = newValue
emit('onValidation')
}
},
)
</script>

View File

@ -1,125 +0,0 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import InputTextarea from './InputTextarea'
import { useField } from 'vee-validate'
import { BFormGroup, BFormInvalidFeedback, BFormTextarea } from 'bootstrap-vue-next'
vi.mock('vee-validate', () => ({
useField: vi.fn(),
}))
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
}),
}))
describe('InputTextarea', () => {
let wrapper
const createWrapper = (props = {}) => {
return mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
...props,
},
global: {
components: {
BFormGroup,
BFormTextarea,
BFormInvalidFeedback,
},
},
})
}
beforeEach(() => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: '',
meta: { valid: true },
})
wrapper = createWrapper()
})
it('renders the component InputTextarea', () => {
expect(wrapper.find('[data-test="input-textarea"]').exists()).toBe(true)
})
it('has a textarea field', () => {
expect(wrapper.findComponent({ name: 'BFormTextarea' }).exists()).toBe(true)
})
describe('properties', () => {
it('has the correct id', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('id')).toBe('input-field-name-input-field')
})
it('has the correct placeholder', () => {
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('placeholder')).toBe('input-field-placeholder')
})
it('has the correct label', () => {
const label = wrapper.find('label')
expect(label.text()).toBe('input-field-label')
})
it('has the correct label-for attribute', () => {
const label = wrapper.find('label')
expect(label.attributes('for')).toBe('input-field-name-input-field')
})
})
describe('input value changes', () => {
it('updates the model value when input changes', async () => {
const wrapper = mount(InputTextarea, {
props: {
rules: {},
name: 'input-field-name',
label: 'input-field-label',
placeholder: 'input-field-placeholder',
},
global: {
components: {
BFormGroup,
BFormInvalidFeedback,
BFormTextarea,
},
},
})
const textarea = wrapper.find('textarea')
await textarea.setValue('New Text')
expect(wrapper.vm.currentValue).toBe('New Text')
})
})
describe('disabled state', () => {
it('disables the textarea when disabled prop is true', async () => {
await wrapper.setProps({ disabled: true })
const textarea = wrapper.findComponent({ name: 'BFormTextarea' })
expect(textarea.attributes('disabled')).toBeDefined()
})
})
it('shows error message when there is an error', async () => {
vi.mocked(useField).mockReturnValue({
value: '',
errorMessage: 'This field is required',
meta: { valid: false },
})
wrapper = createWrapper()
await wrapper.vm.$nextTick()
const errorFeedback = wrapper.findComponent({ name: 'BFormInvalidFeedback' })
expect(errorFeedback.exists()).toBe(true)
expect(errorFeedback.text()).toBe('This field is required')
})
})

View File

@ -1,64 +0,0 @@
<template>
<div>
<BFormGroup :label="label" :label-for="labelFor" data-test="input-textarea">
<BFormTextarea
:id="labelFor"
:model-value="currentValue"
class="bg-248"
:name="name"
:placeholder="placeholder"
:state="meta.valid"
trim
:rows="4"
:max-rows="4"
:disabled="disabled"
no-resize
@update:modelValue="currentValue = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ translatedErrorString }}
</BFormInvalidFeedback>
</BFormGroup>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useField } from 'vee-validate'
import { useI18n } from 'vue-i18n'
import { translateYupErrorString } from '@/validationSchemas'
const props = defineProps({
rules: {
type: Object,
default: () => ({}),
},
name: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
})
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
const { t } = useI18n()
const translatedErrorString = computed(() => translateYupErrorString(errorMessage, t))
const labelFor = computed(() => `${props.name}-input-field`)
</script>
<style lang="scss" scoped>
:deep(.form-control) {
height: unset;
}
</style>

View File

@ -203,6 +203,11 @@
"username": "Benutzername",
"username-placeholder": "Wähle deinen Benutzernamen",
"validation": {
"amount": {
"min": "Der Betrag sollte mindestens {min} groß sein",
"max": "Der Betrag sollte höchstens {max} groß sein",
"decimal-places": "Der Betrag sollte maximal zwei Nachkommastellen enthalten"
},
"gddCreationTime": {
"min": "Die Stunden sollten mindestens {min} groß sein",
"max": "Die Stunden sollten höchstens {max} groß sein",

View File

@ -203,6 +203,11 @@
"username": "Username",
"username-placeholder": "Choose your username",
"validation": {
"amount": {
"min": "The amount should be at least {min} in size",
"max": "The amount should not be larger than {max}",
"decimal-places": "The amount should contain a maximum of two decimal places"
},
"gddCreationTime": {
"min": "The hours should be at least {min} in size",
"max": "The hours should not be larger than {max}",

View File

@ -1,4 +1,10 @@
import { string } from 'yup'
import { validate as validateUuid, version as versionUuid } from 'uuid'
// Email and username regex patterns remain the same
const EMAIL_REGEX =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
const USERNAME_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
export const isLanguageKey = (str) =>
@ -19,3 +25,15 @@ 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 identifier = string().test(
'valid-identifier',
'form.validation.valid-identifier',
(value) => {
const isEmail = !!EMAIL_REGEX.test(value)
const isUsername = !!value.match(USERNAME_REGEX)
// TODO: use valibot and rules from shared
const isGradidoId = validateUuid(value) && versionUuid(value) === 4
return isEmail || isUsername || isGradidoId
},
)