diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue index 21ad0e41a..4128fa44c 100644 --- a/src/components/CrudModel.vue +++ b/src/components/CrudModel.vue @@ -131,11 +131,10 @@ async function fetch(data) { const rows = keyData ? data[keyData] : data; resetData(rows); emit('onFetch', rows); - $props.insertOnLoad && await insert(); + $props.insertOnLoad && (await insert()); return rows; } - function resetData(data) { if (!data) return; if (data && Array.isArray(data)) { @@ -146,15 +145,22 @@ function resetData(data) { formData.value = JSON.parse(JSON.stringify(data)); if (watchChanges.value) watchChanges.value(); //destroy watcher - watchChanges.value = watch(formData, (nVal) => { - hasChanges.value = false; - const filteredNewData = nVal.filter(row => !isRowEmpty(row) || row[$props.primaryKey]); - const filteredOriginal = originalData.value.filter(row => row[$props.primaryKey]); + watchChanges.value = watch( + formData, + (nVal) => { + 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 }); + const changes = getDifferences(filteredOriginal, filteredNewData); + hasChanges.value = !isEmpty(changes); + }, + { deep: true }, + ); } async function reset() { await fetch(originalData.value); @@ -183,9 +189,8 @@ async function onSubmit() { }); } isLoading.value = true; - + await saveChanges($props.saveFn ? formData.value : null); - } async function onSubmitAndGo() { @@ -194,10 +199,10 @@ async function onSubmitAndGo() { } async function saveChanges(data) { - formData.value = formData.value.filter(row => - row[$props.primaryKey] || !isRowEmpty(row) + formData.value = formData.value.filter( + (row) => row[$props.primaryKey] || !isRowEmpty(row), ); - + if ($props.saveFn) { $props.saveFn(data, getChanges); isLoading.value = false; @@ -228,31 +233,29 @@ async function saveChanges(data) { } async function insert(pushData = { ...$props.dataRequired, ...$props.dataDefault }) { - formData.value = formData.value.filter(row => !isRowEmpty(row)); + formData.value = formData.value.filter((row) => !isRowEmpty(row)); const lastRow = formData.value.at(-1); const isLastRowEmpty = lastRow ? isRowEmpty(lastRow) : false; - + if (formData.value.length && isLastRowEmpty) return; 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)); + + 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)); + return Object.keys(row).every((key) => isChange(row, key)); } - -function 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({ @@ -270,7 +273,9 @@ async function remove(data) { (form) => !preRemove.some((index) => index == form.$index), ); formData.value = newData; - hasChanges.value = JSON.stringify(removeIndexField(formData.value)) !== JSON.stringify(removeIndexField(originalData.value)); + hasChanges.value = + JSON.stringify(removeIndexField(formData.value)) !== + JSON.stringify(removeIndexField(originalData.value)); } if (ids.length) { quasar @@ -286,7 +291,7 @@ async function remove(data) { }) .onOk(async () => { newData = newData.filter((form) => !ids.some((id) => id == form[pk])); - fetch(newData); + await reload(); }); } diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index 29f5f1a5a..c9b896331 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -254,17 +254,13 @@ async function save() { old: originalData.value, }); if ($props.reload) await arrayData.fetch({}); + if ($props.goTo) push({ path: $props.goTo }); hasChanges.value = false; } finally { isLoading.value = false; } } -async function saveAndGo() { - await save(); - push({ path: $props.goTo }); -} - function reset() { formData.value = JSON.parse(JSON.stringify(originalData.value)); updateAndEmit('onFetch', { val: originalData.value }); @@ -385,7 +381,7 @@ defineExpose({ diff --git a/src/components/VnTable/VnColumn.vue b/src/components/VnTable/VnColumn.vue index 05c764d73..952b54583 100644 --- a/src/components/VnTable/VnColumn.vue +++ b/src/components/VnTable/VnColumn.vue @@ -185,6 +185,7 @@ const col = computed(() => { newColumn.attrs = { ...newColumn.component?.attrs, autofocus: $props.autofocus }; newColumn.event = { ...newColumn.component?.event, ...$props?.eventHandlers }; } + return newColumn; }); diff --git a/src/components/VnTable/VnContextMenu.vue b/src/components/VnTable/VnContextMenu.vue index c20c213f5..a99b2c478 100644 --- a/src/components/VnTable/VnContextMenu.vue +++ b/src/components/VnTable/VnContextMenu.vue @@ -18,7 +18,7 @@ const arrayData = defineModel({ function handler(event) { const clickedElement = event.target.closest('td'); if (!clickedElement) return; - + event.preventDefault(); target.value = event.target; qmenuRef.value.show(); colField.value = clickedElement.getAttribute('data-col-field'); diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue index 93ced44d8..09f974cc4 100644 --- a/src/components/VnTable/VnTable.vue +++ b/src/components/VnTable/VnTable.vue @@ -151,6 +151,10 @@ const $props = defineProps({ type: String, default: 'vnTable', }, + selectionFn: { + type: Function, + default: null, + }, }); const { t } = useI18n(); @@ -218,10 +222,7 @@ onBeforeMount(() => { onMounted(async () => { if ($props.isEditable) document.addEventListener('click', clickHandler); - document.addEventListener('contextmenu', (event) => { - event.preventDefault(); - contextMenuRef.value.handler(event); - }); + document.addEventListener('contextmenu', contextMenuRef.value.handler); mode.value = quasar.platform.is.mobile && !$props.disableOption?.card ? CARD_MODE @@ -338,10 +339,10 @@ function stopEventPropagation(event) { event.stopPropagation(); } -function reload(params) { +async function reload(params) { selected.value = []; selectAll.value = false; - CrudModelRef.value.reload(params); + await CrudModelRef.value.reload(params); } function columnName(col) { @@ -395,12 +396,14 @@ function hasEditableFormat(column) { } const clickHandler = async (event) => { - const clickedElement = event.target.closest('td'); - const isDateElement = event.target.closest('.q-date'); - const isTimeElement = event.target.closest('.q-time'); - const isQSelectDropDown = event.target.closest('.q-select__dropdown-icon'); + const el = event.target; + const clickedElement = el.closest('td'); + const isDateElement = el.closest('.q-date'); + const isTimeElement = el.closest('.q-time'); + const isQSelectDropDown = el.closest('.q-select__dropdown-icon'); + const isDialog = el.closest('.q-dialog'); - if (isDateElement || isTimeElement || isQSelectDropDown) return; + if (isDateElement || isTimeElement || isQSelectDropDown || isDialog) return; if (clickedElement === null) { await destroyInput(editingRow.value, editingField.value); @@ -447,6 +450,7 @@ async function renderInput(rowId, field, clickedElement) { const row = CrudModelRef.value.formData[rowId]; const oldValue = CrudModelRef.value.formData[rowId][column?.name]; + if (column.disable) return; if (!clickedElement) clickedElement = document.querySelector( `[data-row-index="${rowId}"][data-col-field="${field}"]`, @@ -480,6 +484,7 @@ async function renderInput(rowId, field, clickedElement) { await destroyInput(rowId, field, clickedElement); }, keydown: async (event) => { + await column?.cellEvent?.['keydown']?.(event, row); switch (event.key) { case 'Tab': await handleTabKey(event, rowId, field); @@ -655,7 +660,9 @@ const rowCtrlClickFunction = computed(() => { }); const handleHeaderSelection = (evt, data) => { if (evt === 'updateSelected' && selectAll.value) { - selected.value = tableRef.value.rows; + const fn = $props.selectionFn; + const rows = tableRef.value.rows; + selected.value = fn ? fn(rows) : rows; } else if (evt === 'selectAll') { selected.value = data; } else { @@ -701,7 +708,6 @@ const handleHeaderSelection = (evt, data) => { :search-url="searchUrl" :disable-infinite-scroll="isTableMode" :before-save-fn="removeTextValue" - @save-changes="reload" :has-sub-toolbar="$props.hasSubToolbar ?? isEditable" :auto-load="hasParams || $attrs['auto-load']" > @@ -729,7 +735,15 @@ const handleHeaderSelection = (evt, data) => { :virtual-scroll="isTableMode" @virtual-scroll="onVirtualScroll" @row-click="(event, row) => handleRowClick(event, row)" - @update:selected="emit('update:selected', $event)" + @update:selected=" + (evt) => { + if ($props.selectionFn) selected = $props.selectionFn(evt); + emit( + 'update:selected', + selectionFn ? selectionFn(selected) : selected, + ); + } + " @selection="(details) => handleSelection(details, rows)" :hide-selected-banner="true" :data-cy diff --git a/src/components/__tests__/vnAccountNumber.spec.js b/src/components/__tests__/vnAccountNumber.spec.js new file mode 100644 index 000000000..688e50e6a --- /dev/null +++ b/src/components/__tests__/vnAccountNumber.spec.js @@ -0,0 +1,43 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { createWrapper } from 'app/test/vitest/helper'; +import VnAccountNumber from 'src/components/common/VnAccountNumber.vue'; + +describe('VnAccountNumber', () => { + let wrapper; + let input; + let vnInput; + let spyShort; + + beforeEach(() => { + wrapper = createWrapper(VnAccountNumber); + wrapper = wrapper.wrapper; + input = wrapper.find('input'); + vnInput = wrapper.findComponent({ name: 'VnInput' }); + spyShort = vi.spyOn(wrapper.vm, 'useAccountShortToStandard'); + }); + + it('should filter out non-numeric characters on input event', async () => { + await input.setValue('abc123.45!@#'); + const emitted = wrapper.emitted('update:modelValue'); + expect(emitted.pop()[0]).toBe('123.45'); + expect(spyShort).not.toHaveBeenCalled(); + }); + + it('should apply conversion on blur when valid short value is provided', async () => { + await input.setValue('123.45'); + await vnInput.trigger('blur'); + + const emitted = wrapper.emitted('update:modelValue'); + expect(emitted.pop()[0]).toBe('1230000045'); + expect(spyShort).toHaveBeenCalled(); + }); + + it('should not change value for invalid input values', async () => { + await input.setValue('123'); + await vnInput.trigger('blur'); + + const emitted = wrapper.emitted('update:modelValue'); + expect(emitted.pop()[0]).toBe('123'); + expect(spyShort).toHaveBeenCalled(); + }); +}); diff --git a/src/components/common/VnChangePassword.vue b/src/components/common/VnChangePassword.vue index d8374498f..9fbc8fda8 100644 --- a/src/components/common/VnChangePassword.vue +++ b/src/components/common/VnChangePassword.vue @@ -36,8 +36,6 @@ const validate = async () => { isLoading.value = true; await props.submitFn(newPassword, oldPassword); emit('onSubmit'); - } catch (e) { - notify('errors.writeRequest', 'negative'); } finally { changePassDialog.value.hide(); isLoading.value = false; diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue index 474d68116..3097ade81 100644 --- a/src/components/common/VnInput.vue +++ b/src/components/common/VnInput.vue @@ -6,13 +6,7 @@ import { useRequired } from 'src/composables/useRequired'; const $attrs = useAttrs(); const { isRequired, requiredFieldRule } = useRequired($attrs); const { t } = useI18n(); -const emit = defineEmits([ - 'update:modelValue', - 'update:options', - 'keyup.enter', - 'remove', - 'blur', -]); +const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const $props = defineProps({ modelValue: { @@ -126,6 +120,14 @@ const handleInsertMode = (e) => { const handleUppercase = () => { value.value = value.value?.toUpperCase() || ''; }; + +const listeners = computed(() => + Object.fromEntries( + Object.entries($attrs).filter( + ([key, val]) => key.startsWith('on') && typeof val === 'function', + ), + ), +);