-
-
- {{ t('Go to Salix') }}
-
-
-
-
-en:
- Go to Salix: Go to Salix
-es:
- Go to Salix: Ir a Salix
-
diff --git a/src/components/UserPanel.vue b/src/components/UserPanel.vue
index 810f63044..a0ef73a1f 100644
--- a/src/components/UserPanel.vue
+++ b/src/components/UserPanel.vue
@@ -87,10 +87,10 @@ async function saveDarkMode(value) {
async function saveLanguage(value) {
const query = `/VnUsers/${user.value.id}`;
try {
- await axios.patch(query, {
- lang: value,
- });
+ await axios.patch(query, { lang: value });
+
user.value.lang = value;
+ useState().setUser(user.value);
onDataSaved();
} catch (error) {
onDataError();
diff --git a/src/components/VnTable/VnFilter.vue b/src/components/VnTable/VnFilter.vue
index 999133130..426f5c716 100644
--- a/src/components/VnTable/VnFilter.vue
+++ b/src/components/VnTable/VnFilter.vue
@@ -32,7 +32,10 @@ const $props = defineProps({
defineExpose({ addFilter, props: $props });
const model = defineModel(undefined, { required: true });
-const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
+const arrayData = useArrayData(
+ $props.dataKey,
+ $props.searchUrl ? { searchUrl: $props.searchUrl } : null
+);
const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter };
diff --git a/src/components/VnTable/VnTable.vue b/src/components/VnTable/VnTable.vue
index 9ab080276..07992f616 100644
--- a/src/components/VnTable/VnTable.vue
+++ b/src/components/VnTable/VnTable.vue
@@ -1,20 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t(`${chipLocale}.${tag.label}`) }}:
+ {{ formatFn(tag.value) }}
+
+
+
+
diff --git a/test/vitest/__tests__/components/VnTable.spec.js b/src/components/VnTable/__tests__/VnTable.spec.js
similarity index 82%
rename from test/vitest/__tests__/components/VnTable.spec.js
rename to src/components/VnTable/__tests__/VnTable.spec.js
index 162df727d..74ba06987 100644
--- a/test/vitest/__tests__/components/VnTable.spec.js
+++ b/src/components/VnTable/__tests__/VnTable.spec.js
@@ -1,4 +1,4 @@
-import { describe, expect, it, beforeAll, beforeEach } from 'vitest';
+import { describe, expect, it, beforeAll, beforeEach, vi } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnTable from 'src/components/VnTable/VnTable.vue';
@@ -13,6 +13,15 @@ describe('VnTable', () => {
},
});
vm = wrapper.vm;
+
+ vi.mock('src/composables/useFilterParams', () => {
+ return {
+ useFilterParams: vi.fn(() => ({
+ params: {},
+ orders: {},
+ })),
+ };
+ });
});
beforeEach(() => (vm.selected = []));
diff --git a/src/components/__tests__/CrudModel.spec.js b/src/components/__tests__/CrudModel.spec.js
new file mode 100644
index 000000000..e0afd30ad
--- /dev/null
+++ b/src/components/__tests__/CrudModel.spec.js
@@ -0,0 +1,248 @@
+import { createWrapper, axios } from 'app/test/vitest/helper';
+import CrudModel from 'components/CrudModel.vue';
+import { vi, afterEach, beforeEach, beforeAll, describe, expect, it } from 'vitest';
+
+describe('CrudModel', () => {
+ let wrapper;
+ let vm;
+ let data;
+ beforeAll(() => {
+ wrapper = createWrapper(CrudModel, {
+ global: {
+ stubs: [
+ 'vnPaginate',
+ 'useState',
+ 'arrayData',
+ 'useStateStore',
+ 'vue-i18n',
+ ],
+ mocks: {
+ validate: vi.fn(),
+ },
+ },
+ propsData: {
+ dataRequired: {
+ fk: 1,
+ },
+ dataKey: 'crudModelKey',
+ model: 'crudModel',
+ url: 'crudModelUrl',
+ saveFn: '',
+ },
+ });
+ wrapper=wrapper.wrapper;
+ vm=wrapper.vm;
+ });
+
+ beforeEach(() => {
+ vm.fetch([]);
+ vm.watchChanges = null;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('insert()', () => {
+ it('should new element in list with index 0 if formData not has data', () => {
+ vm.insert();
+
+ expect(vm.formData.length).toEqual(1);
+ expect(vm.formData[0].fk).toEqual(1);
+ expect(vm.formData[0].$index).toEqual(0);
+ });
+ });
+
+ describe('getChanges()', () => {
+ it('should return correct updates and creates', async () => {
+ vm.fetch([
+ { id: 1, name: 'New name one' },
+ { id: 2, name: 'New name two' },
+ { id: 3, name: 'Bruce Wayne' },
+ ]);
+
+ vm.originalData = [
+ { id: 1, name: 'Tony Starks' },
+ { id: 2, name: 'Jessica Jones' },
+ { id: 3, name: 'Bruce Wayne' },
+ ];
+
+ vm.insert();
+ const result = vm.getChanges();
+
+ const expected = {
+ creates: [
+ {
+ $index: 3,
+ fk: 1,
+ },
+ ],
+ updates: [
+ {
+ data: {
+ name: 'New name one',
+ },
+ where: {
+ id: 1,
+ },
+ },
+ {
+ data: {
+ name: 'New name two',
+ },
+ where: {
+ id: 2,
+ },
+ },
+ ],
+ };
+
+ expect(result).toEqual(expected);
+ });
+ });
+
+ describe('getDifferences()', () => {
+ it('should return the differences between two objects', async () => {
+ const obj1 = {
+ a: 1,
+ b: 2,
+ c: 3,
+ };
+ const obj2 = {
+ a: null,
+ b: 4,
+ d: 5,
+ };
+
+ const result = vm.getDifferences(obj1, obj2);
+
+ expect(result).toEqual({
+ a: null,
+ b: 4,
+ d: 5,
+ });
+ });
+ });
+
+ describe('isEmpty()', () => {
+ let dummyObj;
+ let dummyArray;
+ let result;
+ it('should return true if object si null', async () => {
+ dummyObj = null;
+ result = vm.isEmpty(dummyObj);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true if object si undefined', async () => {
+ dummyObj = undefined;
+ result = vm.isEmpty(dummyObj);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return true if object is empty', async () => {
+ dummyObj ={};
+ result = vm.isEmpty(dummyObj);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if object is not empty', async () => {
+ dummyObj = {a:1, b:2, c:3};
+ result = vm.isEmpty(dummyObj);
+
+ expect(result).toBe(false);
+ });
+
+ it('should return true if array is empty', async () => {
+ dummyArray = [];
+ result = vm.isEmpty(dummyArray);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if array is not empty', async () => {
+ dummyArray = [1,2,3];
+ result = vm.isEmpty(dummyArray);
+
+ expect(result).toBe(false);
+ })
+ });
+
+ describe('resetData()', () => {
+ it('should add $index to elements in data[] and sets originalData and formData with data', async () => {
+ data = [{
+ name: 'Tony',
+ lastName: 'Stark',
+ age: 42,
+ }];
+
+ vm.resetData(data);
+
+ expect(vm.originalData).toEqual(data);
+ expect(vm.originalData[0].$index).toEqual(0);
+ expect(vm.formData).toEqual(data);
+ expect(vm.formData[0].$index).toEqual(0);
+ expect(vm.watchChanges).not.toBeNull();
+ });
+
+ it('should dont do nothing if data is null', async () => {
+ vm.resetData(null);
+
+ expect(vm.watchChanges).toBeNull();
+ });
+
+ it('should set originalData and formatData with data and generate watchChanges', async () => {
+ data = {
+ name: 'Tony',
+ lastName: 'Stark',
+ age: 42,
+ };
+
+ vm.resetData(data);
+
+ expect(vm.originalData).toEqual(data);
+ expect(vm.formData).toEqual(data);
+ expect(vm.watchChanges).not.toBeNull();
+ });
+ });
+
+ describe('saveChanges()', () => {
+ data = [{
+ name: 'Tony',
+ lastName: 'Stark',
+ age: 42,
+ }];
+
+ it('should call saveFn if exists', async () => {
+ await wrapper.setProps({ saveFn: vi.fn() });
+
+ vm.saveChanges(data);
+
+ expect(vm.saveFn).toHaveBeenCalledOnce();
+ expect(vm.isLoading).toBe(false);
+ expect(vm.hasChanges).toBe(false);
+
+ await wrapper.setProps({ saveFn: '' });
+ });
+
+ it("should use default url if there's not saveFn", async () => {
+ const postMock =vi.spyOn(axios, 'post');
+
+ vm.formData = [{
+ name: 'Bruce',
+ lastName: 'Wayne',
+ age: 45,
+ }]
+
+ await vm.saveChanges(data);
+
+ expect(postMock).toHaveBeenCalledWith(vm.url + '/crud', data);
+ expect(vm.isLoading).toBe(false);
+ expect(vm.hasChanges).toBe(false);
+ expect(vm.originalData).toEqual(JSON.parse(JSON.stringify(vm.formData)));
+ });
+ });
+});
diff --git a/src/components/__tests__/EditTableCellValueForm.spec.js b/src/components/__tests__/EditTableCellValueForm.spec.js
new file mode 100644
index 000000000..fa47d8f73
--- /dev/null
+++ b/src/components/__tests__/EditTableCellValueForm.spec.js
@@ -0,0 +1,56 @@
+import { createWrapper, axios } from 'app/test/vitest/helper';
+import EditForm from 'components/EditTableCellValueForm.vue';
+import { vi, afterEach, beforeAll, describe, expect, it } from 'vitest';
+
+const fieldA = 'fieldA';
+const fieldB = 'fieldB';
+
+describe('EditForm', () => {
+ let vm;
+ const mockRows = [
+ { id: 1, itemFk: 101 },
+ { id: 2, itemFk: 102 },
+ ];
+ const mockFieldsOptions = [
+ { label: 'Field A', field: fieldA, component: 'input', attrs: {} },
+ { label: 'Field B', field: fieldB, component: 'date', attrs: {} },
+ ];
+ const editUrl = '/api/edit';
+
+ beforeAll(() => {
+ vi.spyOn(axios, 'post').mockResolvedValue({ status: 200 });
+ vm = createWrapper(EditForm, {
+ props: {
+ rows: mockRows,
+ fieldsOptions: mockFieldsOptions,
+ editUrl,
+ },
+ }).vm;
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('onSubmit()', () => {
+ it('should call axios.post with the correct parameters in the payload', async () => {
+ const selectedField = { field: fieldA, component: 'input', attrs: {} };
+ const newValue = 'Test Value';
+
+ vm.selectedField = selectedField;
+ vm.newValue = newValue;
+
+ await vm.onSubmit();
+
+ const payload = axios.post.mock.calls[0][1];
+
+ expect(axios.post).toHaveBeenCalledWith(editUrl, expect.any(Object));
+ expect(payload.field).toEqual(fieldA);
+ expect(payload.newValue).toEqual(newValue);
+
+ expect(payload.lines).toEqual(expect.arrayContaining(mockRows));
+
+ expect(vm.isLoading).toEqual(false);
+ });
+ });
+});
diff --git a/src/components/__tests__/FormModel.spec.js b/src/components/__tests__/FormModel.spec.js
new file mode 100644
index 000000000..e35684bc3
--- /dev/null
+++ b/src/components/__tests__/FormModel.spec.js
@@ -0,0 +1,149 @@
+import { describe, expect, it, beforeAll, vi, afterAll } from 'vitest';
+import { createWrapper, axios } from 'app/test/vitest/helper';
+import FormModel from 'src/components/FormModel.vue';
+
+describe('FormModel', () => {
+ const model = 'mockModel';
+ const url = 'mockUrl';
+ const formInitialData = { mockKey: 'mockVal' };
+
+ describe('modelValue', () => {
+ it('should use the provided model', () => {
+ const { vm } = mount({ propsData: { model } });
+ expect(vm.modelValue).toBe(model);
+ });
+
+ it('should use the route meta title when model is not provided', () => {
+ const { vm } = mount({});
+ expect(vm.modelValue).toBe('formModel_mockTitle');
+ });
+ });
+
+ describe('onMounted()', () => {
+ let mockGet;
+
+ beforeAll(() => {
+ mockGet = vi.spyOn(axios, 'get').mockResolvedValue({ data: {} });
+ });
+
+ afterAll(() => {
+ mockGet.mockRestore();
+ });
+
+ it('should not fetch when has formInitialData', () => {
+ mount({ propsData: { url, model, autoLoad: true, formInitialData } });
+ expect(mockGet).not.toHaveBeenCalled();
+ });
+
+ it('should fetch when there is url and auto-load', () => {
+ mount({ propsData: { url, model, autoLoad: true } });
+ expect(mockGet).toHaveBeenCalled();
+ });
+
+ it('should not observe changes', () => {
+ const { vm } = mount({
+ propsData: { url, model, observeFormChanges: false, formInitialData },
+ });
+
+ expect(vm.hasChanges).toBe(true);
+ vm.reset();
+ expect(vm.hasChanges).toBe(true);
+ });
+
+ it('should observe changes', async () => {
+ const { vm } = mount({
+ propsData: { url, model, formInitialData },
+ });
+ vm.state.set(model, formInitialData);
+ expect(vm.hasChanges).toBe(false);
+
+ vm.formData.mockKey = 'newVal';
+ await vm.$nextTick();
+ expect(vm.hasChanges).toBe(true);
+ vm.formData.mockKey = 'mockVal';
+ });
+ });
+
+ describe('trimData()', () => {
+ let vm;
+ beforeAll(() => {
+ vm = mount({}).vm;
+ });
+
+ it('should trim whitespace from string values', () => {
+ const data = { key1: ' value1 ', key2: ' value2 ' };
+ const trimmedData = vm.trimData(data);
+ expect(trimmedData).toEqual({ key1: 'value1', key2: 'value2' });
+ });
+
+ it('should not modify non-string values', () => {
+ const data = { key1: 123, key2: true, key3: null, key4: undefined };
+ const trimmedData = vm.trimData(data);
+ expect(trimmedData).toEqual(data);
+ });
+ });
+
+ describe('save()', async () => {
+ it('should not call if there are not changes', async () => {
+ const { vm } = mount({ propsData: { url, model } });
+
+ await vm.save();
+ expect(vm.hasChanges).toBe(false);
+ });
+
+ it('should call axios.patch with the right data', async () => {
+ const spy = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
+ const { vm } = mount({ propsData: { url, model, formInitialData } });
+ vm.formData.mockKey = 'newVal';
+ await vm.$nextTick();
+ await vm.save();
+ expect(spy).toHaveBeenCalled();
+ vm.formData.mockKey = 'mockVal';
+ });
+
+ it('should call axios.post with the right data', async () => {
+ const spy = vi.spyOn(axios, 'post').mockResolvedValue({ data: {} });
+ const { vm } = mount({
+ propsData: { url, model, formInitialData, urlCreate: 'mockUrlCreate' },
+ });
+ vm.formData.mockKey = 'newVal';
+ await vm.$nextTick();
+ await vm.save();
+ expect(spy).toHaveBeenCalled();
+ vm.formData.mockKey = 'mockVal';
+ });
+
+ it('should use the saveFn', async () => {
+ const { vm } = mount({
+ propsData: { url, model, formInitialData, saveFn: () => {} },
+ });
+ const spyPatch = vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
+ const spySaveFn = vi.spyOn(vm.$props, 'saveFn');
+
+ vm.formData.mockKey = 'newVal';
+ await vm.$nextTick();
+ await vm.save();
+ expect(spyPatch).not.toHaveBeenCalled();
+ expect(spySaveFn).toHaveBeenCalled();
+ vm.formData.mockKey = 'mockVal';
+ });
+
+ it('should reload the data after save', async () => {
+ const { vm } = mount({
+ propsData: { url, model, formInitialData, reload: true },
+ });
+ vi.spyOn(axios, 'patch').mockResolvedValue({ data: {} });
+
+ vm.formData.mockKey = 'newVal';
+ await vm.$nextTick();
+ await vm.save();
+ vm.formData.mockKey = 'mockVal';
+ });
+ });
+});
+
+function mount({ propsData = {} }) {
+ return createWrapper(FormModel, {
+ propsData,
+ });
+}
diff --git a/test/vitest/__tests__/components/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js
similarity index 100%
rename from test/vitest/__tests__/components/Leftmenu.spec.js
rename to src/components/__tests__/Leftmenu.spec.js
diff --git a/src/components/common/RightMenu.vue b/src/components/common/RightMenu.vue
index 3aa1891f9..32dc2874d 100644
--- a/src/components/common/RightMenu.vue
+++ b/src/components/common/RightMenu.vue
@@ -2,7 +2,11 @@
import { ref, onMounted, useSlots } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
+import { useQuasar } from 'quasar';
+const { t } = useI18n();
+const quasar = useQuasar();
+const stateStore = useStateStore();
const slots = useSlots();
const hasContent = ref(false);
const rightPanel = ref(null);
@@ -11,7 +15,6 @@ onMounted(() => {
rightPanel.value = document.querySelector('#right-panel');
if (!rightPanel.value) return;
- // Check if there's content to display
const observer = new MutationObserver(() => {
hasContent.value = rightPanel.value.childNodes.length;
});
@@ -21,12 +24,9 @@ onMounted(() => {
childList: true,
attributes: true,
});
-
- if (!slots['right-panel'] && !hasContent.value) stateStore.rightDrawer = false;
+ if ((!slots['right-panel'] && !hasContent.value) || quasar.platform.is.mobile)
+ stateStore.rightDrawer = false;
});
-
-const { t } = useI18n();
-const stateStore = useStateStore();
@@ -45,7 +45,7 @@ const stateStore = useStateStore();
-