WIP: feat: #8406 upgraded CrudModel #1627

Draft
provira wants to merge 9 commits from 8406-crudModelUpdate into dev
17 changed files with 155 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: false,
provira marked this conversation as resolved Outdated
Outdated
Review

default false, y donde se quiera, poner true

default false, y donde se quiera, poner true

@jgallego que opinas?

@jgallego que opinas?

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,
@ -56,6 +64,10 @@ const $props = defineProps({
type: Boolean,
default: true,
},
defaultAdd: {
type: Boolean,
default: false,
},
selected: {
type: Object,
default: null,
@ -86,7 +98,10 @@ const vnPaginateRef = ref();
const formData = ref();
const saveButtonRef = ref(null);
const watchChanges = ref();
let isNotEqual = ref(false);
let isLastRowEmpty = ref(false);
const formUrl = computed(() => $props.url);
const rowsContainer = ref(null);
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -122,9 +137,13 @@ async function fetch(data) {
const rows = keyData ? data[keyData] : data;
resetData(rows);
emit('onFetch', rows);
if ($props.insertOnLoad) {
await insert();
}
return rows;
}
function resetData(data) {
if (!data) return;
if (data && Array.isArray(data)) {
@ -135,9 +154,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) => {
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.
hasChanges.value = false;
const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]);
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 +191,9 @@ async function onSubmit() {
});
}
isLoading.value = true;
await saveChanges($props.saveFn ? formData.value : null);
}
async function onSubmitAndGo() {
@ -203,14 +231,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;
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 $index = formData.value.length ? formData.value.at(-1).$index + 1 : 0;
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 +273,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));
}
provira marked this conversation as resolved Outdated
Outdated
Review

Creo que hace lo mismo

Creo que hace lo mismo
if (ids.length) {
quasar
@ -248,9 +292,8 @@ async function remove(data) {
newData = newData.filter((form) => !ids.some((id) => id == form[pk]));
fetch(newData);
});
} else {
reset();
}
emit('update:selected', []);
}
@ -261,7 +304,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 }));
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
} else if (originalData.value[i]) {
const data = getDifferences(originalData.value[i], row);
if (!isEmpty(data)) {
@ -287,6 +330,40 @@ 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) {
if (event.target.classList.contains('q-checkbox__input') ||
event.target.type === 'checkbox' ||
event.target.closest('.q-checkbox')) {
return;
}
const focusableElements = rowsContainer.value?.querySelectorAll(
'input:not([type="checkbox"]), select, textarea, [tabindex]:not([tabindex="-1"]):not(.q-checkbox__input), .q-field__native'
);
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:not([type="checkbox"]), select, textarea, [tabindex]:not([tabindex="-1"]):not(.q-checkbox__input)');
if (newElements.length > focusableElements.length) {
newElements[newElements.length - 1].focus();
}
}
}
async function reload(params) {
const data = await vnPaginateRef.value.fetch(params);
fetch(data);
@ -312,12 +389,14 @@ watch(formUrl, async () => {
v-bind="$attrs"
>
<template #body v-if="formData">
<div ref="rowsContainer" @keydown.tab="handleTab">
<slot
name="body"
:rows="formData"
:validate="validate"
:filter="filter"
></slot>
</div>
</template>
</VnPaginate>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
@ -397,4 +476,16 @@ watch(formUrl, async () => {
:label="t && t('globals.pleaseWait')"
color="primary"
/>
<div class=" bottomButton" v-if="defaultAdd">
<QBtn
@click="insert()"
class="cursor-pointer fill-icon"
color="primary"
icon="add_circle"
size="md"
round
flat
v-shortcut="'+'"
/>
</div>
</template>

View File

@ -663,6 +663,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

@ -193,11 +193,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

@ -138,6 +138,8 @@ const columns = computed(() => [
:filter="developmentsFilter"
ref="claimDevelopmentForm"
:data-required="{ claimFk: route.params.id }"
:defaultAdd="true"
:insert-on-load="true"
v-model:selected="selected"
@save-changes="$router.push(`/claim/${route.params.id}/action`)"
:default-save="false"

View File

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

View File

@ -89,7 +89,9 @@ const columns = computed(() => [
url="InvoiceInIntrastats"
auto-load
:data-required="{ invoiceInFk: invoiceInId }"
:default-add="true"
:filter="filter"
:insert-on-load="true"
v-model:selected="rowsSelected"
@on-fetch="(data) => (invoceInIntrastat = data)"
>

View File

@ -187,6 +187,8 @@ function setCursor(ref) {
url="InvoiceInTaxes"
:filter="filter"
:data-required="{ invoiceInFk: $route.params.id }"
:default-add="true"
:insert-on-load="true"
auto-load
v-model:selected="rowsSelected"
:go-to="`/invoice-in/${$route.params.id}/due-day`"

View File

@ -51,6 +51,7 @@ const submit = async (rows) => {
<CrudModel
:data-required="{ itemFk: route.params.id }"
:default-remove="false"
:insert-on-load="true"
:filter="{
fields: ['id', 'itemFk', 'code'],
where: { itemFk: route.params.id },

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

@ -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

@ -181,6 +181,7 @@ function beforeSave(data) {
model="TicketService"
:filter="crudModelFilter"
:data-required="crudModelRequiredData"
:default-add="true"
auto-load
v-model:selected="selected"
:order="['description ASC']"

View File

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

View File

@ -99,6 +99,10 @@ const columns = computed(() => [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="paymentDate DESC"
:columns="columns"
auto-load

View File

@ -128,6 +128,9 @@ const columns = computed(() => [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="id DESC"
:columns="columns"
auto-load

View File

@ -109,6 +109,9 @@ const columns = [
workerFk: entityId,
},
}"
:crud-model="{
insertOnLoad: false,
}"
order="date DESC"
:columns="columns"
auto-load

View File

@ -170,6 +170,9 @@ function isSigned(row) {
'row-key': 'deviceProductionFk',
selection: 'multiple',
}"
:crud-model="{
insertOnLoad: false,
}"
:table-filter="{ hiddenTags: ['userFk'] }"
>
<template #moreBeforeActions>