From 3f1c0b95faf7d8ff00b10fa93acdbed4047d6b9a Mon Sep 17 00:00:00 2001 From: Javier Segarra <jsegarra@verdnatura.es> Date: Wed, 10 Jul 2024 15:03:25 +0200 Subject: [PATCH 01/12] fix: proposal to avoid notify error --- src/pages/Customer/Card/CustomerUnpaid.vue | 2 +- src/pages/Customer/Card/CustomerWebAccess.vue | 2 +- src/pages/Customer/components/CustomerChangePassword.vue | 2 +- src/pages/Customer/components/CustomerSamplesCreate.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Customer/Card/CustomerUnpaid.vue b/src/pages/Customer/Card/CustomerUnpaid.vue index a9d4a3d66..6c61b92a1 100644 --- a/src/pages/Customer/Card/CustomerUnpaid.vue +++ b/src/pages/Customer/Card/CustomerUnpaid.vue @@ -92,7 +92,7 @@ const onSubmit = async () => { notify('globals.dataSaved', 'positive'); unpaidClient.value = true; } catch (error) { - notify('errors.create', 'negative'); + notify('errors.writeRequest', 'negative'); } finally { isLoading.value = false; } diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 33659dd77..4468c52f0 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -70,7 +70,7 @@ const onSubmit = async () => { notify('globals.dataSaved', 'positive'); if (usersPreviewRef.value) usersPreviewRef.value.fetch(); } catch (error) { - notify('errors.create', 'negative'); + notify('errors.writeRequest', 'negative'); } finally { isLoading.value = false; } diff --git a/src/pages/Customer/components/CustomerChangePassword.vue b/src/pages/Customer/components/CustomerChangePassword.vue index 1bfc5e103..632b11dc9 100644 --- a/src/pages/Customer/components/CustomerChangePassword.vue +++ b/src/pages/Customer/components/CustomerChangePassword.vue @@ -48,7 +48,7 @@ const onSubmit = async () => { await axios.patch(`Clients/${$props.id}/setPassword`, payload); await $props.promise(); } catch (error) { - notify('errors.create', 'negative'); + notify('errors.writeRequest', 'negative'); } finally { isLoading.value = false; if (closeButton.value) closeButton.value.click(); diff --git a/src/pages/Customer/components/CustomerSamplesCreate.vue b/src/pages/Customer/components/CustomerSamplesCreate.vue index be614aa0b..283b8fa97 100644 --- a/src/pages/Customer/components/CustomerSamplesCreate.vue +++ b/src/pages/Customer/components/CustomerSamplesCreate.vue @@ -150,7 +150,7 @@ const onSubmit = async () => { notify('globals.dataSaved', 'positive'); onDataSaved(data); } catch (error) { - notify('errors.create', 'negative'); + notify('errors.writeRequest', 'negative'); } finally { isLoading.value = false; } From e90b78c4c5867f2afd48b9029c9b443d5291c385 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 13 Sep 2024 12:27:37 +0200 Subject: [PATCH 02/12] fix: refs #7702 rollback --- src/pages/Customer/Card/CustomerWebAccess.vue | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 3dc025d9c..f990e720c 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import axios from 'axios'; import { useQuasar } from 'quasar'; -import useNotify from 'src/composables/useNotify'; import VnInput from 'src/components/common/VnInput.vue'; import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import FormModel from 'components/FormModel.vue'; @@ -13,7 +12,6 @@ const { t } = useI18n(); const quasar = useQuasar(); const route = useRoute(); const canChangePassword = ref(0); -const { notify } = useNotify(); const filter = computed(() => { return { @@ -32,13 +30,8 @@ const showChangePasswordDialog = () => { }; async function hasCustomerRole() { - try { - canChangePassword.value = ( - await axios(`Clients/${route.params.id}/hasCustomerRole`) - ).data; - } catch (e) { - notify('errors.writeRequest', 'negative'); - } + const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`); + canChangePassword.value = data; } </script> From 4bf8e1224d1d8cd0082174fe7830baeeddc45b0c Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 13 Sep 2024 12:28:07 +0200 Subject: [PATCH 03/12] chore: refs #7702 rollback --- src/pages/Customer/Card/CustomerWebAccess.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index f990e720c..8d025a365 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import axios from 'axios'; import { useQuasar } from 'quasar'; + import VnInput from 'src/components/common/VnInput.vue'; import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import FormModel from 'components/FormModel.vue'; From 98cdeabe9f91791faa4a623ed9e67237180b0666 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 13 Sep 2024 18:15:28 +0200 Subject: [PATCH 04/12] feat: refs #7702 vnChangePassword --- src/components/common/VnChangePassword.vue | 112 ++++++++++++++++++ src/pages/Customer/Card/CustomerWebAccess.vue | 28 ++--- 2 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 src/components/common/VnChangePassword.vue diff --git a/src/components/common/VnChangePassword.vue b/src/components/common/VnChangePassword.vue new file mode 100644 index 000000000..d7c9ad02f --- /dev/null +++ b/src/components/common/VnChangePassword.vue @@ -0,0 +1,112 @@ +<script setup> +import { ref } from 'vue'; +import { useI18n } from 'vue-i18n'; +import VnRow from '../ui/VnRow.vue'; +import VnInput from './VnInput.vue'; +import FetchData from '../FetchData.vue'; +import useNotify from 'src/composables/useNotify'; + +const props = defineProps({ submitFn: { type: Function, default: () => {} } }); + +const { t } = useI18n(); +const { notify } = useNotify(); + +const form = ref(); +const changePassDialog = ref(); +const passwords = ref({ newPassword: null, repeatPassword: null }); +const requirements = ref([]); + +const validate = async () => { + const { newPassword, repeatPassword } = passwords.value; + if (newPassword !== repeatPassword) { + notify(t("Passwords don't match"), 'negative'); + return; + } + try { + await props.submitFn(newPassword); + } catch (e) { + notify('errors.writeRequest', 'negative'); + } finally { + changePassDialog.value.hide(); + } +}; + +defineExpose({ show: () => changePassDialog.value.show() }); +</script> +<template> + <FetchData + url="UserPasswords/findOne" + auto-load + @on-fetch="(data) => (requirements = data)" + /> + <QDialog ref="changePassDialog"> + <QCard style="width: 350px"> + <QCardSection> + <slot name="header"> + <VnRow class="items-center" style="flex-direction: row"> + <span class="text-h6" v-text="t('Change password')" /> + <QIcon + class="cursor-pointer" + name="close" + size="xs" + style="flex: 0" + v-close-popup + /> + </VnRow> + </slot> + </QCardSection> + <QForm ref="form"> + <QCardSection> + <VnInput + :label="t('New password')" + v-model="passwords.newPassword" + type="password" + :required="true" + :info=" + t('passwordRequirements', { + length: requirements.length, + nAlpha: requirements.nAlpha, + nUpper: requirements.nUpper, + nDigits: requirements.nDigits, + nPunct: requirements.nPunct, + }) + " + autofocus + /> + + <VnInput + :label="t('Repeat password')" + v-model="passwords.repeatPassword" + type="password" + /> + </QCardSection> + </QForm> + <QCardActions> + <slot name="actions"> + <QBtn + :label="t('globals.cancel')" + class="q-ml-sm" + color="primary" + flat + type="reset" + v-close-popup + /> + <QBtn + :label="t('globals.confirm')" + color="primary" + @click="validate" + /> + </slot> + </QCardActions> + </QCard> + </QDialog> +</template> + +<i18n> +es: + Change password: Cambiar contraseña + New password: Nueva contraseña + Repeat password: Repetir contraseña + You must enter a new password: Debes introducir la nueva contraseña + Passwords don't match: Las contraseñas no coinciden +</i18n> diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 8d025a365..ba906a144 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -3,14 +3,11 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute } from 'vue-router'; import axios from 'axios'; -import { useQuasar } from 'quasar'; - import VnInput from 'src/components/common/VnInput.vue'; -import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import FormModel from 'components/FormModel.vue'; +import VnChangePassword from 'src/components/common/VnChangePassword.vue'; const { t } = useI18n(); -const quasar = useQuasar(); const route = useRoute(); const canChangePassword = ref(0); @@ -21,21 +18,11 @@ const filter = computed(() => { }; }); -const showChangePasswordDialog = () => { - quasar.dialog({ - component: CustomerChangePassword, - componentProps: { - id: route.params.id, - }, - }); -}; - async function hasCustomerRole() { const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`); canChangePassword.value = data; } </script> - <template> <FormModel url="VnUsers/preview" @@ -73,12 +60,21 @@ async function hasCustomerRole() { color="primary" icon="edit" :disable="!canChangePassword" - @click="showChangePasswordDialog()" + @click="$refs.changePassRef.show" /> </template> </FormModel> + <VnChangePassword + ref="changePassRef" + :submit-fn=" + async (newPass) => { + await axios.patch(`Clients/${$route.params.id}/setPassword`, { + newPassword: newPass, + }); + } + " + /> </template> - <i18n> es: Enable web access: Habilitar acceso web From 9ec2fb4c77edede9e5b80a34a80263a33ac80a1c Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 16 Sep 2024 10:36:56 +0200 Subject: [PATCH 05/12] feat: refs #7702 fine tunning --- src/components/common/VnChangePassword.vue | 14 +++++++- src/pages/Worker/Card/WorkerDescriptor.vue | 40 ++++++++++------------ 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/components/common/VnChangePassword.vue b/src/components/common/VnChangePassword.vue index d7c9ad02f..a36c6bcff 100644 --- a/src/components/common/VnChangePassword.vue +++ b/src/components/common/VnChangePassword.vue @@ -7,7 +7,7 @@ import FetchData from '../FetchData.vue'; import useNotify from 'src/composables/useNotify'; const props = defineProps({ submitFn: { type: Function, default: () => {} } }); - +const emit = defineEmits(['onSubmit']); const { t } = useI18n(); const { notify } = useNotify(); @@ -15,19 +15,27 @@ const form = ref(); const changePassDialog = ref(); const passwords = ref({ newPassword: null, repeatPassword: null }); const requirements = ref([]); +const isLoading = ref(false); const validate = async () => { const { newPassword, repeatPassword } = passwords.value; + if (!newPassword) { + notify(t('You must enter a new password'), 'negative'); + return; + } if (newPassword !== repeatPassword) { notify(t("Passwords don't match"), 'negative'); return; } try { + isLoading.value = true; await props.submitFn(newPassword); + emit('onSubmit'); } catch (e) { notify('errors.writeRequest', 'negative'); } finally { changePassDialog.value.hide(); + isLoading.value = false; } }; @@ -84,6 +92,8 @@ defineExpose({ show: () => changePassDialog.value.show() }); <QCardActions> <slot name="actions"> <QBtn + :disabled="isLoading" + :loading="isLoading" :label="t('globals.cancel')" class="q-ml-sm" color="primary" @@ -92,6 +102,8 @@ defineExpose({ show: () => changePassDialog.value.show() }); v-close-popup /> <QBtn + :disabled="isLoading" + :loading="isLoading" :label="t('globals.confirm')" color="primary" @click="validate" diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index be30537df..b48859e9b 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'; import CardDescriptor from 'src/components/ui/CardDescriptor.vue'; import VnLv from 'src/components/ui/VnLv.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; -import WorkerChangePasswordForm from 'src/pages/Worker/Card/WorkerChangePasswordForm.vue'; +import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import { useState } from 'src/composables/useState'; import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; @@ -24,18 +24,13 @@ const route = useRoute(); const { t } = useI18n(); const state = useState(); const user = state.getUser(); -const changePasswordFormDialog = ref(null); -const cardDescriptorRef = ref(null); const showEditPhotoForm = ref(false); const toggleEditPictureForm = () => { showEditPhotoForm.value = !showEditPhotoForm.value; }; - const entityId = computed(() => { return $props.id || route.params.id; }); - -const worker = ref(); const workerExcluded = ref(false); const getIsExcluded = async () => { @@ -61,10 +56,10 @@ const handleExcluded = async () => { workerExcluded.value = !workerExcluded.value; }; + const handlePhotoUpdated = (evt = false) => { image.value.reload(evt); }; -const refetch = async () => await cardDescriptorRef.value.getData(); </script> <template> <CardDescriptor @@ -74,15 +69,10 @@ const refetch = async () => await cardDescriptorRef.value.getData(); url="Workers/descriptor" :filter="{ where: { id: entityId } }" title="user.nickname" - @on-fetch=" - (data) => { - worker = data; - getIsExcluded(); - } - " + @on-fetch="getIsExcluded" > - <template #menu="{}"> - <QItem v-ripple clickable @click="handleExcluded()"> + <template #menu="{ entity }"> + <QItem v-ripple clickable @click="handleExcluded"> <QItemSection> {{ workerExcluded @@ -92,16 +82,13 @@ const refetch = async () => await cardDescriptorRef.value.getData(); </QItemSection> </QItem> <QItem - v-if="!worker.user.emailVerified && user.id != worker.id" + v-if="!entity.user.emailVerified && user.id != entity.id" v-ripple clickable - @click="$refs.changePasswordFormDialog.show()" + @click="$refs.changePassRef.show" > <QItemSection> {{ t('Change password') }} - <QDialog ref="changePasswordFormDialog"> - <WorkerChangePasswordForm @on-submit="refetch()" :id="entityId" /> - </QDialog> </QItemSection> </QItem> </template> @@ -163,10 +150,10 @@ const refetch = async () => await cardDescriptorRef.value.getData(); <VnLinkPhone :phone-number="entity.phone" /> </template> </VnLv> - <VnLv :value="worker?.sip?.extension"> + <VnLv :value="entity?.sip?.extension"> <template #label> {{ t('worker.summary.sipExtension') }} - <VnLinkPhone :phone-number="worker?.sip?.extension" /> + <VnLinkPhone :phone-number="entity?.sip?.extension" /> </template> </VnLv> </template> @@ -197,6 +184,15 @@ const refetch = async () => await cardDescriptorRef.value.getData(); </QCardActions> </template> </CardDescriptor> + <VnChangePassword + ref="changePassRef" + :submit-fn=" + async (newPass) => { + await axios.patch(`Workers/${$route.params.id}/setPassword`, { newPass }); + } + " + @on-submit="$refs.cardDescriptorRef?.getData" + /> </template> <style lang="scss" scoped> From 91dccd10d370263d0e9c7087efd0b9e8156b2b8d Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 16 Sep 2024 10:39:39 +0200 Subject: [PATCH 06/12] feat: refs #7702 drop old components --- .../components/CustomerChangePassword.vue | 139 ------------------ .../Worker/Card/WorkerChangePasswordForm.vue | 99 ------------- 2 files changed, 238 deletions(-) delete mode 100644 src/pages/Customer/components/CustomerChangePassword.vue delete mode 100644 src/pages/Worker/Card/WorkerChangePasswordForm.vue diff --git a/src/pages/Customer/components/CustomerChangePassword.vue b/src/pages/Customer/components/CustomerChangePassword.vue deleted file mode 100644 index 632b11dc9..000000000 --- a/src/pages/Customer/components/CustomerChangePassword.vue +++ /dev/null @@ -1,139 +0,0 @@ -<script setup> -import { ref } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import axios from 'axios'; -import { useDialogPluginComponent } from 'quasar'; - -import useNotify from 'src/composables/useNotify'; - -import VnRow from 'components/ui/VnRow.vue'; -import VnInput from 'src/components/common/VnInput.vue'; -import FetchData from 'src/components/FetchData.vue'; - -const { dialogRef } = useDialogPluginComponent(); -const { notify } = useNotify(); -const { t } = useI18n(); - -const $props = defineProps({ - id: { - type: String, - required: true, - }, - promise: { - type: Function, - required: true, - }, -}); -const userPasswords = ref({}); - -const closeButton = ref(null); -const isLoading = ref(false); -const newPassword = ref(''); -const requestPassword = ref(''); - -const onSubmit = async () => { - isLoading.value = true; - - if (newPassword.value !== requestPassword.value) { - notify(t("Passwords don't match"), 'negative'); - isLoading.value = false; - return; - } - - const payload = { - newPassword: newPassword.value, - }; - try { - await axios.patch(`Clients/${$props.id}/setPassword`, payload); - await $props.promise(); - } catch (error) { - notify('errors.writeRequest', 'negative'); - } finally { - isLoading.value = false; - if (closeButton.value) closeButton.value.click(); - } -}; -</script> - -<template> - <QDialog ref="dialogRef"> - <FetchData - @on-fetch="(data) => (userPasswords = data[0])" - auto-load - url="UserPasswords" - /> - <QCard class="q-pa-lg"> - <QCardSection> - <QForm @submit.prevent="onSubmit"> - <span - ref="closeButton" - class="row justify-end close-icon" - v-close-popup - > - <QIcon name="close" size="sm" /> - </span> - - <VnRow class="row q-gutter-md q-mb-md" style="flex-direction: column"> - <div class="col"> - <VnInput - :label="t('New password')" - clearable - v-model="newPassword" - type="password" - > - <template #append> - <QIcon name="info" class="cursor-info"> - <QTooltip> - {{ - t('customer.card.passwordRequirements', { - ...userPasswords, - }) - }} - </QTooltip> - </QIcon> - </template> - </VnInput> - </div> - <div class="col"> - <VnInput - :label="t('Request password')" - clearable - v-model="requestPassword" - type="password" - /> - </div> - </VnRow> - - <div class="q-mt-lg row justify-end"> - <QBtn - :disabled="isLoading" - :label="t('globals.cancel')" - :loading="isLoading" - class="q-ml-sm" - color="primary" - flat - type="reset" - v-close-popup - /> - <QBtn - :disabled="isLoading" - :label="t('Change password')" - :loading="isLoading" - color="primary" - type="submit" - /> - </div> - </QForm> - </QCardSection> - </QCard> - </QDialog> -</template> - -<i18n> -es: - New password: Nueva contraseña - Request password: Repetir contraseña - Change password: Cambiar contraseña - Passwords don't match: Las contraseñas no coinciden -</i18n> diff --git a/src/pages/Worker/Card/WorkerChangePasswordForm.vue b/src/pages/Worker/Card/WorkerChangePasswordForm.vue deleted file mode 100644 index 20132f21e..000000000 --- a/src/pages/Worker/Card/WorkerChangePasswordForm.vue +++ /dev/null @@ -1,99 +0,0 @@ -<script setup> -import { ref, reactive, onMounted } from 'vue'; -import { useI18n } from 'vue-i18n'; - -import VnRow from 'components/ui/VnRow.vue'; -import FormPopup from 'src/components/FormPopup.vue'; -import VnInput from 'src/components/common/VnInput.vue'; - -import axios from 'axios'; -import useNotify from 'src/composables/useNotify.js'; - -const $props = defineProps({ - id: { - type: Object, - default: () => {}, - }, -}); - -const emit = defineEmits(['onSubmit']); - -const { t } = useI18n(); -const { notify } = useNotify(); - -const formData = reactive({ - newPassword: null, - repeatPassword: null, -}); - -const passRequirements = ref([]); - -const setPassword = async () => { - try { - if (!formData.newPassword) { - notify(t('You must enter a new password'), 'negative'); - return; - } - - if (formData.newPassword != formData.repeatPassword) { - notify(t(`Passwords don't match`), 'negative'); - return; - } - - await axios.patch(`Workers/${$props.id}/setPassword`, { - newPass: formData.newPassword, - }); - notify(t('Password changed!'), 'positive'); - emit('onSubmit'); - } catch (err) { - console.error('Error setting password', err); - } -}; - -const getPassRequirements = async () => { - const { data } = await axios.get('UserPasswords/findOne'); - passRequirements.value = data; -}; - -onMounted(async () => await getPassRequirements()); -</script> - -<template> - <FormPopup :title="t('Reset password')" @on-submit="setPassword()"> - <template #form-inputs> - <VnRow> - <VnInput - :label="t('New password')" - v-model="formData.newPassword" - type="password" - :required="true" - :info=" - t('passwordRequirements', { - length: passRequirements.length, - nAlpha: passRequirements.nAlpha, - nUpper: passRequirements.nUpper, - nDigits: passRequirements.nDigits, - nPunct: passRequirements.nPunct, - }) - " - /> - </VnRow> - <VnRow> - <VnInput - :label="t('Repeat password')" - v-model="formData.repeatPassword" - type="password" - /> - </VnRow> - </template> - </FormPopup> -</template> - -<i18n> -es: - Reset password: Restablecer contraseña - New password: Nueva contraseña - Repeat password: Repetir contraseña - You must enter a new password: Debes introducir la nueva contraseña - Passwords don't match: Las contraseñas no coinciden -</i18n> From b015397822a7c0212751672d5fa7d7a5761bb4ce Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Mon, 16 Sep 2024 10:59:32 +0200 Subject: [PATCH 07/12] feat: refs #7702 test wip --- .../common/VnChangePassword.spec.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 test/vitest/__tests__/components/common/VnChangePassword.spec.js diff --git a/test/vitest/__tests__/components/common/VnChangePassword.spec.js b/test/vitest/__tests__/components/common/VnChangePassword.spec.js new file mode 100644 index 000000000..a481efa81 --- /dev/null +++ b/test/vitest/__tests__/components/common/VnChangePassword.spec.js @@ -0,0 +1,47 @@ +import { createWrapper } from 'app/test/vitest/helper'; +import VnChangePassword from 'src/components/common/VnChangePassword.vue'; +import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest'; + +describe('VnSmsDialog', () => { + let vm; + + beforeAll(() => { + vm = createWrapper(VnChangePassword, { + propsData: { + submitFn: vi.fn(), + }, + }).vm; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should notify when new password is empty', async () => { + vm.passwords.newPassword = ''; + vm.passwords.repeatPassword = 'password'; + vi.spyOn(vm, 'notify'); + await vm.validate(); + expect(vm.notify).toHaveBeenCalledWith( + expect.arguments({ + message: 'You must enter a new password', + type: 'negative', + }) + ); + }); + + it("should notify when passwords don't match", async () => { + vm.passwords.newPassword = 'password1'; + vm.passwords.repeatPassword = 'password2'; + vi.spyOn(vm, 'notify'); + await vm.validate(); + expect(vm.notify).toHaveBeenCalledWith("Passwords don't match", 'negative'); + }); + + it('should call submitFn and emit onSubmit when passwords match', async () => { + vm.passwords.newPassword = 'password'; + vm.passwords.repeatPassword = 'password'; + await vm.validate(); + expect(vm.props.submitFn).toHaveBeenCalledWith('password'); + }); +}); From 6cc8ca6731e0b691d0862dad0953366a8bf32d08 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 20 Sep 2024 17:38:02 +0200 Subject: [PATCH 08/12] chore: refs #7702 add tests --- src/components/common/VnChangePassword.vue | 6 +++++ .../common/VnChangePassword.spec.js | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/components/common/VnChangePassword.vue b/src/components/common/VnChangePassword.vue index a36c6bcff..18f538fc3 100644 --- a/src/components/common/VnChangePassword.vue +++ b/src/components/common/VnChangePassword.vue @@ -19,6 +19,8 @@ const isLoading = ref(false); const validate = async () => { const { newPassword, repeatPassword } = passwords.value; + console.log('Validating passwords:', newPassword, repeatPassword); + if (!newPassword) { notify(t('You must enter a new password'), 'negative'); return; @@ -29,11 +31,15 @@ const validate = async () => { } try { isLoading.value = true; + console.log('Calling submitFn with:', newPassword); + await props.submitFn(newPassword); emit('onSubmit'); } catch (e) { + console.error('submitFn failed:', e); notify('errors.writeRequest', 'negative'); } finally { + console.log('Entering finally block'); changePassDialog.value.hide(); isLoading.value = false; } diff --git a/test/vitest/__tests__/components/common/VnChangePassword.spec.js b/test/vitest/__tests__/components/common/VnChangePassword.spec.js index a481efa81..e7a3bdcf7 100644 --- a/test/vitest/__tests__/components/common/VnChangePassword.spec.js +++ b/test/vitest/__tests__/components/common/VnChangePassword.spec.js @@ -1,11 +1,15 @@ -import { createWrapper } from 'app/test/vitest/helper'; +import { createWrapper, axios } from 'app/test/vitest/helper'; import VnChangePassword from 'src/components/common/VnChangePassword.vue'; -import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { vi, beforeEach, afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { Notify } from 'quasar'; describe('VnSmsDialog', () => { let vm; beforeAll(() => { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [], + }); vm = createWrapper(VnChangePassword, { propsData: { submitFn: vi.fn(), @@ -13,6 +17,10 @@ describe('VnSmsDialog', () => { }).vm; }); + beforeEach(() => { + Notify.create = vi.fn(); + }); + afterEach(() => { vi.clearAllMocks(); }); @@ -20,10 +28,10 @@ describe('VnSmsDialog', () => { it('should notify when new password is empty', async () => { vm.passwords.newPassword = ''; vm.passwords.repeatPassword = 'password'; - vi.spyOn(vm, 'notify'); + await vm.validate(); - expect(vm.notify).toHaveBeenCalledWith( - expect.arguments({ + expect(Notify.create).toHaveBeenCalledWith( + expect.objectContaining({ message: 'You must enter a new password', type: 'negative', }) @@ -33,9 +41,13 @@ describe('VnSmsDialog', () => { it("should notify when passwords don't match", async () => { vm.passwords.newPassword = 'password1'; vm.passwords.repeatPassword = 'password2'; - vi.spyOn(vm, 'notify'); await vm.validate(); - expect(vm.notify).toHaveBeenCalledWith("Passwords don't match", 'negative'); + expect(Notify.create).toHaveBeenCalledWith( + expect.objectContaining({ + message: `Passwords don't match`, + type: 'negative', + }) + ); }); it('should call submitFn and emit onSubmit when passwords match', async () => { From 05a1d0e8fbb0a70d8630e72cf6c5952c3ecd3327 Mon Sep 17 00:00:00 2001 From: pablone <pablone@verdnatura.es> Date: Thu, 3 Oct 2024 10:52:16 +0200 Subject: [PATCH 09/12] fix: refs #7129 translates from globals to module locals --- src/i18n/locale/en.yml | 30 +----------------------------- src/i18n/locale/es.yml | 15 --------------- src/pages/Route/locale/en.yml | 14 ++++++++++++++ src/pages/Route/locale/es.yml | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 20a612a82..4c559dfdf 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -870,35 +870,7 @@ wagon: minHeightBetweenTrays: 'The minimum height between trays is ' maxWagonHeight: 'The maximum height of the wagon is ' uncompleteTrays: There are incomplete trays -route: - pageTitles: - agency: Agency List - routes: Routes - cmrsList: CMRs list - RouteList: List - routeCreate: New route - basicData: Basic Data - summary: Summary - RouteRoadmap: Roadmaps - RouteRoadmapCreate: Create roadmap - tickets: Tickets - log: Log - autonomous: Autonomous - RouteExtendedList: Router - cmr: - list: - results: results - cmrFk: CMR id - hasCmrDms: Attached in gestdoc - 'true': 'Yes' - 'false': 'No' - ticketFk: Ticketd id - routeFk: Route id - country: Country - clientFk: Client id - shipped: Preparation date - viewCmr: View CMR - downloadCmrs: Download CMRs + supplier: list: payMethod: Pay method diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index ae0274415..7cb17aad3 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -868,21 +868,6 @@ wagon: minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' maxWagonHeight: 'La altura máxima del vagón es ' uncompleteTrays: Hay bandejas sin completar -route: - cmr: - list: - results: resultados - cmrFk: Id CMR - hasCmrDms: Gestdoc - 'true': Sí - 'false': 'No' - ticketFk: Id ticket - routeFk: Id ruta - country: País - clientFk: Id cliente - shipped: Fecha preparación - viewCmr: Ver CMR - downloadCmrs: Descargar CMRs supplier: list: payMethod: Método de pago diff --git a/src/pages/Route/locale/en.yml b/src/pages/Route/locale/en.yml index 617d704d2..d113fda67 100644 --- a/src/pages/Route/locale/en.yml +++ b/src/pages/Route/locale/en.yml @@ -23,3 +23,17 @@ route: Summary: Summary Route is closed: Route is closed Route is not served: Route is not served + cmr: + list: + results: results + cmrFk: CMR id + hasCmrDms: Attached in gestdoc + 'true': 'Yes' + 'false': 'No' + ticketFk: Ticketd id + routeFk: Route id + country: Country + clientFk: Client id + shipped: Preparation date + viewCmr: View CMR + downloadCmrs: Download CMRs diff --git a/src/pages/Route/locale/es.yml b/src/pages/Route/locale/es.yml index ed96ad915..a6ba4f370 100644 --- a/src/pages/Route/locale/es.yml +++ b/src/pages/Route/locale/es.yml @@ -23,3 +23,17 @@ route: Summary: Resumen Route is closed: La ruta está cerrada Route is not served: La ruta no está servida + cmr: + list: + results: resultados + cmrFk: Id CMR + hasCmrDms: Gestdoc + 'true': Sí + 'false': 'No' + ticketFk: Id ticket + routeFk: Id ruta + country: País + clientFk: Id cliente + shipped: Fecha preparación + viewCmr: Ver CMR + downloadCmrs: Descargar CMRs From c04dad38f12a36aa22d85a7c8e459cbbf01bf495 Mon Sep 17 00:00:00 2001 From: pablone <pablone@verdnatura.es> Date: Thu, 3 Oct 2024 15:02:14 +0200 Subject: [PATCH 10/12] feat: refs #7404 change travel name and remove buyer filter --- src/pages/Entry/EntryStockBought.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/Entry/EntryStockBought.vue b/src/pages/Entry/EntryStockBought.vue index 0b31dde17..2b5ec53f5 100644 --- a/src/pages/Entry/EntryStockBought.vue +++ b/src/pages/Entry/EntryStockBought.vue @@ -45,6 +45,7 @@ const columns = [ optionValue: 'id', useLike: false, }, + columnFilter: false, }, { align: 'center', @@ -157,7 +158,7 @@ function round(value) { @on-fetch=" (data) => { travel = data.find( - (data) => data.warehouseIn.code.toLowerCase() === 'vnh' + (data) => data.warehouseIn?.code.toLowerCase() === 'vnh' ); } " @@ -165,7 +166,7 @@ function round(value) { <VnRow class="travel"> <div v-if="travel"> <span style="color: var(--vn-label-color)"> - {{ t('Booked trucks') }}: + {{ t('Purchase Spaces') }}: </span> <span> {{ travel?.m3 }} @@ -236,6 +237,7 @@ function round(value) { :footer="true" table-height="80vh" auto-load + :column-search="false" > <template #column-workerFk="{ row }"> <span class="link" @click.stop> @@ -288,7 +290,7 @@ function round(value) { es: Edit travel: Editar envío Travel: Envíos - Booked trucks: Camiones reservados + Purchase Spaces: Espacios de compra Buyer: Comprador Reserve: Reservado Bought: Comprado From 67c2e284ba52d4d5df4338d50b05385bf65916c7 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 4 Oct 2024 11:45:10 +0200 Subject: [PATCH 11/12] feat: refs #7702 ask old pass --- src/components/common/VnChangePassword.vue | 26 ++++++++++------- src/i18n/locale/en.yml | 1 + src/i18n/locale/es.yml | 1 + .../Account/Card/AccountDescriptorMenu.vue | 28 ++++++++++++++++++- src/pages/Customer/Card/CustomerWebAccess.vue | 3 +- src/pages/Worker/Card/WorkerDescriptor.vue | 3 +- 6 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/components/common/VnChangePassword.vue b/src/components/common/VnChangePassword.vue index 18f538fc3..79784f3c5 100644 --- a/src/components/common/VnChangePassword.vue +++ b/src/components/common/VnChangePassword.vue @@ -6,7 +6,10 @@ import VnInput from './VnInput.vue'; import FetchData from '../FetchData.vue'; import useNotify from 'src/composables/useNotify'; -const props = defineProps({ submitFn: { type: Function, default: () => {} } }); +const props = defineProps({ + submitFn: { type: Function, default: () => {} }, + askOldPass: { type: Boolean, default: false }, +}); const emit = defineEmits(['onSubmit']); const { t } = useI18n(); const { notify } = useNotify(); @@ -18,8 +21,7 @@ const requirements = ref([]); const isLoading = ref(false); const validate = async () => { - const { newPassword, repeatPassword } = passwords.value; - console.log('Validating passwords:', newPassword, repeatPassword); + const { newPassword, repeatPassword, oldPassword } = passwords.value; if (!newPassword) { notify(t('You must enter a new password'), 'negative'); @@ -29,17 +31,14 @@ const validate = async () => { notify(t("Passwords don't match"), 'negative'); return; } + try { isLoading.value = true; - console.log('Calling submitFn with:', newPassword); - - await props.submitFn(newPassword); + await props.submitFn(newPassword, oldPassword); emit('onSubmit'); } catch (e) { - console.error('submitFn failed:', e); notify('errors.writeRequest', 'negative'); } finally { - console.log('Entering finally block'); changePassDialog.value.hide(); isLoading.value = false; } @@ -58,7 +57,7 @@ defineExpose({ show: () => changePassDialog.value.show() }); <QCardSection> <slot name="header"> <VnRow class="items-center" style="flex-direction: row"> - <span class="text-h6" v-text="t('Change password')" /> + <span class="text-h6" v-text="t('globals.changePass')" /> <QIcon class="cursor-pointer" name="close" @@ -71,6 +70,14 @@ defineExpose({ show: () => changePassDialog.value.show() }); </QCardSection> <QForm ref="form"> <QCardSection> + <VnInput + v-if="props.askOldPass" + :label="t('Old password')" + v-model="passwords.oldPassword" + type="password" + :required="true" + autofocus + /> <VnInput :label="t('New password')" v-model="passwords.newPassword" @@ -122,7 +129,6 @@ defineExpose({ show: () => changePassDialog.value.show() }); <i18n> es: - Change password: Cambiar contraseña New password: Nueva contraseña Repeat password: Repetir contraseña You must enter a new password: Debes introducir la nueva contraseña diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml index 4c559dfdf..b76c147cd 100644 --- a/src/i18n/locale/en.yml +++ b/src/i18n/locale/en.yml @@ -300,6 +300,7 @@ globals: from: From To: To stateFk: State + changePass: Change password errors: statusUnauthorized: Access denied statusInternalServerError: An internal server error has ocurred diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml index 7cb17aad3..d0d5bdc0d 100644 --- a/src/i18n/locale/es.yml +++ b/src/i18n/locale/es.yml @@ -304,6 +304,7 @@ globals: from: Desde To: Hasta stateFk: Estado + changePass: Cambiar contraseña errors: statusUnauthorized: Acceso denegado statusInternalServerError: Ha ocurrido un error interno del servidor diff --git a/src/pages/Account/Card/AccountDescriptorMenu.vue b/src/pages/Account/Card/AccountDescriptorMenu.vue index 0e35d25f3..6f1d2ca1f 100644 --- a/src/pages/Account/Card/AccountDescriptorMenu.vue +++ b/src/pages/Account/Card/AccountDescriptorMenu.vue @@ -4,9 +4,12 @@ import { computed, ref, toRefs } from 'vue'; import { useI18n } from 'vue-i18n'; import { useVnConfirm } from 'composables/useVnConfirm'; import { useRoute } from 'vue-router'; +import { useAcl } from 'src/composables/useAcl'; import { useArrayData } from 'src/composables/useArrayData'; import VnConfirm from 'src/components/ui/VnConfirm.vue'; +import VnChangePassword from 'src/components/common/VnChangePassword.vue'; import useNotify from 'src/composables/useNotify.js'; + const $props = defineProps({ hasAccount: { type: Boolean, @@ -62,6 +65,19 @@ async function sync() { } </script> <template> + <VnChangePassword + ref="changePassRef" + :ask-old-pass="true" + :submit-fn=" + async (newPassword, oldPassword) => { + await axios.patch(`Accounts/change-password`, { + userId: entityId, + newPassword, + oldPassword, + }); + } + " + /> <VnConfirm v-model="showSyncDialog" :message="t('account.card.actions.sync.message')" @@ -92,6 +108,17 @@ async function sync() { /> </template> </VnConfirm> + <QItem + v-if=" + entityId == account.id && + useAcl().hasAny([{ model: 'Account', props: '*', accessType: 'WRITE' }]) + " + v-ripple + clickable + @click="$refs.changePassRef.show()" + > + <QItemSection>{{ t('globals.changePass') }}</QItemSection> + </QItem> <QItem v-if="account.hasAccount" v-ripple @@ -138,6 +165,5 @@ async function sync() { <QItem v-ripple clickable @click="showSyncDialog = true"> <QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection> </QItem> - <QSeparator /> </template> diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index ba906a144..1db32c752 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -56,7 +56,7 @@ async function hasCustomerRole() { </template> <template #moreActions> <QBtn - :label="t('Change password')" + :label="t('globals.changePass')" color="primary" icon="edit" :disable="!canChangePassword" @@ -81,5 +81,4 @@ es: User: Usuario Recovery email: Correo de recuperacion This email is used for user to regain access their account: Este correo electrónico se usa para que el usuario recupere el acceso a su cuenta - Change password: Cambiar contraseña </i18n> diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 6a70e46f5..1cb42bbfb 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -88,7 +88,7 @@ const handlePhotoUpdated = (evt = false) => { @click="$refs.changePassRef.show" > <QItemSection> - {{ t('Change password') }} + {{ t('globals.changePass') }} </QItemSection> </QItem> </template> @@ -209,5 +209,4 @@ const handlePhotoUpdated = (evt = false) => { es: Click to allow the user to be disabled: Marcar para deshabilitar Click to exclude the user from getting disabled: Marcar para no deshabilitar - Change password: Cambiar contraseña </i18n> From ae8e4ba4b975b1c6381fabec08de18cbc190f978 Mon Sep 17 00:00:00 2001 From: jorgep <jorgep@verdnatura.es> Date: Fri, 4 Oct 2024 11:54:40 +0200 Subject: [PATCH 12/12] chore: refs #7702 fix tests --- .../common/VnChangePassword.spec.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/test/vitest/__tests__/components/common/VnChangePassword.spec.js b/test/vitest/__tests__/components/common/VnChangePassword.spec.js index e7a3bdcf7..f5a967bb5 100644 --- a/test/vitest/__tests__/components/common/VnChangePassword.spec.js +++ b/test/vitest/__tests__/components/common/VnChangePassword.spec.js @@ -50,10 +50,21 @@ describe('VnSmsDialog', () => { ); }); - it('should call submitFn and emit onSubmit when passwords match', async () => { - vm.passwords.newPassword = 'password'; - vm.passwords.repeatPassword = 'password'; - await vm.validate(); - expect(vm.props.submitFn).toHaveBeenCalledWith('password'); + describe('if passwords match', () => { + it('should call submitFn and emit password', async () => { + vm.passwords.newPassword = 'password'; + vm.passwords.repeatPassword = 'password'; + await vm.validate(); + expect(vm.props.submitFn).toHaveBeenCalledWith('password', undefined); + }); + + it('should call submitFn and emit password and old password', async () => { + vm.passwords.newPassword = 'password'; + vm.passwords.repeatPassword = 'password'; + vm.passwords.oldPassword = 'oldPassword'; + + await vm.validate(); + expect(vm.props.submitFn).toHaveBeenCalledWith('password', 'oldPassword'); + }); }); });