Merge pull request '5673-hotFix_improve_crudModel_vnSelect' (!100) from 5673-hotFix_improve_crudModel_vnSelect into master
gitea/salix-front/pipeline/head This commit looks good Details

Reviewed-on: #100
Reviewed-by: Javi Gallego <jgallego@verdnatura.es>
This commit is contained in:
Alex Moreno 2023-10-13 07:13:25 +00:00
commit a5710e66e1
11 changed files with 138 additions and 41 deletions

View File

@ -9,7 +9,7 @@
"lint": "eslint --ext .js,.vue ./", "lint": "eslint --ext .js,.vue ./",
"format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore", "format": "prettier --write \"**/*.{js,vue,scss,html,md,json}\" --ignore-path .gitignore",
"test:e2e": "cypress open", "test:e2e": "cypress open",
"test:e2e:ci": "cypress run --browser chromium", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run --browser chromium",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run"

View File

@ -46,7 +46,7 @@ const onResponseError = (error) => {
message = responseError.message; message = responseError.message;
} }
switch (response.status) { switch (response?.status) {
case 500: case 500:
message = 'errors.statusInternalServerError'; message = 'errors.statusInternalServerError';
break; break;
@ -58,7 +58,7 @@ const onResponseError = (error) => {
break; break;
} }
if (session.isLoggedIn() && response.status === 401) { if (session.isLoggedIn() && response?.status === 401) {
session.destroy(); session.destroy();
const hash = window.location.hash; const hash = window.location.hash;
const url = hash.slice(1); const url = hash.slice(1);

View File

@ -8,6 +8,7 @@ import { useStateStore } from 'stores/useStateStore';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import SkeletonTable from 'components/ui/SkeletonTable.vue'; import SkeletonTable from 'components/ui/SkeletonTable.vue';
import { tMobile } from 'src/composables/tMobile';
const quasar = useQuasar(); const quasar = useQuasar();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -62,9 +63,10 @@ const hasChanges = ref(false);
const originalData = ref(); const originalData = ref();
const vnPaginateRef = ref(); const vnPaginateRef = ref();
const formData = ref(); const formData = ref();
const saveButtonRef = ref(null);
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const emit = defineEmits(['onFetch', 'update:selected']); const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({ defineExpose({
reload, reload,
@ -73,12 +75,9 @@ defineExpose({
onSubmit, onSubmit,
reset, reset,
hasChanges, hasChanges,
saveChanges,
}); });
function tMobile(...args) {
if (!quasar.platform.is.mobile) return t(...args);
}
async function fetch(data) { async function fetch(data) {
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
let $index = 0; let $index = 0;
@ -135,6 +134,7 @@ async function saveChanges(data) {
hasChanges.value = false; hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
emit('saveChanges', data);
} }
async function insert() { async function insert() {
@ -269,7 +269,7 @@ watch(formUrl, async () => {
<SkeletonTable v-if="!formData" /> <SkeletonTable v-if="!formData" />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn
:label="tMobile('globals.remove')" :label="tMobile('globals.remove')"
color="primary" color="primary"
@ -292,6 +292,7 @@ watch(formUrl, async () => {
/> />
<QBtn <QBtn
:label="tMobile('globals.save')" :label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary" color="primary"
icon="save" icon="save"
@click="onSubmit" @click="onSubmit"
@ -299,6 +300,7 @@ watch(formUrl, async () => {
:title="t('globals.save')" :title="t('globals.save')"
v-if="$props.defaultSave" v-if="$props.defaultSave"
/> />
<slot name="moreAfterActions" />
</QBtnGroup> </QBtnGroup>
</Teleport> </Teleport>
<QInnerLoading <QInnerLoading

View File

@ -19,6 +19,8 @@ const $props = defineProps({
const { optionLabel, options } = toRefs($props); const { optionLabel, options } = toRefs($props);
const myOptions = ref([]); const myOptions = ref([]);
const myOptionsOriginal = ref([]); const myOptionsOriginal = ref([]);
const vnSelectRef = ref(null);
function setOptions(data) { function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data)); myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data)); myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
@ -29,6 +31,7 @@ const filter = (val, options) => {
const search = val.toLowerCase(); const search = val.toLowerCase();
if (val === '') return options; if (val === '') return options;
return options.filter((row) => { return options.filter((row) => {
const id = row.id; const id = row.id;
const name = row[$props.optionLabel].toLowerCase(); const name = row[$props.optionLabel].toLowerCase();
@ -41,9 +44,17 @@ const filter = (val, options) => {
}; };
const filterHandler = (val, update) => { const filterHandler = (val, update) => {
update(() => { update(
myOptions.value = filter(val, myOptionsOriginal.value); () => {
}); myOptions.value = filter(val, myOptionsOriginal.value);
},
(ref) => {
if (val !== '' && ref.options.length > 0) {
ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true);
}
}
);
}; };
watch(options, (newValue) => { watch(options, (newValue) => {
@ -70,7 +81,15 @@ const value = computed({
map-options map-options
use-input use-input
@filter="filterHandler" @filter="filterHandler"
clearable hide-selected
clear-icon="close" fill-input
/> ref="vnSelectRef"
>
<template #append>
<QIcon name="close" @click.stop="value = null" class="cursor-pointer" />
</template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData">
<slot :name="slotName" v-bind="slotData" />
</template>
</QSelect>
</template> </template>

View File

@ -140,14 +140,6 @@ async function onLoad(...params) {
{{ t('No data to display') }} {{ t('No data to display') }}
</h5> </h5>
</div> </div>
<div
v-if="store.data && store.data.length === 0 && !isLoading"
class="info-row q-pa-md text-center"
>
<h5>
{{ t('No results found') }}
</h5>
</div>
<div <div
v-if="props.skeleton && props.autoLoad && !store.data" v-if="props.skeleton && props.autoLoad && !store.data"
class="card-list q-gutter-y-md" class="card-list q-gutter-y-md"

View File

@ -0,0 +1,8 @@
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
export function tMobile(...args) {
const quasar = useQuasar();
const { t } = useI18n();
if (!quasar.platform.is.mobile) return t(...args);
}

View File

@ -1,12 +1,15 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import CrudModel from 'components/CrudModel.vue'; import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelectFilter from 'components/common/VnSelectFilter.vue'; import VnSelectFilter from 'components/common/VnSelectFilter.vue';
import { getUrl } from 'composables/getUrl';
import { tMobile } from 'composables/tMobile';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const claimDevelopmentForm = ref(); const claimDevelopmentForm = ref();
@ -16,6 +19,12 @@ const claimResponsibles = ref([]);
const claimRedeliveries = ref([]); const claimRedeliveries = ref([]);
const workers = ref([]); const workers = ref([]);
const selected = ref([]); const selected = ref([]);
const insertButtonRef = ref();
let salixUrl;
onMounted(async () => {
salixUrl = await getUrl(`claim/${route.params.id}`);
});
const developmentsFilter = { const developmentsFilter = {
fields: [ fields: [
@ -43,6 +52,7 @@ const columns = computed(() => [
model: 'claimReasonFk', model: 'claimReasonFk',
optionValue: 'id', optionValue: 'id',
optionLabel: 'description', optionLabel: 'description',
tabIndex: 1,
}, },
{ {
name: 'claimResult', name: 'claimResult',
@ -54,6 +64,7 @@ const columns = computed(() => [
model: 'claimResultFk', model: 'claimResultFk',
optionValue: 'id', optionValue: 'id',
optionLabel: 'description', optionLabel: 'description',
tabIndex: 2,
}, },
{ {
name: 'claimResponsible', name: 'claimResponsible',
@ -65,6 +76,7 @@ const columns = computed(() => [
model: 'claimResponsibleFk', model: 'claimResponsibleFk',
optionValue: 'id', optionValue: 'id',
optionLabel: 'description', optionLabel: 'description',
tabIndex: 3,
}, },
{ {
name: 'worker', name: 'worker',
@ -75,6 +87,7 @@ const columns = computed(() => [
model: 'workerFk', model: 'workerFk',
optionValue: 'id', optionValue: 'id',
optionLabel: 'nickname', optionLabel: 'nickname',
tabIndex: 4,
}, },
{ {
name: 'claimRedelivery', name: 'claimRedelivery',
@ -86,8 +99,13 @@ const columns = computed(() => [
model: 'claimRedeliveryFk', model: 'claimRedeliveryFk',
optionValue: 'id', optionValue: 'id',
optionLabel: 'description', optionLabel: 'description',
tabIndex: 5,
}, },
]); ]);
function goToAction() {
location.href = `${salixUrl}/action`;
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -115,8 +133,9 @@ const columns = computed(() => [
auto-load auto-load
/> />
<FetchData <FetchData
url="Workers/activeWithInheritedRole" url="Workers/search"
:where="{ role: 'employee' }" :where="{ active: 1 }"
order="name ASC"
@on-fetch="(data) => (workers = data)" @on-fetch="(data) => (workers = data)"
auto-load auto-load
/> />
@ -129,6 +148,8 @@ const columns = computed(() => [
:data-required="{ claimFk: route.params.id }" :data-required="{ claimFk: route.params.id }"
v-model:selected="selected" v-model:selected="selected"
auto-load auto-load
@save-changes="goToAction"
:default-save="false"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
@ -142,19 +163,40 @@ const columns = computed(() => [
:grid="$q.screen.lt.md" :grid="$q.screen.lt.md"
> >
<template #body-cell="{ row, col }"> <template #body-cell="{ row, col }">
<QTd auto-width> <QTd
auto-width
@keyup.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
>
<VnSelectFilter <VnSelectFilter
:label="col.label" :label="col.label"
v-model="row[col.model]" v-model="row[col.model]"
:options="col.options" :options="col.options"
:option-value="col.optionValue" :option-value="col.optionValue"
:option-label="col.optionLabel" :option-label="col.optionLabel"
/> :autofocus="col.tabIndex == 1"
input-debounce="0"
>
<template #option="scope" v-if="col.name == 'worker'">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>
{{ scope.opt?.nickname }}
{{ scope.opt?.code }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelectFilter>
</QTd> </QTd>
</template> </template>
<template #item="props"> <template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition"> <div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat> <QCard
bordered
flat
@keyup.ctrl.enter.stop="claimDevelopmentForm?.saveChanges()"
>
<QCardSection> <QCardSection>
<QCheckbox v-model="props.selected" dense /> <QCheckbox v-model="props.selected" dense />
</QCardSection> </QCardSection>
@ -169,6 +211,8 @@ const columns = computed(() => [
:option-value="col.optionValue" :option-value="col.optionValue"
:option-label="col.optionLabel" :option-label="col.optionLabel"
dense dense
input-debounce="0"
:autofocus="col.tabIndex == 1"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -178,9 +222,28 @@ const columns = computed(() => [
</template> </template>
</QTable> </QTable>
</template> </template>
<template #moreAfterActions>
<QBtn
:label="tMobile('globals.save')"
ref="saveButtonRef"
color="primary"
icon="save"
:disable="!claimDevelopmentForm?.hasChanges"
@click="claimDevelopmentForm?.onSubmit"
:title="t('globals.save')"
/>
</template>
</CrudModel> </CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="claimDevelopmentForm.insert()" /> <QBtn
ref="insertButtonRef"
fab
color="primary"
icon="add"
@click="claimDevelopmentForm.insert()"
@keydown.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
@keydown.enter.stop
/>
</QPageSticky> </QPageSticky>
</template> </template>

View File

@ -40,7 +40,6 @@ const claimLinesForm = ref();
const claim = ref(null); const claim = ref(null);
async function onFetchClaim(data) { async function onFetchClaim(data) {
claim.value = data; claim.value = data;
fetchMana(); fetchMana();
} }
@ -147,8 +146,11 @@ function showImportDialog() {
quasar quasar
.dialog({ .dialog({
component: ClaimLinesImport, component: ClaimLinesImport,
componentProps: {
ticketId: claim.value.ticketFk,
},
}) })
.onOk(() => arrayData.refresh()); .onOk(() => claimLinesForm.value.reload());
} }
</script> </script>
<template> <template>

View File

@ -14,6 +14,13 @@ const route = useRoute();
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({
ticketId: {
type: Number,
required: true,
},
});
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'delivered', name: 'delivered',
@ -99,7 +106,7 @@ function cancel() {
</script> </script>
<template> <template>
<FetchData <FetchData
url="Sales/getClaimableFromTicket?ticketFk=16" :url="`Sales/getClaimableFromTicket?ticketFk=${$props.ticketId}`"
@on-fetch="(data) => (claimableSales = data)" @on-fetch="(data) => (claimableSales = data)"
auto-load auto-load
/> />

View File

@ -13,15 +13,17 @@ describe('ClaimDevelopment', () => {
it('should reset line', () => { it('should reset line', () => {
cy.selectOption(firstLineReason, 'Novato'); cy.selectOption(firstLineReason, 'Novato');
cy.resetCard(); cy.resetCard();
cy.getValue(firstLineReason).should('have.text', 'Prisas'); cy.getValue(firstLineReason).should('have.value', 'Prisas');
}); });
it('should edit line', () => { it('should edit line', () => {
cy.selectOption(firstLineReason, 'Novato'); cy.selectOption(firstLineReason, 'Novato');
cy.saveCard();
cy.reload(); cy.saveCard();
cy.getValue(firstLineReason).should('have.text', 'Novato'); cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
cy.getValue(firstLineReason).should('have.value', 'Novato');
//Restart data //Restart data
cy.selectOption(firstLineReason, 'Prisas'); cy.selectOption(firstLineReason, 'Prisas');
@ -29,13 +31,16 @@ describe('ClaimDevelopment', () => {
}); });
it('should add and remove new line', () => { it('should add and remove new line', () => {
//add row
cy.addCard(); cy.addCard();
cy.get(thirdRow).should('exist'); cy.get(thirdRow).should('exist');
const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour']; const rowData = [false, 'Novato', 'Roces', 'Compradores', 'employeeNick', 'Tour'];
cy.fillRow(thirdRow, rowData); cy.fillRow(thirdRow, rowData);
cy.saveCard(); cy.saveCard();
cy.login('developer');
cy.visit(`/#/claim/${claimId}/development`);
cy.validateRow(thirdRow, rowData); cy.validateRow(thirdRow, rowData);
cy.reload(); cy.reload();

View File

@ -54,7 +54,7 @@ Cypress.Commands.add('getValue', (selector) => {
else if ($el.find('.q-select__dropdown-icon').length) { else if ($el.find('.q-select__dropdown-icon').length) {
return cy.get( return cy.get(
selector + selector +
'> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > span' '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
); );
} else { } else {
// Puedes añadir un log o lanzar un error si el elemento no es reconocido // Puedes añadir un log o lanzar un error si el elemento no es reconocido
@ -76,7 +76,6 @@ Cypress.Commands.add('checkOption', (selector) => {
// Global buttons // Global buttons
Cypress.Commands.add('saveCard', () => { Cypress.Commands.add('saveCard', () => {
cy.get('[title="Save"]').click(); cy.get('[title="Save"]').click();
cy.get('[title="Save"]').should('have.class', 'disabled');
}); });
Cypress.Commands.add('resetCard', () => { Cypress.Commands.add('resetCard', () => {
cy.get('[title="Reset"]').click(); cy.get('[title="Reset"]').click();
@ -123,7 +122,7 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`); cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
continue; continue;
} }
cy.getValue(`:nth-child(${index + 1})`).should('have.text', value); cy.getValue(`:nth-child(${index + 1})`).should('have.value', value);
} }
}); });
}); });