refactor: refs #8413 vnInputDate component and improve date formatting logic
gitea/salix-front/pipeline/pr-dev This commit is unstable Details

This commit is contained in:
Jose Antonio Tubau 2025-04-11 08:15:03 +02:00
parent 4e0450bf36
commit 6dc9f3ca07
2 changed files with 139 additions and 76 deletions

View File

@ -1,5 +1,5 @@
<script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { nextTick, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
@ -24,57 +24,12 @@ const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const formattedDate = computed({
get() {
if (!model.value) return model.value;
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
let newDate;
if (value) {
// parse input
if (value.includes('/') && value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds(),
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() =>
model.value ? date.formatDate(new Date(model.value), 'YYYY/MM/DD') : model.value,
);
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
});
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
);
const styleAttrs = computed(() => {
return $props.isOutlined
@ -86,19 +41,104 @@ const styleAttrs = computed(() => {
: {};
});
const inputValue = ref('');
const validateAndCleanInput = (value) => {
inputValue.value = value.replace(/[^0-9./-]/g, '');
};
const manageDate = (date) => {
formattedDate.value = date;
inputValue.value = date.split('/').reverse().join('/');
isPopupOpen.value = false;
};
watch(
() => model.value,
(nVal) => {
if (nVal) inputValue.value = date.formatDate(new Date(model.value), dateFormat);
},
{ immediate: true },
);
const formatDate = () => {
let value = inputValue.value;
if (value === model.value) return;
const regex =
/^([0]?[1-9]|[12][0-9]|3[01])([./-])([0]?[1-9]|1[0-2])([./-](\d{1,4}))?$/;
if (!regex.test(value)) return;
value = value.replace(/[.-]/g, '/');
const parts = value.split('/');
if (parts.length < 2) return;
let [day, month, year] = parts;
if (day.length === 1) day = '0' + day;
if (month.length === 1) month = '0' + month;
const currentYear = Date.vnNew().getFullYear();
if (!year) year = currentYear;
const millennium = currentYear.toString().slice(0, 1);
switch (year?.length) {
case 1:
year = `${millennium}00${year}`;
break;
case 2:
year = `${millennium}0${year}`;
break;
case 3:
year = `${millennium}${year}`;
break;
case 4:
break;
}
const isoCandidate = `${year}-${month}-${day}`;
const parsedDate = new Date(isoCandidate);
const isValidDate = // use date.isValid
parsedDate instanceof Date &&
!isNaN(parsedDate) &&
parsedDate.getFullYear() === parseInt(year) &&
parsedDate.getMonth() === parseInt(month) - 1 &&
parsedDate.getDate() === parseInt(day);
if (!isValidDate) return;
if (model.value) {
const original =
model.value instanceof Date ? model.value : new Date(model.value);
parsedDate.setHours(
original.getHours(),
original.getMinutes(),
original.getSeconds(),
original.getMilliseconds(),
);
}
model.value = parsedDate.toISOString();
};
const handleEnter = (event) => {
formatDate();
nextTick(() => {
const newEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
bubbles: true,
cancelable: true,
});
vnInputDateRef.value?.$el?.dispatchEvent(newEvent);
});
};
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="formattedDate"
v-model="inputValue"
class="vn-input-date"
:mask="mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }"
@ -106,8 +146,11 @@ const manageDate = (date) => {
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
@blur="formatDate"
@keydown.enter.prevent="handleEnter"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
@update:model-value="validateAndCleanInput"
>
<template #append>
<QIcon
@ -116,11 +159,12 @@ const manageDate = (date) => {
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
inputValue &&
!$attrs.disable
"
@click="
vnInputDateRef.focus();
inputValue = null;
model = null;
isPopupOpen = false;
"

View File

@ -5,52 +5,71 @@ import VnInputDate from 'components/common/VnInputDate.vue';
let vm;
let wrapper;
function generateWrapper(date, outlined, required) {
function generateWrapper(outlined = false, required = false) {
wrapper = createWrapper(VnInputDate, {
props: {
modelValue: date,
modelValue: '2000-12-31T23:00:00.000Z',
'onUpdate:modelValue': (e) => wrapper.setProps({ modelValue: e }),
},
attrs: {
isOutlined: outlined,
required: required
required: required,
},
});
wrapper = wrapper.wrapper;
vm = wrapper.vm;
};
}
describe('VnInputDate', () => {
describe('formattedDate', () => {
it('formats a valid date correctly', async () => {
generateWrapper('2023-12-25', false, false);
describe('formattedDate', () => {
it('validateAndCleanInput should remove non-numeric characters', async () => {
generateWrapper();
vm.validateAndCleanInput('10a/1s2/2dd0a23');
await vm.$nextTick();
expect(vm.formattedDate).toBe('25/12/2023');
expect(vm.inputValue).toBe('10/12/2023');
});
it('updates the model value when a new date is set', async () => {
const input = wrapper.find('input');
await input.setValue('31/12/2023');
expect(wrapper.emitted()['update:modelValue']).toBeTruthy();
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
it('manageDate should reverse the date', async () => {
generateWrapper();
vm.manageDate('10/12/2023');
await vm.$nextTick();
expect(vm.inputValue).toBe('2023/12/10');
});
it('should not update the model value when an invalid date is set', async () => {
it('formatDate should format the date correctly when a valid date is entered with full year', async () => {
const input = wrapper.find('input');
await input.setValue('invalid-date');
expect(wrapper.emitted()['update:modelValue'][0][0]).toBe('2023-12-31T00:00:00.000Z');
});
await input.setValue('25/12/2002');
await vm.$nextTick();
await vm.formatDate();
expect(vm.model).toBe('2002-12-24T23:00:00.000Z');
});
it('should format the date correctly when a valid date is entered with short year', async () => {
const input = wrapper.find('input');
await input.setValue('31/12/23');
await vm.$nextTick();
await vm.formatDate();
expect(vm.model).toBe('2023-12-30T23:00:00.000Z');
});
it('should format the date correctly when a valid date is entered without year', async () => {
const input = wrapper.find('input');
await input.setValue('12/03');
await vm.$nextTick();
await vm.formatDate();
expect(vm.model).toBe('2001-03-11T23:00:00.000Z');
});
});
describe('styleAttrs', () => {
it('should return empty styleAttrs when isOutlined is false', async () => {
generateWrapper('2023-12-25', false, false);
generateWrapper();
await vm.$nextTick();
expect(vm.styleAttrs).toEqual({});
expect(vm.styleAttrs).toEqual({});
});
it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper('2023-12-25', true, false);
it('should set styleAttrs when isOutlined is true', async () => {
generateWrapper(true, false);
await vm.$nextTick();
expect(vm.styleAttrs.outlined).toBe(true);
});
@ -58,15 +77,15 @@ describe('VnInputDate', () => {
describe('required', () => {
it('should not applies required class when isRequired is false', async () => {
generateWrapper('2023-12-25', false, false);
generateWrapper();
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).not.toContain('required');
});
it('should applies required class when isRequired is true', async () => {
generateWrapper('2023-12-25', false, true);
generateWrapper(false, true);
await vm.$nextTick();
expect(wrapper.find('.vn-input-date').classes()).toContain('required');
});
});
});
});