0
0
Fork 0
salix-front-mindshore-fork2/src/components/FormModel.vue

256 lines
6.7 KiB
Vue

<script setup>
import axios from 'axios';
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
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';
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,
},
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,
},
});
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
if ($props.formInitialData && !$props.autoLoad) {
state.set($props.model, $props.formInitialData);
} else {
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);
}
});
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 startFormWatcher = () => {
watch(
() => formData.value,
(val) => {
hasChanges.value = !isResetting.value && val;
isResetting.value = false;
},
{ deep: true }
);
};
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch() {
try {
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));
} catch (error) {
state.set($props.model, {});
originalData.value = {};
}
}
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.urlCreate) {
response = await axios.post($props.urlCreate, body);
notify('globals.dataCreated', 'positive');
} else {
response = await axios.patch($props.urlUpdate || $props.url, body);
}
emit('onDataSaved', formData.value, response?.data);
originalData.value = JSON.parse(JSON.stringify(formData.value));
hasChanges.value = false;
} catch (err) {
notify('errors.create', '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('globals.reset')"
color="primary"
icon="restart_alt"
flat
@click="reset"
:disable="!hasChanges"
:title="t('globals.reset')"
/>
<QBtn
:label="tMobile('globals.save')"
color="primary"
icon="save"
@click="save"
:disable="!hasChanges"
:title="t('globals.save')"
/>
</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>