<script setup> import axios from 'axios'; import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue'; import { onBeforeRouteLeave } 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'; const quasar = useQuasar(); const state = useState(); const stateStore = useStateStore(); const { t } = useI18n(); const { validate } = useValidator(); const { notify } = useNotify(); const $props = defineProps({ url: { type: String, default: '', }, model: { type: String, default: '', }, 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, }, saveFn: { type: Function, default: null, }, }); const emit = defineEmits(['onFetch', 'onDataSaved']); defineExpose({ save, }); const componentIsRendered = ref(false); onMounted(async () => { nextTick(() => { componentIsRendered.value = true; }); // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla state.set($props.model, $props.formInitialData); if ($props.autoLoad && !$props.formInitialData) { await fetch(); } // Si así se desea disparamos el watcher del form después de 100ms, asi darle tiempo de que se haya cargado la data inicial // para evitar que detecte cambios cuando es data inicial default if ($props.observeFormChanges) { setTimeout(() => { startFormWatcher(); }, 100); } }); onBeforeRouteLeave((to, from, next) => { if (!hasChanges.value) next(); quasar.dialog({ component: VnConfirm, componentProps: { title: t('Unsaved changes will be lost'), message: t('Are you sure exit without saving?'), promise: () => next(), }, }); }); onUnmounted(() => { state.unset($props.model); }); 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({ ...$props.formInitialData }); const formData = computed(() => state.get($props.model)); const formUrl = computed(() => $props.url); const defaultButtons = computed(() => ({ save: { color: 'primary', icon: 'restart_alt', label: 'globals.save', }, reset: { color: 'primary', icon: 'save', label: 'globals.reset', }, ...$props.defaultButtons, })); const startFormWatcher = () => { watch( () => formData.value, (val) => { hasChanges.value = !isResetting.value && val; isResetting.value = false; }, { deep: true } ); }; async function fetch() { const { data } = await axios.get($props.url, { params: { filter: JSON.stringify($props.filter) }, }); state.set($props.model, data); originalData.value = data && JSON.parse(JSON.stringify(data)); emit('onFetch', state.get($props.model)); } async function save() { if ($props.observeFormChanges && !hasChanges.value) { notify('globals.noChanges', 'negative'); return; } isLoading.value = true; try { const body = $props.mapper ? $props.mapper(formData.value) : formData.value; let response; if ($props.saveFn) response = await $props.saveFn(body); else response = await axios[$props.urlCreate ? 'post' : 'patch']( $props.urlCreate || $props.urlUpdate || $props.url, body ); if ($props.urlCreate) notify('globals.dataCreated', 'positive'); emit('onDataSaved', formData.value, response?.data); originalData.value = JSON.parse(JSON.stringify(formData.value)); hasChanges.value = false; } catch (err) { console.error(err); notify('errors.writeRequest', 'negative'); } isLoading.value = false; } function reset() { state.set($props.model, originalData.value); originalData.value = JSON.parse(JSON.stringify(originalData.value)); emit('onFetch', state.get($props.model)); 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); } ); } watch(formUrl, async () => { originalData.value = null; 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)" /> <QBtn :label="tMobile(defaultButtons.save.label)" :color="defaultButtons.save.color" :icon="defaultButtons.save.icon" @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> #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>