Improvements

This commit is contained in:
William Buezas 2025-03-05 15:37:35 -03:00
parent bd1e9b7037
commit 97ec2fabc5
11 changed files with 179 additions and 41 deletions

View File

@ -59,7 +59,7 @@
"salix": "cd ../salix && gulp back",
"db": "cd ../salix && gulp docker",
"cy:open": "npm run db && cypress open",
"test:e2e": "npm run db && cypress run",
"test:e2e": "npm run db && cypress run --headed --config video=false",
"test:unit": "vitest",
"build": "rm -rf dist/ ; quasar build",
"clean": "rm -rf dist/",

View File

@ -81,7 +81,7 @@ const { isHeaderMounted } = storeToRefs(appStore);
const isLoading = ref(false);
const formData = ref({});
const addressFormRef = ref(null);
const formModelRef = ref(null);
const hasChanges = ref(!props.observeFormChanges);
const isResetting = ref(false);
const originalData = ref(null);
@ -119,6 +119,7 @@ onMounted(async () => {
);
}
});
async function fetch() {
try {
let { data } = await api.get(props.url, {
@ -133,9 +134,12 @@ async function fetch() {
}
async function submit() {
console.log('submit: ');
if (props.observeFormChanges && !hasChanges.value)
return notify('globals.noChanges', 'negative');
return notify('noChanges', 'negative');
const isValid = await formModelRef.value.validate();
console.log('isValid', isValid);
if (!isValid) return;
isLoading.value = true;
try {
@ -167,7 +171,7 @@ defineExpose({
<QCard class="form-container" v-bind="$attrs">
<QForm
v-if="!isLoading"
ref="addressFormRef"
ref="formModelRef"
class="form"
:class="separationBetweenInputs"
>

View File

@ -1,15 +1,20 @@
<script setup>
import { computed, ref } from 'vue';
import { computed, ref, useAttrs, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const { t } = useI18n();
const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove'
'remove',
'blur'
]);
const props = defineProps({
const $props = defineProps({
modelValue: {
type: [String, Number],
default: null
@ -25,24 +30,43 @@ const props = defineProps({
clearable: {
type: Boolean,
default: true
},
emptyToNull: {
type: Boolean,
default: true
},
insertable: {
type: Boolean,
default: false
},
maxlength: {
type: Number,
default: null
},
uppercase: {
type: Boolean,
default: false
}
});
const { t } = useI18n();
const requiredFieldRule = val => !!val || t('globals.fieldRequired');
const vnInputRef = ref(null);
const value = computed({
get() {
return props.modelValue;
return $props.modelValue;
},
set(value) {
if ($props.emptyToNull && value === '') value = null;
emit('update:modelValue', value);
}
});
const hover = ref(false);
const styleAttrs = computed(() => {
return props.isOutlined
? { dense: true, outlined: true, rounded: true }
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true
}
: {};
});
@ -51,66 +75,119 @@ const focus = () => {
};
defineExpose({
focus
focus,
vnInputRef
});
const inputRules = [
const mixinRules = [
requiredFieldRule,
...($attrs.rules ?? []),
val => {
const { maxlength } = vnInputRef.value;
if (maxlength && +val.length > maxlength)
return t(`maxLength`, { value: maxlength });
const { min, max } = vnInputRef.value.$attrs;
if (min >= 0) {
if (!min) return null;
if (min >= 0)
if (Math.floor(val) < min) return t('inputMin', { value: min });
}
if (!max) return null;
if (max > 0) {
if (Math.floor(val) > max) return t('inputMax', { value: max });
}
}
];
const handleKeydown = e => {
if (e.key === 'Backspace') return;
if ($props.insertable && e.key.match(/[0-9]/)) {
handleInsertMode(e);
}
};
const handleInsertMode = e => {
e.preventDefault();
const input = e.target;
const cursorPos = input.selectionStart;
const { maxlength } = vnInputRef.value;
let currentValue = value.value;
if (!currentValue) currentValue = e.key;
const newValue = e.key;
if (newValue && !isNaN(newValue) && cursorPos < maxlength) {
value.value =
currentValue.substring(0, cursorPos) +
newValue +
currentValue.substring(cursorPos + 1);
}
nextTick(() => {
input.setSelectionRange(cursorPos + 1, cursorPos + 1);
});
};
const handleUppercase = () => {
value.value = value.value?.toUpperCase() || '';
};
</script>
<template>
<div
:rules="$attrs.required ? [requiredFieldRule] : null"
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputRef"
v-model="value"
v-bind="{ ...$attrs, ...styleAttrs }"
:type="$attrs.type"
:class="{ required: $attrs.required }"
:class="{ required: isRequired }"
@keyup.enter="emit('keyup.enter')"
@blur="emit('blur')"
@keydown="handleKeydown"
:clearable="false"
:rules="inputRules"
:rules="mixinRules"
:lazy-rules="true"
hide-bottom-space
@keyup.enter="emit('keyup.enter')"
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
>
<template
v-if="$slots.prepend"
#prepend
>
<template #prepend v-if="$slots.prepend">
<slot name="prepend" />
</template>
<template #append>
<slot
v-if="$slots.append && !$attrs.disabled"
name="append"
/>
<QIcon
v-if="hover && value && !$attrs.disabled && props.clearable"
name="close"
size="xs"
:style="{
visibility:
hover &&
value &&
!$attrs.disabled &&
!$attrs.readonly &&
$props.clearable
? 'visible'
: 'hidden'
}"
@click="
() => {
value = null;
vnInputRef.focus();
emit('remove');
}
"
/>
></QIcon>
<QIcon
v-if="info"
name="info"
name="match_case"
size="xs"
v-if="
!$attrs.disabled && !$attrs.readonly && $props.uppercase
"
@click="handleUppercase"
class="uppercase-icon"
>
<QTooltip>
{{ t('Convert to uppercase') }}
</QTooltip>
</QIcon>
<slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon v-if="info" name="info">
<QTooltip max-width="350px">
{{ info }}
</QTooltip>
@ -120,20 +197,44 @@ const inputRules = [
</div>
</template>
<style>
.uppercase-icon {
transition:
color 0.3s,
transform 0.2s;
cursor: pointer;
}
.uppercase-icon:hover {
color: #ed9937;
transform: scale(1.2);
}
</style>
<i18n lang="yaml">
en-US:
inputMin: Must be more than {value}
maxLength: The value exceeds {value} characters
inputMax: Must be less than {value}
Convert to uppercase: Convert to uppercase
es-ES:
inputMin: Debe ser mayor a {value}
maxLength: El valor excede los {value} carácteres
inputMax: Debe ser menor a {value}
Convert to uppercase: Convertir a mayúsculas
ca-ES:
inputMin: Ha de ser més gran que {value}
inputMax: Ha de ser menys que {value}
maxLength: El valor excedeix els {value} caràcters
inputMax: Ha de ser menor que {value}
Convert to uppercase: Convertir a majúscules
fr-FR:
inputMin: Doit être supérieur à {value}
inputMax: Doit être supérieur à {value}
maxLength: La valeur dépasse {value} caractères
inputMax: Doit être inférieur à {value}
Convert to uppercase: Convertir en majuscules
pt-PT:
inputMin: Deve ser maior que {value}
inputMax: Deve ser maior que {value}
maxLength: O valor excede {value} caracteres
inputMax: Deve ser menor que {value}
Convert to uppercase: Converter para maiúsculas
</i18n>

View File

@ -67,7 +67,7 @@ const $props = defineProps({
});
const { t } = useI18n();
const requiredFieldRule = val => val ?? t('globals.fieldRequired');
const requiredFieldRule = val => val ?? t('fieldRequired');
const { optionLabel, optionValue, options } = toRefs($props);
const myOptions = ref([]);

View File

@ -0,0 +1,17 @@
import { useI18n } from 'vue-i18n';
export function useRequired($attrs) {
const { t } = useI18n();
const isRequired =
typeof $attrs['required'] === 'boolean'
? $attrs['required']
: Object.keys($attrs).includes('required');
const requiredFieldRule = val =>
isRequired ? !!val || t('fieldRequired') : null;
return {
isRequired,
requiredFieldRule
};
}

View File

@ -134,6 +134,8 @@ export default {
introduceSearchTerm: 'Introdueix un terme de cerca',
noOrdersFound: `No s'han trobat comandes`,
send: 'Enviar',
fieldRequired: 'Aquest camp és obligatori',
noChanges: 'No shan fet canvis',
// Image related translations
'Cant lock cache': 'No es pot bloquejar la memòria cau',
'Bad file format': 'Format de fitxer no reconegut',

View File

@ -167,6 +167,8 @@ export default {
introduceSearchTerm: 'Enter a search term',
noOrdersFound: 'No orders found',
send: 'Send',
fieldRequired: 'Field required',
noChanges: 'No changes',
// Image related translations
'Cant lock cache': 'The cache could not be blocked',
'Bad file format': 'Unrecognized file format',

View File

@ -166,6 +166,8 @@ export default {
introduceSearchTerm: 'Introduce un término de búsqueda',
noOrdersFound: 'No se encontrado pedidos',
send: 'Enviar',
fieldRequired: 'Campo requerido',
noChanges: 'No se han hecho cambios',
// Image related translations
'Cant lock cache': 'La caché no pudo ser bloqueada',
'Bad file format': 'Formato de archivo no reconocido',

View File

@ -134,6 +134,8 @@ export default {
introduceSearchTerm: 'Entrez un terme de recherche',
noOrdersFound: 'Aucune commande trouvée',
send: 'Envoyer',
fieldRequired: 'Champ obligatoire',
noChanges: 'Aucun changement',
// Image related translations
'Cant lock cache': "Le cache n'a pas pu être verrouillé",
'Bad file format': 'Format de fichier non reconnu',

View File

@ -133,6 +133,8 @@ export default {
introduceSearchTerm: 'Digite um termo de pesquisa',
noOrdersFound: 'Nenhum pedido encontrado',
send: 'Enviar',
fieldRequired: 'Campo obrigatório',
noChanges: 'Sem alterações',
// Image related translations
'Cant lock cache': 'O cache não pôde ser bloqueado',
'Bad file format': 'Formato de arquivo inválido',

View File

@ -116,22 +116,26 @@ onMounted(async () => {
v-model="data.nickname"
:label="t('name')"
data-cy="addressFormNickname"
:required="true"
/>
<VnInput
v-model="data.street"
:label="t('address')"
data-cy="addressFormStreet"
:required="true"
/>
<VnInput
v-model="data.city"
:label="t('city')"
data-cy="addressFormCity"
:required="true"
/>
<VnInput
v-model="data.postalCode"
type="number"
:label="t('postalCode')"
data-cy="addressFormPostcode"
:required="true"
/>
<VnSelect
v-model="data.countryFk"
@ -139,12 +143,14 @@ onMounted(async () => {
:options="countriesOptions"
@update:model-value="data.provinceFk = null"
data-cy="addressFormCountry"
:required="true"
/>
<VnSelect
v-model="data.provinceFk"
:label="t('province')"
:options="provincesOptions"
data-cy="addressFormProvince"
:required="true"
/>
</template>
</FormModel>