mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
refactor TransactionForm, remove not longer needed components
This commit is contained in:
parent
90d3f00266
commit
3b218a8da3
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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')
|
||||
}
|
||||
|
||||
@ -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'])
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
@ -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",
|
||||
|
||||
@ -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}",
|
||||
|
||||
@ -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
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user