386 lines
10 KiB
Vue
386 lines
10 KiB
Vue
<script setup>
|
|
import axios from 'axios';
|
|
import { computed, ref, watch } from 'vue';
|
|
import { useRouter, onBeforeRouteLeave } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useQuasar } from 'quasar';
|
|
import { useValidator } from 'src/composables/useValidator';
|
|
import { useStateStore } from 'stores/useStateStore';
|
|
import VnPaginate from 'components/ui/VnPaginate.vue';
|
|
import VnConfirm from 'components/ui/VnConfirm.vue';
|
|
import SkeletonTable from 'components/ui/SkeletonTable.vue';
|
|
import { tMobile } from 'src/composables/tMobile';
|
|
import getDifferences from 'src/filters/getDifferences';
|
|
|
|
const { push } = useRouter();
|
|
const quasar = useQuasar();
|
|
const stateStore = useStateStore();
|
|
const { t } = useI18n();
|
|
const { validate } = useValidator();
|
|
|
|
const $props = defineProps({
|
|
model: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
url: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
limit: {
|
|
type: Number,
|
|
default: 20,
|
|
},
|
|
saveUrl: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
primaryKey: {
|
|
type: String,
|
|
default: 'id',
|
|
},
|
|
dataRequired: {
|
|
type: Object,
|
|
default: () => {},
|
|
},
|
|
defaultSave: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
defaultReset: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
defaultRemove: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
selected: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
saveFn: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
goTo: {
|
|
type: String,
|
|
default: '',
|
|
description: 'It is used for redirect on click "save and continue"',
|
|
},
|
|
hasSubToolbar: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
});
|
|
|
|
const isLoading = ref(false);
|
|
const hasChanges = ref(false);
|
|
const originalData = ref();
|
|
const vnPaginateRef = ref();
|
|
const formData = ref([]);
|
|
const saveButtonRef = ref(null);
|
|
const watchChanges = ref();
|
|
const formUrl = computed(() => $props.url);
|
|
|
|
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
|
|
|
|
defineExpose({
|
|
reload,
|
|
insert,
|
|
remove,
|
|
onSubmit,
|
|
reset,
|
|
hasChanges,
|
|
saveChanges,
|
|
getChanges,
|
|
formData,
|
|
originalData,
|
|
vnPaginateRef,
|
|
});
|
|
|
|
onBeforeRouteLeave((to, from, next) => {
|
|
if (hasChanges.value)
|
|
quasar.dialog({
|
|
component: VnConfirm,
|
|
componentProps: {
|
|
title: t('globals.unsavedPopup.title'),
|
|
message: t('globals.unsavedPopup.subtitle'),
|
|
promise: () => next(),
|
|
},
|
|
});
|
|
else next();
|
|
});
|
|
|
|
async function fetch(data) {
|
|
resetData(data);
|
|
emit('onFetch', data);
|
|
return data;
|
|
}
|
|
|
|
function resetData(data) {
|
|
if (!data) return;
|
|
if (data && Array.isArray(data)) {
|
|
let $index = 0;
|
|
data.map((d) => (d.$index = $index++));
|
|
}
|
|
originalData.value = JSON.parse(JSON.stringify(data));
|
|
formData.value = JSON.parse(JSON.stringify(data));
|
|
|
|
if (watchChanges.value) watchChanges.value(); //destoy watcher
|
|
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
|
|
}
|
|
|
|
async function reset() {
|
|
await fetch(originalData.value);
|
|
hasChanges.value = false;
|
|
}
|
|
|
|
function filter(value, update, filterOptions) {
|
|
update(
|
|
() => {
|
|
const { options, filterFn, field } = filterOptions;
|
|
|
|
options.value = filterFn(options, value, field);
|
|
},
|
|
(ref) => {
|
|
ref.setOptionIndex(-1);
|
|
ref.moveOptionSelection(1, true);
|
|
}
|
|
);
|
|
}
|
|
|
|
async function onSubmit() {
|
|
if (!hasChanges.value) {
|
|
return quasar.notify({
|
|
type: 'negative',
|
|
message: t('globals.noChanges'),
|
|
});
|
|
}
|
|
isLoading.value = true;
|
|
await saveChanges($props.saveFn ? formData.value : null);
|
|
}
|
|
|
|
async function onSubmitAndGo() {
|
|
await onSubmit();
|
|
push({ path: $props.goTo });
|
|
}
|
|
|
|
async function saveChanges(data) {
|
|
if ($props.saveFn) {
|
|
$props.saveFn(data, getChanges);
|
|
isLoading.value = false;
|
|
hasChanges.value = false;
|
|
return;
|
|
}
|
|
const changes = data || getChanges();
|
|
try {
|
|
await axios.post($props.saveUrl || $props.url + '/crud', changes);
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
originalData.value = JSON.parse(JSON.stringify(formData.value));
|
|
if (changes.creates?.length) await vnPaginateRef.value.fetch();
|
|
|
|
hasChanges.value = false;
|
|
emit('saveChanges', data);
|
|
quasar.notify({
|
|
type: 'positive',
|
|
message: t('globals.dataSaved'),
|
|
});
|
|
}
|
|
|
|
async function insert(pushData = $props.dataRequired) {
|
|
const $index = formData.value.length
|
|
? formData.value[formData.value.length - 1].$index + 1
|
|
: 0;
|
|
formData.value.push(Object.assign({ $index }, pushData));
|
|
hasChanges.value = true;
|
|
}
|
|
|
|
async function remove(data) {
|
|
if (!data.length)
|
|
return quasar.notify({
|
|
type: 'warning',
|
|
message: t('globals.noChanges'),
|
|
});
|
|
|
|
const pk = $props.primaryKey;
|
|
let ids = data.map((d) => d[pk]).filter(Boolean);
|
|
let preRemove = data.map((d) => (d[pk] ? null : d.$index)).filter(Boolean);
|
|
let newData = formData.value;
|
|
|
|
if (preRemove.length) {
|
|
newData = newData.filter(
|
|
(form) => !preRemove.some((index) => index == form.$index)
|
|
);
|
|
const changes = getChanges();
|
|
if (!changes.creates?.length && !changes.updates?.length)
|
|
hasChanges.value = false;
|
|
fetch(newData);
|
|
}
|
|
if (ids.length) {
|
|
quasar
|
|
.dialog({
|
|
component: VnConfirm,
|
|
componentProps: {
|
|
title: t('globals.confirmDeletion'),
|
|
message: t('globals.confirmDeletionMessage'),
|
|
newData,
|
|
ids,
|
|
},
|
|
})
|
|
.onOk(async () => {
|
|
await saveChanges({ deletes: ids });
|
|
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
|
|
fetch(newData);
|
|
});
|
|
} else {
|
|
reset();
|
|
}
|
|
emit('update:selected', []);
|
|
}
|
|
|
|
function getChanges() {
|
|
const updates = [];
|
|
const creates = [];
|
|
|
|
const pk = $props.primaryKey;
|
|
for (const [i, row] of formData.value.entries()) {
|
|
if (!row[pk]) {
|
|
creates.push(row);
|
|
} else if (originalData.value) {
|
|
const data = getDifferences(originalData.value[i], row);
|
|
if (!isEmpty(data)) {
|
|
updates.push({
|
|
data,
|
|
where: { [pk]: row[pk] },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const changes = { updates, creates };
|
|
|
|
for (let prop in changes) {
|
|
if (changes[prop].length === 0) changes[prop] = undefined;
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
function isEmpty(obj) {
|
|
if (obj == null) return true;
|
|
if (obj === undefined) return true;
|
|
if (Object.keys(obj).length === 0) return true;
|
|
|
|
if (obj.length > 0) return false;
|
|
}
|
|
|
|
async function reload(params) {
|
|
const data = await vnPaginateRef.value.fetch(params);
|
|
fetch(data);
|
|
}
|
|
|
|
watch(formUrl, async () => {
|
|
originalData.value = null;
|
|
reset();
|
|
});
|
|
</script>
|
|
<template>
|
|
<VnPaginate
|
|
:url="url"
|
|
:limit="limit"
|
|
@on-fetch="fetch"
|
|
@on-change="fetch"
|
|
:skeleton="false"
|
|
ref="vnPaginateRef"
|
|
v-bind="$attrs"
|
|
>
|
|
<template #body v-if="formData">
|
|
<slot
|
|
name="body"
|
|
:rows="formData"
|
|
:validate="validate"
|
|
:filter="filter"
|
|
></slot>
|
|
</template>
|
|
</VnPaginate>
|
|
<SkeletonTable
|
|
v-if="!formData && $attrs.autoLoad"
|
|
:columns="$attrs.columns?.length"
|
|
/>
|
|
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
|
|
<QBtnGroup push style="column-gap: 10px">
|
|
<slot name="moreBeforeActions" />
|
|
<QBtn
|
|
:label="tMobile('globals.remove')"
|
|
color="primary"
|
|
icon="delete"
|
|
flat
|
|
@click="remove(selected)"
|
|
:disable="!selected?.length"
|
|
:title="t('globals.remove')"
|
|
v-if="$props.defaultRemove"
|
|
/>
|
|
<QBtn
|
|
:label="tMobile('globals.reset')"
|
|
color="primary"
|
|
icon="restart_alt"
|
|
flat
|
|
@click="reset"
|
|
:disable="!hasChanges"
|
|
:title="t('globals.reset')"
|
|
v-if="$props.defaultReset"
|
|
/>
|
|
<QBtnDropdown
|
|
v-if="$props.goTo && $props.defaultSave"
|
|
@click="onSubmitAndGo"
|
|
:label="tMobile('globals.saveAndContinue')"
|
|
:title="t('globals.saveAndContinue')"
|
|
:disable="!hasChanges"
|
|
color="primary"
|
|
icon="save"
|
|
split
|
|
>
|
|
<QList>
|
|
<QItem
|
|
color="primary"
|
|
clickable
|
|
v-close-popup
|
|
@click="onSubmit"
|
|
: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-if="!$props.goTo && $props.defaultSave"
|
|
:label="tMobile('globals.save')"
|
|
ref="saveButtonRef"
|
|
color="primary"
|
|
icon="save"
|
|
@click="onSubmit"
|
|
:disable="!hasChanges"
|
|
:title="t('globals.save')"
|
|
/>
|
|
<slot name="moreAfterActions" />
|
|
</QBtnGroup>
|
|
</Teleport>
|
|
<QInnerLoading
|
|
:showing="isLoading"
|
|
:label="t && t('globals.pleaseWait')"
|
|
color="primary"
|
|
/>
|
|
</template>
|