diff --git a/package.json b/package.json index 19b4c7a6f..b7b04287d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "25.16.0", + "version": "25.18.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -89,4 +89,4 @@ "vite": "^6.0.11", "vitest": "^0.31.1" } -} +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 27cc34c38..0217c45c2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,6 +2,7 @@ import { onMounted } from 'vue'; import { useQuasar, Dark } from 'quasar'; import { useI18n } from 'vue-i18n'; +import VnScroll from './components/common/VnScroll.vue'; const quasar = useQuasar(); const { availableLocales, locale, fallbackLocale } = useI18n(); @@ -38,6 +39,7 @@ quasar.iconMapFn = (iconName) => { + diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 32a8db16f..0afe59877 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -54,6 +54,10 @@ const $props = defineProps({ type: [Array], default: () => [], }, + filterFn: { + type: Function, + default: null, + }, exprBuilder: { type: Function, default: null, @@ -62,16 +66,12 @@ const $props = defineProps({ type: Boolean, default: true, }, - defaultFilter: { - type: Boolean, - default: true, - }, fields: { type: Array, default: null, }, include: { - type: [Object, Array], + type: [Object, Array, String], default: null, }, where: { @@ -79,7 +79,7 @@ const $props = defineProps({ default: null, }, sortBy: { - type: String, + type: [String, Array], default: null, }, limit: { @@ -152,10 +152,22 @@ const value = computed({ }, }); +const arrayDataKey = + $props.dataKey ?? + ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); + +const arrayData = useArrayData(arrayDataKey, { + url: $props.url, + searchUrl: false, + mapKey: $attrs['map-key'], +}); + const computedSortBy = computed(() => { return $props.sortBy || $props.optionLabel + ' ASC'; }); +const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); + watch(options, (newValue) => { setOptions(newValue); }); @@ -174,16 +186,6 @@ onMounted(() => { if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300); }); -const arrayDataKey = - $props.dataKey ?? - ($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label)); - -const arrayData = useArrayData(arrayDataKey, { - url: $props.url, - searchUrl: false, - mapKey: $attrs['map-key'], -}); - function findKeyInOptions() { if (!$props.options) return; return filter($props.modelValue, $props.options)?.length; @@ -252,43 +254,41 @@ async function fetchFilter(val) { } async function filterHandler(val, update) { - if (isLoading.value) return update(); - if (!val && lastVal.value === val) { - lastVal.value = val; - return update(); - } - lastVal.value = val; let newOptions; - if (!$props.defaultFilter) return update(); - if ( - $props.url && - ($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0)) - ) { - newOptions = await fetchFilter(val); - } else newOptions = filter(val, myOptionsOriginal.value); - update( - () => { - if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) - newOptions.unshift(noOneOpt.value); + if ($props.filterFn) update($props.filterFn(val)); + else if (!val && lastVal.value === val) update(); + else { + const makeRequest = + ($props.url && $props.limit) || + (!$props.limit && Object.keys(myOptions.value).length === 0); + newOptions = makeRequest + ? await fetchFilter(val) + : filter(val, myOptionsOriginal.value); - myOptions.value = newOptions; - }, - (ref) => { - if (val !== '' && ref.options.length > 0) { - ref.setOptionIndex(-1); - ref.moveOptionSelection(1, true); - } - }, - ); + update( + () => { + if ($props.noOne && noOneText.toLowerCase().includes(val.toLowerCase())) + newOptions.unshift(noOneOpt.value); + + myOptions.value = newOptions; + }, + (ref) => { + if (val !== '' && ref.options.length > 0) { + ref.setOptionIndex(-1); + ref.moveOptionSelection(1, true); + } + }, + ); + } + + lastVal.value = val; } function nullishToTrue(value) { return value ?? true; } -const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); - async function onScroll({ to, direction, from, index }) { const lastIndex = myOptions.value.length - 1; diff --git a/src/components/common/VnSmsDialog.vue b/src/components/common/VnSmsDialog.vue index 8851a33b2..ada2d02fa 100644 --- a/src/components/common/VnSmsDialog.vue +++ b/src/components/common/VnSmsDialog.vue @@ -232,7 +232,7 @@ fr: pt: Portugais pt: Send SMS: Enviar SMS - CustomerDefaultLanguage: Este cliente utiliza o {locale} como seu idioma padrão + CustomerDefaultLanguage: Este cliente utiliza o {locale} como seu idioma padrão Language: Linguagem Phone: Móvel Subject: Assunto diff --git a/src/components/common/__tests__/VnDiscount.spec.js b/src/components/common/__tests__/VnDiscount.spec.js index 5d5be61ac..34c4ff630 100644 --- a/src/components/common/__tests__/VnDiscount.spec.js +++ b/src/components/common/__tests__/VnDiscount.spec.js @@ -4,7 +4,7 @@ import VnDiscount from 'components/common/vnDiscount.vue'; describe('VnDiscount', () => { let vm; - + beforeAll(() => { vm = createWrapper(VnDiscount, { props: { @@ -12,7 +12,9 @@ describe('VnDiscount', () => { price: 100, quantity: 2, discount: 10, - } + mana: 10, + promise: vi.fn(), + }, }).vm; }); @@ -21,8 +23,8 @@ describe('VnDiscount', () => { }); describe('total', () => { - it('should calculate total correctly', () => { + it('should calculate total correctly', () => { expect(vm.total).toBe(180); }); }); -}); \ No newline at end of file +}); diff --git a/src/components/common/__tests__/VnDms.spec.js b/src/components/common/__tests__/VnDms.spec.js index 66d946db3..86f12169b 100644 --- a/src/components/common/__tests__/VnDms.spec.js +++ b/src/components/common/__tests__/VnDms.spec.js @@ -41,10 +41,12 @@ describe('VnDms', () => { companyFk: 2, dmsTypeFk: 3, description: 'This is a test description', - files: { - name: 'example.txt', - content: new Blob(['file content'], { type: 'text/plain' }), - }, + files: [ + { + name: 'example.txt', + content: new Blob(['file content'], { type: 'text/plain' }), + }, + ], }; const expectedBody = { @@ -83,7 +85,7 @@ describe('VnDms', () => { it('should map DMS data correctly and add file to FormData', () => { const [formData, params] = vm.mapperDms(data); - expect(formData.get('example.txt')).toBe(data.files); + expect([formData.get('example.txt')]).toStrictEqual(data.files); expect(expectedBody).toEqual(params.params); }); diff --git a/src/components/common/__tests__/VnInput.spec.js b/src/components/common/__tests__/VnInput.spec.js index 13f9ed804..8eb2c50c8 100644 --- a/src/components/common/__tests__/VnInput.spec.js +++ b/src/components/common/__tests__/VnInput.spec.js @@ -2,7 +2,6 @@ import { createWrapper } from 'app/test/vitest/helper'; import { vi, describe, expect, it } from 'vitest'; import VnInput from 'src/components/common/VnInput.vue'; - describe('VnInput', () => { let vm; let wrapper; @@ -11,26 +10,28 @@ describe('VnInput', () => { function generateWrapper(value, isOutlined, emptyToNull, insertable) { wrapper = createWrapper(VnInput, { props: { - modelValue: value, - isOutlined, emptyToNull, insertable, - maxlength: 101 + modelValue: value, + isOutlined, + emptyToNull, + insertable, + maxlength: 101, }, attrs: { label: 'test', required: true, maxlength: 101, maxLength: 10, - 'max-length':20 + 'max-length': 20, }, }); wrapper = wrapper.wrapper; vm = wrapper.vm; input = wrapper.find('[data-cy="test_input"]'); - }; + } describe('value', () => { it('should emit update:modelValue when value changes', async () => { - generateWrapper('12345', false, false, true) + generateWrapper('12345', false, false, true); await input.setValue('123'); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); expect(wrapper.emitted('update:modelValue')[0]).toEqual(['123']); @@ -46,37 +47,36 @@ describe('VnInput', () => { describe('styleAttrs', () => { it('should return empty styleAttrs when isOutlined is false', async () => { generateWrapper('123', false, false, false); - expect(vm.styleAttrs).toEqual({}); + expect(vm.styleAttrs).toEqual({}); }); - it('should set styleAttrs when isOutlined is true', async () => { + it('should set styleAttrs when isOutlined is true', async () => { generateWrapper('123', true, false, false); expect(vm.styleAttrs.outlined).toBe(true); }); }); - describe('handleKeydown', () => { + describe('handleKeydown', () => { it('should do nothing when "Backspace" key is pressed', async () => { generateWrapper('12345', false, false, true); await input.trigger('keydown', { key: 'Backspace' }); expect(wrapper.emitted('update:modelValue')).toBeUndefined(); const spyhandler = vi.spyOn(vm, 'handleInsertMode'); expect(spyhandler).not.toHaveBeenCalled(); - }); - + /* TODO: #8399 REDMINE */ it.skip('handleKeydown respects insertable behavior', async () => { const expectedValue = '12345'; generateWrapper('1234', false, false, true); - vm.focus() + vm.focus(); await input.trigger('keydown', { key: '5' }); await vm.$nextTick(); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); - expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue ]); - expect(vm.value).toBe( expectedValue); + expect(wrapper.emitted('update:modelValue')[0]).toEqual([expectedValue]); + expect(vm.value).toBe(expectedValue); }); }); @@ -86,6 +86,6 @@ describe('VnInput', () => { const focusSpy = vi.spyOn(input.element, 'focus'); vm.focus(); expect(focusSpy).toHaveBeenCalled(); - }); + }); }); }); diff --git a/src/components/common/__tests__/VnInputDateTime.spec.js b/src/components/common/__tests__/VnInputDateTime.spec.js new file mode 100644 index 000000000..adff1dbc4 --- /dev/null +++ b/src/components/common/__tests__/VnInputDateTime.spec.js @@ -0,0 +1,81 @@ +import { createWrapper } from 'app/test/vitest/helper.js'; +import { describe, it, expect, beforeAll } from 'vitest'; +import VnInputDateTime from 'components/common/VnInputDateTime.vue'; +import vnDateBoot from 'src/boot/vnDate'; + +let vm; +let wrapper; + +beforeAll(() => { + // Initialize the vnDate boot + vnDateBoot(); +}); +function generateWrapper(date, outlined, showEvent) { + wrapper = createWrapper(VnInputDateTime, { + props: { + modelValue: date, + isOutlined: outlined, + showEvent: showEvent, + }, + }); + wrapper = wrapper.wrapper; + vm = wrapper.vm; +} + +describe('VnInputDateTime', () => { + describe('selectedDate', () => { + it('formats a valid datetime correctly', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + expect(vm.selectedDate).toBe('25-12-2023 10:30'); + }); + + it('handles null date value', async () => { + generateWrapper(null, false, true); + await vm.$nextTick(); + expect(vm.selectedDate).not.toBe(null); + }); + + it('updates the model value when a new datetime is set', async () => { + vm.selectedDate = '31-12-2023 15:45'; + await vm.$nextTick(); + expect(wrapper.emitted()['update:modelValue']).toBeTruthy(); + }); + }); + + describe('styleAttrs', () => { + it('should return empty styleAttrs when isOutlined is false', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + expect(vm.styleAttrs).toEqual({}); + }); + + it('should set styleAttrs when isOutlined is true', async () => { + generateWrapper('2023-12-25T10:30:00', true, true); + await vm.$nextTick(); + expect(vm.styleAttrs).toEqual({ + dense: true, + outlined: true, + rounded: true, + }); + }); + }); + + describe('component rendering', () => { + it('should render date and time icons', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + const icons = wrapper.findAllComponents({ name: 'QIcon' }); + expect(icons.length).toBe(2); + expect(icons[0].props('name')).toBe('today'); + expect(icons[1].props('name')).toBe('access_time'); + }); + + it('should render popup proxies for date and time', async () => { + generateWrapper('2023-12-25T10:30:00', false, true); + await vm.$nextTick(); + const popups = wrapper.findAllComponents({ name: 'QPopupProxy' }); + expect(popups.length).toBe(2); + }); + }); +}); diff --git a/src/components/common/__tests__/VnLog.spec.js b/src/components/common/__tests__/VnLog.spec.js index fcb516cc5..399b78a1d 100644 --- a/src/components/common/__tests__/VnLog.spec.js +++ b/src/components/common/__tests__/VnLog.spec.js @@ -90,8 +90,10 @@ describe('VnLog', () => { vm = createWrapper(VnLog, { global: { - stubs: [], - mocks: {}, + stubs: ['FetchData', 'vue-i18n'], + mocks: { + fetch: vi.fn(), + }, }, propsData: { model: 'Claim', diff --git a/src/components/common/__tests__/VnNotes.spec.js b/src/components/common/__tests__/VnNotes.spec.js index e0514cc7b..0d256a736 100644 --- a/src/components/common/__tests__/VnNotes.spec.js +++ b/src/components/common/__tests__/VnNotes.spec.js @@ -26,7 +26,7 @@ describe('VnNotes', () => { ) { vi.spyOn(axios, 'get').mockResolvedValue({ data: [] }); wrapper = createWrapper(VnNotes, { - propsData: options, + propsData: { ...defaultOptions, ...options }, }); wrapper = wrapper.wrapper; vm = wrapper.vm; diff --git a/src/components/ui/EntityDescriptor.vue b/src/components/ui/EntityDescriptor.vue index a5dced551..e6adaa5f7 100644 --- a/src/components/ui/EntityDescriptor.vue +++ b/src/components/ui/EntityDescriptor.vue @@ -44,7 +44,8 @@ onBeforeMount(async () => { }); // It enables to load data only once if the module is the same as the dataKey - if (!isSameDataKey.value || !route.params.id) await getData(); + if (!isSameDataKey.value || !route.params.id || $props.id !== route.params.id) + await getData(); watch( () => [$props.url, $props.filter], async () => { @@ -58,7 +59,8 @@ async function getData() { store.filter = $props.filter ?? {}; isLoading.value = true; try { - const { data } = await arrayData.fetch({ append: false, updateRouter: false }); + await arrayData.fetch({ append: false, updateRouter: false }); + const { data } = store; state.set($props.dataKey, data); emit('onFetch', data); } finally { diff --git a/src/components/ui/VnDescriptor.vue b/src/components/ui/VnDescriptor.vue index 994233eb0..69fd5af6b 100644 --- a/src/components/ui/VnDescriptor.vue +++ b/src/components/ui/VnDescriptor.vue @@ -6,6 +6,7 @@ import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useRoute, useRouter } from 'vue-router'; import { useClipboard } from 'src/composables/useClipboard'; import VnMoreOptions from './VnMoreOptions.vue'; +import { getValueFromPath } from 'src/composables/getValueFromPath'; const entity = defineModel({ type: Object, default: null }); const $props = defineProps({ @@ -56,18 +57,6 @@ const routeName = computed(() => { return `${routeName}Summary`; }); -function getValueFromPath(path) { - if (!path) return; - const keys = path.toString().split('.'); - let current = entity.value; - - for (const key of keys) { - if (current[key] === undefined) return undefined; - else current = current[key]; - } - return current; -} - function copyIdText(id) { copyText(id, { component: { @@ -170,10 +159,10 @@ const toModule = computed(() => {
- {{ getValueFromPath(title) ?? title }} + {{ getValueFromPath(entity, title) ?? title }} { class="subtitle" :data-cy="`${$attrs['data-cy'] ?? 'vnDescriptor'}_subtitle`" > - #{{ getValueFromPath(subtitle) ?? entity.id }} + #{{ getValueFromPath(entity, subtitle) ?? entity.id }} { color="primary" style="position: fixed; z-index: 1; right: 0; bottom: 0" icon="search" + data-cy="vnFilterPanel_search" @click="search()" > @@ -229,6 +230,7 @@ const getLocale = (label) => { { $props.value);