Improvements
This commit is contained in:
parent
bd1e9b7037
commit
97ec2fabc5
|
@ -59,7 +59,7 @@
|
||||||
"salix": "cd ../salix && gulp back",
|
"salix": "cd ../salix && gulp back",
|
||||||
"db": "cd ../salix && gulp docker",
|
"db": "cd ../salix && gulp docker",
|
||||||
"cy:open": "npm run db && cypress open",
|
"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",
|
"test:unit": "vitest",
|
||||||
"build": "rm -rf dist/ ; quasar build",
|
"build": "rm -rf dist/ ; quasar build",
|
||||||
"clean": "rm -rf dist/",
|
"clean": "rm -rf dist/",
|
||||||
|
|
|
@ -81,7 +81,7 @@ const { isHeaderMounted } = storeToRefs(appStore);
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const formData = ref({});
|
const formData = ref({});
|
||||||
const addressFormRef = ref(null);
|
const formModelRef = ref(null);
|
||||||
const hasChanges = ref(!props.observeFormChanges);
|
const hasChanges = ref(!props.observeFormChanges);
|
||||||
const isResetting = ref(false);
|
const isResetting = ref(false);
|
||||||
const originalData = ref(null);
|
const originalData = ref(null);
|
||||||
|
@ -119,6 +119,7 @@ onMounted(async () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function fetch() {
|
async function fetch() {
|
||||||
try {
|
try {
|
||||||
let { data } = await api.get(props.url, {
|
let { data } = await api.get(props.url, {
|
||||||
|
@ -133,9 +134,12 @@ async function fetch() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
console.log('submit: ');
|
|
||||||
if (props.observeFormChanges && !hasChanges.value)
|
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;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
|
@ -167,7 +171,7 @@ defineExpose({
|
||||||
<QCard class="form-container" v-bind="$attrs">
|
<QCard class="form-container" v-bind="$attrs">
|
||||||
<QForm
|
<QForm
|
||||||
v-if="!isLoading"
|
v-if="!isLoading"
|
||||||
ref="addressFormRef"
|
ref="formModelRef"
|
||||||
class="form"
|
class="form"
|
||||||
:class="separationBetweenInputs"
|
:class="separationBetweenInputs"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, useAttrs, nextTick } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
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([
|
const emit = defineEmits([
|
||||||
'update:modelValue',
|
'update:modelValue',
|
||||||
'update:options',
|
'update:options',
|
||||||
'keyup.enter',
|
'keyup.enter',
|
||||||
'remove'
|
'remove',
|
||||||
|
'blur'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const $props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: null
|
default: null
|
||||||
|
@ -25,24 +30,43 @@ const props = defineProps({
|
||||||
clearable: {
|
clearable: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
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 vnInputRef = ref(null);
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get() {
|
get() {
|
||||||
return props.modelValue;
|
return $props.modelValue;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
|
if ($props.emptyToNull && value === '') value = null;
|
||||||
emit('update:modelValue', value);
|
emit('update:modelValue', value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const hover = ref(false);
|
const hover = ref(false);
|
||||||
const styleAttrs = computed(() => {
|
const styleAttrs = computed(() => {
|
||||||
return props.isOutlined
|
return $props.isOutlined
|
||||||
? { dense: true, outlined: true, rounded: true }
|
? {
|
||||||
|
dense: true,
|
||||||
|
outlined: true,
|
||||||
|
rounded: true
|
||||||
|
}
|
||||||
: {};
|
: {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,66 +75,119 @@ const focus = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
focus
|
focus,
|
||||||
|
vnInputRef
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputRules = [
|
const mixinRules = [
|
||||||
|
requiredFieldRule,
|
||||||
|
...($attrs.rules ?? []),
|
||||||
val => {
|
val => {
|
||||||
|
const { maxlength } = vnInputRef.value;
|
||||||
|
if (maxlength && +val.length > maxlength)
|
||||||
|
return t(`maxLength`, { value: maxlength });
|
||||||
const { min, max } = vnInputRef.value.$attrs;
|
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 (Math.floor(val) < min) return t('inputMin', { value: min });
|
||||||
}
|
if (!max) return null;
|
||||||
if (max > 0) {
|
if (max > 0) {
|
||||||
if (Math.floor(val) > max) return t('inputMax', { value: max });
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div @mouseover="hover = true" @mouseleave="hover = false">
|
||||||
:rules="$attrs.required ? [requiredFieldRule] : null"
|
|
||||||
@mouseover="hover = true"
|
|
||||||
@mouseleave="hover = false"
|
|
||||||
>
|
|
||||||
<QInput
|
<QInput
|
||||||
ref="vnInputRef"
|
ref="vnInputRef"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
v-bind="{ ...$attrs, ...styleAttrs }"
|
v-bind="{ ...$attrs, ...styleAttrs }"
|
||||||
:type="$attrs.type"
|
:type="$attrs.type"
|
||||||
:class="{ required: $attrs.required }"
|
:class="{ required: isRequired }"
|
||||||
|
@keyup.enter="emit('keyup.enter')"
|
||||||
|
@blur="emit('blur')"
|
||||||
|
@keydown="handleKeydown"
|
||||||
:clearable="false"
|
:clearable="false"
|
||||||
:rules="inputRules"
|
:rules="mixinRules"
|
||||||
:lazy-rules="true"
|
:lazy-rules="true"
|
||||||
hide-bottom-space
|
hide-bottom-space
|
||||||
@keyup.enter="emit('keyup.enter')"
|
:data-cy="$attrs.dataCy ?? $attrs.label + '_input'"
|
||||||
>
|
>
|
||||||
<template
|
<template #prepend v-if="$slots.prepend">
|
||||||
v-if="$slots.prepend"
|
|
||||||
#prepend
|
|
||||||
>
|
|
||||||
<slot name="prepend" />
|
<slot name="prepend" />
|
||||||
</template>
|
</template>
|
||||||
<template #append>
|
<template #append>
|
||||||
<slot
|
|
||||||
v-if="$slots.append && !$attrs.disabled"
|
|
||||||
name="append"
|
|
||||||
/>
|
|
||||||
<QIcon
|
<QIcon
|
||||||
v-if="hover && value && !$attrs.disabled && props.clearable"
|
|
||||||
name="close"
|
name="close"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
:style="{
|
||||||
|
visibility:
|
||||||
|
hover &&
|
||||||
|
value &&
|
||||||
|
!$attrs.disabled &&
|
||||||
|
!$attrs.readonly &&
|
||||||
|
$props.clearable
|
||||||
|
? 'visible'
|
||||||
|
: 'hidden'
|
||||||
|
}"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
value = null;
|
value = null;
|
||||||
|
vnInputRef.focus();
|
||||||
emit('remove');
|
emit('remove');
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
></QIcon>
|
||||||
|
|
||||||
<QIcon
|
<QIcon
|
||||||
v-if="info"
|
name="match_case"
|
||||||
name="info"
|
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">
|
<QTooltip max-width="350px">
|
||||||
{{ info }}
|
{{ info }}
|
||||||
</QTooltip>
|
</QTooltip>
|
||||||
|
@ -120,20 +197,44 @@ const inputRules = [
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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">
|
<i18n lang="yaml">
|
||||||
en-US:
|
en-US:
|
||||||
inputMin: Must be more than {value}
|
inputMin: Must be more than {value}
|
||||||
|
maxLength: The value exceeds {value} characters
|
||||||
inputMax: Must be less than {value}
|
inputMax: Must be less than {value}
|
||||||
|
Convert to uppercase: Convert to uppercase
|
||||||
es-ES:
|
es-ES:
|
||||||
inputMin: Debe ser mayor a {value}
|
inputMin: Debe ser mayor a {value}
|
||||||
|
maxLength: El valor excede los {value} carácteres
|
||||||
inputMax: Debe ser menor a {value}
|
inputMax: Debe ser menor a {value}
|
||||||
|
Convert to uppercase: Convertir a mayúsculas
|
||||||
ca-ES:
|
ca-ES:
|
||||||
inputMin: Ha de ser més gran que {value}
|
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:
|
fr-FR:
|
||||||
inputMin: Doit être supérieur à {value}
|
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:
|
pt-PT:
|
||||||
inputMin: Deve ser maior que {value}
|
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>
|
</i18n>
|
||||||
|
|
|
@ -67,7 +67,7 @@ const $props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const requiredFieldRule = val => val ?? t('globals.fieldRequired');
|
const requiredFieldRule = val => val ?? t('fieldRequired');
|
||||||
|
|
||||||
const { optionLabel, optionValue, options } = toRefs($props);
|
const { optionLabel, optionValue, options } = toRefs($props);
|
||||||
const myOptions = ref([]);
|
const myOptions = ref([]);
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
|
@ -134,6 +134,8 @@ export default {
|
||||||
introduceSearchTerm: 'Introdueix un terme de cerca',
|
introduceSearchTerm: 'Introdueix un terme de cerca',
|
||||||
noOrdersFound: `No s'han trobat comandes`,
|
noOrdersFound: `No s'han trobat comandes`,
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Aquest camp és obligatori',
|
||||||
|
noChanges: 'No s’han fet canvis',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'No es pot bloquejar la memòria cau',
|
'Cant lock cache': 'No es pot bloquejar la memòria cau',
|
||||||
'Bad file format': 'Format de fitxer no reconegut',
|
'Bad file format': 'Format de fitxer no reconegut',
|
||||||
|
|
|
@ -167,6 +167,8 @@ export default {
|
||||||
introduceSearchTerm: 'Enter a search term',
|
introduceSearchTerm: 'Enter a search term',
|
||||||
noOrdersFound: 'No orders found',
|
noOrdersFound: 'No orders found',
|
||||||
send: 'Send',
|
send: 'Send',
|
||||||
|
fieldRequired: 'Field required',
|
||||||
|
noChanges: 'No changes',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'The cache could not be blocked',
|
'Cant lock cache': 'The cache could not be blocked',
|
||||||
'Bad file format': 'Unrecognized file format',
|
'Bad file format': 'Unrecognized file format',
|
||||||
|
|
|
@ -166,6 +166,8 @@ export default {
|
||||||
introduceSearchTerm: 'Introduce un término de búsqueda',
|
introduceSearchTerm: 'Introduce un término de búsqueda',
|
||||||
noOrdersFound: 'No se encontrado pedidos',
|
noOrdersFound: 'No se encontrado pedidos',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Campo requerido',
|
||||||
|
noChanges: 'No se han hecho cambios',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'La caché no pudo ser bloqueada',
|
'Cant lock cache': 'La caché no pudo ser bloqueada',
|
||||||
'Bad file format': 'Formato de archivo no reconocido',
|
'Bad file format': 'Formato de archivo no reconocido',
|
||||||
|
|
|
@ -134,6 +134,8 @@ export default {
|
||||||
introduceSearchTerm: 'Entrez un terme de recherche',
|
introduceSearchTerm: 'Entrez un terme de recherche',
|
||||||
noOrdersFound: 'Aucune commande trouvée',
|
noOrdersFound: 'Aucune commande trouvée',
|
||||||
send: 'Envoyer',
|
send: 'Envoyer',
|
||||||
|
fieldRequired: 'Champ obligatoire',
|
||||||
|
noChanges: 'Aucun changement',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': "Le cache n'a pas pu être verrouillé",
|
'Cant lock cache': "Le cache n'a pas pu être verrouillé",
|
||||||
'Bad file format': 'Format de fichier non reconnu',
|
'Bad file format': 'Format de fichier non reconnu',
|
||||||
|
|
|
@ -133,6 +133,8 @@ export default {
|
||||||
introduceSearchTerm: 'Digite um termo de pesquisa',
|
introduceSearchTerm: 'Digite um termo de pesquisa',
|
||||||
noOrdersFound: 'Nenhum pedido encontrado',
|
noOrdersFound: 'Nenhum pedido encontrado',
|
||||||
send: 'Enviar',
|
send: 'Enviar',
|
||||||
|
fieldRequired: 'Campo obrigatório',
|
||||||
|
noChanges: 'Sem alterações',
|
||||||
// Image related translations
|
// Image related translations
|
||||||
'Cant lock cache': 'O cache não pôde ser bloqueado',
|
'Cant lock cache': 'O cache não pôde ser bloqueado',
|
||||||
'Bad file format': 'Formato de arquivo inválido',
|
'Bad file format': 'Formato de arquivo inválido',
|
||||||
|
|
|
@ -116,22 +116,26 @@ onMounted(async () => {
|
||||||
v-model="data.nickname"
|
v-model="data.nickname"
|
||||||
:label="t('name')"
|
:label="t('name')"
|
||||||
data-cy="addressFormNickname"
|
data-cy="addressFormNickname"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="data.street"
|
v-model="data.street"
|
||||||
:label="t('address')"
|
:label="t('address')"
|
||||||
data-cy="addressFormStreet"
|
data-cy="addressFormStreet"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="data.city"
|
v-model="data.city"
|
||||||
:label="t('city')"
|
:label="t('city')"
|
||||||
data-cy="addressFormCity"
|
data-cy="addressFormCity"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<VnInput
|
<VnInput
|
||||||
v-model="data.postalCode"
|
v-model="data.postalCode"
|
||||||
type="number"
|
type="number"
|
||||||
:label="t('postalCode')"
|
:label="t('postalCode')"
|
||||||
data-cy="addressFormPostcode"
|
data-cy="addressFormPostcode"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<VnSelect
|
<VnSelect
|
||||||
v-model="data.countryFk"
|
v-model="data.countryFk"
|
||||||
|
@ -139,12 +143,14 @@ onMounted(async () => {
|
||||||
:options="countriesOptions"
|
:options="countriesOptions"
|
||||||
@update:model-value="data.provinceFk = null"
|
@update:model-value="data.provinceFk = null"
|
||||||
data-cy="addressFormCountry"
|
data-cy="addressFormCountry"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<VnSelect
|
<VnSelect
|
||||||
v-model="data.provinceFk"
|
v-model="data.provinceFk"
|
||||||
:label="t('province')"
|
:label="t('province')"
|
||||||
:options="provincesOptions"
|
:options="provincesOptions"
|
||||||
data-cy="addressFormProvince"
|
data-cy="addressFormProvince"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</FormModel>
|
</FormModel>
|
||||||
|
|
Loading…
Reference in New Issue