forked from verdnatura/salix-front
397 lines
11 KiB
Vue
397 lines
11 KiB
Vue
<script setup>
|
|
import axios from 'axios';
|
|
import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
|
|
import { onBeforeRouteLeave, useRouter, useRoute } 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';
|
|
|
|
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 myForm = ref(null);
|
|
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"',
|
|
},
|
|
reload: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
defaultTrim: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
maxWidth: {
|
|
type: [String, Boolean],
|
|
default: '800px',
|
|
},
|
|
});
|
|
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 = computed(() => state.get(modelValue));
|
|
const formData = ref({});
|
|
const defaultButtons = computed(() => ({
|
|
save: {
|
|
dataCy: 'saveDefaultBtn',
|
|
color: 'primary',
|
|
icon: 'save',
|
|
label: 'globals.save',
|
|
click: () => myForm.value.submit(),
|
|
type: 'submit',
|
|
},
|
|
reset: {
|
|
dataCy: 'resetDefaultBtn',
|
|
color: 'primary',
|
|
icon: 'restart_alt',
|
|
label: 'globals.reset',
|
|
click: () => reset(),
|
|
},
|
|
...$props.defaultButtons,
|
|
}));
|
|
|
|
onMounted(async () => {
|
|
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(
|
|
originalData,
|
|
(val) => {
|
|
if (val) formData.value = JSON.parse(JSON.stringify(val));
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
|
|
watch(
|
|
() => [$props.url, $props.filter],
|
|
async () => {
|
|
state.set(modelValue, null);
|
|
reset();
|
|
await fetch();
|
|
},
|
|
);
|
|
|
|
onBeforeRouteLeave((to, from, next) => {
|
|
if (hasChanges.value && $props.observeFormChanges)
|
|
quasar.dialog({
|
|
component: VnConfirm,
|
|
componentProps: {
|
|
title: t('globals.unsavedPopup.title'),
|
|
message: t('globals.unsavedPopup.subtitle'),
|
|
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, {});
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function save() {
|
|
if ($props.observeFormChanges && !hasChanges.value)
|
|
return notify('globals.noChanges', 'negative');
|
|
|
|
isLoading.value = true;
|
|
try {
|
|
formData.value = trimData(formData.value);
|
|
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 || 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);
|
|
if ($props.reload) await arrayData.fetch({});
|
|
hasChanges.value = false;
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
async function saveAndGo() {
|
|
await save();
|
|
push({ path: $props.goTo });
|
|
}
|
|
|
|
function reset() {
|
|
formData.value = JSON.parse(JSON.stringify(originalData.value));
|
|
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);
|
|
if (!$props.url) arrayData.store.data = val;
|
|
|
|
emit(evt, state.get(modelValue), res);
|
|
}
|
|
|
|
function trimData(data) {
|
|
if (!$props.defaultTrim) return data;
|
|
for (const key in data) {
|
|
if (typeof data[key] == 'string') data[key] = data[key].trim();
|
|
}
|
|
return data;
|
|
}
|
|
|
|
defineExpose({
|
|
save,
|
|
isLoading,
|
|
hasChanges,
|
|
reset,
|
|
fetch,
|
|
formData,
|
|
});
|
|
</script>
|
|
<template>
|
|
<div class="column items-center full-width">
|
|
<QForm
|
|
ref="myForm"
|
|
v-if="formData"
|
|
@submit="save"
|
|
@reset="reset"
|
|
class="q-pa-md"
|
|
:style="maxWidth ? 'max-width: ' + maxWidth : ''"
|
|
id="formModel"
|
|
:prevent-submit="$attrs['prevent-submit']"
|
|
>
|
|
<QCard>
|
|
<slot
|
|
v-if="formData"
|
|
name="form"
|
|
:data="formData"
|
|
:validate="validate"
|
|
:filter="filter"
|
|
/>
|
|
<SkeletonForm v-else />
|
|
</QCard>
|
|
</QForm>
|
|
</div>
|
|
<Teleport
|
|
to="#st-actions"
|
|
v-if="
|
|
$props.defaultActions &&
|
|
stateStore?.isSubToolbarShown() &&
|
|
componentIsRendered
|
|
"
|
|
>
|
|
<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="defaultButtons.reset.click"
|
|
:disable="!hasChanges"
|
|
:title="t(defaultButtons.reset.label)"
|
|
/>
|
|
<QBtnDropdown
|
|
data-cy="saveAndContinueDefaultBtn"
|
|
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="defaultButtons.save.click"
|
|
:disable="!hasChanges"
|
|
:title="t(defaultButtons.save.label)"
|
|
/>
|
|
</QBtnGroup>
|
|
</Teleport>
|
|
|
|
<QInnerLoading
|
|
:showing="isLoading"
|
|
:label="t('globals.pleaseWait')"
|
|
color="primary"
|
|
style="min-width: 100%"
|
|
/>
|
|
</template>
|
|
<style lang="scss" scoped>
|
|
.q-notifications {
|
|
color: black;
|
|
}
|
|
#formModel {
|
|
width: 100%;
|
|
}
|
|
|
|
.q-card {
|
|
padding: 32px;
|
|
}
|
|
</style>
|