ref #6104 recreate vnlog #96

Merged
jorgep merged 24 commits from 6104-changeVnLog into dev 2023-12-11 11:34:36 +00:00
19 changed files with 1349 additions and 199 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "23.40.01", "version": "23.36.01",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {

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": "cd ../salix && gulp docker && cd ../salix-front && cypress run --browser chromium", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
jorgep marked this conversation as resolved Outdated
Outdated
Review

como mucho quita lo del browser i que use electron que es el de por defecto, pero gulp si q esta bien que lo haga

como mucho quita lo del browser i que use electron que es el de por defecto, pero gulp si q esta bien que lo haga
"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

@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
// app boot file (/src/boot) // app boot file (/src/boot)
// --> boot files are part of "main.js" // --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files // https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate'], boot: ['i18n', 'axios', 'vnDate', 'validations'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'], css: ['app.scss'],

6
src/boot/validations.js Normal file
View File

@ -0,0 +1,6 @@
import { boot } from 'quasar/wrappers';
import { useValidationsStore } from 'src/stores/useValidationsStore';
export default boot(async ({ store }) => {
await useValidationsStore(store).fetchModels();
});

View File

@ -46,7 +46,7 @@ async function fetch() {
if ($props.limit) filter.limit = $props.limit; if ($props.limit) filter.limit = $props.limit;
const { data } = await axios.get($props.url, { const { data } = await axios.get($props.url, {
params: { filter }, params: { filter: JSON.stringify(filter) },
}); });
emit('onFetch', data); emit('onFetch', data);

View File

@ -0,0 +1,88 @@
<script setup>
import { watch } from 'vue';
import { toDateString } from 'src/filters';
const props = defineProps({
value: { type: [String, Number, Boolean, Object], default: undefined },
});
const maxStrLen = 512;
let t = '';
let cssClass = '';
let type;
const updateValue = () => {
type = typeof props.value;
if (props.value == null) {
t = '∅';
cssClass = 'json-null';
} else {
cssClass = `json-${type}`;
switch (type) {
case 'number':
if (Number.isInteger(props.value)) {
t = props.value.toString();
} else {
t = (
Math.round((props.value + Number.EPSILON) * 1000) / 1000
).toString();
}
break;
case 'boolean':
t = props.value ? '✓' : '✗';
cssClass = `json-${props.value ? 'true' : 'false'}`;
break;
case 'string':
t =
props.value.length <= maxStrLen
? props.value
: props.value.substring(0, maxStrLen) + '...';
break;
case 'object':
if (props.value instanceof Date) {
t = toDateString(props.value);
} else {
t = props.value.toString();
}
break;
default:
t = props.value.toString();
}
}
};
watch(() => props.value, updateValue);
updateValue();
</script>
<template>
<span
:title="type === 'string' && props.value.length > maxStrLen ? props.value : ''"
:class="{ [cssClass]: t !== '' }"
>
{{ t }}
</span>
</template>
<style scoped>
.json-string {
color: #d172cc;
}
.json-object {
color: #d1a572;
}
.json-number {
color: #85d0ff;
}
.json-true {
color: #7dc489;
}
.json-false {
color: #c74949;
}
.json-null {
color: #cd7c7c;
font-style: italic;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
export function djb2a(string) {
let hash = 5381;
for (let i = 0; i < string.length; i++)
hash = ((hash << 5) + hash) ^ string.charCodeAt(i);
return hash >>> 0;
}
export function useColor(value) {
return '#' + colors[djb2a(value || '') % colors.length];
}
const colors = [
'b5b941', // Yellow
'ae9681', // Peach
'd78767', // Salmon
'cc7000', // Orange bright
'e2553d', // Coral
'8B0000', // Red dark
'de4362', // Red crimson
'FF1493', // Ping intense
'be39a2', // Pink light
'b754cf', // Purple middle
'a87ba8', // Pink
'8a69cd', // Blue lavender
'ab20ab', // Purple dark
'00b5b8', // Turquoise
'1fa8a1', // Green ocean
'5681cf', // Blue steel
'3399fe', // Blue sky
'6d9c3e', // Green chartreuse
'51bb51', // Green lime
'518b8b', // Gray board
'7e7e7e', // Gray
'5d5d5d', // Gray dark
];

View File

@ -1,19 +1,12 @@
import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios';
import validator from 'validator'; import validator from 'validator';
import { useValidationsStore } from 'src/stores/useValidationsStore';
const models = ref(null);
export function useValidator() { export function useValidator() {
if (!models.value) fetch(); const models = useValidationsStore().validations;
function fetch() {
axios.get('Schemas/ModelInfo').then((response) => (models.value = response.data));
}
function validate(propertyRule) { function validate(propertyRule) {
const modelInfo = models.value; const modelInfo = models;
if (!modelInfo || !propertyRule) return; if (!modelInfo || !propertyRule) return;
const rule = propertyRule.split('.'); const rule = propertyRule.split('.');
@ -75,5 +68,6 @@ export function useValidator() {
return { return {
validate, validate,
models,
}; };
} }

View File

@ -2,6 +2,7 @@ import toLowerCase from './toLowerCase';
import toDate from './toDate'; import toDate from './toDate';
import toDateString from './toDateString'; import toDateString from './toDateString';
import toDateHour from './toDateHour'; import toDateHour from './toDateHour';
import toRelativeDate from './toRelativeDate';
import toCurrency from './toCurrency'; import toCurrency from './toCurrency';
import toPercentage from './toPercentage'; import toPercentage from './toPercentage';
import toLowerCamel from './toLowerCamel'; import toLowerCamel from './toLowerCamel';
@ -13,6 +14,7 @@ export {
toDate, toDate,
toDateString, toDateString,
toDateHour, toDateHour,
toRelativeDate,
toCurrency, toCurrency,
toPercentage, toPercentage,
dashIfEmpty, dashIfEmpty,

View File

@ -0,0 +1,32 @@
import { useI18n } from 'vue-i18n';
export default function formatDate(dateVal) {
const { t } = useI18n();
const today = new Date();
if (dateVal == null) return '';
const date = new Date(dateVal);
const dateZeroTime = new Date(dateVal);
dateZeroTime.setHours(0, 0, 0, 0);
const diff = Math.trunc(
(today.getTime() - dateZeroTime.getTime()) / (1000 * 3600 * 24)
);
let format;
if (diff === 0) format = t('globals.today');
else if (diff === 1) format = t('globals.yesterday');
else if (diff > 1 && diff < 7) {
const options = { weekday: 'short' };
format = date.toLocaleDateString(t('globals.dateFormat'), options);
} else if (today.getFullYear() === date.getFullYear()) {
const options = { day: 'numeric', month: 'short' };
format = date.toLocaleDateString(t('globals.dateFormat'), options);
} else {
const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
format = date.toLocaleDateString(t('globals.dateFormat'), options);
}
// Formatear la hora en HH:mm
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${format} ${hours}:${minutes}`;
}

View File

@ -5,6 +5,9 @@ export default {
en: 'English', en: 'English',
}, },
language: 'Language', language: 'Language',
entity: 'Entity',
user: 'User',
details: 'Details',
collapseMenu: 'Collapse left menu', collapseMenu: 'Collapse left menu',
backToDashboard: 'Return to dashboard', backToDashboard: 'Return to dashboard',
notifications: 'Notifications', notifications: 'Notifications',
@ -13,8 +16,11 @@ export default {
pinnedModules: 'Pinned modules', pinnedModules: 'Pinned modules',
darkMode: 'Dark mode', darkMode: 'Dark mode',
logOut: 'Log out', logOut: 'Log out',
date: 'Date',
dataSaved: 'Data saved', dataSaved: 'Data saved',
dataDeleted: 'Data deleted', dataDeleted: 'Data deleted',
search: 'Search',
changes: 'Changes',
add: 'Add', add: 'Add',
create: 'Create', create: 'Create',
save: 'Save', save: 'Save',
@ -36,6 +42,9 @@ export default {
summary: { summary: {
basicData: 'Basic data', basicData: 'Basic data',
}, },
today: 'Today',
yesterday: 'Yesterday',
dateFormat: 'en-GB',
microsip: 'Open in MicroSIP', microsip: 'Open in MicroSIP',
noSelectedRows: `You don't have any line selected`, noSelectedRows: `You don't have any line selected`,
}, },

View File

@ -5,6 +5,9 @@ export default {
en: 'Inglés', en: 'Inglés',
}, },
language: 'Idioma', language: 'Idioma',
entity: 'Entidad',
user: 'Usuario',
details: 'Detalles',
collapseMenu: 'Contraer menú lateral', collapseMenu: 'Contraer menú lateral',
backToDashboard: 'Volver al tablón', backToDashboard: 'Volver al tablón',
notifications: 'Notificaciones', notifications: 'Notificaciones',
@ -13,8 +16,11 @@ export default {
pinnedModules: 'Módulos fijados', pinnedModules: 'Módulos fijados',
darkMode: 'Modo oscuro', darkMode: 'Modo oscuro',
logOut: 'Cerrar sesión', logOut: 'Cerrar sesión',
date: 'Fecha',
dataSaved: 'Datos guardados', dataSaved: 'Datos guardados',
dataDeleted: 'Datos eliminados', dataDeleted: 'Datos eliminados',
search: 'Buscar',
changes: 'Cambios',
add: 'Añadir', add: 'Añadir',
create: 'Crear', create: 'Crear',
save: 'Guardar', save: 'Guardar',
@ -36,6 +42,9 @@ export default {
summary: { summary: {
basicData: 'Datos básicos', basicData: 'Datos básicos',
}, },
today: 'Hoy',
yesterday: 'Ayer',
dateFormat: 'es-ES',
noSelectedRows: `No tienes ninguna línea seleccionada`, noSelectedRows: `No tienes ninguna línea seleccionada`,
microsip: 'Abrir en MicroSIP', microsip: 'Abrir en MicroSIP',
}, },

View File

@ -0,0 +1,18 @@
import axios from 'axios';
import { defineStore } from 'pinia';
export const useValidationsStore = defineStore('validationsStore', {
state: () => ({
validations: null,
}),
actions: {
async fetchModels() {
try {
const { data } = await axios.get('Schemas/modelinfo');
this.validations = data;
} catch (error) {
console.error('Error al obtener las validaciones:', error);
}
},
},
});

View File

@ -0,0 +1,25 @@
/// <reference types="cypress" />
jorgep marked this conversation as resolved
Review

Los he probado y fallan

Los he probado y fallan
Review

A mi me funcionan, mirémoslo en mi ordenador a ver @alexm

A mi me funcionan, mirémoslo en mi ordenador a ver @alexm
Review

Lo miramos en mi ordenador y si que funcionaba

Lo miramos en mi ordenador y si que funcionaba
describe('VnLog', () => {
const chips = [
':nth-child(1) > :nth-child(1) > .q-item__label > .q-chip > .q-chip__content',
':nth-child(2) > :nth-child(1) > .q-item__label > .q-chip > .q-chip__content',
];
beforeEach(() => {
cy.login('developer');
cy.visit(`/#/claim/${1}/log`);
cy.openRightMenu('.timeline');
jorgep marked this conversation as resolved Outdated
Outdated
Review

Mira si pots gastar les funcions (commands.js) de cypress o si et fa falta alguno creartelo

Mira si pots gastar les funcions (commands.js) de cypress o si et fa falta alguno creartelo
});
it('should filter by insert actions', () => {
cy.checkOption(':nth-child(7) > .q-checkbox');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Document');
cy.validateContent(chips[1], 'Beginning');
});
it('should filter by entity', () => {
cy.selectOption('.q-drawer--right .q-item > .q-select', 'Claim');
cy.get('.q-page').click();
cy.validateContent(chips[0], 'Claim');
});
});

View File

@ -1,4 +1,6 @@
describe('WorkerList', () => { describe('WorkerList', () => {
const workerFieldNames =
'.card-list-body > .list-items > :nth-child(2) > .value > span';
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1280, 720);
cy.login('developer'); cy.login('developer');
@ -6,20 +8,14 @@ describe('WorkerList', () => {
}); });
jorgep marked this conversation as resolved
Review

Mira si pots simplificar els e2e gastant comandos de cypress test/cypress/support/commands.js
Exemple: test/cypress/integration/claimDevelopment.spec.js

Mira si pots simplificar els e2e gastant comandos de cypress test/cypress/support/commands.js Exemple: test/cypress/integration/claimDevelopment.spec.js
it('should load workers', () => { it('should load workers', () => {
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span') cy.get(workerFieldNames).eq(0).should('have.text', 'JessicaJones');
.eq(0) cy.get(workerFieldNames).eq(1).should('have.text', 'BruceBanner');
.should('have.text', 'JessicaJones'); cy.get(workerFieldNames).eq(2).should('have.text', 'CharlesXavier');
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(1)
.should('have.text', 'BruceBanner');
cy.get('.card-list-body > .list-items > :nth-child(2) > .value > span')
.eq(2)
.should('have.text', 'CharlesXavier');
}); });
it('should open the worker summary', () => { it('should open the worker summary', () => {
cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(1).click(); cy.openListSummary(0);
cy.get('.summaryHeader div').should('have.text', '1109 - Bruce Banner'); cy.get('.summaryHeader div').should('have.text', '1110 - Jessica Jones');
cy.get('.summary .header').eq(0).invoke('text').should('include', 'Basic data'); cy.get('.summary .header').eq(0).invoke('text').should('include', 'Basic data');
cy.get('.summary .header').eq(1).should('have.text', 'User data'); cy.get('.summary .header').eq(1).should('have.text', 'User data');
}); });

View File

@ -51,15 +51,14 @@ Cypress.Commands.add('getValue', (selector) => {
return cy.get(selector + '.q-checkbox__inner'); return cy.get(selector + '.q-checkbox__inner');
} }
// Si es un QSelect // Si es un QSelect
else if ($el.find('.q-select__dropdown-icon').length) { 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 > input' '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
); );
} 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
cy.log('Elemento no soportado'); cy.log('Elemento no soportado');
}
}); });
}); });
@ -70,7 +69,7 @@ Cypress.Commands.add('selectOption', (selector, option) => {
}); });
Cypress.Commands.add('checkOption', (selector) => { Cypress.Commands.add('checkOption', (selector) => {
cy.wrap(selector).find('.q-checkbox__inner').click(); cy.get(selector).find('.q-checkbox__inner').click();
}); });
// Global buttons // Global buttons
@ -131,4 +130,17 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
} }
}); });
}); });
Cypress.Commands.add('openListSummary', (row) => {
cy.get('.card-list-body .actions .q-btn:nth-child(2)').eq(row).click();
});
Cypress.Commands.add('openRightMenu', (element) => {
if (element) cy.waitForElement(element);
cy.get('#actions-append').click();
});
Cypress.Commands.add('validateContent', (selector, expectedValue) => {
cy.get(selector).should('have.text', expectedValue);
});
// registerCommands(); // registerCommands();

