WIP: feat: #8406 upgraded CrudModel #1627

Draft
provira wants to merge 6 commits from 8406-crudModelUpdate into dev
9 changed files with 113 additions and 31 deletions

View File

@ -1,6 +1,6 @@
<script setup>
import axios from 'axios';
import { computed, ref, useAttrs, watch } from 'vue';
import { computed, ref, useAttrs, watch, nextTick } from 'vue';
import { useRouter, onBeforeRouteLeave } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
@ -42,7 +42,15 @@ const $props = defineProps({
},
dataRequired: {
type: Object,
default: () => {},
default: () => ({}),
},
dataDefault: {
type: Object,
default: () => ({}),
},
insertOnLoad: {
type: Boolean,
default: true,
provira marked this conversation as resolved
Review

default false, y donde se quiera, poner true

default false, y donde se quiera, poner true
Review

@jgallego que opinas?

@jgallego que opinas?
Review

la mayoria de los campos no se usan para crear nuevos registros, por tanto, por defecto false creo que es mejor, además el hecho de quererlo en el formulario de creación para mi tiene que ser explicito. Por tanto, default: false

la mayoria de los campos no se usan para crear nuevos registros, por tanto, por defecto false creo que es mejor, además el hecho de quererlo en el formulario de creación para mi tiene que ser explicito. Por tanto, default: false
},
defaultSave: {
type: Boolean,
@ -86,7 +94,11 @@ const vnPaginateRef = ref();
const formData = ref([]);
const saveButtonRef = ref(null);
const watchChanges = ref();
let isNotEqual = ref(false);
let isLastRowEmpty = ref(false);
const isFirstFetch = ref(true);
const formUrl = computed(() => $props.url);
const rowsContainer = ref(null);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -122,9 +134,14 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data;
resetData(rows);
emit('onFetch', rows);
if (isFirstFetch.value && $props.insertOnLoad) {
await insert();
}
isFirstFetch.value = false;
return rows;
}
function resetData(data) {
if (!data) return;
if (data && Array.isArray(data)) {
@ -135,9 +152,16 @@ function resetData(data) {
formData.value = JSON.parse(JSON.stringify(data));
if (watchChanges.value) watchChanges.value(); //destroy watcher
watchChanges.value = watch(formData, () => (hasChanges.value = true), { deep: true });
}
watchChanges.value = watch(formData, (nVal) => {
hasChanges.value = false;
const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]);
provira marked this conversation as resolved Outdated
Outdated
Review

Hay unas funciones para sacar los cambios
import { getDifferences, getUpdatedValues } from 'src/filters';

Las usaria para esto tambien

Hay unas funciones para sacar los cambios `import { getDifferences, getUpdatedValues } from 'src/filters';` Las usaria para esto tambien

Me suena que esto lo miramos juntos... Prueba a ver si se puede simplificar con las funciones que dice Alex.

Me suena que esto lo miramos juntos... Prueba a ver si se puede simplificar con las funciones que dice Alex.
const filteredOriginal = originalData.value.filter(row => row[$props.primaryKey]);
const changes = getDifferences(filteredOriginal, filteredNewData);
hasChanges.value = !isEmpty(changes);
}, { deep: true });
}
async function reset() {
await fetch(originalData.value);
hasChanges.value = false;
@ -165,7 +189,9 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
@ -203,14 +229,32 @@ async function saveChanges(data) {
});
}
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 insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) {
formData.value = formData.value.filter(row => !isRowEmpty(row));
const lastRow = formData.value.at(-1);
const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false;
if (formData.value.length > 0 && isLastRowEmpty) return;
const $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
provira marked this conversation as resolved Outdated
Outdated
Review

Para ignorar las líneas vacías, usaría un filter
Para quitar las líneas vacías, usaría un map

Para ignorar las líneas vacías, usaría un filter Para quitar las líneas vacías, usaría un map
const nRow = Object.assign({ $index }, pushData);
formData.value.push(nRow);
const hasChange = Object.keys(nRow).some(key => !isChange(nRow, key));
if (hasChange) hasChanges.value = true;
}
function isRowEmpty(row) {
return Object.keys(row).every(key => isChange(row, key));
}
function isChange(row,key){
return !row[key] || key == '$index' || Object.hasOwn($props.dataRequired || {}, key);
}
async function remove(data) {
if (!data.length)
return quasar.notify({
@ -227,10 +271,8 @@ async function remove(data) {
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);
formData.value = newData;
hasChanges.value = JSON.stringify(removeIndexField(formData.value)) !== JSON.stringify(removeIndexField(originalData.value));
}
if (ids.length) {
quasar
provira marked this conversation as resolved Outdated
Outdated
Review

Creo que hace lo mismo

Creo que hace lo mismo
@ -248,9 +290,8 @@ async function remove(data) {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
} else {
reset();
}
emit('update:selected', []);
}
@ -261,7 +302,7 @@ function getChanges() {
const pk = $props.primaryKey;
for (const [i, row] of formData.value.entries()) {
if (!row[pk]) {
creates.push(row);
creates.push(Object.assign(row, { ...$props.dataRequired }));
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
provira marked this conversation as resolved Outdated
Outdated
Review

si no hay filas, fetch? Se tiene original data

si no hay filas, fetch? Se tiene original data
if (!isEmpty(data)) {
@ -287,6 +328,33 @@ function isEmpty(obj) {
return !Object.keys(obj).length;
}
function removeIndexField(data) {
if (Array.isArray(data)) {
return data.map(({ $index, ...rest }) => rest);
} else if (typeof data === 'object' && data !== null) {
const { $index, ...rest } = data;
return rest;
}
}
async function handleTab(event) {
const focusableElements = rowsContainer.value?.querySelectorAll(
'input, select, textarea, [tabindex]:not([tabindex="-1"]), .q-field__native, .q-checkbox__input'
);
if (!focusableElements || focusableElements.length === 0) return;
const lastElement = focusableElements[focusableElements.length - 1];
if (event.target === lastElement) {
event.preventDefault();
await insert();
await nextTick();
const newElements = rowsContainer.value.querySelectorAll('input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (newElements.length > focusableElements.length) {
newElements[newElements.length - 1].focus();
}
}
}
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
@ -308,12 +376,14 @@ watch(formUrl, async () => {
v-bind="$attrs"
>
<template #body v-if="formData">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
<div ref="rowsContainer" @keydown.tab.prevent="handleTab">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</div>
</template>
</VnPaginate>
<SkeletonTable

View File

@ -653,6 +653,7 @@ const rowCtrlClickFunction = computed(() => {
:class="$attrs['class'] ?? 'q-px-md'"
:limit="$attrs['limit'] ?? 100"
ref="CrudModelRef"
:insert-on-load="crudModel.insertOnLoad"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl"
:disable-infinite-scroll="isTableMode"

View File

@ -199,11 +199,11 @@ describe('CrudModel', () => {
});
it('should set originalData and formatData with data and generate watchChanges', async () => {
data = {
data = [{
name: 'Tony',
lastName: 'Stark',
age: 42,
};
}];
vm.resetData(data);

View File

@ -89,6 +89,7 @@ onBeforeMount(async () => await setTaxableBase());
data-key="InvoiceInDueDays"
url="InvoiceInDueDays"
:filter="filter"
:insert-on-load="false"
auto-load
:data-required="{ invoiceInFk: invoiceId }"
v-model:selected="rowsSelected"

View File

@ -76,15 +76,22 @@ const insertTag = (rows) => {
model="ItemTags"
url="ItemTags"
:data-required="{
$index: undefined,
itemFk: route.params.id,
priority: undefined,
tag: {
isFree: undefined,
isFree: true,
value: undefined,
name: undefined,
},
}"
:data-default="{
tag: {
isFree: true,
value: undefined,
name: undefined,
},
tagFk: undefined,
priority: undefined,
}"
:default-remove="false"
:user-filter="{

View File

@ -58,6 +58,7 @@ const submitTaxes = async (data) => {
:save-fn="submitTaxes"
:filter="taxesFilter"
:default-remove="false"
:insert-on-load="false"
data-key="ItemTax"
model="ItemTax"
ref="ItemTaxRef"

View File

@ -24,10 +24,10 @@ const crudModelFilter = reactive({
where: { ticketFk: route.params.id },
});
const crudModelRequiredData = computed(() => ({
const crudModelDefaultData = computed(() => ({
created: Date.vnNew(),
packagingFk: null,
quantity: 0,
created: Date.vnNew(),
ticketFk: route.params.id,
}));
@ -59,7 +59,7 @@ watch(
url="TicketPackagings"
model="TicketPackagings"
:filter="crudModelFilter"
:data-required="crudModelRequiredData"
:data-default="crudModelDefaultData"
:default-remove="false"
auto-load
style="max-width: 800px"

View File

@ -719,6 +719,7 @@ watch(
:create-as-dialog="false"
:crud-model="{
disableInfiniteScroll: true,
insertOnLoad: false,
}"
:default-remove="false"
:default-reset="false"

View File

@ -174,6 +174,7 @@ const setUserParams = (params) => {
:create="false"
:crud-model="{
disableInfiniteScroll: true,
insertOnLoad: false,
}"
:table="{
'row-key': 'itemFk',