<script setup> import axios from 'axios'; import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; import { onBeforeRouteLeave, useRouter } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useQuasar } from 'quasar'; import { useState } from 'src/composables/useState'; import { useStateStore } from 'stores/useStateStore'; import { useValidator } from 'src/composables/useValidator'; import useNotify from 'src/composables/useNotify.js'; import SkeletonForm from 'components/ui/SkeletonForm.vue'; import VnConfirm from './ui/VnConfirm.vue'; import { tMobile } from 'src/composables/tMobile'; import { useArrayData } from 'src/composables/useArrayData'; import { useRoute } from 'vue-router'; const { push } = useRouter(); const quasar = useQuasar(); const state = useState(); const stateStore = useStateStore(); const { t } = useI18n(); const { validate } = useValidator(); const { notify } = useNotify(); const route = useRoute(); const $props = defineProps({ url: { type: String, default: '', }, model: { type: String, default: null, }, filter: { type: Object, default: null, }, urlUpdate: { type: String, default: null, }, urlCreate: { type: String, default: null, }, defaultActions: { type: Boolean, default: true, }, defaultButtons: { type: Object, default: () => {}, }, autoLoad: { type: Boolean, default: false, }, formInitialData: { type: Object, default: () => {}, }, 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, }, clearStoreOnUnmount: { type: Boolean, default: true, }, saveFn: { type: Function, default: null, }, goTo: { type: String, default: '', description: 'It is used for redirect on click "save and continue"', }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); const modelValue = computed( () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}` ).value; const componentIsRendered = ref(false); const arrayData = useArrayData(modelValue); const isLoading = ref(false); // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas const isResetting = ref(false); const hasChanges = ref(!$props.observeFormChanges); const originalData = ref({}); const formData = computed(() => state.get(modelValue)); const formUrl = computed(() => $props.url); const defaultButtons = computed(() => ({ save: { color: 'primary', icon: 'save', label: 'globals.save', }, reset: { color: 'primary', icon: 'restart_alt', label: 'globals.reset', }, ...$props.defaultButtons, })); onMounted(async () => { originalData.value = JSON.parse(JSON.stringify($props.formInitialData ?? {})); nextTick(() => (componentIsRendered.value = true)); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla state.set(modelValue, $props.formInitialData); if (!$props.formInitialData) { if ($props.autoLoad && $props.url) await fetch(); else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); } 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 } ); } }); if (!$props.url) watch( () => arrayData.store.data, (val) => updateAndEmit('onFetch', val) ); watch(formUrl, async () => { originalData.value = null; reset(); await fetch(); }); onBeforeRouteLeave((to, from, next) => { if (hasChanges.value && $props.observeFormChanges) quasar.dialog({ component: VnConfirm, componentProps: { title: t('Unsaved changes will be lost'), message: t('Are you sure exit without saving?'), promise: () => next(), }, }); else next(); }); onUnmounted(() => { // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas. if (hasChanges.value) return state.set(modelValue, originalData.value); if ($props.clearStoreOnUnmount) state.unset(modelValue); }); async function fetch() { try { let { data } = await axios.get($props.url, { params: { filter: JSON.stringify($props.filter) }, }); if (Array.isArray(data)) data = data[0] ?? {}; updateAndEmit('onFetch', data); } catch (e) { state.set(modelValue, {}); originalData.value = {}; } } async function save() { if ($props.observeFormChanges && !hasChanges.value) return notify('globals.noChanges', 'negative'); isLoading.value = true; try { const body = $props.mapper ? $props.mapper(formData.value) : formData.value; const method = $props.urlCreate ? 'post' : 'patch'; const url = $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; let response; if ($props.saveFn) response = await $props.saveFn(body); else response = await axios[method](url, body); if ($props.urlCreate) notify('globals.dataCreated', 'positive'); updateAndEmit('onDataSaved', formData.value, response?.data); } catch (err) { console.error(err); notify('errors.writeRequest', 'negative'); } finally { hasChanges.value = false; isLoading.value = false; } } async function saveAndGo() { await save(); push({ path: $props.goTo }); } function reset() { updateAndEmit('onFetch', originalData.value); if ($props.observeFormChanges) { hasChanges.value = false; isResetting.value = true; } } // eslint-disable-next-line vue/no-dupe-keys function filter(value, update, filterOptions) { update( () => { const { options, filterFn } = filterOptions; options.value = filterFn(options, value); }, (ref) => { ref.setOptionIndex(-1); ref.moveOptionSelection(1, true); } ); } function updateAndEmit(evt, val, res) { state.set(modelValue, val); originalData.value = val && JSON.parse(JSON.stringify(val)); if (!$props.url) arrayData.store.data = val; emit(evt, state.get(modelValue), res); } defineExpose({ save, isLoading, hasChanges, reset, fetch, }); </script> <template> <div class="column items-center full-width"> <QForm v-if="formData" @submit="save" @reset="reset" class="q-pa-md" id="formModel" > <QCard> <slot name="form" :data="formData" :validate="validate" :filter="filter" /> </QCard> </QForm> </div> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && componentIsRendered" > <div v-if="$props.defaultActions"> <QBtnGroup push class="q-gutter-x-sm"> <slot name="moreActions" /> <QBtn :label="tMobile(defaultButtons.reset.label)" :color="defaultButtons.reset.color" :icon="defaultButtons.reset.icon" flat @click="reset" :disable="!hasChanges" :title="t(defaultButtons.reset.label)" /> <QBtnDropdown v-if="$props.goTo" @click="saveAndGo" :label="tMobile('globals.saveAndContinue')" :title="t('globals.saveAndContinue')" :disable="!hasChanges" color="primary" icon="save" split > <QList> <QItem clickable v-close-popup @click="save" :title="t('globals.save')" > <QItemSection> <QItemLabel> <QIcon name="save" color="white" class="q-mr-sm" size="sm" /> {{ t('globals.save').toUpperCase() }} </QItemLabel> </QItemSection> </QItem> </QList> </QBtnDropdown> <QBtn v-else :label="tMobile('globals.save')" color="primary" icon="save" @click="save" :disable="!hasChanges" :title="t(defaultButtons.save.label)" /> </QBtnGroup> </div> </Teleport> <SkeletonForm v-if="!formData" /> <QInnerLoading :showing="isLoading" :label="t('globals.pleaseWait')" color="primary" style="min-width: 100%" /> </template> <style lang="scss" scoped> .q-notifications { color: black; } #formModel { max-width: 800px; width: 100%; } .q-card { padding: 32px; } </style> <i18n> es: Unsaved changes will be lost: Los cambios que no haya guardado se perderán Are you sure exit without saving?: ¿Seguro que quiere salir sin guardar? </i18n>