View File

@ -1,75 +1,134 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper'; import { createWrapper, axios } from 'app/test/vitest/helper';
import VnLog from 'src/components/common/VnLog.vue'; import VnLog from 'src/components/common/VnLog.vue';
describe('VnLog', () => { describe('VnLog', () => {
let vm; let vm;
beforeAll(() => { const fakeLogTreeData = [
{
id: 2,
originFk: 1,
userFk: 18,
action: 'update',
changedModel: 'ClaimObservation',
oldInstance: {},
newInstance: {
claimFk: 1,
text: 'Waiting for customer',
},
creationDate: '2023-09-18T12:25:34.000Z',
changedModelId: '1',
changedModelValue: null,
description: null,
user: {
id: 18,
name: 'salesPerson',
nickname: 'salesPersonNick',
image: '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd',
worker: {
id: 18,
userFk: 18,
},
},
},
{
id: 1,
originFk: 1,
userFk: 18,
action: 'update',
changedModel: 'Claim',
oldInstance: {
hasToPickUp: false,
},
newInstance: {
hasToPickUp: true,
},
creationDate: '2023-09-18T12:25:34.000Z',
changedModelId: '1',
changedModelValue: null,
description: null,
user: {
id: 18,
name: 'salesPerson',
nickname: 'salesPersonNick',
image: '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd',
worker: {
id: 18,
userFk: 18,
},
},
},
];
const mockValidations = {
Claim: {
locale: {
name: 'reclamación',
},
},
ClaimObservation: {
locale: {
name: 'observación',
},
},
ClaimDms: {
locale: {
name: 'documento',
},
},
ClaimBeginning: {
locale: {
name: 'comienzo',
},
},
};
beforeAll(async () => {
axios.get.mockImplementation(() => {
return { data: fakeLogTreeData };
});
vm = createWrapper(VnLog, { vm = createWrapper(VnLog, {
global: { global: {
stubs: ['FetchData', 'VnPaginate'], stubs: [],
mocks: { mocks: {},
fetch: vi.fn(),
},
}, },
propsData: { propsData: {
model: "Claim", model: 'Claim',
}, },
}).vm; }).vm;
vm.validations = mockValidations;
}); });
afterEach(() => { afterEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
describe('formatValue()', () => { it('should correctly set logTree', async () => {
it('should return Yes if has a true boolean', async () => { vm.logTree = vm.getLogs(fakeLogTreeData);
const result = vm.formatValue(true); expect(vm.logTree[0].originFk).toEqual(1);
expect(vm.logTree[0].logs[0].user.name).toEqual('salesPerson');
expect(result).toEqual('Yes');
});
it('should return No if has a true boolean', async () => {
const result = vm.formatValue(false);
expect(result).toEqual('No');
});
it('should return Nothing if has no params', async () => {
const result = vm.formatValue();
expect(result).toEqual('Nothing');
});
it('should return a string from a string value', async () => {
const result = vm.formatValue('Something');
expect(result).toEqual(`"Something"`);
});
it('should call to format a date', async () => {
vi.mock('src/filters', () => ({
toDate: ()=>{
return "Date formatted"
},
}));
const result = vm.formatValue('01-01-1970');
expect(result).toEqual("Date formatted");
});
}); });
describe('actionColor()', () => { it('should correctly set the selectedFilters when filtering', () => {
it('should return positive if insert', async () => { vm.searchInput = '1';
const result = vm.actionColor('insert'); vm.userSelect = '21';
vm.checkboxOptions.insert.selected = true;
vm.checkboxOptions.update.selected = true;
expect(result).toEqual('positive'); vm.selectFilter('search');
}); vm.selectFilter('userSelect');
it('should return positive if update', async () => {
const result = vm.actionColor('update');
expect(result).toEqual('positive'); expect(vm.selectedFilters.changedModelId).toEqual('1');
expect(vm.selectedFilters.userFk).toEqual('21');
expect(vm.selectedFilters.action).toEqual({ inq: ['insert', 'update'] });
}); });
it('should return negative if delete', async () => {
const result = vm.actionColor('delete');
expect(result).toEqual('negative'); it('should correctly set the date from', () => {
}); vm.dateFrom = '18-09-2023';
vm.selectFilter('date', 'from');
expect(vm.selectedFilters.creationDate.between).toEqual([
new Date('2023-09-18T00:00:00.000Z'),
new Date('2023-09-18T21:59:59.999Z'),
]);
}); });
}); });

View File

@ -13,7 +13,6 @@ installQuasarPlugin({
Dialog, Dialog,
}, },
}); });
const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false }); const pinia = createTestingPinia({ createSpy: vi.fn, stubActions: false });
jorgep marked this conversation as resolved Outdated
Outdated
Review

No crec que en test de front el front dega fer peticions al back.
Deuries poder tirar els test de front sense tindre arrancat res

No crec que en test de front el front dega fer peticions al back. Deuries poder tirar els test de front sense tindre arrancat res
const mockPush = vi.fn(); const mockPush = vi.fn();
@ -35,8 +34,10 @@ vi.mock('vue-router', () => ({
}), }),
})); }));
vi.mock('axios');
vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { vi.spyOn(useValidator, 'useValidator').mockImplementation(() => {
return { validate: vi.fn(), fetch: vi.fn() }; return { validate: vi.fn() };
}); });
class FormDataMock { class FormDataMock {