232 lines
6.1 KiB
Vue
232 lines
6.1 KiB
Vue
<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>
|