forked from verdnatura/salix-front
256 lines
6.7 KiB
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>
|