From 55e0c68576fe2ab1b20e6134d134bf49655aa91c Mon Sep 17 00:00:00 2001 From: wbuezas <wbuezas@verdnatura.es> Date: Fri, 14 Mar 2025 13:34:43 -0300 Subject: [PATCH 1/2] Account view refactor --- package.json | 2 +- quasar.config.js | 2 +- src/boot/error-handler.js | 66 -------- src/components/common/FormModel.vue | 231 ++++++++++++++++++++++++++++ src/components/common/VnInput.vue | 171 +++++++++++++++----- src/components/common/VnSelect.vue | 2 +- src/composables/useRequired.js | 17 ++ src/i18n/ca-ES/index.js | 2 + src/i18n/en-US/index.js | 2 + src/i18n/es-ES/index.js | 2 + src/i18n/fr-FR/index.js | 2 + src/i18n/pt-PT/index.js | 2 + src/pages/Account/AccountConfig.vue | 151 +++++++++++------- src/stores/user.js | 2 +- 14 files changed, 492 insertions(+), 162 deletions(-) delete mode 100644 src/boot/error-handler.js create mode 100644 src/components/common/FormModel.vue create mode 100644 src/composables/useRequired.js diff --git a/package.json b/package.json index 64004f37..da3b8435 100644 --- a/package.json +++ b/package.json @@ -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/", diff --git a/quasar.config.js b/quasar.config.js index 84e934d1..a9d50c62 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -23,7 +23,7 @@ module.exports = configure(function (ctx) { // app boot file (/src/boot) // --> boot files are part of "main.js" // https://v2.quasar.dev/quasar-cli-webpack/boot-files - boot: ['i18n', 'axios', 'vnDate', 'error-handler', 'app'], + boot: ['i18n', 'axios', 'vnDate', 'app'], // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css css: ['app.scss', 'width.scss', 'responsive.scss'], diff --git a/src/boot/error-handler.js b/src/boot/error-handler.js deleted file mode 100644 index 1e11c952..00000000 --- a/src/boot/error-handler.js +++ /dev/null @@ -1,66 +0,0 @@ -export default async ({ app }) => { - /* - window.addEventListener('error', - e => onWindowError(e)); - window.addEventListener('unhandledrejection', - e => onWindowRejection(e)); - - ,onWindowError(event) { - errorHandler(event.error); - } - ,onWindowRejection(event) { - errorHandler(event.reason); - } -*/ - app.config.errorHandler = (err, vm, info) => { - errorHandler(err, vm) - } - - function errorHandler (err, vm) { - let message - let tMessage - let res = err.response - - // XXX: Compatibility with old JSON service - if (err.name === 'JsonException') { - res = { - status: err.statusCode, - data: { error: { message: err.message } } - } - } - - if (res) { - const status = res.status - - if (status >= 400 && status < 500) { - switch (status) { - case 401: - tMessage = 'loginFailed' - break - case 403: - tMessage = 'authenticationRequired' - vm.$router.push('/login') - break - case 404: - tMessage = 'notFound' - break - default: - message = res.data.error.message - } - } else if (status >= 500) { - tMessage = 'internalServerError' - } - } else { - tMessage = 'somethingWentWrong' - console.error(err) - } - - if (tMessage) { - message = vm.$t(tMessage) - } - vm.$q.notify({ - message, - type: 'negative' - }) - } -} diff --git a/src/components/common/FormModel.vue b/src/components/common/FormModel.vue new file mode 100644 index 00000000..fc5d1217 --- /dev/null +++ b/src/components/common/FormModel.vue @@ -0,0 +1,231 @@ +<script setup> +import { ref, inject, onMounted, computed, Teleport, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; + +import { useAppStore } from 'stores/app'; +import { storeToRefs } from 'pinia'; +import useNotify from 'src/composables/useNotify.js'; + +const props = defineProps({ + title: { + type: String, + default: '' + }, + table: { + type: String, + default: '' + }, + schema: { + type: String, + default: '' + }, + // Objeto con los datos iniciales del form, si este objeto es definido, no se ejecuta la query fetch + formInitialData: { + type: Object, + default: () => {} + }, + autoLoad: { + type: Boolean, + default: true + }, + defaultActions: { + type: Boolean, + default: true + }, + showBottomActions: { + type: Boolean, + default: false + }, + saveFn: { + type: Function, + default: null + }, + separationBetweenInputs: { + type: String, + default: 'xs' + }, + url: { + type: String, + default: '' + }, + urlUpdate: { + type: String, + default: null + }, + urlCreate: { + type: String, + default: null + }, + observeFormChanges: { + type: Boolean, + default: true, + description: + 'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)' + }, + mapper: { + type: Function, + default: null + }, + filter: { + type: Object, + default: null + } +}); + +const emit = defineEmits(['onDataSaved', 'onDataFetched']); +const api = inject('api'); +const { t } = useI18n(); +const { notify } = useNotify(); +const appStore = useAppStore(); +const { isHeaderMounted } = storeToRefs(appStore); + +const isLoading = ref(false); +const formData = ref({}); +const formModelRef = ref(null); +const hasChanges = ref(!props.observeFormChanges); +const isResetting = ref(false); +const originalData = ref(null); + +const separationBetweenInputs = computed(() => { + return `q-gutter-y-${props.separationBetweenInputs}`; +}); + +const onSubmitSuccess = () => { + emit('onDataSaved'); + notify(t('dataSaved'), 'positive'); +}; + +onMounted(async () => { + if (!props.formInitialData) { + if (props.autoLoad && props.url) await fetch(); + originalData.value = { ...formData.value }; + } else { + formData.value = { ...props.formInitialData }; + originalData.value = { ...props.formInitialData }; + } + + if (props.observeFormChanges) { + watch( + () => formData.value, + (newVal, oldVal) => { + if (!oldVal) return; + hasChanges.value = + !isResetting.value && + JSON.stringify(newVal) !== + JSON.stringify(originalData.value); + isResetting.value = false; + }, + { deep: true } + ); + } +}); + +async function fetch() { + try { + let { data } = await api.get(props.url, { + params: { filter: JSON.stringify(props.filter) } + }); + if (Array.isArray(data)) data = data[0] ?? {}; + formData.value = { ...data }; + emit('onDataFetched', formData.value); + } catch (e) { + throw e; + } +} + +const submitForm = async evt => { + const isFormValid = await formModelRef.value.validate(); + if (isFormValid) await save(evt); +}; + +async function save() { + if (props.observeFormChanges && !hasChanges.value) + return notify('noChanges', 'negative'); + + isLoading.value = true; + try { + const body = props.mapper + ? props.mapper(formData.value, originalData.value) + : formData.value; + const method = props.urlCreate ? 'post' : 'patch'; + const url = props.urlCreate || props.urlUpdate || props.url; + + await Promise.resolve( + props.saveFn ? props.saveFn(body) : api[method](url, body) + ); + + onSubmitSuccess(); + hasChanges.value = false; + } finally { + isLoading.value = false; + } +} + +defineExpose({ + formData, + submitForm +}); +</script> + +<template> + <QCard class="form-container" v-bind="$attrs"> + <QForm ref="formModelRef" class="form" :class="separationBetweenInputs"> + <span v-if="title" class="text-h6 text-bold"> + {{ title }} + </span> + <slot name="form" :data="formData" /> + <slot name="extraForm" :data="formData" /> + <component + v-if="isHeaderMounted" + :is="showBottomActions ? 'div' : Teleport" + to="#actions" + class="flex row justify-end q-gutter-x-sm" + :class="{ 'q-mt-md': showBottomActions }" + > + <QBtn + v-if="defaultActions && showBottomActions" + :label="t('cancel')" + :icon="showBottomActions ? undefined : 'check'" + rounded + no-caps + flat + v-close-popup + > + <QTooltip>{{ t('cancel') }}</QTooltip> + </QBtn> + <QBtn + v-if="defaultActions" + :label="t('save')" + :icon="showBottomActions ? undefined : 'check'" + rounded + no-caps + flat + :loading="isLoading" + :disabled="!showBottomActions && !hasChanges" + @click="submitForm()" + data-cy="formModelDefaultSaveButton" + > + <QTooltip>{{ t('save') }}</QTooltip> + </QBtn> + <slot name="actions" :data="formData" /> + </component> + </QForm> + </QCard> +</template> + +<style lang="scss" scoped> +.form-container { + width: 100%; + height: max-content; + padding: 32px; + max-width: 544px; + display: flex; + justify-content: center; +} + +.form { + display: flex; + flex-direction: column; + width: 100%; +} +</style> diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 508ce132..7dcf1164 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -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> diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index c4a3e1df..65dbb2f2 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -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([]); diff --git a/src/composables/useRequired.js b/src/composables/useRequired.js new file mode 100644 index 00000000..a77345b5 --- /dev/null +++ b/src/composables/useRequired.js @@ -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 + }; +} diff --git a/src/i18n/ca-ES/index.js b/src/i18n/ca-ES/index.js index 456795a5..48d214e0 100644 --- a/src/i18n/ca-ES/index.js +++ b/src/i18n/ca-ES/index.js @@ -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 s’han fet canvis', // Image related translations 'Cant lock cache': 'No es pot bloquejar la memòria cau', 'Bad file format': 'Format de fitxer no reconegut', diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index 3e21b66d..80107dd5 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -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', diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js index 5034ff70..15639c2d 100644 --- a/src/i18n/es-ES/index.js +++ b/src/i18n/es-ES/index.js @@ -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', diff --git a/src/i18n/fr-FR/index.js b/src/i18n/fr-FR/index.js index d30dee52..2eea9a38 100644 --- a/src/i18n/fr-FR/index.js +++ b/src/i18n/fr-FR/index.js @@ -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', diff --git a/src/i18n/pt-PT/index.js b/src/i18n/pt-PT/index.js index 39bbe5de..460335e0 100644 --- a/src/i18n/pt-PT/index.js +++ b/src/i18n/pt-PT/index.js @@ -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', diff --git a/src/pages/Account/AccountConfig.vue b/src/pages/Account/AccountConfig.vue index 0aec3240..edbb181e 100644 --- a/src/pages/Account/AccountConfig.vue +++ b/src/pages/Account/AccountConfig.vue @@ -3,8 +3,9 @@ import { ref, inject, onMounted, computed } from 'vue'; import { useI18n } from 'vue-i18n'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -import VnForm from 'src/components/common/VnForm.vue'; import ChangePasswordForm from 'src/components/ui/ChangePasswordForm.vue'; +import FormModel from 'src/components/common/FormModel.vue'; +import useNotify from 'src/composables/useNotify.js'; import { useUserStore } from 'stores/user'; import { useAppStore } from 'stores/app'; @@ -12,54 +13,76 @@ import { storeToRefs } from 'pinia'; const userStore = useUserStore(); const { t } = useI18n(); -const jApi = inject('jApi'); +const api = inject('api'); const appStore = useAppStore(); const { isHeaderMounted } = storeToRefs(appStore); const { user } = storeToRefs(userStore); +const { notify } = useNotify(); const vnFormRef = ref(null); -const vnFormRef2 = ref(null); const changePasswordFormDialog = ref(null); const showChangePasswordForm = ref(false); const langOptions = ref([]); -const pks = computed(() => ({ id: userStore?.user?.id })); -const fetchConfigDataSql = { - query: ` - SELECT u.id, u.name, u.email, u.nickname, - u.lang, c.isToBeMailed, c.id clientFk - FROM account.myUser u - LEFT JOIN myClient c - ON u.id = c.id`, - params: {} -}; +const formInitialData = ref({}); +const showForm = ref(false); -const fetchLanguagesSql = async () => { +const fetchLanguages = async () => { try { - const data = await jApi.query( - 'SELECT code, name FROM language WHERE isActive' - ); + const filter = { fields: ['code', 'name'], where: { isActive: true } }; + const { data } = await api.get('/languages', { + params: { filter: JSON.stringify(filter) } + }); langOptions.value = data; } catch (error) { console.error(error); } }; +const fetchFormInitialData = async () => { + try { + const filter = { + where: { id: user?.value?.id }, + fields: ['id', 'name', 'isToBeMailed'], + include: { + relation: 'user', + scope: { + fields: ['nickname', 'lang', 'email'] + } + } + }; + const { data } = await api.get('/Clients', { + params: { filter: JSON.stringify(filter) } + }); + + const { user: userData, ...restOfData } = data[0]; + + formInitialData.value = { + ...restOfData, + nickname: userData?.nickname, + lang: userData?.lang, + email: userData?.email + }; + } catch (error) { + console.error(error); + } finally { + showForm.value = true; + } +}; + const updateUserNickname = async nickname => { try { - await vnFormRef.value.submit(); + await submitAccountData(nickname); + await submitNickname(nickname); user.value.nickname = nickname; } catch (error) { console.error(error); } }; -const formatMailData = data => { - data.isToBeMailed = Boolean(data.isToBeMailed); -}; - const updateConfigLang = async lang => { try { - await vnFormRef.value.submit(); + if (!lang) return; + await submitAccountData({ lang }); userStore.updateUserLang(lang); const siteLocaleLang = appStore.localeOptions.find( locale => locale.value === lang @@ -70,7 +93,32 @@ const updateConfigLang = async lang => { } }; -onMounted(() => fetchLanguagesSql()); +const submitAccountData = async data => { + try { + const params = { + ...data + }; + await api.patch(`/VnUsers/${user?.value?.id}`, params); + notify(t('dataSaved'), 'positive'); + } catch (error) { + console.error(error); + } +}; + +const submitIsToBeMailed = async isToBeMailed => { + try { + const payload = { isToBeMailed }; + await api.patch(`/Clients/${user.value.id}`, payload); + notify(t('dataSaved'), 'positive'); + } catch (error) { + console.error(error); + } +}; + +onMounted(async () => { + fetchLanguages(); + fetchFormInitialData(); +}); </script> <template> @@ -92,14 +140,14 @@ onMounted(() => fetchLanguagesSql()); @click="showChangePasswordForm = true" /> </Teleport> - <VnForm + <FormModel + v-if="showForm" ref="vnFormRef" + :save-fn="submitFormFn" + :form-initial-data="formInitialData" :title="t('personalInformation')" - :fetch-form-data-sql="fetchConfigDataSql" - :pks="pks" - table="myUser" - schema="account" - :default-actions="false" + :show-bottom-actions="false" + :defaultActions="false" > <template #form="{ data }"> <VnInput @@ -111,14 +159,16 @@ onMounted(() => fetchLanguagesSql()); <VnInput v-model="data.email" :label="t('email')" - @keyup.enter="vnFormRef.submit()" - @blur="vnFormRef.submit()" + @keyup.enter="submitAccountData({ email: data.email })" + @blur="submitAccountData({ email: data.email })" /> <VnInput v-model="data.nickname" :label="t('nickname')" - @keyup.enter="updateUserNickname(data.nickname)" - @blur="updateUserNickname(data.nickname)" + @keyup.enter=" + updateUserNickname({ nickname: data.nickname }) + " + @blur="updateUserNickname({ nickname: data.nickname })" data-cy="configViewNickname" /> <VnSelect @@ -130,30 +180,17 @@ onMounted(() => fetchLanguagesSql()); @update:model-value="updateConfigLang(data.lang)" data-cy="configViewLang" /> + <QCheckbox + v-model="data.isToBeMailed" + :label="t('isToBeMailed')" + :toggle-indeterminate="false" + @update:model-value=" + submitIsToBeMailed(data.isToBeMailed) + " + dense + /> </template> - <template #extraForm> - <VnForm - class="no-form-container q-mt-md" - ref="vnFormRef2" - :pks="pks" - table="myClient" - schema="hedera" - :fetch-form-data-sql="fetchConfigDataSql" - :default-actions="false" - @on-data-fetched="$event => formatMailData($event)" - > - <template #form="{ data }"> - <QCheckbox - v-model="data.isToBeMailed" - :label="t('isToBeMailed')" - :toggle-indeterminate="false" - @update:model-value="vnFormRef2.submit()" - dense - /> - </template> - </VnForm> - </template> - </VnForm> + </FormModel> </QPage> <QDialog ref="changePasswordFormDialog" diff --git a/src/stores/user.js b/src/stores/user.js index 91278afa..78fd2f70 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -38,8 +38,8 @@ export const useUserStore = defineStore('user', () => { router.push({ name: 'login' }); } } else { - await fetchTokenConfig(); await fetchUser(); + await fetchTokenConfig(); await supplantInit(); startInterval(); } From 37c95ba13d800eb74462e561e0db45c7c93c8061 Mon Sep 17 00:00:00 2001 From: wbuezas <wbuezas@verdnatura.es> Date: Wed, 19 Mar 2025 14:10:07 -0300 Subject: [PATCH 2/2] Input change --- src/components/common/VnInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 7dcf1164..f20754ee 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -144,7 +144,7 @@ const handleUppercase = () => { :rules="mixinRules" :lazy-rules="true" hide-bottom-space - :data-cy="$attrs.dataCy ?? $attrs.label + '_input'" + :data-cy="$attrs['data-cy'] ?? $attrs.label + '_input'" > <template #prepend v-if="$slots.prepend"> <slot name="prepend" />