Address views refactor
This commit is contained in:
parent
47c61a7b35
commit
bd1e9b7037
|
@ -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'],
|
||||
|
|
|
@ -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'
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
<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 addressFormRef = 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;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
console.log('submit: ');
|
||||
if (props.observeFormChanges && !hasChanges.value)
|
||||
return notify('globals.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;
|
||||
|
||||
let response;
|
||||
|
||||
if (props.saveFn) await props.saveFn(body);
|
||||
else await api[method](url, body);
|
||||
|
||||
onSubmitSuccess();
|
||||
hasChanges.value = false;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formData,
|
||||
submit
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<QCard class="form-container" v-bind="$attrs">
|
||||
<QForm
|
||||
v-if="!isLoading"
|
||||
ref="addressFormRef"
|
||||
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
|
||||
:disabled="!showBottomActions && !hasChanges"
|
||||
@click="submit()"
|
||||
data-cy="formModelDefaultSaveButton"
|
||||
>
|
||||
<QTooltip>{{ t('save') }}</QTooltip>
|
||||
</QBtn>
|
||||
<slot name="actions" :data="formData" />
|
||||
</component>
|
||||
</QForm>
|
||||
<QSpinner v-else color="primary" size="3em" :thickness="2" />
|
||||
</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>
|
|
@ -5,32 +5,26 @@ import { useRouter, useRoute } from 'vue-router';
|
|||
|
||||
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 FormModel from 'src/components/common/FormModel.vue';
|
||||
|
||||
import { useUserStore } from 'stores/user';
|
||||
import { useAppStore } from 'stores/app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
const jApi = inject('jApi');
|
||||
const api = inject('api');
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { isHeaderMounted } = storeToRefs(appStore);
|
||||
|
||||
const vnFormRef = ref(null);
|
||||
const countriesOptions = ref([]);
|
||||
const provincesOptions = ref([]);
|
||||
const pks = { id: route.params.id };
|
||||
const isEditMode = route.params.id !== '0';
|
||||
const fetchAddressDataSql = {
|
||||
query: `
|
||||
SELECT a.id, a.street, a.nickname, a.city, a.postalCode, a.provinceFk, p.countryFk
|
||||
FROM myAddress a
|
||||
LEFT JOIN vn.province p ON p.id = a.provinceFk
|
||||
WHERE a.id = #address
|
||||
`,
|
||||
params: { address: route.params.id }
|
||||
};
|
||||
const editAddressData = ref(null);
|
||||
const showForm = ref(false);
|
||||
|
||||
watch(
|
||||
() => vnFormRef?.value?.formData?.countryFk,
|
||||
|
@ -40,23 +34,49 @@ watch(
|
|||
const goBack = () => router.push({ name: 'addressesList' });
|
||||
|
||||
const getCountries = async () => {
|
||||
countriesOptions.value = await jApi.query(
|
||||
`SELECT id, name FROM vn.country
|
||||
ORDER BY name`
|
||||
);
|
||||
const filter = { fields: ['id', 'name'], order: 'name' };
|
||||
const { data } = await api.get('Countries', {
|
||||
params: { filter: JSON.stringify(filter) }
|
||||
});
|
||||
countriesOptions.value = data;
|
||||
};
|
||||
|
||||
const getProvinces = async countryFk => {
|
||||
if (!countryFk) return;
|
||||
provincesOptions.value = await jApi.query(
|
||||
`SELECT id, name FROM vn.province
|
||||
WHERE countryFk = #id
|
||||
ORDER BY name`,
|
||||
{ id: countryFk }
|
||||
);
|
||||
|
||||
const filter = {
|
||||
where: { countryFk },
|
||||
fields: ['id', 'name'],
|
||||
order: 'name'
|
||||
};
|
||||
const { data } = await api.get('Provinces', {
|
||||
params: { filter: JSON.stringify(filter) }
|
||||
});
|
||||
provincesOptions.value = data;
|
||||
};
|
||||
|
||||
onMounted(() => getCountries());
|
||||
const getAddressDetails = async () => {
|
||||
const { data } = await api.get(`Addresses/${route.params.id}`);
|
||||
if (!data) return;
|
||||
|
||||
const { nickname, street, city, postalCode, province, provinceFk } = data;
|
||||
editAddressData.value = {
|
||||
nickname,
|
||||
street,
|
||||
city,
|
||||
postalCode,
|
||||
countryFk: province?.countryFk,
|
||||
provinceFk
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (isEditMode) {
|
||||
await getAddressDetails();
|
||||
}
|
||||
getCountries();
|
||||
showForm.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -74,19 +94,21 @@ onMounted(() => getCountries());
|
|||
</QTooltip>
|
||||
</QBtn>
|
||||
</Teleport>
|
||||
<VnForm
|
||||
<FormModel
|
||||
v-if="showForm"
|
||||
ref="vnFormRef"
|
||||
:fetch-form-data-sql="fetchAddressDataSql"
|
||||
:columns-to-ignore-update="['countryFk']"
|
||||
:create-model-default="{
|
||||
field: 'clientFk',
|
||||
value: 'account.myUser_getId()'
|
||||
}"
|
||||
:pks="pks"
|
||||
:is-edit-mode="isEditMode"
|
||||
:url-create="
|
||||
!isEditMode
|
||||
? `Clients/${userStore?.user?.id}/createAddress`
|
||||
: ''
|
||||
"
|
||||
:url-update="
|
||||
isEditMode
|
||||
? `Clients/${userStore?.user?.id}/updateAddress/${route?.params?.id}`
|
||||
: ''
|
||||
"
|
||||
:form-initial-data="editAddressData"
|
||||
:title="t(isEditMode ? 'editAddress' : 'addAddress')"
|
||||
table="myAddress"
|
||||
schema="hedera"
|
||||
@on-data-saved="goBack()"
|
||||
>
|
||||
<template #form="{ data }">
|
||||
|
@ -125,7 +147,7 @@ onMounted(() => getCountries());
|
|||
data-cy="addressFormProvince"
|
||||
/>
|
||||
</template>
|
||||
</VnForm>
|
||||
</FormModel>
|
||||
</QPage>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -10,13 +10,16 @@ import useNotify from 'src/composables/useNotify.js';
|
|||
import { useVnConfirm } from 'src/composables/useVnConfirm.js';
|
||||
import { useAppStore } from 'stores/app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useUserStore } from 'stores/user';
|
||||
|
||||
const router = useRouter();
|
||||
const jApi = inject('jApi');
|
||||
const api = inject('api');
|
||||
const { notify } = useNotify();
|
||||
const { t } = useI18n();
|
||||
const { openConfirmationModal } = useVnConfirm();
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const { isHeaderMounted } = storeToRefs(appStore);
|
||||
|
||||
const addresses = ref([]);
|
||||
|
@ -65,16 +68,13 @@ const changeDefaultAddress = async () => {
|
|||
notify(t('defaultAddressModified'), 'positive');
|
||||
};
|
||||
|
||||
const removeAddress = async id => {
|
||||
async function removeAddress(address) {
|
||||
try {
|
||||
await jApi.execQuery(
|
||||
`START TRANSACTION;
|
||||
UPDATE hedera.myAddress SET isActive = FALSE
|
||||
WHERE ((id = #id));
|
||||
SELECT isActive FROM hedera.myAddress WHERE ((id = #id));
|
||||
COMMIT`,
|
||||
await api.patch(
|
||||
`/Clients/${userStore?.user?.id}/updateAddress/${address.id}`,
|
||||
{
|
||||
id
|
||||
...address,
|
||||
isActive: false
|
||||
}
|
||||
);
|
||||
getActiveAddresses();
|
||||
|
@ -82,7 +82,7 @@ const removeAddress = async id => {
|
|||
} catch (error) {
|
||||
console.error('Error removing address:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
getDefaultAddress();
|
||||
|
@ -145,7 +145,7 @@ onMounted(async () => {
|
|||
openConfirmationModal(
|
||||
null,
|
||||
t('confirmDeleteAddress'),
|
||||
() => removeAddress(address.id)
|
||||
() => removeAddress(address)
|
||||
)
|
||||
"
|
||||
>
|
||||
|
|
|
@ -46,15 +46,28 @@ describe('PendingOrders', () => {
|
|||
.should('contain', data.postcode);
|
||||
};
|
||||
|
||||
it('should fail if we enter a wrong postcode', () => {
|
||||
cy.dataCy('newAddressBtn').should('exist');
|
||||
cy.dataCy('newAddressBtn').click();
|
||||
cy.dataCy('formModelDefaultSaveButton').should('exist');
|
||||
cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
|
||||
const addressFormData = getRandomAddressFormData();
|
||||
fillFormWithData(addressFormData);
|
||||
cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
|
||||
cy.dataCy('formModelDefaultSaveButton').click();
|
||||
cy.checkNotify('negative');
|
||||
});
|
||||
|
||||
it('should create a new address', () => {
|
||||
cy.dataCy('newAddressBtn').should('exist');
|
||||
cy.dataCy('newAddressBtn').click();
|
||||
cy.dataCy('formDefaultSaveButton').should('exist');
|
||||
cy.dataCy('formDefaultSaveButton').should('be.disabled');
|
||||
cy.dataCy('formModelDefaultSaveButton').should('exist');
|
||||
cy.dataCy('formModelDefaultSaveButton').should('be.disabled');
|
||||
const addressFormData = getRandomAddressFormData();
|
||||
addressFormData.postcode = '46460'; // Usamos un postcode válido
|
||||
fillFormWithData(addressFormData);
|
||||
cy.dataCy('formDefaultSaveButton').should('not.be.disabled');
|
||||
cy.dataCy('formDefaultSaveButton').click();
|
||||
cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
|
||||
cy.dataCy('formModelDefaultSaveButton').click();
|
||||
cy.checkNotify('positive', 'Datos guardados');
|
||||
verifyAddressCardData(addressFormData);
|
||||
});
|
||||
|
@ -71,9 +84,10 @@ describe('PendingOrders', () => {
|
|||
});
|
||||
// Fill form with new data
|
||||
const addressFormData = getRandomAddressFormData();
|
||||
addressFormData.postcode = '46460'; // Usamos un postcode válido
|
||||
fillFormWithData(addressFormData);
|
||||
cy.dataCy('formDefaultSaveButton').should('not.be.disabled');
|
||||
cy.dataCy('formDefaultSaveButton').click();
|
||||
cy.dataCy('formModelDefaultSaveButton').should('not.be.disabled');
|
||||
cy.dataCy('formModelDefaultSaveButton').click();
|
||||
cy.checkNotify('positive', 'Datos guardados');
|
||||
verifyAddressCardData(addressFormData);
|
||||
});
|
||||
|
|
|
@ -90,5 +90,6 @@ Cypress.Commands.add('setConfirmDialog', () => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('checkNotify', (status, content) => {
|
||||
cy.dataCy(`${status}Notify`).should('contain', content);
|
||||
if (content) cy.dataCy(`${status}Notify`).should('contain', content);
|
||||
else cy.dataCy(`${status}Notify`).should('exist');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue