salix-front/src/components/CrudModel.vue

391 lines
10 KiB
Vue
Raw Normal View History

<script setup>
import axios from 'axios';
import { computed, ref, watch } from 'vue';
2024-03-06 15:23:19 +00:00
import { useRouter } 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';
2024-03-06 15:23:19 +00:00
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: '',
},
2024-04-15 06:40:15 +00:00
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,
},
2024-03-06 15:23:19 +00:00
goTo: {
type: String,
default: '',
description: 'It is used for redirect on click "save and continue"',
},
2024-06-17 09:29:36 +00:00
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,
2024-04-15 06:40:15 +00:00
getChanges,
2024-04-24 13:31:33 +00:00
formData,
2024-06-11 13:00:43 +00:00
vnPaginateRef,
});
async function fetch(data) {
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
resetData(data);
emit('onFetch', data);
return data;
}
function resetData(data) {
if (!data) return;
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;
2024-04-26 11:02:17 +00:00
await saveChanges($props.saveFn ? formData.value : null);
}
2024-06-21 11:47:42 +00:00
async function onSubmitAndGo() {
2024-03-06 15:23:19 +00:00
await onSubmit();
push({ path: $props.goTo });
}
async function saveChanges(data) {
2024-04-26 11:02:17 +00:00
if ($props.saveFn) {
$props.saveFn(data, getChanges);
isLoading.value = false;
2024-04-29 13:16:37 +00:00
hasChanges.value = false;
2024-04-26 11:02:17 +00:00
return;
}
const changes = data || getChanges();
try {
await axios.post($props.saveUrl || $props.url + '/crud', changes);
} catch (e) {
return (isLoading.value = false);
}
originalData.value = JSON.parse(JSON.stringify(formData.value));
if (changes.creates?.length) await vnPaginateRef.value.fetch();
hasChanges.value = false;
isLoading.value = false;
emit('saveChanges', data);
2023-11-09 09:29:28 +00:00
quasar.notify({
type: 'positive',
message: t('globals.dataSaved'),
});
}
async function insert() {
const $index = formData.value.length
? formData.value[formData.value.length - 1].$index + 1
: 0;
formData.value.push(Object.assign({ $index }, $props.dataRequired));
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: {
2024-02-12 14:06:20 +00:00
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);
});
}
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 };
2024-02-06 14:13:43 +00:00
for (let prop in changes) {
if (changes[prop].length === 0) changes[prop] = undefined;
}
return changes;
}
function getDifferences(obj1, obj2) {
let diff = {};
delete obj1.$index;
delete obj2.$index;
for (let key in obj1) {
2024-01-09 13:46:27 +00:00
if (obj2[key] && JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
diff[key] = obj2[key];
}
}
for (let key in obj2) {
2024-01-09 13:46:27 +00:00
if (
obj1[key] === undefined ||
JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])
) {
diff[key] = obj2[key];
}
}
2024-01-09 13:46:27 +00:00
return diff;
}
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;
}
2024-06-11 13:00:43 +00:00
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"
2024-04-15 06:40:15 +00:00
:limit="limit"
@on-fetch="fetch"
@on-change="resetData"
:skeleton="false"
ref="vnPaginateRef"
2024-06-11 13:00:43 +00:00
v-bind="$attrs"
>
<template #body v-if="formData">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</template>
</VnPaginate>
2024-06-14 07:19:51 +00:00
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" />
2024-06-17 09:29:36 +00:00
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar">
2023-11-07 11:42:39 +00:00
<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"
/>
2024-03-06 15:23:19 +00:00
<QBtnDropdown
v-if="$props.goTo && $props.defaultSave"
2024-06-21 11:47:42 +00:00
@click="onSubmitAndGo"
2024-03-07 13:54:20 +00:00
:label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')"
2024-03-06 15:23:19 +00:00
:disable="!hasChanges"
color="primary"
icon="save"
split
>
<QList>
2024-03-07 13:54:20 +00:00
<QItem
color="primary"
clickable
v-close-popup
@click="onSubmit"
:title="t('globals.save')"
>
2024-03-06 15:23:19 +00:00
<QItemSection>
2024-03-07 13:54:20 +00:00
<QItemLabel>
<QIcon
name="save"
color="white"
class="q-mr-sm"
size="sm"
/>
{{ t('globals.save').toUpperCase() }}
</QItemLabel>
2024-03-06 15:23:19 +00:00
</QItemSection>
</QItem>
</QList>
</QBtnDropdown>
<QBtn
2024-03-06 15:23:19 +00:00
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>