feature/Address-view-refactor #123

Merged
jsegarra merged 6 commits from wbuezas/hedera-web-mindshore:feature/Address-view-refactor into beta 2025-03-21 14:40:28 +00:00
11 changed files with 179 additions and 41 deletions
Showing only changes of commit 97ec2fabc5 - Show all commits

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",
Review

esto es necesario?

esto es necesario?
Review

Si, ya que aparentemente hay un bug en cypresss que en la version terminal fallan los tests.

Si, ya que aparentemente hay un bug en cypresss que en la version terminal fallan los tests.
"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"
/>

y si le pasamos required tal cual, no deberia ir?
O es porque no estamos comporbando que la key de required exista en los $attrs?

y si le pasamos required tal cual, no deberia ir? O es porque no estamos comporbando que la key de required exista en los $attrs?

Recuerdo que en un momento en lilium se hacia algo asi por algo en especial. Pero parece que aca pasando solo required funciona igual

Commit: 5e3387fb6d

Recuerdo que en un momento en lilium se hacia algo asi por algo en especial. Pero parece que aca pasando solo `required` funciona igual Commit: https://gitea.verdnatura.es/verdnatura/hedera-web/commit/5e3387fb6d1af7ebc97b730e41cbbb6a3db58600
</template>
</FormModel>